Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion runtime/parser/parse_canvas.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import Chart from "@rilldata/web-common/components/icons/Chart.svelte";
import ExploreIcon from "@rilldata/web-common/components/icons/ExploreIcon.svelte";
import ReportIcon from "@rilldata/web-common/components/icons/ReportIcon.svelte";
import TableIcon from "@rilldata/web-common/components/icons/TableIcon.svelte";

Check failure on line 7 in web-common/src/features/entity-management/resource-icon-mapping.ts

View workflow job for this annotation

GitHub Actions / build

'TableIcon' is defined but never used
import ThemeIcon from "@rilldata/web-common/components/icons/ThemeIcon.svelte";
import { ResourceKind } from "@rilldata/web-common/features/entity-management/resource-selectors";
import ConnectorIcon from "../../components/icons/ConnectorIcon.svelte";
Expand All @@ -14,7 +14,8 @@
import SettingsIcon from "@rilldata/web-common/components/icons/SettingsIcon.svelte";

export const resourceIconMapping = {
[ResourceKind.Source]: TableIcon,
// Source is deprecated and merged with Model - use Model icon
[ResourceKind.Source]: ModelIcon,
[ResourceKind.Connector]: ConnectorIcon,
[ResourceKind.Model]: ModelIcon,
[ResourceKind.MetricsView]: MetricsViewIcon,
Expand Down Expand Up @@ -47,7 +48,7 @@
[ResourceKind.Model]: "model",
[ResourceKind.MetricsView]: "metrics",
[ResourceKind.Explore]: "explore",
[ResourceKind.API]: "API",
[ResourceKind.API]: "api",
[ResourceKind.Component]: "component",
[ResourceKind.Canvas]: "canvas",
[ResourceKind.Theme]: "theme",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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: {
Expand All @@ -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", () => {
Expand Down
17 changes: 6 additions & 11 deletions web-common/src/features/entity-management/resource-selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,29 +112,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;
}
Expand Down
13 changes: 6 additions & 7 deletions web-common/src/features/file-explorer/NavFile.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +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 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;
Expand Down Expand Up @@ -63,8 +62,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);
Expand Down Expand Up @@ -160,9 +161,7 @@
Duplicate
</NavigationMenuItem>
{#if resourceKind}
{#if resourceKind === ResourceKind.Source}
<SourceMenuItems {filePath} />
{:else if resourceKind === ResourceKind.Model}
{#if resourceKind === ResourceKind.Model}
<ModelMenuItems {filePath} />
{:else if resourceKind === ResourceKind.MetricsView}
<MetricsViewMenuItems {filePath} />
Expand Down
3 changes: 1 addition & 2 deletions web-common/src/features/resource-graph/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ The graph page supports two URL parameters:
Navigate to `/graph?kind=<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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
{showControls}
showLock={false}
enableExpand={false}
isOverlay={true}
{fitViewPadding}
{fitViewMinZoom}
{fitViewMaxZoom}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,19 @@
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 = {
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
Expand All @@ -77,26 +76,48 @@

// 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;
for (const raw of rawSeeds) {
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;
Expand Down Expand Up @@ -143,23 +164,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,
Expand All @@ -171,7 +190,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,
Expand All @@ -180,9 +198,8 @@
};
$: {
const nextSummary: SummaryMemo = {
sources: sourcesCount,
metrics: metricsCount,
models: modelsCount,
metrics: metricsCount,
dashboards: dashboardsCount,
resources: normalizedResources,
activeToken: overviewActiveToken,
Expand Down Expand Up @@ -397,14 +414,12 @@
{#if showSummary && currentExpandedId === null}
<slot
name="summary"
sources={sourcesCount}
{metricsCount}
{modelsCount}
dashboards={dashboardsCount}
>
<div class="top-summary">
<SummaryGraph
sources={summaryMemo.sources}
metrics={summaryMemo.metrics}
models={summaryMemo.models}
dashboards={summaryMemo.dashboards}
Expand Down Expand Up @@ -479,6 +494,7 @@
showLock={false}
fillParent={true}
enableExpand={enableExpansion}
{isOverlay}
{fitViewPadding}
{fitViewMinZoom}
{fitViewMaxZoom}
Expand All @@ -505,6 +521,7 @@
showLock={true}
fillParent={false}
enableExpand={enableExpansion}
{isOverlay}
{fitViewPadding}
{fitViewMinZoom}
{fitViewMaxZoom}
Expand Down
Loading
Loading