diff --git a/src/components/agents/config/ConfigFlagsSummary.vue b/src/components/agents/config/ConfigFlagsSummary.vue new file mode 100644 index 00000000..c3d3773e --- /dev/null +++ b/src/components/agents/config/ConfigFlagsSummary.vue @@ -0,0 +1,83 @@ + + + diff --git a/src/components/agents/config/effective/AccessIcon.vue b/src/components/agents/config/effective/AccessIcon.vue new file mode 100644 index 00000000..065347fb --- /dev/null +++ b/src/components/agents/config/effective/AccessIcon.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/components/agents/config/effective/EditableField.vue b/src/components/agents/config/effective/EditableField.vue new file mode 100644 index 00000000..c3179cd9 --- /dev/null +++ b/src/components/agents/config/effective/EditableField.vue @@ -0,0 +1,135 @@ + + + diff --git a/src/components/agents/config/effective/InlineScalarEditor.vue b/src/components/agents/config/effective/InlineScalarEditor.vue new file mode 100644 index 00000000..63a85bd8 --- /dev/null +++ b/src/components/agents/config/effective/InlineScalarEditor.vue @@ -0,0 +1,250 @@ + + + diff --git a/src/components/agents/config/effective/MapFieldEditor.vue b/src/components/agents/config/effective/MapFieldEditor.vue new file mode 100644 index 00000000..482c5480 --- /dev/null +++ b/src/components/agents/config/effective/MapFieldEditor.vue @@ -0,0 +1,83 @@ + + + diff --git a/src/components/agents/config/effective/PendingPill.vue b/src/components/agents/config/effective/PendingPill.vue new file mode 100644 index 00000000..16d16d47 --- /dev/null +++ b/src/components/agents/config/effective/PendingPill.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/components/agents/config/effective/__tests__/emptyValue.spec.ts b/src/components/agents/config/effective/__tests__/emptyValue.spec.ts new file mode 100644 index 00000000..49a440f9 --- /dev/null +++ b/src/components/agents/config/effective/__tests__/emptyValue.spec.ts @@ -0,0 +1,34 @@ +import { describe, expect, it, vi } from 'vitest'; +import type { ConfigDoc } from '@/types/agent-config'; +import { definedInAnyBase, setStringKey } from '../emptyValue'; + +const PTR = '/plugins/p/config/k'; +const withKey = { plugins: { p: { config: { k: 'file' } } } } as ConfigDoc; +const without = { plugins: { p: { config: {} } } } as ConfigDoc; + +function fakeDraft() { + return { set: vi.fn(), unset: vi.fn() }; +} + +describe('emptied string keys', () => { + it('knows whether any base defines the key', () => { + expect(definedInAnyBase([null, without, withKey], PTR)).toBe(true); + expect(definedInAnyBase([null, without], PTR)).toBe(false); + }); + + it('empty unsets a key some file defines', () => { + const d = fakeDraft(); + setStringKey(d, [without, withKey], PTR, ''); + expect(d.unset).toHaveBeenCalledWith(PTR); + expect(d.set).not.toHaveBeenCalled(); + }); + + it('empty keeps "" for an overlay-only key; a value is always set', () => { + const d = fakeDraft(); + setStringKey(d, [without], PTR, ''); + expect(d.set).toHaveBeenCalledWith(PTR, ''); + setStringKey(d, [withKey], PTR, 'v'); + expect(d.set).toHaveBeenCalledWith(PTR, 'v'); + expect(d.unset).not.toHaveBeenCalled(); + }); +}); diff --git a/src/components/agents/config/effective/emptyValue.ts b/src/components/agents/config/effective/emptyValue.ts new file mode 100644 index 00000000..90942d4c --- /dev/null +++ b/src/components/agents/config/effective/emptyValue.ts @@ -0,0 +1,26 @@ +// The "emptied value" rule shared by the inline editors of string map keys (plugin config +// and labels): emptying a key that any instance's file defines inherits the file value again +// (the overlay drops the key); a key only the overlay adds keeps an empty string (its Remove / +// delete action removes it). +import type { OverlayDraft } from '@/composables/agent-config/useOverlayDraft'; +import type { ConfigDoc } from '@/types/agent-config'; +import { hasAt } from '@/utils/agent-config/json-pointer'; + +/** Whether any of `bases` (the instances' files) defines a value at `ptr`. */ +export function definedInAnyBase( + bases: readonly (ConfigDoc | null | undefined)[], + ptr: string, +): boolean { + return bases.some((b) => !!b && hasAt(b, ptr)); +} + +/** Writes `value` at `ptr`, applying the empty-value rule above. */ +export function setStringKey( + draft: Pick, + bases: readonly (ConfigDoc | null | undefined)[], + ptr: string, + value: string, +): void { + if (!value && definedInAnyBase(bases, ptr)) draft.unset(ptr); + else draft.set(ptr, value); +} diff --git a/src/components/agents/config/effective/nullKeys.ts b/src/components/agents/config/effective/nullKeys.ts new file mode 100644 index 00000000..42b9a8b0 --- /dev/null +++ b/src/components/agents/config/effective/nullKeys.ts @@ -0,0 +1,27 @@ +// policy_data is stored as an RFC 7396 merge patch, where `null` at an object key means +// "delete the key": a null value can't be stored for a key. Items of an array may be null (an +// array is written whole). +import { isPlainObject } from '@/utils/agent-config/merge-patch'; +import type { PatchOp } from '@/utils/agent-config/policy-data-patch'; + +export const NULL_KEY_ERROR = + "null can't be stored for a key (it means delete in an overlay)"; + +/** The dotted path of the first object key holding null in `v` (outside arrays), or null. */ +export function nullKeyPath(v: unknown, path: string[] = []): string | null { + if (!isPlainObject(v)) return null; + for (const [k, child] of Object.entries(v)) { + if (child === null) return [...path, k].join('.'); + const found = nullKeyPath(child, [...path, k]); + if (found !== null) return found; + } + return null; +} + +/** Whether `ops` (key-level edits) would write null for some key. */ +export function opsSetNullKey(ops: readonly PatchOp[]): boolean { + return ops.some( + (o) => + o.op === 'set' && (o.value === null || nullKeyPath(o.value) !== null), + ); +} diff --git a/src/components/agents/config/effective/scalar.ts b/src/components/agents/config/effective/scalar.ts new file mode 100644 index 00000000..03a3fd1d --- /dev/null +++ b/src/components/agents/config/effective/scalar.ts @@ -0,0 +1,8 @@ +// Kinds of inline scalar editors on the Effective view (R69). +export type ScalarKind = + | 'text' + | 'cron' + | 'duration' + | 'bool' + | 'verbosity' + | 'protocol';