settings/apps page to view and delete published apps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 18:14:39 +00:00
co-authored by Claude Opus 4.6
parent 74ace56120
commit 0a00172e49
5 changed files with 170 additions and 1 deletions
+1
View File
@@ -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';