Move plugin settings access from sidebar to app card
Remove the "Extensions" section from the sidebar nav. Instead, each app card on the Apps page shows an "Extension" button when the current user has access to that app's plugin (matched by service ID). The button links to /settings/plugins/:id alongside the existing admin Settings button. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -169,7 +169,7 @@ docker compose up --build -d
|
|||||||
│ │ └── useTheme.ts ← Theme toggle
|
│ │ └── useTheme.ts ← Theme toggle
|
||||||
│ ├── components/
|
│ ├── components/
|
||||||
│ │ ├── AppShell.tsx ← Layout: Sidebar + scrollable main
|
│ │ ├── AppShell.tsx ← Layout: Sidebar + scrollable main
|
||||||
│ │ ├── Sidebar.tsx ← Collapsible nav; "Extensions" section auto-populated from /api/plugins
|
│ │ ├── Sidebar.tsx ← Collapsible nav (icons ↔ icons+labels)
|
||||||
│ │ ├── ThemeToggle.tsx ← Light/dark mode toggle
|
│ │ ├── ThemeToggle.tsx ← Light/dark mode toggle
|
||||||
│ │ ├── PluginSchemaForm.tsx ← JSON Schema → React form (boolean/string/number/readOnly)
|
│ │ ├── PluginSchemaForm.tsx ← JSON Schema → React form (boolean/string/number/readOnly)
|
||||||
│ │ └── ui/ ← shadcn/ui components (Button, Input, …)
|
│ │ └── ui/ ← shadcn/ui components (Button, Input, …)
|
||||||
|
|||||||
+5
-5
@@ -61,11 +61,11 @@ Cards are rendered dynamically from `GET /api/services` (polled every 30 s via T
|
|||||||
- Sections auto-open when navigating to their route
|
- Sections auto-open when navigating to their route
|
||||||
- In collapsed (icons-only) mode, clicking the Apps icon navigates to `/apps`
|
- In collapsed (icons-only) mode, clicking the Apps icon navigates to `/apps`
|
||||||
|
|
||||||
**Extensions** section (dynamic):
|
**App cards — Extension button:**
|
||||||
- Populated from `GET /api/plugins` (polled via TanStack Query, `retry: false`)
|
- `GET /api/plugins` is queried on the Apps page (already user-filtered by backend)
|
||||||
- Only shown when the user has access to at least one plugin
|
- If an app's `id` matches a plugin `id`, an "Extension" button is shown on that card
|
||||||
- Each entry links to `/settings/plugins/:id`
|
- Button links to `/settings/plugins/:id` alongside the existing admin "Settings" button
|
||||||
- No code changes needed to add future plugin-enabled feature containers
|
- Only users with plugin access see the button (backend filters `GET /api/plugins`)
|
||||||
|
|
||||||
### Documents page (`/apps/documents`)
|
### Documents page (`/apps/documents`)
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,11 @@ import {
|
|||||||
Users,
|
Users,
|
||||||
UsersRound,
|
UsersRound,
|
||||||
Palette,
|
Palette,
|
||||||
Puzzle,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import ThemeToggle from "@/components/ThemeToggle";
|
import ThemeToggle from "@/components/ThemeToggle";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { getMe, getPlugins, listCategories } from "@/api/client";
|
import { getMe, listCategories } from "@/api/client";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
@@ -57,14 +56,6 @@ export default function Sidebar() {
|
|||||||
enabled: appsOpen && docsOpen && !!user,
|
enabled: appsOpen && docsOpen && !!user,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: plugins = [] } = useQuery({
|
|
||||||
queryKey: ["plugins"],
|
|
||||||
queryFn: getPlugins,
|
|
||||||
enabled: !!user,
|
|
||||||
// Empty array on 404/error — regular users simply see no plugins
|
|
||||||
retry: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const navItemClass = (isActive: boolean) =>
|
const navItemClass = (isActive: boolean) =>
|
||||||
cn(
|
cn(
|
||||||
"flex items-center rounded-lg transition-colors",
|
"flex items-center rounded-lg transition-colors",
|
||||||
@@ -218,40 +209,6 @@ export default function Sidebar() {
|
|||||||
)}
|
)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
{/* Extensions — visible only when the user has accessible plugins */}
|
|
||||||
{plugins.length > 0 && (
|
|
||||||
<div>
|
|
||||||
{sidebarExpanded ? (
|
|
||||||
<>
|
|
||||||
<div className="px-3 py-1.5">
|
|
||||||
<span className="text-xs font-semibold uppercase tracking-wider text-muted">
|
|
||||||
Extensions
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
{plugins.map((plugin) => (
|
|
||||||
<NavLink
|
|
||||||
key={plugin.id}
|
|
||||||
to={`/settings/plugins/${plugin.id}`}
|
|
||||||
className={({ isActive }) => subItemClass(isActive)}
|
|
||||||
>
|
|
||||||
<Puzzle className="h-4 w-4 shrink-0" />
|
|
||||||
<span className="whitespace-nowrap truncate">{plugin.name}</span>
|
|
||||||
</NavLink>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<NavLink
|
|
||||||
to={`/settings/plugins/${plugins[0].id}`}
|
|
||||||
className={({ isActive }) => navItemClass(isActive)}
|
|
||||||
>
|
|
||||||
<Puzzle className="h-5 w-5 shrink-0" />
|
|
||||||
</NavLink>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Admin — expandable */}
|
{/* Admin — expandable */}
|
||||||
{user?.is_admin && (
|
{user?.is_admin && (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { getMe, getServices } from "../api/client";
|
import { getMe, getPlugins, getServices } from "../api/client";
|
||||||
|
|
||||||
const cardBase: React.CSSProperties = {
|
const cardBase: React.CSSProperties = {
|
||||||
backgroundColor: "rgb(var(--color-surface))",
|
backgroundColor: "rgb(var(--color-surface))",
|
||||||
@@ -34,6 +34,12 @@ export default function AppsPage() {
|
|||||||
refetchInterval: 30_000,
|
refetchInterval: 30_000,
|
||||||
refetchIntervalInBackground: true,
|
refetchIntervalInBackground: true,
|
||||||
});
|
});
|
||||||
|
const { data: plugins = [] } = useQuery({
|
||||||
|
queryKey: ["plugins"],
|
||||||
|
queryFn: getPlugins,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
const pluginIds = new Set(plugins.map((p) => p.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 32, maxWidth: 900, margin: "0 auto" }}>
|
<div style={{ padding: 32, maxWidth: 900, margin: "0 auto" }}>
|
||||||
@@ -93,6 +99,23 @@ export default function AppsPage() {
|
|||||||
Settings
|
Settings
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
{pluginIds.has(svc.id) && (
|
||||||
|
<Link
|
||||||
|
to={`/settings/plugins/${svc.id}`}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
padding: "6px 14px",
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
borderRadius: 4,
|
||||||
|
textDecoration: "none",
|
||||||
|
fontSize: 14,
|
||||||
|
color: "#333",
|
||||||
|
}}
|
||||||
|
title="Extension settings"
|
||||||
|
>
|
||||||
|
Extension
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardWrapper>
|
</CardWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user