Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
57 changes: 56 additions & 1 deletion runtime/parser/parse_canvas.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,62 @@ 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
// 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 {
if !existingRefs[ref] {
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
18 changes: 9 additions & 9 deletions runtime/parser/parse_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions runtime/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions web-common/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
--explore: var(--color-indigo-700);
--metrics: var(--color-violet-600);
--model: var(--color-cyan-600);
--source: var(--model);
--source: var(--color-emerald-600);
--api: var(--color-orange-400);
--data: var(--color-fuchsia-500);
--theme: var(--color-pink-500);
Expand Down Expand Up @@ -307,7 +307,7 @@
--explore: var(--color-indigo-light-400);
--metrics: var(--color-violet-light-400);
--model: var(--color-cyan-light-500);
--source: var(--model);
--source: var(--color-emerald-600);
--api: var(--color-orange-light-300);
--data: var(--color-fuchsia-light-400);
--theme: var(--color-pink-light-400);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const resourceShorthandMapping = {
[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 @@ -4,13 +4,14 @@ import type { V1Resource } from "@rilldata/web-common/runtime-client";

describe("resource-selectors", () => {
describe("coerceResourceKind", () => {
it("should return Model kind for normal models", () => {
it("should return Model kind for models with model dependencies", () => {
const resource: V1Resource = {
meta: {
name: {
kind: ResourceKind.Model,
name: "orders",
},
refs: [{ kind: ResourceKind.Model, name: "raw_orders" }],
},
model: {
spec: {
Expand All @@ -24,13 +25,56 @@ describe("resource-selectors", () => {
expect(coerceResourceKind(resource)).toBe(ResourceKind.Model);
});

it("should return Source kind for root models with no model dependencies", () => {
const resource: V1Resource = {
meta: {
name: {
kind: ResourceKind.Model,
name: "raw_orders",
},
refs: [], // No model refs - this is a root model
},
model: {
spec: {
definedAsSource: false,
},
state: {
resultTable: "raw_orders",
},
},
};
expect(coerceResourceKind(resource)).toBe(ResourceKind.Source);
});

it("should return Source kind for models with only connector refs (no model refs)", () => {
const resource: V1Resource = {
meta: {
name: {
kind: ResourceKind.Model,
name: "raw_data",
},
refs: [{ kind: ResourceKind.Connector, name: "duckdb" }],
},
model: {
spec: {
definedAsSource: false,
},
state: {
resultTable: "raw_data",
},
},
};
expect(coerceResourceKind(resource)).toBe(ResourceKind.Source);
});

it("should return Source kind for models defined-as-source with matching table name", () => {
const resource: V1Resource = {
meta: {
name: {
kind: ResourceKind.Model,
name: "raw_orders",
},
refs: [{ kind: ResourceKind.Model, name: "other_model" }],
},
model: {
spec: {
Expand All @@ -41,6 +85,7 @@ describe("resource-selectors", () => {
},
},
};
// definedAsSource takes precedence even with model refs
expect(coerceResourceKind(resource)).toBe(ResourceKind.Source);
});

Expand All @@ -51,6 +96,7 @@ describe("resource-selectors", () => {
kind: ResourceKind.Model,
name: "raw_orders",
},
refs: [{ kind: ResourceKind.Model, name: "other_model" }],
},
model: {
spec: {
Expand All @@ -61,6 +107,7 @@ describe("resource-selectors", () => {
},
},
};
// Has model deps and definedAsSource doesn't match table name
expect(coerceResourceKind(resource)).toBe(ResourceKind.Model);
});

Expand Down Expand Up @@ -111,13 +158,14 @@ describe("resource-selectors", () => {
expect(coerceResourceKind(resource)).toBe(ResourceKind.Explore);
});

it("should handle case where definedAsSource is false explicitly", () => {
it("should return Source for models with undefined refs (treated as no model deps)", () => {
const resource: V1Resource = {
meta: {
name: {
kind: ResourceKind.Model,
name: "orders",
},
// No refs property - should be treated as no model dependencies
},
model: {
spec: {
Expand All @@ -128,8 +176,7 @@ describe("resource-selectors", () => {
},
},
};
// Even though table name matches, definedAsSource is false
expect(coerceResourceKind(resource)).toBe(ResourceKind.Model);
expect(coerceResourceKind(resource)).toBe(ResourceKind.Source);
});
});
});
28 changes: 23 additions & 5 deletions web-common/src/features/entity-management/resource-selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function displayResourceKind(kind: ResourceKind | undefined) {
case ResourceKind.Report:
return "report";
case ResourceKind.Source:
return "source";
return "source model";
case ResourceKind.Connector:
return "connector";
case ResourceKind.Model:
Expand Down Expand Up @@ -110,10 +110,21 @@ export function prettyResourceKind(kind: string) {
return kind.replace(/^rill\.runtime\.v1\./, "");
}

/**
* Check if a model has any dependencies on other models.
* A model is a "root" model (Source Model) if it has no model refs.
*/
function hasModelDependencies(res: V1Resource): boolean {
const refs = res.meta?.refs ?? [];
return refs.some((ref) => ref.kind === ResourceKind.Model);
}

/**
* Coerce resource kind to match UI representation.
* Models that are defined-as-source are displayed as Sources in the sidebar and graph.
* This ensures consistent representation across the application.
* Source Models are displayed as Sources in the sidebar and graph:
* 1. Actual Source resources (legacy)
* 2. Models with definedAsSource: true (legacy converted sources)
* 3. Models with no model dependencies (root models in the DAG)
*
* @param res - The resource to check
* @returns The coerced ResourceKind, or undefined if the resource has no kind
Expand All @@ -122,19 +133,26 @@ export function prettyResourceKind(kind: string) {
* // A model that is defined-as-source
* coerceResourceKind(modelResource) // Returns ResourceKind.Source
*
* // A normal model
* // A root model with no model dependencies
* coerceResourceKind(rootModel) // Returns ResourceKind.Source
*
* // A normal model that depends on other models
* 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
// Legacy: model defined-as-source with matching result table
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;
}
// New: root models with no model dependencies are Source Models
if (!hasModelDependencies(res)) {
return ResourceKind.Source;
}
}
return raw;
}
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
Loading
Loading