mongo-scheduler-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +1 -0
- package/README.md +122 -0
- package/eslint.config.js +23 -0
- package/index.html +13 -0
- package/package.json +44 -0
- package/postcss.config.js +6 -0
- package/public/dashboard-preview.png +0 -0
- package/public/favicon.svg +12 -0
- package/src/App.css +42 -0
- package/src/App.tsx +233 -0
- package/src/components/JobDetails.tsx +124 -0
- package/src/components/JobFilters.tsx +59 -0
- package/src/components/JobFormModal.tsx +372 -0
- package/src/components/JobList.tsx +171 -0
- package/src/components/Pagination.tsx +107 -0
- package/src/components/StatsCards.tsx +52 -0
- package/src/index.css +29 -0
- package/src/main.tsx +10 -0
- package/src/services/api.ts +58 -0
- package/src/types/job.ts +43 -0
- package/src/utils/helpers.ts +31 -0
- package/tailwind.config.js +8 -0
- package/tsconfig.app.json +28 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +7 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Activity, CheckCircle, XCircle, Clock, Loader, Ban } from 'lucide-react';
|
|
2
|
+
import type { JobStats } from '../types/job';
|
|
3
|
+
|
|
4
|
+
interface StatsCardProps {
|
|
5
|
+
stats: JobStats | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function StatsCards({ stats }: StatsCardProps) {
|
|
9
|
+
if (!stats) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
|
|
12
|
+
{[...Array(6)].map((_, i) => (
|
|
13
|
+
<div key={i} className="bg-gray-800/50 rounded-lg p-4 animate-pulse">
|
|
14
|
+
<div className="h-4 bg-gray-700 rounded w-16 mb-2"></div>
|
|
15
|
+
<div className="h-8 bg-gray-700 rounded w-12"></div>
|
|
16
|
+
</div>
|
|
17
|
+
))}
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const cards = [
|
|
23
|
+
{ label: 'Total', value: stats.total, icon: Activity, color: 'text-purple-400' },
|
|
24
|
+
{ label: 'Pending', value: stats.pending, icon: Clock, color: 'text-blue-400' },
|
|
25
|
+
{ label: 'Running', value: stats.running, icon: Loader, color: 'text-yellow-400' },
|
|
26
|
+
{ label: 'Completed', value: stats.completed, icon: CheckCircle, color: 'text-green-400' },
|
|
27
|
+
{ label: 'Failed', value: stats.failed, icon: XCircle, color: 'text-red-400' },
|
|
28
|
+
{ label: 'Cancelled', value: stats.cancelled, icon: Ban, color: 'text-gray-400' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
|
|
33
|
+
{cards.map((card) => {
|
|
34
|
+
const Icon = card.icon;
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
key={card.label}
|
|
38
|
+
className="bg-gradient-to-br from-gray-800/80 to-gray-900/80 backdrop-blur-sm rounded-lg p-4 border border-gray-700/50 hover:border-gray-600/50 transition-all duration-200"
|
|
39
|
+
>
|
|
40
|
+
<div className="flex items-center justify-between mb-2">
|
|
41
|
+
<span className="text-sm text-gray-400">{card.label}</span>
|
|
42
|
+
<Icon className={`w-4 h-4 ${card.color}`} />
|
|
43
|
+
</div>
|
|
44
|
+
<div className={`text-2xl font-bold ${card.color}`}>
|
|
45
|
+
{card.value.toLocaleString()}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
})}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
package/src/index.css
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
7
|
+
line-height: 1.5;
|
|
8
|
+
font-weight: 400;
|
|
9
|
+
|
|
10
|
+
color-scheme: dark;
|
|
11
|
+
|
|
12
|
+
background-color: #0a0a0a;
|
|
13
|
+
color: #ffffff;
|
|
14
|
+
|
|
15
|
+
font-synthesis: none;
|
|
16
|
+
text-rendering: optimizeLegibility;
|
|
17
|
+
-webkit-font-smoothing: antialiased;
|
|
18
|
+
-moz-osx-font-smoothing: grayscale;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
* {
|
|
22
|
+
margin: 0;
|
|
23
|
+
padding: 0;
|
|
24
|
+
box-sizing: border-box;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
body {
|
|
28
|
+
min-height: 100vh;
|
|
29
|
+
}
|
package/src/main.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import type { Job, JobQuery, JobStats } from "../types/job";
|
|
3
|
+
|
|
4
|
+
const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:3000";
|
|
5
|
+
|
|
6
|
+
const api = axios.create({
|
|
7
|
+
baseURL: API_BASE_URL,
|
|
8
|
+
headers: {
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const jobsApi = {
|
|
14
|
+
// Get all jobs with optional filters
|
|
15
|
+
getJobs: async (query?: JobQuery): Promise<Job[]> => {
|
|
16
|
+
const response = await api.get("/jobs", { params: query });
|
|
17
|
+
return response.data;
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
// Get job by ID
|
|
21
|
+
getJob: async (id: string): Promise<Job> => {
|
|
22
|
+
const response = await api.get(`/jobs/${id}`);
|
|
23
|
+
return response.data;
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// Get job statistics
|
|
27
|
+
getStats: async (): Promise<JobStats> => {
|
|
28
|
+
const response = await api.get("/jobs/stats");
|
|
29
|
+
return response.data;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// Cancel a job
|
|
33
|
+
cancelJob: async (id: string): Promise<void> => {
|
|
34
|
+
await api.post(`/jobs/${id}/cancel`);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// Retry a failed job
|
|
38
|
+
retryJob: async (id: string): Promise<void> => {
|
|
39
|
+
await api.post(`/jobs/${id}/retry`);
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// Create a new job
|
|
43
|
+
createJob: async (jobData: Partial<Job>): Promise<Job> => {
|
|
44
|
+
const response = await api.post("/jobs", jobData);
|
|
45
|
+
return response.data;
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// Update an existing job
|
|
49
|
+
updateJob: async (id: string, jobData: Partial<Job>): Promise<Job> => {
|
|
50
|
+
const response = await api.put(`/jobs/${id}`, jobData);
|
|
51
|
+
return response.data;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Delete a job
|
|
55
|
+
deleteJob: async (id: string): Promise<void> => {
|
|
56
|
+
await api.delete(`/jobs/${id}`);
|
|
57
|
+
},
|
|
58
|
+
};
|
package/src/types/job.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface Job {
|
|
2
|
+
_id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
data?: any;
|
|
5
|
+
status: "pending" | "running" | "completed" | "failed" | "cancelled";
|
|
6
|
+
attempts: number;
|
|
7
|
+
nextRunAt: Date | string;
|
|
8
|
+
lastRunAt?: Date | string;
|
|
9
|
+
lockedAt?: Date | string;
|
|
10
|
+
lockedBy?: string;
|
|
11
|
+
retry?: {
|
|
12
|
+
maxAttempts: number;
|
|
13
|
+
delay: number | ((attempt: number) => number);
|
|
14
|
+
};
|
|
15
|
+
repeat?: {
|
|
16
|
+
cron?: string;
|
|
17
|
+
every?: number;
|
|
18
|
+
timezone?: string;
|
|
19
|
+
};
|
|
20
|
+
lastError?: string;
|
|
21
|
+
createdAt: Date | string;
|
|
22
|
+
updatedAt: Date | string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface JobQuery {
|
|
26
|
+
name?: string;
|
|
27
|
+
status?: string | string[];
|
|
28
|
+
limit?: number;
|
|
29
|
+
skip?: number;
|
|
30
|
+
sort?: {
|
|
31
|
+
field: "nextRunAt" | "createdAt" | "updatedAt";
|
|
32
|
+
order: "asc" | "desc";
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface JobStats {
|
|
37
|
+
total: number;
|
|
38
|
+
pending: number;
|
|
39
|
+
running: number;
|
|
40
|
+
completed: number;
|
|
41
|
+
failed: number;
|
|
42
|
+
cancelled: number;
|
|
43
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { formatDistance, format } from "date-fns";
|
|
2
|
+
|
|
3
|
+
export const formatDate = (date: Date | string): string => {
|
|
4
|
+
return format(new Date(date), "MMM dd, yyyy HH:mm:ss");
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const formatRelativeTime = (date: Date | string): string => {
|
|
8
|
+
return formatDistance(new Date(date), new Date(), { addSuffix: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const getStatusColor = (status: string): string => {
|
|
12
|
+
const colors = {
|
|
13
|
+
pending: "bg-blue-500/20 text-blue-400 border-blue-500/30",
|
|
14
|
+
running: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30",
|
|
15
|
+
completed: "bg-green-500/20 text-green-400 border-green-500/30",
|
|
16
|
+
failed: "bg-red-500/20 text-red-400 border-red-500/30",
|
|
17
|
+
cancelled: "bg-gray-500/20 text-gray-400 border-gray-500/30",
|
|
18
|
+
};
|
|
19
|
+
return colors[status as keyof typeof colors] || colors.pending;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const getStatusIcon = (status: string): string => {
|
|
23
|
+
const icons = {
|
|
24
|
+
pending: "⏱️",
|
|
25
|
+
running: "▶️",
|
|
26
|
+
completed: "✅",
|
|
27
|
+
failed: "❌",
|
|
28
|
+
cancelled: "🚫",
|
|
29
|
+
};
|
|
30
|
+
return icons[status as keyof typeof icons] || "⏱️";
|
|
31
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"types": ["vite/client"],
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
|
|
11
|
+
/* Bundler mode */
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"moduleDetection": "force",
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
|
|
19
|
+
/* Linting */
|
|
20
|
+
"strict": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"erasableSyntaxOnly": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"noUncheckedSideEffectImports": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["src"]
|
|
28
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"erasableSyntaxOnly": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["vite.config.ts"]
|
|
26
|
+
}
|