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 @@