settings/apps page to view and delete published apps
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,7 @@ export function App() {
|
||||
<Route path="/settings/resources" element={user?.role !== 'Member' ? <Dashboard.ResourceSettings /> : <Navigate to="/" replace />} />
|
||||
<Route path="/settings/users" element={user?.role === 'Super Admin' ? <Dashboard.UserSettings /> : <Navigate to="/" replace />} />
|
||||
<Route path="/settings/integrations" element={<Dashboard.IntegrationsSettings />} />
|
||||
<Route path="/settings/apps" element={<Dashboard.AppsSettings />} />
|
||||
<Route path="/automation" element={<Dashboard.Automation />} />
|
||||
<Route path="/chat" element={<Dashboard.SessionListPage />} />
|
||||
<Route path="/chat/new" element={<Dashboard.SessionListPage isNew />} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Link } from 'react-router';
|
||||
import * as Dropdown from '@/components/ui/dropdown-menu';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { User, Users, LogOut, Settings, Package, Puzzle, Sun, Moon } from 'lucide-react';
|
||||
import { User, Users, LogOut, Settings, Package, Puzzle, Rocket, Sun, Moon } from 'lucide-react';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import { useTranslation } from '@/lib/i18n';
|
||||
import { useColorMode } from '@/components/ui/ThemeProvider';
|
||||
@@ -65,6 +65,12 @@ export function UserMenu() {
|
||||
Integrations
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild className="cursor-pointer">
|
||||
<Link to="/settings/apps">
|
||||
<Rocket className="mr-2 h-4 w-4" />
|
||||
Apps
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{user?.role === 'Super Admin' && (
|
||||
<DropdownMenuItem asChild className="cursor-pointer">
|
||||
<Link to="/settings/users">
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { useState } from 'react';
|
||||
import { Trash2, Loader2, ExternalLink } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useUserApps, type AppManifest } from 'state/useUserApps';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
|
||||
export const AppsList = () => {
|
||||
const client = useClient();
|
||||
const { apps, isLoading, refetch } = useUserApps();
|
||||
const [deleting, setDeleting] = useState<AppManifest | null>(null);
|
||||
const [deletingInProgress, setDeletingInProgress] = useState(false);
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (!deleting) return;
|
||||
setDeletingInProgress(true);
|
||||
try {
|
||||
await client.delete(`/apps/${deleting.slug}`);
|
||||
toast.success(`Deleted ${deleting.name}`);
|
||||
refetch();
|
||||
} catch {
|
||||
toast.error('Failed to delete app');
|
||||
} finally {
|
||||
setDeletingInProgress(false);
|
||||
setDeleting(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-duck-dark/30 dark:text-foreground/30" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (apps.length === 0) {
|
||||
return (
|
||||
<div className="text-sm text-duck-dark/40 dark:text-foreground/40 py-8 text-center">
|
||||
No published apps yet. Publish a project from the Projects page.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
{apps.map((app) => (
|
||||
<div
|
||||
key={app.slug}
|
||||
className="flex items-center gap-4 rounded-lg border border-duck-dark/10 dark:border-foreground/10 p-4"
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-duck-dark dark:text-foreground">{app.name}</span>
|
||||
<span className="text-[10px] font-mono text-duck-dark/30 dark:text-foreground/30">v{app.version}</span>
|
||||
</div>
|
||||
{app.description && (
|
||||
<p className="text-xs text-duck-dark/50 dark:text-foreground/50 mt-0.5 truncate">{app.description}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-3 mt-1.5 text-[11px] text-duck-dark/30 dark:text-foreground/30">
|
||||
<span>Source: <span className="font-mono">{app.sourceProject}</span></span>
|
||||
<span>Commit: <span className="font-mono">{app.commitHash}</span></span>
|
||||
<span>Published: {new Date(app.publishedAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href={`/api/app-serve/${app.slug}/`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="shrink-0 p-2 rounded-md text-duck-dark/30 dark:text-foreground/30 hover:text-duck-teal hover:bg-duck-teal/10 transition-colors cursor-pointer"
|
||||
title="Open in new tab"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDeleting(app)}
|
||||
className="shrink-0 p-2 rounded-md text-duck-dark/30 dark:text-foreground/30 hover:text-red-500 hover:bg-red-500/10 transition-colors cursor-pointer"
|
||||
title="Delete app"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<AlertDialog open={deleting !== null} onOpenChange={(open) => { if (!open) setDeleting(null); }}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete published app</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete <strong>{deleting?.name}</strong>? This removes the published build. The source project is not affected.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={deletingInProgress}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={confirmDelete} disabled={deletingInProgress} className="bg-red-600 hover:bg-red-700">
|
||||
{deletingInProgress ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Delete'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Rocket, List } from 'lucide-react';
|
||||
import type { LayoutNode, PanelComponents } from 'officerdev';
|
||||
import { WorkspaceLayout } from 'officerdev';
|
||||
|
||||
import { createSettingsPanelComponents, type SettingsSection } from '../SettingsPanel';
|
||||
import { AppsList } from './AppsList';
|
||||
|
||||
const GLOBAL_KEY = 'APPS_SETTINGS_SELECTED';
|
||||
|
||||
const sections: SettingsSection[] = [
|
||||
{ key: 'published-apps', icon: List, title: 'Published Apps', description: 'View and manage your published apps', content: <AppsList /> },
|
||||
];
|
||||
|
||||
const { Sidebar, Content } = createSettingsPanelComponents({
|
||||
globalKey: GLOBAL_KEY,
|
||||
sidebarIcon: Rocket,
|
||||
sidebarLabel: 'Apps',
|
||||
sections,
|
||||
});
|
||||
|
||||
const layout: LayoutNode = {
|
||||
type: 'group',
|
||||
id: 'apps-settings-root',
|
||||
direction: 'horizontal',
|
||||
children: [
|
||||
{ node: { type: 'panel', id: 'apps-settings-left', appType: null }, size: 20 },
|
||||
{ node: { type: 'panel', id: 'apps-settings-right', appType: null }, size: 80 },
|
||||
],
|
||||
};
|
||||
|
||||
export const AppsSettings = () => {
|
||||
const panelComponents: PanelComponents = useMemo(
|
||||
() => ({
|
||||
'apps-settings-left': Sidebar,
|
||||
'apps-settings-right': Content,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full pt-2">
|
||||
<WorkspaceLayout layout={layout} onLayoutChange={() => {}} components={panelComponents} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -3,3 +3,4 @@ export * from './SystemSettings';
|
||||
export * from './ResourceSettings';
|
||||
export * from './UserSettings';
|
||||
export * from './IntegrationsSettings';
|
||||
export * from './AppsSettings';
|
||||
|
||||
Reference in New Issue
Block a user