ec57eff59a
Wire up named dashboards end-to-end. The /d/:slug route (already
referenced by useDashboards-driven nav entries from slice 4) now
renders NamedDashboardPage instead of 404ing.
Backend: GET /api/dashboards/slug/:slug resolves a dashboard by slug
(404 when not found). The store method existed from slice 3; only the
router endpoint was missing.
NamedDashboardPage: renders a named dashboard's payload -- an ordered
list of inline items with a type discriminator. This slice ships
'link' items (PinnedServiceLink -> navigates to /services/:type/:id).
Full widget composition on named dashboards is a follow-up; the main
Dashboard keeps the rich WidgetConfigDialog.
PinnedServiceLink: card component (lucide icon + label) navigating to
a service page or specific tab.
Dashboard management UI on the Services page (DashboardManagementCard):
list existing dashboards with reorder/delete, create via dialog
(label -> auto-slug), add pinned service links per dashboard (label +
enabled-service dropdown). Placed on Services (the admin hub) rather
than Settings to avoid an extra nav trip.
Dashboard payload model: inline items ({ items: [{ type: 'link', label,
target }] }) -- self-contained, no separate widget-instance fetch
needed. The type discriminator allows future widget items without
breaking existing payloads.
Tests: NamedDashboardPage (renders links, not-found state),
PinnedServiceLink (renders + navigates). 112 frontend tests pass (+6);
271 backend tests pass (no regression); lint/build green both sides.
Refs openspec/changes/services-as-hub-ia/ (spec R5, tasks slice 10).
92 lines
2.4 KiB
TypeScript
92 lines
2.4 KiB
TypeScript
import { useMemo } from "react";
|
|
import { useParams } from "react-router-dom";
|
|
import { Boxes } from "lucide-react";
|
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { useDashboardBySlug } from "../hooks/useDashboards";
|
|
import { PinnedServiceLink } from "../components/PinnedServiceLink";
|
|
|
|
/**
|
|
* Payload model for named dashboards (design choice: inline items, not widget
|
|
* instance ids). The payload stores an ordered list of items:
|
|
*
|
|
* ```
|
|
* { items: DashboardItem[] }
|
|
* ```
|
|
*
|
|
* Where `DashboardItem` is either a pinned service link (this slice) or a
|
|
* future widget reference (follow-up). Widget composition on named dashboards
|
|
* is deferred — the main Dashboard already has the rich widget config dialog.
|
|
*/
|
|
interface LinkItem {
|
|
type: "link";
|
|
label: string;
|
|
target: string;
|
|
}
|
|
|
|
type DashboardItem = LinkItem;
|
|
|
|
function parseItems(payload: Record<string, unknown>): DashboardItem[] {
|
|
const items = payload.items;
|
|
if (!Array.isArray(items)) return [];
|
|
return items.filter(
|
|
(item): item is LinkItem =>
|
|
typeof item === "object" &&
|
|
item !== null &&
|
|
item.type === "link" &&
|
|
typeof item.label === "string" &&
|
|
typeof item.target === "string",
|
|
);
|
|
}
|
|
|
|
export function NamedDashboardPage() {
|
|
const { slug = "" } = useParams<{ slug: string }>();
|
|
const { data: dashboard, isLoading, isError } = useDashboardBySlug(slug);
|
|
|
|
const items = useMemo(
|
|
() => parseItems(dashboard?.payload ?? {}),
|
|
[dashboard?.payload],
|
|
);
|
|
|
|
if (isLoading) {
|
|
return <Skeleton className="h-32 w-full" />;
|
|
}
|
|
|
|
if (isError || !dashboard) {
|
|
return (
|
|
<Alert>
|
|
<AlertDescription>
|
|
Dashboard not found. It may have been deleted or the link is invalid.
|
|
</AlertDescription>
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col gap-4">
|
|
<div>
|
|
<h2 className="text-xl font-semibold">{dashboard.label}</h2>
|
|
</div>
|
|
{items.length === 0 ? (
|
|
<Alert>
|
|
<AlertDescription>
|
|
This dashboard has no shortcuts yet. Add pinned service links from
|
|
the dashboard management panel on the Services page.
|
|
</AlertDescription>
|
|
</Alert>
|
|
) : (
|
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
|
|
{items.map((item, index) => (
|
|
<PinnedServiceLink
|
|
key={`${item.target}-${index}`}
|
|
label={item.label}
|
|
target={item.target}
|
|
icon={Boxes}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|