mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 10:15:40 +00:00
feat(ui): enhance settings dialog with tabbed interface and scrollable areas
- Add Radix UI scroll-area and tabs components - Redesign settings dialog with tabbed layout for better organization - Add Card components for structured content display - Update accounts and profiles tables UI - Add scrollable areas for better content management - Improve overall dialog loading and styling
This commit is contained in:
1 parent
1f215d8ecb
commit
4adb94b90c
9 files changed
+224
-67
No files matched your search
@@ -10,8 +10,10 @@
|
|||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
@@ -184,6 +186,8 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
||||||
|
|
||||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||||
|
|
||||||
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
|
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
|
||||||
@@ -226,10 +230,14 @@
|
|||||||
|
|
||||||
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
|
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
|
||||||
|
|
||||||
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
|
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
|
||||||
|
|
||||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
|
||||||
|
|
||||||
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
|
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
|
||||||
|
|
||||||
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
||||||
|
|||||||
@@ -21,8 +21,10 @@
|
|||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
|||||||
@@ -29,11 +29,12 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) {
|
|||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: 'Name',
|
header: 'Name',
|
||||||
|
size: 200,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{row.original.name}
|
<span className="font-medium">{row.original.name}</span>
|
||||||
{row.original.name === defaultAccount && (
|
{row.original.name === defaultAccount && (
|
||||||
<span className="text-xs bg-primary text-primary-foreground px-1.5 py-0.5 rounded">
|
<span className="text-xs bg-primary/10 text-primary px-1.5 py-0.5 rounded border border-primary/20">
|
||||||
default
|
default
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -43,39 +44,46 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) {
|
|||||||
{
|
{
|
||||||
accessorKey: 'type',
|
accessorKey: 'type',
|
||||||
header: 'Type',
|
header: 'Type',
|
||||||
cell: ({ row }) => row.original.type || 'oauth',
|
size: 100,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="capitalize text-muted-foreground">{row.original.type || 'oauth'}</span>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'created',
|
accessorKey: 'created',
|
||||||
header: 'Created',
|
header: 'Created',
|
||||||
|
size: 150,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const date = new Date(row.original.created);
|
const date = new Date(row.original.created);
|
||||||
return date.toLocaleDateString();
|
return <span className="text-muted-foreground">{date.toLocaleDateString()}</span>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'last_used',
|
accessorKey: 'last_used',
|
||||||
header: 'Last Used',
|
header: 'Last Used',
|
||||||
|
size: 150,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
if (!row.original.last_used) return '-';
|
if (!row.original.last_used) return <span className="text-muted-foreground/50">-</span>;
|
||||||
const date = new Date(row.original.last_used);
|
const date = new Date(row.original.last_used);
|
||||||
return date.toLocaleDateString();
|
return <span className="text-muted-foreground">{date.toLocaleDateString()}</span>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: 'Actions',
|
header: 'Actions',
|
||||||
|
size: 100,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const isDefault = row.original.name === defaultAccount;
|
const isDefault = row.original.name === defaultAccount;
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant={isDefault ? 'outline' : 'default'}
|
variant={isDefault ? 'secondary' : 'default'}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
className="h-8 px-2 w-full"
|
||||||
disabled={isDefault || setDefaultMutation.isPending}
|
disabled={isDefault || setDefaultMutation.isPending}
|
||||||
onClick={() => setDefaultMutation.mutate(row.original.name)}
|
onClick={() => setDefaultMutation.mutate(row.original.name)}
|
||||||
>
|
>
|
||||||
<Check className="w-4 h-4 mr-1" />
|
<Check className={`w-3 h-3 mr-1.5 ${isDefault ? 'opacity-50' : ''}`} />
|
||||||
{isDefault ? 'Default' : 'Set Default'}
|
{isDefault ? 'Active' : 'Set Default'}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -111,7 +119,7 @@ export function AccountsTable({ data, defaultAccount }: AccountsTableProps) {
|
|||||||
type: 'w-[100px]',
|
type: 'w-[100px]',
|
||||||
created: 'w-[150px]',
|
created: 'w-[150px]',
|
||||||
last_used: 'w-[150px]',
|
last_used: 'w-[150px]',
|
||||||
actions: 'w-[120px]',
|
actions: 'w-[100px]',
|
||||||
}[header.id] || 'w-auto';
|
}[header.id] || 'w-auto';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -20,17 +20,16 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { MoreHorizontal, Trash2, Edit, Settings } from 'lucide-react';
|
import { MoreHorizontal, Trash2, Edit } from 'lucide-react';
|
||||||
import { useDeleteProfile } from '@/hooks/use-profiles';
|
import { useDeleteProfile } from '@/hooks/use-profiles';
|
||||||
import type { Profile } from '@/lib/api-client';
|
import type { Profile } from '@/lib/api-client';
|
||||||
|
|
||||||
interface ProfilesTableProps {
|
interface ProfilesTableProps {
|
||||||
data: Profile[];
|
data: Profile[];
|
||||||
onEdit?: (profile: Profile) => void;
|
|
||||||
onEditSettings?: (profile: Profile) => void;
|
onEditSettings?: (profile: Profile) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProfilesTable({ data, onEdit, onEditSettings }: ProfilesTableProps) {
|
export function ProfilesTable({ data, onEditSettings }: ProfilesTableProps) {
|
||||||
const deleteMutation = useDeleteProfile();
|
const deleteMutation = useDeleteProfile();
|
||||||
|
|
||||||
const columns: ColumnDef<Profile>[] = [
|
const columns: ColumnDef<Profile>[] = [
|
||||||
@@ -64,22 +63,16 @@ export function ProfilesTable({ data, onEdit, onEditSettings }: ProfilesTablePro
|
|||||||
<MoreHorizontal className="w-4 h-4" />
|
<MoreHorizontal className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end" className="bg-white dark:bg-zinc-950 border shadow-md">
|
||||||
{onEdit && (
|
|
||||||
<DropdownMenuItem onClick={() => onEdit(row.original)}>
|
|
||||||
<Edit className="w-4 h-4 mr-2" />
|
|
||||||
Edit Profile
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
{onEditSettings && (
|
{onEditSettings && (
|
||||||
<DropdownMenuItem onClick={() => onEditSettings(row.original)}>
|
<DropdownMenuItem onClick={() => onEditSettings(row.original)}>
|
||||||
<Settings className="w-4 h-4 mr-2" />
|
<Edit className="w-4 h-4 mr-2" />
|
||||||
Edit Settings
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-red-600"
|
className="text-red-600 focus:text-red-600 focus:bg-red-100/50"
|
||||||
onClick={() => deleteMutation.mutate(row.original.name)}
|
onClick={() => deleteMutation.mutate(row.original.name)}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4 mr-2" />
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ interface SettingsDialogProps {
|
|||||||
* Inner component that manages local edits state
|
* Inner component that manages local edits state
|
||||||
* Gets unmounted/remounted via key prop when dialog closes/opens
|
* Gets unmounted/remounted via key prop when dialog closes/opens
|
||||||
*/
|
*/
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
|
||||||
function SettingsDialogContent({
|
function SettingsDialogContent({
|
||||||
profileName,
|
profileName,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -147,42 +151,94 @@ function SettingsDialogContent({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Environment Settings: {profileName}</DialogTitle>
|
<DialogTitle>Edit Profile: {profileName}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Edit environment variables for this profile. Sensitive keys are masked.
|
Configure environment variables and settings for this profile.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-12">
|
||||||
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
|
||||||
<span className="ml-2 text-muted-foreground">Loading settings...</span>
|
<span className="ml-3 text-muted-foreground">Loading settings...</span>
|
||||||
</div>
|
</div>
|
||||||
) : currentSettings?.env && Object.keys(currentSettings.env).length > 0 ? (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col h-[60vh]">
|
||||||
{Object.entries(currentSettings.env).map(([key, value]) => (
|
<Tabs defaultValue="env" className="flex-1 flex flex-col overflow-hidden">
|
||||||
<div key={key}>
|
<TabsList className="w-full justify-start border-b rounded-none p-0 h-auto bg-transparent">
|
||||||
<Label>{key}</Label>
|
<TabsTrigger
|
||||||
{isSensitiveKey(key) ? (
|
value="env"
|
||||||
<MaskedInput value={value} onChange={(e) => updateEnvValue(key, e.target.value)} />
|
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2"
|
||||||
) : (
|
>
|
||||||
<Input
|
Environment
|
||||||
value={value}
|
</TabsTrigger>
|
||||||
onChange={(e) => updateEnvValue(key, e.target.value)}
|
<TabsTrigger
|
||||||
className="font-mono"
|
value="general"
|
||||||
/>
|
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-2"
|
||||||
)}
|
>
|
||||||
</div>
|
General
|
||||||
))}
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
{data && (
|
<TabsContent value="env" className="flex-1 overflow-hidden p-4 pt-4 m-0">
|
||||||
<div className="pt-4 text-xs text-muted-foreground border-t">
|
<ScrollArea className="h-full pr-4">
|
||||||
<p>Path: {data.path}</p>
|
{currentSettings?.env && Object.keys(currentSettings.env).length > 0 ? (
|
||||||
<p>Last modified: {new Date(data.mtime).toLocaleString()}</p>
|
<div className="space-y-6">
|
||||||
</div>
|
{Object.entries(currentSettings.env).map(([key, value]) => (
|
||||||
)}
|
<div key={key} className="space-y-2">
|
||||||
|
<Label className="text-sm font-medium text-foreground">{key}</Label>
|
||||||
|
{isSensitiveKey(key) ? (
|
||||||
|
<MaskedInput
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => updateEnvValue(key, e.target.value)}
|
||||||
|
className="font-mono text-sm"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => updateEnvValue(key, e.target.value)}
|
||||||
|
className="font-mono text-sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="py-12 text-center text-muted-foreground bg-muted/30 rounded-lg border border-dashed">
|
||||||
|
<p>No environment variables configured.</p>
|
||||||
|
<p className="text-xs mt-1">Add variables in your settings.json file.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-4">
|
<TabsContent value="general" className="p-4 m-0">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Profile Information</CardTitle>
|
||||||
|
<CardDescription>Details about this configuration file.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{data && (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-3 gap-2 text-sm">
|
||||||
|
<span className="font-medium text-muted-foreground">Path</span>
|
||||||
|
<code className="col-span-2 bg-muted p-1 rounded text-xs break-all">
|
||||||
|
{data.path}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-2 text-sm">
|
||||||
|
<span className="font-medium text-muted-foreground">Last Modified</span>
|
||||||
|
<span className="col-span-2">{new Date(data.mtime).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<div className="pt-4 mt-auto border-t flex justify-end gap-2">
|
||||||
<Button type="button" variant="outline" onClick={onClose}>
|
<Button type="button" variant="outline" onClick={onClose}>
|
||||||
<X className="w-4 h-4 mr-2" /> Cancel
|
<X className="w-4 h-4 mr-2" /> Cancel
|
||||||
</Button>
|
</Button>
|
||||||
@@ -193,16 +249,12 @@ function SettingsDialogContent({
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Save className="w-4 h-4 mr-2" /> Save
|
<Save className="w-4 h-4 mr-2" /> Save Changes
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="py-8 text-center text-muted-foreground">
|
|
||||||
No environment variables configured for this profile.
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
@@ -231,7 +283,7 @@ export function SettingsDialog({ open, onClose, profileName }: SettingsDialogPro
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
<DialogContent className="sm:max-w-3xl max-h-[80vh] overflow-y-auto">
|
||||||
{/* Key prop ensures fresh state on each open */}
|
{/* Key prop ensures fresh state on each open */}
|
||||||
{open && profileName && (
|
{open && profileName && (
|
||||||
<SettingsDialogContent
|
<SettingsDialogContent
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const ScrollArea = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof ScrollAreaPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn('relative overflow-hidden', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
));
|
||||||
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const ScrollBar = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof ScrollAreaPrimitive.Scrollbar>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Scrollbar>
|
||||||
|
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.Scrollbar
|
||||||
|
ref={ref}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
'flex touch-none select-none transition-colors',
|
||||||
|
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-[1px]',
|
||||||
|
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-[1px]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Thumb className="relative flex-1 rounded-full bg-border" />
|
||||||
|
</ScrollAreaPrimitive.Scrollbar>
|
||||||
|
));
|
||||||
|
ScrollBar.displayName = ScrollAreaPrimitive.Scrollbar.displayName;
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar };
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root;
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
@@ -27,6 +27,9 @@
|
|||||||
--ring: oklch(0.2 0.02 40); /* Match primary */
|
--ring: oklch(0.2 0.02 40); /* Match primary */
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
|
|
||||||
|
--popover: oklch(0.9635 0.0067 97.35); /* Match background */
|
||||||
|
--popover-foreground: oklch(0.2 0.02 40); /* Match foreground */
|
||||||
|
|
||||||
/* Sidebar colors - Light */
|
/* Sidebar colors - Light */
|
||||||
--sidebar: oklch(0.9635 0.0067 97.35); /* Pampas */
|
--sidebar: oklch(0.9635 0.0067 97.35); /* Pampas */
|
||||||
--sidebar-foreground: oklch(0.2 0.02 40);
|
--sidebar-foreground: oklch(0.2 0.02 40);
|
||||||
@@ -63,6 +66,9 @@
|
|||||||
|
|
||||||
--ring: oklch(0.9635 0.0067 97.35);
|
--ring: oklch(0.9635 0.0067 97.35);
|
||||||
|
|
||||||
|
--popover: oklch(0.21 0.006 100); /* Match dark bg */
|
||||||
|
--popover-foreground: oklch(0.9635 0.0067 97.35); /* Match dark fg */
|
||||||
|
|
||||||
/* Sidebar Dark Theme */
|
/* Sidebar Dark Theme */
|
||||||
--sidebar: oklch(0.21 0.006 100); /* Match bg */
|
--sidebar: oklch(0.21 0.006 100); /* Match bg */
|
||||||
--sidebar-foreground: oklch(0.9635 0.0067 97.35);
|
--sidebar-foreground: oklch(0.9635 0.0067 97.35);
|
||||||
|
|||||||
+1
-10
@@ -19,11 +19,6 @@ export function ApiPage() {
|
|||||||
const [settingsProfileName, setSettingsProfileName] = useState<string | null>(null);
|
const [settingsProfileName, setSettingsProfileName] = useState<string | null>(null);
|
||||||
const { data, isLoading } = useProfiles();
|
const { data, isLoading } = useProfiles();
|
||||||
|
|
||||||
const handleEdit = (profile: Profile) => {
|
|
||||||
setEditingProfile(profile);
|
|
||||||
setDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditSettings = (profile: Profile) => {
|
const handleEditSettings = (profile: Profile) => {
|
||||||
setSettingsProfileName(profile.name);
|
setSettingsProfileName(profile.name);
|
||||||
setSettingsDialogOpen(true);
|
setSettingsDialogOpen(true);
|
||||||
@@ -57,11 +52,7 @@ export function ApiPage() {
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="text-muted-foreground">Loading profiles...</div>
|
<div className="text-muted-foreground">Loading profiles...</div>
|
||||||
) : (
|
) : (
|
||||||
<ProfilesTable
|
<ProfilesTable data={data?.profiles || []} onEditSettings={handleEditSettings} />
|
||||||
data={data?.profiles || []}
|
|
||||||
onEdit={handleEdit}
|
|
||||||
onEditSettings={handleEditSettings}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ProfileDialog open={dialogOpen} onClose={handleCloseDialog} profile={editingProfile} />
|
<ProfileDialog open={dialogOpen} onClose={handleCloseDialog} profile={editingProfile} />
|
||||||
|
|||||||
Reference in new issue
Block a user