@goplusvn/core 0.1.59 → 0.1.60
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/PLATFORM.md +18 -0
- package/bin/goerp-init.mjs +141 -0
- package/package.json +4 -4
- package/src/cron/db-cron-manager.ts +3 -3
- package/src/cron/index.ts +2 -2
- package/src/{infrastructure/cron/cron-manager.ts → cron/simple-cron-job.ts} +1 -1
- package/src/infrastructure/__tests__/architecture-verification.spec.ts +13 -97
- package/src/infrastructure/index.ts +4 -7
- package/src/ui/management/index.ts +3 -2
- package/templates/starter-app/.dockerignore +41 -0
- package/templates/starter-app/.env.example +25 -0
- package/templates/starter-app/AGENTS.md +52 -0
- package/templates/starter-app/Dockerfile +74 -0
- package/templates/starter-app/README.md +141 -0
- package/templates/starter-app/gitignore +9 -0
- package/templates/starter-app/next.config.mjs +50 -0
- package/templates/starter-app/package.json +55 -0
- package/templates/starter-app/postcss.config.mjs +5 -0
- package/templates/starter-app/prisma/migrations/20260801000000_init/migration.sql +504 -0
- package/templates/starter-app/prisma/migrations/20260801091814_goerp_audit_logs_0001_init/migration.sql +33 -0
- package/templates/starter-app/prisma/migrations/20260801091815_goerp_background_tasks_0001_init/migration.sql +23 -0
- package/templates/starter-app/prisma/migrations/20260801091816_goerp_error_logs_0001_init/migration.sql +32 -0
- package/templates/starter-app/prisma/migrations/20260801091817_goerp_notifications_0001_init/migration.sql +59 -0
- package/templates/starter-app/prisma/migrations/20260801091818_goerp_system_jobs_0001_init/migration.sql +47 -0
- package/templates/starter-app/prisma/schema/auth.prisma +87 -0
- package/templates/starter-app/prisma/schema/domain.prisma +24 -0
- package/templates/starter-app/prisma/schema/goerp-audit-logs.prisma +23 -0
- package/templates/starter-app/prisma/schema/goerp-background-tasks.prisma +25 -0
- package/templates/starter-app/prisma/schema/goerp-error-logs.prisma +31 -0
- package/templates/starter-app/prisma/schema/goerp-notifications.prisma +49 -0
- package/templates/starter-app/prisma/schema/goerp-system-jobs.prisma +45 -0
- package/templates/starter-app/prisma/schema/organization.prisma +31 -0
- package/templates/starter-app/prisma/schema/rbac.prisma +100 -0
- package/templates/starter-app/prisma/schema/schema.prisma +8 -0
- package/templates/starter-app/prisma/schema/system.prisma +22 -0
- package/templates/starter-app/prisma/seed.ts +127 -0
- package/templates/starter-app/prisma.config.ts +20 -0
- package/templates/starter-app/public/.gitkeep +2 -0
- package/templates/starter-app/scripts/rbac-sync.ts +235 -0
- package/templates/starter-app/src/__tests__/architecture.test.ts +151 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/audit/page.tsx +24 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/error-logs/page.tsx +21 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/jobs/page.tsx +18 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/settings/page.tsx +22 -0
- package/templates/starter-app/src/app/[lang]/(main)/crud/[entity]/page.tsx +41 -0
- package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +13 -0
- package/templates/starter-app/src/app/[lang]/(main)/notifications/page.tsx +13 -0
- package/templates/starter-app/src/app/[lang]/(main)/page.tsx +130 -0
- package/templates/starter-app/src/app/[lang]/(main)/roles/[id]/edit/page.tsx +60 -0
- package/templates/starter-app/src/app/[lang]/(main)/roles/new/page.tsx +41 -0
- package/templates/starter-app/src/app/[lang]/(main)/roles/page.tsx +48 -0
- package/templates/starter-app/src/app/[lang]/(main)/tasks/page.tsx +13 -0
- package/templates/starter-app/src/app/[lang]/(plain)/layout.tsx +10 -0
- package/templates/starter-app/src/app/[lang]/(plain)/sign-in/page.tsx +39 -0
- package/templates/starter-app/src/app/[lang]/layout.tsx +21 -0
- package/templates/starter-app/src/app/api/admin/system/audit/route.ts +60 -0
- package/templates/starter-app/src/app/api/admin/system/jobs/[name]/history/route.ts +39 -0
- package/templates/starter-app/src/app/api/admin/system/jobs/route.ts +96 -0
- package/templates/starter-app/src/app/api/admin/system/settings/all/route.ts +17 -0
- package/templates/starter-app/src/app/api/admin/system/settings/create/route.ts +19 -0
- package/templates/starter-app/src/app/api/admin/system/settings/delete/route.ts +20 -0
- package/templates/starter-app/src/app/api/admin/system/settings/route.ts +49 -0
- package/templates/starter-app/src/app/api/admin/system/settings/toggle-status/route.ts +28 -0
- package/templates/starter-app/src/app/api/admin/system/settings/update/route.ts +24 -0
- package/templates/starter-app/src/app/api/admin/system/settings/update-full/route.ts +23 -0
- package/templates/starter-app/src/app/api/better-auth/[...all]/route.ts +13 -0
- package/templates/starter-app/src/app/api/crud/[entity]/[id]/route.ts +13 -0
- package/templates/starter-app/src/app/api/crud/[entity]/route.ts +14 -0
- package/templates/starter-app/src/app/api/error-logs/[id]/route.ts +23 -0
- package/templates/starter-app/src/app/api/error-logs/route.ts +124 -0
- package/templates/starter-app/src/app/api/files/[...key]/route.ts +25 -0
- package/templates/starter-app/src/app/api/notifications/read/route.ts +29 -0
- package/templates/starter-app/src/app/api/notifications/route.ts +26 -0
- package/templates/starter-app/src/app/api/notifications/unread-count/route.ts +14 -0
- package/templates/starter-app/src/app/api/rbac/permissions-version/route.ts +11 -0
- package/templates/starter-app/src/app/api/roles/[id]/route.ts +14 -0
- package/templates/starter-app/src/app/api/roles/route.ts +18 -0
- package/templates/starter-app/src/app/api/tasks/[id]/download/route.ts +52 -0
- package/templates/starter-app/src/app/api/tasks/route.ts +37 -0
- package/templates/starter-app/src/app/api/upload/route.ts +15 -0
- package/templates/starter-app/src/app/globals.css +15 -0
- package/templates/starter-app/src/app/layout.tsx +16 -0
- package/templates/starter-app/src/app/page.tsx +8 -0
- package/templates/starter-app/src/components/layout/main-layout-wrapper.tsx +30 -0
- package/templates/starter-app/src/configs/entities/department.config.ts +24 -0
- package/templates/starter-app/src/configs/entities/index.ts +13 -0
- package/templates/starter-app/src/configs/i18n.ts +12 -0
- package/templates/starter-app/src/configs/permissions/index.ts +45 -0
- package/templates/starter-app/src/configs/permissions/master-data.permissions.ts +31 -0
- package/templates/starter-app/src/configs/permissions/system.permissions.ts +131 -0
- package/templates/starter-app/src/configs/permissions/types.ts +63 -0
- package/templates/starter-app/src/configs/tenant.ts +18 -0
- package/templates/starter-app/src/data/dictionary.ts +8 -0
- package/templates/starter-app/src/data/navigations.ts +61 -0
- package/templates/starter-app/src/instrumentation.ts +105 -0
- package/templates/starter-app/src/lib/api-handler.ts +157 -0
- package/templates/starter-app/src/lib/auth-client.ts +57 -0
- package/templates/starter-app/src/lib/auth.ts +62 -0
- package/templates/starter-app/src/lib/better-auth.ts +107 -0
- package/templates/starter-app/src/lib/branch-scope.ts +53 -0
- package/templates/starter-app/src/lib/cron/db-cron-manager.ts +42 -0
- package/templates/starter-app/src/lib/crud/index.ts +13 -0
- package/templates/starter-app/src/lib/errors/app-error.ts +2 -0
- package/templates/starter-app/src/lib/errors/error-handler.ts +5 -0
- package/templates/starter-app/src/lib/errors/log-server-error.ts +15 -0
- package/templates/starter-app/src/lib/errors/server-error.ts +11 -0
- package/templates/starter-app/src/lib/logger.ts +30 -0
- package/templates/starter-app/src/lib/page-guard.ts +35 -0
- package/templates/starter-app/src/lib/prisma.ts +80 -0
- package/templates/starter-app/src/lib/rbac/access.ts +87 -0
- package/templates/starter-app/src/lib/storage.ts +28 -0
- package/templates/starter-app/src/providers/index.tsx +54 -0
- package/templates/starter-app/src/providers/mode-provider.tsx +31 -0
- package/templates/starter-app/src/providers/theme-provider.tsx +21 -0
- package/templates/starter-app/src/proxy.ts +45 -0
- package/templates/starter-app/src/server/services/notification-service.ts +31 -0
- package/templates/starter-app/src/server/services/system-config-service.ts +163 -0
- package/templates/starter-app/src/server/tasks/handlers/export-departments.ts +58 -0
- package/templates/starter-app/src/server/tasks/index.ts +16 -0
- package/templates/starter-app/src/server/tasks/task-runner.ts +40 -0
- package/templates/starter-app/src/types/session.ts +29 -0
- package/templates/starter-app/tsconfig.json +47 -0
- package/templates/starter-app/vitest.config.ts +17 -0
- package/src/infrastructure/cron/index.ts +0 -6
- package/src/infrastructure/event-bus/event-bus.ts +0 -145
- package/src/infrastructure/event-bus/index.ts +0 -2
- package/src/infrastructure/event-bus/types.ts +0 -22
- package/src/infrastructure/lock/decorators.ts +0 -67
- package/src/infrastructure/lock/index.ts +0 -2
- package/src/infrastructure/lock/lock-manager.ts +0 -33
- package/src/plugin/apps-registry.ts +0 -97
- package/src/plugin/index.ts +0 -5
- package/src/plugin/types.ts +0 -41
- package/src/ui/management/audit-log-page.tsx +0 -14
- package/src/ui/management/job-management.tsx +0 -308
- package/src/workflow/activity-timeline.tsx +0 -412
- package/src/workflow/approval-workflow.tsx +0 -31
- package/src/workflow/index.ts +0 -2
- /package/src/{infrastructure/cron → cron}/types.ts +0 -0
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import type { LockOptions } from "./lock-manager";
|
|
2
|
-
import { LockManager } from "./lock-manager";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Decorator to acquire a lock before executing a method.
|
|
6
|
-
* Requires the class to have a `lockManager` property.
|
|
7
|
-
*/
|
|
8
|
-
export function WithLock(keyPath: string, options?: LockOptions) {
|
|
9
|
-
return function (
|
|
10
|
-
target: any,
|
|
11
|
-
propertyKey: string,
|
|
12
|
-
descriptor: PropertyDescriptor,
|
|
13
|
-
) {
|
|
14
|
-
const originalMethod = descriptor.value;
|
|
15
|
-
|
|
16
|
-
descriptor.value = async function (...args: any[]) {
|
|
17
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
-
const self = this as any;
|
|
19
|
-
|
|
20
|
-
if (!self.lockManager) {
|
|
21
|
-
throw new Error(
|
|
22
|
-
`Class '${target.constructor.name}' must have a 'lockManager' property to use @WithLock`,
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const resolvedKey = resolveKey(keyPath, args);
|
|
27
|
-
// Prefix lock key to avoid collisions
|
|
28
|
-
const lockKey = `lock:${resolvedKey}`;
|
|
29
|
-
|
|
30
|
-
const acquired = await self.lockManager.acquire(lockKey, options);
|
|
31
|
-
|
|
32
|
-
if (!acquired) {
|
|
33
|
-
throw new Error(`Could not acquire lock for key: ${lockKey}`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
return await originalMethod.apply(this, args);
|
|
38
|
-
} finally {
|
|
39
|
-
await self.lockManager.release(lockKey);
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
return descriptor;
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function resolveKey(path: string, args: any[]): string {
|
|
48
|
-
// If path format is "{0}" or "{0}.id"
|
|
49
|
-
const match = path.match(/\{(\d+)\}(.*)/);
|
|
50
|
-
if (match) {
|
|
51
|
-
const argIndex = parseInt(match[1]);
|
|
52
|
-
let value = args[argIndex];
|
|
53
|
-
const subPath = match[2]; // e.g., ".id"
|
|
54
|
-
|
|
55
|
-
if (!subPath) return String(value);
|
|
56
|
-
|
|
57
|
-
const props = subPath.replace(/^\./, "").split(".");
|
|
58
|
-
for (const prop of props) {
|
|
59
|
-
if (value === undefined || value === null) break;
|
|
60
|
-
value = value[prop];
|
|
61
|
-
}
|
|
62
|
-
return String(value);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Legacy/Simple support: direct string
|
|
66
|
-
return path;
|
|
67
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { Cache } from "../cache/types";
|
|
2
|
-
|
|
3
|
-
export interface LockOptions {
|
|
4
|
-
ttl?: number;
|
|
5
|
-
retryTimes?: number;
|
|
6
|
-
retryDelay?: number;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class LockManager {
|
|
10
|
-
constructor(private cache: Cache) {}
|
|
11
|
-
|
|
12
|
-
async acquire(key: string, options?: LockOptions): Promise<boolean> {
|
|
13
|
-
const ttl = options?.ttl || 10;
|
|
14
|
-
const retryTimes = options?.retryTimes || 1;
|
|
15
|
-
const retryDelay = options?.retryDelay || 100;
|
|
16
|
-
|
|
17
|
-
for (let i = 0; i < retryTimes; i++) {
|
|
18
|
-
const existing = await this.cache.get(key);
|
|
19
|
-
if (!existing) {
|
|
20
|
-
await this.cache.set(key, "LOCKED", ttl);
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
if (i < retryTimes - 1) {
|
|
24
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async release(key: string): Promise<void> {
|
|
31
|
-
await this.cache.del(key);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
// @goerp/core/plugin
|
|
2
|
-
// AppsRegistry - Central registry for GoerpApp instances
|
|
3
|
-
|
|
4
|
-
import type { EntityConfig, NavigationType } from "../types"
|
|
5
|
-
|
|
6
|
-
import type { GoerpApp } from "./types"
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* AppsRegistry - Manages registration and lookup of GoerpApp
|
|
10
|
-
* Host app registers installed apps; registry merges navigation and entity configs
|
|
11
|
-
*/
|
|
12
|
-
export class AppsRegistry {
|
|
13
|
-
private apps: GoerpApp[] = []
|
|
14
|
-
private appsById = new Map<string, GoerpApp>()
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Register an app. Later registrations override earlier entity configs for same entity.
|
|
18
|
-
*/
|
|
19
|
-
register(app: GoerpApp): void {
|
|
20
|
-
if (this.appsById.has(app.appId)) {
|
|
21
|
-
console.warn(`[AppsRegistry] App "${app.appId}" already registered. Replacing.`)
|
|
22
|
-
}
|
|
23
|
-
this.appsById.set(app.appId, app)
|
|
24
|
-
this.apps = Array.from(this.appsById.values())
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Register multiple apps in order
|
|
29
|
-
*/
|
|
30
|
-
registerAll(apps: GoerpApp[]): void {
|
|
31
|
-
for (const app of apps) {
|
|
32
|
-
this.register(app)
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Get app by appId
|
|
38
|
-
*/
|
|
39
|
-
getApp(appId: string): GoerpApp | undefined {
|
|
40
|
-
return this.appsById.get(appId)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Get all registered apps
|
|
45
|
-
*/
|
|
46
|
-
getAllApps(): GoerpApp[] {
|
|
47
|
-
return [...this.apps]
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Get merged navigation from all apps (order: registration order)
|
|
52
|
-
* Groups with same title have their items merged
|
|
53
|
-
*/
|
|
54
|
-
getNavigation(): NavigationType[] {
|
|
55
|
-
const byTitle = new Map<string, NavigationType>()
|
|
56
|
-
|
|
57
|
-
for (const app of this.apps) {
|
|
58
|
-
for (const group of app.navigation) {
|
|
59
|
-
const existing = byTitle.get(group.title)
|
|
60
|
-
const items = group.items ?? []
|
|
61
|
-
if (existing) {
|
|
62
|
-
existing.items = [...(existing.items ?? []), ...items]
|
|
63
|
-
} else {
|
|
64
|
-
byTitle.set(group.title, { ...group, items: [...items] })
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return Array.from(byTitle.values())
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Get entity config for entity key.
|
|
73
|
-
* Later-registered apps override earlier for same entity.
|
|
74
|
-
*/
|
|
75
|
-
getEntityConfig(entity: string): EntityConfig | undefined {
|
|
76
|
-
for (let i = this.apps.length - 1; i >= 0; i--) {
|
|
77
|
-
const config = this.apps[i].entityConfigs[entity]
|
|
78
|
-
if (config) return config
|
|
79
|
-
}
|
|
80
|
-
return undefined
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Check if entity exists in any app
|
|
85
|
-
*/
|
|
86
|
-
hasEntity(entity: string): boolean {
|
|
87
|
-
return this.getEntityConfig(entity) !== undefined
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Clear all registrations (for testing)
|
|
92
|
-
*/
|
|
93
|
-
clear(): void {
|
|
94
|
-
this.apps = []
|
|
95
|
-
this.appsById.clear()
|
|
96
|
-
}
|
|
97
|
-
}
|
package/src/plugin/index.ts
DELETED
package/src/plugin/types.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
// @goerp/core/plugin
|
|
2
|
-
// GoerpApp types - maps to AppDefinition in core-db
|
|
3
|
-
|
|
4
|
-
import type { ComponentType } from "react"
|
|
5
|
-
|
|
6
|
-
import type { EntityConfig, NavigationType } from "../types"
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Route definition for /app/[appId]/[pageId]
|
|
10
|
-
* Used when an app has custom pages beyond CRUD
|
|
11
|
-
*/
|
|
12
|
-
export interface RouteDefinition {
|
|
13
|
-
pageId: string
|
|
14
|
-
label: string
|
|
15
|
-
path: string
|
|
16
|
-
component: ComponentType<Record<string, unknown>>
|
|
17
|
-
permission?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* GoerpApp - Plugin/App descriptor
|
|
22
|
-
* Maps to AppDefinition.appId in core-db
|
|
23
|
-
*/
|
|
24
|
-
export interface GoerpApp {
|
|
25
|
-
/** Unique app identifier - maps to AppDefinition.appId */
|
|
26
|
-
appId: string
|
|
27
|
-
/** Display name */
|
|
28
|
-
name: string
|
|
29
|
-
/** Semantic version */
|
|
30
|
-
version: string
|
|
31
|
-
/** Human-readable description */
|
|
32
|
-
description?: string
|
|
33
|
-
/** Core system app (always enabled) */
|
|
34
|
-
isCore?: boolean
|
|
35
|
-
/** Navigation items for sidebar */
|
|
36
|
-
navigation: NavigationType[]
|
|
37
|
-
/** Entity configs for CRUD (/crud/[entity]) */
|
|
38
|
-
entityConfigs: Record<string, EntityConfig>
|
|
39
|
-
/** Custom routes for /app/[appId]/[pageId] */
|
|
40
|
-
routes?: RouteDefinition[]
|
|
41
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @deprecated Dùng `@goerp/core/system/pages/system-audit-page` (`SystemAuditPage`).
|
|
5
|
-
*
|
|
6
|
-
* Bản cũ ở đây đọc `auditManager` trong RAM ngay trong CLIENT component — luôn
|
|
7
|
-
* rỗng, vì dữ liệu audit nằm ở server. Nay trỏ thẳng sang trang thật (đọc bảng
|
|
8
|
-
* `audit_logs` qua API) để app đang import tên `AuditLogPage` không phải sửa
|
|
9
|
-
* ngay; app mới hãy import thẳng `SystemAuditPage`.
|
|
10
|
-
*/
|
|
11
|
-
export {
|
|
12
|
-
SystemAuditPage as AuditLogPage,
|
|
13
|
-
type SystemAuditPageProps as AuditLogPageProps,
|
|
14
|
-
} from "../../system/pages/system-audit-page";
|
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import React, { useState, useEffect } from "react";
|
|
4
|
-
import type { Job, JobLog } from "../../system/job-manager";
|
|
5
|
-
import { jobManager } from "../../system/job-manager";
|
|
6
|
-
// Ideally we would import components from @goerp/core/ui/primitives but we are inside core
|
|
7
|
-
// So we mock or use basic HTML/Tailwind for now to avoid circular deps if structure is complex
|
|
8
|
-
// In a real app we'd use the proper UI components.
|
|
9
|
-
// Assuming this page is used inside an app that provides the context or we use relative imports carefully.
|
|
10
|
-
|
|
11
|
-
// Relative imports to core UI components
|
|
12
|
-
import { Badge } from "../primitives/badge";
|
|
13
|
-
import { Button } from "../primitives/button";
|
|
14
|
-
import { Card, CardHeader, CardTitle, CardContent } from "../primitives/card";
|
|
15
|
-
import { ScrollArea } from "../primitives/scroll-area";
|
|
16
|
-
import {
|
|
17
|
-
Table,
|
|
18
|
-
TableBody,
|
|
19
|
-
TableCell,
|
|
20
|
-
TableHead,
|
|
21
|
-
TableHeader,
|
|
22
|
-
TableRow,
|
|
23
|
-
} from "../primitives/table";
|
|
24
|
-
|
|
25
|
-
import {
|
|
26
|
-
Dialog,
|
|
27
|
-
DialogContent,
|
|
28
|
-
DialogHeader,
|
|
29
|
-
DialogTitle,
|
|
30
|
-
DialogTrigger,
|
|
31
|
-
DialogFooter,
|
|
32
|
-
} from "../primitives/dialog";
|
|
33
|
-
import { Input } from "../primitives/input";
|
|
34
|
-
import { Label } from "../primitives/label";
|
|
35
|
-
|
|
36
|
-
export const JobManagementPage = () => {
|
|
37
|
-
const [jobs, setJobs] = useState<Job[]>([]);
|
|
38
|
-
const [selectedJob, setSelectedJob] = useState<string | null>(null);
|
|
39
|
-
const [logs, setLogs] = useState<JobLog[]>([]);
|
|
40
|
-
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
|
41
|
-
const [editingJob, setEditingJob] = useState<Job | null>(null);
|
|
42
|
-
const [editForm, setEditForm] = useState({ schedule: "", description: "" });
|
|
43
|
-
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
44
|
-
|
|
45
|
-
// Initial load and periodic refresh
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
setJobs(jobManager.getJobs());
|
|
48
|
-
const interval = setInterval(() => {
|
|
49
|
-
setJobs(jobManager.getJobs());
|
|
50
|
-
if (selectedJob) {
|
|
51
|
-
// Refresh logs if a job is selected
|
|
52
|
-
setLogs(jobManager.getLogs(selectedJob));
|
|
53
|
-
}
|
|
54
|
-
}, 1000); // 1s refresh for demo niceness
|
|
55
|
-
return () => clearInterval(interval);
|
|
56
|
-
}, [refreshTrigger, selectedJob]);
|
|
57
|
-
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
if (selectedJob) {
|
|
60
|
-
setLogs(jobManager.getLogs(selectedJob));
|
|
61
|
-
} else {
|
|
62
|
-
setLogs([]);
|
|
63
|
-
}
|
|
64
|
-
}, [selectedJob]);
|
|
65
|
-
|
|
66
|
-
const handleStart = (id: string) => {
|
|
67
|
-
jobManager.startJob(id);
|
|
68
|
-
setRefreshTrigger((prev) => prev + 1);
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const handleStop = (id: string) => {
|
|
72
|
-
jobManager.stopJob(id);
|
|
73
|
-
setRefreshTrigger((prev) => prev + 1);
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const handleEditClick = (job: Job, e: React.MouseEvent) => {
|
|
77
|
-
e.stopPropagation();
|
|
78
|
-
setEditingJob(job);
|
|
79
|
-
setEditForm({
|
|
80
|
-
schedule: job.schedule || "",
|
|
81
|
-
description: job.description || "",
|
|
82
|
-
});
|
|
83
|
-
setIsDialogOpen(true);
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const handleSaveEdit = () => {
|
|
87
|
-
if (editingJob) {
|
|
88
|
-
jobManager.updateJob(editingJob.id, editForm);
|
|
89
|
-
setRefreshTrigger((prev) => prev + 1);
|
|
90
|
-
setIsDialogOpen(false);
|
|
91
|
-
setEditingJob(null);
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const getStatusBadge = (status: string) => {
|
|
96
|
-
switch (status) {
|
|
97
|
-
case "running":
|
|
98
|
-
return (
|
|
99
|
-
<Badge
|
|
100
|
-
variant="default"
|
|
101
|
-
className="bg-emerald-500 hover:bg-emerald-600"
|
|
102
|
-
>
|
|
103
|
-
Running
|
|
104
|
-
</Badge>
|
|
105
|
-
);
|
|
106
|
-
case "stopped":
|
|
107
|
-
return <Badge variant="secondary">Stopped</Badge>;
|
|
108
|
-
case "failed":
|
|
109
|
-
return <Badge variant="destructive">Failed</Badge>;
|
|
110
|
-
default:
|
|
111
|
-
return <Badge variant="outline">{status}</Badge>;
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const [logSearchTerm, setLogSearchTerm] = useState("");
|
|
116
|
-
|
|
117
|
-
const filteredLogs = logs.filter(
|
|
118
|
-
(log) =>
|
|
119
|
-
log.message.toLowerCase().includes(logSearchTerm.toLowerCase()) ||
|
|
120
|
-
log.level.toLowerCase().includes(logSearchTerm.toLowerCase()),
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
return (
|
|
124
|
-
<div className="p-6 space-y-6 h-full flex flex-col">
|
|
125
|
-
<div className="flex justify-between items-center mb-4">
|
|
126
|
-
<div>
|
|
127
|
-
<h1 className="text-2xl font-bold tracking-tight">Job Management</h1>
|
|
128
|
-
<p className="text-muted-foreground text-sm">
|
|
129
|
-
Monitor and control system background jobs.
|
|
130
|
-
</p>
|
|
131
|
-
</div>
|
|
132
|
-
<Button
|
|
133
|
-
onClick={() => setRefreshTrigger((prev) => prev + 1)}
|
|
134
|
-
variant="outline"
|
|
135
|
-
>
|
|
136
|
-
Refresh
|
|
137
|
-
</Button>
|
|
138
|
-
</div>
|
|
139
|
-
|
|
140
|
-
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 flex-1 min-h-0">
|
|
141
|
-
{/* Job List */}
|
|
142
|
-
<Card className="md:col-span-2 flex flex-col min-h-0">
|
|
143
|
-
<CardHeader>
|
|
144
|
-
<CardTitle>Active Jobs</CardTitle>
|
|
145
|
-
</CardHeader>
|
|
146
|
-
<CardContent className="flex-1 overflow-auto p-0">
|
|
147
|
-
<Table>
|
|
148
|
-
<TableHeader>
|
|
149
|
-
<TableRow>
|
|
150
|
-
<TableHead>Job Name</TableHead>
|
|
151
|
-
<TableHead>Schedule</TableHead>
|
|
152
|
-
<TableHead>Status</TableHead>
|
|
153
|
-
<TableHead>Last Run</TableHead>
|
|
154
|
-
<TableHead className="text-right">Actions</TableHead>
|
|
155
|
-
</TableRow>
|
|
156
|
-
</TableHeader>
|
|
157
|
-
<TableBody>
|
|
158
|
-
{jobs.map((job) => (
|
|
159
|
-
<TableRow
|
|
160
|
-
key={job.id}
|
|
161
|
-
className={`cursor-pointer ${selectedJob === job.id ? "bg-muted/50" : ""}`}
|
|
162
|
-
onClick={() => setSelectedJob(job.id)}
|
|
163
|
-
>
|
|
164
|
-
<TableCell>
|
|
165
|
-
<div className="font-medium">{job.name}</div>
|
|
166
|
-
<div className="text-xs text-muted-foreground">
|
|
167
|
-
{job.description}
|
|
168
|
-
</div>
|
|
169
|
-
</TableCell>
|
|
170
|
-
<TableCell>{job.schedule}</TableCell>
|
|
171
|
-
<TableCell>{getStatusBadge(job.status)}</TableCell>
|
|
172
|
-
<TableCell className="text-muted-foreground">
|
|
173
|
-
{job.lastRun ? job.lastRun.toLocaleTimeString() : "-"}
|
|
174
|
-
</TableCell>
|
|
175
|
-
<TableCell className="text-right">
|
|
176
|
-
<div
|
|
177
|
-
className="flex justify-end gap-2"
|
|
178
|
-
onClick={(e) => e.stopPropagation()}
|
|
179
|
-
>
|
|
180
|
-
<Button
|
|
181
|
-
size="sm"
|
|
182
|
-
variant="outline"
|
|
183
|
-
onClick={(e) => handleEditClick(job, e)}
|
|
184
|
-
>
|
|
185
|
-
Edit
|
|
186
|
-
</Button>
|
|
187
|
-
{job.status === "running" ? (
|
|
188
|
-
<Button
|
|
189
|
-
size="sm"
|
|
190
|
-
variant="destructive"
|
|
191
|
-
onClick={() => handleStop(job.id)}
|
|
192
|
-
>
|
|
193
|
-
Stop
|
|
194
|
-
</Button>
|
|
195
|
-
) : (
|
|
196
|
-
<Button size="sm" onClick={() => handleStart(job.id)}>
|
|
197
|
-
Start
|
|
198
|
-
</Button>
|
|
199
|
-
)}
|
|
200
|
-
</div>
|
|
201
|
-
</TableCell>
|
|
202
|
-
</TableRow>
|
|
203
|
-
))}
|
|
204
|
-
</TableBody>
|
|
205
|
-
</Table>
|
|
206
|
-
</CardContent>
|
|
207
|
-
</Card>
|
|
208
|
-
|
|
209
|
-
{/* Edit Dialog */}
|
|
210
|
-
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
|
211
|
-
<DialogContent>
|
|
212
|
-
<DialogHeader>
|
|
213
|
-
<DialogTitle>Edit Job: {editingJob?.name}</DialogTitle>
|
|
214
|
-
</DialogHeader>
|
|
215
|
-
<div className="space-y-4 py-4">
|
|
216
|
-
<div className="space-y-2">
|
|
217
|
-
<Label htmlFor="schedule">Schedule</Label>
|
|
218
|
-
<Input
|
|
219
|
-
id="schedule"
|
|
220
|
-
value={editForm.schedule}
|
|
221
|
-
onChange={(e) =>
|
|
222
|
-
setEditForm({ ...editForm, schedule: e.target.value })
|
|
223
|
-
}
|
|
224
|
-
/>
|
|
225
|
-
</div>
|
|
226
|
-
<div className="space-y-2">
|
|
227
|
-
<Label htmlFor="description">Description</Label>
|
|
228
|
-
<Input
|
|
229
|
-
id="description"
|
|
230
|
-
value={editForm.description}
|
|
231
|
-
onChange={(e) =>
|
|
232
|
-
setEditForm({ ...editForm, description: e.target.value })
|
|
233
|
-
}
|
|
234
|
-
/>
|
|
235
|
-
</div>
|
|
236
|
-
</div>
|
|
237
|
-
<DialogFooter>
|
|
238
|
-
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>
|
|
239
|
-
Cancel
|
|
240
|
-
</Button>
|
|
241
|
-
<Button onClick={handleSaveEdit}>Save Changes</Button>
|
|
242
|
-
</DialogFooter>
|
|
243
|
-
</DialogContent>
|
|
244
|
-
</Dialog>
|
|
245
|
-
|
|
246
|
-
{/* Log Viewer */}
|
|
247
|
-
<Card className="flex flex-col min-h-0 h-full">
|
|
248
|
-
<CardHeader className="pb-3 border-b flex flex-row items-center justify-between space-y-0">
|
|
249
|
-
<CardTitle>
|
|
250
|
-
{selectedJob
|
|
251
|
-
? `Logs: ${jobs.find((j) => j.id === selectedJob)?.name}`
|
|
252
|
-
: "Job Logs"}
|
|
253
|
-
</CardTitle>
|
|
254
|
-
</CardHeader>
|
|
255
|
-
<div className="p-2 border-b">
|
|
256
|
-
<input
|
|
257
|
-
type="text"
|
|
258
|
-
placeholder="Search logs..."
|
|
259
|
-
className="w-full px-3 py-2 text-sm border rounded-md bg-background"
|
|
260
|
-
value={logSearchTerm}
|
|
261
|
-
onChange={(e) => setLogSearchTerm(e.target.value)}
|
|
262
|
-
/>
|
|
263
|
-
</div>
|
|
264
|
-
<CardContent className="flex-1 p-0 min-h-0 bg-slate-50 dark:bg-slate-900/50">
|
|
265
|
-
<ScrollArea className="h-[600px] w-full p-4">
|
|
266
|
-
{!selectedJob && (
|
|
267
|
-
<div className="text-center text-muted-foreground py-10">
|
|
268
|
-
Select a job to view its logs
|
|
269
|
-
</div>
|
|
270
|
-
)}
|
|
271
|
-
{selectedJob && filteredLogs.length === 0 && (
|
|
272
|
-
<div className="text-center text-muted-foreground py-10">
|
|
273
|
-
{logs.length === 0
|
|
274
|
-
? "No logs available"
|
|
275
|
-
: "No logs match your search"}
|
|
276
|
-
</div>
|
|
277
|
-
)}
|
|
278
|
-
<div className="space-y-2 font-mono text-xs">
|
|
279
|
-
{filteredLogs.map((log) => (
|
|
280
|
-
<div
|
|
281
|
-
key={log.id}
|
|
282
|
-
className="flex gap-2 border-b border-border/50 pb-1 last:border-0"
|
|
283
|
-
>
|
|
284
|
-
<span className="text-muted-foreground whitespace-nowrap">
|
|
285
|
-
{log.timestamp.toLocaleTimeString()}
|
|
286
|
-
</span>
|
|
287
|
-
<span
|
|
288
|
-
className={`font-semibold uppercase w-16 ${
|
|
289
|
-
log.level === "error"
|
|
290
|
-
? "text-red-500"
|
|
291
|
-
: log.level === "warning"
|
|
292
|
-
? "text-yellow-500"
|
|
293
|
-
: "text-blue-500"
|
|
294
|
-
}`}
|
|
295
|
-
>
|
|
296
|
-
[{log.level}]
|
|
297
|
-
</span>
|
|
298
|
-
<span className="break-all">{log.message}</span>
|
|
299
|
-
</div>
|
|
300
|
-
))}
|
|
301
|
-
</div>
|
|
302
|
-
</ScrollArea>
|
|
303
|
-
</CardContent>
|
|
304
|
-
</Card>
|
|
305
|
-
</div>
|
|
306
|
-
</div>
|
|
307
|
-
);
|
|
308
|
-
};
|