mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-29 00:01:22 +00:00
small fix, don't worry about it
This commit is contained in:
1 parent
08bd0c6e5c
commit
08cb5754f3
638 files changed
+57330
-34704
No files matched your search
@@ -0,0 +1,763 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
getApiBaseUrl,
|
||||
getCurrentUserProfile,
|
||||
listAuditLogs,
|
||||
listBuilds,
|
||||
listProjects,
|
||||
listTemplates,
|
||||
updateCurrentUserProfile,
|
||||
} from '@/lib/api-client';
|
||||
import { getAuthBaseUrl, signOutAuthSession } from '@/lib/auth-client';
|
||||
import { useBuildUpdates } from '@/lib/use-build-updates';
|
||||
import {
|
||||
Clock,
|
||||
Activity,
|
||||
Gauge,
|
||||
Users,
|
||||
Shield,
|
||||
FileText,
|
||||
Settings,
|
||||
User,
|
||||
Key,
|
||||
Database,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
LogOut,
|
||||
Check,
|
||||
AlertCircle,
|
||||
Loader2,
|
||||
BookOpen,
|
||||
Folder,
|
||||
Box,
|
||||
Terminal,
|
||||
Radio,
|
||||
} from 'lucide-react';
|
||||
|
||||
function SecondaryPageHeader({ title, description }: { title: string; description: string }) {
|
||||
return (
|
||||
<div className="border-b border-[var(--border-subtle)] bg-[var(--bg-base)]/50 backdrop-blur-sm">
|
||||
<div className="mx-auto w-full max-w-[1400px] px-6 py-4">
|
||||
<h1 className="text-xl font-semibold text-[var(--text-primary)]">{title}</h1>
|
||||
<p className="text-sm text-[var(--text-secondary)] mt-1">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
title,
|
||||
value,
|
||||
description,
|
||||
icon: Icon,
|
||||
color = 'default',
|
||||
}: {
|
||||
title: string;
|
||||
value: string;
|
||||
description: string;
|
||||
icon: typeof Clock;
|
||||
color?: 'default' | 'success' | 'warning' | 'error';
|
||||
}) {
|
||||
const colorClasses = {
|
||||
default: 'text-[var(--text-primary)]',
|
||||
success: 'text-[var(--success)]',
|
||||
warning: 'text-[var(--warning)]',
|
||||
error: 'text-[var(--error)]',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="panel p-5">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="w-10 h-10 rounded-[var(--radius-md)] bg-[var(--surface-muted)] flex items-center justify-center">
|
||||
<Icon size={18} className="text-[var(--text-tertiary)]" />
|
||||
</div>
|
||||
</div>
|
||||
<p className={`text-2xl font-semibold ${colorClasses[color]}`}>{value}</p>
|
||||
<p className="text-xs font-medium text-[var(--text-muted)] mt-1">{title}</p>
|
||||
<p className="text-xs text-[var(--text-tertiary)] mt-2">{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type LocalStorageSummary = {
|
||||
canvasKeys: string[];
|
||||
totalKeys: number;
|
||||
};
|
||||
|
||||
function getLocalStorageSummary(): LocalStorageSummary {
|
||||
if (typeof window === 'undefined') {
|
||||
return { canvasKeys: [], totalKeys: 0 };
|
||||
}
|
||||
|
||||
const canvasKeys: string[] = [];
|
||||
for (let index = 0; index < localStorage.length; index += 1) {
|
||||
const key = localStorage.key(index);
|
||||
if (key && key.startsWith('containr.canvas.v1.')) {
|
||||
canvasKeys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
canvasKeys.sort((left, right) => left.localeCompare(right));
|
||||
|
||||
return {
|
||||
canvasKeys,
|
||||
totalKeys: localStorage.length,
|
||||
};
|
||||
}
|
||||
|
||||
function endpointStateBadge(isLoading: boolean, isError: boolean): {
|
||||
label: string;
|
||||
toneClass: string;
|
||||
} {
|
||||
if (isLoading) {
|
||||
return { label: 'Loading', toneClass: 'text-[var(--warn)]' };
|
||||
}
|
||||
if (isError) {
|
||||
return { label: 'Unavailable', toneClass: 'text-[var(--bad)]' };
|
||||
}
|
||||
return { label: 'Available', toneClass: 'text-[var(--ok)]' };
|
||||
}
|
||||
|
||||
export function UsagePage() {
|
||||
const queryClient = useQueryClient();
|
||||
const buildsQuery = useQuery({
|
||||
queryKey: ['usage-builds'],
|
||||
queryFn: () => listBuilds({ page: 1, limit: 100 }),
|
||||
});
|
||||
|
||||
const builds = buildsQuery.data?.builds ?? [];
|
||||
const liveConnected = useBuildUpdates(
|
||||
builds.map((build) => build.id),
|
||||
() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['usage-builds'] });
|
||||
},
|
||||
);
|
||||
|
||||
const successfulBuilds = builds.filter((build) => build.status === 'success').length;
|
||||
const failedBuilds = builds.filter((build) => build.status === 'failed').length;
|
||||
const runningBuilds = builds.filter((build) => build.status === 'running').length;
|
||||
const pendingBuilds = builds.filter((build) => build.status === 'pending').length;
|
||||
const completedDurationsMs = builds
|
||||
.filter((build) => Boolean(build.startedAt && build.completedAt))
|
||||
.map((build) => {
|
||||
const started = new Date(build.startedAt as string).getTime();
|
||||
const completed = new Date(build.completedAt as string).getTime();
|
||||
return Math.max(0, completed - started);
|
||||
})
|
||||
.filter((duration) => Number.isFinite(duration) && duration > 0);
|
||||
|
||||
const totalBuildDurationMs = completedDurationsMs.reduce((sum, duration) => sum + duration, 0);
|
||||
const averageBuildDurationMs =
|
||||
completedDurationsMs.length > 0 ? totalBuildDurationMs / completedDurationsMs.length : 0;
|
||||
|
||||
const totalBuildHours = totalBuildDurationMs / 3_600_000;
|
||||
const avgBuildMinutes = averageBuildDurationMs / 60_000;
|
||||
const activeBuildPressure = runningBuilds + pendingBuilds;
|
||||
const uniqueServices = new Set(builds.map((build) => build.serviceId).filter(Boolean)).size;
|
||||
|
||||
let buildActivityBody = 'Track deployment frequency and failed rollouts over time.';
|
||||
let runtimeBody = 'Build duration telemetry will appear after completed builds are available.';
|
||||
let capacityBody = 'No active build pressure detected.';
|
||||
|
||||
if (buildsQuery.isLoading) {
|
||||
buildActivityBody = 'Loading recent build activity from Containr API.';
|
||||
runtimeBody = 'Loading build runtime metrics from Containr API.';
|
||||
capacityBody = 'Loading active queue depth and service fan-out.';
|
||||
} else if (buildsQuery.isError) {
|
||||
buildActivityBody = 'Unable to fetch build telemetry right now. Check API connectivity and auth.';
|
||||
runtimeBody = 'Unable to calculate runtime metrics while build telemetry is unavailable.';
|
||||
capacityBody = 'Unable to evaluate queue pressure while build telemetry is unavailable.';
|
||||
} else if (builds.length > 0) {
|
||||
buildActivityBody = `${builds.length} recent builds, ${successfulBuilds} successful, ${failedBuilds} failed${runningBuilds > 0 ? `, ${runningBuilds} running` : ''}.`;
|
||||
|
||||
if (completedDurationsMs.length > 0) {
|
||||
runtimeBody = `${totalBuildHours.toFixed(1)}h total build runtime across ${completedDurationsMs.length} completed builds (avg ${avgBuildMinutes.toFixed(1)}m).`;
|
||||
} else {
|
||||
runtimeBody = 'Builds exist, but completed duration samples are not available yet.';
|
||||
}
|
||||
|
||||
if (activeBuildPressure > 0) {
|
||||
capacityBody = `${activeBuildPressure} active jobs in queue (${runningBuilds} running, ${pendingBuilds} pending) across ${uniqueServices} service${uniqueServices === 1 ? '' : 's'}.`;
|
||||
} else {
|
||||
capacityBody = `No pending or running jobs. Last sampled ${builds.length} builds touched ${uniqueServices} service${uniqueServices === 1 ? '' : 's'}.`;
|
||||
}
|
||||
} else {
|
||||
buildActivityBody = 'No builds recorded yet. Trigger a service deployment to populate this view.';
|
||||
runtimeBody = 'No completed builds yet, so runtime totals are not available.';
|
||||
capacityBody = 'Queue depth is empty because no build history is available yet.';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<SecondaryPageHeader
|
||||
title="Usage"
|
||||
description="Platform usage metrics and operational summaries"
|
||||
/>
|
||||
<div className="mx-auto w-full max-w-[1400px] px-6 py-6">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<div className={`w-2 h-2 rounded-full ${liveConnected ? 'bg-[var(--success)] animate-pulse' : 'bg-[var(--text-muted)]'}`} />
|
||||
<span className={`text-sm ${liveConnected ? 'text-[var(--success)]' : 'text-[var(--text-muted)]'}`}>
|
||||
{liveConnected ? 'Live sync active' : 'Offline'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{buildsQuery.isLoading ? (
|
||||
<div className="py-16 text-center">
|
||||
<Loader2 size={24} className="animate-spin mx-auto text-[var(--text-tertiary)]" />
|
||||
<p className="mt-3 text-sm text-[var(--text-muted)]">Loading usage data...</p>
|
||||
</div>
|
||||
) : buildsQuery.isError ? (
|
||||
<div className="panel p-8 text-center">
|
||||
<div className="w-12 h-12 mx-auto mb-4 rounded-full bg-[var(--error-soft)] flex items-center justify-center">
|
||||
<AlertCircle size={24} className="text-[var(--error)]" />
|
||||
</div>
|
||||
<p className="text-sm text-[var(--error)]">Failed to load usage data</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<StatCard
|
||||
title="Runtime Hours"
|
||||
value={totalBuildHours > 0 ? `${totalBuildHours.toFixed(1)}h` : '—'}
|
||||
description={runtimeBody}
|
||||
icon={Clock}
|
||||
color={completedDurationsMs.length > 0 ? 'success' : 'default'}
|
||||
/>
|
||||
<StatCard
|
||||
title="Build Activity"
|
||||
value={String(builds.length)}
|
||||
description={buildActivityBody}
|
||||
icon={Activity}
|
||||
color={failedBuilds > 0 ? 'warning' : 'success'}
|
||||
/>
|
||||
<StatCard
|
||||
title="Capacity"
|
||||
value={String(activeBuildPressure)}
|
||||
description={capacityBody}
|
||||
icon={Gauge}
|
||||
color={activeBuildPressure > 0 ? 'warning' : 'default'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PeoplePage() {
|
||||
const profileQuery = useQuery({
|
||||
queryKey: ['user-profile'],
|
||||
queryFn: getCurrentUserProfile,
|
||||
});
|
||||
|
||||
const projectsQuery = useQuery({
|
||||
queryKey: ['people-projects'],
|
||||
queryFn: listProjects,
|
||||
});
|
||||
const auditLogsQuery = useQuery({
|
||||
queryKey: ['people-audit-logs'],
|
||||
queryFn: () => listAuditLogs({ page: 1, limit: 20 }),
|
||||
});
|
||||
|
||||
let membersBody = 'Owner-only mode currently. Multi-user collaboration is planned.';
|
||||
|
||||
if (profileQuery.isLoading) {
|
||||
membersBody = 'Loading authenticated profile from Containr API.';
|
||||
} else if (profileQuery.isError) {
|
||||
membersBody = 'Unable to fetch current user profile. Verify API connectivity and Better Auth session.';
|
||||
} else if (profileQuery.data) {
|
||||
membersBody = `${profileQuery.data.name} (${profileQuery.data.email}) is currently authenticated as platform owner.`;
|
||||
}
|
||||
|
||||
let rolesBody = 'Predefined roles will map to service and project permissions.';
|
||||
|
||||
if (projectsQuery.isLoading) {
|
||||
rolesBody = 'Loading project access footprint for current user.';
|
||||
} else if (projectsQuery.isError) {
|
||||
rolesBody = 'Project visibility check failed. Role scope cannot be calculated right now.';
|
||||
} else {
|
||||
const projectCount = projectsQuery.data?.length ?? 0;
|
||||
rolesBody = `Current account can access ${projectCount} project${projectCount === 1 ? '' : 's'} in owner mode.`;
|
||||
}
|
||||
|
||||
let auditBody = 'Human-readable action log for project and service changes.';
|
||||
|
||||
if (auditLogsQuery.isLoading) {
|
||||
auditBody = 'Loading recent audit events from Containr API.';
|
||||
} else if (auditLogsQuery.isError) {
|
||||
auditBody = 'Unable to load recent audit events. Verify API connectivity and Better Auth session.';
|
||||
} else {
|
||||
const auditLogs = auditLogsQuery.data ?? [];
|
||||
if (auditLogs.length > 0) {
|
||||
const latest = auditLogs[0];
|
||||
auditBody = `${auditLogs.length} recent events. Latest action: ${latest.action} on ${latest.resource}${
|
||||
latest.resourceId ? ` (${latest.resourceId})` : ''
|
||||
}.`;
|
||||
} else {
|
||||
auditBody = 'No audit events recorded yet for this account.';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<SecondaryPageHeader
|
||||
title="People"
|
||||
description="Team management and access control"
|
||||
/>
|
||||
<div className="mx-auto w-full max-w-[1400px] px-6 py-6">
|
||||
{profileQuery.isLoading ? (
|
||||
<div className="py-16 text-center">
|
||||
<Loader2 size={24} className="animate-spin mx-auto text-[var(--text-tertiary)]" />
|
||||
<p className="mt-3 text-sm text-[var(--text-muted)]">Loading team data...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<StatCard
|
||||
title="Members"
|
||||
value={profileQuery.data ? '1' : '—'}
|
||||
description={membersBody}
|
||||
icon={Users}
|
||||
color={profileQuery.data ? 'success' : 'default'}
|
||||
/>
|
||||
<StatCard
|
||||
title="Roles"
|
||||
value={projectsQuery.data?.length ? String(projectsQuery.data.length) : '—'}
|
||||
description={rolesBody}
|
||||
icon={Shield}
|
||||
color="default"
|
||||
/>
|
||||
<StatCard
|
||||
title="Audit Trail"
|
||||
value={auditLogsQuery.data ? String(auditLogsQuery.data.length) : '—'}
|
||||
description={auditBody}
|
||||
icon={FileText}
|
||||
color={auditLogsQuery.data?.length ? 'success' : 'default'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsPage() {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const profileQuery = useQuery({
|
||||
queryKey: ['settings-profile'],
|
||||
queryFn: getCurrentUserProfile,
|
||||
});
|
||||
|
||||
const [nameDraft, setNameDraft] = useState<string | null>(null);
|
||||
const [avatarDraft, setAvatarDraft] = useState<string | null>(null);
|
||||
const [storageSummary, setStorageSummary] = useState<LocalStorageSummary>(() =>
|
||||
getLocalStorageSummary(),
|
||||
);
|
||||
|
||||
const resolvedNameDraft = nameDraft ?? profileQuery.data?.name ?? '';
|
||||
const resolvedAvatarDraft = avatarDraft ?? profileQuery.data?.avatarUrl ?? '';
|
||||
|
||||
const hasProfileChanges = useMemo(() => {
|
||||
if (!profileQuery.data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
resolvedNameDraft.trim() !== profileQuery.data.name ||
|
||||
resolvedAvatarDraft.trim() !== (profileQuery.data.avatarUrl ?? '')
|
||||
);
|
||||
}, [profileQuery.data, resolvedAvatarDraft, resolvedNameDraft]);
|
||||
|
||||
const updateProfileMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
updateCurrentUserProfile({
|
||||
name: resolvedNameDraft.trim(),
|
||||
avatarUrl: resolvedAvatarDraft.trim() || undefined,
|
||||
}),
|
||||
onSuccess: (profile) => {
|
||||
queryClient.setQueryData(['settings-profile'], profile);
|
||||
queryClient.setQueryData(['user-profile'], profile);
|
||||
setNameDraft(null);
|
||||
setAvatarDraft(null);
|
||||
},
|
||||
});
|
||||
|
||||
const refreshStorage = () => {
|
||||
setStorageSummary(getLocalStorageSummary());
|
||||
};
|
||||
|
||||
const clearCanvasCache = () => {
|
||||
for (const key of storageSummary.canvasKeys) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
refreshStorage();
|
||||
};
|
||||
|
||||
const signOutLocalSession = async () => {
|
||||
try {
|
||||
await signOutAuthSession();
|
||||
} catch {
|
||||
// Continue with local cleanup even if remote sign-out call fails.
|
||||
}
|
||||
queryClient.removeQueries();
|
||||
await queryClient.invalidateQueries({ queryKey: ['auth-session'] });
|
||||
navigate('/auth/sign-in', { replace: true });
|
||||
refreshStorage();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<SecondaryPageHeader
|
||||
title="Settings"
|
||||
description="Manage account profile and local configuration"
|
||||
/>
|
||||
<div className="mx-auto w-full max-w-[1400px] px-6 py-6">
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||
{/* Profile Section */}
|
||||
<section className="panel p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-[var(--radius-md)] bg-[var(--accent-primary-soft)] flex items-center justify-center">
|
||||
<User size={18} className="text-[var(--accent-primary)]" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-[var(--text-primary)]">Profile</h2>
|
||||
<p className="text-xs text-[var(--text-tertiary)]">Backed by GET/PUT /user/profile</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{profileQuery.isLoading ? (
|
||||
<div className="py-8 text-center">
|
||||
<Loader2 size={20} className="animate-spin mx-auto text-[var(--text-tertiary)]" />
|
||||
</div>
|
||||
) : profileQuery.isError ? (
|
||||
<div className="px-4 py-3 rounded-[var(--radius-md)] bg-[var(--error-soft)] text-sm text-[var(--error)]">
|
||||
Failed to load profile
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium uppercase tracking-wider text-[var(--text-muted)] mb-2">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
value={resolvedNameDraft}
|
||||
onChange={(e) => setNameDraft(e.target.value)}
|
||||
className="w-full h-10 px-3 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] text-sm focus:border-[var(--accent-primary)] transition-colors"
|
||||
placeholder="Your name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium uppercase tracking-wider text-[var(--text-muted)] mb-2">
|
||||
Avatar URL
|
||||
</label>
|
||||
<input
|
||||
value={resolvedAvatarDraft}
|
||||
onChange={(e) => setAvatarDraft(e.target.value)}
|
||||
className="w-full h-10 px-3 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] text-sm focus:border-[var(--accent-primary)] transition-colors"
|
||||
placeholder="https://example.com/avatar.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<button
|
||||
onClick={() => updateProfileMutation.mutate()}
|
||||
disabled={!hasProfileChanges || updateProfileMutation.isPending}
|
||||
className="flex items-center gap-2 h-9 px-4 rounded-[var(--radius-md)] text-white text-sm font-medium shadow-lg disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
style={{ background: '#e8316a' }}
|
||||
>
|
||||
{updateProfileMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Check size={14} />
|
||||
Save Profile
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => profileQuery.refetch()}
|
||||
className="flex items-center gap-2 h-9 px-4 rounded-[var(--radius-md)] border border-[var(--border-subtle)] text-sm font-medium hover:border-[var(--border-default)] transition-colors"
|
||||
>
|
||||
<RefreshCw size={14} />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{updateProfileMutation.isError && (
|
||||
<div className="px-4 py-3 rounded-[var(--radius-md)] bg-[var(--error-soft)] text-sm text-[var(--error)]">
|
||||
{updateProfileMutation.error instanceof Error ? updateProfileMutation.error.message : 'Failed to update profile'}
|
||||
</div>
|
||||
)}
|
||||
{updateProfileMutation.isSuccess && (
|
||||
<div className="px-4 py-3 rounded-[var(--radius-md)] bg-[var(--success-soft)] text-sm text-[var(--success)] flex items-center gap-2">
|
||||
<Check size={16} />
|
||||
Profile updated successfully
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Runtime & Local State Section */}
|
||||
<section className="panel p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-[var(--radius-md)] bg-[var(--surface-muted)] flex items-center justify-center">
|
||||
<Settings size={18} className="text-[var(--text-tertiary)]" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-[var(--text-primary)]">Runtime & Local State</h2>
|
||||
<p className="text-xs text-[var(--text-tertiary)]">API endpoints, cookie-session mode, and storage diagnostics</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="p-4 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)]">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Key size={14} className="text-[var(--text-tertiary)]" />
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">API Base</span>
|
||||
</div>
|
||||
<p className="mono text-xs text-[var(--text-primary)] break-all">{getApiBaseUrl()}</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)]">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Key size={14} className="text-[var(--text-tertiary)]" />
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Auth Base</span>
|
||||
</div>
|
||||
<p className="mono text-xs text-[var(--text-primary)] break-all">{getAuthBaseUrl()}</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)]">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Shield size={14} className="text-[var(--text-tertiary)]" />
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Session Mode</span>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-primary)]">HttpOnly Better Auth cookie (no local auth token)</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)]">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Database size={14} className="text-[var(--text-tertiary)]" />
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Canvas Cache</span>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-primary)]">
|
||||
{storageSummary.canvasKeys.length} cached canvas entries ({storageSummary.totalKeys} total keys)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3 mt-4">
|
||||
<button
|
||||
onClick={refreshStorage}
|
||||
className="flex items-center gap-2 h-9 px-4 rounded-[var(--radius-md)] border border-[var(--border-subtle)] text-sm font-medium hover:border-[var(--border-default)] transition-colors"
|
||||
>
|
||||
<RefreshCw size={14} />
|
||||
Refresh
|
||||
</button>
|
||||
<button
|
||||
onClick={clearCanvasCache}
|
||||
disabled={storageSummary.canvasKeys.length === 0}
|
||||
className="flex items-center gap-2 h-9 px-4 rounded-[var(--radius-md)] border border-[var(--warning-soft)] text-[var(--warning)] text-sm font-medium hover:bg-[var(--warning-soft)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
Clear Cache
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
void signOutLocalSession();
|
||||
}}
|
||||
className="flex items-center gap-2 h-9 px-4 rounded-[var(--radius-md)] border border-[var(--error-soft)] text-[var(--error)] text-sm font-medium hover:bg-[var(--error-soft)] transition-colors"
|
||||
>
|
||||
<LogOut size={14} />
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{storageSummary.canvasKeys.length > 0 && (
|
||||
<div className="mt-4 p-4 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)]">
|
||||
<p className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)] mb-2">Cached Keys</p>
|
||||
<ul className="space-y-1">
|
||||
{storageSummary.canvasKeys.slice(0, 5).map((key) => (
|
||||
<li key={key} className="mono text-xs text-[var(--text-tertiary)] truncate">
|
||||
{key}
|
||||
</li>
|
||||
))}
|
||||
{storageSummary.canvasKeys.length > 5 && (
|
||||
<li className="text-xs text-[var(--text-muted)]">
|
||||
+{storageSummary.canvasKeys.length - 5} more...
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DocsPage() {
|
||||
const profileQuery = useQuery({
|
||||
queryKey: ['docs-profile'],
|
||||
queryFn: getCurrentUserProfile,
|
||||
});
|
||||
const projectsQuery = useQuery({
|
||||
queryKey: ['docs-projects'],
|
||||
queryFn: listProjects,
|
||||
});
|
||||
const templatesQuery = useQuery({
|
||||
queryKey: ['docs-templates'],
|
||||
queryFn: () => listTemplates(),
|
||||
});
|
||||
const buildsQuery = useQuery({
|
||||
queryKey: ['docs-builds'],
|
||||
queryFn: () => listBuilds({ page: 1, limit: 1 }),
|
||||
});
|
||||
|
||||
const profileStatus = endpointStateBadge(profileQuery.isLoading, profileQuery.isError);
|
||||
const projectStatus = endpointStateBadge(projectsQuery.isLoading, projectsQuery.isError);
|
||||
const templateStatus = endpointStateBadge(templatesQuery.isLoading, templatesQuery.isError);
|
||||
const buildStatus = endpointStateBadge(buildsQuery.isLoading, buildsQuery.isError);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<SecondaryPageHeader
|
||||
title="Docs"
|
||||
description="Operational references and API documentation"
|
||||
/>
|
||||
<div className="mx-auto w-full max-w-[1400px] px-6 py-6">
|
||||
{/* API Status Grid */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div className="panel p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<User size={14} className={profileStatus.toneClass} />
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Profile</span>
|
||||
</div>
|
||||
<p className={`text-sm font-semibold ${profileStatus.toneClass}`}>{profileStatus.label}</p>
|
||||
<p className="text-xs text-[var(--text-tertiary)] mt-1">
|
||||
{profileQuery.data ? `${profileQuery.data.name} authenticated` : 'GET /user/profile'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="panel p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Folder size={14} className={projectStatus.toneClass} />
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Projects</span>
|
||||
</div>
|
||||
<p className={`text-sm font-semibold ${projectStatus.toneClass}`}>{projectStatus.label}</p>
|
||||
<p className="text-xs text-[var(--text-tertiary)] mt-1">
|
||||
{projectsQuery.data ? `${projectsQuery.data.length} projects` : 'GET /projects'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="panel p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Box size={14} className={templateStatus.toneClass} />
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Templates</span>
|
||||
</div>
|
||||
<p className={`text-sm font-semibold ${templateStatus.toneClass}`}>{templateStatus.label}</p>
|
||||
<p className="text-xs text-[var(--text-tertiary)] mt-1">
|
||||
{templatesQuery.data ? `${templatesQuery.data.length} templates` : 'GET /templates'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="panel p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Activity size={14} className={buildStatus.toneClass} />
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Builds</span>
|
||||
</div>
|
||||
<p className={`text-sm font-semibold ${buildStatus.toneClass}`}>{buildStatus.label}</p>
|
||||
<p className="text-xs text-[var(--text-tertiary)] mt-1">
|
||||
{buildsQuery.data ? `${buildsQuery.data.total} builds` : 'GET /builds'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||
{/* Repository Paths */}
|
||||
<section className="panel p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-[var(--radius-md)] bg-[var(--accent-primary-soft)] flex items-center justify-center">
|
||||
<BookOpen size={18} className="text-[var(--accent-primary)]" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-[var(--text-primary)]">Repository Paths</h2>
|
||||
<p className="text-xs text-[var(--text-tertiary)]">Source files and documentation</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="p-4 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)]">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<FileText size={14} className="text-[var(--text-tertiary)]" />
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">API Contract</span>
|
||||
</div>
|
||||
<p className="mono text-xs text-[var(--text-primary)]">docs/api/openapi.yaml</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)]">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<FileText size={14} className="text-[var(--text-tertiary)]" />
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Generated Types</span>
|
||||
</div>
|
||||
<p className="mono text-xs text-[var(--text-primary)]">frontend/src/generated/api-types.ts</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)]">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<BookOpen size={14} className="text-[var(--text-tertiary)]" />
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Primary Guides</span>
|
||||
</div>
|
||||
<p className="mono text-xs text-[var(--text-primary)]">README.md • DOCKER_SETUP.md • docs/guides/</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)]">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<FileText size={14} className="text-[var(--text-tertiary)]" />
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">UI References</span>
|
||||
</div>
|
||||
<p className="mono text-xs text-[var(--text-primary)]">docs/references/self.html</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Common Commands */}
|
||||
<section className="panel p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-[var(--radius-md)] bg-[var(--surface-muted)] flex items-center justify-center">
|
||||
<Terminal size={18} className="text-[var(--text-tertiary)]" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-[var(--text-primary)]">Common Commands</h2>
|
||||
<p className="text-xs text-[var(--text-tertiary)]">Development and build scripts</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--bg-void)]">
|
||||
<pre className="mono text-xs text-[var(--text-secondary)] whitespace-pre-wrap">
|
||||
{`# Regenerate frontend API types
|
||||
npm --prefix frontend run generate:api
|
||||
|
||||
# Frontend type-check + build
|
||||
npm --prefix frontend run build:check
|
||||
|
||||
# Backend API tests
|
||||
cd backend && go test ./internal/api/...`}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-4 rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)]">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Radio size={14} className="text-[var(--text-tertiary)]" />
|
||||
<span className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)]">Active API Base</span>
|
||||
</div>
|
||||
<p className="mono text-xs text-[var(--text-primary)] break-all">{getApiBaseUrl()}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export { ComponentShowcase } from './pages/ComponentShowcase';
|
||||
@@ -0,0 +1,472 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Cpu,
|
||||
MemoryStick,
|
||||
Zap,
|
||||
Users,
|
||||
Activity,
|
||||
TrendingUp,
|
||||
Database
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
LineChart,
|
||||
LineAreaChart,
|
||||
MultiLineChart,
|
||||
DonutChart,
|
||||
BarChart,
|
||||
EnhancedMetricCard,
|
||||
CacheMetricCard,
|
||||
PerformanceMetricCard,
|
||||
useToast
|
||||
} from '@/shared/components';
|
||||
|
||||
// Sample data
|
||||
const sampleLineData = [12, 19, 15, 25, 22, 30, 28, 35, 32, 38, 35, 42, 38, 45, 42, 48, 45, 52, 48, 55, 52, 58, 55, 60];
|
||||
const sampleAreaData = [40, 45, 42, 50, 48, 55, 52, 60, 58, 65, 62, 70, 68, 75, 72];
|
||||
const sampleBarData = [20, 35, 28, 45, 38, 52, 48, 60, 55, 68, 62, 75, 70, 82, 78, 88, 85, 92, 88, 95, 92, 98, 95, 100];
|
||||
|
||||
export function ComponentShowcase() {
|
||||
const { showToast } = useToast();
|
||||
const [activeTab, setActiveTab] = useState<'charts' | 'cards' | 'ui'>('charts');
|
||||
|
||||
const handleCardClick = (name: string) => {
|
||||
showToast(`${name} clicked!`, 'info');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen p-8" style={{ background: '#16171c' }}>
|
||||
<div className="ambient-glow" />
|
||||
|
||||
<div className="max-w-7xl mx-auto relative z-10">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div
|
||||
className="w-12 h-12 rounded-xl flex items-center justify-center"
|
||||
style={{ background: '#e8316a' }}
|
||||
>
|
||||
<Activity size={24} className="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-[var(--text-primary)]">
|
||||
Component Showcase
|
||||
</h1>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Production-grade components matching self.html design
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-[var(--border-subtle)] gap-1">
|
||||
<button
|
||||
onClick={() => setActiveTab('charts')}
|
||||
className={`tab ${activeTab === 'charts' ? 'active' : ''}`}
|
||||
>
|
||||
<Activity size={14} />
|
||||
Charts
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('cards')}
|
||||
className={`tab ${activeTab === 'cards' ? 'active' : ''}`}
|
||||
>
|
||||
<Cpu size={14} />
|
||||
Metric Cards
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('ui')}
|
||||
className={`tab ${activeTab === 'ui' ? 'active' : ''}`}
|
||||
>
|
||||
<Zap size={14} />
|
||||
UI Elements
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Charts Tab */}
|
||||
{activeTab === 'charts' && (
|
||||
<div className="space-y-8">
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-4">
|
||||
Line Charts
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="panel p-6">
|
||||
<h3 className="text-sm font-medium text-[var(--text-secondary)] mb-4">
|
||||
Basic Line Chart (CPU Style)
|
||||
</h3>
|
||||
<LineChart
|
||||
data={sampleLineData}
|
||||
color="#ff7043"
|
||||
height={120}
|
||||
showDots={true}
|
||||
smooth={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="panel p-6">
|
||||
<h3 className="text-sm font-medium text-[var(--text-secondary)] mb-4">
|
||||
Line Chart (No Dots)
|
||||
</h3>
|
||||
<LineChart
|
||||
data={sampleLineData}
|
||||
color="#6c8ef0"
|
||||
height={120}
|
||||
showDots={false}
|
||||
smooth={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-4">
|
||||
Area Charts
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="panel p-6">
|
||||
<h3 className="text-sm font-medium text-[var(--text-secondary)] mb-4">
|
||||
Area Chart (Active Users Style)
|
||||
</h3>
|
||||
<LineAreaChart
|
||||
data={sampleAreaData}
|
||||
color="#e8316a"
|
||||
fillOpacity={0.15}
|
||||
height={150}
|
||||
/>
|
||||
</div>
|
||||
<div className="panel p-6">
|
||||
<h3 className="text-sm font-medium text-[var(--text-secondary)] mb-4">
|
||||
Area Chart (Success Color)
|
||||
</h3>
|
||||
<LineAreaChart
|
||||
data={sampleAreaData}
|
||||
color="#3dd68c"
|
||||
fillOpacity={0.2}
|
||||
height={150}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-4">
|
||||
Multi-Line & Bar Charts
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="panel p-6">
|
||||
<h3 className="text-sm font-medium text-[var(--text-secondary)] mb-4">
|
||||
Multi-Line Chart (Performance Style)
|
||||
</h3>
|
||||
<MultiLineChart
|
||||
datasets={[
|
||||
{ data: sampleLineData.map(v => v + 10), color: '#6c8ef0', fillOpacity: 0.15 },
|
||||
{ data: sampleLineData, color: '#9c7ef0', fillOpacity: 0.15 }
|
||||
]}
|
||||
height={120}
|
||||
/>
|
||||
</div>
|
||||
<div className="panel p-6">
|
||||
<h3 className="text-sm font-medium text-[var(--text-secondary)] mb-4">
|
||||
Bar Chart (Timeline Style)
|
||||
</h3>
|
||||
<BarChart
|
||||
data={sampleBarData}
|
||||
color="#3dd68c"
|
||||
height={120}
|
||||
gap={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-4">
|
||||
Donut Charts
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="panel p-6 flex flex-col items-center">
|
||||
<h3 className="text-sm font-medium text-[var(--text-secondary)] mb-4">
|
||||
RAM Usage (65%)
|
||||
</h3>
|
||||
<DonutChart percentage={65} color="#9c7ef0" size={160} />
|
||||
</div>
|
||||
<div className="panel p-6 flex flex-col items-center">
|
||||
<h3 className="text-sm font-medium text-[var(--text-secondary)] mb-4">
|
||||
Disk Usage (82%)
|
||||
</h3>
|
||||
<DonutChart percentage={82} color="#ff7043" size={160} />
|
||||
</div>
|
||||
<div className="panel p-6 flex flex-col items-center">
|
||||
<h3 className="text-sm font-medium text-[var(--text-secondary)] mb-4">
|
||||
Network (45%)
|
||||
</h3>
|
||||
<DonutChart percentage={45} color="#6c8ef0" size={160} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Metric Cards Tab */}
|
||||
{activeTab === 'cards' && (
|
||||
<div className="space-y-8">
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-4">
|
||||
Standard Metric Cards
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
<EnhancedMetricCard
|
||||
title="CPU Usage"
|
||||
value="12%"
|
||||
subtitle="Daily usage"
|
||||
status="good"
|
||||
statusText="Good"
|
||||
icon={<Cpu size={16} />}
|
||||
chart={<LineChart data={sampleLineData} color="#ff7043" height={76} />}
|
||||
onClick={() => handleCardClick('CPU Usage')}
|
||||
animationDelay={0.04}
|
||||
/>
|
||||
<EnhancedMetricCard
|
||||
title="Memory"
|
||||
value="65%"
|
||||
subtitle="Container footprint"
|
||||
status="average"
|
||||
statusText="Average"
|
||||
icon={<MemoryStick size={16} />}
|
||||
chart={<LineChart data={sampleLineData.map(v => v + 20)} color="#9c7ef0" height={76} />}
|
||||
onClick={() => handleCardClick('Memory')}
|
||||
animationDelay={0.09}
|
||||
/>
|
||||
<EnhancedMetricCard
|
||||
title="Requests"
|
||||
value="2.4K"
|
||||
subtitle="Last 60 minutes"
|
||||
icon={<Zap size={16} />}
|
||||
chart={<BarChart data={sampleBarData} color="#3dd68c" height={76} gap={2} />}
|
||||
onClick={() => handleCardClick('Requests')}
|
||||
animationDelay={0.14}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-4">
|
||||
Horizontal Layout Cards
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||
<EnhancedMetricCard
|
||||
title="Active Users"
|
||||
value="475 K"
|
||||
subtitle="Users active right now"
|
||||
icon={<Users size={16} />}
|
||||
chart={<LineAreaChart data={sampleAreaData} color="#e8316a" height={130} />}
|
||||
details={
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{['🇨🇳', '🇮🇩', '🇲🇲', '🇲🇾', '🇯🇵', '🇮🇳', '🇰🇷', '🇵🇭'].map((flag, i) => (
|
||||
<span key={i} className="flag">{flag}</span>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
horizontal
|
||||
onClick={() => handleCardClick('Active Users')}
|
||||
animationDelay={0.04}
|
||||
/>
|
||||
<EnhancedMetricCard
|
||||
title="Database Queries"
|
||||
value="12.5K"
|
||||
subtitle="Queries per minute"
|
||||
icon={<Database size={16} />}
|
||||
chart={<LineAreaChart data={sampleAreaData.map(v => v + 10)} color="#6c8ef0" height={130} />}
|
||||
details={
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-2 h-2 rounded-full bg-[var(--success)]" />
|
||||
<span className="text-[var(--text-secondary)]">Read: 8.2K</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-2 h-2 rounded-full bg-[var(--warning)]" />
|
||||
<span className="text-[var(--text-secondary)]">Write: 4.3K</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
horizontal
|
||||
onClick={() => handleCardClick('Database Queries')}
|
||||
animationDelay={0.09}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-4">
|
||||
Specialized Cards
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||
<CacheMetricCard
|
||||
totalMB={352}
|
||||
cacheMB={212}
|
||||
nonCacheMB={85}
|
||||
onClick={() => handleCardClick('Cache')}
|
||||
animationDelay={0.04}
|
||||
/>
|
||||
<PerformanceMetricCard
|
||||
percentage={89}
|
||||
upSpeed={10.4}
|
||||
downSpeed={5.2}
|
||||
datasets={[
|
||||
{ data: sampleLineData.map(v => v + 20), color: '#6c8ef0', fillOpacity: 0.15 },
|
||||
{ data: sampleLineData.map(v => v + 10), color: '#9c7ef0', fillOpacity: 0.15 }
|
||||
]}
|
||||
onClick={() => handleCardClick('Performance')}
|
||||
animationDelay={0.09}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* UI Elements Tab */}
|
||||
{activeTab === 'ui' && (
|
||||
<div className="space-y-8">
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-4">
|
||||
Badges & Indicators
|
||||
</h2>
|
||||
<div className="panel p-6 space-y-4">
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<span className="badge-active">
|
||||
<span className="live-dot" />
|
||||
Active
|
||||
</span>
|
||||
<span className="badge-stopped">Stopped</span>
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-[var(--success-soft)] text-[var(--success)] text-xs font-medium">
|
||||
<div className="w-2 h-2 rounded-full bg-[var(--success)]" />
|
||||
Healthy
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-[var(--warning-soft)] text-[var(--warning)] text-xs font-medium">
|
||||
<div className="w-2 h-2 rounded-full bg-[var(--warning)]" />
|
||||
Degraded
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-[var(--error-soft)] text-[var(--error)] text-xs font-medium">
|
||||
<div className="w-2 h-2 rounded-full bg-[var(--error)]" />
|
||||
Critical
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-4">
|
||||
Buttons
|
||||
</h2>
|
||||
<div className="panel p-6 space-y-4">
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<button className="btn-stop">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<circle cx="12" cy="12" r="3" fill="currentColor" stroke="none"/>
|
||||
</svg>
|
||||
STOP
|
||||
</button>
|
||||
<button className="btn-restart">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round">
|
||||
<polyline points="1 4 1 10 7 10"/>
|
||||
<path d="M3.51 15a9 9 0 1 0 .49-4.5"/>
|
||||
</svg>
|
||||
RESTART
|
||||
</button>
|
||||
<div className="arrow-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth={2.5} strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="9 18 15 12 9 6" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-4">
|
||||
Pills & Tabs
|
||||
</h2>
|
||||
<div className="panel p-6 space-y-6">
|
||||
<div>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-3">Pill Group</p>
|
||||
<div className="pill-group">
|
||||
<div className="pill active">Day</div>
|
||||
<div className="pill">Month</div>
|
||||
<div className="pill">Year</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-3">Tabs</p>
|
||||
<div className="flex border-b border-[var(--border-subtle)]">
|
||||
<button className="tab active">
|
||||
<Activity size={14} />
|
||||
Metrics
|
||||
</button>
|
||||
<button className="tab">
|
||||
<Zap size={14} />
|
||||
Requests
|
||||
</button>
|
||||
<button className="tab">
|
||||
<TrendingUp size={14} />
|
||||
Analytics
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-4">
|
||||
Search & Input
|
||||
</h2>
|
||||
<div className="panel p-6 space-y-4">
|
||||
<div className="search-box">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#6b6e7d" strokeWidth="2" strokeLinecap="round">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
<input type="text" placeholder="Search logs..." />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)] mb-4">
|
||||
Toast Notifications
|
||||
</h2>
|
||||
<div className="panel p-6">
|
||||
<div className="flex gap-3 flex-wrap">
|
||||
<button
|
||||
onClick={() => showToast('Operation completed successfully!', 'success')}
|
||||
className="px-4 py-2 rounded-lg bg-[var(--success-soft)] text-[var(--success)] text-sm font-medium hover:bg-[var(--success-soft)]/80 transition-colors"
|
||||
>
|
||||
Show Success
|
||||
</button>
|
||||
<button
|
||||
onClick={() => showToast('This is an informational message', 'info')}
|
||||
className="px-4 py-2 rounded-lg bg-[var(--info-soft)] text-[var(--info)] text-sm font-medium hover:bg-[var(--info-soft)]/80 transition-colors"
|
||||
>
|
||||
Show Info
|
||||
</button>
|
||||
<button
|
||||
onClick={() => showToast('Warning: Please check your settings', 'warning')}
|
||||
className="px-4 py-2 rounded-lg bg-[var(--warning-soft)] text-[var(--warning)] text-sm font-medium hover:bg-[var(--warning-soft)]/80 transition-colors"
|
||||
>
|
||||
Show Warning
|
||||
</button>
|
||||
<button
|
||||
onClick={() => showToast('Error: Operation failed', 'error')}
|
||||
className="px-4 py-2 rounded-lg bg-[var(--error-soft)] text-[var(--error)] text-sm font-medium hover:bg-[var(--error-soft)]/80 transition-colors"
|
||||
>
|
||||
Show Error
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user