@gadmin2n/schematics 0.0.150 → 0.0.151
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/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.md +125 -36
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +42 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/gitignore +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/.seed-timestamp +0 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/check-dev-env.js +218 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/page-manage.ts +78 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +37 -67
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/index.ts +0 -17
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.controller.ts +46 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +60 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/external-callback.controller.ts +3 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/externalInteraction/workflow-callback-url.controller.ts +9 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.controller.ts +55 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.module.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/page-transfer.service.ts +491 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/perm-transfer.dto.ts +213 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/perm-transfer.module.ts +28 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/resource-transfer.service.ts +137 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/role-transfer.service.ts +248 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/transfer-token.service.ts +65 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/permTransfer/user-transfer.service.ts +146 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.controller.ts +55 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.module.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.controller.ts +55 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.module.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user-group.provider.spec.ts +46 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/subjectProvider/providers/user-group.provider.ts +3 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.ts +55 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.module.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +96 -14
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.ts +41 -41
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-database-function.spec.ts +36 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-database-function.ts +90 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-catalog.spec.ts +4 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-event-trigger.pg.spec.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution-idempotency.spec.ts +115 -63
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-outbox.service.spec.ts +166 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-outbox.service.ts +217 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.spec.ts +33 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.ts +57 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle.service.spec.ts +196 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle.service.ts +125 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-node-type.controller.ts +9 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +148 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.module.ts +12 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +14 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +75 -45
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowConfig/node-config-validator.service.spec.ts +25 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/TransferExportModal.tsx +115 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/TransferImportModal.tsx +257 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +0 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +40 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +15 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +40 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +15 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/plugins/devShellPlugin.ts +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agendaJob/list.tsx +89 -65
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/page/list.tsx +51 -127
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/permissionReadme/index.tsx +53 -109
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/resource/list.tsx +45 -41
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/role/list.tsx +51 -72
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/user/list.tsx +49 -40
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.test.tsx +16 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ReadonlyNodePanel.test.tsx +28 -10
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.test.tsx +60 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +94 -12
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/node-instance-show.css +13 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/schema-form/default-widgets.tsx +3 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +8 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +167 -94
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerConfigEditor.tsx +95 -95
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerLifecycleConfigEditor.test.tsx +81 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/EventTriggerLifecycleConfigEditor.tsx +126 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +48 -52
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/forEachConnection.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/newNodeConfig.ts +1 -3
- package/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/config/ac.config.ts +0 -24
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/SqlModal.tsx +0 -495
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
NodeOutputsMap,
|
|
35
35
|
validateWorkflowHandles,
|
|
36
36
|
} from './workflow-handle-validator';
|
|
37
|
+
import { WorkflowLifecycleService } from './workflow-lifecycle.service';
|
|
37
38
|
|
|
38
39
|
export interface EventCatalogDeliveryRow {
|
|
39
40
|
registrationCode: string;
|
|
@@ -407,6 +408,7 @@ export class WorkflowService {
|
|
|
407
408
|
private temporalService: TemporalService,
|
|
408
409
|
private humanApprovalService: HumanApprovalService,
|
|
409
410
|
private workflowCompiler: WorkflowCompilerService,
|
|
411
|
+
private workflowLifecycle: WorkflowLifecycleService,
|
|
410
412
|
) {}
|
|
411
413
|
|
|
412
414
|
/**
|
|
@@ -2012,15 +2014,13 @@ export class WorkflowService {
|
|
|
2012
2014
|
retryCheckpoint: readRetryCheckpoint(instance.context),
|
|
2013
2015
|
});
|
|
2014
2016
|
} catch (error) {
|
|
2015
|
-
await this.
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
},
|
|
2023
|
-
finishedAt: new Date(),
|
|
2017
|
+
await this.workflowLifecycle.markFailed({
|
|
2018
|
+
instanceId: instance.id,
|
|
2019
|
+
expectedStatuses: ['PENDING'],
|
|
2020
|
+
temporalRunId,
|
|
2021
|
+
result: {
|
|
2022
|
+
code: 'TEMPORAL_START_FAILED',
|
|
2023
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2024
2024
|
},
|
|
2025
2025
|
});
|
|
2026
2026
|
throw error;
|
|
@@ -2486,21 +2486,27 @@ export class WorkflowService {
|
|
|
2486
2486
|
@Interval(60_000)
|
|
2487
2487
|
async reconcileUnstartedInstances(): Promise<void> {
|
|
2488
2488
|
const cutoff = new Date(Date.now() - 60_000);
|
|
2489
|
-
await this.prisma.workflowInstance.
|
|
2489
|
+
const unreservedInstances = await this.prisma.workflowInstance.findMany({
|
|
2490
2490
|
where: {
|
|
2491
2491
|
status: 'PENDING',
|
|
2492
2492
|
temporalRunId: null,
|
|
2493
2493
|
createdAt: { lte: cutoff },
|
|
2494
2494
|
},
|
|
2495
|
-
|
|
2496
|
-
|
|
2495
|
+
orderBy: { id: 'asc' },
|
|
2496
|
+
take: 100,
|
|
2497
|
+
select: { id: true },
|
|
2498
|
+
});
|
|
2499
|
+
for (const instance of unreservedInstances) {
|
|
2500
|
+
await this.workflowLifecycle.markFailed({
|
|
2501
|
+
instanceId: instance.id,
|
|
2502
|
+
expectedStatuses: ['PENDING'],
|
|
2503
|
+
temporalRunId: null,
|
|
2497
2504
|
result: {
|
|
2498
2505
|
code: 'TEMPORAL_START_NOT_RESERVED',
|
|
2499
2506
|
message: 'Workflow failed before Temporal execution was reserved',
|
|
2500
2507
|
},
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
});
|
|
2508
|
+
});
|
|
2509
|
+
}
|
|
2504
2510
|
|
|
2505
2511
|
const reservedInstances = await this.prisma.workflowInstance.findMany({
|
|
2506
2512
|
where: {
|
|
@@ -2542,13 +2548,30 @@ export class WorkflowService {
|
|
|
2542
2548
|
);
|
|
2543
2549
|
continue;
|
|
2544
2550
|
}
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2551
|
+
try {
|
|
2552
|
+
await this.temporalService.startWorkflow({
|
|
2553
|
+
instanceId: Number(instance.id),
|
|
2554
|
+
workflowId: Number(instance.workflowId),
|
|
2555
|
+
versionId: Number(instance.versionId),
|
|
2556
|
+
dsl: version.dsl,
|
|
2557
|
+
context: (instance.context ?? {}) as Record<string, any>,
|
|
2558
|
+
retryCheckpoint: readRetryCheckpoint(instance.context),
|
|
2559
|
+
});
|
|
2560
|
+
} catch (error) {
|
|
2561
|
+
await this.failUnstartedInstance(
|
|
2562
|
+
instance.id,
|
|
2563
|
+
instance.temporalRunId!,
|
|
2564
|
+
error instanceof Error ? error.message : String(error),
|
|
2565
|
+
);
|
|
2566
|
+
continue;
|
|
2567
|
+
}
|
|
2568
|
+
await this.prisma.workflowEventDelivery.updateMany({
|
|
2569
|
+
where: { workflowInstanceId: instance.id, status: 'FAILED' },
|
|
2570
|
+
data: {
|
|
2571
|
+
status: 'DISPATCHED',
|
|
2572
|
+
dispatchedAt: new Date(),
|
|
2573
|
+
lastError: null,
|
|
2574
|
+
},
|
|
2552
2575
|
});
|
|
2553
2576
|
temporalState = await this.temporalService.inspectWorkflow(
|
|
2554
2577
|
instance.temporalRunId!,
|
|
@@ -2574,15 +2597,13 @@ export class WorkflowService {
|
|
|
2574
2597
|
instanceId: bigint,
|
|
2575
2598
|
temporalRunId: string,
|
|
2576
2599
|
): Promise<void> {
|
|
2577
|
-
await this.
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
},
|
|
2585
|
-
finishedAt: new Date(),
|
|
2600
|
+
await this.workflowLifecycle.markFailed({
|
|
2601
|
+
instanceId,
|
|
2602
|
+
expectedStatuses: ['RUNNING'],
|
|
2603
|
+
temporalRunId,
|
|
2604
|
+
result: {
|
|
2605
|
+
code: 'TEMPORAL_EXECUTION_MISSING',
|
|
2606
|
+
message: 'Temporal execution disappeared while Workflow was running',
|
|
2586
2607
|
},
|
|
2587
2608
|
});
|
|
2588
2609
|
}
|
|
@@ -2592,13 +2613,11 @@ export class WorkflowService {
|
|
|
2592
2613
|
temporalRunId: string,
|
|
2593
2614
|
message: string,
|
|
2594
2615
|
): Promise<void> {
|
|
2595
|
-
await this.
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
finishedAt: new Date(),
|
|
2601
|
-
},
|
|
2616
|
+
await this.workflowLifecycle.markFailed({
|
|
2617
|
+
instanceId,
|
|
2618
|
+
expectedStatuses: ['PENDING'],
|
|
2619
|
+
temporalRunId,
|
|
2620
|
+
result: { code: 'TEMPORAL_START_RECONCILIATION_FAILED', message },
|
|
2602
2621
|
});
|
|
2603
2622
|
}
|
|
2604
2623
|
|
|
@@ -2663,12 +2682,23 @@ export class WorkflowService {
|
|
|
2663
2682
|
}
|
|
2664
2683
|
return;
|
|
2665
2684
|
}
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
:
|
|
2670
|
-
|
|
2671
|
-
:
|
|
2685
|
+
if (state.status !== 'COMPLETED' && state.status !== 'CANCELLED') {
|
|
2686
|
+
await this.workflowLifecycle.markFailed({
|
|
2687
|
+
instanceId,
|
|
2688
|
+
expectedStatuses: ['PENDING', 'RUNNING'],
|
|
2689
|
+
temporalRunId,
|
|
2690
|
+
result:
|
|
2691
|
+
state.result === undefined
|
|
2692
|
+
? {
|
|
2693
|
+
code: 'TEMPORAL_TERMINAL_STATUS_RECONCILED',
|
|
2694
|
+
temporalStatus: state.status,
|
|
2695
|
+
}
|
|
2696
|
+
: normalizeTemporalCompletedResult(state.result),
|
|
2697
|
+
startedAt: state.startedAt,
|
|
2698
|
+
finishedAt: state.finishedAt,
|
|
2699
|
+
});
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2672
2702
|
await this.prisma.workflowInstance.updateMany({
|
|
2673
2703
|
where: {
|
|
2674
2704
|
id: instanceId,
|
|
@@ -2676,7 +2706,7 @@ export class WorkflowService {
|
|
|
2676
2706
|
temporalRunId,
|
|
2677
2707
|
},
|
|
2678
2708
|
data: {
|
|
2679
|
-
status,
|
|
2709
|
+
status: state.status,
|
|
2680
2710
|
result:
|
|
2681
2711
|
state.result === undefined
|
|
2682
2712
|
? {
|
|
@@ -409,4 +409,29 @@ describe('event trigger operation rules schema', () => {
|
|
|
409
409
|
]),
|
|
410
410
|
).toThrow(BadRequestException);
|
|
411
411
|
});
|
|
412
|
+
it('accepts a configured failure callback and rejects a non-relative path', () => {
|
|
413
|
+
const base = {
|
|
414
|
+
registrationCode: 'request-changes',
|
|
415
|
+
processingMode: 'PARALLEL',
|
|
416
|
+
operationRules: [{ operation: 'INSERT' }],
|
|
417
|
+
};
|
|
418
|
+
expect(() =>
|
|
419
|
+
service.validateOrThrow('event:6', schema, {
|
|
420
|
+
...base,
|
|
421
|
+
failureCallback: {
|
|
422
|
+
connectionRef: 'hardware-service',
|
|
423
|
+
path: '/api/hardware-disposal/workflow-failed',
|
|
424
|
+
},
|
|
425
|
+
}),
|
|
426
|
+
).not.toThrow();
|
|
427
|
+
expect(() =>
|
|
428
|
+
service.validateOrThrow('event:6', schema, {
|
|
429
|
+
...base,
|
|
430
|
+
failureCallback: {
|
|
431
|
+
connectionRef: 'hardware-service',
|
|
432
|
+
path: 'https://unexpected.example.com/callback',
|
|
433
|
+
},
|
|
434
|
+
}),
|
|
435
|
+
).toThrow(BadRequestException);
|
|
436
|
+
});
|
|
412
437
|
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { CopyOutlined, LoadingOutlined } from '@ant-design/icons';
|
|
2
|
+
import { Alert, Button, Input, Modal, Space, message } from 'antd';
|
|
3
|
+
import { customRequest } from 'helpers/http';
|
|
4
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
|
|
7
|
+
export type TransferEntity = 'user' | 'role' | 'page' | 'resource';
|
|
8
|
+
|
|
9
|
+
interface TransferExportModalProps {
|
|
10
|
+
open: boolean;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
/** e.g. `role/SYSTEM_ADMIN/export-token`; null disables fetching */
|
|
13
|
+
exportTokenEndpoint: string | null;
|
|
14
|
+
/** drives the "what this export contains" hint */
|
|
15
|
+
entity: TransferEntity;
|
|
16
|
+
title?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generic "export via signed URL" modal (workflow ExportModal pattern).
|
|
21
|
+
* Asks the backend for a 30-minute export URL and lets the user copy it
|
|
22
|
+
* into the target environment's TransferImportModal.
|
|
23
|
+
*/
|
|
24
|
+
export function TransferExportModal({
|
|
25
|
+
open,
|
|
26
|
+
onClose,
|
|
27
|
+
exportTokenEndpoint,
|
|
28
|
+
entity,
|
|
29
|
+
title,
|
|
30
|
+
}: TransferExportModalProps) {
|
|
31
|
+
const { t } = useTranslation();
|
|
32
|
+
const [url, setUrl] = useState<string>('');
|
|
33
|
+
const [loading, setLoading] = useState(false);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (open && exportTokenEndpoint) {
|
|
37
|
+
setUrl('');
|
|
38
|
+
setLoading(true);
|
|
39
|
+
customRequest<{ url: string }>(exportTokenEndpoint, 'POST')
|
|
40
|
+
.then((res) => setUrl(res.url))
|
|
41
|
+
.catch((e: any) =>
|
|
42
|
+
message.error(e?.message ?? t('transfer.export.failed')),
|
|
43
|
+
)
|
|
44
|
+
.finally(() => setLoading(false));
|
|
45
|
+
}
|
|
46
|
+
}, [open, exportTokenEndpoint]);
|
|
47
|
+
|
|
48
|
+
const handleCopy = useCallback(async () => {
|
|
49
|
+
try {
|
|
50
|
+
await navigator.clipboard.writeText(url);
|
|
51
|
+
message.success(t('transfer.export.copied'));
|
|
52
|
+
} catch {
|
|
53
|
+
// Fallback for older browsers
|
|
54
|
+
const textarea = document.getElementById(
|
|
55
|
+
'transfer-export-url-input',
|
|
56
|
+
) as HTMLTextAreaElement;
|
|
57
|
+
textarea?.select();
|
|
58
|
+
document.execCommand('copy');
|
|
59
|
+
message.success(t('transfer.export.copied'));
|
|
60
|
+
}
|
|
61
|
+
}, [url]);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Modal
|
|
65
|
+
title={title ?? t('transfer.export.title')}
|
|
66
|
+
open={open}
|
|
67
|
+
onCancel={onClose}
|
|
68
|
+
footer={
|
|
69
|
+
<Space>
|
|
70
|
+
<Button onClick={onClose}>{t('buttons.cancel')}</Button>
|
|
71
|
+
<Button
|
|
72
|
+
type="primary"
|
|
73
|
+
icon={<CopyOutlined />}
|
|
74
|
+
onClick={handleCopy}
|
|
75
|
+
disabled={!url}
|
|
76
|
+
>
|
|
77
|
+
{t('transfer.export.copyUrl')}
|
|
78
|
+
</Button>
|
|
79
|
+
</Space>
|
|
80
|
+
}
|
|
81
|
+
>
|
|
82
|
+
<Alert
|
|
83
|
+
type="info"
|
|
84
|
+
showIcon
|
|
85
|
+
style={{ marginBottom: 16 }}
|
|
86
|
+
message={t('transfer.export.scopeTitle')}
|
|
87
|
+
description={t(`transfer.export.scope.${entity}`)}
|
|
88
|
+
/>
|
|
89
|
+
{loading ? (
|
|
90
|
+
<div style={{ textAlign: 'center', padding: 24 }}>
|
|
91
|
+
<LoadingOutlined style={{ fontSize: 24 }} />
|
|
92
|
+
<p>{t('transfer.export.generating')}</p>
|
|
93
|
+
</div>
|
|
94
|
+
) : (
|
|
95
|
+
<>
|
|
96
|
+
<p
|
|
97
|
+
style={{
|
|
98
|
+
marginBottom: 8,
|
|
99
|
+
color: 'var(--ant-color-text-secondary)',
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
{t('transfer.export.urlHint')}
|
|
103
|
+
</p>
|
|
104
|
+
<Input.TextArea
|
|
105
|
+
id="transfer-export-url-input"
|
|
106
|
+
value={url}
|
|
107
|
+
readOnly
|
|
108
|
+
autoSize={{ minRows: 3, maxRows: 5 }}
|
|
109
|
+
onFocus={(e) => e.target.select()}
|
|
110
|
+
/>
|
|
111
|
+
</>
|
|
112
|
+
)}
|
|
113
|
+
</Modal>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { CheckOutlined, CloudDownloadOutlined } from '@ant-design/icons';
|
|
2
|
+
import { Alert, Button, Input, Modal, Space, Spin, message } from 'antd';
|
|
3
|
+
import { customRequest } from 'helpers/http';
|
|
4
|
+
import React, { useCallback, useState } from 'react';
|
|
5
|
+
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
|
|
8
|
+
interface MissingRef {
|
|
9
|
+
type: 'page' | 'resource' | 'parentPage';
|
|
10
|
+
code: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface PreviewResponse {
|
|
14
|
+
current: any;
|
|
15
|
+
incoming: any;
|
|
16
|
+
missingRefs: MissingRef[];
|
|
17
|
+
conflicts: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface TransferImportModalProps {
|
|
21
|
+
open: boolean;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
onSuccess: () => void;
|
|
24
|
+
/** e.g. 'role/import-preview' */
|
|
25
|
+
previewEndpoint: string;
|
|
26
|
+
/** e.g. 'role/import-confirm' */
|
|
27
|
+
confirmEndpoint: string;
|
|
28
|
+
/** extra follow-up note shown after a successful import (e.g. "grant the
|
|
29
|
+
* page to a role, otherwise the menu stays hidden") */
|
|
30
|
+
successHint?: string;
|
|
31
|
+
title?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type Phase = 'input' | 'preview' | 'confirming';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generic "import from a source-environment export URL" modal
|
|
38
|
+
* (workflow ImportModal pattern): paste URL → diff preview → confirm.
|
|
39
|
+
* Import is blocked when the preview reports missing referenced
|
|
40
|
+
* pages/resources or conflicts.
|
|
41
|
+
*/
|
|
42
|
+
export function TransferImportModal({
|
|
43
|
+
open,
|
|
44
|
+
onClose,
|
|
45
|
+
onSuccess,
|
|
46
|
+
previewEndpoint,
|
|
47
|
+
confirmEndpoint,
|
|
48
|
+
successHint,
|
|
49
|
+
title,
|
|
50
|
+
}: TransferImportModalProps) {
|
|
51
|
+
const { t } = useTranslation();
|
|
52
|
+
const [sourceUrl, setSourceUrl] = useState('');
|
|
53
|
+
const [phase, setPhase] = useState<Phase>('input');
|
|
54
|
+
const [loading, setLoading] = useState(false);
|
|
55
|
+
const [currentJson, setCurrentJson] = useState('');
|
|
56
|
+
const [incomingJson, setIncomingJson] = useState('');
|
|
57
|
+
const [missingRefs, setMissingRefs] = useState<MissingRef[]>([]);
|
|
58
|
+
const [conflicts, setConflicts] = useState<string[]>([]);
|
|
59
|
+
const [payload, setPayload] = useState<any>(null);
|
|
60
|
+
|
|
61
|
+
const reset = useCallback(() => {
|
|
62
|
+
setSourceUrl('');
|
|
63
|
+
setPhase('input');
|
|
64
|
+
setLoading(false);
|
|
65
|
+
setCurrentJson('');
|
|
66
|
+
setIncomingJson('');
|
|
67
|
+
setMissingRefs([]);
|
|
68
|
+
setConflicts([]);
|
|
69
|
+
setPayload(null);
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
const handleClose = useCallback(() => {
|
|
73
|
+
reset();
|
|
74
|
+
onClose();
|
|
75
|
+
}, [reset, onClose]);
|
|
76
|
+
|
|
77
|
+
const blocked = missingRefs.length > 0 || conflicts.length > 0;
|
|
78
|
+
|
|
79
|
+
const handleFetchPreview = useCallback(async () => {
|
|
80
|
+
if (!sourceUrl.trim()) {
|
|
81
|
+
message.warning(t('transfer.import.pasteUrl'));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setLoading(true);
|
|
86
|
+
try {
|
|
87
|
+
// Step 1: Fetch from source URL (cross-origin)
|
|
88
|
+
const response = await fetch(sourceUrl.trim());
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
const errorData = await response.json().catch(() => null);
|
|
91
|
+
throw new Error(
|
|
92
|
+
errorData?.message ||
|
|
93
|
+
t('transfer.import.fetchFailed', { status: response.status }),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const exportData = await response.json();
|
|
97
|
+
|
|
98
|
+
// Step 2: Send to our import-preview endpoint
|
|
99
|
+
const preview = await customRequest<PreviewResponse>(
|
|
100
|
+
previewEndpoint,
|
|
101
|
+
'POST',
|
|
102
|
+
{ payload: exportData },
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Step 3: Format for diff viewer
|
|
106
|
+
setCurrentJson(
|
|
107
|
+
preview.current ? JSON.stringify(preview.current, null, 2) : '',
|
|
108
|
+
);
|
|
109
|
+
setIncomingJson(JSON.stringify(preview.incoming, null, 2));
|
|
110
|
+
setMissingRefs(preview.missingRefs ?? []);
|
|
111
|
+
setConflicts(preview.conflicts ?? []);
|
|
112
|
+
setPayload(exportData);
|
|
113
|
+
setPhase('preview');
|
|
114
|
+
} catch (e: any) {
|
|
115
|
+
if (e.message?.includes('expired') || e.message?.includes('invalid')) {
|
|
116
|
+
message.error(t('transfer.import.expired'));
|
|
117
|
+
} else {
|
|
118
|
+
message.error(e?.message || t('transfer.import.unreachable'));
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
setLoading(false);
|
|
122
|
+
}
|
|
123
|
+
}, [sourceUrl, previewEndpoint]);
|
|
124
|
+
|
|
125
|
+
const handleConfirm = useCallback(async () => {
|
|
126
|
+
if (!payload || blocked) return;
|
|
127
|
+
setPhase('confirming');
|
|
128
|
+
try {
|
|
129
|
+
const result = await customRequest<{ success: boolean; key: string }>(
|
|
130
|
+
confirmEndpoint,
|
|
131
|
+
'POST',
|
|
132
|
+
{ payload },
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
message.success(t('transfer.import.success', { name: result.key }));
|
|
136
|
+
if (successHint) {
|
|
137
|
+
// longer duration: this is an actionable follow-up, not a status blip
|
|
138
|
+
message.info(successHint, 10);
|
|
139
|
+
}
|
|
140
|
+
reset();
|
|
141
|
+
onClose();
|
|
142
|
+
onSuccess();
|
|
143
|
+
} catch (e: any) {
|
|
144
|
+
message.error(e?.message ?? t('transfer.import.failed'));
|
|
145
|
+
setPhase('preview');
|
|
146
|
+
}
|
|
147
|
+
}, [
|
|
148
|
+
payload,
|
|
149
|
+
blocked,
|
|
150
|
+
confirmEndpoint,
|
|
151
|
+
successHint,
|
|
152
|
+
reset,
|
|
153
|
+
onClose,
|
|
154
|
+
onSuccess,
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<Modal
|
|
159
|
+
title={title ?? t('transfer.import.title')}
|
|
160
|
+
open={open}
|
|
161
|
+
onCancel={handleClose}
|
|
162
|
+
width={phase === 'preview' ? '90vw' : 520}
|
|
163
|
+
footer={
|
|
164
|
+
phase === 'input' ? (
|
|
165
|
+
<Space>
|
|
166
|
+
<Button onClick={handleClose}>{t('buttons.cancel')}</Button>
|
|
167
|
+
<Button
|
|
168
|
+
type="primary"
|
|
169
|
+
icon={<CloudDownloadOutlined />}
|
|
170
|
+
onClick={handleFetchPreview}
|
|
171
|
+
loading={loading}
|
|
172
|
+
>
|
|
173
|
+
{t('transfer.import.fetchPreview')}
|
|
174
|
+
</Button>
|
|
175
|
+
</Space>
|
|
176
|
+
) : (
|
|
177
|
+
<Space>
|
|
178
|
+
<Button onClick={handleClose}>{t('buttons.cancel')}</Button>
|
|
179
|
+
<Button
|
|
180
|
+
type="primary"
|
|
181
|
+
icon={<CheckOutlined />}
|
|
182
|
+
onClick={handleConfirm}
|
|
183
|
+
loading={phase === 'confirming'}
|
|
184
|
+
disabled={blocked}
|
|
185
|
+
>
|
|
186
|
+
{t('transfer.import.confirm')}
|
|
187
|
+
</Button>
|
|
188
|
+
</Space>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
>
|
|
192
|
+
{phase === 'input' && (
|
|
193
|
+
<>
|
|
194
|
+
<p style={{ marginBottom: 8 }}>{t('transfer.import.urlHint')}</p>
|
|
195
|
+
<Input.TextArea
|
|
196
|
+
value={sourceUrl}
|
|
197
|
+
onChange={(e) => setSourceUrl(e.target.value)}
|
|
198
|
+
placeholder="https://dev.example.com/api/role/export/SYSTEM_ADMIN?token=..."
|
|
199
|
+
autoSize={{ minRows: 3, maxRows: 5 }}
|
|
200
|
+
/>
|
|
201
|
+
</>
|
|
202
|
+
)}
|
|
203
|
+
|
|
204
|
+
{phase === 'preview' && (
|
|
205
|
+
<div style={{ maxHeight: '70vh', overflow: 'auto' }}>
|
|
206
|
+
{missingRefs.length > 0 && (
|
|
207
|
+
<Alert
|
|
208
|
+
type="warning"
|
|
209
|
+
showIcon
|
|
210
|
+
style={{ marginBottom: 12 }}
|
|
211
|
+
message={t('transfer.import.missingRefsTitle')}
|
|
212
|
+
description={
|
|
213
|
+
<ul style={{ margin: 0, paddingInlineStart: 20 }}>
|
|
214
|
+
{missingRefs.map((m) => (
|
|
215
|
+
<li key={`${m.type}:${m.code}`}>
|
|
216
|
+
{t(`transfer.import.missingRefType.${m.type}`)}: {m.code}
|
|
217
|
+
</li>
|
|
218
|
+
))}
|
|
219
|
+
</ul>
|
|
220
|
+
}
|
|
221
|
+
/>
|
|
222
|
+
)}
|
|
223
|
+
{conflicts.length > 0 && (
|
|
224
|
+
<Alert
|
|
225
|
+
type="error"
|
|
226
|
+
showIcon
|
|
227
|
+
style={{ marginBottom: 12 }}
|
|
228
|
+
message={t('transfer.import.conflictsTitle')}
|
|
229
|
+
description={
|
|
230
|
+
<ul style={{ margin: 0, paddingInlineStart: 20 }}>
|
|
231
|
+
{conflicts.map((c) => (
|
|
232
|
+
<li key={c}>{c}</li>
|
|
233
|
+
))}
|
|
234
|
+
</ul>
|
|
235
|
+
}
|
|
236
|
+
/>
|
|
237
|
+
)}
|
|
238
|
+
<ReactDiffViewer
|
|
239
|
+
oldValue={currentJson}
|
|
240
|
+
newValue={incomingJson}
|
|
241
|
+
splitView
|
|
242
|
+
compareMethod={DiffMethod.LINES}
|
|
243
|
+
leftTitle={t('transfer.import.current')}
|
|
244
|
+
rightTitle={t('transfer.import.incoming')}
|
|
245
|
+
/>
|
|
246
|
+
</div>
|
|
247
|
+
)}
|
|
248
|
+
|
|
249
|
+
{phase === 'confirming' && (
|
|
250
|
+
<div style={{ textAlign: 'center', padding: 40 }}>
|
|
251
|
+
<Spin size="large" />
|
|
252
|
+
<p style={{ marginTop: 16 }}>{t('transfer.import.importing')}</p>
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
</Modal>
|
|
256
|
+
);
|
|
257
|
+
}
|
package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
BgColorsOutlined,
|
|
7
7
|
ScheduleOutlined,
|
|
8
8
|
ApartmentOutlined,
|
|
9
|
-
SafetyCertificateOutlined,
|
|
10
9
|
TeamOutlined,
|
|
11
10
|
UserOutlined,
|
|
12
11
|
} from '@ant-design/icons';
|
|
@@ -50,7 +49,6 @@ export const iconMapping: Record<string, React.ReactNode> = {
|
|
|
50
49
|
agenda: <ScheduleOutlined />,
|
|
51
50
|
workflow: <ApartmentOutlined />,
|
|
52
51
|
person_directory: <UserOutlined />,
|
|
53
|
-
user: <SafetyCertificateOutlined />,
|
|
54
52
|
people_group: <TeamOutlined />,
|
|
55
53
|
};
|
|
56
54
|
|
|
@@ -371,5 +371,45 @@
|
|
|
371
371
|
"renameDuplicate": "Name already exists",
|
|
372
372
|
"renameFailed": "Rename failed"
|
|
373
373
|
}
|
|
374
|
+
},
|
|
375
|
+
"transfer": {
|
|
376
|
+
"import": {
|
|
377
|
+
"title": "Import",
|
|
378
|
+
"fetchPreview": "Fetch & Preview",
|
|
379
|
+
"confirm": "Confirm Import",
|
|
380
|
+
"urlHint": "Paste the export URL generated by the source environment:",
|
|
381
|
+
"current": "Current env",
|
|
382
|
+
"incoming": "Source env",
|
|
383
|
+
"importing": "Importing…",
|
|
384
|
+
"pasteUrl": "Please paste an export URL",
|
|
385
|
+
"fetchFailed": "Failed to fetch data: {{status}}",
|
|
386
|
+
"expired": "Export link expired or invalid. Please regenerate it in the source environment.",
|
|
387
|
+
"unreachable": "Cannot reach the source environment. Check the URL and retry.",
|
|
388
|
+
"success": "\"{{name}}\" imported successfully!",
|
|
389
|
+
"failed": "Import failed",
|
|
390
|
+
"missingRefsTitle": "The target environment is missing these references. Import them first:",
|
|
391
|
+
"missingRefType": {
|
|
392
|
+
"page": "Page",
|
|
393
|
+
"resource": "Resource",
|
|
394
|
+
"parentPage": "Parent page"
|
|
395
|
+
},
|
|
396
|
+
"conflictsTitle": "Conflicts detected, import blocked:",
|
|
397
|
+
"pageRoleHint": "The page was imported but is not granted to any role yet, so it will not appear in the menu. Import the matching role, or grant the page to a role under Role Management."
|
|
398
|
+
},
|
|
399
|
+
"export": {
|
|
400
|
+
"title": "Export",
|
|
401
|
+
"copyUrl": "Copy URL",
|
|
402
|
+
"generating": "Generating export URL…",
|
|
403
|
+
"urlHint": "Export URL (valid for 30 minutes). Paste it into the target environment's import dialog:",
|
|
404
|
+
"failed": "Failed to generate export URL",
|
|
405
|
+
"copied": "URL copied to clipboard",
|
|
406
|
+
"scopeTitle": "What this export contains",
|
|
407
|
+
"scope": {
|
|
408
|
+
"user": "Includes: userid, name, roles, active status. Excludes: API keys and other credentials (never transferred across environments).",
|
|
409
|
+
"role": "Includes: role details, accessible pages (rolePages) and resource permissions (roleResource). Note: referenced pages and resources must already exist in the target environment.",
|
|
410
|
+
"page": "Includes: this page with all its descendants and their resource bindings (pageResource). Excludes: role grants (rolePages) — after importing, also import the matching role or grant it under Role Management, otherwise the menu stays hidden.",
|
|
411
|
+
"resource": "Includes: resource code, name, description and the common-resource flag."
|
|
412
|
+
}
|
|
413
|
+
}
|
|
374
414
|
}
|
|
375
415
|
}
|