From 8a6a2f62c72c5f6a4532a07237b03d7dd458683d Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:50:54 -0500 Subject: [PATCH 1/8] canvas resource DAG --- runtime/parser/parse_canvas.go | 50 ++++++++++- .../resource-icon-mapping.ts | 6 +- .../src/features/resource-graph/README.md | 3 +- .../embedding/ResourceGraph.svelte | 54 +++++++----- .../embedding/ResourceGraphOverlay.svelte | 44 ++++++++-- .../graph-canvas/ResourceNode.svelte | 14 ++-- .../graph-canvas/graph-builder.ts | 23 ++++- .../navigation/seed-parser.spec.ts | 7 +- .../resource-graph/navigation/seed-parser.ts | 84 +++++++++++++++---- .../summary/SummaryGraph.svelte | 48 ++++------- .../resource-graph/summary/SummaryNode.svelte | 12 +-- 11 files changed, 243 insertions(+), 102 deletions(-) diff --git a/runtime/parser/parse_canvas.go b/runtime/parser/parse_canvas.go index 517bdd345fa..7b766182232 100644 --- a/runtime/parser/parse_canvas.go +++ b/runtime/parser/parse_canvas.go @@ -255,7 +255,55 @@ func (p *Parser) parseCanvas(node *Node) error { } } - // Track canvas + // Collect metrics view refs from components for direct Canvas -> MetricsView links in the DAG + // This must be done BEFORE calling insertResource so the refs are included + metricsViewRefs := make(map[ResourceName]bool) + // Extract from inline components + for _, def := range inlineComponentDefs { + for _, ref := range def.refs { + if ref.Kind == ResourceKindMetricsView { + metricsViewRefs[ref] = true + } + } + } + // Extract from external components + for _, row := range tmp.Rows { + for _, item := range row.Items { + if item.Component != "" { + // Check if this is an external component (not inline) + isInline := false + for _, def := range inlineComponentDefs { + if def.name == item.Component { + isInline = true + break + } + } + if !isInline { + // Look up the external component + componentName := ResourceName{Kind: ResourceKindComponent, Name: item.Component} + if component, ok := p.Resources[componentName.Normalized()]; ok { + // Extract metrics view refs from the component + // Check Refs first (processed refs), fall back to rawRefs if Refs is empty + refsToCheck := component.Refs + if len(refsToCheck) == 0 { + refsToCheck = component.rawRefs + } + for _, ref := range refsToCheck { + if ref.Kind == ResourceKindMetricsView { + metricsViewRefs[ref] = true + } + } + } + } + } + } + } + // Add metrics view refs directly to canvas node refs BEFORE insertResource + for ref := range metricsViewRefs { + node.Refs = append(node.Refs, ref) + } + + // Track canvas (now with MetricsView refs included) r, err := p.insertResource(ResourceKindCanvas, node.Name, node.Paths, node.Refs...) if err != nil { return err diff --git a/web-common/src/features/entity-management/resource-icon-mapping.ts b/web-common/src/features/entity-management/resource-icon-mapping.ts index 79010571d32..52ce9b6e7e5 100644 --- a/web-common/src/features/entity-management/resource-icon-mapping.ts +++ b/web-common/src/features/entity-management/resource-icon-mapping.ts @@ -12,7 +12,8 @@ import ConnectorIcon from "../../components/icons/ConnectorIcon.svelte"; import MetricsViewIcon from "../../components/icons/MetricsViewIcon.svelte"; export const resourceIconMapping = { - [ResourceKind.Source]: TableIcon, + // Source is deprecated and merged with Model - use Model icon + [ResourceKind.Source]: Code2Icon, // Same as Model [ResourceKind.Connector]: ConnectorIcon, [ResourceKind.Model]: Code2Icon, [ResourceKind.MetricsView]: MetricsViewIcon, @@ -26,7 +27,8 @@ export const resourceIconMapping = { }; export const resourceColorMapping = { - [ResourceKind.Source]: "#059669", + // Source is deprecated and merged with Model - use Model color + [ResourceKind.Source]: "#0891B2", // Same as Model (turquoise) [ResourceKind.Connector]: "#6B7280", [ResourceKind.Model]: "#0891B2", [ResourceKind.MetricsView]: "#7C3AED", diff --git a/web-common/src/features/resource-graph/README.md b/web-common/src/features/resource-graph/README.md index 787d5b90b5b..6c1b13dcd4e 100644 --- a/web-common/src/features/resource-graph/README.md +++ b/web-common/src/features/resource-graph/README.md @@ -43,8 +43,7 @@ The graph page supports two URL parameters: Navigate to `/graph?kind=` to show all graphs of a resource kind: - `?kind=metrics` - Show all MetricsView graphs -- `?kind=models` - Show all Model graphs -- `?kind=sources` - Show all Source graphs +- `?kind=models` - Show all Model graphs (includes Sources, as Source is deprecated) - `?kind=dashboards` - Show all Dashboard/Explore graphs ### Show specific resources diff --git a/web-common/src/features/resource-graph/embedding/ResourceGraph.svelte b/web-common/src/features/resource-graph/embedding/ResourceGraph.svelte index 4602ee4abf2..0adfd654aab 100644 --- a/web-common/src/features/resource-graph/embedding/ResourceGraph.svelte +++ b/web-common/src/features/resource-graph/embedding/ResourceGraph.svelte @@ -45,18 +45,16 @@ export let expandedHeightDesktop: string = UI_CONFIG.EXPANDED_HEIGHT_DESKTOP; type SummaryMemo = { - sources: number; - metrics: number; models: number; + metrics: number; dashboards: number; resources: V1Resource[]; - activeToken: "metrics" | "sources" | "models" | "dashboards" | null; + activeToken: "metrics" | "models" | "dashboards" | null; }; function summaryEquals(a: SummaryMemo, b: SummaryMemo) { return ( - a.sources === b.sources && - a.metrics === b.metrics && a.models === b.models && + a.metrics === b.metrics && a.dashboards === b.dashboards && a.resources === b.resources && a.activeToken === b.activeToken @@ -77,7 +75,9 @@ // Determine if we're filtering by a specific kind (e.g., ?kind=metrics) // This is used to filter out groups that don't contain any resource of the filtered kind - $: filterKind = (function (): ResourceKind | undefined { + // Special case: "dashboards" includes both Explore and Canvas + // Special case: Source is normalized to Model (Source is deprecated) + $: filterKind = (function (): ResourceKind | "dashboards" | undefined { const rawSeeds = seeds ?? []; // Only apply kind filter if all seeds are kind tokens (e.g., ["metrics"] or ["sources"]) if (rawSeeds.length === 0) return undefined; @@ -85,18 +85,38 @@ const kind = isKindToken((raw || "").toLowerCase()); if (!kind) return undefined; // Mixed seeds, no single kind filter } + // Check if it's the dashboards token (which includes both Explore and Canvas) + const firstSeed = (rawSeeds[0] || "").toLowerCase(); + if (firstSeed === "dashboards" || firstSeed === "dashboard") { + return "dashboards"; // Special token to indicate both Explore and Canvas + } // All seeds are kind tokens - return the first one's kind - return isKindToken((rawSeeds[0] || "").toLowerCase()); + // Normalize Source to Model (Source is deprecated, merged with Model) + const kind = isKindToken(firstSeed); + if (kind === ResourceKind.Source) { + return ResourceKind.Model; + } + return kind; })(); // Determine which overview node should be highlighted based on current seeds + // Sources are normalized to models (Source is deprecated) + // For Canvas with MetricsView seeds, prioritize the Canvas token (dashboards) over MetricsView tokens $: overviewActiveToken = (function (): | "metrics" - | "sources" | "models" | "dashboards" | null { const rawSeeds = seeds ?? []; + + // Check the first seed first - this should be the anchor resource (e.g., Canvas) + // This ensures Canvas/Explore tokens are prioritized over MetricsView tokens + if (rawSeeds.length > 0) { + const firstToken = tokenForSeedString(rawSeeds[0]); + if (firstToken) return firstToken; + } + + // Fall back to checking all seeds if first seed didn't yield a token for (const raw of rawSeeds) { const token = tokenForSeedString(raw); if (token) return token; @@ -143,23 +163,21 @@ // Compute resource counts for the summary graph header. // We compute directly in a single pass rather than using filter().length for performance. // This is more efficient (O(n) instead of O(4n)) and clearer in intent. - $: ({ sourcesCount, modelsCount, metricsCount, dashboardsCount } = + // Sources and Models are merged since Source is deprecated. + $: ({ modelsCount, metricsCount, dashboardsCount } = (function computeCounts() { - let sources = 0, - models = 0, + let models = 0, metrics = 0, dashboards = 0; for (const r of normalizedResources) { if (r?.meta?.hidden) continue; const k = coerceResourceKind(r); if (!k) continue; - if (k === ResourceKind.Source) sources++; - else if (k === ResourceKind.Model) models++; + if (k === ResourceKind.Source || k === ResourceKind.Model) models++; else if (k === ResourceKind.MetricsView) metrics++; - else if (k === ResourceKind.Explore) dashboards++; + else if (k === ResourceKind.Explore || k === ResourceKind.Canvas) dashboards++; } return { - sourcesCount: sources, modelsCount: models, metricsCount: metrics, dashboardsCount: dashboards, @@ -171,7 +189,6 @@ // even if counts haven't actually changed. The summaryEquals function does shallow comparison // of counts while checking resources array reference equality. let summaryMemo: SummaryMemo = { - sources: 0, models: 0, metrics: 0, dashboards: 0, @@ -180,9 +197,8 @@ }; $: { const nextSummary: SummaryMemo = { - sources: sourcesCount, - metrics: metricsCount, models: modelsCount, + metrics: metricsCount, dashboards: dashboardsCount, resources: normalizedResources, activeToken: overviewActiveToken, @@ -397,14 +413,12 @@ {#if showSummary && currentExpandedId === null}
= { - [ResourceKind.Source]: "source", + [ResourceKind.Source]: "model", // Sources are treated as models (deprecated) [ResourceKind.Model]: "model", [ResourceKind.MetricsView]: "metrics", [ResourceKind.Explore]: "dashboard", + [ResourceKind.Canvas]: "dashboard", }; const KIND_TOKEN_BY_KIND: Record = { - [ResourceKind.Source]: "sources", + [ResourceKind.Source]: "models", // Sources are treated as models (deprecated) [ResourceKind.Model]: "models", [ResourceKind.MetricsView]: "metrics", [ResourceKind.Explore]: "dashboards", + [ResourceKind.Canvas]: "dashboards", }; $: anchorName = anchorResource?.meta?.name?.name ?? null; $: anchorKind = anchorResource?.meta?.name?.kind as ResourceKind | undefined; - $: supportsGraph = anchorKind ? ALLOWED_FOR_GRAPH.has(anchorKind) : false; + // Check if kind is allowed (handles both enum and string values) + // Convert to string for comparison since ResourceKind enum values are strings + $: supportsGraph = anchorKind ? ALLOWED_FOR_GRAPH.has(String(anchorKind) as ResourceKind) : false; // Type-safe access to graphable kind properties $: graphableKind = supportsGraph && anchorKind ? (anchorKind as GraphableKind) : null; + // Use the same seed format as the project graph page + // This ensures consistent behavior between overlay and project graph + $: overlaySeeds = (function (): string[] | undefined { + if (!anchorName || !anchorKind) return undefined; + + // Normalize kind to string for comparison (handles both enum and string values) + const kindStr = String(anchorKind); + + // Use the same format that would come from URL parameters + // For Canvas/Explore, use "dashboard:" prefix (same as project graph) + // For other resources, use their kind prefix + if (kindStr === ResourceKind.Canvas || kindStr === ResourceKind.Explore) { + return [`dashboard:${anchorName}`]; + } else if (kindStr === ResourceKind.Source || kindStr === ResourceKind.Model) { + return [`model:${anchorName}`]; + } else if (kindStr === ResourceKind.MetricsView) { + return [`metrics:${anchorName}`]; + } + + // Fallback to kind:name format using the resource's actual kind + const kindPart = kindStr.toLowerCase().replace("rill.runtime.v1.", "").replace("metricsview", "metrics"); + return [`${kindPart}:${anchorName}`]; + })(); + + // Keep the old anchorSeed for graphHref calculation $: anchorSeed = graphableKind && anchorName ? `${NAME_SEED_ALIAS[graphableKind]}:${anchorName}` : null; - - $: overlaySeeds = anchorSeed ? [anchorSeed] : undefined; $: graphHref = graphableKind ? `/graph?kind=${encodeURIComponent(KIND_TOKEN_BY_KIND[graphableKind])}` : "/graph"; - $: emptyReason = !anchorSeed ? "unsupported" : null; + $: emptyReason = !overlaySeeds || overlaySeeds.length === 0 ? "unsupported" : null; function closeOverlay() { if (onClose) { @@ -142,7 +170,7 @@ syncExpandedParam={false} showSummary={false} showCardTitles={false} - maxGroups={1} + maxGroups={null} showControls={false} enableExpansion={false} fitViewPadding={0.08} diff --git a/web-common/src/features/resource-graph/graph-canvas/ResourceNode.svelte b/web-common/src/features/resource-graph/graph-canvas/ResourceNode.svelte index c2a77d0e8b4..b747da0bda0 100644 --- a/web-common/src/features/resource-graph/graph-canvas/ResourceNode.svelte +++ b/web-common/src/features/resource-graph/graph-canvas/ResourceNode.svelte @@ -54,13 +54,15 @@ const DEFAULT_ICON = resourceIconMapping[ResourceKind.Model]; $: kind = data?.kind; + // Normalize Source to Model (Source is deprecated, merged with Model) + $: normalizedKind = kind === ResourceKind.Source ? ResourceKind.Model : kind; $: icon = - kind && resourceIconMapping[kind] - ? resourceIconMapping[kind] + normalizedKind && resourceIconMapping[normalizedKind] + ? resourceIconMapping[normalizedKind] : DEFAULT_ICON; $: color = - kind && resourceColorMapping[kind] - ? resourceColorMapping[kind] + normalizedKind && resourceColorMapping[normalizedKind] + ? resourceColorMapping[normalizedKind] : DEFAULT_COLOR; $: reconcileStatus = data?.resource?.meta?.reconcileStatus; $: hasError = !!data?.resource?.meta?.reconcileError; @@ -145,8 +147,8 @@

{data?.label}

- {#if kind} - {displayResourceKind(kind)} + {#if normalizedKind} + {displayResourceKind(normalizedKind)} {:else} Unknown {/if} diff --git a/web-common/src/features/resource-graph/graph-canvas/graph-builder.ts b/web-common/src/features/resource-graph/graph-canvas/graph-builder.ts index d15ac9ca44d..2db09357476 100644 --- a/web-common/src/features/resource-graph/graph-canvas/graph-builder.ts +++ b/web-common/src/features/resource-graph/graph-canvas/graph-builder.ts @@ -38,6 +38,7 @@ const ALLOWED_KINDS = new Set([ ResourceKind.Model, ResourceKind.MetricsView, ResourceKind.Explore, + ResourceKind.Canvas, ]); function toResourceKind(name?: V1ResourceName): ResourceKind | undefined { @@ -75,12 +76,13 @@ type BuildGraphOptions = { * Uses Dagre for automatic layout and maintains cached node positions for stability. * * This function: - * - Filters resources to only allowed kinds (Source, Model, MetricsView, Explore) + * - Filters resources to only allowed kinds (Source, Model, MetricsView, Explore, Canvas) + * Note: Sources and Models are merged in the UI (Source is deprecated) * - Creates nodes with dynamic widths based on label length * - Generates edges based on resource references * - Applies Dagre layout with configurable spacing * - Caches node positions per namespace for consistent placement - * - Enforces rank constraints (Sources at top, Explores/Canvas at bottom) + * - Enforces rank constraints (Explores/Canvas at bottom, Sources/Models flow naturally) * * @param resources - Array of V1Resource objects to visualize * @param opts - Optional configuration for layout and caching @@ -127,8 +129,11 @@ export function buildResourceGraph( const nodeWidth = estimateNodeWidth(label); let rankConstraint: "min" | "max" | undefined; switch (kind) { + // Sources and Models are merged (Source is deprecated), both treated the same case ResourceKind.Source: - rankConstraint = "min"; + case ResourceKind.Model: + // No special rank constraint - let them flow naturally in the graph + rankConstraint = undefined; break; case ResourceKind.Explore: case ResourceKind.Canvas: @@ -178,6 +183,7 @@ export function buildResourceGraph( } } + const edgeIds = new Set(); const edges: Edge[] = []; @@ -509,7 +515,7 @@ function updateGroupingCaches(groups: ResourceGraphGrouping[]): void { export function partitionResourcesBySeeds( resources: V1Resource[], seeds: (string | V1ResourceName)[], - filterKind?: ResourceKind, + filterKind?: ResourceKind | "dashboards", ): ResourceGraphGrouping[] { const resourceMap = buildVisibleResourceMap(resources); const { incoming, outgoing } = buildDirectedAdjacency(resourceMap); @@ -533,10 +539,19 @@ export function partitionResourcesBySeeds( // If filtering by a specific kind, remove groups that don't contain any resource of that kind // Use coerceResourceKind to handle "defined-as-source" models correctly + // Special case: "dashboards" includes both Explore and Canvas + // Special case: "models" includes both Source and Model (merged, Source is deprecated) if (filterKind) { return groups.filter((group) => group.resources.some((r) => { const kind = coerceResourceKind(r); + if (filterKind === "dashboards") { + return kind === ResourceKind.Explore || kind === ResourceKind.Canvas; + } + if (filterKind === ResourceKind.Model) { + // Include both Source and Model when filtering by models + return kind === ResourceKind.Source || kind === ResourceKind.Model; + } return kind === filterKind; }), ); diff --git a/web-common/src/features/resource-graph/navigation/seed-parser.spec.ts b/web-common/src/features/resource-graph/navigation/seed-parser.spec.ts index fc0229046c1..1e145d37705 100644 --- a/web-common/src/features/resource-graph/navigation/seed-parser.spec.ts +++ b/web-common/src/features/resource-graph/navigation/seed-parser.spec.ts @@ -737,15 +737,16 @@ describe("seed-utils", () => { }); describe("ALLOWED_FOR_GRAPH", () => { - it("should include Source, Model, MetricsView, and Explore", () => { + it("should include Source, Model, MetricsView, Explore, and Canvas", () => { expect(ALLOWED_FOR_GRAPH.has(ResourceKind.Source)).toBe(true); expect(ALLOWED_FOR_GRAPH.has(ResourceKind.Model)).toBe(true); expect(ALLOWED_FOR_GRAPH.has(ResourceKind.MetricsView)).toBe(true); expect(ALLOWED_FOR_GRAPH.has(ResourceKind.Explore)).toBe(true); + expect(ALLOWED_FOR_GRAPH.has(ResourceKind.Canvas)).toBe(true); }); - it("should have exactly 4 kinds", () => { - expect(ALLOWED_FOR_GRAPH.size).toBe(4); + it("should have exactly 5 kinds", () => { + expect(ALLOWED_FOR_GRAPH.size).toBe(5); }); }); diff --git a/web-common/src/features/resource-graph/navigation/seed-parser.ts b/web-common/src/features/resource-graph/navigation/seed-parser.ts index 6b85dbeaf3e..1832d30deae 100644 --- a/web-common/src/features/resource-graph/navigation/seed-parser.ts +++ b/web-common/src/features/resource-graph/navigation/seed-parser.ts @@ -27,7 +27,7 @@ export const URL_PARAMS = { /** * Valid kind tokens that can be used in the `kind` URL parameter. */ -export type KindToken = "metrics" | "sources" | "models" | "dashboards"; +export type KindToken = "metrics" | "models" | "dashboards"; /** * Normalize plural forms to singular forms for graph seed parsing. @@ -42,7 +42,7 @@ function normalizePluralToSingular(kind: string): string { metrics: "metricsview", models: "model", sources: "source", - dashboards: "explore", + dashboards: "explore", // Maps to explore for token resolution, but expandSeedsByKind handles both Explore and Canvas // Also handle "dashboard" -> "explore" for consistency dashboard: "explore", // Handle "metric" -> "metricsview" @@ -73,6 +73,7 @@ export const ALLOWED_FOR_GRAPH = new Set([ ResourceKind.Model, ResourceKind.MetricsView, ResourceKind.Explore, + ResourceKind.Canvas, ]); /** @@ -138,45 +139,55 @@ export function isKindToken(s: string): ResourceKind | undefined { /** * Convert a ResourceKind to its display token (plural form). * Used for highlighting overview nodes in the summary graph. + * Sources and Models are merged (Source is deprecated). * * @param kind - ResourceKind or string kind - * @returns Token string ("sources", "models", "metrics", "dashboards") or null + * @returns Token string ("models", "metrics", "dashboards") or null * * @example * tokenForKind(ResourceKind.Model) // "models" - * tokenForKind("rill.runtime.v1.Source") // "sources" + * tokenForKind(ResourceKind.Source) // "models" (merged with models) + * tokenForKind("rill.runtime.v1.Source") // "models" */ export function tokenForKind( kind?: ResourceKind | string | null, -): "metrics" | "sources" | "models" | "dashboards" | null { +): "metrics" | "models" | "dashboards" | null { if (!kind) return null; const key = `${kind}`.toLowerCase(); - if (key.includes("source")) return "sources"; - if (key.includes("model")) return "models"; + // Sources and Models are merged (Source is deprecated) + if (key.includes("source") || key.includes("model")) return "models"; if (key.includes("metricsview") || key.includes("metric")) return "metrics"; - if (key.includes("explore") || key.includes("dashboard")) return "dashboards"; + if (key.includes("explore") || key.includes("dashboard") || key.includes("canvas")) return "dashboards"; return null; } /** * Convert a seed string to its display token. * Parses the kind from the seed and converts it to a token. + * Sources and Models are merged (Source is deprecated). * - * @param seed - Seed string (e.g., "model:orders", "metrics", "orders") + * @param seed - Seed string (e.g., "model:orders", "source:raw_data", "metrics", "orders") * @returns Token string or null * * @example * tokenForSeedString("model:orders") // "models" + * tokenForSeedString("source:raw_data") // "models" (merged with models) + * tokenForSeedString("sources") // "models" * tokenForSeedString("metrics") // "metrics" * tokenForSeedString("orders") // "metrics" (defaults to metrics) */ export function tokenForSeedString( seed?: string | null, -): "metrics" | "sources" | "models" | "dashboards" | null { +): "metrics" | "models" | "dashboards" | null { if (!seed) return null; const normalized = seed.trim().toLowerCase(); if (!normalized) return null; + // Handle "sources" token - treat as "models" since they're merged + if (normalized === "sources" || normalized === "source") { + return "models"; + } + // Check if it's a kind token first const kindToken = isKindToken(normalized); if (kindToken) return tokenForKind(kindToken); @@ -185,6 +196,14 @@ export function tokenForSeedString( const idx = normalized.indexOf(":"); if (idx !== -1) { const kindPart = normalized.slice(0, idx); + // Handle "source:" prefix - treat as "models" + if (kindPart === "source" || kindPart === "sources") { + return "models"; + } + // Handle "dashboard:" or "canvas:" prefix - treat as "dashboards" + if (kindPart === "dashboard" || kindPart === "dashboards" || kindPart === "canvas") { + return "dashboards"; + } const mapped = resolveKindAlias(kindPart); if (mapped) return tokenForKind(mapped); return tokenForKind(kindPart); @@ -199,7 +218,8 @@ export function tokenForSeedString( * Handles three input formats: * 1. Explicit seeds ("kind:name") - kept as-is * 2. Name-only seeds ("name") - defaults to MetricsView - * 3. Kind tokens ("metrics", "sources") - expands to all visible resources of that kind + * 3. Kind tokens ("metrics", "models", "sources") - expands to all visible resources of that kind + * Note: "sources" token is treated as "models" (includes both Source and Model resources) * * @param seedStrings - Array of seed strings to expand * @param resources - All resources to consider for expansion @@ -210,8 +230,11 @@ export function tokenForSeedString( * expandSeedsByKind(["metrics"], resources, coerceKind) * // Returns one seed per MetricsView resource * - * expandSeedsByKind(["model:orders", "sources"], resources, coerceKind) - * // Returns the orders model plus one seed per Source resource + * expandSeedsByKind(["models"], resources, coerceKind) + * // Returns one seed per Model and Source resource (merged) + * + * expandSeedsByKind(["sources"], resources, coerceKind) + * // Returns one seed per Model and Source resource (same as "models") */ export function expandSeedsByKind( seedStrings: string[] | undefined, @@ -246,17 +269,35 @@ export function expandSeedsByKind( continue; } + // Handle "sources" token - treat as "models" (includes both Source and Model) + const isSourcesToken = raw.toLowerCase() === "sources" || raw.toLowerCase() === "source"; + const isModelsToken = raw.toLowerCase() === "models" || raw.toLowerCase() === "model"; + // Check if it's a kind token const kindToken = isKindToken(raw); - if (!kindToken) { + if (!kindToken && !isSourcesToken) { // Name-only, defaults to metrics view name pushSeed(normalizeSeed(raw)); continue; } // Expand: one seed per visible resource of this kind + // Special case: "dashboards" includes both Explore and Canvas + // Special case: "sources" and "models" both include Source and Model (merged) + const isDashboardsToken = raw.toLowerCase() === "dashboards" || raw.toLowerCase() === "dashboard"; + const isModelsOrSourcesToken = isModelsToken || isSourcesToken; for (const r of visible) { - if (coerceKindFn(r) !== kindToken) continue; + const resourceKind = coerceKindFn(r); + if (isDashboardsToken) { + // Include both Explore and Canvas for dashboards token + if (resourceKind !== ResourceKind.Explore && resourceKind !== ResourceKind.Canvas) continue; + } else if (isModelsOrSourcesToken) { + // Include both Source and Model for models/sources token (merged) + if (resourceKind !== ResourceKind.Source && resourceKind !== ResourceKind.Model) continue; + } else { + // Normal kind matching + if (resourceKind !== kindToken) continue; + } const name = r.meta?.name?.name; const kind = r.meta?.name?.kind; // use actual runtime kind for matching ids if (!name || !kind) continue; @@ -272,8 +313,9 @@ export function expandSeedsByKind( */ export interface GraphUrlParams { /** - * Kind filter (e.g., "metrics", "models", "sources", "dashboards"). + * Kind filter (e.g., "metrics", "models", "dashboards"). * When set, shows all graphs of this resource kind. + * Note: "sources" is treated as "models" (merged). */ kind: KindToken | null; @@ -315,10 +357,16 @@ export function parseGraphUrlParams( // Parse kind parameter const kindParam = params.get(URL_PARAMS.KIND)?.trim().toLowerCase() || null; - const validKind = kindParam as KindToken | null; + // Normalize "sources" to "models" since they're merged + let normalizedKindParam = kindParam; + if (kindParam === "sources" || kindParam === "source") { + normalizedKindParam = "models"; + } + + const validKind = normalizedKindParam as KindToken | null; const kind = validKind && - ["metrics", "sources", "models", "dashboards"].includes(validKind) + ["metrics", "models", "dashboards"].includes(validKind) ? validKind : null; diff --git a/web-common/src/features/resource-graph/summary/SummaryGraph.svelte b/web-common/src/features/resource-graph/summary/SummaryGraph.svelte index e0496e45139..2611cd17c76 100644 --- a/web-common/src/features/resource-graph/summary/SummaryGraph.svelte +++ b/web-common/src/features/resource-graph/summary/SummaryGraph.svelte @@ -15,15 +15,13 @@ import type { V1Resource } from "@rilldata/web-common/runtime-client"; import { goto } from "$app/navigation"; - export let sources = 0; export let metrics = 0; export let models = 0; export let dashboards = 0; // Full list of resources (for selection panel) export let resources: V1Resource[] = []; - // Active token to highlight: 'sources' | 'metrics' | 'models' | 'dashboards' + // Active token to highlight: 'metrics' | 'models' | 'dashboards' export let activeToken: - | "sources" | "metrics" | "models" | "dashboards" @@ -62,13 +60,10 @@ const edgesStore = writable([]); function navigateTokenForNode(id: string) { - let token: "sources" | "metrics" | "models" | "dashboards" | null = null; + let token: "metrics" | "models" | "dashboards" | null = null; let count = 0; - if (id === "sources") { - token = "sources"; - count = sources; - } else if (id === "metrics") { + if (id === "metrics") { token = "metrics"; count = metrics; } else if (id === "models") { @@ -86,39 +81,27 @@ } // Build nodes spaced across the available width + // Sources and Models are merged (Source is deprecated) function buildNodes( width: number, counts: { - sources: number; metrics: number; models: number; dashboards: number; }, - token: "sources" | "metrics" | "models" | "dashboards" | null, + token: "metrics" | "models" | "dashboards" | null, ) { const pad = 40; const eff = Math.max(120, width - pad * 2); - const step = Math.floor(eff / 3); + const step = Math.floor(eff / 2); const y = 60; // center larger nodes vertically in taller canvas - const { sources, metrics, models, dashboards } = counts; - const isActive = (key: "sources" | "metrics" | "models" | "dashboards") => + const { metrics, models, dashboards } = counts; + const isActive = (key: "metrics" | "models" | "dashboards") => token === key; return [ - { - id: "sources", - position: { x: pad + step * 0, y }, - type: "summary-count", - selected: isActive("sources"), - data: { - label: "Sources", - count: sources, - kind: ResourceKind.Source, - active: isActive("sources"), - }, - }, { id: "models", - position: { x: pad + step * 1, y }, + position: { x: pad + step * 0, y }, type: "summary-count", selected: isActive("models"), data: { @@ -130,7 +113,7 @@ }, { id: "metrics", - position: { x: pad + step * 2, y }, + position: { x: pad + step * 1, y }, type: "summary-count", selected: isActive("metrics"), data: { @@ -142,7 +125,7 @@ }, { id: "dashboards", - position: { x: pad + step * 3, y }, + position: { x: pad + step * 2, y }, type: "summary-count", selected: isActive("dashboards"), data: { @@ -162,9 +145,8 @@ targetHandle: "in", } as const; return [ - { id: "e1", source: "sources", target: "models", ...shared }, - { id: "e2", source: "models", target: "metrics", ...shared }, - { id: "e3", source: "metrics", target: "dashboards", ...shared }, + { id: "e1", source: "models", target: "metrics", ...shared }, + { id: "e2", source: "metrics", target: "dashboards", ...shared }, ] satisfies Edge[]; } @@ -172,7 +154,7 @@ $: { const width = containerEl?.clientWidth ?? 800; nodesStore.set( - buildNodes(width, { sources, metrics, models, dashboards }, activeToken), + buildNodes(width, { metrics, models, dashboards }, activeToken), ); edgesStore.set(buildEdges()); } @@ -183,7 +165,7 @@ $: flowColorMode = ($themeControl === "dark" ? "dark" : "light") as | "dark" | "light"; - $: flowKey = `overview|${sources}|${metrics}|${models}|${dashboards}|${containerKey}|${flowColorMode}`; + $: flowKey = `overview|${models}|${metrics}|${dashboards}|${containerKey}|${flowColorMode}`; const edgeOptions = { type: "straight", diff --git a/web-common/src/features/resource-graph/summary/SummaryNode.svelte b/web-common/src/features/resource-graph/summary/SummaryNode.svelte index c5ea6f089eb..cc368d6d1b5 100644 --- a/web-common/src/features/resource-graph/summary/SummaryNode.svelte +++ b/web-common/src/features/resource-graph/summary/SummaryNode.svelte @@ -53,8 +53,10 @@ positionAbsoluteY, ); - $: color = resourceColorMapping[data?.kind] || "#6B7280"; - $: Icon = resourceIconMapping[data?.kind] || null; + // Normalize Source to Model (Source is deprecated, merged with Model) + $: normalizedKind = data?.kind === ResourceKind.Source ? ResourceKind.Model : data?.kind; + $: color = normalizedKind && resourceColorMapping[normalizedKind] || "#6B7280"; + $: Icon = normalizedKind && resourceIconMapping[normalizedKind] || null; $: label = data?.label ?? ""; $: count = data?.count ?? 0; $: isActive = data?.active ?? selected; @@ -66,10 +68,10 @@ const kind = data?.kind; let token: string | null = null; - if (kind === ResourceKind.Source) token = "sources"; + // Sources and Models are merged (Source is deprecated) + if (kind === ResourceKind.Source || kind === ResourceKind.Model) token = "models"; else if (kind === ResourceKind.MetricsView) token = "metrics"; - else if (kind === ResourceKind.Model) token = "models"; - else if (kind === ResourceKind.Explore) token = "dashboards"; + else if (kind === ResourceKind.Explore || kind === ResourceKind.Canvas) token = "dashboards"; if (token) goto(`/graph?kind=${token}`); } From 769ef523a5fadab96b5115010d24bfa4f82a8cbd Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:54:21 -0500 Subject: [PATCH 2/8] Update ResourceGraphOverlay.svelte --- .../embedding/ResourceGraphOverlay.svelte | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/web-common/src/features/resource-graph/embedding/ResourceGraphOverlay.svelte b/web-common/src/features/resource-graph/embedding/ResourceGraphOverlay.svelte index 5d1e8c0a079..36b46b55c8e 100644 --- a/web-common/src/features/resource-graph/embedding/ResourceGraphOverlay.svelte +++ b/web-common/src/features/resource-graph/embedding/ResourceGraphOverlay.svelte @@ -16,14 +16,12 @@ // Type for resource kinds that support graph visualization type GraphableKind = - | ResourceKind.Source | ResourceKind.Model | ResourceKind.MetricsView | ResourceKind.Explore | ResourceKind.Canvas; const NAME_SEED_ALIAS: Record = { - [ResourceKind.Source]: "model", // Sources are treated as models (deprecated) [ResourceKind.Model]: "model", [ResourceKind.MetricsView]: "metrics", [ResourceKind.Explore]: "dashboard", @@ -31,7 +29,6 @@ }; const KIND_TOKEN_BY_KIND: Record = { - [ResourceKind.Source]: "models", // Sources are treated as models (deprecated) [ResourceKind.Model]: "models", [ResourceKind.MetricsView]: "metrics", [ResourceKind.Explore]: "dashboards", @@ -39,9 +36,11 @@ }; $: anchorName = anchorResource?.meta?.name?.name ?? null; - $: anchorKind = anchorResource?.meta?.name?.kind as ResourceKind | undefined; + // Normalize Source to Model immediately (Source is deprecated) + $: rawAnchorKind = anchorResource?.meta?.name?.kind as ResourceKind | undefined; + $: anchorKind = rawAnchorKind === ResourceKind.Source ? ResourceKind.Model : rawAnchorKind; + // Check if kind is allowed (handles both enum and string values) - // Convert to string for comparison since ResourceKind enum values are strings $: supportsGraph = anchorKind ? ALLOWED_FOR_GRAPH.has(String(anchorKind) as ResourceKind) : false; // Type-safe access to graphable kind properties @@ -50,26 +49,20 @@ // Use the same seed format as the project graph page // This ensures consistent behavior between overlay and project graph + // Note: anchorKind is already normalized (Source -> Model) above $: overlaySeeds = (function (): string[] | undefined { if (!anchorName || !anchorKind) return undefined; - // Normalize kind to string for comparison (handles both enum and string values) - const kindStr = String(anchorKind); - // Use the same format that would come from URL parameters - // For Canvas/Explore, use "dashboard:" prefix (same as project graph) - // For other resources, use their kind prefix - if (kindStr === ResourceKind.Canvas || kindStr === ResourceKind.Explore) { + if (anchorKind === ResourceKind.Canvas || anchorKind === ResourceKind.Explore) { return [`dashboard:${anchorName}`]; - } else if (kindStr === ResourceKind.Source || kindStr === ResourceKind.Model) { + } else if (anchorKind === ResourceKind.Model) { return [`model:${anchorName}`]; - } else if (kindStr === ResourceKind.MetricsView) { + } else if (anchorKind === ResourceKind.MetricsView) { return [`metrics:${anchorName}`]; } - // Fallback to kind:name format using the resource's actual kind - const kindPart = kindStr.toLowerCase().replace("rill.runtime.v1.", "").replace("metricsview", "metrics"); - return [`${kindPart}:${anchorName}`]; + return undefined; })(); // Keep the old anchorSeed for graphHref calculation From 1058a083f8e2bc7d92bc9e61377b350eefda1a15 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:59:37 -0500 Subject: [PATCH 3/8] overwrite source as model --- .../resource-selectors.spec.ts | 28 ++-------- .../entity-management/resource-selectors.ts | 17 +++--- .../src/features/file-explorer/NavFile.svelte | 13 +++-- .../navigation/seed-parser.spec.ts | 52 ++++++++++++------- .../resource-graph/navigation/seed-parser.ts | 10 +++- 5 files changed, 58 insertions(+), 62 deletions(-) diff --git a/web-common/src/features/entity-management/resource-selectors.spec.ts b/web-common/src/features/entity-management/resource-selectors.spec.ts index bc8bb17f92a..85022e3a57d 100644 --- a/web-common/src/features/entity-management/resource-selectors.spec.ts +++ b/web-common/src/features/entity-management/resource-selectors.spec.ts @@ -24,7 +24,7 @@ describe("resource-selectors", () => { expect(coerceResourceKind(resource)).toBe(ResourceKind.Model); }); - it("should return Source kind for models defined-as-source with matching table name", () => { + it("should return Model kind for models defined-as-source (Source is deprecated)", () => { const resource: V1Resource = { meta: { name: { @@ -41,30 +41,11 @@ describe("resource-selectors", () => { }, }, }; - expect(coerceResourceKind(resource)).toBe(ResourceKind.Source); - }); - - it("should return Model kind for models defined-as-source with non-matching table name", () => { - const resource: V1Resource = { - meta: { - name: { - kind: ResourceKind.Model, - name: "raw_orders", - }, - }, - model: { - spec: { - definedAsSource: true, - }, - state: { - resultTable: "different_table", - }, - }, - }; + // Models defined-as-source are now treated as Models (Source is deprecated) expect(coerceResourceKind(resource)).toBe(ResourceKind.Model); }); - it("should pass through Source kind unchanged", () => { + it("should normalize Source to Model (Source is deprecated)", () => { const resource: V1Resource = { meta: { name: { @@ -78,7 +59,8 @@ describe("resource-selectors", () => { }, }, }; - expect(coerceResourceKind(resource)).toBe(ResourceKind.Source); + // Source is normalized to Model (Source is deprecated) + expect(coerceResourceKind(resource)).toBe(ResourceKind.Model); }); it("should pass through MetricsView kind unchanged", () => { diff --git a/web-common/src/features/entity-management/resource-selectors.ts b/web-common/src/features/entity-management/resource-selectors.ts index a41b507aeda..456936b25f4 100644 --- a/web-common/src/features/entity-management/resource-selectors.ts +++ b/web-common/src/features/entity-management/resource-selectors.ts @@ -83,29 +83,24 @@ export function prettyResourceKind(kind: string) { /** * Coerce resource kind to match UI representation. - * Models that are defined-as-source are displayed as Sources in the sidebar and graph. + * Sources are normalized to Models (Source is deprecated). * This ensures consistent representation across the application. * * @param res - The resource to check * @returns The coerced ResourceKind, or undefined if the resource has no kind * * @example - * // A model that is defined-as-source - * coerceResourceKind(modelResource) // Returns ResourceKind.Source + * // A source resource + * coerceResourceKind(sourceResource) // Returns ResourceKind.Model * * // A normal model * coerceResourceKind(normalModel) // Returns ResourceKind.Model */ export function coerceResourceKind(res: V1Resource): ResourceKind | undefined { const raw = res.meta?.name?.kind as ResourceKind | undefined; - if (raw === ResourceKind.Model) { - // A resource is a Source if it's a model defined-as-source and its result table matches the resource name - const name = res.meta?.name?.name; - const resultTable = res.model?.state?.resultTable; - const definedAsSource = res.model?.spec?.definedAsSource; - if (name && resultTable === name && definedAsSource === true) { - return ResourceKind.Source; - } + // Normalize Source to Model (Source is deprecated) + if (raw === ResourceKind.Source) { + return ResourceKind.Model; } return raw; } diff --git a/web-common/src/features/file-explorer/NavFile.svelte b/web-common/src/features/file-explorer/NavFile.svelte index c5933c7ee85..b00442b34e3 100644 --- a/web-common/src/features/file-explorer/NavFile.svelte +++ b/web-common/src/features/file-explorer/NavFile.svelte @@ -37,9 +37,8 @@ import { ResourceKind } from "../entity-management/resource-selectors"; import ExploreMenuItems from "../explores/ExploreMenuItems.svelte"; import MetricsViewMenuItems from "../metrics-views/MetricsViewMenuItems.svelte"; - import ModelMenuItems from "../models/navigation/ModelMenuItems.svelte"; - import SourceMenuItems from "../sources/navigation/SourceMenuItems.svelte"; - import { PROTECTED_DIRECTORIES, PROTECTED_FILES } from "./protected-paths"; +import ModelMenuItems from "../models/navigation/ModelMenuItems.svelte"; +import { PROTECTED_DIRECTORIES, PROTECTED_FILES } from "./protected-paths"; export let filePath: string; export let onRename: (filePath: string, isDir: boolean) => void; @@ -67,8 +66,10 @@ $: ({ instanceId } = $runtime); - $: resourceKind = ($resourceName?.kind ?? + // Normalize Source to Model (Source is deprecated) + $: rawResourceKind = ($resourceName?.kind ?? $inferredResourceKind) as ResourceKind; + $: resourceKind = rawResourceKind === ResourceKind.Source ? ResourceKind.Model : rawResourceKind; $: padding = getPaddingFromPath(filePath); $: topLevelFolder = getTopLevelFolder(filePath); $: isProtectedDirectory = PROTECTED_DIRECTORIES.includes(topLevelFolder); @@ -169,9 +170,7 @@ Duplicate {#if resourceKind} - {#if resourceKind === ResourceKind.Source} - - {:else if resourceKind === ResourceKind.Model} + {#if resourceKind === ResourceKind.Model} {:else if resourceKind === ResourceKind.MetricsView} diff --git a/web-common/src/features/resource-graph/navigation/seed-parser.spec.ts b/web-common/src/features/resource-graph/navigation/seed-parser.spec.ts index 1e145d37705..8ed92dd9a6f 100644 --- a/web-common/src/features/resource-graph/navigation/seed-parser.spec.ts +++ b/web-common/src/features/resource-graph/navigation/seed-parser.spec.ts @@ -143,8 +143,8 @@ describe("seed-utils", () => { expect(isKindToken("models")).toBe(ResourceKind.Model); }); - it("should recognize 'sources' as Source token", () => { - expect(isKindToken("sources")).toBe(ResourceKind.Source); + it("should recognize 'sources' as Model token (Source normalized to Model)", () => { + expect(isKindToken("sources")).toBe(ResourceKind.Model); }); it("should recognize 'dashboards' as Explore token", () => { @@ -155,8 +155,8 @@ describe("seed-utils", () => { expect(isKindToken("model")).toBe(ResourceKind.Model); }); - it("should recognize singular 'source' as Source token", () => { - expect(isKindToken("source")).toBe(ResourceKind.Source); + it("should recognize singular 'source' as Model token (Source normalized to Model)", () => { + expect(isKindToken("source")).toBe(ResourceKind.Model); }); it("should recognize singular 'metric' as MetricsView token", () => { @@ -190,8 +190,8 @@ describe("seed-utils", () => { }); describe("tokenForKind", () => { - it("should return 'sources' for Source kind", () => { - expect(tokenForKind(ResourceKind.Source)).toBe("sources"); + it("should return 'models' for Source kind (Source normalized to Model)", () => { + expect(tokenForKind(ResourceKind.Source)).toBe("models"); }); it("should return 'models' for Model kind", () => { @@ -322,7 +322,7 @@ describe("seed-utils", () => { }); }); - it("should expand 'models' kind token to all Model resources", () => { + it("should expand 'models' kind token to all Model and Source resources (merged)", () => { const resources: V1Resource[] = [ { meta: { @@ -346,7 +346,8 @@ describe("seed-utils", () => { const result = expandSeedsByKind(["models"], resources, mockCoerceKind); - expect(result).toHaveLength(2); + // Should include both Model and Source resources (merged) + expect(result).toHaveLength(3); expect(result).toContainEqual({ kind: ResourceKind.Model, name: "orders", @@ -355,9 +356,13 @@ describe("seed-utils", () => { kind: ResourceKind.Model, name: "customers", }); + expect(result).toContainEqual({ + kind: ResourceKind.Source, + name: "raw_data", + }); }); - it("should expand 'sources' kind token to all Source resources", () => { + it("should expand 'sources' kind token to all Source and Model resources (merged)", () => { const resources: V1Resource[] = [ { meta: { @@ -381,7 +386,8 @@ describe("seed-utils", () => { const result = expandSeedsByKind(["sources"], resources, mockCoerceKind); - expect(result).toHaveLength(2); + // Should include both Source and Model resources (merged) + expect(result).toHaveLength(3); expect(result).toContainEqual({ kind: ResourceKind.Source, name: "raw_orders", @@ -390,6 +396,10 @@ describe("seed-utils", () => { kind: ResourceKind.Source, name: "raw_users", }); + expect(result).toContainEqual({ + kind: ResourceKind.Model, + name: "orders", + }); }); it("should expand 'metrics' kind token to all MetricsView resources", () => { @@ -675,10 +685,10 @@ describe("seed-utils", () => { it("should use coerceKind function for kind determination", () => { const customCoerceKind = (res: V1Resource) => { - // Simulate coercing models to sources + // Normalize Source to Model (Source is deprecated) const kind = res.meta?.name?.kind as ResourceKind; - if (kind === ResourceKind.Model && res.meta?.name?.name === "special") { - return ResourceKind.Source; + if (kind === ResourceKind.Source) { + return ResourceKind.Model; } return kind; }; @@ -686,7 +696,7 @@ describe("seed-utils", () => { const resources: V1Resource[] = [ { meta: { - name: { kind: ResourceKind.Model, name: "special" }, + name: { kind: ResourceKind.Source, name: "raw_data" }, hidden: false, }, }, @@ -699,16 +709,20 @@ describe("seed-utils", () => { ]; const result = expandSeedsByKind( - ["sources"], + ["models"], resources, customCoerceKind, ); - // Should find the "special" model that's coerced to Source - expect(result).toHaveLength(1); - expect(result[0]).toEqual({ + // Should find both Source (normalized to Model) and Model resources + expect(result).toHaveLength(2); + expect(result).toContainEqual({ + kind: ResourceKind.Source, + name: "raw_data", + }); + expect(result).toContainEqual({ kind: ResourceKind.Model, - name: "special", + name: "normal", }); }); diff --git a/web-common/src/features/resource-graph/navigation/seed-parser.ts b/web-common/src/features/resource-graph/navigation/seed-parser.ts index 1832d30deae..a02778aaa7b 100644 --- a/web-common/src/features/resource-graph/navigation/seed-parser.ts +++ b/web-common/src/features/resource-graph/navigation/seed-parser.ts @@ -122,18 +122,24 @@ export function normalizeSeed(s: string): string | V1ResourceName { * * Uses the normalization function to handle plural forms, then looks up * in the canonical ResourceShortNameToResourceKind mapping. + * Sources are normalized to Models (Source is deprecated). * * @param s - String to check * @returns The ResourceKind if it's a kind token, undefined otherwise * * @example * isKindToken("metrics") // ResourceKind.MetricsView - * isKindToken("sources") // ResourceKind.Source + * isKindToken("sources") // ResourceKind.Model (Source normalized to Model) * isKindToken("orders") // undefined (not a kind token) */ export function isKindToken(s: string): ResourceKind | undefined { // Use the same normalization and resolution logic - return resolveKindAlias(s); + const kind = resolveKindAlias(s); + // Normalize Source to Model (Source is deprecated) + if (kind === ResourceKind.Source) { + return ResourceKind.Model; + } + return kind; } /** From 94b8fcc36280c0cfc3966085ebb208a931cb1821 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:50:28 -0500 Subject: [PATCH 4/8] https://www.loom.com/share/f21f8e222af24cc78f843d87d0611d0e --- .../embedding/GraphOverlay.svelte | 1 + .../embedding/ResourceGraph.svelte | 3 + .../embedding/ResourceGraphOverlay.svelte | 1 + .../graph-canvas/GraphCanvas.svelte | 8 +- .../graph-canvas/ResourceNode.svelte | 426 +++++++++++------- 5 files changed, 273 insertions(+), 166 deletions(-) diff --git a/web-common/src/features/resource-graph/embedding/GraphOverlay.svelte b/web-common/src/features/resource-graph/embedding/GraphOverlay.svelte index 10643c39d86..26fad15bb4e 100644 --- a/web-common/src/features/resource-graph/embedding/GraphOverlay.svelte +++ b/web-common/src/features/resource-graph/embedding/GraphOverlay.svelte @@ -71,6 +71,7 @@ {showControls} showLock={false} enableExpand={false} + isOverlay={true} {fitViewPadding} {fitViewMinZoom} {fitViewMaxZoom} diff --git a/web-common/src/features/resource-graph/embedding/ResourceGraph.svelte b/web-common/src/features/resource-graph/embedding/ResourceGraph.svelte index ec7252e98a0..5e14a63da93 100644 --- a/web-common/src/features/resource-graph/embedding/ResourceGraph.svelte +++ b/web-common/src/features/resource-graph/embedding/ResourceGraph.svelte @@ -43,6 +43,7 @@ export let gridColumns: number = UI_CONFIG.DEFAULT_GRID_COLUMNS; export let expandedHeightMobile: string = UI_CONFIG.EXPANDED_HEIGHT_MOBILE; export let expandedHeightDesktop: string = UI_CONFIG.EXPANDED_HEIGHT_DESKTOP; + export let isOverlay = false; type SummaryMemo = { models: number; @@ -493,6 +494,7 @@ showLock={false} fillParent={true} enableExpand={enableExpansion} + {isOverlay} {fitViewPadding} {fitViewMinZoom} {fitViewMaxZoom} @@ -519,6 +521,7 @@ showLock={true} fillParent={false} enableExpand={enableExpansion} + {isOverlay} {fitViewPadding} {fitViewMinZoom} {fitViewMaxZoom} diff --git a/web-common/src/features/resource-graph/embedding/ResourceGraphOverlay.svelte b/web-common/src/features/resource-graph/embedding/ResourceGraphOverlay.svelte index 774030a4e0c..cdc408cbfd5 100644 --- a/web-common/src/features/resource-graph/embedding/ResourceGraphOverlay.svelte +++ b/web-common/src/features/resource-graph/embedding/ResourceGraphOverlay.svelte @@ -166,6 +166,7 @@ maxGroups={null} showControls={false} enableExpansion={false} + isOverlay={true} fitViewPadding={0.08} fitViewMinZoom={0.01} fitViewMaxZoom={1.35} diff --git a/web-common/src/features/resource-graph/graph-canvas/GraphCanvas.svelte b/web-common/src/features/resource-graph/graph-canvas/GraphCanvas.svelte index eebde11b9e2..e6bf6dd9402 100644 --- a/web-common/src/features/resource-graph/graph-canvas/GraphCanvas.svelte +++ b/web-common/src/features/resource-graph/graph-canvas/GraphCanvas.svelte @@ -43,6 +43,7 @@ export let fitViewMinZoom: number = FIT_VIEW_CONFIG.MIN_ZOOM; export let fitViewMaxZoom: number = FIT_VIEW_CONFIG.MAX_ZOOM; export let overlay = false; + export let isOverlay = false; export let onExpand: () => void = () => {}; let hasNodes = false; @@ -241,7 +242,7 @@ const nodesWithRoots = (graph.nodes as Node[]).map( (node) => ({ ...node, - data: { ...node.data, isRoot: rootSet.has(node.id) }, + data: { ...node.data, isRoot: rootSet.has(node.id), isOverlay }, }), ); nodesStore.set(nodesWithRoots); @@ -387,4 +388,9 @@ .expand-btn:hover { @apply bg-surface-muted text-fg-primary; } + + /* Override xyflow pane background to match app theme */ + :global(.svelte-flow .svelte-flow__pane) { + background-color: var(--surface-background, #ffffff); + } diff --git a/web-common/src/features/resource-graph/graph-canvas/ResourceNode.svelte b/web-common/src/features/resource-graph/graph-canvas/ResourceNode.svelte index 553ef146a7f..1fdb5a2944d 100644 --- a/web-common/src/features/resource-graph/graph-canvas/ResourceNode.svelte +++ b/web-common/src/features/resource-graph/graph-canvas/ResourceNode.svelte @@ -1,5 +1,5 @@ - - -

{ - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - handleClick(); + function handleRefresh(e?: MouseEvent) { + e?.stopPropagation(); + if (!isModel || !data?.resource?.meta?.name?.name) return; + + void $triggerMutation.mutateAsync({ + instanceId, + data: { + models: [{ model: data.resource.meta.name.name, full: true }], + }, + }); + } + + function handleViewGraph(e?: MouseEvent) { + e?.stopPropagation(); + if (!data?.resource?.meta?.name) return; + + const resourceKindName = data.resource.meta.name.kind; + const resourceNameValue = data.resource.meta.name.name; + + // Determine kind token for URL + let kindToken = "models"; + if (resourceKindName === "rill.runtime.v1.MetricsView") { + kindToken = "metrics"; + } else if ( + resourceKindName === "rill.runtime.v1.Explore" || + resourceKindName === "rill.runtime.v1.Canvas" + ) { + kindToken = "dashboards"; } - }} -> - - -
- -
-
-

{data?.label}

-

- {#if normalizedKind} - {displayResourceKind(normalizedKind)} - {:else} - Unknown + + // Build expanded ID (ResourceKind:Name format) + const expandedId = encodeURIComponent( + `${resourceKindName}:${resourceNameValue}`, + ); + + goto(`/graph?kind=${kindToken}&expanded=${expandedId}`); + } + + +{#if hasError} + + + +

+ + + + + {#if !isInOverlay} +
+ + + + + + {#if artifact?.path} + + + Edit file + + {/if} + {#if isModel} + + + Refresh + + {/if} + + + View lineage + + + +
{/if} -

- {#if effectiveStatusLabel} -

- {effectiveStatusLabel} -

- {/if} -
- + +
+
+

{data?.label}

+

+ {#if normalizedKind} + {displayResourceKind(normalizedKind)} + {:else} + Unknown + {/if} +

+

{effectiveStatusLabel}

+
+
+ +
+ Error +
{data?.resource?.meta?.reconcileError}
+
+
+ +{:else} + + +
- - - - {#if showError && hasError} - +{/if} diff --git a/web-common/src/features/resource-graph/graph-canvas/ResourceNode.svelte b/web-common/src/features/resource-graph/graph-canvas/ResourceNode.svelte index 2a412a9b463..4784f09497e 100644 --- a/web-common/src/features/resource-graph/graph-canvas/ResourceNode.svelte +++ b/web-common/src/features/resource-graph/graph-canvas/ResourceNode.svelte @@ -21,7 +21,7 @@ import NavigationMenuItem from "@rilldata/web-common/layout/navigation/NavigationMenuItem.svelte"; import Tooltip from "@rilldata/web-common/components/tooltip/Tooltip.svelte"; import TooltipContent from "@rilldata/web-common/components/tooltip/TooltipContent.svelte"; -import { GitFork } from "lucide-svelte"; + import { GitFork } from "lucide-svelte"; import { builderActions, getAttrs } from "bits-ui"; export let id: string; @@ -107,6 +107,7 @@ import { GitFork } from "lucide-svelte"; $: isModelOrSource = kind === ResourceKind.Model || kind === ResourceKind.Source; $: isInOverlay = (data as any)?.isOverlay === true; + $: isRefreshing = $triggerMutation.isPending; function openFile(e?: MouseEvent) { e?.stopPropagation(); @@ -126,7 +127,8 @@ import { GitFork } from "lucide-svelte"; function handleRefresh(e?: MouseEvent) { e?.stopPropagation(); - if (!isModelOrSource || !data?.resource?.meta?.name?.name) return; + if (!isModelOrSource || !data?.resource?.meta?.name?.name || isRefreshing) + return; void $triggerMutation.mutateAsync({ instanceId, @@ -216,9 +218,12 @@ import { GitFork } from "lucide-svelte"; {/if} {#if isModelOrSource} - + - Refresh + {isRefreshing ? "Refreshing..." : "Refresh"} {/if} @@ -248,9 +253,8 @@ import { GitFork } from "lucide-svelte";
Error -
{data?.resource?.meta?.reconcileError}
+
{data?.resource?.meta
+            ?.reconcileError}
@@ -305,9 +309,12 @@ import { GitFork } from "lucide-svelte";
{/if} {#if isModelOrSource} - + - Refresh + {isRefreshing ? "Refreshing..." : "Refresh"} {/if} diff --git a/web-common/src/features/resource-graph/graph-canvas/graph-builder.ts b/web-common/src/features/resource-graph/graph-canvas/graph-builder.ts index e7e1af3842a..8f021bcd08b 100644 --- a/web-common/src/features/resource-graph/graph-canvas/graph-builder.ts +++ b/web-common/src/features/resource-graph/graph-canvas/graph-builder.ts @@ -183,7 +183,6 @@ export function buildResourceGraph( } } - const edgeIds = new Set(); const edges: Edge[] = []; diff --git a/web-common/src/features/resource-graph/navigation/seed-parser.ts b/web-common/src/features/resource-graph/navigation/seed-parser.ts index 1086cb8201e..f47bc1ce44c 100644 --- a/web-common/src/features/resource-graph/navigation/seed-parser.ts +++ b/web-common/src/features/resource-graph/navigation/seed-parser.ts @@ -156,7 +156,12 @@ export function tokenForKind( if (key.includes("source")) return "sources"; if (key.includes("model")) return "models"; if (key.includes("metricsview") || key.includes("metric")) return "metrics"; - if (key.includes("explore") || key.includes("dashboard") || key.includes("canvas")) return "dashboards"; + if ( + key.includes("explore") || + key.includes("dashboard") || + key.includes("canvas") + ) + return "dashboards"; return null; } @@ -190,7 +195,11 @@ export function tokenForSeedString( if (idx !== -1) { const kindPart = normalized.slice(0, idx); // Handle "dashboard:" or "canvas:" prefix - treat as "dashboards" - if (kindPart === "dashboard" || kindPart === "dashboards" || kindPart === "canvas") { + if ( + kindPart === "dashboard" || + kindPart === "dashboards" || + kindPart === "canvas" + ) { return "dashboards"; } const mapped = resolveKindAlias(kindPart); @@ -267,12 +276,17 @@ export function expandSeedsByKind( // Expand: one seed per visible resource of this kind // Special case: "dashboards" includes both Explore and Canvas - const isDashboardsToken = raw.toLowerCase() === "dashboards" || raw.toLowerCase() === "dashboard"; + const isDashboardsToken = + raw.toLowerCase() === "dashboards" || raw.toLowerCase() === "dashboard"; for (const r of visible) { const resourceKind = coerceKindFn(r); if (isDashboardsToken) { // Include both Explore and Canvas for dashboards token - if (resourceKind !== ResourceKind.Explore && resourceKind !== ResourceKind.Canvas) continue; + if ( + resourceKind !== ResourceKind.Explore && + resourceKind !== ResourceKind.Canvas + ) + continue; } else { // Normal kind matching if (resourceKind !== kindToken) continue; diff --git a/web-common/src/features/resource-graph/summary/SummaryNode.svelte b/web-common/src/features/resource-graph/summary/SummaryNode.svelte index b5879d2bcba..740fd24960f 100644 --- a/web-common/src/features/resource-graph/summary/SummaryNode.svelte +++ b/web-common/src/features/resource-graph/summary/SummaryNode.svelte @@ -74,7 +74,8 @@ if (kind === ResourceKind.Source) token = "sources"; else if (kind === ResourceKind.MetricsView) token = "metrics"; else if (kind === ResourceKind.Model) token = "models"; - else if (kind === ResourceKind.Explore || kind === ResourceKind.Canvas) token = "dashboards"; + else if (kind === ResourceKind.Explore || kind === ResourceKind.Canvas) + token = "dashboards"; if (token) goto(`/graph?kind=${token}`); } From 548bede00b0bfe62f995d2980b3e43cee0a621f0 Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:37:23 -0500 Subject: [PATCH 7/8] code qual --- .../resource-graph/graph-canvas/ResourceNode.svelte | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/web-common/src/features/resource-graph/graph-canvas/ResourceNode.svelte b/web-common/src/features/resource-graph/graph-canvas/ResourceNode.svelte index 4784f09497e..49dfa6a5526 100644 --- a/web-common/src/features/resource-graph/graph-canvas/ResourceNode.svelte +++ b/web-common/src/features/resource-graph/graph-canvas/ResourceNode.svelte @@ -109,8 +109,7 @@ $: isInOverlay = (data as any)?.isOverlay === true; $: isRefreshing = $triggerMutation.isPending; - function openFile(e?: MouseEvent) { - e?.stopPropagation(); + function openFile() { if (!artifact?.path) return; // Set code view preference for this file @@ -125,8 +124,7 @@ goto(`/files${artifact.path}`); } - function handleRefresh(e?: MouseEvent) { - e?.stopPropagation(); + function handleRefresh() { if (!isModelOrSource || !data?.resource?.meta?.name?.name || isRefreshing) return; @@ -138,8 +136,7 @@ }); } - function handleViewGraph(e?: MouseEvent) { - e?.stopPropagation(); + function handleViewGraph() { if (!data?.resource?.meta?.name) return; const resourceKindName = data.resource.meta.name.kind; From 7dd76a54570d18741e14cb9488b13c73fb57a1dd Mon Sep 17 00:00:00 2001 From: royendo <67675319+royendo@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:41:33 -0500 Subject: [PATCH 8/8] code review, prettier and code qual --- runtime/parser/parse_canvas.go | 9 +++- runtime/parser/parse_node.go | 18 ++++---- runtime/parser/parser.go | 5 ++- .../src/features/file-explorer/NavFile.svelte | 9 ++-- .../embedding/ResourceGraph.svelte | 7 ++-- .../embedding/ResourceGraphOverlay.svelte | 39 ++++++++---------- .../graph-canvas/graph-builder.ts | 1 - .../navigation/seed-parser.spec.ts | 6 +-- .../resource-graph/navigation/seed-parser.ts | 41 ++++++++++++++----- .../summary/SummaryGraph.svelte | 6 +-- .../resource-graph/summary/SummaryNode.svelte | 9 ++-- 11 files changed, 85 insertions(+), 65 deletions(-) diff --git a/runtime/parser/parse_canvas.go b/runtime/parser/parse_canvas.go index 7b766182232..6288f9093b3 100644 --- a/runtime/parser/parse_canvas.go +++ b/runtime/parser/parse_canvas.go @@ -299,8 +299,15 @@ func (p *Parser) parseCanvas(node *Node) error { } } // Add metrics view refs directly to canvas node refs BEFORE insertResource + // Check for duplicates to avoid adding refs that already exist + existingRefs := make(map[ResourceName]bool) + for _, ref := range node.Refs { + existingRefs[ref] = true + } for ref := range metricsViewRefs { - node.Refs = append(node.Refs, ref) + if !existingRefs[ref] { + node.Refs = append(node.Refs, ref) + } } // Track canvas (now with MetricsView refs included) diff --git a/runtime/parser/parse_node.go b/runtime/parser/parse_node.go index 4e950135a35..9685bb4ef4f 100644 --- a/runtime/parser/parse_node.go +++ b/runtime/parser/parse_node.go @@ -248,26 +248,26 @@ func (p *Parser) parseStem(paths []string, ymlPath, yml, sqlPath, sql string) (* v, ok := v.(string) if !ok { err = fmt.Errorf("invalid type %T for property 'type'", v) - break - } - res.Kind, err = ParseResourceKind(v) - if err != nil { - break + } else { + res.Kind, err = ParseResourceKind(v) } case "name": v, ok := v.(string) if !ok { err = fmt.Errorf("invalid type %T for property 'name'", v) - break + } else { + res.Name = v } - res.Name = v case "connector": v, ok := v.(string) if !ok { err = fmt.Errorf("invalid type %T for property 'connector'", v) - break + } else { + res.Connector = v } - res.Connector = v + } + if err != nil { + break } } if err != nil { diff --git a/runtime/parser/parser.go b/runtime/parser/parser.go index f5f25d4fbd7..51bc9b799d0 100644 --- a/runtime/parser/parser.go +++ b/runtime/parser/parser.go @@ -634,12 +634,13 @@ func (p *Parser) parsePaths(ctx context.Context, paths []string) error { // NOTE 2: Using a map since the two-way check (necessary for reparses) may match the same resource twice. modelsWithNameErrs := make(map[ResourceName]string) for _, r := range p.insertedResources { - if r.Name.Kind == ResourceKindSource { + switch r.Name.Kind { + case ResourceKindSource: n := ResourceName{Kind: ResourceKindModel, Name: r.Name.Name}.Normalized() if _, ok := p.Resources[n]; ok { modelsWithNameErrs[n] = r.Name.Name } - } else if r.Name.Kind == ResourceKindModel { + case ResourceKindModel: n := ResourceName{Kind: ResourceKindSource, Name: r.Name.Name}.Normalized() if r2, ok := p.Resources[n]; ok { modelsWithNameErrs[r.Name.Normalized()] = r2.Name.Name diff --git a/web-common/src/features/file-explorer/NavFile.svelte b/web-common/src/features/file-explorer/NavFile.svelte index 0be29f3fc4c..ceeec8c0c1b 100644 --- a/web-common/src/features/file-explorer/NavFile.svelte +++ b/web-common/src/features/file-explorer/NavFile.svelte @@ -33,8 +33,8 @@ import { ResourceKind } from "../entity-management/resource-selectors"; import ExploreMenuItems from "../explores/ExploreMenuItems.svelte"; import MetricsViewMenuItems from "../metrics-views/MetricsViewMenuItems.svelte"; -import ModelMenuItems from "../models/navigation/ModelMenuItems.svelte"; -import { PROTECTED_DIRECTORIES, PROTECTED_FILES } from "./protected-paths"; + import ModelMenuItems from "../models/navigation/ModelMenuItems.svelte"; + import { PROTECTED_DIRECTORIES, PROTECTED_FILES } from "./protected-paths"; export let filePath: string; export let onRename: (filePath: string, isDir: boolean) => void; @@ -65,7 +65,10 @@ import { PROTECTED_DIRECTORIES, PROTECTED_FILES } from "./protected-paths"; // Normalize Source to Model (Source is deprecated) $: rawResourceKind = ($resourceName?.kind ?? $inferredResourceKind) as ResourceKind; - $: resourceKind = rawResourceKind === ResourceKind.Source ? ResourceKind.Model : rawResourceKind; + $: resourceKind = + rawResourceKind === ResourceKind.Source + ? ResourceKind.Model + : rawResourceKind; $: padding = getPaddingFromPath(filePath); $: topLevelFolder = getTopLevelFolder(filePath); $: isProtectedDirectory = PROTECTED_DIRECTORIES.includes(topLevelFolder); diff --git a/web-common/src/features/resource-graph/embedding/ResourceGraph.svelte b/web-common/src/features/resource-graph/embedding/ResourceGraph.svelte index ec7252e98a0..355bce7e444 100644 --- a/web-common/src/features/resource-graph/embedding/ResourceGraph.svelte +++ b/web-common/src/features/resource-graph/embedding/ResourceGraph.svelte @@ -108,14 +108,14 @@ | "dashboards" | null { const rawSeeds = seeds ?? []; - + // Check the first seed first - this should be the anchor resource (e.g., Canvas) // This ensures Canvas/Explore tokens are prioritized over MetricsView tokens if (rawSeeds.length > 0) { const firstToken = tokenForSeedString(rawSeeds[0]); if (firstToken) return firstToken; } - + // Fall back to checking all seeds if first seed didn't yield a token for (const raw of rawSeeds) { const token = tokenForSeedString(raw); @@ -175,7 +175,8 @@ if (!k) continue; if (k === ResourceKind.Source || k === ResourceKind.Model) models++; else if (k === ResourceKind.MetricsView) metrics++; - else if (k === ResourceKind.Explore || k === ResourceKind.Canvas) dashboards++; + else if (k === ResourceKind.Explore || k === ResourceKind.Canvas) + dashboards++; } return { modelsCount: models, diff --git a/web-common/src/features/resource-graph/embedding/ResourceGraphOverlay.svelte b/web-common/src/features/resource-graph/embedding/ResourceGraphOverlay.svelte index 774030a4e0c..8ddaf90855c 100644 --- a/web-common/src/features/resource-graph/embedding/ResourceGraphOverlay.svelte +++ b/web-common/src/features/resource-graph/embedding/ResourceGraphOverlay.svelte @@ -21,13 +21,6 @@ | ResourceKind.Explore | ResourceKind.Canvas; - const NAME_SEED_ALIAS: Record = { - [ResourceKind.Model]: "model", - [ResourceKind.MetricsView]: "metrics", - [ResourceKind.Explore]: "dashboard", - [ResourceKind.Canvas]: "dashboard", - }; - const KIND_TOKEN_BY_KIND: Record = { [ResourceKind.Model]: "models", [ResourceKind.MetricsView]: "metrics", @@ -37,11 +30,16 @@ $: anchorName = anchorResource?.meta?.name?.name ?? null; // Normalize Source to Model immediately (Source is deprecated) - $: rawAnchorKind = anchorResource?.meta?.name?.kind as ResourceKind | undefined; - $: anchorKind = rawAnchorKind === ResourceKind.Source ? ResourceKind.Model : rawAnchorKind; - + $: rawAnchorKind = anchorResource?.meta?.name?.kind as + | ResourceKind + | undefined; + $: anchorKind = + rawAnchorKind === ResourceKind.Source ? ResourceKind.Model : rawAnchorKind; + // Check if kind is allowed (handles both enum and string values) - $: supportsGraph = anchorKind ? ALLOWED_FOR_GRAPH.has(String(anchorKind) as ResourceKind) : false; + $: supportsGraph = anchorKind + ? ALLOWED_FOR_GRAPH.has(String(anchorKind) as ResourceKind) + : false; // Type-safe access to graphable kind properties $: graphableKind = @@ -52,29 +50,28 @@ // Note: anchorKind is already normalized (Source -> Model) above $: overlaySeeds = (function (): string[] | undefined { if (!anchorName || !anchorKind) return undefined; - + // Use the same format that would come from URL parameters - if (anchorKind === ResourceKind.Canvas || anchorKind === ResourceKind.Explore) { + if ( + anchorKind === ResourceKind.Canvas || + anchorKind === ResourceKind.Explore + ) { return [`dashboard:${anchorName}`]; } else if (anchorKind === ResourceKind.Model) { return [`model:${anchorName}`]; } else if (anchorKind === ResourceKind.MetricsView) { return [`metrics:${anchorName}`]; } - + return undefined; })(); - - // Keep the old anchorSeed for graphHref calculation - $: anchorSeed = - graphableKind && anchorName - ? `${NAME_SEED_ALIAS[graphableKind]}:${anchorName}` - : null; + $: graphHref = graphableKind ? `/graph?kind=${encodeURIComponent(KIND_TOKEN_BY_KIND[graphableKind])}` : "/graph"; - $: emptyReason = !overlaySeeds || overlaySeeds.length === 0 ? "unsupported" : null; + $: emptyReason = + !overlaySeeds || overlaySeeds.length === 0 ? "unsupported" : null; function closeOverlay() { if (onClose) { diff --git a/web-common/src/features/resource-graph/graph-canvas/graph-builder.ts b/web-common/src/features/resource-graph/graph-canvas/graph-builder.ts index 2db09357476..d12fa3dadf2 100644 --- a/web-common/src/features/resource-graph/graph-canvas/graph-builder.ts +++ b/web-common/src/features/resource-graph/graph-canvas/graph-builder.ts @@ -183,7 +183,6 @@ export function buildResourceGraph( } } - const edgeIds = new Set(); const edges: Edge[] = []; diff --git a/web-common/src/features/resource-graph/navigation/seed-parser.spec.ts b/web-common/src/features/resource-graph/navigation/seed-parser.spec.ts index 8ed92dd9a6f..1e02a8e542d 100644 --- a/web-common/src/features/resource-graph/navigation/seed-parser.spec.ts +++ b/web-common/src/features/resource-graph/navigation/seed-parser.spec.ts @@ -708,11 +708,7 @@ describe("seed-utils", () => { }, ]; - const result = expandSeedsByKind( - ["models"], - resources, - customCoerceKind, - ); + const result = expandSeedsByKind(["models"], resources, customCoerceKind); // Should find both Source (normalized to Model) and Model resources expect(result).toHaveLength(2); diff --git a/web-common/src/features/resource-graph/navigation/seed-parser.ts b/web-common/src/features/resource-graph/navigation/seed-parser.ts index a02778aaa7b..820babe1715 100644 --- a/web-common/src/features/resource-graph/navigation/seed-parser.ts +++ b/web-common/src/features/resource-graph/navigation/seed-parser.ts @@ -163,7 +163,12 @@ export function tokenForKind( // Sources and Models are merged (Source is deprecated) if (key.includes("source") || key.includes("model")) return "models"; if (key.includes("metricsview") || key.includes("metric")) return "metrics"; - if (key.includes("explore") || key.includes("dashboard") || key.includes("canvas")) return "dashboards"; + if ( + key.includes("explore") || + key.includes("dashboard") || + key.includes("canvas") + ) + return "dashboards"; return null; } @@ -207,7 +212,11 @@ export function tokenForSeedString( return "models"; } // Handle "dashboard:" or "canvas:" prefix - treat as "dashboards" - if (kindPart === "dashboard" || kindPart === "dashboards" || kindPart === "canvas") { + if ( + kindPart === "dashboard" || + kindPart === "dashboards" || + kindPart === "canvas" + ) { return "dashboards"; } const mapped = resolveKindAlias(kindPart); @@ -276,9 +285,11 @@ export function expandSeedsByKind( } // Handle "sources" token - treat as "models" (includes both Source and Model) - const isSourcesToken = raw.toLowerCase() === "sources" || raw.toLowerCase() === "source"; - const isModelsToken = raw.toLowerCase() === "models" || raw.toLowerCase() === "model"; - + const isSourcesToken = + raw.toLowerCase() === "sources" || raw.toLowerCase() === "source"; + const isModelsToken = + raw.toLowerCase() === "models" || raw.toLowerCase() === "model"; + // Check if it's a kind token const kindToken = isKindToken(raw); if (!kindToken && !isSourcesToken) { @@ -290,16 +301,25 @@ export function expandSeedsByKind( // Expand: one seed per visible resource of this kind // Special case: "dashboards" includes both Explore and Canvas // Special case: "sources" and "models" both include Source and Model (merged) - const isDashboardsToken = raw.toLowerCase() === "dashboards" || raw.toLowerCase() === "dashboard"; + const isDashboardsToken = + raw.toLowerCase() === "dashboards" || raw.toLowerCase() === "dashboard"; const isModelsOrSourcesToken = isModelsToken || isSourcesToken; for (const r of visible) { const resourceKind = coerceKindFn(r); if (isDashboardsToken) { // Include both Explore and Canvas for dashboards token - if (resourceKind !== ResourceKind.Explore && resourceKind !== ResourceKind.Canvas) continue; + if ( + resourceKind !== ResourceKind.Explore && + resourceKind !== ResourceKind.Canvas + ) + continue; } else if (isModelsOrSourcesToken) { // Include both Source and Model for models/sources token (merged) - if (resourceKind !== ResourceKind.Source && resourceKind !== ResourceKind.Model) continue; + if ( + resourceKind !== ResourceKind.Source && + resourceKind !== ResourceKind.Model + ) + continue; } else { // Normal kind matching if (resourceKind !== kindToken) continue; @@ -368,11 +388,10 @@ export function parseGraphUrlParams( if (kindParam === "sources" || kindParam === "source") { normalizedKindParam = "models"; } - + const validKind = normalizedKindParam as KindToken | null; const kind = - validKind && - ["metrics", "models", "dashboards"].includes(validKind) + validKind && ["metrics", "models", "dashboards"].includes(validKind) ? validKind : null; diff --git a/web-common/src/features/resource-graph/summary/SummaryGraph.svelte b/web-common/src/features/resource-graph/summary/SummaryGraph.svelte index 13e40087e32..9f8134bb393 100644 --- a/web-common/src/features/resource-graph/summary/SummaryGraph.svelte +++ b/web-common/src/features/resource-graph/summary/SummaryGraph.svelte @@ -21,11 +21,7 @@ // Full list of resources (for selection panel) export let resources: V1Resource[] = []; // Active token to highlight: 'metrics' | 'models' | 'dashboards' - export let activeToken: - | "metrics" - | "models" - | "dashboards" - | null = null; + export let activeToken: "metrics" | "models" | "dashboards" | null = null; let containerEl: HTMLDivElement | null = null; let containerKey = ""; diff --git a/web-common/src/features/resource-graph/summary/SummaryNode.svelte b/web-common/src/features/resource-graph/summary/SummaryNode.svelte index a22db92c7c5..9c3b92eeb7e 100644 --- a/web-common/src/features/resource-graph/summary/SummaryNode.svelte +++ b/web-common/src/features/resource-graph/summary/SummaryNode.svelte @@ -61,8 +61,7 @@ normalizedKind && resourceShorthandMapping[normalizedKind] ? `var(--${resourceShorthandMapping[normalizedKind]})` : DEFAULT_COLOR; - $: Icon = - (normalizedKind && resourceIconMapping[normalizedKind]) || null; + $: Icon = (normalizedKind && resourceIconMapping[normalizedKind]) || null; $: label = data?.label ?? ""; $: count = data?.count ?? 0; $: isActive = data?.active ?? selected; @@ -75,9 +74,11 @@ const kind = data?.kind; let token: string | null = null; // Sources and Models are merged (Source is deprecated) - if (kind === ResourceKind.Source || kind === ResourceKind.Model) token = "models"; + if (kind === ResourceKind.Source || kind === ResourceKind.Model) + token = "models"; else if (kind === ResourceKind.MetricsView) token = "metrics"; - else if (kind === ResourceKind.Explore || kind === ResourceKind.Canvas) token = "dashboards"; + else if (kind === ResourceKind.Explore || kind === ResourceKind.Canvas) + token = "dashboards"; if (token) goto(`/graph?kind=${token}`); }