From b8014735e80eff8a52b328805c05c71947c9230a Mon Sep 17 00:00:00 2001 From: Nishant Bangarwa Date: Fri, 10 Jul 2026 12:46:21 +0530 Subject: [PATCH] Add resource tags to dashboard visual editors --- .../canvas/inspector/PageEditor.svelte | 44 ++++++++++- .../features/visual-editing/tag-utils.spec.ts | 44 +++++++++++ .../src/features/visual-editing/tag-utils.ts | 79 +++++++++++++++++++ .../workspaces/VisualExploreEditing.svelte | 35 ++++++++ 4 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 web-common/src/features/visual-editing/tag-utils.spec.ts create mode 100644 web-common/src/features/visual-editing/tag-utils.ts diff --git a/web-common/src/features/canvas/inspector/PageEditor.svelte b/web-common/src/features/canvas/inspector/PageEditor.svelte index 23cdee6006de..0c7d1cf63d58 100644 --- a/web-common/src/features/canvas/inspector/PageEditor.svelte +++ b/web-common/src/features/canvas/inspector/PageEditor.svelte @@ -2,6 +2,7 @@ import { m } from "@rilldata/web-common/lib/i18n/gen/messages"; import Input from "@rilldata/web-common/components/forms/Input.svelte"; import InputLabel from "@rilldata/web-common/components/forms/InputLabel.svelte"; + import TagInput from "@rilldata/web-common/components/forms/TagInput.svelte"; import Switch from "@rilldata/web-common/components/forms/Switch.svelte"; import { getParsedDocument } from "@rilldata/web-common/features/canvas/inspector/selectors"; import { getCanvasStore } from "@rilldata/web-common/features/canvas/state-managers/state-managers"; @@ -13,6 +14,11 @@ } from "@rilldata/web-common/features/entity-management/resource-selectors"; import MultiSelectInput from "@rilldata/web-common/features/visual-editing/MultiSelectInput.svelte"; import SidebarWrapper from "@rilldata/web-common/features/visual-editing/SidebarWrapper.svelte"; + import { + getResourceTagSuggestions, + normalizeTags, + readRootYamlTags, + } from "@rilldata/web-common/features/visual-editing/tag-utils"; import ThemeInput from "@rilldata/web-common/features/visual-editing/ThemeInput.svelte"; import { DEFAULT_RANGES, @@ -26,7 +32,10 @@ } from "@rilldata/web-common/lib/time/config"; import { allTimeZones } from "@rilldata/web-common/lib/time/timezone"; import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2"; - import { createRuntimeServiceGetInstance } from "@rilldata/web-common/runtime-client"; + import { + createRuntimeServiceGetInstance, + createRuntimeServiceListResources, + } from "@rilldata/web-common/runtime-client"; import { YAMLMap, YAMLSeq } from "yaml"; import { DEFAULT_DASHBOARD_WIDTH } from "../layout-util"; @@ -57,6 +66,22 @@ $: rawTimeZones = $parsedDocument.get("time_zones"); $: rawMaxWidth = $parsedDocument.get("max_width"); + $: resourcesQuery = createRuntimeServiceListResources( + client, + {}, + { + query: { + select: (data) => data.resources ?? [], + }, + }, + ); + + $: resourceTags = readRootYamlTags($parsedDocument); + $: tagSuggestions = getResourceTagSuggestions( + $resourcesQuery?.data, + resourceTags, + ); + $: timeZones = new Set( rawTimeZones instanceof YAMLSeq ? rawTimeZones.toJSON().filter(isString) @@ -128,6 +153,16 @@ } } + async function updateResourceTags(tags: string[]) { + const normalizedTags = normalizeTags(tags); + + if (normalizedTags.length) { + await updateProperties({ tags: normalizedTags }); + } else { + await updateProperties({}, ["tags"]); + } + } + let currentTab: string = "options"; @@ -154,6 +189,13 @@ }} /> +
+ +
{ + it("reads top-level YAML tags from scalar nodes", () => { + const document = parseDocument(` +type: metrics_view +tags: + - finance + - "growth" + - "" + - finance +`); + + expect(readRootYamlTags(document)).toEqual(["finance", "growth"]); + }); + + it("writes and removes top-level YAML tags", () => { + const document = parseDocument("type: explore\n"); + + setRootYamlTags(document, [" analytics ", "growth", "analytics"]); + expect(readRootYamlTags(document)).toEqual(["analytics", "growth"]); + + setRootYamlTags(document, []); + expect(document.has("tags")).toBe(false); + }); + + it("builds suggestions from resource metadata and extra tags", () => { + expect( + getResourceTagSuggestions( + [ + { meta: { tags: ["finance", "growth"] } }, + { meta: { tags: ["growth", "sales"] } }, + ] as never, + ["executive"], + ), + ).toEqual(["executive", "finance", "growth", "sales"]); + }); +}); diff --git a/web-common/src/features/visual-editing/tag-utils.ts b/web-common/src/features/visual-editing/tag-utils.ts new file mode 100644 index 000000000000..4657ffc381fe --- /dev/null +++ b/web-common/src/features/visual-editing/tag-utils.ts @@ -0,0 +1,79 @@ +import type { V1Resource } from "@rilldata/web-common/runtime-client"; +import { Scalar, YAMLSeq } from "yaml"; + +type YamlDocumentWithTags = { + get(key: string): unknown; + set(key: string, value: unknown): void; + delete(key: string): unknown; + createNode(value: unknown): unknown; +}; + +function readYamlString(node: unknown): string { + if (typeof node === "string") return node.trim(); + + if (node instanceof Scalar) { + return typeof node.value === "string" ? node.value.trim() : ""; + } + + if (node && typeof node === "object" && "value" in node) { + const value = (node as { value: unknown }).value; + return typeof value === "string" ? value.trim() : ""; + } + + return ""; +} + +export function normalizeTags(tags: string[] | undefined): string[] { + const seen = new Set(); + const normalized: string[] = []; + + for (const tag of tags ?? []) { + const value = tag.trim(); + if (!value || seen.has(value)) continue; + seen.add(value); + normalized.push(value); + } + + return normalized; +} + +export function readYamlTags(node: unknown): string[] { + if (!(node instanceof YAMLSeq)) return []; + + return normalizeTags(node.items.map(readYamlString)); +} + +export function readRootYamlTags(document: { get(key: string): unknown }) { + return readYamlTags(document.get("tags")); +} + +export function setRootYamlTags( + document: YamlDocumentWithTags, + tags: string[], +) { + const normalizedTags = normalizeTags(tags); + + if (normalizedTags.length) { + document.set("tags", document.createNode(normalizedTags)); + } else { + document.delete("tags"); + } +} + +export function getResourceTagSuggestions( + resources: V1Resource[] | undefined, + ...extraTags: Array +) { + return buildTagSuggestions( + resources?.flatMap((resource) => resource.meta?.tags ?? []), + ...extraTags, + ); +} + +export function buildTagSuggestions( + ...sources: Array +): string[] { + return normalizeTags(sources.flatMap((source) => source ?? [])).sort((a, b) => + a.localeCompare(b), + ); +} diff --git a/web-common/src/features/workspaces/VisualExploreEditing.svelte b/web-common/src/features/workspaces/VisualExploreEditing.svelte index 1074c9aab9a0..cd7d87a925f1 100644 --- a/web-common/src/features/workspaces/VisualExploreEditing.svelte +++ b/web-common/src/features/workspaces/VisualExploreEditing.svelte @@ -1,6 +1,7 @@