fix(ui): conceal sensitive collection defaults

This commit is contained in:
Vincent Koc
2026-07-31 14:35:20 +08:00
parent a83a7905f7
commit 711ce34c53
2 changed files with 84 additions and 13 deletions
@@ -132,6 +132,65 @@ describe("config form collection defaults", () => {
expect(onPatch).not.toHaveBeenCalled();
});
it("conceals sensitive collection defaults and disables restore until revealed", () => {
const container = document.createElement("div");
const onPatch = vi.fn();
const hints = {
"settings.profile": { sensitive: true },
"settings.tokens": { sensitive: true },
};
render(
renderObject(
{
schema: {
type: "object",
title: "Profile",
default: { apiKey: "default-secret" },
properties: { apiKey: { type: "string" } },
},
value: { apiKey: "authored-secret" },
path: ["settings", "profile"],
hints,
unsupported: new Set(),
disabled: false,
revealSensitive: false,
onPatch,
},
renderNode,
),
container,
);
expect(container.textContent).not.toContain("default-secret");
expect(resetButton(container).disabled).toBe(true);
render(
renderArray(
{
schema: {
type: "array",
title: "Tokens",
items: { type: "string" },
default: ["default-token"],
},
value: ["authored-token"],
path: ["settings", "tokens"],
hints,
unsupported: new Set(),
disabled: false,
revealSensitive: false,
onPatch,
},
renderNode,
),
container,
);
expect(container.textContent).not.toContain("default-token");
expect(resetButton(container).disabled).toBe(true);
});
it("removes an optional object as one value and keeps inherited children inherited", () => {
const container = document.createElement("div");
const onPatch = vi.fn();
@@ -53,16 +53,30 @@ import { configFieldId, hintForPath, type JsonSchema } from "./config-form.share
import { renderSettingsEmpty } from "./settings-ui.ts";
const UNSET_ARRAY_SOURCE_IDENTITY = Symbol("unset-array-source");
const UNSET_MAP_SOURCE_IDENTITY = Symbol("unset-map-source");
function collectionDefaultPresentation(params: ConfigNodeRenderParams, effectiveValue: unknown) {
const redacted = getSensitiveRenderState({
path: params.path,
value: effectiveValue,
hints: params.hints,
revealSensitive: params.revealSensitive ?? false,
isSensitivePathRevealed: params.isSensitivePathRevealed,
}).isRedacted;
return {
description: redacted ? nothing : renderSchemaDefaultDescription(params.schema, params.value),
action: renderRestoreDefaultButton({
...params,
disabled: params.disabled || redacted,
}),
};
}
function openCollectionDraft(event: Event, draftId: string): void {
const block = (event.currentTarget as HTMLElement).closest(".cfg-block");
const draft = Array.from(block?.children ?? []).find((child) => child.id === draftId);
const openDraft = (draft as Partial<ConfigFormCollectionDraft> | undefined)?.openDraft;
if (typeof openDraft === "function") {
openDraft.call(draft);
}
(draft as Partial<ConfigFormCollectionDraft> | undefined)?.openDraft?.call(draft);
}
const UNSET_MAP_SOURCE_IDENTITY = Symbol("unset-map-source");
export function renderObject(
params: ConfigNodeRenderParams,
@@ -97,6 +111,7 @@ export function renderObject(
fallback && typeof fallback === "object" && !Array.isArray(fallback)
? (fallback as Record<string, unknown>)
: {};
const defaultPresentation = collectionDefaultPresentation(params, fallback);
const entries = objectPropertyKeys(schema)
.map((key) => [key, objectPropertySchema(schema, key)] as const)
.filter((entry): entry is readonly [string, ConfigNodeRenderParams["schema"]] =>
@@ -206,14 +221,12 @@ export function renderObject(
<span class="settings-row__title">${label}</span>
${help ? html`<span class="settings-row__desc">${help}</span>` : nothing}
${schema.default !== undefined
? html`<span class="settings-row__desc"
>${renderSchemaDefaultDescription(schema, value)}</span
>`
? html`<span class="settings-row__desc">${defaultPresentation.description}</span>`
: nothing}
${renderTags(tags)}
</div>
<div class="settings-row__control">
${renderRestoreDefaultButton(params)}
${defaultPresentation.action}
<span class="settings-row__chevron cfg-object__chevron">${icons.chevronDown}</span>
</div>
</summary>
@@ -271,6 +284,7 @@ export function renderArray(
: Array.isArray(schema.default)
? schema.default
: UNSET_ARRAY_SOURCE_IDENTITY;
const defaultPresentation = collectionDefaultPresentation(params, arrayValue);
const rowIdentities = rowIdentitiesForArray(arrayValue);
const {
minItems: minimumItems,
@@ -352,9 +366,7 @@ export function renderArray(
${showLabel ? html`<span class="settings-row__title">${label}</span>` : nothing}
${showLabel && help ? html`<span class="settings-row__desc">${help}</span>` : nothing}
${showLabel && schema.default !== undefined
? html`<span class="settings-row__desc"
>${renderSchemaDefaultDescription(schema, value)}</span
>`
? html`<span class="settings-row__desc">${defaultPresentation.description}</span>`
: nothing}
${renderTags(tags)}
</div>
@@ -364,7 +376,7 @@ export function renderArray(
count: String(arrayValue.length),
})}</span
>
${renderRestoreDefaultButton(params)}
${defaultPresentation.action}
<button
type="button"
class="btn btn--sm"