From b61efe92dee9018429957dd34f32f44e9161d58a Mon Sep 17 00:00:00 2001 From: Sean Teramae Date: Wed, 22 Jul 2026 11:42:04 -0700 Subject: [PATCH 1/2] feat(studio): DD schema list in builder Signed-off-by: Sean Teramae --- .../src/components/AddColumnPalette/index.tsx | 2 +- .../ColumnConfigPanel/SeedDatasetConfig.tsx | 10 +- .../BuilderToolbar.tsx | 22 +++- .../DataDesignerJobBuildRoute/SchemaList.tsx | 74 +++++++++++++ .../DataDesignerJobBuildRoute/SchemaRow.tsx | 101 ++++++++++++++++++ .../DataDesignerJobBuildRoute/columns.test.ts | 62 +++++++++++ .../DataDesignerJobBuildRoute/columns.ts | 65 ++++++++++- .../describeColumn.test.ts | 70 ++++++++++++ .../describeColumn.ts | 98 +++++++++++++++++ .../DataDesignerJobBuildRoute/index.tsx | 27 +++-- 10 files changed, 517 insertions(+), 14 deletions(-) create mode 100644 web/packages/studio/src/routes/DataDesignerJobBuildRoute/SchemaList.tsx create mode 100644 web/packages/studio/src/routes/DataDesignerJobBuildRoute/SchemaRow.tsx create mode 100644 web/packages/studio/src/routes/DataDesignerJobBuildRoute/describeColumn.test.ts create mode 100644 web/packages/studio/src/routes/DataDesignerJobBuildRoute/describeColumn.ts diff --git a/web/packages/studio/src/components/AddColumnPalette/index.tsx b/web/packages/studio/src/components/AddColumnPalette/index.tsx index fc2d6b40a3..1c2cbf1ed2 100644 --- a/web/packages/studio/src/components/AddColumnPalette/index.tsx +++ b/web/packages/studio/src/components/AddColumnPalette/index.tsx @@ -73,7 +73,7 @@ export const AddColumnPalette: FC = ({ attributes={{ Input: { 'aria-label': 'Search column types' } }} /> - + {filteredGroups.length === 0 ? ( No column types match “{search}”. diff --git a/web/packages/studio/src/components/ColumnConfigPanel/SeedDatasetConfig.tsx b/web/packages/studio/src/components/ColumnConfigPanel/SeedDatasetConfig.tsx index 8fe513fca0..014ba43412 100644 --- a/web/packages/studio/src/components/ColumnConfigPanel/SeedDatasetConfig.tsx +++ b/web/packages/studio/src/components/ColumnConfigPanel/SeedDatasetConfig.tsx @@ -96,6 +96,11 @@ export const SeedDatasetConfig: FC = ({ columnIndex }) = } }, [availableColumns, availableColumnsPath, availableColumnsValue, setValue]); + const samplingItems = SAMPLING_STRATEGY_OPTIONS.map((option) => ({ + children: option.label, + value: option.value, + })); + return ( <> = ({ columnIndex }) = ({ - children: option.label, - value: option.value, - }))} + items={samplingItems} useControllerProps={{ name: samplingStrategyPath }} formFieldProps={{ slotLabel: 'Sampling strategy', diff --git a/web/packages/studio/src/routes/DataDesignerJobBuildRoute/BuilderToolbar.tsx b/web/packages/studio/src/routes/DataDesignerJobBuildRoute/BuilderToolbar.tsx index 20c1b34f57..0e49fcd9d2 100644 --- a/web/packages/studio/src/routes/DataDesignerJobBuildRoute/BuilderToolbar.tsx +++ b/web/packages/studio/src/routes/DataDesignerJobBuildRoute/BuilderToolbar.tsx @@ -3,17 +3,23 @@ import { ControlledTextInput } from '@nemo/common/src/components/form/ControlledTextInput'; import { LoadingButton } from '@nemo/common/src/components/LoadingButton'; -import { Button, Flex, Tag, Text } from '@nvidia/foundations-react-core'; +import { Button, Flex, SegmentedControl, Tag, Text } from '@nvidia/foundations-react-core'; import type { StartOptionTag } from '@studio/components/CreateFilesetStart/types'; import type { JobBuilderFormValues } from '@studio/routes/DataDesignerJobBuildRoute/useJobBuilder'; -import { FileJson, Pencil } from 'lucide-react'; +import { FileJson, ListTree, Pencil, SplinePointer } from 'lucide-react'; import { type FC, useState } from 'react'; import { useFormContext, useWatch } from 'react-hook-form'; +/** Which renderer the center pane shows: the flat schema list or the DAG canvas. */ +export type BuilderViewMode = 'list' | 'canvas'; + export interface BuilderToolbarProps { /** The template's badge (recipe use case), shown when building from a template. */ templateTag?: StartOptionTag; columnCount: number; + /** Which renderer the center pane shows. */ + viewMode: BuilderViewMode; + onViewModeChange: (mode: BuilderViewMode) => void; onPreview: () => void; isPreviewing: boolean; onSubmit: () => void; @@ -23,6 +29,8 @@ export interface BuilderToolbarProps { export const BuilderToolbar: FC = ({ templateTag, columnCount, + viewMode, + onViewModeChange, onPreview, isPreviewing, onSubmit, @@ -47,6 +55,7 @@ export const BuilderToolbar: FC = ({ setIsEditingName(false)} formFieldProps={{ className: 'w-[220px]' }} attributes={{ Input: { 'aria-label': 'Fileset name' } }} @@ -79,6 +88,15 @@ export const BuilderToolbar: FC = ({ + onViewModeChange(value as BuilderViewMode)} + items={[ + { value: 'list', children: }, + { value: 'canvas', children: }, + ]} + /> Rows diff --git a/web/packages/studio/src/routes/DataDesignerJobBuildRoute/SchemaList.tsx b/web/packages/studio/src/routes/DataDesignerJobBuildRoute/SchemaList.tsx new file mode 100644 index 0000000000..36e1fa398d --- /dev/null +++ b/web/packages/studio/src/routes/DataDesignerJobBuildRoute/SchemaList.tsx @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Flex, Stack, Text } from '@nvidia/foundations-react-core'; +import { + getColumnReferences, + topologicalSortColumns, +} from '@studio/routes/DataDesignerJobBuildRoute/columns'; +import { SchemaRow } from '@studio/routes/DataDesignerJobBuildRoute/SchemaRow'; +import type { JobBuilderFormValues } from '@studio/routes/DataDesignerJobBuildRoute/useJobBuilder'; +import { type FC, useMemo } from 'react'; +import { useFormContext, useWatch } from 'react-hook-form'; + +export interface SchemaListProps { + selectedId: string | null; + onSelect: (id: string | null) => void; + onDelete: (id: string) => void; +} + +/** + * A flat, top-to-bottom list of the recipe's columns — a simpler alternative to the DAG + * canvas. Each row shows the column's type and summary, with its dependencies listed + * inline as relationship tags rather than drawn as edges. Selecting a row opens the same + * config pane the canvas uses, so the surrounding left/right panels are unchanged. + */ +export const SchemaList: FC = ({ selectedId, onSelect, onDelete }) => { + const { control } = useFormContext(); + const columnRecord = useWatch({ control, name: 'columns' }); + const columns = useMemo(() => topologicalSortColumns(columnRecord), [columnRecord]); + const referencesById = useMemo(() => { + const knownNames = new Set(columns.map((column) => column.name).filter(Boolean)); + return new Map(columns.map((column) => [column.id, getColumnReferences(column, knownNames)])); + }, [columns]); + + return ( + + + + + Schema + + + Reference columns with {'{{ column }}'}. + + + + + {columns.length === 0 ? ( + + + No columns yet — add one from the left to get started. + + + ) : ( + + {columns.map((column) => ( + onSelect(column.id)} + onDelete={() => onDelete(column.id)} + /> + ))} + + )} + + ); +}; diff --git a/web/packages/studio/src/routes/DataDesignerJobBuildRoute/SchemaRow.tsx b/web/packages/studio/src/routes/DataDesignerJobBuildRoute/SchemaRow.tsx new file mode 100644 index 0000000000..aca39a7285 --- /dev/null +++ b/web/packages/studio/src/routes/DataDesignerJobBuildRoute/SchemaRow.tsx @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Badge, Flex, Stack, Tag, Text } from '@nvidia/foundations-react-core'; +import { CardIconBadge } from '@studio/components/common/SelectableCard'; +import type { BuilderColumn } from '@studio/routes/DataDesignerJobBuildRoute/columns'; +import { describeColumn } from '@studio/routes/DataDesignerJobBuildRoute/describeColumn'; +import { Box, Trash2 } from 'lucide-react'; +import type { FC } from 'react'; + +/** Accent color → NVIDIA Foundations text token, matching the DAG node icon styling. */ +const ACCENT_ICON_CLASS: Record = { + blue: 'text-[color:var(--text-color-accent-blue)]', + gray: 'text-[color:var(--text-color-accent-gray)]', + green: 'text-[color:var(--text-color-accent-green)]', + purple: 'text-[color:var(--text-color-accent-purple)]', + red: 'text-[color:var(--text-color-accent-red)]', + teal: 'text-[color:var(--text-color-accent-teal)]', + yellow: 'text-[color:var(--text-color-accent-yellow)]', +}; + +export interface SchemaRowProps { + column: BuilderColumn; + /** Names of columns this one references, shown inline as `{{ name }}` relationship tags. */ + references: string[]; + selected: boolean; + onSelect: () => void; + onDelete: () => void; +} + +/** + * One column rendered as a row in the schema list: a generation-step number, an icon badge, + * the column name, a type badge, a one-line summary, and its relationship tags. Selecting the + * row opens the same config pane the DAG canvas uses; the trailing button deletes the column. + */ +export const SchemaRow: FC = ({ + column, + references, + selected, + onSelect, + onDelete, +}) => { + const { option } = column; + const { typeLabel, detail } = describeColumn(column); + const Icon = option.icon ?? Box; + + return ( + + + + + + ); +}; diff --git a/web/packages/studio/src/routes/DataDesignerJobBuildRoute/columns.test.ts b/web/packages/studio/src/routes/DataDesignerJobBuildRoute/columns.test.ts index 51eddcc324..153f3ab374 100644 --- a/web/packages/studio/src/routes/DataDesignerJobBuildRoute/columns.test.ts +++ b/web/packages/studio/src/routes/DataDesignerJobBuildRoute/columns.test.ts @@ -13,6 +13,7 @@ import { defaultColumnName, extractJinjaReferences, findColumnOption, + topologicalSortColumns, validateColumnName, validateColumns, } from '@studio/routes/DataDesignerJobBuildRoute/columns'; @@ -494,6 +495,67 @@ describe('preference-pairs template', () => { }); }); +describe('topologicalSortColumns', () => { + const ids = (columns: BuilderColumn[]) => columns.map((c) => c.id); + + it('places each column after the columns it references', () => { + // Input order deliberately reversed: judge → response → instruction → topic. + const input = [ + column('c3', 'quality', 'llm-judge', { prompt: 'Rate {{ response }}', model_alias: 'm' }), + column('c2', 'response', 'llm-text', { + prompt: 'Answer {{ instruction }}', + model_alias: 'm', + }), + column('c1', 'instruction', 'llm-text', { + prompt: 'Ask about {{ topic }}', + model_alias: 'm', + }), + column('c0', 'topic', 'sampler', { values: 'a, b' }, SamplerType.category), + ]; + expect(ids(topologicalSortColumns(input))).toEqual(['c0', 'c1', 'c2', 'c3']); + }); + + it('keeps independent columns and ties in their original relative order', () => { + const input = [ + column('a', 'alpha', 'sampler', { values: 'x' }, SamplerType.category), + column('b', 'beta', 'sampler', { values: 'y' }, SamplerType.category), + column('c', 'gamma', 'sampler', { values: 'z' }, SamplerType.category), + ]; + expect(ids(topologicalSortColumns(input))).toEqual(['a', 'b', 'c']); + }); + + it('groups by dependency depth, not add order — a shallow dependent sorts above a deeper one', () => { + // entity_type → description → structured is a depth-2 chain; llm_text_1 depends only on + // entity_type (depth 1). Even though it was added last, it must sort above `structured`. + const input = [ + column('c0', 'entity_type', 'sampler', { values: 'a, b' }, SamplerType.category), + column('c1', 'description', 'llm-text', { + prompt: 'Describe {{ entity_type }}', + model_alias: 'm', + }), + column('c2', 'structured', 'llm-structured', { + prompt: 'Structure {{ description }}', + model_alias: 'm', + output_format: '{}', + }), + column('c3', 'llm_text_1', 'llm-text', { + prompt: 'Another {{ entity_type }}', + model_alias: 'm', + }), + ]; + // depths: entity_type 0, description 1, llm_text_1 1, structured 2. + expect(ids(topologicalSortColumns(input))).toEqual(['c0', 'c1', 'c3', 'c2']); + }); + + it('emits cyclic references without looping', () => { + const input = [ + column('c1', 'one', 'expression', { expr: '{{ two }}' }), + column('c2', 'two', 'expression', { expr: '{{ one }}' }), + ]; + expect(ids(topologicalSortColumns(input)).sort()).toEqual(['c1', 'c2']); + }); +}); + describe('palette catalog', () => { it('has a field descriptor path for every catalog column type', () => { // Sanity: findColumnOption resolves every option the palette can emit. diff --git a/web/packages/studio/src/routes/DataDesignerJobBuildRoute/columns.ts b/web/packages/studio/src/routes/DataDesignerJobBuildRoute/columns.ts index a86a8c3b8b..6bd3ecf231 100644 --- a/web/packages/studio/src/routes/DataDesignerJobBuildRoute/columns.ts +++ b/web/packages/studio/src/routes/DataDesignerJobBuildRoute/columns.ts @@ -675,7 +675,24 @@ const columnDependencies = (column: BuilderColumn, knownNames: Set): Set return deps; }; -export const buildGraph = (columns: BuilderColumn[]): { nodes: DagNode[]; edges: DagEdge[] } => { +/** + * The names of existing columns this column references (the same dependencies that draw + * edges in {@link buildGraph}). Used by the schema-list renderer to show each column's + * relationships inline instead of as graph edges. + */ +export const getColumnReferences = (column: BuilderColumn, knownNames: Set): string[] => [ + ...columnDependencies(column, knownNames), +]; + +/** + * The set of referenceable column names and the name→id lookup used to resolve + * dependencies. Seed-dataset columns also contribute the names they provide, mapped back + * to the seed column. Shared by {@link buildGraph} and {@link topologicalSortColumns} so + * both agree on what counts as a dependency. + */ +const buildNameMaps = ( + columns: BuilderColumn[] +): { knownNames: Set; idByName: Map } => { const knownNames = new Set(columns.map((column) => column.name).filter(Boolean)); const idByName = new Map(columns.filter((c) => c.name).map((c) => [c.name, c.id])); @@ -686,6 +703,52 @@ export const buildGraph = (columns: BuilderColumn[]): { nodes: DagNode[]; edges: if (!idByName.has(name)) idByName.set(name, column.id); } } + return { knownNames, idByName }; +}; + +/** + * Orders columns into dependency layers: each column's depth is the longest chain of + * references beneath it (a column with no references is depth 0), and columns are sorted by + * that depth ascending. So every column still comes after everything it references, and + * shallower columns group above deeper ones regardless of the order they were added — a + * column that depends only on a root sits above one that depends on a mid-chain column. + * Equal-depth ties keep their original relative order; reference cycles resolve to a finite + * depth without looping. + */ +export const topologicalSortColumns = (columns: BuilderColumn[]): BuilderColumn[] => { + const { knownNames, idByName } = buildNameMaps(columns); + const depsById = new Map( + columns.map((column) => [ + column.id, + [...columnDependencies(column, knownNames)] + .map((name) => idByName.get(name)) + .filter((id): id is string => !!id && id !== column.id), + ]) + ); + + const depthById = new Map(); + const onPath = new Set(); + const depthOf = (id: string): number => { + const cached = depthById.get(id); + if (cached !== undefined) return cached; + if (onPath.has(id)) return 0; // cycle edge: stop descending, contribute no depth + onPath.add(id); + const deps = depsById.get(id) ?? []; + const depth = deps.length === 0 ? 0 : 1 + Math.max(...deps.map(depthOf)); + onPath.delete(id); + depthById.set(id, depth); + return depth; + }; + + // Stable sort by depth, then by original index, so ties keep their input order. + return columns + .map((column, index) => ({ column, index, depth: depthOf(column.id) })) + .sort((a, b) => a.depth - b.depth || a.index - b.index) + .map((entry) => entry.column); +}; + +export const buildGraph = (columns: BuilderColumn[]): { nodes: DagNode[]; edges: DagEdge[] } => { + const { knownNames, idByName } = buildNameMaps(columns); const nodes: DagNode[] = []; const edges: DagEdge[] = []; diff --git a/web/packages/studio/src/routes/DataDesignerJobBuildRoute/describeColumn.test.ts b/web/packages/studio/src/routes/DataDesignerJobBuildRoute/describeColumn.test.ts new file mode 100644 index 0000000000..cedc438d3b --- /dev/null +++ b/web/packages/studio/src/routes/DataDesignerJobBuildRoute/describeColumn.test.ts @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { SamplerType } from '@nemo/sdk/generated/data-designer/schema'; +import type { ColumnTypeOption } from '@studio/components/AddColumnPalette/types'; +import { + type BuilderColumn, + findColumnOption, +} from '@studio/routes/DataDesignerJobBuildRoute/columns'; +import { describeColumn } from '@studio/routes/DataDesignerJobBuildRoute/describeColumn'; + +const optionFor = (columnType: string, samplerType?: string): ColumnTypeOption => { + const found = findColumnOption({ columnType, samplerType } as never); + if (!found) throw new Error(`no palette option for ${columnType}/${samplerType}`); + return found; +}; + +const column = ( + columnType: string, + values: Record, + samplerType?: string +): BuilderColumn => ({ + id: 'col-0', + name: 'col', + option: optionFor(columnType, samplerType), + values, +}); + +describe('describeColumn', () => { + it('labels the badge with the palette option label', () => { + expect(describeColumn(column('llm-text', {})).typeLabel).toBe('LLM-Text'); + expect(describeColumn(column('sampler', {}, SamplerType.category)).typeLabel).toBe('Category'); + }); + + it('joins a few category values with slashes and counts larger sets', () => { + expect( + describeColumn(column('sampler', { values: 'easy, medium, hard' }, SamplerType.category)) + .detail + ).toBe('easy / medium / hard'); + expect( + describeColumn( + column('sampler', { values: 'CUDA, Triton, TensorRT, cuDNN, NCCL' }, SamplerType.category) + ).detail + ).toBe('5 values · CUDA, Triton, TensorRT, cuDNN, …'); + }); + + it('summarizes an llm-judge column from its parsed scores', () => { + const scores = JSON.stringify([ + { name: 'relevance' }, + { name: 'accuracy' }, + { name: 'fluency' }, + ]); + expect(describeColumn(column('llm-judge', { scores })).detail).toBe( + 'judge · 3 scores: relevance, accuracy, fluency' + ); + }); + + it('falls back to a bare label when judge scores are missing or malformed', () => { + expect(describeColumn(column('llm-judge', {})).detail).toBe('judge'); + expect(describeColumn(column('llm-judge', { scores: 'not json' })).detail).toBe('judge'); + }); + + it('describes generators and transforms', () => { + expect(describeColumn(column('llm-text', {})).detail).toBe('generator'); + expect(describeColumn(column('llm-code', { code_lang: 'python' })).detail).toBe( + 'code generator · python' + ); + expect(describeColumn(column('expression', {})).detail).toBe('expression · no LLM'); + }); +}); diff --git a/web/packages/studio/src/routes/DataDesignerJobBuildRoute/describeColumn.ts b/web/packages/studio/src/routes/DataDesignerJobBuildRoute/describeColumn.ts new file mode 100644 index 0000000000..a7057ba8d1 --- /dev/null +++ b/web/packages/studio/src/routes/DataDesignerJobBuildRoute/describeColumn.ts @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { SamplerType } from '@nemo/sdk/generated/data-designer/schema'; +import { + type BuilderColumn, + getSeedAvailableColumns, + SEED_FILESET_REF_KEY, +} from '@studio/routes/DataDesignerJobBuildRoute/columns'; + +/** A short, human summary of a column for the schema-list renderer. */ +export interface ColumnDescription { + /** Badge label for the column's type (or sampler sub-type). */ + typeLabel: string; + /** One-line summary of what the column produces, or `null` if it's not yet configured. */ + detail: string | null; +} + +/** Splits a comma-separated field value into trimmed, non-empty entries. */ +const splitList = (value: string | undefined): string[] => + (value ?? '') + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean); + +/** `${n} values · a, b, c, …`, collapsing to `a / b / c` when there are only a few. */ +const summarizeValues = (values: string[]): string => { + if (values.length === 0) return 'no values yet'; + if (values.length <= 3) return values.join(' / '); + return `${values.length} values · ${values.slice(0, 4).join(', ')}, …`; +}; + +/** Names pulled from an `llm-judge` column's `scores` JSON array, if it parses. */ +const parseScoreNames = (scores: string | undefined): string[] => { + if (!scores?.trim()) return []; + try { + const parsed: unknown = JSON.parse(scores); + if (!Array.isArray(parsed)) return []; + return parsed + .map((score) => + score && typeof score === 'object' ? (score as { name?: unknown }).name : undefined + ) + .filter((name): name is string => typeof name === 'string' && name.length > 0); + } catch { + return []; + } +}; + +/** + * Produces the badge label and summary line shown for a column in the schema list. The + * summary is intentionally terse — relationships (referenced columns) are surfaced + * separately as tags on the row. + */ +export const describeColumn = (column: BuilderColumn): ColumnDescription => { + const { option, values } = column; + + const detail = ((): string | null => { + switch (option.columnType) { + case 'sampler': + if (option.samplerType === SamplerType.category) { + return summarizeValues(splitList(values.values)); + } + return option.description; + case 'llm-text': + return 'generator'; + case 'llm-code': + return values.code_lang ? `code generator · ${values.code_lang}` : 'code generator'; + case 'llm-structured': + return 'structured generator'; + case 'image': + return 'image generator'; + case 'llm-judge': { + const names = parseScoreNames(values.scores); + if (names.length === 0) return 'judge'; + return `judge · ${names.length} ${names.length === 1 ? 'score' : 'scores'}: ${names.join(', ')}`; + } + case 'embedding': + return values.target_column ? `embedding of ${values.target_column}` : 'embedding'; + case 'expression': + return 'expression · no LLM'; + case 'validation': + return values.validator_type ? `validation · ${values.validator_type}` : 'validation'; + case 'seed-dataset': { + const fileset = values[SEED_FILESET_REF_KEY]?.trim(); + const columns = getSeedAvailableColumns(column); + if (fileset) + return `seed · ${fileset}${columns.length ? ` · ${columns.length} columns` : ''}`; + return 'seed dataset'; + } + case 'custom': + return 'custom function'; + default: + return option.description; + } + })(); + + return { typeLabel: option.label, detail }; +}; diff --git a/web/packages/studio/src/routes/DataDesignerJobBuildRoute/index.tsx b/web/packages/studio/src/routes/DataDesignerJobBuildRoute/index.tsx index f1d7a76bfa..1ccc00bd3b 100644 --- a/web/packages/studio/src/routes/DataDesignerJobBuildRoute/index.tsx +++ b/web/packages/studio/src/routes/DataDesignerJobBuildRoute/index.tsx @@ -17,7 +17,10 @@ import { BuilderCanvas } from '@studio/routes/DataDesignerJobBuildRoute/BuilderC import { BuilderConfigPane } from '@studio/routes/DataDesignerJobBuildRoute/BuilderConfigPane'; import { BuilderDetailsPanel } from '@studio/routes/DataDesignerJobBuildRoute/BuilderDetailsPanel'; import { BuilderPalette } from '@studio/routes/DataDesignerJobBuildRoute/BuilderPalette'; -import { BuilderToolbar } from '@studio/routes/DataDesignerJobBuildRoute/BuilderToolbar'; +import { + BuilderToolbar, + type BuilderViewMode, +} from '@studio/routes/DataDesignerJobBuildRoute/BuilderToolbar'; import { buildDataDesignerConfig, validateColumns, @@ -26,6 +29,7 @@ import { buildServedModelNames, validateModels, } from '@studio/routes/DataDesignerJobBuildRoute/models'; +import { SchemaList } from '@studio/routes/DataDesignerJobBuildRoute/SchemaList'; import { useJobBuilder } from '@studio/routes/DataDesignerJobBuildRoute/useJobBuilder'; import { getDataDesignerJobDetailsRoute, @@ -89,6 +93,7 @@ export const DataDesignerJobBuildRoute: FC = () => { [providersPage?.data] ); + const [viewMode, setViewMode] = useState('list'); const [validationErrors, setValidationErrors] = useState([]); const [isDetailsOpen, setIsDetailsOpen] = useState(false); @@ -161,6 +166,8 @@ export const DataDesignerJobBuildRoute: FC = () => { { />
- + {viewMode === 'list' ? ( + + ) : ( + + )}
Date: Thu, 23 Jul 2026 11:17:14 -0700 Subject: [PATCH 2/2] PR feedback Signed-off-by: Sean Teramae --- .../DataDesignerJobBuildRoute/SchemaRow.tsx | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/web/packages/studio/src/routes/DataDesignerJobBuildRoute/SchemaRow.tsx b/web/packages/studio/src/routes/DataDesignerJobBuildRoute/SchemaRow.tsx index aca39a7285..91b9ba32b6 100644 --- a/web/packages/studio/src/routes/DataDesignerJobBuildRoute/SchemaRow.tsx +++ b/web/packages/studio/src/routes/DataDesignerJobBuildRoute/SchemaRow.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { Badge, Flex, Stack, Tag, Text } from '@nvidia/foundations-react-core'; +import { Badge, Button, Flex, Stack, Tag, Text } from '@nvidia/foundations-react-core'; import { CardIconBadge } from '@studio/components/common/SelectableCard'; import type { BuilderColumn } from '@studio/routes/DataDesignerJobBuildRoute/columns'; import { describeColumn } from '@studio/routes/DataDesignerJobBuildRoute/describeColumn'; @@ -88,14 +88,18 @@ export const SchemaRow: FC = ({
- + + + ); };