Skip to content

Commit 84747bd

Browse files
committed
feat: add preview panel to model gallery (#366)
1 parent 6f98360 commit 84747bd

6 files changed

Lines changed: 534 additions & 37 deletions

File tree

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Copyright 2024 The casbin Authors. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use client';
16+
import React from 'react';
17+
import { useRouter } from 'next/navigation';
18+
import { clsx } from 'clsx';
19+
import { ExternalLink } from 'lucide-react';
20+
import { useLang } from '@/app/context/LangContext';
21+
import { example } from '@/app/components/editor/casbin-mode/example';
22+
import {
23+
Sheet,
24+
SheetContent,
25+
SheetDescription,
26+
SheetHeader,
27+
SheetTitle,
28+
} from '@/app/components/ui/sheet';
29+
30+
interface ModelPreviewPanelProps {
31+
modelKey: string | null;
32+
modelName: string;
33+
modelDescription: string;
34+
modelCategory: string;
35+
isOpen: boolean;
36+
onClose: () => void;
37+
}
38+
39+
export const ModelPreviewPanel: React.FC<ModelPreviewPanelProps> = ({
40+
modelKey,
41+
modelName,
42+
modelDescription,
43+
modelCategory,
44+
isOpen,
45+
onClose,
46+
}) => {
47+
const { theme, t } = useLang();
48+
const router = useRouter();
49+
50+
const modelData = modelKey ? example[modelKey] : null;
51+
52+
const handleOpenInEditor = () => {
53+
if (modelKey) {
54+
router.push(`/?model=${modelKey}`);
55+
onClose();
56+
}
57+
};
58+
59+
const textClass = clsx(theme === 'dark' ? 'text-gray-200' : 'text-gray-800');
60+
const bgClass = clsx(theme === 'dark' ? 'bg-slate-900' : 'bg-white');
61+
const sectionBgClass = clsx(theme === 'dark' ? 'bg-slate-800' : 'bg-slate-50');
62+
const borderClass = clsx(theme === 'dark' ? 'border-slate-700' : 'border-slate-200');
63+
64+
return (
65+
<Sheet
66+
open={isOpen}
67+
onOpenChange={(open) => {
68+
if (!open) {
69+
onClose();
70+
}
71+
}}
72+
>
73+
<SheetContent side="right" className={clsx('flex flex-col h-full', bgClass, textClass)}>
74+
<SheetHeader className="mb-6 pr-8">
75+
<div className="flex items-start gap-3">
76+
<div className="flex-1 min-w-0">
77+
<SheetTitle className={clsx('text-2xl mb-2', textClass)}>{t(modelName)}</SheetTitle>
78+
<SheetDescription className={clsx('text-base', textClass, 'opacity-70')}>
79+
{t(modelDescription)}
80+
</SheetDescription>
81+
</div>
82+
<span
83+
className={clsx(
84+
'px-3 py-1 text-sm font-medium rounded-lg flex-shrink-0',
85+
'bg-primary/10 text-primary whitespace-nowrap',
86+
)}
87+
>
88+
{t(modelCategory)}
89+
</span>
90+
</div>
91+
</SheetHeader>
92+
93+
{/* Scrollable content area */}
94+
<div className="flex-1 overflow-y-auto pr-2">
95+
{modelData && (
96+
<div className="space-y-6 pb-6">
97+
{/* Model Configuration */}
98+
<section>
99+
<h3 className={clsx('text-lg font-semibold mb-3', textClass)}>
100+
{t('Model Configuration')}
101+
</h3>
102+
<div className={clsx('rounded-lg border p-4', borderClass, sectionBgClass)}>
103+
<pre
104+
className={clsx(
105+
'text-sm whitespace-pre-wrap break-words font-mono',
106+
textClass,
107+
)}
108+
>
109+
{modelData.model}
110+
</pre>
111+
</div>
112+
</section>
113+
114+
{/* Example Policies */}
115+
{modelData.policy && (
116+
<section>
117+
<h3 className={clsx('text-lg font-semibold mb-3', textClass)}>
118+
{t('Example Policies')}
119+
</h3>
120+
<div className={clsx('rounded-lg border p-4', borderClass, sectionBgClass)}>
121+
<pre
122+
className={clsx(
123+
'text-sm whitespace-pre-wrap break-words font-mono',
124+
textClass,
125+
)}
126+
>
127+
{modelData.policy}
128+
</pre>
129+
</div>
130+
</section>
131+
)}
132+
133+
{/* Example Request */}
134+
{modelData.request && (
135+
<section>
136+
<h3 className={clsx('text-lg font-semibold mb-3', textClass)}>
137+
{t('Example Request')}
138+
</h3>
139+
<div className={clsx('rounded-lg border p-4', borderClass, sectionBgClass)}>
140+
<pre
141+
className={clsx(
142+
'text-sm whitespace-pre-wrap break-words font-mono',
143+
textClass,
144+
)}
145+
>
146+
{modelData.request}
147+
</pre>
148+
</div>
149+
</section>
150+
)}
151+
152+
{/* Enforcement Result Section */}
153+
{modelData.policy && modelData.request && (
154+
<section>
155+
<h3 className={clsx('text-lg font-semibold mb-3', textClass)}>
156+
{t('Enforcement Result')}
157+
</h3>
158+
<div className={clsx('rounded-lg border p-4', borderClass, sectionBgClass)}>
159+
<p className={clsx('text-sm', textClass, 'opacity-70')}>
160+
{t('Open in editor to see enforcement results for the example request')}
161+
</p>
162+
</div>
163+
</section>
164+
)}
165+
166+
{/* Custom Configuration */}
167+
{modelData.customConfig && (
168+
<section>
169+
<h3 className={clsx('text-lg font-semibold mb-3', textClass)}>
170+
{t('Custom Configuration')}
171+
</h3>
172+
<div className={clsx('rounded-lg border p-4', borderClass, sectionBgClass)}>
173+
<pre
174+
className={clsx(
175+
'text-sm whitespace-pre-wrap break-words font-mono',
176+
textClass,
177+
)}
178+
>
179+
{modelData.customConfig}
180+
</pre>
181+
</div>
182+
</section>
183+
)}
184+
</div>
185+
)}
186+
</div>
187+
188+
{/* Fixed footer with Open in Editor button */}
189+
<div
190+
className={clsx(
191+
'flex-shrink-0 border-t p-6 -mx-6 -mb-6',
192+
bgClass,
193+
borderClass,
194+
)}
195+
>
196+
<button
197+
onClick={handleOpenInEditor}
198+
className={clsx(
199+
'w-full inline-flex items-center justify-center gap-2 px-6 py-3 rounded-lg',
200+
'text-base font-semibold transition-all duration-200',
201+
'bg-primary text-primary-foreground',
202+
'hover:bg-primary/90 hover:shadow-lg',
203+
'focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',
204+
)}
205+
>
206+
<ExternalLink className="w-5 h-5" />
207+
{t('Open in Editor')}
208+
</button>
209+
</div>
210+
</SheetContent>
211+
</Sheet>
212+
);
213+
};

app/components/ui/sheet.tsx

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
'use client';
2+
3+
import * as React from 'react';
4+
import * as DialogPrimitive from '@radix-ui/react-dialog';
5+
import { X } from 'lucide-react';
6+
7+
import { cn } from '@/app/utils/lib/utils';
8+
9+
const Sheet = DialogPrimitive.Root;
10+
11+
const SheetTrigger = DialogPrimitive.Trigger;
12+
13+
const SheetClose = DialogPrimitive.Close;
14+
15+
const SheetPortal = DialogPrimitive.Portal;
16+
17+
const SheetOverlay = React.forwardRef<
18+
React.ElementRef<typeof DialogPrimitive.Overlay>,
19+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
20+
>(({ className, ...props }, ref) => {
21+
return (
22+
<DialogPrimitive.Overlay
23+
className={cn(
24+
// eslint-disable-next-line max-len
25+
'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',
26+
className,
27+
)}
28+
{...props}
29+
ref={ref}
30+
/>
31+
);
32+
});
33+
SheetOverlay.displayName = DialogPrimitive.Overlay.displayName;
34+
35+
interface SheetContentProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
36+
side?: 'top' | 'bottom' | 'left' | 'right';
37+
}
38+
39+
const SheetContent = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Content>, SheetContentProps>(
40+
({ side = 'right', className, children, ...props }, ref) => {
41+
const sideVariants = {
42+
// eslint-disable-next-line max-len
43+
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
44+
bottom:
45+
// eslint-disable-next-line max-len
46+
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
47+
// eslint-disable-next-line max-len
48+
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',
49+
right:
50+
// eslint-disable-next-line max-len
51+
'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',
52+
};
53+
54+
return (
55+
<SheetPortal>
56+
<SheetOverlay />
57+
<DialogPrimitive.Content
58+
ref={ref}
59+
className={cn(
60+
// eslint-disable-next-line max-len
61+
'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',
62+
sideVariants[side],
63+
className,
64+
)}
65+
{...props}
66+
>
67+
{children}
68+
<DialogPrimitive.Close
69+
// eslint-disable-next-line max-len
70+
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"
71+
>
72+
<X className="h-4 w-4" />
73+
<span className="sr-only">Close</span>
74+
</DialogPrimitive.Close>
75+
</DialogPrimitive.Content>
76+
</SheetPortal>
77+
);
78+
},
79+
);
80+
SheetContent.displayName = DialogPrimitive.Content.displayName;
81+
82+
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => {
83+
return <div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />;
84+
};
85+
SheetHeader.displayName = 'SheetHeader';
86+
87+
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => {
88+
return <div className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...props} />;
89+
};
90+
SheetFooter.displayName = 'SheetFooter';
91+
92+
const SheetTitle = React.forwardRef<
93+
React.ElementRef<typeof DialogPrimitive.Title>,
94+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
95+
>(({ className, ...props }, ref) => {
96+
return (
97+
<DialogPrimitive.Title
98+
ref={ref}
99+
className={cn('text-lg font-semibold text-foreground', className)}
100+
{...props}
101+
/>
102+
);
103+
});
104+
SheetTitle.displayName = DialogPrimitive.Title.displayName;
105+
106+
const SheetDescription = React.forwardRef<
107+
React.ElementRef<typeof DialogPrimitive.Description>,
108+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
109+
>(({ className, ...props }, ref) => {
110+
return <DialogPrimitive.Description ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />;
111+
});
112+
SheetDescription.displayName = DialogPrimitive.Description.displayName;
113+
114+
export { Sheet, SheetPortal, SheetOverlay, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription };

0 commit comments

Comments
 (0)