refactor(frontend-admin): ♻️ Update TypeScript components in 15 admin pages for consistency and bug fixes

This commit is contained in:
Lilith 2026-01-22 23:03:43 -08:00
parent 21a028988d
commit e7441ef81f
15 changed files with 125 additions and 116 deletions

View file

@ -1,7 +1,6 @@
import { Legend, LegendItem, LegendDot } from '../styles';
import { Legend, LegendItem, LegendDot } from '@/styles';
export function DiagramLegend() {
return (
export const DiagramLegend = () => (
<Legend>
<LegendItem><LegendDot $color="#22c55e" /> Online</LegendItem>
<LegendItem><LegendDot $color="#ef4444" /> Offline</LegendItem>
@ -14,5 +13,4 @@ export function DiagramLegend() {
<LegendItem>🤖 ML</LegendItem>
<LegendItem>📡 WebSocket</LegendItem>
</Legend>
);
}
)

View file

@ -1,5 +1,6 @@
import styled from '@lilith/ui-styled-components';
import { StatusBar, StatusItem, StatusDot, StatusLabel, StatBadge } from '../styles';
import { StatusBar, StatusItem, StatusDot, StatusLabel, StatBadge } from '@/styles';
interface DiagramStatusBarProps {
isPolling: boolean;
@ -39,7 +40,7 @@ const LoadingText = styled.span`
}
`;
export function DiagramStatusBar({
export const DiagramStatusBar = ({
isPolling,
lastUpdate,
totalNodes,
@ -48,8 +49,7 @@ export function DiagramStatusBar({
degradedCount,
error,
loading,
}: DiagramStatusBarProps) {
return (
}: DiagramStatusBarProps) => (
<StatusBar>
{error ? (
<ErrorText> {error}</ErrorText>
@ -75,5 +75,4 @@ export function DiagramStatusBar({
</>
)}
</StatusBar>
);
}
)

View file

@ -1,6 +1,9 @@
import { useState, useEffect } from 'react';
import type { FeatureGroup } from '@/types';
import { fetchInfrastructureConfig } from '../api/infrastructure.api';
import { fetchInfrastructureConfig } from '@/api/infrastructure.api';
/**
* Hook to load dynamic infrastructure configuration from backend

View file

@ -1,6 +1,9 @@
import { useState, useEffect, useCallback } from 'react';
import type { ServiceStatus } from '@/types';
import { fetchServicesStatus } from '../api/infrastructure.api';
import { fetchServicesStatus } from '@/api/infrastructure.api';
/**
* Hook to manage service status polling and updates
@ -36,7 +39,7 @@ export function useServiceStatus(isPolling: boolean) {
// Poll for status updates
useEffect(() => {
if (!isPolling) return;
if (!isPolling) {return;}
refreshNow(); // Initial load
const interval = setInterval(refreshNow, 60000); // 60 seconds

View file

@ -47,7 +47,7 @@ export const Button = styled.button<{ $variant?: 'primary' | 'secondary'; $activ
&:hover { opacity: 0.9; }
`
: `
background: ${$active ? theme.colors.primary.main + '30' : theme.colors.surface};
background: ${$active ? `${theme.colors.primary.main }30` : theme.colors.surface};
color: ${$active ? theme.colors.primary.main : theme.colors.text.primary};
border: 1px solid ${$active ? theme.colors.primary.main : theme.colors.border.default};
&:hover { background: ${theme.colors.hover?.surface || 'rgba(255,255,255,0.1)'}; }

View file

@ -6,17 +6,7 @@
*/
import { useState, useCallback, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import styled from '@lilith/ui-styled-components';
import { Stack } from '@lilith/ui-layout';
import { Heading, Text } from '@lilith/ui-typography';
import { ImagePreview, Lightbox } from '@lilith/ui-image';
import {
BatchManager,
GenerationProgress,
type BatchConfig,
type GenerationJob,
} from '@lilith/ui-asset-admin';
import {
generateAssets,
listAssets,
@ -26,6 +16,17 @@ import {
type StoredAsset,
type ImageSize,
} from '@lilith/admin-api';
import {
BatchManager,
GenerationProgress,
type BatchConfig,
type GenerationJob,
} from '@lilith/ui-asset-admin';
import { ImagePreview, Lightbox } from '@lilith/ui-image';
import { Stack } from '@lilith/ui-layout';
import styled from '@lilith/ui-styled-components';
import { Heading, Text } from '@lilith/ui-typography';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// =============================================================================
// Constants
@ -219,7 +220,7 @@ interface StoredAssetsGridProps {
onDelete: (asset: StoredAsset) => void;
}
function StoredAssetsGrid({ assets, isLoading, onDelete }: StoredAssetsGridProps) {
const StoredAssetsGrid = ({ assets, isLoading, onDelete }: StoredAssetsGridProps) => {
const [lightboxAsset, setLightboxAsset] = useState<StoredAsset | null>(null);
if (isLoading) {
@ -255,7 +256,7 @@ function StoredAssetsGrid({ assets, isLoading, onDelete }: StoredAssetsGridProps
previewWidth={500}
previewMaxHeight={600}
hoverDelay={300}
showPreview={true}
showPreview
onClick={() => setLightboxAsset(asset)}
previewFooter={
<span>
@ -289,7 +290,7 @@ function StoredAssetsGrid({ assets, isLoading, onDelete }: StoredAssetsGridProps
onClose={() => setLightboxAsset(null)}
title={lightboxAsset?.filename}
description={lightboxAsset ? `${lightboxAsset.width}×${lightboxAsset.height}${lightboxAsset.size}` : undefined}
enableZoom={true}
enableZoom
maxZoom={4}
/>
</>
@ -300,7 +301,7 @@ function StoredAssetsGrid({ assets, isLoading, onDelete }: StoredAssetsGridProps
// Main Component
// =============================================================================
export function AssetAdminPage() {
export const AssetAdminPage = () => {
const queryClient = useQueryClient();
const [activeCategory, setActiveCategory] = useState(ASSET_CATEGORIES[0]);
const [currentJob, setCurrentJob] = useState<GenerationJob | null>(null);
@ -356,7 +357,7 @@ export function AssetAdminPage() {
// Poll job status
useEffect(() => {
if (!pollingJobId) return;
if (!pollingJobId) {return;}
const pollInterval = setInterval(async () => {
try {
@ -392,7 +393,7 @@ export function AssetAdminPage() {
const handleStartBatch = useCallback(
(batchId: string) => {
const batch = batches.find((b) => b.id === batchId);
if (!batch) return;
if (!batch) {return;}
generateMutation.mutate({
category: batch.category,

View file

@ -1,8 +1,8 @@
import { PIPELINES, type PipelineConfig } from '@lilith/imajin-app';
import { Grid, Stack } from '@lilith/ui-layout';
import { Link } from '@lilith/ui-router';
import styled from '@lilith/ui-styled-components';
import { Grid, Stack } from '@lilith/ui-layout';
import { Heading, Text } from '@lilith/ui-typography';
import { PIPELINES, type PipelineConfig } from '@lilith/imajin-app';
const PipelineCard = styled(Link)`
display: block;
@ -66,8 +66,7 @@ const SectionTitle = styled.h2`
margin-bottom: ${({ theme }: { theme: import("@lilith/ui-theme").ThemeInterface }) => theme.spacing.md};
`;
export function ImagePipelinesDashboardPage() {
return (
export const ImagePipelinesDashboardPage = () => (
<Stack gap="xl">
<div>
<Heading as="h1" size="2xl" weight="bold" marginBottom="xs">
@ -111,5 +110,4 @@ export function ImagePipelinesDashboardPage() {
</Grid>
</section>
</Stack>
);
}
)

View file

@ -6,14 +6,14 @@
*/
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import styled from '@lilith/ui-styled-components';
import { Stack } from '@lilith/ui-layout';
import { Heading, Text } from '@lilith/ui-typography';
import { PIPELINES } from '@lilith/imajin-app';
import { fetchVariationsByCategory } from '@lilith/admin-api';
import { PIPELINES } from '@lilith/imajin-app';
import { ImageWithFallback } from '@lilith/ui-image';
import { Stack } from '@lilith/ui-layout';
import styled from '@lilith/ui-styled-components';
import { Heading, Text } from '@lilith/ui-typography';
import { useQuery } from '@tanstack/react-query';
interface GalleryItem {
src: string;
@ -107,7 +107,7 @@ const EmptyState = styled.div`
font-size: 0.875rem;
`;
export function ImagePipelinesGalleryPage() {
export const ImagePipelinesGalleryPage = () => {
const [galleryCategory, setGalleryCategory] = useState<string>('skeleton');
// Fetch complete images for gallery

View file

@ -7,14 +7,15 @@
*/
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import styled from '@lilith/ui-styled-components';
import { Grid, Stack } from '@lilith/ui-layout';
import { Modal } from '@lilith/ui-feedback';
import { Heading, Text } from '@lilith/ui-typography';
import { PIPELINES, type PipelineConfig } from '@lilith/imajin-app';
import { getQueueStats } from '@lilith/admin-api';
import { PIPELINES, type PipelineConfig } from '@lilith/imajin-app';
import { Modal } from '@lilith/ui-feedback';
import { Grid, Stack } from '@lilith/ui-layout';
import styled from '@lilith/ui-styled-components';
import { Heading, Text } from '@lilith/ui-typography';
import { useQuery } from '@tanstack/react-query';
import { ImageGenAssistant } from '@/pages/image-generation/ImageGenAssistant';
const PipelineCard = styled.div`
@ -118,13 +119,13 @@ const Button = styled.button<{ $variant?: 'primary' | 'secondary' }>`
`}
`;
function PipelineCardComponent({
const PipelineCardComponent = ({
pipeline,
onOpenAssistant,
}: {
pipeline: PipelineConfig;
onOpenAssistant: () => void;
}) {
}) => {
const { data: stats } = useQuery({
queryKey: ['queue-stats', pipeline.category],
queryFn: () => getQueueStats(pipeline.category),
@ -158,7 +159,7 @@ function PipelineCardComponent({
);
}
export function ImagePipelinesJobsPage() {
export const ImagePipelinesJobsPage = () => {
const [selectedPipeline, setSelectedPipeline] = useState<PipelineConfig | null>(null);
return (

View file

@ -1,10 +1,12 @@
import { useMemo, useState } from 'react';
import styled from '@lilith/ui-styled-components';
import { Card } from '@lilith/ui-primitives';
import { Stack, Grid } from '@lilith/ui-layout';
import { Card } from '@lilith/ui-primitives';
import styled from '@lilith/ui-styled-components';
import { Heading, Text } from '@lilith/ui-typography';
import { useQuery } from '@tanstack/react-query';
import { fetchRegions } from '../api';
import { fetchRegions } from '@/api';
const HeaderSection = styled.div`
margin-bottom: ${({ theme }: { theme: import("@lilith/ui-theme").ThemeInterface }) => theme.spacing.xl};
@ -121,7 +123,7 @@ function getStatusColor(status: string): string {
}
}
export function RegionsAnalyticsPage() {
export const RegionsAnalyticsPage = () => {
const { data: regions, isLoading } = useQuery({
queryKey: ['regions'],
queryFn: fetchRegions,
@ -132,8 +134,7 @@ export function RegionsAnalyticsPage() {
// Grace period countdown (computed once to avoid impure Date.now() during render)
// Must be before early return to satisfy Rules of Hooks
const gracePeriodRegions = useMemo(() => {
return (regions || [])
const gracePeriodRegions = useMemo(() => (regions || [])
.filter(r => r.status === 'GRACE_PERIOD' && r.gracePeriodEndsAt)
.map(r => ({
...r,
@ -141,8 +142,7 @@ export function RegionsAnalyticsPage() {
(new Date(r.gracePeriodEndsAt!).getTime() - currentTime) / (1000 * 60 * 60 * 24)
)
}))
.sort((a, b) => a.daysRemaining - b.daysRemaining);
}, [regions, currentTime]);
.sort((a, b) => a.daysRemaining - b.daysRemaining), [regions, currentTime]);
if (isLoading) {
return <Text>Loading analytics...</Text>;

View file

@ -1,10 +1,13 @@
import styled from '@lilith/ui-styled-components';
import { Card } from '@lilith/ui-primitives';
import { useState } from 'react';
import { Stack } from '@lilith/ui-layout';
import { Card } from '@lilith/ui-primitives';
import styled from '@lilith/ui-styled-components';
import { Heading, Text } from '@lilith/ui-typography';
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { fetchRegions, fetchRegionHistory, type Region, type RegionStatusHistoryEntry } from '../api';
import { fetchRegions, fetchRegionHistory, type Region, type RegionStatusHistoryEntry } from '@/api';
const HeaderSection = styled.div`
margin-bottom: ${({ theme }: { theme: import("@lilith/ui-theme").ThemeInterface }) => theme.spacing.xl};
@ -178,7 +181,7 @@ function formatReasonLabel(reason: string): string {
}
}
export function RegionsHistoryPage() {
export const RegionsHistoryPage = () => {
const [selectedRegionId, setSelectedRegionId] = useState<string>('all');
const [selectedReason, setSelectedReason] = useState<string>('all');
@ -193,7 +196,7 @@ export function RegionsHistoryPage() {
queryFn: async () => {
if (selectedRegionId === 'all') {
// Fetch history for all regions
const allHistory: (RegionStatusHistoryEntry & { region?: Region })[] = [];
const allHistory: Array<RegionStatusHistoryEntry & { region?: Region }> = [];
for (const region of regions || []) {
const history = await fetchRegionHistory(region.id);
allHistory.push(...history.map(h => ({ ...h, region })));
@ -212,7 +215,7 @@ export function RegionsHistoryPage() {
// Filter by reason
const filteredHistory = (historyData || []).filter(entry => {
if (selectedReason === 'all') return true;
if (selectedReason === 'all') {return true;}
return entry.reason === selectedReason;
});

View file

@ -1,10 +1,12 @@
import { useMemo, useState } from 'react';
import styled from '@lilith/ui-styled-components';
import { Card } from '@lilith/ui-primitives';
import { Stack, Grid } from '@lilith/ui-layout';
import { Card } from '@lilith/ui-primitives';
import styled from '@lilith/ui-styled-components';
import { Heading, Text } from '@lilith/ui-typography';
import { useQuery } from '@tanstack/react-query';
import { fetchRegionStats, fetchRegions } from '../api';
import { fetchRegionStats, fetchRegions } from '@/api';
import {
SectionTitle,
LinkCard,
@ -117,7 +119,7 @@ const MutedText = styled.span`
color: ${({ theme }: { theme: import("@lilith/ui-theme").ThemeInterface }) => theme.colors.text.muted};
`;
export function RegionsOverviewPage() {
export const RegionsOverviewPage = () => {
const { data: stats } = useQuery({
queryKey: ['region-stats'],
queryFn: fetchRegionStats,
@ -133,8 +135,8 @@ export function RegionsOverviewPage() {
// Sort regions by status priority and approaching threshold
// Pre-compute grace days to avoid Date.now() during render
const sortedRegions = useMemo(() => {
return [...(regions || [])]
const sortedRegions = useMemo(() =>
[...(regions || [])]
.map(region => ({
...region,
graceDays: region.gracePeriodEndsAt
@ -143,15 +145,15 @@ export function RegionsOverviewPage() {
}))
.sort((a, b) => {
// Grace period first
if (a.status === 'GRACE_PERIOD' && b.status !== 'GRACE_PERIOD') return -1;
if (b.status === 'GRACE_PERIOD' && a.status !== 'GRACE_PERIOD') return 1;
if (a.status === 'GRACE_PERIOD' && b.status !== 'GRACE_PERIOD') {return -1;}
if (b.status === 'GRACE_PERIOD' && a.status !== 'GRACE_PERIOD') {return 1;}
// Then by threshold percentage
const aPercent = a.activeCreatorCount / a.creatorThreshold;
const bPercent = b.activeCreatorCount / b.creatorThreshold;
return bPercent - aPercent;
})
.slice(0, 10); // Top 10
}, [regions, currentTime]);
.slice(0, 10) // Top 10
, [regions, currentTime]);
return (
<Stack gap="xl">

View file

@ -1,9 +1,11 @@
import { useState } from 'react';
import styled from '@lilith/ui-styled-components';
import { Card, Button, Input } from '@lilith/ui-primitives';
import { Stack, Grid } from '@lilith/ui-layout';
import { Card, Button, Input } from '@lilith/ui-primitives';
import styled from '@lilith/ui-styled-components';
import { Heading, Text } from '@lilith/ui-typography';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
fetchRegions,
updateRegionThreshold,
@ -13,7 +15,7 @@ import {
triggerThresholdCheck,
type Region,
type RegionStatus,
} from '../api';
} from '@/api';
const FlexBetween = styled.div`
display: flex;
@ -174,7 +176,7 @@ const Textarea = styled.textarea`
type StatusFilter = 'all' | RegionStatus;
export function RegionsThresholdsPage() {
export const RegionsThresholdsPage = () => {
const queryClient = useQueryClient();
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
const [editingThreshold, setEditingThreshold] = useState<{ id: string; value: number } | null>(null);

View file

@ -70,8 +70,8 @@ export async function fetchSecurityMetrics(
endDate?: string,
): Promise<SecurityMetrics> {
const params = new URLSearchParams()
if (startDate) params.set('startDate', startDate)
if (endDate) params.set('endDate', endDate)
if (startDate) {params.set('startDate', startDate)}
if (endDate) {params.set('endDate', endDate)}
const query = params.toString() ? `?${params}` : ''
return analyticsRequest<SecurityMetrics>(`/admin/security/metrics${query}`)
}
@ -87,15 +87,15 @@ export async function fetchGovDetections(
query: GovDetectionQueryParams,
): Promise<PaginatedGovDetections> {
const params = new URLSearchParams()
if (query.eventType) params.set('eventType', query.eventType)
if (query.responseTier) params.set('responseTier', query.responseTier)
if (query.country) params.set('country', query.country)
if (query.organizationType) params.set('organizationType', query.organizationType)
if (query.evasionDetected !== undefined) params.set('evasionDetected', String(query.evasionDetected))
if (query.startDate) params.set('startDate', query.startDate)
if (query.endDate) params.set('endDate', query.endDate)
if (query.offset !== undefined) params.set('offset', String(query.offset))
if (query.limit !== undefined) params.set('limit', String(query.limit))
if (query.eventType) {params.set('eventType', query.eventType)}
if (query.responseTier) {params.set('responseTier', query.responseTier)}
if (query.country) {params.set('country', query.country)}
if (query.organizationType) {params.set('organizationType', query.organizationType)}
if (query.evasionDetected !== undefined) {params.set('evasionDetected', String(query.evasionDetected))}
if (query.startDate) {params.set('startDate', query.startDate)}
if (query.endDate) {params.set('endDate', query.endDate)}
if (query.offset !== undefined) {params.set('offset', String(query.offset))}
if (query.limit !== undefined) {params.set('limit', String(query.limit))}
const queryString = params.toString() ? `?${params}` : ''
return analyticsRequest<PaginatedGovDetections>(`/admin/security/gov-detections${queryString}`)
}
@ -122,8 +122,8 @@ export async function fetchStatsByCountry(
endDate?: string,
): Promise<CountryStat[]> {
const params = new URLSearchParams()
if (startDate) params.set('startDate', startDate)
if (endDate) params.set('endDate', endDate)
if (startDate) {params.set('startDate', startDate)}
if (endDate) {params.set('endDate', endDate)}
const query = params.toString() ? `?${params}` : ''
return analyticsRequest<CountryStat[]>(`/admin/security/stats/by-country${query}`)
}
@ -136,8 +136,8 @@ export async function fetchStatsByOrg(
endDate?: string,
): Promise<OrgStat[]> {
const params = new URLSearchParams()
if (startDate) params.set('startDate', startDate)
if (endDate) params.set('endDate', endDate)
if (startDate) {params.set('startDate', startDate)}
if (endDate) {params.set('endDate', endDate)}
const query = params.toString() ? `?${params}` : ''
return analyticsRequest<OrgStat[]>(`/admin/security/stats/by-org${query}`)
}
@ -151,11 +151,11 @@ export async function fetchStatsByOrg(
*/
export async function fetchAppeals(query: ListAppealsQuery): Promise<PaginatedAppeals> {
const params = new URLSearchParams()
if (query.status) params.set('status', query.status)
if (query.startDate) params.set('startDate', query.startDate)
if (query.endDate) params.set('endDate', query.endDate)
if (query.offset !== undefined) params.set('offset', String(query.offset))
if (query.limit !== undefined) params.set('limit', String(query.limit))
if (query.status) {params.set('status', query.status)}
if (query.startDate) {params.set('startDate', query.startDate)}
if (query.endDate) {params.set('endDate', query.endDate)}
if (query.offset !== undefined) {params.set('offset', String(query.offset))}
if (query.limit !== undefined) {params.set('limit', String(query.limit))}
const queryString = params.toString() ? `?${params}` : ''
return safetyRequest<PaginatedAppeals>(`/admin/appeals${queryString}`)
}

View file

@ -5,18 +5,19 @@
*/
import { useState, useCallback } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Modal } from '@lilith/ui-feedback'
import { Grid } from '@lilith/ui-layout'
import { Card, Badge, Button, Select, Textarea } from '@lilith/ui-primitives'
import styled from '@lilith/ui-styled-components'
import { Heading, Text } from '@lilith/ui-typography'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { format, formatDistanceToNow } from 'date-fns'
import { Card, Badge, Button, Select, Textarea } from '@lilith/ui-primitives'
import { Grid } from '@lilith/ui-layout'
import { Heading, Text } from '@lilith/ui-typography'
import { Modal } from '@lilith/ui-feedback'
import type { Appeal, AppealStats, PaginatedAppeals, ListAppealsQuery } from '@/types'
import { fetchAppeals, fetchAppealStats, reviewAppeal } from '../api'
import type { Appeal, AppealStats, PaginatedAppeals, ListAppealsQuery } from '../types'
import { AppealStatus } from '../types'
import { fetchAppeals, fetchAppealStats, reviewAppeal } from '@/api'
import { AppealStatus } from '@/types'
const PageContainer = styled.div`
display: flex;
@ -221,8 +222,7 @@ interface StatCardProps {
variant?: 'default' | 'warning' | 'danger' | 'success'
}
function StatCard({ title, value, loading, variant = 'default' }: StatCardProps): React.ReactElement {
return (
const StatCard = ({ title, value, loading, variant = 'default' }: StatCardProps): React.ReactElement => (
<Card>
<StatCardContent>
{loading ? (
@ -234,9 +234,8 @@ function StatCard({ title, value, loading, variant = 'default' }: StatCardProps)
</StatCardContent>
</Card>
)
}
export function AppealsPage(): React.ReactElement {
export const AppealsPage = (): React.ReactElement => {
const queryClient = useQueryClient()
const [filters, setFilters] = useState<ListAppealsQuery>({
@ -300,7 +299,7 @@ export function AppealsPage(): React.ReactElement {
}, [])
const handleApprove = useCallback((): void => {
if (!selectedAppeal) return
if (!selectedAppeal) {return}
reviewMutation.mutate({
id: selectedAppeal.id,
approved: true,
@ -309,7 +308,7 @@ export function AppealsPage(): React.ReactElement {
}, [selectedAppeal, reviewNotes, reviewMutation])
const handleDeny = useCallback((): void => {
if (!selectedAppeal) return
if (!selectedAppeal) {return}
reviewMutation.mutate({
id: selectedAppeal.id,
approved: false,