@oj-module/cli 0.2.2 → 0.2.5
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/devkit/README.md +14 -3
- package/devkit/SKILL.md +16 -4
- package/devkit/framework-dev.md +8 -6
- package/devkit/manual.md +243 -23
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +143 -8
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/args.ts +67 -0
- package/src/build.ts +11 -3
- package/src/bundle.ts +197 -0
- package/src/contract/__tests__/handler-scan.test.ts +82 -0
- package/src/contract/check.ts +2 -1
- package/src/contract/handler-scan.ts +32 -0
- package/src/deterministic-tar.ts +120 -0
- package/src/dev.ts +4 -2
- package/src/enable.ts +28 -0
- package/src/index.ts +63 -2
- package/src/info.ts +3 -2
- package/src/init.ts +147 -14
- package/src/install.ts +268 -0
- package/src/keygen.ts +88 -0
- package/src/layout.ts +31 -0
- package/src/lifecycle/__tests__/authoring.test.ts +109 -0
- package/src/lifecycle/__tests__/bundle.test.ts +150 -0
- package/src/lifecycle/__tests__/deterministic-tar.test.ts +145 -0
- package/src/lifecycle/__tests__/fixtures/README.md +59 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/EXPECTED-VIOLATIONS.md +27 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/api/src/Demo/manifest.yaml +6 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/manifest.yaml +40 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/entry.ts +14 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/en-US.json +12 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/zh-CN.json +12 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/manifest.yaml +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/migrations/0001__init.sql +6 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/ping/api.ts +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/schema.yaml +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/manifest.yaml +96 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/entry.ts +23 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/en-US.json +29 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/zh-CN.json +29 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz +0 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.bad.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.unknown.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.d.mts +21 -0
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.mjs +196 -0
- package/src/lifecycle/__tests__/helpers.ts +78 -0
- package/src/lifecycle/__tests__/install.test.ts +514 -0
- package/src/lifecycle/__tests__/keygen.test.ts +61 -0
- package/src/lifecycle/__tests__/manifest-contract.test.ts +228 -0
- package/src/lifecycle/__tests__/publish.test.ts +80 -0
- package/src/lifecycle/__tests__/sig-fixtures.test.ts +100 -0
- package/src/lifecycle/__tests__/sign.test.ts +131 -0
- package/src/lifecycle/__tests__/smoke.test.ts +61 -0
- package/src/lifecycle/__tests__/trust.test.ts +177 -0
- package/src/lifecycle/__tests__/uninstall.test.ts +87 -0
- package/src/lifecycle/keygen.ts +40 -0
- package/src/lifecycle/sign.ts +74 -0
- package/src/manifest-contract.ts +696 -0
- package/src/oj-cert.ts +19 -0
- package/src/oj-cmd.ts +3 -3
- package/src/preview.ts +2 -2
- package/src/publish.ts +150 -0
- package/src/sign.ts +128 -0
- package/src/site.ts +146 -0
- package/src/trust.ts +116 -0
- package/src/uninstall.ts +29 -0
- package/src/usage.ts +35 -1
- package/templates/README.md +1 -1
- package/templates/api/config.yaml +47 -0
- package/templates/api/seed.mysql.sql +1 -1
- package/templates/api/seed.pg.sql +1 -1
- package/templates/api/src/_platform/_lifecycle/atomic.ts +98 -0
- package/templates/api/src/_platform/_lifecycle/history.ts +31 -0
- package/templates/api/src/_platform/_lifecycle/installer.ts +1080 -0
- package/templates/api/src/_platform/_lifecycle/lock.ts +148 -0
- package/templates/api/src/_platform/_lifecycle/semver.ts +61 -0
- package/templates/api/src/_platform/_lifecycle/sha384.ts +187 -0
- package/templates/api/src/_platform/_lifecycle/state.ts +256 -0
- package/templates/api/src/_platform/_lifecycle/steps/deploy-web.ts +182 -0
- package/templates/api/src/_platform/_lifecycle/steps/download.ts +90 -0
- package/templates/api/src/_platform/_lifecycle/steps/extract.ts +239 -0
- package/templates/api/src/_platform/_lifecycle/steps/migrate.ts +363 -0
- package/templates/api/src/_platform/_lifecycle/steps/preflight.ts +590 -0
- package/templates/api/src/_platform/_lifecycle/steps/register.ts +211 -0
- package/templates/api/src/_platform/_lifecycle/steps/reload.ts +81 -0
- package/templates/api/src/_platform/_lifecycle/steps/smoke.ts +216 -0
- package/templates/api/src/_platform/_lifecycle/steps/verify.ts +207 -0
- package/templates/api/src/_platform/_lifecycle/tasks.ts +119 -0
- package/templates/api/src/_platform/_lifecycle/text-edit.ts +110 -0
- package/templates/api/src/_platform/_lifecycle/tgz-guard.ts +41 -0
- package/templates/api/src/_platform/_lifecycle/trust.ts +229 -0
- package/templates/api/src/_platform/_lifecycle/types.ts +94 -0
- package/templates/api/src/_platform/_lifecycle/uninstall.ts +283 -0
- package/templates/api/src/_platform/_lifecycle/verify-checks.ts +228 -0
- package/templates/api/src/_platform/_lifecycle/verify-report.ts +92 -0
- package/templates/api/src/_platform/_shared/service/access.ts +2 -2
- package/templates/api/src/_platform/_shared/service/settings.ts +10 -1
- package/templates/api/src/_platform/_shared/service/solution-perms.ts +14 -7
- package/templates/api/src/_platform/_shared/site-facade.ts +44 -0
- package/templates/api/src/_platform/manifest.yaml +1 -0
- package/templates/api/src/_platform/market/api.ts +883 -0
- package/templates/api/src/_platform/market/bootstrap.ts +39 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.mysql.sql +17 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.pg.sql +16 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.sql +18 -0
- package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.mysql.sql +2 -0
- package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.sql +4 -0
- package/templates/api/src/_platform/migrations/0012__market_history.mysql.sql +13 -0
- package/templates/api/src/_platform/migrations/0012__market_history.pg.sql +12 -0
- package/templates/api/src/_platform/migrations/0012__market_history.sql +14 -0
- package/templates/api/src/_platform/schema.yaml +21 -4
- package/templates/api/src/_platform/seed.sql +2 -2
- package/templates/api/src/_platform/solutions/api.ts +212 -0
- package/templates/api/src/demo/manifest.yaml +5 -0
- package/templates/api/src/demo/migrations/0001__create_todos.sql +9 -0
- package/templates/api/src/demo/schema.yaml +11 -0
- package/templates/api/src/demo/seed.sql +5 -0
- package/templates/api/src/demo/todos/api.ts +59 -0
- package/templates/api/src/home/line/api.ts +57 -0
- package/templates/api/src/home/manifest.yaml +6 -0
- package/templates/api/src/home/migrations/0001__create_home_tables.sql +13 -0
- package/templates/api/src/home/pie/api.ts +40 -0
- package/templates/api/src/home/schema.yaml +14 -0
- package/templates/api/src/home/seed.sql +70 -0
- package/templates/api/src/system/oplog/api.ts +44 -2
- package/templates/api/tests/fixture.ts +16 -0
- package/templates/api/tests/lifecycle/atomic-state.spec.ts +183 -0
- package/templates/api/tests/lifecycle/bootstrap.spec.ts +116 -0
- package/templates/api/tests/lifecycle/deploy-smoke.spec.ts +492 -0
- package/templates/api/tests/lifecycle/installer.spec.ts +1218 -0
- package/templates/api/tests/lifecycle/market-api.spec.ts +971 -0
- package/templates/api/tests/lifecycle/migrate.spec.ts +575 -0
- package/templates/api/tests/lifecycle/preflight.spec.ts +255 -0
- package/templates/api/tests/lifecycle/register.spec.ts +194 -0
- package/templates/api/tests/lifecycle/semver.spec.ts +43 -0
- package/templates/api/tests/lifecycle/sha384.spec.ts +48 -0
- package/templates/api/tests/lifecycle/sig-fixtures.spec.ts +65 -0
- package/templates/api/tests/lifecycle/solutions-api.spec.ts +98 -0
- package/templates/api/tests/lifecycle/steps-fs.spec.ts +623 -0
- package/templates/api/tests/lifecycle/text-edit.spec.ts +177 -0
- package/templates/api/tests/lifecycle/tgz-guard.spec.ts +103 -0
- package/templates/api/tests/lifecycle/trust.spec.ts +310 -0
- package/templates/api/tests/lifecycle/uninstall.spec.ts +386 -0
- package/templates/api/tests/lifecycle/verify-checks.spec.ts +385 -0
- package/templates/api/tests/lifecycle/verify-report.spec.ts +290 -0
- package/templates/api/tests/rbac-settings.spec.ts +8 -0
- package/templates/init.include.yaml +56 -0
- package/templates/openapi.json +527 -0
- package/templates/package.json +52 -0
- package/templates/web/src/_platform/__client/api.schemas.ts +8 -0
- package/templates/web/src/_platform/__client/api.ts +335 -0
- package/templates/web/src/about/entry.ts +42 -0
- package/templates/web/src/about/locales/en-US.json +16 -0
- package/templates/web/src/about/locales/zh-CN.json +16 -0
- package/templates/web/src/about/pages/constants.ts +20 -0
- package/templates/web/src/about/pages/index.tsx +135 -0
- package/templates/web/src/access/entry.ts +74 -0
- package/templates/web/src/access/locales/en-US.json +46 -0
- package/templates/web/src/access/locales/zh-CN.json +46 -0
- package/templates/web/src/access/pages/access-mode/index.tsx +16 -0
- package/templates/web/src/access/pages/admin-visible/index.tsx +16 -0
- package/templates/web/src/access/pages/button-control/index.tsx +181 -0
- package/templates/web/src/access/pages/common-visible/index.tsx +16 -0
- package/templates/web/src/access/pages/page-control/index.tsx +142 -0
- package/templates/web/src/demo/__client/api.schemas.ts +20 -0
- package/templates/web/src/demo/__client/api.ts +89 -0
- package/templates/web/src/exception/entry.ts +80 -0
- package/templates/web/src/exception/locales/en-US.json +13 -0
- package/templates/web/src/exception/locales/zh-CN.json +13 -0
- package/templates/web/src/exception/pages/403/index.tsx +30 -0
- package/templates/web/src/exception/pages/404/index.tsx +30 -0
- package/templates/web/src/exception/pages/500/index.tsx +30 -0
- package/templates/web/src/exception/pages/unknown-component/index.tsx +38 -0
- package/templates/web/src/home/__client/api.schemas.ts +22 -0
- package/templates/web/src/home/__client/api.ts +113 -0
- package/templates/web/src/login/locales/en-US.json +6 -0
- package/templates/web/src/login/locales/zh-CN.json +6 -0
- package/templates/web/src/login/pages/login.tsx +67 -18
- package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +93 -0
- package/templates/web/src/oj-market/__tests__/HistoryDrawer.test.tsx +28 -0
- package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +69 -0
- package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +43 -0
- package/templates/web/src/oj-market/__tests__/UninstallConfirm.test.tsx +30 -0
- package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +70 -0
- package/templates/web/src/oj-market/__tests__/poll.test.ts +24 -0
- package/templates/web/src/oj-market/__tests__/render.tsx +14 -0
- package/templates/web/src/oj-market/__tests__/runtime-stub.ts +14 -0
- package/templates/web/src/oj-market/__tests__/semver.test.ts +26 -0
- package/templates/web/src/oj-market/api/MarketClientProvider.tsx +12 -0
- package/templates/web/src/oj-market/api/client.ts +135 -0
- package/templates/web/src/oj-market/api/context.ts +11 -0
- package/templates/web/src/oj-market/api/defaultClient.ts +7 -0
- package/templates/web/src/oj-market/components/CardGrid.tsx +76 -0
- package/templates/web/src/oj-market/components/CardItem.tsx +102 -0
- package/templates/web/src/oj-market/components/DetailDrawer.tsx +53 -0
- package/templates/web/src/oj-market/components/HistoryDrawer.tsx +75 -0
- package/templates/web/src/oj-market/components/InstallDialog.tsx +112 -0
- package/templates/web/src/oj-market/components/RollbackDialog.tsx +79 -0
- package/templates/web/src/oj-market/components/UninstallConfirm.tsx +74 -0
- package/templates/web/src/oj-market/entry.ts +27 -0
- package/templates/web/src/oj-market/hooks/useMarket.ts +91 -0
- package/templates/web/src/oj-market/locales/en-US.json +100 -0
- package/templates/web/src/oj-market/locales/zh-CN.json +100 -0
- package/templates/web/src/oj-market/pages/MarketApp.tsx +136 -0
- package/templates/web/src/oj-market/types.ts +84 -0
- package/templates/web/src/oj-market/utils/poll.ts +36 -0
- package/templates/web/src/oj-market/utils/semver.ts +14 -0
- package/templates/web/src/outside/entry.ts +80 -0
- package/templates/web/src/outside/locales/en-US.json +10 -0
- package/templates/web/src/outside/locales/zh-CN.json +10 -0
- package/templates/web/src/personal-center/entry.ts +1 -10
- package/templates/web/src/personal-center/locales/en-US.json +1 -5
- package/templates/web/src/personal-center/locales/zh-CN.json +1 -5
- package/templates/web/src/route-nest/entry.ts +73 -0
- package/templates/web/src/route-nest/locales/en-US.json +9 -0
- package/templates/web/src/route-nest/locales/zh-CN.json +9 -0
- package/templates/web/src/route-nest/pages/menu1/menu1-1/index.tsx +5 -0
- package/templates/web/src/route-nest/pages/menu1/menu1-2/index.tsx +5 -0
- package/templates/web/src/route-nest/pages/menu2/index.tsx +5 -0
- package/templates/web/src/system/__client/api.schemas.ts +12 -0
- package/templates/web/src/system/__client/api.ts +25 -1
- package/templates/web/src/system/pages/oplog/index.tsx +1 -1
- package/templates/web/src/system/pages/roles/index.tsx +1 -0
- package/templates/web/src/system/pages/settings/index.tsx +1 -1
- package/templates/web/src/system/pages/users/index.tsx +1 -1
- package/templates/web.config.ts +32 -40
- package/vendor/host-versions.json +3 -3
- package/templates/pnpm-workspace.yaml +0 -14
- package/templates/web/src/personal-center/pages/settings/index.tsx +0 -5
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { IndexRow, TaskRecord } from "../types";
|
|
2
|
+
import { Alert, Button, Modal, Progress, Space, Typography } from "antd";
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import { useMarketClient } from "../api/context";
|
|
6
|
+
import { useInstallTask } from "../hooks/useMarket";
|
|
7
|
+
|
|
8
|
+
export interface InstallDialogProps {
|
|
9
|
+
row: IndexRow | null
|
|
10
|
+
open: boolean
|
|
11
|
+
onClose: () => void
|
|
12
|
+
onDone?: (record: TaskRecord) => void
|
|
13
|
+
/** 显式目标版本(回滚用);缺省取 row.latest(安装/升级) */
|
|
14
|
+
version?: string | null
|
|
15
|
+
/** 动作模式:安装/升级/回滚(仅影响标题文案;回滚 = 对旧版降级安装) */
|
|
16
|
+
mode?: "install" | "upgrade" | "rollback"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const isTerminal = (r: TaskRecord | null) => r != null && r.status !== "running";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 安装/升级/回滚对话框:触发 POST install → 800ms 轮询 TaskRecord 终态渲染(reason+retryable+hintKey 双语)。
|
|
23
|
+
* 回滚 = 对已装旧版降级安装(显式 version);降版迁移差异校验(R12)的可读失败由终态 error 渲染。
|
|
24
|
+
*/
|
|
25
|
+
export function InstallDialog({ row, open, onClose, onDone, version, mode }: InstallDialogProps) {
|
|
26
|
+
const { t } = useTranslation();
|
|
27
|
+
const client = useMarketClient();
|
|
28
|
+
const [taskId, setTaskId] = useState<string | null>(null);
|
|
29
|
+
const [started, setStarted] = useState(false);
|
|
30
|
+
const { record, done } = useInstallTask(taskId);
|
|
31
|
+
|
|
32
|
+
const targetVersion = version ?? row?.latest ?? null;
|
|
33
|
+
const title
|
|
34
|
+
= mode === "rollback"
|
|
35
|
+
? t("oj-market:rollback:running")
|
|
36
|
+
: row?.installed
|
|
37
|
+
? t("oj-market:card:upgrade")
|
|
38
|
+
: t("oj-market:task:installing");
|
|
39
|
+
|
|
40
|
+
// 打开即触发安装(version 缺省取 latest;已装视为升级;显式 version = 回滚降级安装)
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (open && row && !started) {
|
|
43
|
+
setStarted(true);
|
|
44
|
+
client
|
|
45
|
+
.install({ name: row.name, version: targetVersion, source: "cdn" })
|
|
46
|
+
.then(res => setTaskId(res.task_id))
|
|
47
|
+
.catch(e => setTaskId(`__error__${e instanceof Error ? e.message : String(e)}`));
|
|
48
|
+
}
|
|
49
|
+
if (!open) {
|
|
50
|
+
setTaskId(null);
|
|
51
|
+
setStarted(false);
|
|
52
|
+
}
|
|
53
|
+
}, [open, row, started, client, targetVersion]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (isTerminal(record) && done)
|
|
57
|
+
onDone?.(record);
|
|
58
|
+
}, [record, done, onDone]);
|
|
59
|
+
|
|
60
|
+
const terminal = isTerminal(record);
|
|
61
|
+
const failed = record?.status === "failed" || record?.status === "partial";
|
|
62
|
+
const errorTask = typeof taskId === "string" && taskId.startsWith("__error__");
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Modal
|
|
66
|
+
title={title}
|
|
67
|
+
open={open}
|
|
68
|
+
onCancel={onClose}
|
|
69
|
+
footer={terminal ? <Button onClick={onClose}>{t("oj-market:task:close")}</Button> : null}
|
|
70
|
+
data-testid="install-dialog"
|
|
71
|
+
>
|
|
72
|
+
{errorTask && <Alert type="error" message={taskId!.slice("__error__".length)} />}
|
|
73
|
+
{!errorTask && !record && (
|
|
74
|
+
<Typography.Text>
|
|
75
|
+
{t("oj-market:task:installing")}
|
|
76
|
+
…
|
|
77
|
+
</Typography.Text>
|
|
78
|
+
)}
|
|
79
|
+
{record && (
|
|
80
|
+
<>
|
|
81
|
+
<Progress percent={record.progress} status={terminal ? (failed ? "exception" : "success") : "active"} />
|
|
82
|
+
<div data-testid="install-stage">{record.stage ? t(`oj-market:task:stage.${record.stage}`, record.stage) : record.stage}</div>
|
|
83
|
+
<div data-testid="install-message">{record.message}</div>
|
|
84
|
+
{failed && record.error && (
|
|
85
|
+
<Alert
|
|
86
|
+
type="error"
|
|
87
|
+
showIcon
|
|
88
|
+
message={record.error.reason}
|
|
89
|
+
description={(
|
|
90
|
+
<Space direction="vertical">
|
|
91
|
+
<span>{record.error.retryable ? t("oj-market:task:retryable") : t("oj-market:task:notRetryable")}</span>
|
|
92
|
+
{record.error.hintKey && <span data-testid="install-hint">{t(record.error.hintKey, record.error.hintKey)}</span>}
|
|
93
|
+
</Space>
|
|
94
|
+
)}
|
|
95
|
+
/>
|
|
96
|
+
)}
|
|
97
|
+
{failed && (
|
|
98
|
+
<Button
|
|
99
|
+
style={{ marginTop: 12 }}
|
|
100
|
+
onClick={() => {
|
|
101
|
+
setTaskId(null);
|
|
102
|
+
setStarted(false);
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
{t("oj-market:card:retry")}
|
|
106
|
+
</Button>
|
|
107
|
+
)}
|
|
108
|
+
</>
|
|
109
|
+
)}
|
|
110
|
+
</Modal>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { IndexRow } from "../types";
|
|
2
|
+
import { Alert, Modal, Radio, Space, Spin, Typography } from "antd";
|
|
3
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import { useMarketHistory } from "../hooks/useMarket";
|
|
6
|
+
import { compareVersions } from "../utils/semver";
|
|
7
|
+
|
|
8
|
+
export interface RollbackDialogProps {
|
|
9
|
+
row: IndexRow | null
|
|
10
|
+
open: boolean
|
|
11
|
+
onClose: () => void
|
|
12
|
+
onRollback: (row: IndexRow, version: string) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 回滚版本选择(P1-15b):候选 = 该方案 market_history 出现过且版本 < 已装版本的历史版本
|
|
17
|
+
* (compareVersions;宿主 CDN 索引版本列表未进 IndexRow 契约,故以历史已装版本为回滚真源)。
|
|
18
|
+
* 回滚 = 对所选旧版重新安装(降级);迁移差异校验(R12)的可读失败由 InstallDialog 终态 error 渲染。
|
|
19
|
+
*/
|
|
20
|
+
export function RollbackDialog({ row, open, onClose, onRollback }: RollbackDialogProps) {
|
|
21
|
+
const { t } = useTranslation();
|
|
22
|
+
const { status, data } = useMarketHistory();
|
|
23
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
24
|
+
|
|
25
|
+
const candidates = useMemo(() => {
|
|
26
|
+
const installed = row?.installed;
|
|
27
|
+
if (!row || !installed)
|
|
28
|
+
return [] as string[];
|
|
29
|
+
const versions = new Set<string>();
|
|
30
|
+
for (const h of status === "ok" ? data : []) {
|
|
31
|
+
if (h.solution === row.name && compareVersions(h.version, installed) < 0)
|
|
32
|
+
versions.add(h.version);
|
|
33
|
+
}
|
|
34
|
+
return [...versions].sort((a, b) => compareVersions(b, a)); // 较新的旧版在前
|
|
35
|
+
}, [row, status, data]);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
setSelected(candidates.length ? candidates[0] : null);
|
|
39
|
+
}, [candidates]);
|
|
40
|
+
|
|
41
|
+
const loading = status === "loading";
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Modal
|
|
45
|
+
title={t("oj-market:rollback:title")}
|
|
46
|
+
open={open}
|
|
47
|
+
onCancel={onClose}
|
|
48
|
+
okText={t("oj-market:rollback:confirm")}
|
|
49
|
+
cancelText={t("oj-market:confirm:cancel")}
|
|
50
|
+
okButtonProps={{ danger: true, disabled: !selected }}
|
|
51
|
+
onOk={() => row && selected && onRollback(row, selected)}
|
|
52
|
+
data-testid="rollback-dialog"
|
|
53
|
+
>
|
|
54
|
+
<Typography.Paragraph>
|
|
55
|
+
{t("oj-market:rollback:body", { name: row?.name ?? "", installed: row?.installed ?? "-" })}
|
|
56
|
+
</Typography.Paragraph>
|
|
57
|
+
<Alert type="warning" showIcon message={t("oj-market:rollback:warning")} style={{ marginBottom: 12 }} />
|
|
58
|
+
{loading && <Spin />}
|
|
59
|
+
{!loading && candidates.length === 0 && (
|
|
60
|
+
<div data-testid="rollback-empty">{t("oj-market:rollback:noCandidates")}</div>
|
|
61
|
+
)}
|
|
62
|
+
{!loading && candidates.length > 0 && (
|
|
63
|
+
<Radio.Group
|
|
64
|
+
value={selected ?? undefined}
|
|
65
|
+
onChange={e => setSelected(e.target.value)}
|
|
66
|
+
data-testid="rollback-versions"
|
|
67
|
+
>
|
|
68
|
+
<Space direction="vertical">
|
|
69
|
+
{candidates.map(v => (
|
|
70
|
+
<Radio key={v} value={v}>
|
|
71
|
+
{v}
|
|
72
|
+
</Radio>
|
|
73
|
+
))}
|
|
74
|
+
</Space>
|
|
75
|
+
</Radio.Group>
|
|
76
|
+
)}
|
|
77
|
+
</Modal>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { IndexRow, UninstallImpact } from "../types";
|
|
2
|
+
import { Alert, Modal, Spin, Typography } from "antd";
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import { useMarketClient } from "../api/context";
|
|
6
|
+
|
|
7
|
+
export interface UninstallConfirmProps {
|
|
8
|
+
row: IndexRow | null
|
|
9
|
+
open: boolean
|
|
10
|
+
onClose: () => void
|
|
11
|
+
onConfirmed: (row: IndexRow) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 卸载二次确认(P3 U1 语义前移):打开即拉取影响面预检清单(受影响角色/用户计数),
|
|
16
|
+
* 供 superadmin 二次确认。空锁/级联由服务端(P1-15a)统一把关,UI 仅做可读呈现。
|
|
17
|
+
*/
|
|
18
|
+
export function UninstallConfirm({ row, open, onClose, onConfirmed }: UninstallConfirmProps) {
|
|
19
|
+
const { t } = useTranslation();
|
|
20
|
+
const client = useMarketClient();
|
|
21
|
+
const [impact, setImpact] = useState<UninstallImpact | null>(null);
|
|
22
|
+
const [loading, setLoading] = useState(false);
|
|
23
|
+
const [error, setError] = useState<string | null>(null);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!open || !row)
|
|
27
|
+
return;
|
|
28
|
+
setLoading(true);
|
|
29
|
+
setError(null);
|
|
30
|
+
setImpact(null);
|
|
31
|
+
client
|
|
32
|
+
.uninstallImpact(row.name)
|
|
33
|
+
.then(setImpact)
|
|
34
|
+
.catch(e => setError(e instanceof Error ? e.message : String(e)))
|
|
35
|
+
.finally(() => setLoading(false));
|
|
36
|
+
}, [open, row, client]);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Modal
|
|
40
|
+
title={t("oj-market:confirm:uninstallTitle")}
|
|
41
|
+
open={open}
|
|
42
|
+
onCancel={onClose}
|
|
43
|
+
okText={t("oj-market:confirm:ok")}
|
|
44
|
+
cancelText={t("oj-market:confirm:cancel")}
|
|
45
|
+
okButtonProps={{ danger: true }}
|
|
46
|
+
onOk={() => row && onConfirmed(row)}
|
|
47
|
+
data-testid="uninstall-confirm"
|
|
48
|
+
>
|
|
49
|
+
<Typography.Paragraph>
|
|
50
|
+
{t("oj-market:confirm:uninstallBody", { name: row?.name ?? "" })}
|
|
51
|
+
</Typography.Paragraph>
|
|
52
|
+
{loading && <Spin tip={t("oj-market:confirm:impactTitle")} />}
|
|
53
|
+
{error && <Alert type="warning" message={error} />}
|
|
54
|
+
{impact && (
|
|
55
|
+
<div data-testid="uninstall-impact">
|
|
56
|
+
<Typography.Text strong>{t("oj-market:confirm:impactTitle")}</Typography.Text>
|
|
57
|
+
<div>
|
|
58
|
+
{t("oj-market:history:colAction.uninstall")}
|
|
59
|
+
:
|
|
60
|
+
{impact.affectedRoles}
|
|
61
|
+
{" "}
|
|
62
|
+
·
|
|
63
|
+
{t("oj-market:history:colOperator")}
|
|
64
|
+
:
|
|
65
|
+
{impact.affectedUsers}
|
|
66
|
+
</div>
|
|
67
|
+
{impact.affectedRoles === 0 && impact.affectedUsers === 0 && (
|
|
68
|
+
<Typography.Text type="secondary">{t("oj-market:confirm:impactEmpty")}</Typography.Text>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</Modal>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineModule } from "@oj-module/runtime";
|
|
2
|
+
import MarketApp from "./pages/MarketApp";
|
|
3
|
+
|
|
4
|
+
// 方案市场前端模块(第一方专用,R29:消费 /api/_platform/market/* 编排面,第三方方案不得仿照此调用面)。
|
|
5
|
+
// 菜单标题走 i18next 命名空间语法 "<moduleName>:menu.<key>"(i18n 资源在 locales/,安装时随包注册)。
|
|
6
|
+
export default defineModule({
|
|
7
|
+
name: "oj-market",
|
|
8
|
+
description: "oj 方案市场应用——卡片展示/安装/卸载/历史(第一方专用,R29)",
|
|
9
|
+
version: "0.1.0",
|
|
10
|
+
routes: [
|
|
11
|
+
{
|
|
12
|
+
path: "/oj-market",
|
|
13
|
+
handle: { layout: "container", order: 200, title: "oj-market:menu.main" },
|
|
14
|
+
children: [
|
|
15
|
+
{
|
|
16
|
+
index: true,
|
|
17
|
+
Component: MarketApp,
|
|
18
|
+
handle: { title: "oj-market:menu.main" },
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
i18n: {
|
|
24
|
+
"zh-CN": () => import("./locales/zh-CN.json"),
|
|
25
|
+
"en-US": () => import("./locales/en-US.json"),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { IndexRow, MarketHistory, TaskRecord } from "../types";
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
import { useMarketClient } from "../api/context";
|
|
4
|
+
import { poll } from "../utils/poll";
|
|
5
|
+
|
|
6
|
+
/** 加载态三态(空态/加载/错误)统一建模,便于组件 DRY 渲染 */
|
|
7
|
+
export type LoadState<T> = { status: "loading" } | { status: "ok", data: T } | { status: "error", error: string };
|
|
8
|
+
|
|
9
|
+
/** 卡片索引加载(含重试);消费 GET /_platform/market/index */
|
|
10
|
+
export function useMarketIndex(): LoadState<IndexRow[]> & { retry: () => void } {
|
|
11
|
+
const client = useMarketClient();
|
|
12
|
+
const [state, setState] = useState<LoadState<IndexRow[]>>({ status: "loading" });
|
|
13
|
+
|
|
14
|
+
const load = useCallback(() => {
|
|
15
|
+
let active = true;
|
|
16
|
+
setState({ status: "loading" });
|
|
17
|
+
client
|
|
18
|
+
.index()
|
|
19
|
+
.then(data => active && setState({ status: "ok", data }))
|
|
20
|
+
.catch(e => active && setState({ status: "error", error: e instanceof Error ? e.message : String(e) }));
|
|
21
|
+
return () => {
|
|
22
|
+
active = false;
|
|
23
|
+
};
|
|
24
|
+
}, [client]);
|
|
25
|
+
|
|
26
|
+
useEffect(() => load(), [load]);
|
|
27
|
+
|
|
28
|
+
return { ...state, retry: load };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 任务轮询:给定 task_id,800ms 轮询至终态(done/failed/partial);超时不抛,返回当前值 */
|
|
32
|
+
export function useInstallTask(taskId: string | null): {
|
|
33
|
+
record: TaskRecord | null
|
|
34
|
+
done: boolean
|
|
35
|
+
} {
|
|
36
|
+
const client = useMarketClient();
|
|
37
|
+
const [record, setRecord] = useState<TaskRecord | null>(null);
|
|
38
|
+
const [done, setDone] = useState(false);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!taskId)
|
|
42
|
+
return;
|
|
43
|
+
let active = true;
|
|
44
|
+
setRecord(null);
|
|
45
|
+
setDone(false);
|
|
46
|
+
poll(
|
|
47
|
+
() => client.task(taskId),
|
|
48
|
+
r => r.status !== "running",
|
|
49
|
+
{ intervalMs: 800, timeoutMs: 30 * 60 * 1000 },
|
|
50
|
+
)
|
|
51
|
+
.then(rec => active && setRecord(rec))
|
|
52
|
+
.catch(e => active && setRecord({
|
|
53
|
+
id: taskId,
|
|
54
|
+
name: "",
|
|
55
|
+
version: "",
|
|
56
|
+
status: "failed",
|
|
57
|
+
stage: null,
|
|
58
|
+
progress: 0,
|
|
59
|
+
message: e instanceof Error ? e.message : String(e),
|
|
60
|
+
stages: [],
|
|
61
|
+
}))
|
|
62
|
+
.finally(() => active && setDone(true));
|
|
63
|
+
return () => {
|
|
64
|
+
active = false;
|
|
65
|
+
};
|
|
66
|
+
}, [client, taskId]);
|
|
67
|
+
|
|
68
|
+
return { record, done };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 市场操作历史加载(只读消费本仓端点) */
|
|
72
|
+
export function useMarketHistory(): LoadState<MarketHistory[]> & { retry: () => void } {
|
|
73
|
+
const client = useMarketClient();
|
|
74
|
+
const [state, setState] = useState<LoadState<MarketHistory[]>>({ status: "loading" });
|
|
75
|
+
|
|
76
|
+
const load = useCallback(() => {
|
|
77
|
+
let active = true;
|
|
78
|
+
setState({ status: "loading" });
|
|
79
|
+
client
|
|
80
|
+
.history()
|
|
81
|
+
.then(data => active && setState({ status: "ok", data }))
|
|
82
|
+
.catch(e => active && setState({ status: "error", error: e instanceof Error ? e.message : String(e) }));
|
|
83
|
+
return () => {
|
|
84
|
+
active = false;
|
|
85
|
+
};
|
|
86
|
+
}, [client]);
|
|
87
|
+
|
|
88
|
+
useEffect(() => load(), [load]);
|
|
89
|
+
|
|
90
|
+
return { ...state, retry: load };
|
|
91
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"menu": {
|
|
3
|
+
"main": "Market"
|
|
4
|
+
},
|
|
5
|
+
"perm": {
|
|
6
|
+
"view": "View solution market"
|
|
7
|
+
},
|
|
8
|
+
"role": {
|
|
9
|
+
"viewer": "Solution market viewer"
|
|
10
|
+
},
|
|
11
|
+
"settings": {
|
|
12
|
+
"group": {
|
|
13
|
+
"display": "Display preferences"
|
|
14
|
+
},
|
|
15
|
+
"display": {
|
|
16
|
+
"greeting": "Greeting"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"card": {
|
|
20
|
+
"title": "Solution Market",
|
|
21
|
+
"subtitle": "Browse, install and manage oj solutions",
|
|
22
|
+
"loading": "Loading cards…",
|
|
23
|
+
"empty": "No solutions available (market address not configured or index empty)",
|
|
24
|
+
"error": "Failed to load cards",
|
|
25
|
+
"retry": "Retry",
|
|
26
|
+
"version": "Version",
|
|
27
|
+
"latest": "Latest",
|
|
28
|
+
"installed": "Installed",
|
|
29
|
+
"notInstalled": "Not installed",
|
|
30
|
+
"enabled": "Enabled",
|
|
31
|
+
"disabled": "Disabled",
|
|
32
|
+
"upgradable": "Upgradable",
|
|
33
|
+
"details": "Details",
|
|
34
|
+
"history": "History",
|
|
35
|
+
"install": "Install",
|
|
36
|
+
"uninstall": "Uninstall",
|
|
37
|
+
"enable": "Enable",
|
|
38
|
+
"disable": "Disable",
|
|
39
|
+
"upgrade": "Upgrade",
|
|
40
|
+
"rollback": "Rollback"
|
|
41
|
+
},
|
|
42
|
+
"task": {
|
|
43
|
+
"installing": "Installing",
|
|
44
|
+
"uninstalling": "Uninstalling",
|
|
45
|
+
"progress": "Progress",
|
|
46
|
+
"stage": "Stage",
|
|
47
|
+
"status": {
|
|
48
|
+
"running": "Running",
|
|
49
|
+
"done": "Done",
|
|
50
|
+
"failed": "Failed",
|
|
51
|
+
"partial": "Partial"
|
|
52
|
+
},
|
|
53
|
+
"reason": "Reason",
|
|
54
|
+
"retryable": "Retryable",
|
|
55
|
+
"notRetryable": "Not retryable",
|
|
56
|
+
"hint": "Hint",
|
|
57
|
+
"close": "Close",
|
|
58
|
+
"viewLog": "View log"
|
|
59
|
+
},
|
|
60
|
+
"confirm": {
|
|
61
|
+
"uninstallTitle": "Confirm uninstall",
|
|
62
|
+
"uninstallBody": "Uninstalling \"{{name}}\" affects the roles and users below, and the site must keep at least one locked module. This cannot be undone. Continue?",
|
|
63
|
+
"impactTitle": "Impact pre-check",
|
|
64
|
+
"impactEmpty": "No affected roles or users (orphan solution)",
|
|
65
|
+
"cancel": "Cancel",
|
|
66
|
+
"ok": "Confirm uninstall"
|
|
67
|
+
},
|
|
68
|
+
"rollback": {
|
|
69
|
+
"title": "Rollback version",
|
|
70
|
+
"running": "Rolling back",
|
|
71
|
+
"body": "Pick a prior version to roll \"{{name}}\" back to (currently installed {{installed}}). Rollback reinstalls the chosen older version; if the migration diff check (R12) fails you will get a readable error.",
|
|
72
|
+
"selectLabel": "Target version",
|
|
73
|
+
"noCandidates": "No earlier version in history to roll back to",
|
|
74
|
+
"confirm": "Confirm rollback",
|
|
75
|
+
"warning": "Rollback reinstalls the chosen older version; it is rejected if the older migration is irreversible."
|
|
76
|
+
},
|
|
77
|
+
"history": {
|
|
78
|
+
"title": "Operation history",
|
|
79
|
+
"empty": "No records yet",
|
|
80
|
+
"colSolution": "Solution",
|
|
81
|
+
"colVersion": "Version",
|
|
82
|
+
"colAction": "Action",
|
|
83
|
+
"colStatus": "Status",
|
|
84
|
+
"colOperator": "Operator",
|
|
85
|
+
"colTime": "Time",
|
|
86
|
+
"action": {
|
|
87
|
+
"install": "Install",
|
|
88
|
+
"uninstall": "Uninstall",
|
|
89
|
+
"enable": "Enable",
|
|
90
|
+
"disable": "Disable",
|
|
91
|
+
"upgrade": "Upgrade",
|
|
92
|
+
"rollback": "Rollback"
|
|
93
|
+
},
|
|
94
|
+
"status": {
|
|
95
|
+
"done": "Done",
|
|
96
|
+
"failed": "Failed",
|
|
97
|
+
"partial": "Partial"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"menu": {
|
|
3
|
+
"main": "方案市场"
|
|
4
|
+
},
|
|
5
|
+
"perm": {
|
|
6
|
+
"view": "查看方案市场"
|
|
7
|
+
},
|
|
8
|
+
"role": {
|
|
9
|
+
"viewer": "方案市场查看者"
|
|
10
|
+
},
|
|
11
|
+
"settings": {
|
|
12
|
+
"group": {
|
|
13
|
+
"display": "显示偏好"
|
|
14
|
+
},
|
|
15
|
+
"display": {
|
|
16
|
+
"greeting": "问候语"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"card": {
|
|
20
|
+
"title": "方案市场",
|
|
21
|
+
"subtitle": "浏览、安装与管理 oj 方案",
|
|
22
|
+
"loading": "卡片加载中…",
|
|
23
|
+
"empty": "市场暂无可用方案(未配置市场地址或索引为空)",
|
|
24
|
+
"error": "卡片加载失败",
|
|
25
|
+
"retry": "重试",
|
|
26
|
+
"version": "版本",
|
|
27
|
+
"latest": "最新",
|
|
28
|
+
"installed": "已装",
|
|
29
|
+
"notInstalled": "未安装",
|
|
30
|
+
"enabled": "已启用",
|
|
31
|
+
"disabled": "已停用",
|
|
32
|
+
"upgradable": "可升级",
|
|
33
|
+
"details": "详情",
|
|
34
|
+
"history": "历史",
|
|
35
|
+
"install": "安装",
|
|
36
|
+
"uninstall": "卸载",
|
|
37
|
+
"enable": "启用",
|
|
38
|
+
"disable": "停用",
|
|
39
|
+
"upgrade": "升级",
|
|
40
|
+
"rollback": "回滚"
|
|
41
|
+
},
|
|
42
|
+
"task": {
|
|
43
|
+
"installing": "安装中",
|
|
44
|
+
"uninstalling": "卸载中",
|
|
45
|
+
"progress": "进度",
|
|
46
|
+
"stage": "阶段",
|
|
47
|
+
"status": {
|
|
48
|
+
"running": "进行中",
|
|
49
|
+
"done": "完成",
|
|
50
|
+
"failed": "失败",
|
|
51
|
+
"partial": "部分完成"
|
|
52
|
+
},
|
|
53
|
+
"reason": "原因",
|
|
54
|
+
"retryable": "可重试",
|
|
55
|
+
"notRetryable": "不可重试",
|
|
56
|
+
"hint": "提示",
|
|
57
|
+
"close": "关闭",
|
|
58
|
+
"viewLog": "查看日志"
|
|
59
|
+
},
|
|
60
|
+
"confirm": {
|
|
61
|
+
"uninstallTitle": "确认卸载",
|
|
62
|
+
"uninstallBody": "卸载「{{name}}」将影响以下角色与用户,且站点至少保留一个已锁模块。此操作不可撤销,确定继续?",
|
|
63
|
+
"impactTitle": "影响面预检",
|
|
64
|
+
"impactEmpty": "无受影响角色或用户(孤立方案)",
|
|
65
|
+
"cancel": "取消",
|
|
66
|
+
"ok": "确认卸载"
|
|
67
|
+
},
|
|
68
|
+
"rollback": {
|
|
69
|
+
"title": "回滚版本",
|
|
70
|
+
"running": "回滚中",
|
|
71
|
+
"body": "选择「{{name}}」要回滚到的历史版本(当前已装 {{installed}})。回滚即按所选旧版重新安装,迁移差异校验(R12)不通过时会给出可读失败。",
|
|
72
|
+
"selectLabel": "目标版本",
|
|
73
|
+
"noCandidates": "无更早的历史版本可回滚",
|
|
74
|
+
"confirm": "确认回滚",
|
|
75
|
+
"warning": "回滚会按所选旧版重新安装;若旧版迁移不可逆将被拒绝。"
|
|
76
|
+
},
|
|
77
|
+
"history": {
|
|
78
|
+
"title": "操作历史",
|
|
79
|
+
"empty": "暂无操作记录",
|
|
80
|
+
"colSolution": "方案",
|
|
81
|
+
"colVersion": "版本",
|
|
82
|
+
"colAction": "操作",
|
|
83
|
+
"colStatus": "状态",
|
|
84
|
+
"colOperator": "操作人",
|
|
85
|
+
"colTime": "时间",
|
|
86
|
+
"action": {
|
|
87
|
+
"install": "安装",
|
|
88
|
+
"uninstall": "卸载",
|
|
89
|
+
"enable": "启用",
|
|
90
|
+
"disable": "停用",
|
|
91
|
+
"upgrade": "升级",
|
|
92
|
+
"rollback": "回滚"
|
|
93
|
+
},
|
|
94
|
+
"status": {
|
|
95
|
+
"done": "成功",
|
|
96
|
+
"failed": "失败",
|
|
97
|
+
"partial": "部分"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|