feat: strip SaaS UI to vendor management dashboard
- Delete EnvironmentsPage, EnvironmentDetailPage, AppDetailPage - Delete EnvironmentTree and DeploymentStatusBadge components - Simplify DashboardPage to show tenant info, license status, server link - Remove environment/app/deployment routes from router - Remove environment section from sidebar, keep dashboard/license/platform - Strip API hooks to tenant/license/me only - Remove environment/app/deployment/observability types Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +0,0 @@
|
||||
import { Badge } from '@cameleer/design-system';
|
||||
|
||||
// Badge color values: 'primary' | 'success' | 'warning' | 'error' | 'running' | 'auto'
|
||||
const STATUS_COLORS: Record<string, 'primary' | 'success' | 'warning' | 'error' | 'running' | 'auto'> = {
|
||||
BUILDING: 'warning',
|
||||
STARTING: 'warning',
|
||||
RUNNING: 'running',
|
||||
FAILED: 'error',
|
||||
STOPPED: 'auto',
|
||||
};
|
||||
|
||||
export function DeploymentStatusBadge({ status }: { status: string }) {
|
||||
const color = STATUS_COLORS[status] ?? 'auto';
|
||||
return <Badge label={status} color={color} />;
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router';
|
||||
import { SidebarTree, type SidebarTreeNode } from '@cameleer/design-system';
|
||||
import { useAuth } from '../auth/useAuth';
|
||||
import { useEnvironments, useApps } from '../api/hooks';
|
||||
import type { EnvironmentResponse } from '../types/api';
|
||||
|
||||
/**
|
||||
* Renders one environment entry as a SidebarTreeNode.
|
||||
* This is a "render nothing, report data" component: it fetches apps for
|
||||
* the given environment and invokes `onNode` with the assembled tree node
|
||||
* whenever the data changes.
|
||||
*
|
||||
* Using a dedicated component per env is the idiomatic way to call a hook
|
||||
* for each item in a dynamic list without violating Rules of Hooks.
|
||||
*/
|
||||
function EnvWithApps({
|
||||
env,
|
||||
onNode,
|
||||
}: {
|
||||
env: EnvironmentResponse;
|
||||
onNode: (node: SidebarTreeNode) => void;
|
||||
}) {
|
||||
const { data: apps } = useApps(env.id);
|
||||
|
||||
const children: SidebarTreeNode[] = (apps ?? []).map((app) => ({
|
||||
id: app.id,
|
||||
label: app.displayName,
|
||||
path: `/environments/${env.id}/apps/${app.id}`,
|
||||
}));
|
||||
|
||||
const node: SidebarTreeNode = {
|
||||
id: env.id,
|
||||
label: env.displayName,
|
||||
path: `/environments/${env.id}`,
|
||||
children: children.length > 0 ? children : undefined,
|
||||
};
|
||||
|
||||
// Calling onNode during render is intentional here: we want the parent to
|
||||
// collect the latest node on every render. The parent guards against
|
||||
// infinite loops by doing a shallow equality check before updating state.
|
||||
onNode(node);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function EnvironmentTree() {
|
||||
const { tenantId } = useAuth();
|
||||
const { data: environments } = useEnvironments(tenantId ?? '');
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const [starred, setStarred] = useState<Set<string>>(new Set());
|
||||
const [envNodes, setEnvNodes] = useState<Map<string, SidebarTreeNode>>(new Map());
|
||||
|
||||
const handleToggleStar = useCallback((id: string) => {
|
||||
setStarred((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) {
|
||||
next.delete(id);
|
||||
} else {
|
||||
next.add(id);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleNode = useCallback((node: SidebarTreeNode) => {
|
||||
setEnvNodes((prev) => {
|
||||
const existing = prev.get(node.id);
|
||||
// Avoid infinite re-renders: only update when something meaningful changed.
|
||||
if (
|
||||
existing &&
|
||||
existing.label === node.label &&
|
||||
existing.path === node.path &&
|
||||
existing.children?.length === node.children?.length
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
return new Map(prev).set(node.id, node);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const envs = environments ?? [];
|
||||
|
||||
// Build the final node list, falling back to env-only nodes until apps load.
|
||||
const nodes: SidebarTreeNode[] = envs.map(
|
||||
(env) =>
|
||||
envNodes.get(env.id) ?? {
|
||||
id: env.id,
|
||||
label: env.displayName,
|
||||
path: `/environments/${env.id}`,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Invisible data-fetchers: one per environment */}
|
||||
{envs.map((env) => (
|
||||
<EnvWithApps key={env.id} env={env} onNode={handleNode} />
|
||||
))}
|
||||
|
||||
<SidebarTree
|
||||
nodes={nodes}
|
||||
selectedPath={location.pathname}
|
||||
isStarred={(id) => starred.has(id)}
|
||||
onToggleStar={handleToggleStar}
|
||||
onNavigate={(path) => navigate(path)}
|
||||
persistKey="env-tree"
|
||||
autoRevealPath={location.pathname}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useState } from 'react';
|
||||
import { Outlet, useNavigate } from 'react-router';
|
||||
import {
|
||||
AppShell,
|
||||
@@ -8,7 +7,6 @@ import {
|
||||
import { useAuth } from '../auth/useAuth';
|
||||
import { useScopes } from '../auth/useScopes';
|
||||
import { useOrgStore } from '../auth/useOrganization';
|
||||
import { EnvironmentTree } from './EnvironmentTree';
|
||||
import cameleerLogo from '@cameleer/design-system/assets/cameleer3-logo.svg';
|
||||
|
||||
function CameleerLogo() {
|
||||
@@ -35,19 +33,6 @@ function DashboardIcon() {
|
||||
);
|
||||
}
|
||||
|
||||
function EnvIcon() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M2 4h12M2 8h12M2 12h12"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function LicenseIcon() {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
@@ -82,11 +67,8 @@ export function Layout() {
|
||||
const scopes = useScopes();
|
||||
const { username } = useOrgStore();
|
||||
|
||||
const [envSectionOpen, setEnvSectionOpen] = useState(true);
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
const sidebar = (
|
||||
<Sidebar collapsed={collapsed} onCollapseToggle={() => setCollapsed((c) => !c)}>
|
||||
<Sidebar collapsed={false} onCollapseToggle={() => {}}>
|
||||
<Sidebar.Header
|
||||
logo={<CameleerLogo />}
|
||||
title="Cameleer SaaS"
|
||||
@@ -103,16 +85,6 @@ export function Layout() {
|
||||
{null}
|
||||
</Sidebar.Section>
|
||||
|
||||
{/* Environments — expandable tree */}
|
||||
<Sidebar.Section
|
||||
icon={<EnvIcon />}
|
||||
label="Environments"
|
||||
open={envSectionOpen}
|
||||
onToggle={() => setEnvSectionOpen((o) => !o)}
|
||||
>
|
||||
<EnvironmentTree />
|
||||
</Sidebar.Section>
|
||||
|
||||
{/* License */}
|
||||
<Sidebar.Section
|
||||
icon={<LicenseIcon />}
|
||||
@@ -136,10 +108,10 @@ export function Layout() {
|
||||
)}
|
||||
|
||||
<Sidebar.Footer>
|
||||
{/* Link to the observability SPA (direct port, not via Traefik prefix) */}
|
||||
{/* Link to the server observability dashboard */}
|
||||
<Sidebar.FooterLink
|
||||
icon={<ObsIcon />}
|
||||
label="View Dashboard"
|
||||
label="Open Server Dashboard"
|
||||
onClick={() => window.open('/server/', '_blank', 'noopener')}
|
||||
/>
|
||||
</Sidebar.Footer>
|
||||
|
||||
Reference in New Issue
Block a user