Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 213 additions & 0 deletions app/components/gallery/ModelPreviewPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// Copyright 2024 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

'use client';
import React from 'react';
import { useRouter } from 'next/navigation';
import { clsx } from 'clsx';
import { ExternalLink } from 'lucide-react';
import { useLang } from '@/app/context/LangContext';
import { example } from '@/app/components/editor/casbin-mode/example';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from '@/app/components/ui/sheet';

interface ModelPreviewPanelProps {
modelKey: string | null;
modelName: string;
modelDescription: string;
modelCategory: string;
isOpen: boolean;
onClose: () => void;
}

export const ModelPreviewPanel: React.FC<ModelPreviewPanelProps> = ({
modelKey,
modelName,
modelDescription,
modelCategory,
isOpen,
onClose,
}) => {
const { theme, t } = useLang();
const router = useRouter();

const modelData = modelKey ? example[modelKey] : null;

const handleOpenInEditor = () => {
if (modelKey) {
router.push(`/?model=${modelKey}`);
onClose();
}
};

const textClass = clsx(theme === 'dark' ? 'text-gray-200' : 'text-gray-800');
const bgClass = clsx(theme === 'dark' ? 'bg-slate-900' : 'bg-white');
const sectionBgClass = clsx(theme === 'dark' ? 'bg-slate-800' : 'bg-slate-50');
const borderClass = clsx(theme === 'dark' ? 'border-slate-700' : 'border-slate-200');

return (
<Sheet
open={isOpen}
onOpenChange={(open) => {
if (!open) {
onClose();
}
}}
>
<SheetContent side="right" className={clsx('flex flex-col h-full', bgClass, textClass)}>
<SheetHeader className="mb-6 pr-8">
<div className="flex items-start gap-3">
<div className="flex-1 min-w-0">
<SheetTitle className={clsx('text-2xl mb-2', textClass)}>{t(modelName)}</SheetTitle>
<SheetDescription className={clsx('text-base', textClass, 'opacity-70')}>
{t(modelDescription)}
</SheetDescription>
</div>
<span
className={clsx(
'px-3 py-1 text-sm font-medium rounded-lg flex-shrink-0',
'bg-primary/10 text-primary whitespace-nowrap',
)}
>
{t(modelCategory)}
</span>
</div>
</SheetHeader>

{/* Scrollable content area */}
<div className="flex-1 overflow-y-auto pr-2">
{modelData && (
<div className="space-y-6 pb-6">
{/* Model Configuration */}
<section>
<h3 className={clsx('text-lg font-semibold mb-3', textClass)}>
{t('Model Configuration')}
</h3>
<div className={clsx('rounded-lg border p-4', borderClass, sectionBgClass)}>
<pre
className={clsx(
'text-sm whitespace-pre-wrap break-words font-mono',
textClass,
)}
>
{modelData.model}
</pre>
</div>
</section>

{/* Example Policies */}
{modelData.policy && (
<section>
<h3 className={clsx('text-lg font-semibold mb-3', textClass)}>
{t('Example Policies')}
</h3>
<div className={clsx('rounded-lg border p-4', borderClass, sectionBgClass)}>
<pre
className={clsx(
'text-sm whitespace-pre-wrap break-words font-mono',
textClass,
)}
>
{modelData.policy}
</pre>
</div>
</section>
)}

{/* Example Request */}
{modelData.request && (
<section>
<h3 className={clsx('text-lg font-semibold mb-3', textClass)}>
{t('Example Request')}
</h3>
<div className={clsx('rounded-lg border p-4', borderClass, sectionBgClass)}>
<pre
className={clsx(
'text-sm whitespace-pre-wrap break-words font-mono',
textClass,
)}
>
{modelData.request}
</pre>
</div>
</section>
)}

{/* Enforcement Result Section */}
{modelData.policy && modelData.request && (
<section>
<h3 className={clsx('text-lg font-semibold mb-3', textClass)}>
{t('Enforcement Result')}
</h3>
<div className={clsx('rounded-lg border p-4', borderClass, sectionBgClass)}>
<p className={clsx('text-sm', textClass, 'opacity-70')}>
{t('Open in editor to see enforcement results for the example request')}
</p>
</div>
</section>
)}

{/* Custom Configuration */}
{modelData.customConfig && (
<section>
<h3 className={clsx('text-lg font-semibold mb-3', textClass)}>
{t('Custom Configuration')}
</h3>
<div className={clsx('rounded-lg border p-4', borderClass, sectionBgClass)}>
<pre
className={clsx(
'text-sm whitespace-pre-wrap break-words font-mono',
textClass,
)}
>
{modelData.customConfig}
</pre>
</div>
</section>
)}
</div>
)}
</div>

{/* Fixed footer with Open in Editor button */}
<div
className={clsx(
'flex-shrink-0 border-t p-6 -mx-6 -mb-6',
bgClass,
borderClass,
)}
>
<button
onClick={handleOpenInEditor}
className={clsx(
'w-full inline-flex items-center justify-center gap-2 px-6 py-3 rounded-lg',
'text-base font-semibold transition-all duration-200',
'bg-primary text-primary-foreground',
'hover:bg-primary/90 hover:shadow-lg',
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
)}
>
<ExternalLink className="w-5 h-5" />
{t('Open in Editor')}
</button>
</div>
</SheetContent>
</Sheet>
);
};
114 changes: 114 additions & 0 deletions app/components/ui/sheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
'use client';

import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';

import { cn } from '@/app/utils/lib/utils';

const Sheet = DialogPrimitive.Root;

const SheetTrigger = DialogPrimitive.Trigger;

const SheetClose = DialogPrimitive.Close;

const SheetPortal = DialogPrimitive.Portal;

const SheetOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => {
return (
<DialogPrimitive.Overlay
className={cn(
// eslint-disable-next-line max-len
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className,
)}
{...props}
ref={ref}
/>
);
});
SheetOverlay.displayName = DialogPrimitive.Overlay.displayName;

interface SheetContentProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
side?: 'top' | 'bottom' | 'left' | 'right';
}

const SheetContent = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Content>, SheetContentProps>(
({ side = 'right', className, children, ...props }, ref) => {
const sideVariants = {
// eslint-disable-next-line max-len
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
bottom:
// eslint-disable-next-line max-len
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
// eslint-disable-next-line max-len
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
right:
// eslint-disable-next-line max-len
'inset-y-0 right-0 h-full w-full sm:w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-xl',
};

return (
<SheetPortal>
<SheetOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
// eslint-disable-next-line max-len
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
sideVariants[side],
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close
// eslint-disable-next-line max-len
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary"
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</SheetPortal>
);
},
);
SheetContent.displayName = DialogPrimitive.Content.displayName;

const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => {
return <div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />;
};
SheetHeader.displayName = 'SheetHeader';

const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => {
return <div className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...props} />;
};
SheetFooter.displayName = 'SheetFooter';

const SheetTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => {
return (
<DialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold text-foreground', className)}
{...props}
/>
);
});
SheetTitle.displayName = DialogPrimitive.Title.displayName;

const SheetDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => {
return <DialogPrimitive.Description ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />;
});
SheetDescription.displayName = DialogPrimitive.Description.displayName;

export { Sheet, SheetPortal, SheetOverlay, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription };
Loading