feat(admin/frontend): add page components and dashboard utilities
Add page-level components for queue administration: - QueueDashboardPage: Overview of all queues with summary stats - QueueDetailPage: Detail view with job management - QueueGrid: Grid layout for queue cards - QueueSummaryBar: Aggregated statistics bar - useJobSearch: Hook for job search functionality Bump version to 1.3.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b788862c87
commit
4f1f4e4ed3
11 changed files with 673 additions and 3 deletions
123
admin/frontend/src/components/QueueGrid.tsx
Normal file
123
admin/frontend/src/components/QueueGrid.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import React from 'react';
|
||||
import type { QueueStats } from '../types';
|
||||
|
||||
export interface QueueGridProps {
|
||||
queues: QueueStats[];
|
||||
onQueueClick?: (queue: QueueStats) => void;
|
||||
onPause?: (queueName: string) => void;
|
||||
onResume?: (queueName: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grid display of queue cards with stats and actions.
|
||||
*/
|
||||
export function QueueGrid({
|
||||
queues,
|
||||
onQueueClick,
|
||||
onPause,
|
||||
onResume,
|
||||
className = '',
|
||||
}: QueueGridProps) {
|
||||
if (queues.length === 0) {
|
||||
return <div className="queue-grid-empty">No queues found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`queue-grid ${className}`}>
|
||||
{queues.map((queue) => (
|
||||
<div
|
||||
key={queue.name}
|
||||
className={`queue-grid-card ${onQueueClick ? 'queue-grid-card-clickable' : ''}`}
|
||||
onClick={onQueueClick ? () => onQueueClick(queue) : undefined}
|
||||
role={onQueueClick ? 'button' : undefined}
|
||||
tabIndex={onQueueClick ? 0 : undefined}
|
||||
onKeyDown={
|
||||
onQueueClick
|
||||
? (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onQueueClick(queue);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="queue-grid-card-header">
|
||||
<h3 className="queue-grid-card-name">{queue.name}</h3>
|
||||
<span
|
||||
className={`queue-grid-card-status ${
|
||||
queue.paused
|
||||
? 'queue-status-warning'
|
||||
: queue.failed > 0
|
||||
? 'queue-status-error'
|
||||
: 'queue-status-success'
|
||||
}`}
|
||||
>
|
||||
{queue.paused ? 'Paused' : queue.failed > 0 ? 'Has Failures' : 'Active'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="queue-grid-stats">
|
||||
<div className="queue-grid-stat">
|
||||
<div className="queue-grid-stat-value">{queue.waiting}</div>
|
||||
<div className="queue-grid-stat-label">Waiting</div>
|
||||
</div>
|
||||
<div className="queue-grid-stat">
|
||||
<div className="queue-grid-stat-value queue-stat-active">{queue.active}</div>
|
||||
<div className="queue-grid-stat-label">Active</div>
|
||||
</div>
|
||||
<div className="queue-grid-stat">
|
||||
<div
|
||||
className={`queue-grid-stat-value ${queue.failed > 0 ? 'queue-stat-failed' : ''}`}
|
||||
>
|
||||
{queue.failed}
|
||||
</div>
|
||||
<div className="queue-grid-stat-label">Failed</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="queue-grid-stats">
|
||||
<div className="queue-grid-stat">
|
||||
<div className="queue-grid-stat-value queue-stat-completed">{queue.completed}</div>
|
||||
<div className="queue-grid-stat-label">Completed</div>
|
||||
</div>
|
||||
<div className="queue-grid-stat">
|
||||
<div className="queue-grid-stat-value">{queue.delayed}</div>
|
||||
<div className="queue-grid-stat-label">Delayed</div>
|
||||
</div>
|
||||
<div className="queue-grid-stat" />
|
||||
</div>
|
||||
|
||||
{(onPause || onResume) && (
|
||||
<div
|
||||
className="queue-grid-actions"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="group"
|
||||
aria-label="Queue actions"
|
||||
>
|
||||
{queue.paused && onResume && (
|
||||
<button
|
||||
className="queue-action-button queue-action-primary"
|
||||
onClick={() => onResume(queue.name)}
|
||||
type="button"
|
||||
>
|
||||
Resume
|
||||
</button>
|
||||
)}
|
||||
{!queue.paused && onPause && (
|
||||
<button
|
||||
className="queue-action-button queue-action-secondary"
|
||||
onClick={() => onPause(queue.name)}
|
||||
type="button"
|
||||
>
|
||||
Pause
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
admin/frontend/src/components/QueueSummaryBar.tsx
Normal file
41
admin/frontend/src/components/QueueSummaryBar.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import React from 'react';
|
||||
import type { QueueSummaryStats } from '../types';
|
||||
|
||||
export interface QueueSummaryBarProps {
|
||||
summary: QueueSummaryStats;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary bar showing aggregated queue statistics.
|
||||
*/
|
||||
export function QueueSummaryBar({ summary, className = '' }: QueueSummaryBarProps) {
|
||||
const stats = [
|
||||
{ label: 'Queues', value: summary.queues.length, colorClass: '' },
|
||||
{ label: 'Waiting', value: summary.totalWaiting, colorClass: '' },
|
||||
{ label: 'Active', value: summary.totalActive, colorClass: 'queue-summary-stat-active' },
|
||||
{
|
||||
label: 'Completed',
|
||||
value: summary.totalCompleted,
|
||||
colorClass: 'queue-summary-stat-completed',
|
||||
},
|
||||
{
|
||||
label: 'Failed',
|
||||
value: summary.totalFailed,
|
||||
colorClass: summary.totalFailed > 0 ? 'queue-summary-stat-failed' : '',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={`queue-summary-bar ${className}`}>
|
||||
{stats.map((stat) => (
|
||||
<div key={stat.label} className="queue-summary-stat">
|
||||
<div className={`queue-summary-stat-value ${stat.colorClass}`}>
|
||||
{stat.value.toLocaleString()}
|
||||
</div>
|
||||
<div className="queue-summary-stat-label">{stat.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -34,3 +34,9 @@ export type { ServiceFilterProps, ServiceOption } from './ServiceFilter';
|
|||
|
||||
export { JobDetailPanel } from './JobDetailPanel';
|
||||
export type { JobDetailPanelProps } from './JobDetailPanel';
|
||||
|
||||
export { QueueGrid } from './QueueGrid';
|
||||
export type { QueueGridProps } from './QueueGrid';
|
||||
|
||||
export { QueueSummaryBar } from './QueueSummaryBar';
|
||||
export type { QueueSummaryBarProps } from './QueueSummaryBar';
|
||||
|
|
|
|||
|
|
@ -8,3 +8,4 @@ export { useJobs } from './useJobs';
|
|||
export { useJobDetail } from './useJobDetail';
|
||||
export { useQueueControl } from './useQueueControl';
|
||||
export { useBulkJobActions } from './useBulkJobActions';
|
||||
export { useJobSearch, type UseJobSearchOptions } from './useJobSearch';
|
||||
|
|
|
|||
59
admin/frontend/src/hooks/useJobSearch.ts
Normal file
59
admin/frontend/src/hooks/useJobSearch.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { JobDetails } from '../types';
|
||||
|
||||
const DEFAULT_API_URL = '/api/admin/queues';
|
||||
|
||||
export interface UseJobSearchOptions {
|
||||
/** API base URL */
|
||||
apiUrl?: string;
|
||||
/** Search query (minimum 2 characters) */
|
||||
query: string;
|
||||
/** Optional queue name to scope search */
|
||||
queueName?: string;
|
||||
/** Enable/disable the query */
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
async function searchJobs(
|
||||
apiUrl: string,
|
||||
query: string,
|
||||
queueName?: string
|
||||
): Promise<JobDetails[]> {
|
||||
const params = new URLSearchParams({ q: query });
|
||||
if (queueName) params.set('queue', queueName);
|
||||
|
||||
const response = await fetch(`${apiUrl}/search?${params}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Search failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to search jobs across queues.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { data: results, isLoading } = useJobSearch({
|
||||
* query: searchTerm,
|
||||
* queueName: 'analytics', // optional
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function useJobSearch(options: UseJobSearchOptions) {
|
||||
const {
|
||||
apiUrl = DEFAULT_API_URL,
|
||||
query,
|
||||
queueName,
|
||||
enabled = true,
|
||||
} = options;
|
||||
|
||||
return useQuery<JobDetails[]>({
|
||||
queryKey: ['jobs', 'search', query, queueName],
|
||||
queryFn: () => searchJobs(apiUrl, query, queueName),
|
||||
enabled: enabled && query.length >= 2,
|
||||
staleTime: 5000,
|
||||
});
|
||||
}
|
||||
|
|
@ -12,8 +12,11 @@ export {
|
|||
useJobDetail,
|
||||
useQueueControl,
|
||||
useBulkJobActions,
|
||||
useJobSearch,
|
||||
} from './hooks';
|
||||
|
||||
export type { UseJobSearchOptions } from './hooks';
|
||||
|
||||
// Export all components
|
||||
export {
|
||||
QueueDashboard,
|
||||
|
|
@ -27,6 +30,8 @@ export {
|
|||
SearchBar,
|
||||
ServiceFilter,
|
||||
JobDetailPanel,
|
||||
QueueGrid,
|
||||
QueueSummaryBar,
|
||||
} from './components';
|
||||
|
||||
export type {
|
||||
|
|
@ -42,8 +47,15 @@ export type {
|
|||
ServiceFilterProps,
|
||||
ServiceOption,
|
||||
JobDetailPanelProps,
|
||||
QueueGridProps,
|
||||
QueueSummaryBarProps,
|
||||
} from './components';
|
||||
|
||||
// Export all pages
|
||||
export { QueueDashboardPage, QueueDetailPage } from './pages';
|
||||
|
||||
export type { QueueDashboardPageProps, QueueDetailPageProps } from './pages';
|
||||
|
||||
// Export all types
|
||||
export type {
|
||||
QueueSummary,
|
||||
|
|
@ -64,4 +76,10 @@ export type {
|
|||
BulkOperationResult,
|
||||
BulkOperationOptions,
|
||||
CleanByAgeOptions,
|
||||
// Dashboard page types
|
||||
QueueStats,
|
||||
QueueSummaryStats,
|
||||
JobStatus,
|
||||
ServiceInfo,
|
||||
QueueAdminConfig,
|
||||
} from './types';
|
||||
|
|
|
|||
151
admin/frontend/src/pages/QueueDashboardPage.tsx
Normal file
151
admin/frontend/src/pages/QueueDashboardPage.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useQueueMetrics } from '../hooks/useQueueMetrics';
|
||||
import { useQueueControl } from '../hooks/useQueueControl';
|
||||
import { QueueGrid } from '../components/QueueGrid';
|
||||
import { QueueSummaryBar } from '../components/QueueSummaryBar';
|
||||
import type { QueueStats, QueueSummaryStats } from '../types';
|
||||
|
||||
export interface QueueDashboardPageProps {
|
||||
/** Base API URL for queue endpoints */
|
||||
apiUrl?: string;
|
||||
/** Refresh interval in milliseconds */
|
||||
refreshInterval?: number;
|
||||
/** Custom class name */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard page showing all queues with summary statistics.
|
||||
*
|
||||
* Uses react-router-dom for navigation to queue details.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Route path="/queues" element={<QueueDashboardPage apiUrl="/api/admin/queues" />} />
|
||||
* ```
|
||||
*/
|
||||
export function QueueDashboardPage({
|
||||
apiUrl = '/api/admin/queues',
|
||||
refreshInterval = 5000,
|
||||
className = '',
|
||||
}: QueueDashboardPageProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { metrics, isLoading, error } = useQueueMetrics({
|
||||
apiUrl,
|
||||
refreshInterval,
|
||||
});
|
||||
|
||||
const { pauseQueue, resumeQueue, isPending } = useQueueControl({
|
||||
apiUrl,
|
||||
});
|
||||
|
||||
// Transform metrics to QueueStats format for grid display
|
||||
const queueStats: QueueStats[] = useMemo(
|
||||
() =>
|
||||
metrics.map((m) => ({
|
||||
name: m.name,
|
||||
waiting: m.counts.waiting,
|
||||
active: m.counts.active,
|
||||
completed: m.counts.completed,
|
||||
failed: m.counts.failed,
|
||||
delayed: m.counts.delayed,
|
||||
paused: m.isPaused,
|
||||
})),
|
||||
[metrics]
|
||||
);
|
||||
|
||||
// Calculate summary stats
|
||||
const summary: QueueSummaryStats = useMemo(
|
||||
() => ({
|
||||
queues: queueStats,
|
||||
totalWaiting: queueStats.reduce((sum, q) => sum + q.waiting, 0),
|
||||
totalActive: queueStats.reduce((sum, q) => sum + q.active, 0),
|
||||
totalCompleted: queueStats.reduce((sum, q) => sum + q.completed, 0),
|
||||
totalFailed: queueStats.reduce((sum, q) => sum + q.failed, 0),
|
||||
}),
|
||||
[queueStats]
|
||||
);
|
||||
|
||||
const handleQueueClick = useCallback(
|
||||
(queue: QueueStats) => {
|
||||
navigate(`/queues/${encodeURIComponent(queue.name)}`);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const handlePause = useCallback(
|
||||
(queueName: string) => {
|
||||
pauseQueue({ queueName });
|
||||
},
|
||||
[pauseQueue]
|
||||
);
|
||||
|
||||
const handleResume = useCallback(
|
||||
(queueName: string) => {
|
||||
resumeQueue({ queueName });
|
||||
},
|
||||
[resumeQueue]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={`queue-dashboard-page ${className}`}>
|
||||
<div className="queue-dashboard-loading">
|
||||
<div className="queue-loading-spinner" />
|
||||
<p>Loading queues...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={`queue-dashboard-page ${className}`}>
|
||||
<div className="queue-dashboard-error">
|
||||
<h3>Error Loading Queue Data</h3>
|
||||
<p>{error.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (queueStats.length === 0) {
|
||||
return (
|
||||
<div className={`queue-dashboard-page ${className}`}>
|
||||
<div className="queue-dashboard-header">
|
||||
<h1>Job Queues</h1>
|
||||
<p>Monitor and manage background job queues</p>
|
||||
</div>
|
||||
<div className="queue-dashboard-empty">
|
||||
<p>No queue data available</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`queue-dashboard-page ${className}`}>
|
||||
<div className="queue-dashboard-header">
|
||||
<h1>Job Queues</h1>
|
||||
<p>Monitor and manage background job queues</p>
|
||||
</div>
|
||||
|
||||
<QueueSummaryBar summary={summary} className="queue-dashboard-summary" />
|
||||
|
||||
<QueueGrid
|
||||
queues={queueStats}
|
||||
onQueueClick={handleQueueClick}
|
||||
onPause={!isPending ? handlePause : undefined}
|
||||
onResume={!isPending ? handleResume : undefined}
|
||||
/>
|
||||
|
||||
<div className="queue-dashboard-footer">
|
||||
Auto-refreshes every {refreshInterval / 1000}s · Last updated:{' '}
|
||||
{new Date().toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
182
admin/frontend/src/pages/QueueDetailPage.tsx
Normal file
182
admin/frontend/src/pages/QueueDetailPage.tsx
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
|
||||
import { useQueueMetrics } from '../hooks/useQueueMetrics';
|
||||
import { useQueueControl } from '../hooks/useQueueControl';
|
||||
import { useBulkJobActions } from '../hooks/useBulkJobActions';
|
||||
|
||||
import { JobsTable } from '../components/JobsTable';
|
||||
import { BulkActionsBar } from '../components/BulkActionsBar';
|
||||
|
||||
import type { QueueStats } from '../types';
|
||||
|
||||
export interface QueueDetailPageProps {
|
||||
/** Base API URL for queue endpoints */
|
||||
apiUrl?: string;
|
||||
/** Custom class name */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detail page for a specific queue showing jobs with filtering.
|
||||
*
|
||||
* Uses react-router-dom useParams to get queueName from URL.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Route path="/queues/:queueName" element={<QueueDetailPage apiUrl="/api/admin/queues" />} />
|
||||
* ```
|
||||
*/
|
||||
export function QueueDetailPage({
|
||||
apiUrl = '/api/admin/queues',
|
||||
className = '',
|
||||
}: QueueDetailPageProps) {
|
||||
const { queueName } = useParams<{ queueName: string }>();
|
||||
|
||||
// Queries
|
||||
const { metrics, refetch } = useQueueMetrics({
|
||||
apiUrl,
|
||||
refreshInterval: 5000,
|
||||
enabled: !!queueName,
|
||||
});
|
||||
|
||||
// Find current queue stats from metrics
|
||||
const queueStats: QueueStats | null = useMemo(() => {
|
||||
const found = metrics.find((m) => m.name === queueName);
|
||||
if (!found) return null;
|
||||
return {
|
||||
name: found.name,
|
||||
waiting: found.counts.waiting,
|
||||
active: found.counts.active,
|
||||
completed: found.counts.completed,
|
||||
failed: found.counts.failed,
|
||||
delayed: found.counts.delayed,
|
||||
paused: found.isPaused,
|
||||
};
|
||||
}, [metrics, queueName]);
|
||||
|
||||
// Mutations
|
||||
const { retryJob } = useQueueControl({
|
||||
apiUrl,
|
||||
onSuccess: () => {
|
||||
refetch();
|
||||
},
|
||||
});
|
||||
|
||||
const { retryAllFailed: bulkRetryAllFailed, cancelPending, cleanByAge } = useBulkJobActions({
|
||||
apiUrl,
|
||||
});
|
||||
|
||||
// Bulk action handlers
|
||||
const handleRetryAllFailed = useCallback(async () => {
|
||||
if (queueName) {
|
||||
await bulkRetryAllFailed.mutateAsync({ queueName });
|
||||
}
|
||||
}, [queueName, bulkRetryAllFailed]);
|
||||
|
||||
const handleCancelPending = useCallback(async () => {
|
||||
if (queueName) {
|
||||
await cancelPending.mutateAsync({ queueName });
|
||||
}
|
||||
}, [queueName, cancelPending]);
|
||||
|
||||
const handleCleanCompleted = useCallback(
|
||||
async (maxAgeMs: number) => {
|
||||
if (queueName) {
|
||||
await cleanByAge.mutateAsync({
|
||||
queueName,
|
||||
options: { status: 'completed', maxAgeMs },
|
||||
});
|
||||
}
|
||||
},
|
||||
[queueName, cleanByAge]
|
||||
);
|
||||
|
||||
const handleCleanFailed = useCallback(
|
||||
async (maxAgeMs: number) => {
|
||||
if (queueName) {
|
||||
await cleanByAge.mutateAsync({
|
||||
queueName,
|
||||
options: { status: 'failed', maxAgeMs },
|
||||
});
|
||||
}
|
||||
},
|
||||
[queueName, cleanByAge]
|
||||
);
|
||||
|
||||
const handleRetryJob = useCallback(
|
||||
(jobId: string) => {
|
||||
if (queueName) {
|
||||
retryJob({ queueName, jobId });
|
||||
}
|
||||
},
|
||||
[queueName, retryJob]
|
||||
);
|
||||
|
||||
if (!queueName) {
|
||||
return (
|
||||
<div className={`queue-detail-page ${className}`}>
|
||||
<div className="queue-detail-empty">
|
||||
<p>No queue selected</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`queue-detail-page ${className}`}>
|
||||
{/* Breadcrumb */}
|
||||
<nav className="queue-detail-breadcrumb">
|
||||
<Link to="/queues" className="queue-breadcrumb-link">
|
||||
Queues
|
||||
</Link>
|
||||
<span className="queue-breadcrumb-separator">/</span>
|
||||
<span className="queue-breadcrumb-current">{queueName}</span>
|
||||
</nav>
|
||||
|
||||
{/* Header */}
|
||||
<div className="queue-detail-header">
|
||||
<div className="queue-detail-title-section">
|
||||
<h1 className="queue-detail-title">{queueName}</h1>
|
||||
{queueStats && (
|
||||
<p className="queue-detail-status">
|
||||
<span
|
||||
className={
|
||||
queueStats.paused ? 'queue-status-warning' : 'queue-status-success'
|
||||
}
|
||||
>
|
||||
{queueStats.paused ? 'Paused' : 'Active'}
|
||||
</span>
|
||||
{' · '}
|
||||
{queueStats.waiting + queueStats.active + queueStats.delayed} pending jobs
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bulk Actions */}
|
||||
{queueStats && (
|
||||
<div className="queue-detail-bulk-actions">
|
||||
<BulkActionsBar
|
||||
queueName={queueName}
|
||||
failedCount={queueStats.failed}
|
||||
pendingCount={queueStats.waiting + queueStats.delayed}
|
||||
onRetryAllFailed={handleRetryAllFailed}
|
||||
onCancelPending={handleCancelPending}
|
||||
onCleanCompleted={handleCleanCompleted}
|
||||
onCleanFailed={handleCleanFailed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Jobs Table (includes its own filters and pagination) */}
|
||||
<div className="queue-detail-table">
|
||||
<JobsTable
|
||||
queueName={queueName}
|
||||
apiUrl={apiUrl}
|
||||
onRetryJob={handleRetryJob}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
9
admin/frontend/src/pages/index.ts
Normal file
9
admin/frontend/src/pages/index.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* Queue Admin Page Components
|
||||
*/
|
||||
|
||||
export { QueueDashboardPage } from './QueueDashboardPage';
|
||||
export type { QueueDashboardPageProps } from './QueueDashboardPage';
|
||||
|
||||
export { QueueDetailPage } from './QueueDetailPage';
|
||||
export type { QueueDetailPageProps } from './QueueDetailPage';
|
||||
|
|
@ -216,3 +216,53 @@ export interface RetryAllFailedOptions {
|
|||
queueName: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Queue Stats Types (for dashboard pages)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Per-queue statistics for dashboard display.
|
||||
*/
|
||||
export interface QueueStats {
|
||||
name: string;
|
||||
waiting: number;
|
||||
active: number;
|
||||
completed: number;
|
||||
failed: number;
|
||||
delayed: number;
|
||||
paused: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregated queue summary with totals.
|
||||
*/
|
||||
export interface QueueSummaryStats {
|
||||
queues: QueueStats[];
|
||||
totalWaiting: number;
|
||||
totalActive: number;
|
||||
totalCompleted: number;
|
||||
totalFailed: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Job status filter type.
|
||||
*/
|
||||
export type JobStatus = 'waiting' | 'active' | 'completed' | 'failed' | 'delayed';
|
||||
|
||||
/**
|
||||
* Service configuration for filtering.
|
||||
*/
|
||||
export interface ServiceInfo {
|
||||
value: string;
|
||||
label: string;
|
||||
queueName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for queue admin pages.
|
||||
*/
|
||||
export interface QueueAdminConfig {
|
||||
apiUrl: string;
|
||||
refreshInterval?: number;
|
||||
}
|
||||
|
|
|
|||
36
package.json
36
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@lilith/queue",
|
||||
"version": "1.2.3",
|
||||
"version": "1.3.0",
|
||||
"description": "Job queue ecosystem: core types, NestJS integration, ML batching, reporting, and admin dashboard",
|
||||
"exports": {
|
||||
"./core": {
|
||||
|
|
@ -136,6 +136,10 @@
|
|||
"typeorm": "^0.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@lilith/ui-data": "^1.0.0",
|
||||
"@lilith/ui-feedback": "^1.0.0",
|
||||
"@lilith/ui-layout": "^1.0.0",
|
||||
"@lilith/ui-primitives": "^1.0.0",
|
||||
"@nestjs/bullmq": "^10.0.0 || ^11.0.0",
|
||||
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||
"@nestjs/core": "^10.0.0 || ^11.0.0",
|
||||
|
|
@ -145,9 +149,11 @@
|
|||
"@nestjs/websockets": "^10.0.0 || ^11.0.0",
|
||||
"cron": "^3.0.0",
|
||||
"ioredis": "^5.0.0",
|
||||
"react": "^18.0.0",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-router-dom": "^6.0.0 || ^7.0.0",
|
||||
"socket.io": "^4.0.0",
|
||||
"socket.io-client": "^4.7.0"
|
||||
"socket.io-client": "^4.7.0",
|
||||
"styled-components": "^6.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@nestjs/bullmq": {
|
||||
|
|
@ -171,6 +177,18 @@
|
|||
"@nestjs/websockets": {
|
||||
"optional": true
|
||||
},
|
||||
"@lilith/ui-primitives": {
|
||||
"optional": true
|
||||
},
|
||||
"@lilith/ui-data": {
|
||||
"optional": true
|
||||
},
|
||||
"@lilith/ui-feedback": {
|
||||
"optional": true
|
||||
},
|
||||
"@lilith/ui-layout": {
|
||||
"optional": true
|
||||
},
|
||||
"cron": {
|
||||
"optional": true
|
||||
},
|
||||
|
|
@ -180,14 +198,24 @@
|
|||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-router-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"socket.io": {
|
||||
"optional": true
|
||||
},
|
||||
"socket.io-client": {
|
||||
"optional": true
|
||||
},
|
||||
"styled-components": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lilith/ui-data": "^1.1.0",
|
||||
"@lilith/ui-feedback": "^1.1.0",
|
||||
"@lilith/ui-layout": "^1.1.0",
|
||||
"@lilith/ui-primitives": "^1.1.0",
|
||||
"@nestjs/bullmq": "^11.0.4",
|
||||
"@nestjs/common": "^11.1.11",
|
||||
"@nestjs/core": "^11.1.11",
|
||||
|
|
@ -206,10 +234,12 @@
|
|||
"ioredis": "^5.0.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^7.11.0",
|
||||
"reflect-metadata": "^0.1.14",
|
||||
"rxjs": "^7.8.1",
|
||||
"socket.io": "^4.8.3",
|
||||
"socket.io-client": "^4.7.0",
|
||||
"styled-components": "^6.1.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue