diff --git a/web/hwlab-cloud-web/scripts/r6-architecture-parity.test.ts b/web/hwlab-cloud-web/scripts/r6-architecture-parity.test.ts index 2d69b01b..42b87482 100644 --- a/web/hwlab-cloud-web/scripts/r6-architecture-parity.test.ts +++ b/web/hwlab-cloud-web/scripts/r6-architecture-parity.test.ts @@ -91,9 +91,9 @@ test("R6 common module map keeps HWLAB views on shared console primitives", () = [ "AsyncBoundary", "OperationsWorkspace", - "EntityIndex", - "InspectorPanel", - "ViewModeSwitch", + "AccessUserIndex", + "AccessPermissionWorkspace", + "AccessUserInspector", ], ], [ @@ -101,9 +101,8 @@ test("R6 common module map keeps HWLAB views on shared console primitives", () = [ "AsyncBoundary", "OperationsWorkspace", - "EntityIndex", - "FactPath", - "ViewModeSwitch", + "SecretInventoryIndex", + "SecretInventoryWorkspace", ], ], [ @@ -111,8 +110,9 @@ test("R6 common module map keeps HWLAB views on shared console primitives", () = [ "AsyncBoundary", "OperationsWorkspace", - "EntityIndex", - "InspectorPanel", + "UserDirectoryIndex", + "UserDirectoryCollection", + "UserAccountInspector", "ViewModeSwitch", ], ], @@ -121,8 +121,8 @@ test("R6 common module map keeps HWLAB views on shared console primitives", () = [ "AsyncBoundary", "OperationsWorkspace", - "EntityIndex", - "InspectorPanel", + "BillingCodeIndex", + "BillingCodeInspector", "ViewModeSwitch", ], ], @@ -228,16 +228,16 @@ test("R2 console rewrite owns the final visual layer and removes private letter for (const icon of ["menu", "info", "logout", "empty", "project", "key", "provider", "skills"]) assert.match(iconSource, new RegExp(`\\b${icon}:`, "u"), `ConsoleIcon should define ${icon}`); - const migratedViews = [ - "src/components/common/EmptyState.vue", - "src/views/SkillsView.vue", - "src/views/projects/ProjectsView.vue", - "src/views/admin/ProviderProfilesView.vue", - "src/views/user/ApiKeysView.vue", + const migratedViews: Array<[string, string]> = [ + ["src/components/common/EmptyState.vue", "ConsoleIcon"], + ["src/views/SkillsView.vue", "EntityCard"], + ["src/views/projects/ProjectsView.vue", "ProjectResourceCard"], + ["src/views/admin/ProviderProfilesView.vue", "EntityCard"], + ["src/views/user/ApiKeysView.vue", "ApiKeyResourceCard"], ]; - for (const relativePath of migratedViews) { + for (const [relativePath, sharedComponent] of migratedViews) { const source = read(relativePath); - assert.match(source, /ConsoleIcon/u, `${relativePath} should use the shared icon system`); + assert.match(source, new RegExp(sharedComponent, "u"), `${relativePath} should use ${sharedComponent}`); assert.doesNotMatch(source, />\s*(?:SK|PJ|AI|KEY|HW)\s* { + const moduleExpectations: Array<[string, string[]]> = [ + ["src/components/access/AccessUserIndex.vue", ["EntityIndex", "AccessUserRow"]], + ["src/components/access/AccessPermissionWorkspace.vue", ["permission-matrix", "FactPath", "ViewModeSwitch"]], + ["src/components/access/AccessUserInspector.vue", ["InspectorPanel", "Tool grants", "Tuples"]], + ["src/components/secrets/SecretInventoryIndex.vue", ["EntityIndex", "StatusBadge"]], + ["src/components/secrets/SecretInventoryWorkspace.vue", ["FactPath", "SectionFrame", "安全边界"]], + ["src/components/users/UserDirectoryIndex.vue", ["EntityIndex", "Pagination"]], + ["src/components/users/UserDirectoryCollection.vue", ["ConsoleIcon", "DataGrid", "用户实体网格"]], + ["src/components/users/UserAccountInspector.vue", ["InspectorPanel", "entitlements", "reservations", "ledger"]], + ["src/components/billing/BillingCodeIndex.vue", ["EntityIndex", "RedeemCodeSummary"]], + ["src/components/billing/BillingCodeInspector.vue", ["InspectorPanel", "Credits", "Usage"]], + ["src/components/hwpod/HwpodDevicesWorkspace.vue", ["HwpodDeviceCard", "DataGrid", "ResourceCollection"]], + ["src/components/hwpod/HwpodNodesWorkspace.vue", ["挂载 HWPOD", "HwpodReadinessRail", "DataGrid"]], + ["src/components/hwpod/HwpodOnboardingWorkspace.vue", ["Python single file", "SHA-256", "Staged readiness"]], + ["src/components/common/OverlaySurface.vue", ["useOverlaySurface", "ConsoleIcon", "Teleport"]], + ["src/components/mdtodo/MdtodoTaskPanel.vue", ["ContentViewer", "editingBody", "Markdown"]], + ["src/components/mdtodo/MdtodoReportPanel.vue", ["ContentViewer", "openFullscreen", "closeFullscreen"]], + ]; + for (const [relativePath, terms] of moduleExpectations) { + const source = read(relativePath); + for (const term of terms) assert.match(source, new RegExp(term, "u"), `${relativePath} should contain ${term}`); + } + + const cardMigrations: Array<[string, string]> = [ + ["src/views/SkillsView.vue", "EntityCard"], + ["src/views/admin/ProviderProfilesView.vue", "EntityCard"], + ["src/views/projects/ProjectsView.vue", "ProjectResourceCard"], + ["src/views/user/ApiKeysView.vue", "ApiKeyResourceCard"], + ]; + for (const [relativePath, component] of cardMigrations) assert.match(read(relativePath), new RegExp(component, "u")); + + assert.doesNotMatch(read("src/views/SkillsView.vue"), /\.skill-card\b|\.skill-glyph\b/u); + assert.doesNotMatch(read("src/views/admin/ProviderProfilesView.vue"), /\.provider-card\b|\.provider-glyph\b/u); + assert.doesNotMatch(read("src/views/projects/ProjectsView.vue"), /\.project-card\b|\.project-glyph\b/u); + assert.doesNotMatch(read("src/views/user/ApiKeysView.vue"), /\.api-key-card\b|\.api-key-glyph\b/u); +}); + test("R6 Sub2API source comparison is recorded in parent/child issue scope", () => { const issueBody = readIssueBodyFixture(); assert.match(issueBody, /Wei-Shaw\/sub2api@v0\.1\.136=a2f76e4/u); diff --git a/web/hwlab-cloud-web/src/components/access/AccessMatrixPanel.vue b/web/hwlab-cloud-web/src/components/access/AccessMatrixPanel.vue index 64eaf31e..8ecbe935 100644 --- a/web/hwlab-cloud-web/src/components/access/AccessMatrixPanel.vue +++ b/web/hwlab-cloud-web/src/components/access/AccessMatrixPanel.vue @@ -3,13 +3,11 @@ import { computed, onMounted, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; import AsyncBoundary from "@/components/common/AsyncBoundary.vue"; import EmptyState from "@/components/common/EmptyState.vue"; -import EntityIndex from "@/components/console/EntityIndex.vue"; -import FactPath from "@/components/console/FactPath.vue"; -import InspectorPanel from "@/components/console/InspectorPanel.vue"; import OperationsWorkspace from "@/components/console/OperationsWorkspace.vue"; -import ViewModeSwitch from "@/components/console/ViewModeSwitch.vue"; -import SectionFrame from "@/components/layout/SectionFrame.vue"; import StatusStrip from "@/components/layout/StatusStrip.vue"; +import AccessPermissionWorkspace from "./AccessPermissionWorkspace.vue"; +import AccessUserIndex from "./AccessUserIndex.vue"; +import AccessUserInspector from "./AccessUserInspector.vue"; import { useConsoleViewState } from "@/composables/useConsoleViewState"; import { useAccessStore } from "@/stores/access"; import { @@ -135,16 +133,14 @@ function closeInspector(): void { access.clearSelection(); void router.push({ name: "Access", query: route.query }); } -function onRoleChange(event: Event): void { - const role = (event.target as HTMLSelectElement).value as AccessUserRole; +function onRoleChange(role: AccessUserRole): void { void access.updateSelectedUser({ role }); } -function onStatusChange(event: Event): void { - const status = (event.target as HTMLSelectElement).value as AccessUserStatus; +function onStatusChange(status: AccessUserStatus): void { void access.updateSelectedUser({ status }); } -function onToolChange(toolId: string, event: Event): void { - void access.setTool(toolId, (event.target as HTMLInputElement).checked); +function onToolChange(toolId: string, allowed: boolean): void { + void access.setTool(toolId, allowed); } function valueText(value: unknown): string { return typeof value === "string" && value.trim() @@ -182,219 +178,18 @@ function valueText(value: unknown): string { :inspector-open="Boolean(access.selectedUser)" > - -
- - - -
-
- 用户{{ tool.label }} -
- -
-
- -
-
-
- OpenFGA 技术详情 -
-
-
mode
-
{{ valueText(openfga.mode) }}
-
-
-
store
-
{{ valueText(openfga.storeName || openfga.storeId) }}
-
-
-
model
-
{{ valueText(openfga.modelId) }}
-
-
-
values
-
- {{ openfga.valuesRedacted === false ? "visible" : "redacted" }} -
-
-
-
-
+ - diff --git a/web/hwlab-cloud-web/src/views/admin/SecretsView.vue b/web/hwlab-cloud-web/src/views/admin/SecretsView.vue index a11b8f48..e025b125 100644 --- a/web/hwlab-cloud-web/src/views/admin/SecretsView.vue +++ b/web/hwlab-cloud-web/src/views/admin/SecretsView.vue @@ -6,13 +6,11 @@ import ApiErrorDiagnostic from "@/components/common/ApiErrorDiagnostic.vue"; import AsyncBoundary from "@/components/common/AsyncBoundary.vue"; import EmptyState from "@/components/common/EmptyState.vue"; import StatusBadge from "@/components/common/StatusBadge.vue"; -import EntityIndex from "@/components/console/EntityIndex.vue"; -import FactPath from "@/components/console/FactPath.vue"; import OperationsWorkspace from "@/components/console/OperationsWorkspace.vue"; -import ViewModeSwitch from "@/components/console/ViewModeSwitch.vue"; import PageCommandBar from "@/components/layout/PageCommandBar.vue"; -import SectionFrame from "@/components/layout/SectionFrame.vue"; import StatusStrip from "@/components/layout/StatusStrip.vue"; +import SecretInventoryIndex from "@/components/secrets/SecretInventoryIndex.vue"; +import SecretInventoryWorkspace from "@/components/secrets/SecretInventoryWorkspace.vue"; import { useConsoleViewState } from "@/composables/useConsoleViewState"; import { normalizeAdminSecretsRows, @@ -266,147 +264,15 @@ function displayDate(value: string): string { label="ExternalSecret 库存工作区" > -
-
- -
- -
- -
- -
-
-
Status
-
- -
-
-
-
Reason
-
{{ selected.reason || "-" }}
-
-
-
Refresh
-
{{ selected.refreshInterval || "-" }}
-
-
-
Age
-
{{ selected.ageText }}
-
-
-
Store scope
-
{{ selected.storeScope || "-" }}
-
-
-
Resource version
-
- {{ selected.targetSecretResourceVersion || "-" }} -
-
-
-
Last updated
-
{{ displayDate(selected.lastUpdated) }}
-
-
-
Generated
-
{{ displayDate(summary?.generatedAt || "") }}
-
-
-
-

- 本页不展示 Secret 值、原始 provider payload - 或可逆凭据。Plan、事件、fingerprint 与原始诊断仅在授权详情层披露。 -

-
-
- + - diff --git a/web/hwlab-cloud-web/src/views/user/ApiKeysView.vue b/web/hwlab-cloud-web/src/views/user/ApiKeysView.vue index b55f034b..51e5bdf6 100644 --- a/web/hwlab-cloud-web/src/views/user/ApiKeysView.vue +++ b/web/hwlab-cloud-web/src/views/user/ApiKeysView.vue @@ -9,7 +9,7 @@ import BaseDialog from "@/components/common/BaseDialog.vue"; import ConfirmDialog from "@/components/common/ConfirmDialog.vue"; import EmptyState from "@/components/common/EmptyState.vue"; import ResourceCollection from "@/components/common/ResourceCollection.vue"; -import ConsoleIcon from "@/components/console/ConsoleIcon.vue"; +import ApiKeyResourceCard from "@/components/api-keys/ApiKeyResourceCard.vue"; import BoundedWorkspace from "@/components/layout/BoundedWorkspace.vue"; import PageCommandBar from "@/components/layout/PageCommandBar.vue"; import StatusStrip from "@/components/layout/StatusStrip.vue"; @@ -196,26 +196,8 @@ function formatDate(value: unknown): string {
USER BILLING AUTHORITY

当前 API Keys

prefix / lifecycle / timestamps
- - + + @@ -286,21 +268,6 @@ function formatDate(value: unknown): string { .api-key-collection { display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 9px; } .api-key-collection[data-view="list"] { grid-template-columns: 1fr; } .api-key-collection[data-density="compact"] { gap: 5px; } -.api-key-card { display: grid; grid-template-columns: 42px minmax(0, 1fr) auto; gap: 10px; align-items: center; min-width: 0; border: 1px solid #d0dbe1; border-radius: 8px; background: linear-gradient(145deg, #fff, #f5f8f9); padding: 12px; color: inherit; text-decoration: none; } -.api-key-card:hover, -.api-key-card.selected { border-color: #31918f; box-shadow: 0 7px 18px rgba(28, 91, 91, .1); } -.api-key-collection[data-density="compact"] .api-key-card { padding: 8px 10px; } -.api-key-collection[data-view="list"] .api-key-card { grid-template-columns: 42px minmax(170px, 1fr) minmax(110px, .55fr) 90px minmax(130px, .6fr) auto; } -.api-key-glyph { display: grid; width: 38px; height: 38px; place-items: center; border: 1px solid #71b8b4; border-radius: 7px; background: #def2ef; color: #14615f; font: 850 9px/1 ui-monospace, monospace; } -.api-key-name { display: grid; min-width: 0; gap: 4px; } -.api-key-name strong { overflow: hidden; color: #1b2e36; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; } -.api-key-name code, -.api-key-prefix { overflow: hidden; color: #63757d; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; } -.api-key-time { display: none; min-width: 0; gap: 3px; } -.api-key-collection[data-view="list"] .api-key-time { display: grid; } -.api-key-time span { color: #7b8990; font-size: 8px; font-weight: 800; letter-spacing: .08em; } -.api-key-time strong { color: #3e5059; font-size: 10px; } -.api-key-arrow { color: #277e7c; } .api-key-inspector { position: sticky; top: 0; align-self: start; display: grid; gap: 13px; max-height: calc(100dvh - 120px); overflow: auto; padding: 14px; box-shadow: inset 3px 0 #17a39e; } .api-key-inspector > header a { display: grid; width: 28px; height: 28px; place-items: center; border: 1px solid #c7d1d7; border-radius: 6px; color: #344850; text-decoration: none; } .api-key-inspector dl { display: grid; gap: 0; margin: 0; } @@ -329,7 +296,6 @@ function formatDate(value: unknown): string { .api-key-summary-strip p { grid-column: 1 / -1; border-top: 1px solid #dbe3e7; padding: 10px 13px; } .api-key-collection, .api-key-collection[data-view="list"] { grid-template-columns: 1fr; } - .api-key-collection[data-view="list"] .api-key-card { grid-template-columns: 42px minmax(0, 1fr) auto; } .api-key-collection[data-view="list"] .api-key-prefix, .api-key-collection[data-view="list"] .api-key-time { display: none; } }