diff --git a/web/packages/common/src/components/DataView/internal/StatusResult.tsx b/web/packages/common/src/components/DataView/internal/StatusResult.tsx index 00bad84d2c..54551f9870 100644 --- a/web/packages/common/src/components/DataView/internal/StatusResult.tsx +++ b/web/packages/common/src/components/DataView/internal/StatusResult.tsx @@ -76,7 +76,7 @@ export function StatusResult({ kind="secondary" size="small" > - Clear filters + Clear Filters ) : undefined; return renderEmptyState ? ( diff --git a/web/packages/common/src/components/EntityEmptyState/EntityEmptyState.test.tsx b/web/packages/common/src/components/EntityEmptyState/EntityEmptyState.test.tsx index 118057d4ef..6dbd9361a9 100644 --- a/web/packages/common/src/components/EntityEmptyState/EntityEmptyState.test.tsx +++ b/web/packages/common/src/components/EntityEmptyState/EntityEmptyState.test.tsx @@ -38,18 +38,18 @@ describe('EntityEmptyState', () => { expect(screen.getByText(descriptor.subheading)).toBeInTheDocument(); }); - it('toggles between the NeMo CLI command and the agent prompt', async () => { + it('toggles between the agent prompt and the NeMo CLI command', async () => { const user = userEvent.setup(); wrap(); const help = screen.getByTestId('entity-empty-state-help'); - // CLI is the default selection. - expect(help).toHaveTextContent(descriptor.cliCommand as string); - expect(help).not.toHaveTextContent(descriptor.skillPrompt as string); - - await user.click(screen.getByRole('radio', { name: 'Ask an agent' })); + // Ask an agent is the default selection. expect(help).toHaveTextContent(descriptor.skillPrompt as string); expect(help).not.toHaveTextContent(descriptor.cliCommand as string); + + await user.click(screen.getByRole('radio', { name: 'CLI' })); + expect(help).toHaveTextContent(descriptor.cliCommand as string); + expect(help).not.toHaveTextContent(descriptor.skillPrompt as string); }); it('invokes onCreate from the primary CTA', async () => { @@ -67,6 +67,28 @@ describe('EntityEmptyState', () => { screen.queryByRole('button', { name: descriptor.createAction?.label }) ).not.toBeInTheDocument(); }); + + it('resets the CLI/agent selection when the descriptor changes on rerender', () => { + const membersDescriptor = ENTITY_EMPTY_STATES.members; + const { rerender } = wrap(); + + const help = screen.getByTestId('entity-empty-state-help'); + // `members` has no skillPrompt, so its only (and default) option is CLI. + expect(help).toHaveTextContent(membersDescriptor.cliCommand as string); + + rerender( + + + + + + ); + + // `guardrails` has a skillPrompt, so `kind` must reset to the agent default instead of + // sticking with the previous descriptor's CLI-only selection. + expect(help).toHaveTextContent(descriptor.skillPrompt as string); + expect(help).not.toHaveTextContent(descriptor.cliCommand as string); + }); }); describe('no-results', () => { @@ -79,7 +101,7 @@ describe('EntityEmptyState', () => { screen.queryByRole('button', { name: descriptor.createAction?.label }) ).not.toBeInTheDocument(); - await userEvent.click(screen.getByRole('button', { name: 'Clear filters' })); + await userEvent.click(screen.getByRole('button', { name: 'Clear Filters' })); expect(onClearFilters).toHaveBeenCalledTimes(1); }); }); diff --git a/web/packages/common/src/components/EntityEmptyState/index.tsx b/web/packages/common/src/components/EntityEmptyState/index.tsx index 31158018d1..3fe3b9a06a 100644 --- a/web/packages/common/src/components/EntityEmptyState/index.tsx +++ b/web/packages/common/src/components/EntityEmptyState/index.tsx @@ -69,7 +69,7 @@ export const EntityEmptyState: FC = ({ slotFooter={ onClearFilters ? ( ) : null } @@ -135,7 +135,18 @@ const SelfServiceHelp: FC<{ cliCommand?: string; skillPrompt?: string }> = ({ skillPrompt, }) => { const toast = useToast(); - const [kind, setKind] = useState(cliCommand ? 'cli' : 'agent'); + const defaultKind: HelpKind = skillPrompt ? 'agent' : 'cli'; + + // `kind` should track the current descriptor's default unless the user has picked a value for + // it. Resetting during render (rather than in an effect) when the descriptor changes avoids a + // stale selection painting for a frame, while leaving a same-descriptor rerender's user choice + // untouched. + const [prevDescriptor, setPrevDescriptor] = useState({ cliCommand, skillPrompt }); + const [kind, setKind] = useState(defaultKind); + if (prevDescriptor.cliCommand !== cliCommand || prevDescriptor.skillPrompt !== skillPrompt) { + setPrevDescriptor({ cliCommand, skillPrompt }); + setKind(defaultKind); + } const items: { value: HelpKind; children: React.ReactNode }[] = []; if (skillPrompt) diff --git a/web/packages/common/src/components/EntityEmptyState/registry.ts b/web/packages/common/src/components/EntityEmptyState/registry.ts index e7f35c7fce..a0405b05f7 100644 --- a/web/packages/common/src/components/EntityEmptyState/registry.ts +++ b/web/packages/common/src/components/EntityEmptyState/registry.ts @@ -1,7 +1,24 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ShieldCheck, type LucideIcon } from 'lucide-react'; +import { + Anchor, + BrainCircuit, + ChartNetwork, + Database, + FlaskConical, + FolderOpen, + HatGlasses, + Lightbulb, + ListChecks, + LockKeyhole, + Radar, + Rocket, + ShieldCheck, + UsersRound, + VenetianMask, + type LucideIcon, +} from 'lucide-react'; /** * A create call-to-action for a first-use empty state. @@ -37,7 +54,34 @@ export interface EmptyStateDescriptor { } /** Keys of entities that have a standardized empty state. */ -export type EntityKey = 'guardrails'; +export type EntityKey = + | 'guardrails' + | 'guardrailChecks' + | 'filesets' + | 'filesetFiles' + | 'customModels' + | 'baseModels' + | 'deployments' + | 'inferenceProviders' + | 'virtualModels' + | 'secrets' + | 'members' + | 'jobs' + | 'anonymizerJobs' + | 'dataDesignerJobs' + | 'safeSynthesizerJobs' + | 'agentEvaluations' + | 'evaluationResults' + | 'evaluationSessions' + | 'experiments' + | 'evalComparison' + | 'optimizerInsights' + | 'insightExperiments' + | 'insightTraces' + | 'telemetryTraces' + | 'telemetrySpans' + | 'agentMonitorRuns' + | 'agents'; /** * Canonical empty-state registry. Grows one entry at a time as entities migrate @@ -50,8 +94,205 @@ export const ENTITY_EMPTY_STATES: Record = { subheading: 'Guardrail configs add content-safety, jailbreak, and PII rails to the models in this workspace.', // Create is a modal owned by the route, so the callsite supplies `onCreate`. - createAction: { label: 'Create guardrail config' }, + createAction: { label: 'Create Guardrail Config' }, cliCommand: 'nemo guardrail configs create ', skillPrompt: 'Help me create my first guardrail config with the nemo-guardrails skill', }, + guardrailChecks: { + icon: ListChecks, + heading: 'No tests yet', + subheading: 'Add a test case on the Tests tab, then run it to see its result here.', + cliCommand: + 'nemo guardrail check --config --messages \'[{"role":"user","content":""}]\'', + skillPrompt: 'Help me verify my guardrail config with the nemo-guardrails skill', + }, + filesets: { + icon: Database, + heading: 'No filesets yet', + subheading: + 'Filesets group the files your agents and jobs read from — training data, models, or other artifacts.', + createAction: { label: 'Create Fileset' }, + cliCommand: 'nemo files filesets create --workspace ', + skillPrompt: 'Help me create my first fileset with the nemo-files skill', + }, + filesetFiles: { + icon: FolderOpen, + heading: 'No files yet', + subheading: 'Upload files to this fileset to make them available to agents and jobs.', + createAction: { label: 'Upload Files' }, + cliCommand: 'nemo files upload --fileset --workspace ', + skillPrompt: 'Help me upload files to a fileset with the nemo-files skill', + }, + customModels: { + icon: BrainCircuit, + heading: 'No custom models yet', + subheading: 'Customize a model with fine-tuning or prompt tuning to meet your specific needs.', + createAction: { label: 'Customize Model' }, + cliCommand: 'nemo customization automodel submit .json --workspace ', + skillPrompt: 'Help me create my first custom model with the nemo-customizer skill', + }, + baseModels: { + icon: BrainCircuit, + heading: 'No base models available', + subheading: + 'Registered inference providers in this workspace automatically surface their base models here.', + }, + deployments: { + icon: Rocket, + heading: 'No deployments yet', + subheading: 'Deploy a custom model to serve it for inference.', + createAction: { label: 'Create Deployment' }, + cliCommand: 'nemo inference deployments create --input-file .json', + skillPrompt: 'Help me deploy my first model with the nemo-build-agent skill', + }, + inferenceProviders: { + icon: Radar, + heading: 'No inference providers yet', + subheading: + 'Register an inference provider to make its models available for chat and evaluation.', + createAction: { label: 'Add Inference Provider' }, + cliCommand: + 'nemo inference providers create --workspace --host-url "" --api-key-secret-name ""', + skillPrompt: 'Help me create my first inference provider with the nemo-inference skill', + }, + virtualModels: { + icon: Radar, + heading: 'No virtual models yet', + subheading: + 'Virtual models route inference traffic across one or more providers, with optional switchyard and guardrail middleware.', + createAction: { label: 'Create Virtual Model' }, + cliCommand: + 'nemo inference virtual-models create --workspace --models \'[{"model":"/","backend_format":"OPENAI_CHAT"}]\'', + skillPrompt: 'Help me create my first virtual model with the nemo-inference skill', + }, + secrets: { + icon: LockKeyhole, + heading: 'No secrets yet', + subheading: + 'Store API keys and credentials as secrets so providers and jobs can reference them securely.', + createAction: { label: 'Create Secret' }, + cliCommand: + 'nemo secrets create --value "" --workspace ', + skillPrompt: 'Help me create my first secret with the nemo-secrets skill', + }, + members: { + icon: UsersRound, + heading: 'No members yet', + subheading: + 'Add a member to grant Viewer, Editor, or Admin access beyond the implicit workspace owners.', + createAction: { label: 'Add Member' }, + cliCommand: + 'nemo workspaces members create --workspace --principal --roles ', + }, + jobs: { + icon: FlaskConical, + heading: 'No jobs yet', + subheading: + 'Jobs from customization, evaluation, anonymization, and data generation appear here once submitted.', + }, + anonymizerJobs: { + icon: VenetianMask, + heading: 'No anonymizer jobs yet', + subheading: + 'Detect and protect PII in your datasets through context-aware replacement and rewriting.', + createAction: { label: 'Anonymize Data' }, + cliCommand: 'nemo anonymizer run submit --spec-file .yaml --workspace ', + skillPrompt: 'Help me create my first anonymizer job with the nemo-anonymizer skill', + }, + dataDesignerJobs: { + icon: Lightbulb, + heading: 'No Data Designer jobs yet', + subheading: 'Create and manage Data Designer jobs to generate or transform synthetic datasets.', + createAction: { label: 'New Job' }, + cliCommand: 'nemo data-designer create run .yaml --num-records ', + skillPrompt: + 'Help me create my first synthetic dataset with the nemo-data-designer-plugin skill', + }, + safeSynthesizerJobs: { + icon: ShieldCheck, + heading: 'No Safe Synthesizer jobs yet', + subheading: 'Generate a private version of a sensitive tabular dataset.', + createAction: { label: 'Synthesize Data' }, + cliCommand: + 'nemo safe-synthesizer run-local --workspace --spec-file .json --data-source --output-dir ', + skillPrompt: + 'Help me create my first Safe Synthesizer run with the nemo-safe-synthesizer skill', + }, + agentEvaluations: { + icon: FlaskConical, + heading: 'No evaluation jobs yet', + subheading: + 'Apply a model_optimization suggestion or submit an evaluate-agent job to see results here.', + cliCommand: + 'nemo evaluator agent-evaluate submit --spec-file .json --workspace ', + skillPrompt: + 'Help me create my first agent evaluation with the nemo-nemo-evaluator-plugin skill', + }, + evaluationResults: { + icon: FlaskConical, + heading: 'No evaluations yet', + subheading: 'Submit an evaluation job to score a model or agent against a benchmark.', + createAction: { label: 'Create Evaluation' }, + cliCommand: 'nemo evaluator evaluate submit --spec-file .json --workspace ', + skillPrompt: 'Help me create my first evaluation with the nemo-nemo-evaluator-plugin skill', + }, + evaluationSessions: { + icon: FlaskConical, + heading: 'No test cases', + subheading: 'Run an experiment to see test case results here.', + }, + experiments: { + icon: FlaskConical, + heading: 'No experiments yet', + subheading: 'Log an experiment to compare evaluation runs across models and configurations.', + createAction: { label: 'Create Experiment' }, + cliCommand: 'nemo experiments create --input-file .json', + skillPrompt: 'Help me log my first experiment with the nemo-experiments-upload skill', + }, + evalComparison: { + icon: ChartNetwork, + heading: 'No evaluations selected', + subheading: 'Select evaluations to compare their results side by side.', + }, + optimizerInsights: { + icon: Lightbulb, + heading: 'No insights yet', + subheading: 'Run an optimizer analysis on an agent to surface insights here.', + cliCommand: 'nemo agents optimize-skills run --spec-file .yml', + skillPrompt: 'Help me run my first optimizer analysis with the nemo-skills-optimization skill', + }, + insightExperiments: { + icon: FlaskConical, + heading: 'No experiments yet', + subheading: 'This insight has no linked experiments yet.', + createAction: { label: 'Run Experiment' }, + }, + insightTraces: { + icon: Anchor, + heading: 'No traces yet', + subheading: 'This insight has no linked traces yet.', + }, + telemetryTraces: { + icon: Anchor, + heading: 'No traces yet', + subheading: 'Trace summaries will appear here after spans are ingested.', + }, + telemetrySpans: { + icon: Anchor, + heading: 'No spans yet', + subheading: 'Spans will appear here once your agent starts sending telemetry.', + }, + agentMonitorRuns: { + icon: HatGlasses, + heading: 'No runs yet', + subheading: + 'Agent invocations populate this list once telemetry reaches the nemo-agent-telemetry fileset.', + }, + agents: { + icon: HatGlasses, + heading: 'No agents yet', + subheading: 'Build and deploy an agent to see it listed here.', + cliCommand: 'nemo agents create --name --agent-config ', + skillPrompt: 'Help me create my first agent with the nemo-build-agent skill', + }, }; diff --git a/web/packages/common/src/components/ErrorPanel/index.tsx b/web/packages/common/src/components/ErrorPanel/index.tsx index 2691b78ed7..4e40a7321a 100644 --- a/web/packages/common/src/components/ErrorPanel/index.tsx +++ b/web/packages/common/src/components/ErrorPanel/index.tsx @@ -21,6 +21,8 @@ export interface ErrorPanelProps { attributes?: { ErrorMessage?: ComponentProps; }; + /** The underlying error, when known (e.g. resolved by `RouteErrorPanel` from `useRouteError`). */ + error?: unknown; } /** @@ -43,38 +45,20 @@ const getErrorCode = (error: unknown): string | undefined => { }; /** - * Generic error panel component for React Router's errorElement. + * Generic, hook-free error panel. Renders from the `error`/`errorMessage` props it is given — + * it never reads a data-router error itself, so it is safe to render inline (e.g. a DataView's + * `renderErrorState`) as well as anywhere else outside a route `errorElement`. * - * Use this as the errorElement in your route configuration to display - * error UI with a custom title and error message attributes. + * Use {@link RouteErrorPanel} instead when rendering as a route's `errorElement`. * * @example * ```tsx - * // In your route configuration: - * { - * path: ROUTES.workspace.evaluation, - * element: , - * errorElement: , - * } - * - * // With custom error message props: - * { - * path: ROUTES.workspace.filesets, - * element: , - * errorElement: ( - * , - * slotFooter: , - * }} - * /> - * ), - * } + * renderErrorState: () => ( + * + * ) * ``` */ -export const ErrorPanel: FC = ({ title, errorMessage, attributes }) => { - const error = useRouteError(); +export const ErrorPanel: FC = ({ title, errorMessage, attributes, error }) => { const errorCode = getErrorCode(error); const errorMessageInternal = errorMessage ?? getRouterErrorMessage(error) ?? DEFAULT_ERROR_MESSAGE; @@ -116,3 +100,41 @@ export const ErrorPanel: FC = ({ title, errorMessage, attribute ); }; + +/** + * Route-only wrapper around {@link ErrorPanel}. Resolves the current route error via + * `useRouteError()` — which is only valid inside a data router's `errorElement` tree — and hands + * it down to the hook-free `ErrorPanel`. + * + * Use this as the `errorElement` in your route configuration to display error UI with a custom + * title and error message attributes. + * + * @example + * ```tsx + * // In your route configuration: + * { + * path: ROUTES.workspace.evaluation, + * element: , + * errorElement: , + * } + * + * // With custom error message props: + * { + * path: ROUTES.workspace.filesets, + * element: , + * errorElement: ( + * , + * slotFooter: , + * }} + * /> + * ), + * } + * ``` + */ +export const RouteErrorPanel: FC> = (props) => { + const error = useRouteError(); + return ; +}; diff --git a/web/packages/studio/src/components/CustomizationFilesetDetailsPanel/index.tsx b/web/packages/studio/src/components/CustomizationFilesetDetailsPanel/index.tsx index 3adc68eeed..56f8d6964e 100644 --- a/web/packages/studio/src/components/CustomizationFilesetDetailsPanel/index.tsx +++ b/web/packages/studio/src/components/CustomizationFilesetDetailsPanel/index.tsx @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import * as DataView from '@nemo/common/src/components/DataView/internal'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { KVPair } from '@nemo/common/src/components/KVPair'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { formatFileSize } from '@nemo/common/src/components/UploadModal/utils'; import { useToast } from '@nemo/common/src/providers/toast/useToast'; import { useFilesRetrieveFileset as useGetDataset } from '@nemo/sdk/generated/platform/api'; @@ -133,11 +133,7 @@ export const CustomizationFilesetDetailsPanel = ({ filesetUri }: Props) => { ( - + )} /> diff --git a/web/packages/studio/src/components/DatasetFileManagementSidePanel/index.test.tsx b/web/packages/studio/src/components/DatasetFileManagementSidePanel/index.test.tsx index 03dedfcd9a..e1591c78e9 100644 --- a/web/packages/studio/src/components/DatasetFileManagementSidePanel/index.test.tsx +++ b/web/packages/studio/src/components/DatasetFileManagementSidePanel/index.test.tsx @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { ENTITY_EMPTY_STATES } from '@nemo/common/src/components/EntityEmptyState/registry'; import { DatasetFileManagementSidePanel } from '@studio/components/DatasetFileManagementSidePanel'; import { GITKEEP_FILENAME } from '@studio/components/FilesTable/utils'; import { render } from '@studio/tests/util/render'; @@ -111,7 +112,7 @@ describe('DatasetFileManagementSidePanel', () => { ], }); - expect(await screen.findByText('No Files')).toBeInTheDocument(); + expect(await screen.findByText(ENTITY_EMPTY_STATES.filesetFiles.heading)).toBeInTheDocument(); }); it('shows subfolder breadcrumb segments when navigating into a folder', async () => { diff --git a/web/packages/studio/src/components/DatasetsTable/index.test.tsx b/web/packages/studio/src/components/DatasetsTable/index.test.tsx index 17edaede54..56a30ec7f4 100644 --- a/web/packages/studio/src/components/DatasetsTable/index.test.tsx +++ b/web/packages/studio/src/components/DatasetsTable/index.test.tsx @@ -1,7 +1,8 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import type { FilesetOutput } from '@nemo/sdk/generated/platform/schema'; +import { ENTITY_EMPTY_STATES } from '@nemo/common/src/components/EntityEmptyState/registry'; +import { FilesetPurpose, type FilesetOutput } from '@nemo/sdk/generated/platform/schema'; import { DatasetCreateModalMode } from '@studio/components/DatasetCreateModal/constants'; import { DatasetsTable } from '@studio/components/DatasetsTable'; import { PLATFORM_BASE_URL } from '@studio/constants/environment'; @@ -84,6 +85,21 @@ vi.mock('@studio/components/DatasetCreateModal', () => ({ ), })); +vi.mock('@studio/components/FilesetCreateModal', () => ({ + FilesetCreateModal: vi.fn(({ open, purpose }: { open?: boolean; purpose?: string }) => + open ? ( +
+ {purpose} +
+ ) : null + ), +})); + +vi.mock('@studio/constants/environment', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, FILESET_DETAILS_ENABLED: true }; +}); + const FILESETS_URL = `${PLATFORM_BASE_URL}/apis/files/v2/workspaces/:workspace/filesets`; const makeDataset = (overrides: Partial & { name: string }): FilesetOutput => @@ -449,25 +465,33 @@ describe('DatasetsTable', () => { }); }); - it('shows the "Manage Filesets" empty state with no filters active', async () => { + it('shows the first-use empty state with no filters active and opens the create modal', async () => { installListHandler([]); renderTable(); expect( - await screen.findByText('Manage Filesets', undefined, { timeout: LG_SELECTOR_TIMEOUT }) - ).toBeInTheDocument(); - expect( - screen.getByText( - 'Create a fileset to upload training data, models, or other files. Choose a purpose — Generic, Dataset, or Model — to control which metadata is available.' - ) + await screen.findByText(ENTITY_EMPTY_STATES.filesets.heading, undefined, { + timeout: LG_SELECTOR_TIMEOUT, + }) ).toBeInTheDocument(); + expect(screen.getByText(ENTITY_EMPTY_STATES.filesets.subheading)).toBeInTheDocument(); + + const createButton = screen.getByRole('button', { + name: ENTITY_EMPTY_STATES.filesets.createAction?.label, + }); + await user.click(createButton); + + const modal = await screen.findByTestId('fileset-create-modal'); + expect(within(modal).getByTestId('fileset-modal-purpose')).toHaveTextContent( + FilesetPurpose.dataset + ); }); - it('shows "No Results Found" and a Clear Filters button when search is active', async () => { + it('shows the no-results empty state with a Clear filters button when search is active', async () => { installListHandler([]); renderTable({ enableFilters: true }); - // Type into the search bar to activate hasSearchOrFilters + // Type into the search bar to activate hasSearchApplied const searchInput = await screen.findByPlaceholderText(/search/i, undefined, { timeout: LG_SELECTOR_TIMEOUT, }); @@ -475,10 +499,12 @@ describe('DatasetsTable', () => { await user.paste('no-match'); expect( - await screen.findByText('No Results Found', undefined, { timeout: LG_SELECTOR_TIMEOUT }) + await screen.findByText('No results found', undefined, { timeout: LG_SELECTOR_TIMEOUT }) + ).toBeInTheDocument(); + expect( + screen.getByText('No items match your current search or filters.') ).toBeInTheDocument(); - expect(screen.getByText('No filesets match your filters')).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /Clear Filters/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Clear filters/i })).toBeInTheDocument(); }); }); diff --git a/web/packages/studio/src/components/DatasetsTable/index.tsx b/web/packages/studio/src/components/DatasetsTable/index.tsx index c0c84c065d..bf9ef5dea7 100644 --- a/web/packages/studio/src/components/DatasetsTable/index.tsx +++ b/web/packages/studio/src/components/DatasetsTable/index.tsx @@ -3,26 +3,25 @@ import { StudioDataView } from '@nemo/common/src/components/DataView/StudioDataView'; import { DeleteConfirmationModal } from '@nemo/common/src/components/DeleteConfirmationModal'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { ErrorMessage } from '@nemo/common/src/components/ErrorMessage'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { getEntityReference } from '@nemo/common/src/namedEntity'; +import { FilesetPurpose } from '@nemo/sdk/generated/platform/schema'; import { Button } from '@nvidia/foundations-react-core'; import { DatasetCreateModal } from '@studio/components/DatasetCreateModal'; import { DatasetCreateModalMode } from '@studio/components/DatasetCreateModal/constants'; import { makeDatasetsTableColumns } from '@studio/components/DatasetsTable/columns'; import { type DatasetsTableProps } from '@studio/components/DatasetsTable/types'; import { useDatasetsTable } from '@studio/components/DatasetsTable/useDatasetsTable'; -import { DocumentationButton } from '@studio/components/DocumentationButton'; +import { FilesetCreateModal } from '@studio/components/FilesetCreateModal'; import { Loading } from '@studio/components/Layouts/Loading'; -import { NewDatasetButton } from '@studio/components/NewDatasetButton'; -import { NewModelFilesetButton } from '@studio/components/NewModelFilesetButton'; import { FILESET_DETAILS_ENABLED } from '@studio/constants/environment'; -import { LINK_DOCS_DATASETS } from '@studio/constants/links'; import { DatasetBulkDeleteModal } from '@studio/routes/FilesetListRoute/DatasetBulkDeleteModal'; import { getNewFilesetRoute } from '@studio/routes/utils'; -import { X, Database, Trash } from 'lucide-react'; +import { useBoolean } from '@studio/util/hooks/useBoolean'; +import { Trash } from 'lucide-react'; import { type FC } from 'react'; -import { Link } from 'react-router'; +import { useNavigate } from 'react-router'; export type { DatasetsTableProps } from '@studio/components/DatasetsTable/types'; @@ -45,7 +44,6 @@ export const DatasetsTable: FC = ({ const { workspace, dataViewState, - hasSearchOrFilters, modalDataset, setModalDataset, modalOpen, @@ -72,6 +70,16 @@ export const DatasetsTable: FC = ({ purposeFilter, }); + const navigate = useNavigate(); + const [createModalOpen, openCreateModal, closeCreateModal] = useBoolean(false); + const handleCreateFileset = () => { + if (FILESET_DETAILS_ENABLED) { + openCreateModal(); + } else { + navigate(getNewFilesetRoute(workspace)); + } + }; + // Column definitions const makeColumns = makeDatasetsTableColumns({ enableSelection, @@ -139,37 +147,18 @@ export const DatasetsTable: FC = ({ requestStatus: isFetching ? 'loading' : undefined, }, DataViewTableContent: { - renderEmptyState: () => - hasSearchOrFilters ? ( - - Clear Filters - - } + renderEmptyState: ({ hasFiltersApplied, hasSearchApplied }) => + hasFiltersApplied || hasSearchApplied ? ( + ) : ( - } - actions={ - <> - - {FILESET_DETAILS_ENABLED ? ( - <> - - - - ) : ( - - )} - - } + ), }, @@ -195,6 +184,15 @@ export const DatasetsTable: FC = ({ open={modalOpen === 'edit'} /> )} + + {createModalOpen && ( + + )} ); diff --git a/web/packages/studio/src/components/IntakeLists/IntakeSpansTable.tsx b/web/packages/studio/src/components/IntakeLists/IntakeSpansTable.tsx index baeb838da3..b5f37df881 100644 --- a/web/packages/studio/src/components/IntakeLists/IntakeSpansTable.tsx +++ b/web/packages/studio/src/components/IntakeLists/IntakeSpansTable.tsx @@ -4,9 +4,9 @@ import { getErrorMessage } from '@nemo/common/src/api/common/utils'; import { dateTimeFilter } from '@nemo/common/src/components/DataView/dateTimeFilter'; import * as DataView from '@nemo/common/src/components/DataView/internal'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { ErrorMessage } from '@nemo/common/src/components/ErrorMessage'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { getSortParamWithWhitelist } from '@nemo/common/src/utils/query'; import { useListSpans } from '@nemo/sdk/generated/platform/api'; @@ -16,7 +16,7 @@ import { type SpanFilter, type SpanSortField, } from '@nemo/sdk/generated/platform/schema'; -import { Anchor, Button, Text } from '@nvidia/foundations-react-core'; +import { Anchor, Text } from '@nvidia/foundations-react-core'; import { IntakeTelemetryStatusBadge } from '@studio/components/IntakeDetail/IntakeComponents/IntakeTelemetryStatusBadge'; import { isDefaultStartedAtFilter, @@ -39,7 +39,7 @@ import { type SpanTableRow, } from '@studio/util/intakeTelemetry'; import { keepPreviousData } from '@tanstack/react-query'; -import { type ComponentProps, type FC, type ReactNode, useMemo, useState } from 'react'; +import { type ComponentProps, type FC, useMemo, useState } from 'react'; import { Link, useNavigate } from 'react-router'; const SPAN_STATUS_FILTER_OPTIONS = [ @@ -112,10 +112,6 @@ export interface IntakeSpansTableProps { defaultPageSize?: number; showTraceColumn?: boolean; showHierarchy?: boolean; - emptyHeader?: string; - emptyMessage?: string; - emptyStateActions?: ReactNode; - noResultsActions?: ReactNode; /** Override span row click. `null` disables interaction entirely (no cursor-pointer). */ onRowClick?: ((span: SpanTableRow) => void) | null; } @@ -145,10 +141,6 @@ const SeededIntakeSpansTable: FC< defaultPageSize, showTraceColumn = true, showHierarchy = false, - emptyHeader = 'No Spans', - emptyMessage = 'Spans will appear here after trace data is ingested.', - emptyStateActions, - noResultsActions, onRowClick, defaultStartedAtFilter, }) => { @@ -382,23 +374,13 @@ const SeededIntakeSpansTable: FC< DataViewTableContent: { renderEmptyState: () => hasActiveFilters ? ( - - Clear Filters - - ) - } + ) : ( - + ), }, }} diff --git a/web/packages/studio/src/components/IntakeLists/IntakeTracesTable.tsx b/web/packages/studio/src/components/IntakeLists/IntakeTracesTable.tsx index 6a47cdf979..913c4b30fd 100644 --- a/web/packages/studio/src/components/IntakeLists/IntakeTracesTable.tsx +++ b/web/packages/studio/src/components/IntakeLists/IntakeTracesTable.tsx @@ -3,13 +3,12 @@ import { getErrorMessage } from '@nemo/common/src/api/common/utils'; import { EditColumnsMenu } from '@nemo/common/src/components/DataView/internal'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { ErrorMessage } from '@nemo/common/src/components/ErrorMessage'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { getSortParamWithWhitelist } from '@nemo/common/src/utils/query'; import { useListTraces } from '@nemo/sdk/generated/platform/api'; import type { Trace, TraceFilter, TraceSortField } from '@nemo/sdk/generated/platform/schema'; -import { Button } from '@nvidia/foundations-react-core'; import { isDefaultStartedAtFilter, makeDefaultStartedAtFilter, @@ -22,14 +21,12 @@ import { useWorkspaceFromPathIfExists } from '@studio/hooks/useWorkspaceFromPath import { getIntakeSessionTraceRoute } from '@studio/routes/utils'; import { keepPreviousData } from '@tanstack/react-query'; import { Columns3 } from 'lucide-react'; -import { type FC, type ReactNode, useState } from 'react'; +import { type FC, useState } from 'react'; import { useNavigate } from 'react-router'; export interface IntakeTracesTableProps { workspace?: string; slotEndPortalTargetId?: string; - emptyStateActions?: ReactNode; - noResultsActions?: ReactNode; } export const IntakeTracesTable: FC = (props) => { @@ -45,13 +42,7 @@ export const IntakeTracesTable: FC = (props) => { const SeededIntakeTracesTable: FC< IntakeTracesTableProps & { defaultStartedAtFilter: StartedAtFilterEntry } -> = ({ - workspace: workspaceProp, - slotEndPortalTargetId, - emptyStateActions, - noResultsActions, - defaultStartedAtFilter, -}) => { +> = ({ workspace: workspaceProp, slotEndPortalTargetId, defaultStartedAtFilter }) => { const navigate = useNavigate(); const routeWorkspace = useWorkspaceFromPathIfExists(); const workspace = workspaceProp ?? routeWorkspace; @@ -134,23 +125,13 @@ const SeededIntakeTracesTable: FC< DataViewTableContent: { renderEmptyState: () => hasActiveFilters ? ( - - Clear Filters - - ) - } + ) : ( - + ), }, }} diff --git a/web/packages/studio/src/components/dataViews/AgentEvaluationsDataView/index.tsx b/web/packages/studio/src/components/dataViews/AgentEvaluationsDataView/index.tsx index 5b22e58f9c..21ee16ef76 100644 --- a/web/packages/studio/src/components/dataViews/AgentEvaluationsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/AgentEvaluationsDataView/index.tsx @@ -1,16 +1,21 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { getErrorMessage } from '@nemo/common/src/api/common/utils'; import { withOperators } from '@nemo/common/src/api/filterOperators'; import { ROW_ACTIONS_COLUMN_SIZE, ROW_SELECTION_COLUMN_SIZE, StudioDataView, } from '@nemo/common/src/components/DataView/StudioDataView'; +import { + EntityEmptyState, + type EntityEmptyStateProps, +} from '@nemo/common/src/components/EntityEmptyState'; +import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { QuickActionsMenuRoot } from '@nemo/common/src/components/QuickActionsMenu/QuickActionsMenuRoot'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; import { StatusBadge } from '@nemo/common/src/components/StatusBadge'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { JOB_POLLING_INTERVAL_MS } from '@nemo/common/src/constants'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { getSortParamWithWhitelist } from '@nemo/common/src/utils/query'; @@ -38,6 +43,11 @@ import { Link, useNavigate } from 'react-router'; type AgentEvalJobRow = AgentEvaluateJob & { id: string }; +/** Variant-specific `EntityEmptyState` props for the `agentEvaluations` entity, minus `entity`/`className`. */ +type AgentEvaluationsEmptyStateProps = + | Omit, 'entity' | 'className'> + | Omit, 'entity' | 'className'>; + const STATUS_OPTIONS_WITH_ALL = [{ value: '', label: 'All' }, ...STATUS_FILTER_OPTIONS]; const SORTABLE_FIELDS = Object.values(AgentEvaluateJobsSortField).filter((v) => !v.startsWith('-')); @@ -183,15 +193,10 @@ export const AgentEvaluationsDataView = () => { }), ]; - const hasActiveFilters = - !!dataViewState.debouncedSearchBar || dataViewState.debouncedColumnFilters.length > 0; - const isInitialEmpty = jobs.length === 0 && !isLoading && !error && !hasActiveFilters; - if (error) { return ( - ); } @@ -225,23 +230,13 @@ export const AgentEvaluationsDataView = () => { requestStatus: isLoading && !jobsData ? 'loading' : undefined, }, DataViewTableContent: { - renderEmptyState: () => - isInitialEmpty ? ( - - ) : ( - - Clear Filters - - } - /> - ), + renderEmptyState: ({ hasFiltersApplied, hasSearchApplied }) => { + const emptyStateProps: AgentEvaluationsEmptyStateProps = + hasFiltersApplied || hasSearchApplied + ? { variant: 'no-results', onClearFilters: dataViewState.resetFilters } + : { variant: 'first-use' }; + return ; + }, }, }} /> diff --git a/web/packages/studio/src/components/dataViews/AgentsDataView/index.test.tsx b/web/packages/studio/src/components/dataViews/AgentsDataView/index.test.tsx index 9c586bb7aa..3bb677cd25 100644 --- a/web/packages/studio/src/components/dataViews/AgentsDataView/index.test.tsx +++ b/web/packages/studio/src/components/dataViews/AgentsDataView/index.test.tsx @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { ENTITY_EMPTY_STATES } from '@nemo/common/src/components/EntityEmptyState/registry'; import { AgentsTable } from '@studio/components/dataViews/AgentsDataView'; import { PLATFORM_BASE_URL } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; @@ -109,9 +110,11 @@ describe('CombinedAgentsTable', () => { renderTable(); expect( - await screen.findByText('No Agents Found', undefined, { timeout: XL_SELECTOR_TIMEOUT }) + await screen.findByText(ENTITY_EMPTY_STATES.agents.heading, undefined, { + timeout: XL_SELECTOR_TIMEOUT, + }) ).toBeInTheDocument(); - expect(screen.getByText('No agents have been created yet.')).toBeInTheDocument(); + expect(screen.getByText(ENTITY_EMPTY_STATES.agents.subheading)).toBeInTheDocument(); }); }); diff --git a/web/packages/studio/src/components/dataViews/AgentsDataView/index.tsx b/web/packages/studio/src/components/dataViews/AgentsDataView/index.tsx index 6520c5f38d..237c8f175a 100644 --- a/web/packages/studio/src/components/dataViews/AgentsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/AgentsDataView/index.tsx @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { getErrorMessage } from '@nemo/common/src/api/common/utils'; import { Root as DataViewRoot } from '@nemo/common/src/components/DataView/internal'; import { ROW_ACTIONS_COLUMN_SIZE, @@ -8,9 +9,9 @@ import { StudioDataView, } from '@nemo/common/src/components/DataView/StudioDataView'; import { DeleteConfirmationModal } from '@nemo/common/src/components/DeleteConfirmationModal'; -import { ErrorMessage } from '@nemo/common/src/components/ErrorMessage'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; +import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { JOB_POLLING_INTERVAL_LONG } from '@nemo/common/src/constants'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { useToast } from '@nemo/common/src/providers/toast/useToast'; @@ -28,13 +29,11 @@ import type { Agent } from '@nemo/sdk/generated/agents/schema/Agent'; import type { AgentDeployment } from '@nemo/sdk/generated/agents/schema/AgentDeployment'; import { Button, Text } from '@nvidia/foundations-react-core'; import { getAgentModelNames } from '@studio/components/dataViews/AgentsDataView/utils'; -import { DocumentationButton } from '@studio/components/DocumentationButton'; import { MODEL_COMPARE_ENABLED } from '@studio/constants/environment'; -import { LINK_DOCS_STUDIO } from '@studio/constants/links'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { getModelCompareRoute } from '@studio/routes/utils'; import { keepPreviousData, useQueryClient } from '@tanstack/react-query'; -import { HatGlasses, Trash } from 'lucide-react'; +import { Trash } from 'lucide-react'; import { ComponentProps, FC, useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router'; @@ -317,10 +316,6 @@ export const AgentsTable: FC = ({ }), ]; - if (agentsError) { - return ; - } - return ( <> = ({ DataViewRoot: { data: tableData, totalCount, - requestStatus: agentsLoading && !agentsData ? 'loading' : undefined, + requestStatus: agentsError + ? 'error' + : agentsLoading && !agentsData + ? 'loading' + : undefined, }, DataViewTableContent: { - renderEmptyState: () => ( - } - actions={} + renderEmptyState: ({ hasFiltersApplied, hasSearchApplied }) => + hasFiltersApplied || hasSearchApplied ? ( + + ) : ( + + ), + renderErrorState: () => ( + ), }, diff --git a/web/packages/studio/src/components/dataViews/AnonymizerJobsDataView/index.tsx b/web/packages/studio/src/components/dataViews/AnonymizerJobsDataView/index.tsx index c1661fcd68..d9840a62d1 100644 --- a/web/packages/studio/src/components/dataViews/AnonymizerJobsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/AnonymizerJobsDataView/index.tsx @@ -7,9 +7,9 @@ import { ROW_SELECTION_COLUMN_SIZE, StudioDataView, } from '@nemo/common/src/components/DataView/StudioDataView'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; import { StatusBadge } from '@nemo/common/src/components/StatusBadge'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { JOB_POLLING_INTERVAL_MS } from '@nemo/common/src/constants'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { getSortParam } from '@nemo/common/src/utils/query'; @@ -30,9 +30,9 @@ import { STATUS_FILTER_OPTIONS } from '@studio/constants/platformJobs'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { getAnonymizerJobRoute, getNewAnonymizerRoute } from '@studio/routes/utils'; import { keepPreviousData, useQueryClient } from '@tanstack/react-query'; -import { Trash, VenetianMask } from 'lucide-react'; +import { Trash } from 'lucide-react'; import { type ComponentProps, type FC, useCallback, useMemo, useState } from 'react'; -import { Link, useNavigate } from 'react-router'; +import { useNavigate } from 'react-router'; type AnonymizerJobWithId = AnonymizerJob & { id: string }; @@ -220,25 +220,16 @@ export const AnonymizerJobsDataView: FC = () => { DataViewTableContent: { renderEmptyState: () => hasActiveFilters ? ( - - Clear Filters - - } + ) : ( - } - header="Anonymizer Jobs" - emptyMessage="Detect and protect PII in your datasets through context-aware replacement and rewriting." - actions={ - - } + navigate(getNewAnonymizerRoute(workspace))} /> ), }, diff --git a/web/packages/studio/src/components/dataViews/CustomModelsDataView/index.test.tsx b/web/packages/studio/src/components/dataViews/CustomModelsDataView/index.test.tsx index a3188258e5..27451e084e 100644 --- a/web/packages/studio/src/components/dataViews/CustomModelsDataView/index.test.tsx +++ b/web/packages/studio/src/components/dataViews/CustomModelsDataView/index.test.tsx @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { ENTITY_EMPTY_STATES } from '@nemo/common/src/components/EntityEmptyState/registry'; import { ModelEntitysPage } from '@nemo/sdk/generated/platform/schema'; import { CustomModelsDataView } from '@studio/components/dataViews/CustomModelsDataView'; import { PLATFORM_BASE_URL } from '@studio/constants/environment'; @@ -65,7 +66,7 @@ describe('CustomModelsDataView', () => { renderComponent(); - expect(await screen.findByText('Manage Custom Models')).toBeInTheDocument(); + expect(await screen.findByText(ENTITY_EMPTY_STATES.customModels.heading)).toBeInTheDocument(); }); it('renders the error state when there is an error fetching models', async () => { diff --git a/web/packages/studio/src/components/dataViews/CustomModelsDataView/index.tsx b/web/packages/studio/src/components/dataViews/CustomModelsDataView/index.tsx index 22147f7b2f..b1cd1bc029 100644 --- a/web/packages/studio/src/components/dataViews/CustomModelsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/CustomModelsDataView/index.tsx @@ -9,9 +9,9 @@ import { StudioDataView, } from '@nemo/common/src/components/DataView/StudioDataView'; import { DeleteConfirmationModal } from '@nemo/common/src/components/DeleteConfirmationModal'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { ErrorMessage } from '@nemo/common/src/components/ErrorMessage'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { getSortParam } from '@nemo/common/src/utils/query'; import { @@ -30,18 +30,17 @@ import type { } from '@nemo/sdk/generated/platform/schema'; import { Button, Text, Tooltip } from '@nvidia/foundations-react-core'; import { queryClient } from '@studio/api/queryClient'; +import { CustomizeModelModal } from '@studio/components/CustomizeModelModal'; import { FINETUNING_TYPE_FILTER_OPTIONS } from '@studio/components/dataViews/CustomModelsDataView/constants'; -import { CustomizeModelButton } from '@studio/components/dataViews/CustomModelsDataView/CustomizeModelButton'; import { DeploymentIndicator } from '@studio/components/dataViews/CustomModelsDataView/DeploymentIndicator'; import { KindTag } from '@studio/components/dataViews/CustomModelsDataView/KindTag'; -import { DocumentationButton } from '@studio/components/DocumentationButton'; import { BaseModelSearchFilterField } from '@studio/components/FilterFields'; import type { ModelPanelTab } from '@studio/components/sidePanels/ModelPanels/ModelPanel'; import { INTAKE_ENABLED } from '@studio/constants/environment'; -import { LINK_DOCS_STUDIO_CUSTOMIZATION } from '@studio/constants/links'; import { getIntakeTracesRoute } from '@studio/routes/utils'; +import { useBoolean } from '@studio/util/hooks/useBoolean'; import { keepPreviousData } from '@tanstack/react-query'; -import { BrainCircuit, X, Trash } from 'lucide-react'; +import { Trash } from 'lucide-react'; import { ComponentProps, FC, useCallback, useMemo, useState } from 'react'; import { useNavigate } from 'react-router'; @@ -133,6 +132,7 @@ export const CustomModelsDataView: FC = ({ const navigate = useNavigate(); const { mutateAsync: deleteModel } = useModelsDeleteModel(); const { mutateAsync: deleteAdapter } = useModelsDeleteModelAdapter(); + const [isCustomizeModalOpen, openCustomizeModal, closeCustomizeModal] = useBoolean(false); const dataViewState = useStudioDataViewState({ defaultSort: [{ id: 'created_at', desc: true }], @@ -442,28 +442,18 @@ export const CustomModelsDataView: FC = ({ ), renderEmptyState: () => hasActiveFilters ? ( - - Clear Filters - - } + ) : placeholderComponent ? ( <>{placeholderComponent} ) : ( - } - actions={ - <> - - - - } + ), }, @@ -482,6 +472,11 @@ export const CustomModelsDataView: FC = ({ simpleConfirm /> )} + ); }; diff --git a/web/packages/studio/src/components/dataViews/DataDesignerJobsDataView/index.tsx b/web/packages/studio/src/components/dataViews/DataDesignerJobsDataView/index.tsx index a5cdbfeeff..e47bb08fd4 100644 --- a/web/packages/studio/src/components/dataViews/DataDesignerJobsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/DataDesignerJobsDataView/index.tsx @@ -7,9 +7,9 @@ import { ROW_SELECTION_COLUMN_SIZE, StudioDataView, } from '@nemo/common/src/components/DataView/StudioDataView'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; import { StatusBadge } from '@nemo/common/src/components/StatusBadge'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { JOB_POLLING_INTERVAL_MS } from '@nemo/common/src/constants'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { getSortParam } from '@nemo/common/src/utils/query'; @@ -26,14 +26,13 @@ import type { import { Banner, Button, Text } from '@nvidia/foundations-react-core'; import { BulkDeleteModal } from '@studio/components/BulkDeleteModal'; import { DataDesignerJobActionsMenu } from '@studio/components/DataDesignerJobActionsMenu'; -import { DataDesignerIconFc } from '@studio/constants/constants'; import { STATUS_FILTER_OPTIONS } from '@studio/constants/platformJobs'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { getDataDesignerJobDetailsRoute, getNewDataDesignerJobRoute } from '@studio/routes/utils'; import { keepPreviousData, useQueryClient } from '@tanstack/react-query'; import { Trash } from 'lucide-react'; import { ComponentProps, FC, useCallback, useMemo, useState } from 'react'; -import { Link, useNavigate } from 'react-router'; +import { useNavigate } from 'react-router'; type DataDesignerJobWithId = DataDesignerJob & { id: string }; @@ -220,25 +219,16 @@ export const DataDesignerJobsDataView: FC = () => { DataViewTableContent: { renderEmptyState: () => hasActiveFilters ? ( - - Clear Filters - - } + ) : ( - } - header="Data Designer Jobs" - emptyMessage="Create and manage data designer jobs to generate or transform datasets." - actions={ - - } + navigate(getNewDataDesignerJobRoute(workspace))} /> ), }, diff --git a/web/packages/studio/src/components/dataViews/DeploymentsDataView/index.tsx b/web/packages/studio/src/components/dataViews/DeploymentsDataView/index.tsx index 04620f07da..a4a93a6e63 100644 --- a/web/packages/studio/src/components/dataViews/DeploymentsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/DeploymentsDataView/index.tsx @@ -13,10 +13,10 @@ import { getErrorMessage } from '@nemo/common/src/api/common/utils'; import { withOperators } from '@nemo/common/src/api/filterOperators'; import { StudioDataView } from '@nemo/common/src/components/DataView/StudioDataView'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; import { StatusBadge } from '@nemo/common/src/components/StatusBadge'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { useModelsListDeployments } from '@nemo/sdk/generated/platform/api'; import { @@ -24,15 +24,15 @@ import { ModelDeploymentFilter, ModelDeploymentStatus, } from '@nemo/sdk/generated/platform/schema'; -import { Button, Flex, Stack, Text } from '@nvidia/foundations-react-core'; -import { CUSTOMIZER_ENABLED } from '@studio/constants/environment'; +import { Stack, Text } from '@nvidia/foundations-react-core'; import { keepPreviousData } from '@tanstack/react-query'; -import { Rocket, Trash2 } from 'lucide-react'; +import { Trash2 } from 'lucide-react'; import { ComponentProps, FC, useCallback } from 'react'; export interface DeploymentsDataViewProps { workspace: string; - emptyStateActions?: React.ReactNode; + /** Opens the create-deployment flow from the first-use empty state. */ + readonly onCreate?: () => void; /** Opens the URL-driven deployment details panel (row click). */ onDeploymentRowClick: (deployment: ModelDeployment) => void; /** Opens the shared delete confirmation flow (row action menu). */ @@ -44,7 +44,7 @@ export interface DeploymentsDataViewProps { export const DeploymentsDataView: FC = ({ workspace, - emptyStateActions, + onCreate, onDeploymentRowClick, onRequestDeleteDeployment, attributes, @@ -53,10 +53,6 @@ export const DeploymentsDataView: FC = ({ defaultSort: [{ id: 'created_at', desc: true }], }); - const resetFilters = useCallback(() => { - dataViewState.resetFilters(); - }, [dataViewState]); - const sortState = dataViewState.sorting.state[0]; const sortParam = sortState ? `${sortState.desc ? '-' : ''}${sortState.id}` : '-created_at'; @@ -133,8 +129,6 @@ export const DeploymentsDataView: FC = ({ [onRequestDeleteDeployment] ); - const hasSearchOrFilters = !!dataViewState.debouncedSearchBar; - return ( = ({ requestStatus: error ? 'error' : isFetching ? 'loading' : undefined, }, DataViewTableContent: { - renderEmptyState: () => { - if (data?.data.length === 0 && !isFetching && !hasSearchOrFilters) { - return ( - } - header="Manage Deployments" - emptyMessage={ - CUSTOMIZER_ENABLED - ? 'Deploy an open source model from the base models or create a fine-tuned model.' - : 'Deploy an open source model from the base models.' - } - actions={{emptyStateActions}} - /> - ); - } - return ( - - Clear Search - - } + renderEmptyState: ({ hasFiltersApplied, hasSearchApplied }) => + hasFiltersApplied || hasSearchApplied ? ( + - ); - }, + ) : ( + + ), renderErrorState: () => ( = ({ attributes={{ DataViewRoot: { data: rows, totalCount: rows.length }, DataViewTableContent: { - renderEmptyState: () => ( - - ), + renderEmptyState: () => , }, }} /> diff --git a/web/packages/studio/src/components/dataViews/EvaluationResultsDataView/index.tsx b/web/packages/studio/src/components/dataViews/EvaluationResultsDataView/index.tsx index 7c1894b937..356699a62a 100644 --- a/web/packages/studio/src/components/dataViews/EvaluationResultsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/EvaluationResultsDataView/index.tsx @@ -1,12 +1,14 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { getErrorMessage } from '@nemo/common/src/api/common/utils'; import { withOperators } from '@nemo/common/src/api/filterOperators'; import { dateTimeFilter } from '@nemo/common/src/components/DataView/dateTimeFilter'; import { StudioDataView } from '@nemo/common/src/components/DataView/StudioDataView'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; +import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; import { StatusBadge } from '@nemo/common/src/components/StatusBadge'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { getSortParamWithWhitelist } from '@nemo/common/src/utils/query'; import { useEvaluatorListEvaluateJobs } from '@nemo/sdk/generated/evaluator/api'; @@ -15,14 +17,10 @@ import { type EvaluateJobsListFilter, EvaluateJobsSortField, } from '@nemo/sdk/generated/evaluator/schema'; -import { Button, Flex, StatusMessage } from '@nvidia/foundations-react-core'; -import { DocumentationButton } from '@studio/components/DocumentationButton'; -import { LINK_DOCS_STUDIO_EVALUATION } from '@studio/constants/links'; import { STATUS_FILTER_OPTIONS } from '@studio/constants/platformJobs'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { getEvaluationResultDetailsRoute } from '@studio/routes/utils'; import { keepPreviousData } from '@tanstack/react-query'; -import { ListChecks } from 'lucide-react'; import { ComponentProps } from 'react'; import { useNavigate } from 'react-router'; @@ -110,12 +108,7 @@ export const EvaluationResultsDataView = () => { const isInitialEmpty = jobs.length === 0 && !isFetching && !error && !hasActiveFilters; if (error) { - return ( - - ); + return ; } return ( @@ -139,33 +132,12 @@ export const EvaluationResultsDataView = () => { DataViewTableContent: { renderEmptyState: () => isInitialEmpty ? ( - - } - slotFooter={ - - - - } - /> - + ) : ( - - Clear Filters - - } + ), }, diff --git a/web/packages/studio/src/components/dataViews/EvaluationSessionsDataView/Empty.tsx b/web/packages/studio/src/components/dataViews/EvaluationSessionsDataView/Empty.tsx deleted file mode 100644 index 5ed32c9b14..0000000000 --- a/web/packages/studio/src/components/dataViews/EvaluationSessionsDataView/Empty.tsx +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; -import { - Button, - CodeSnippet, - TabsContent, - TabsList, - TabsRoot, - TabsTrigger, - Text, -} from '@nvidia/foundations-react-core'; -import { LINK_DOCS_EXPERIMENTS_CLI } from '@studio/constants/links'; -import { Bot, ChevronRight, File, FlaskConical, Terminal } from 'lucide-react'; - -interface EmptyProps { - experimentName: string; - datasetName: string; -} - -export const Empty = ({ experimentName, datasetName }: EmptyProps) => { - const cliCommand = - `nemo exp run \\\n` + - ` --group "${experimentName}" \\\n` + - ` --dataset "${datasetName}" \\\n` + - ` --evaluators correctness,helpfulness,groundedness,tool-error`; - - return ( - } - header="No test cases" - emptyMessage="Run an experiment to see test case results." - actions={ -
- - - - - NeMo Assistant - - - - CLI command - - - - -
- } - /> - ); -}; diff --git a/web/packages/studio/src/components/dataViews/EvaluationSessionsDataView/index.tsx b/web/packages/studio/src/components/dataViews/EvaluationSessionsDataView/index.tsx index 87eba33c3a..530ec729c3 100644 --- a/web/packages/studio/src/components/dataViews/EvaluationSessionsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/EvaluationSessionsDataView/index.tsx @@ -6,9 +6,9 @@ import { EditColumnsMenu, } from '@nemo/common/src/components/DataView/internal'; import { StudioDataView } from '@nemo/common/src/components/DataView/StudioDataView'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; import { StatusBadge } from '@nemo/common/src/components/StatusBadge'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { formatDurationMs } from '@nemo/common/src/utils/date'; import { formatEvaluatorScore, snakeCaseToTitleCase } from '@nemo/common/src/utils/formatters'; @@ -24,7 +24,6 @@ import type { ListEvaluationSessionsParams, } from '@nemo/sdk/generated/platform/schema'; import { Text, Tooltip } from '@nvidia/foundations-react-core'; -import { Empty } from '@studio/components/dataViews/EvaluationSessionsDataView/Empty'; import { IntakePayloadPreviewCell } from '@studio/components/IntakeLists/IntakePayloadPreviewCell'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { getEvaluationSessionTraceDetailRoute } from '@studio/routes/utils'; @@ -331,35 +330,16 @@ export const EvaluationSessionsDataView: FC = ( }, DataViewSearchBar: { placeholder: 'Search case...' }, DataViewTableContent: { - renderEmptyState: () => { - const hasActiveFilters = - !!dataViewState.searchBar.state || dataViewState.columnFiltering.state.length > 0; - if (hasActiveFilters) { - return ( - - Change your filters and try again, or{' '} - - . - - } - /> - ); - } - return ( - '} + renderEmptyState: ({ hasFiltersApplied, hasSearchApplied }) => + hasFiltersApplied || hasSearchApplied ? ( + - ); - }, + ) : ( + + ), }, }} /> diff --git a/web/packages/studio/src/components/dataViews/ExperimentDataView/Empty.tsx b/web/packages/studio/src/components/dataViews/ExperimentDataView/Empty.tsx deleted file mode 100644 index ce71e66c00..0000000000 --- a/web/packages/studio/src/components/dataViews/ExperimentDataView/Empty.tsx +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; -import { - Button, - CodeSnippet, - TabsContent, - TabsList, - TabsRoot, - TabsTrigger, - Text, -} from '@nvidia/foundations-react-core'; -import { LINK_DOCS_EXPERIMENTS_CLI } from '@studio/constants/links'; -import { Bot, ChevronRight, File, FlaskConical, Terminal } from 'lucide-react'; - -interface EmptyProps { - experimentName: string; -} - -export const Empty = ({ experimentName }: EmptyProps) => { - const escapedGroupName = experimentName.replace(/'/g, "'\\''"); - const cliCommand = - `nemo exp run \\\n` + - ` --group '${escapedGroupName}' \\\n` + - ` --dataset "" \\\n` + - ` --evaluators correctness,helpfulness,groundedness,tool-error`; - - return ( - } - header="No Evaluations" - emptyMessage="Run an evaluation to see results for this experiment." - actions={ -
- - - - - NeMo Assistant - - - - CLI command - - - - -
- } - /> - ); -}; diff --git a/web/packages/studio/src/components/dataViews/ExperimentDataView/index.tsx b/web/packages/studio/src/components/dataViews/ExperimentDataView/index.tsx index 6392616b7c..b60ebe0ddc 100644 --- a/web/packages/studio/src/components/dataViews/ExperimentDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/ExperimentDataView/index.tsx @@ -12,6 +12,7 @@ import { ROW_SELECTION_COLUMN_SIZE, StudioDataView, } from '@nemo/common/src/components/DataView/StudioDataView'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { ErrorMessage } from '@nemo/common/src/components/ErrorMessage'; import { QuickActionsMenuRoot } from '@nemo/common/src/components/QuickActionsMenu/QuickActionsMenuRoot'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; @@ -25,7 +26,6 @@ import { ChangesetBadge } from '@studio/components/ChangesetBadge'; import { ExperimentParetoChart } from '@studio/components/charts/ExperimentParetoChart'; import { AddToGroupModal } from '@studio/components/dataViews/ExperimentDataView/AddToGroupModal'; import '@studio/components/dataViews/ExperimentDataView/ExperimentDataView.css'; -import { Empty } from '@studio/components/dataViews/ExperimentDataView/Empty'; import { MeanValueTooltipCell } from '@studio/components/dataViews/ExperimentDataView/MeanValueTooltipCell'; import { type EvaluationRow, @@ -610,8 +610,14 @@ export const ExperimentDataView: FC = ({ group, paretoV DataViewTableContent: { enableColumnReordering: true, renderEmptyState: ({ hasFiltersApplied, hasSearchApplied }) => - hasFiltersApplied || hasSearchApplied ? null : ( - + hasFiltersApplied || hasSearchApplied ? ( + + ) : ( + ), }, }} diff --git a/web/packages/studio/src/components/dataViews/GuardrailChecksDataView/index.test.tsx b/web/packages/studio/src/components/dataViews/GuardrailChecksDataView/index.test.tsx index 4551a880d0..18901e4d86 100644 --- a/web/packages/studio/src/components/dataViews/GuardrailChecksDataView/index.test.tsx +++ b/web/packages/studio/src/components/dataViews/GuardrailChecksDataView/index.test.tsx @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { ENTITY_EMPTY_STATES } from '@nemo/common/src/components/EntityEmptyState/registry'; import { DEFAULT_PAGE_SIZE } from '@nemo/common/src/constants/pagination'; import { GUARDRAIL_CHECKS_ENTITY_TYPE, @@ -220,7 +221,9 @@ describe('GuardrailChecksDataView', () => { renderComponent([]); expect( - await screen.findByText('No tests yet', undefined, { timeout: XL_SELECTOR_TIMEOUT }) + await screen.findByText(ENTITY_EMPTY_STATES.guardrailChecks.heading, undefined, { + timeout: XL_SELECTOR_TIMEOUT, + }) ).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /Clear Filters/i })).not.toBeInTheDocument(); }); @@ -233,9 +236,9 @@ describe('GuardrailChecksDataView', () => { await user.type(screen.getByPlaceholderText('Search tests...'), 'no-such-test'); expect( - await screen.findByText('No Results Found', undefined, { timeout: XL_SELECTOR_TIMEOUT }) + await screen.findByText('No results found', undefined, { timeout: XL_SELECTOR_TIMEOUT }) ).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /Clear Filters/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Clear Filters' })).toBeInTheDocument(); }); describe('detail selection', () => { diff --git a/web/packages/studio/src/components/dataViews/GuardrailChecksDataView/index.tsx b/web/packages/studio/src/components/dataViews/GuardrailChecksDataView/index.tsx index 7d6c637468..7dbd764a5d 100644 --- a/web/packages/studio/src/components/dataViews/GuardrailChecksDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/GuardrailChecksDataView/index.tsx @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { StudioDataView } from '@nemo/common/src/components/DataView/StudioDataView'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { useDeferredUnmount } from '@nemo/common/src/hooks/useDeferredUnmount'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; -import { Button, Text } from '@nvidia/foundations-react-core'; +import { Text } from '@nvidia/foundations-react-core'; import type { GuardrailCheckEntity, Verdict } from '@studio/api/guardrail-checks/types'; import { getCheckInputText, @@ -18,7 +18,6 @@ import { RESULT_FILTER_OPTIONS, } from '@studio/components/dataViews/GuardrailChecksDataView/checkStatus'; import { ResultIndicator } from '@studio/components/dataViews/GuardrailChecksDataView/ResultIndicator'; -import { ListChecks } from 'lucide-react'; import { type ComponentProps, type FC, type ReactNode, useCallback, useMemo } from 'react'; /** Everything a detail view needs about the selected row. */ @@ -200,21 +199,13 @@ export const GuardrailChecksDataView: FC = ({ DataViewTableContent: { renderEmptyState: () => hasSearchOrFilters ? ( - - Clear Filters - - } + ) : ( - } - header="No tests yet" - emptyMessage="Add a test case on the Tests tab, then run it to see its result here." - /> + ), }, }} diff --git a/web/packages/studio/src/components/dataViews/GuardrailsDataView/GuardrailsDataView.test.tsx b/web/packages/studio/src/components/dataViews/GuardrailsDataView/GuardrailsDataView.test.tsx index 2581339968..608c3a19f9 100644 --- a/web/packages/studio/src/components/dataViews/GuardrailsDataView/GuardrailsDataView.test.tsx +++ b/web/packages/studio/src/components/dataViews/GuardrailsDataView/GuardrailsDataView.test.tsx @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { ENTITY_EMPTY_STATES } from '@nemo/common/src/components/EntityEmptyState/registry'; import type { GuardrailConfig } from '@nemo/sdk/generated/platform/schema'; import { GuardrailsDataView } from '@studio/components/dataViews/GuardrailsDataView'; import { PLATFORM_BASE_URL } from '@studio/constants/environment'; @@ -103,11 +104,13 @@ describe('GuardrailsDataView', () => { ); renderComponent({ onCreate }); expect( - await screen.findByText('No guardrail configs yet', undefined, { + await screen.findByText(ENTITY_EMPTY_STATES.guardrails.heading, undefined, { timeout: XL_SELECTOR_TIMEOUT, }) ).toBeInTheDocument(); - const createButton = screen.getByRole('button', { name: 'Create guardrail config' }); + const createButton = screen.getByRole('button', { + name: ENTITY_EMPTY_STATES.guardrails.createAction?.label, + }); expect(createButton).toBeInTheDocument(); await user.click(createButton); diff --git a/web/packages/studio/src/components/dataViews/InferenceProvidersDataView/index.tsx b/web/packages/studio/src/components/dataViews/InferenceProvidersDataView/index.tsx index 3bd52a3b05..9903f1b746 100644 --- a/web/packages/studio/src/components/dataViews/InferenceProvidersDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/InferenceProvidersDataView/index.tsx @@ -17,10 +17,10 @@ import { StudioDataView, } from '@nemo/common/src/components/DataView/StudioDataView'; import { DeleteConfirmationModal } from '@nemo/common/src/components/DeleteConfirmationModal'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; import { StatusBadge } from '@nemo/common/src/components/StatusBadge'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { useDeferredUnmount } from '@nemo/common/src/hooks/useDeferredUnmount'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { useToast } from '@nemo/common/src/providers/toast/useToast'; @@ -34,17 +34,16 @@ import { ModelProviderFilter, ModelProviderSort, } from '@nemo/sdk/generated/platform/schema'; -import { Button, Flex, Stack, StatusMessage, Text } from '@nvidia/foundations-react-core'; -import { LINK_DOCS_INFERENCE_PROVIDERS } from '@studio/constants/links'; +import { Stack, Text } from '@nvidia/foundations-react-core'; import { EditInferenceProviderModal } from '@studio/routes/InferenceProvidersListRoute/EditInferenceProviderModal'; import { InferenceProviderDetailsSidePanel } from '@studio/routes/InferenceProvidersListRoute/InferenceProviderDetailsSidePanel'; import { keepPreviousData, useQueryClient } from '@tanstack/react-query'; -import { Workflow } from 'lucide-react'; import { ComponentProps, FC, useCallback, useMemo, useState } from 'react'; export interface InferenceProvidersDataViewProps { workspace: string; - emptyStateActions?: React.ReactNode; + /** Opens the create-provider flow from the first-use empty state. */ + readonly onCreate?: () => void; attributes?: { Stack?: React.ComponentProps; }; @@ -56,7 +55,7 @@ type ModalState = 'delete' | 'edit' | 'none'; export const InferenceProvidersDataView: FC = ({ workspace, - emptyStateActions, + onCreate, attributes, }) => { const toast = useToast(); @@ -216,39 +215,6 @@ export const InferenceProvidersDataView: FC = ( [] ); - const hasSearchOrFilters = !!dataViewState.debouncedSearchBar; - const isInitialEmpty = - providersWithId.length === 0 && !isFetching && !error && !hasSearchOrFilters; - - const emptyState = ( - - } - slotFooter={ - - - {emptyStateActions} - - } - /> - - ); - return ( = ( requestStatus: error ? 'error' : isFetching ? 'loading' : undefined, }, DataViewTableContent: { - renderEmptyState: () => - isInitialEmpty ? ( - emptyState + renderEmptyState: ({ hasFiltersApplied, hasSearchApplied }) => + hasFiltersApplied || hasSearchApplied ? ( + ) : ( - - Clear Search - - } + ), renderErrorState: () => ( diff --git a/web/packages/studio/src/components/dataViews/JobsDataView/index.test.tsx b/web/packages/studio/src/components/dataViews/JobsDataView/index.test.tsx index d4394ccda5..fe0a0023fe 100644 --- a/web/packages/studio/src/components/dataViews/JobsDataView/index.test.tsx +++ b/web/packages/studio/src/components/dataViews/JobsDataView/index.test.tsx @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { ENTITY_EMPTY_STATES } from '@nemo/common/src/components/EntityEmptyState/registry'; import { PlatformJobResponse, PlatformJobResponsesPage, @@ -69,8 +70,7 @@ describe('JobsDataView', () => { renderComponent(); - expect(await screen.findByText('Manage Jobs')).toBeInTheDocument(); - expect(screen.getByText('Documentation')).toBeInTheDocument(); + expect(await screen.findByText(ENTITY_EMPTY_STATES.jobs.heading)).toBeInTheDocument(); }); it('renders job data in the table', async () => { diff --git a/web/packages/studio/src/components/dataViews/JobsDataView/index.tsx b/web/packages/studio/src/components/dataViews/JobsDataView/index.tsx index 1ce408cea6..34b75ad6d5 100644 --- a/web/packages/studio/src/components/dataViews/JobsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/JobsDataView/index.tsx @@ -6,10 +6,10 @@ import { withOperators } from '@nemo/common/src/api/filterOperators'; import { CancelJobButton } from '@nemo/common/src/components/CancelJobButton'; import { dateTimeFilter } from '@nemo/common/src/components/DataView/dateTimeFilter'; import { StudioDataView } from '@nemo/common/src/components/DataView/StudioDataView'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; import { StatusBadge } from '@nemo/common/src/components/StatusBadge'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { getSortParam } from '@nemo/common/src/utils/query'; import { useJobsListJobs } from '@nemo/sdk/generated/platform/api'; @@ -18,7 +18,7 @@ import type { PlatformJobResponse, PlatformJobsListFilter, } from '@nemo/sdk/generated/platform/schema'; -import { Button, Flex, StatusMessage } from '@nvidia/foundations-react-core'; +import { Flex } from '@nvidia/foundations-react-core'; import { HIDDEN_JOB_SOURCES, JOB_SOURCE, @@ -26,12 +26,11 @@ import { } from '@studio/components/dataViews/JobsDataView/constants'; import { getJobDetailRoute } from '@studio/components/dataViews/JobsDataView/utils'; import { CUSTOMIZER_ENABLED } from '@studio/constants/environment'; -import { LINK_DOCS_JOBS } from '@studio/constants/links'; import { STATUS_FILTER_OPTIONS } from '@studio/constants/platformJobs'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { iconColorClass } from '@studio/routes/constants'; import { keepPreviousData } from '@tanstack/react-query'; -import { ChartBar, Cog, LayoutList, ListChecks, Sliders, Sparkles } from 'lucide-react'; +import { ChartBar, Cog, LayoutList, Sliders, Sparkles } from 'lucide-react'; import { ComponentProps, type ReactNode, useRef } from 'react'; import { useNavigate } from 'react-router'; @@ -206,10 +205,6 @@ export const JobsDataView = () => { }), ]; - const hasActiveFilters = - !!dataViewState.debouncedSearchBar || dataViewState.debouncedColumnFilters.length > 0; - const isInitialEmpty = jobs.length === 0 && !isFetching && !error && !hasActiveFilters; - if (error) { return ; } @@ -232,41 +227,15 @@ export const JobsDataView = () => { requestStatus: isFetching ? 'loading' : undefined, }, DataViewTableContent: { - renderEmptyState: () => - isInitialEmpty ? ( - - } - slotFooter={ - - - - } - /> - - ) : ( - - Clear Filters - - } + renderEmptyState: ({ hasFiltersApplied, hasSearchApplied }) => + hasFiltersApplied || hasSearchApplied ? ( + + ) : ( + ), }, }} diff --git a/web/packages/studio/src/components/dataViews/MembersDataView/index.tsx b/web/packages/studio/src/components/dataViews/MembersDataView/index.tsx index cb580e9ee1..353c93b6ce 100644 --- a/web/packages/studio/src/components/dataViews/MembersDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/MembersDataView/index.tsx @@ -3,19 +3,20 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { getErrorMessage } from '@nemo/common/src/api/common/utils'; import { ROW_ACTIONS_COLUMN_SIZE, StudioDataView, } from '@nemo/common/src/components/DataView/StudioDataView'; -import { ErrorMessage } from '@nemo/common/src/components/ErrorMessage'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; +import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { useEntitiesListWorkspaceMembers } from '@nemo/sdk/generated/platform/api'; import type { WorkspaceMember } from '@nemo/sdk/generated/platform/schema'; -import { Button, Text } from '@nvidia/foundations-react-core'; +import { Text } from '@nvidia/foundations-react-core'; import { Loading } from '@studio/components/Layouts/Loading'; -import { Pencil, Trash, UsersRound } from 'lucide-react'; +import { Pencil, Trash } from 'lucide-react'; import { ComponentProps, FC, useCallback, useMemo } from 'react'; export interface MembersDataViewProps { @@ -149,20 +150,11 @@ export const MembersDataView: FC = ({ }, DataViewTableContent: { renderEmptyState: () => ( - } - header="No members yet" - emptyMessage="Besides implicit workspace owners, no principals have been granted Viewer, Editor, or Admin access yet." - actions={ - - } - /> + ), renderErrorState: () => ( - ), }, diff --git a/web/packages/studio/src/components/dataViews/SafeSynthesizerJobsDataView/index.tsx b/web/packages/studio/src/components/dataViews/SafeSynthesizerJobsDataView/index.tsx index 802e0ae89d..ef57dea371 100644 --- a/web/packages/studio/src/components/dataViews/SafeSynthesizerJobsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/SafeSynthesizerJobsDataView/index.tsx @@ -9,11 +9,11 @@ import { ROW_SELECTION_COLUMN_SIZE, StudioDataView, } from '@nemo/common/src/components/DataView/StudioDataView'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { QuickActionsMenuRoot } from '@nemo/common/src/components/QuickActionsMenu/QuickActionsMenuRoot'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; import { ScoreGauge } from '@nemo/common/src/components/ScoreGauge'; import { StatusBadge } from '@nemo/common/src/components/StatusBadge'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { JOB_POLLING_INTERVAL_MS } from '@nemo/common/src/constants'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { getSortParam } from '@nemo/common/src/utils/query'; @@ -31,9 +31,7 @@ import { import { Banner, Button, Stack } from '@nvidia/foundations-react-core'; import { BulkDeleteModal } from '@studio/components/BulkDeleteModal'; import { isCancellableJob } from '@studio/components/dataViews/SafeSynthesizerJobsDataView/utils'; -import { DocumentationButton } from '@studio/components/DocumentationButton'; import { FilesetFilePreviewLink } from '@studio/components/SafeSynthesizerFilesetPreview/FilesetFilePreviewLink'; -import { LINK_DOCS_SAFE_SYNTHESIZER } from '@studio/constants/links'; import { STATUS_FILTER_OPTIONS } from '@studio/constants/platformJobs'; import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { @@ -42,7 +40,7 @@ import { getSafeSynthesizerJobRoute, } from '@studio/routes/utils'; import { keepPreviousData, useQueries, useQueryClient } from '@tanstack/react-query'; -import { ShieldCheck, Trash } from 'lucide-react'; +import { Trash } from 'lucide-react'; import { ComponentProps, FC, useCallback, useMemo, useState } from 'react'; import { Link, useNavigate } from 'react-router'; @@ -368,28 +366,16 @@ export const SafeSynthesizerJobsDataView: FC = () => { DataViewTableContent: { renderEmptyState: () => hasActiveFilters ? ( - - Clear Filters - - } + ) : ( - } - actions={ - <> - - - - } + navigate(getNewSafeSynthesizerRoute(workspace))} /> ), }, diff --git a/web/packages/studio/src/components/dataViews/SecretsDataView/index.test.tsx b/web/packages/studio/src/components/dataViews/SecretsDataView/index.test.tsx index 5d65addbc7..3201dca665 100644 --- a/web/packages/studio/src/components/dataViews/SecretsDataView/index.test.tsx +++ b/web/packages/studio/src/components/dataViews/SecretsDataView/index.test.tsx @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { ENTITY_EMPTY_STATES } from '@nemo/common/src/components/EntityEmptyState/registry'; import { SecretsDataView } from '@studio/components/dataViews/SecretsDataView'; import { PLATFORM_BASE_URL } from '@studio/constants/environment'; import { server } from '@studio/mocks/node'; @@ -13,7 +14,7 @@ import { MemoryRouter } from 'react-router'; const workspace = 'default'; -const renderComponent = (props?: { emptyStateActions?: React.ReactNode }) => { +const renderComponent = (props?: { onCreate?: () => void }) => { return render( @@ -74,21 +75,24 @@ describe('SecretsDataView', () => { ) ); - renderComponent(); + renderComponent({ onCreate: () => {} }); expect( - await screen.findByText('Manage Secrets', undefined, { timeout: XL_SELECTOR_TIMEOUT }) + await screen.findByText(ENTITY_EMPTY_STATES.secrets.heading, undefined, { + timeout: XL_SELECTOR_TIMEOUT, + }) ).toBeInTheDocument(); + expect(screen.getByText(ENTITY_EMPTY_STATES.secrets.subheading)).toBeInTheDocument(); expect( - screen.getByText( - 'Start by creating a secret, refer to the documentation for formatting details.' - ) + screen.getByRole('button', { name: ENTITY_EMPTY_STATES.secrets.createAction?.label }) ).toBeInTheDocument(); - expect(screen.getByRole('link', { name: /Documentation/ })).toBeInTheDocument(); }); - it('renders empty state actions when provided', async () => { + it('invokes onCreate when the create button is clicked', async () => { + const user = userEvent.setup(); + const onCreate = vi.fn(); + server.use( http.get(`${PLATFORM_BASE_URL}/apis/secrets/v2/workspaces/:workspace/secrets`, () => HttpResponse.json({ @@ -104,15 +108,16 @@ describe('SecretsDataView', () => { ) ); - renderComponent({ - emptyStateActions: , - }); + renderComponent({ onCreate }); - expect( - await screen.findByText('Manage Secrets', undefined, { timeout: XL_SELECTOR_TIMEOUT }) - ).toBeInTheDocument(); + const createButton = await screen.findByRole( + 'button', + { name: ENTITY_EMPTY_STATES.secrets.createAction?.label }, + { timeout: XL_SELECTOR_TIMEOUT } + ); + await user.click(createButton); - expect(screen.getByRole('button', { name: 'Create Secret' })).toBeInTheDocument(); + expect(onCreate).toHaveBeenCalledTimes(1); }); }); @@ -146,11 +151,13 @@ describe('SecretsDataView', () => { await user.type(searchInput, 'nonexistent-secret-name'); expect( - await screen.findByText('No Results Found', undefined, { timeout: XL_SELECTOR_TIMEOUT }) + await screen.findByText('No results found', undefined, { timeout: XL_SELECTOR_TIMEOUT }) ).toBeInTheDocument(); - expect(screen.getByText('No secrets match your search')).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Clear Search' })).toBeInTheDocument(); + expect( + screen.getByText('No items match your current search or filters.') + ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Clear Filters' })).toBeInTheDocument(); }); }); }); diff --git a/web/packages/studio/src/components/dataViews/SecretsDataView/index.tsx b/web/packages/studio/src/components/dataViews/SecretsDataView/index.tsx index 2709d5e509..30cdf9d775 100644 --- a/web/packages/studio/src/components/dataViews/SecretsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/SecretsDataView/index.tsx @@ -16,24 +16,22 @@ import { StudioDataView, } from '@nemo/common/src/components/DataView/StudioDataView'; import { DeleteConfirmationModal } from '@nemo/common/src/components/DeleteConfirmationModal'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { useToast } from '@nemo/common/src/providers/toast/useToast'; import { useSecretsDeleteSecret, useSecretsListSecrets } from '@nemo/sdk/generated/platform/api'; -import { PlatformSecretResponse } from '@nemo/sdk/generated/platform/schema'; -import { Button, Stack, Text } from '@nvidia/foundations-react-core'; -import { DocumentationButton } from '@studio/components/DocumentationButton'; -import { LINK_DOCS_SECRETS } from '@studio/constants/links'; +import type { PlatformSecretResponse } from '@nemo/sdk/generated/platform/schema'; +import { Stack, Text } from '@nvidia/foundations-react-core'; import { EditSecretModal } from '@studio/routes/SecretsListRoute/EditSecretModal'; import { keepPreviousData } from '@tanstack/react-query'; -import { LockKeyhole, Pencil, Trash } from 'lucide-react'; +import { Pencil, Trash } from 'lucide-react'; import { ComponentProps, FC, useCallback, useMemo, useState } from 'react'; export interface SecretsDataViewProps { workspace: string; - emptyStateActions?: React.ReactNode; + onCreate?: () => void; attributes?: { Stack?: React.ComponentProps; }; @@ -43,11 +41,7 @@ type SecretWithId = PlatformSecretResponse & { id: string }; type ModalState = 'delete' | 'edit' | 'none'; -export const SecretsDataView: FC = ({ - workspace, - emptyStateActions, - attributes, -}) => { +export const SecretsDataView: FC = ({ workspace, onCreate, attributes }) => { const toast = useToast(); const dataViewState = useStudioDataViewState({ @@ -174,8 +168,6 @@ export const SecretsDataView: FC = ({ [] ); - const hasActiveFilters = !!searchBar; - return ( = ({ requestStatus: error ? 'error' : isFetching ? 'loading' : undefined, }, DataViewTableContent: { - renderEmptyState: () => - hasActiveFilters ? ( - - Clear Search - - } + renderEmptyState: ({ hasFiltersApplied, hasSearchApplied }) => + hasFiltersApplied || hasSearchApplied ? ( + ) : ( - } - header="Manage Secrets" - emptyMessage="Start by creating a secret, refer to the documentation for formatting details." - actions={ - - - {emptyStateActions} - - } - /> + ), renderErrorState: () => ( { server.use(http.get(VMS_URL, () => HttpResponse.json(page([])))); renderDataView(); - expect(await screen.findByText('No Virtual Models')).toBeInTheDocument(); + expect(await screen.findByText(ENTITY_EMPTY_STATES.virtualModels.heading)).toBeInTheDocument(); }); it('deletes a virtual model through the row action', async () => { diff --git a/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx b/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx index a4eb248827..f66f04da76 100644 --- a/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx +++ b/web/packages/studio/src/components/dataViews/VirtualModelsDataView/index.tsx @@ -9,9 +9,9 @@ import { StudioDataView, } from '@nemo/common/src/components/DataView/StudioDataView'; import { DeleteConfirmationModal } from '@nemo/common/src/components/DeleteConfirmationModal'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { useDeferredUnmount } from '@nemo/common/src/hooks/useDeferredUnmount'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { useToast } from '@nemo/common/src/providers/toast/useToast'; @@ -25,11 +25,10 @@ import type { VirtualModel, VirtualModelFilter, } from '@nemo/sdk/generated/platform/schema'; -import { Button, Flex, Stack, StatusMessage, Text } from '@nvidia/foundations-react-core'; +import { Stack, Text } from '@nvidia/foundations-react-core'; import { BaseModelSearchFilterField } from '@studio/components/FilterFields'; import { VirtualModelDetailsSidePanel } from '@studio/routes/VirtualModelsListRoute/VirtualModelDetailsSidePanel'; import { keepPreviousData, useQueryClient } from '@tanstack/react-query'; -import { Waypoints } from 'lucide-react'; import { type ComponentProps, type FC, useCallback, useMemo, useState } from 'react'; export interface VirtualModelsDataViewProps { @@ -237,27 +236,6 @@ export const VirtualModelsDataView: FC = ({ [openDetailsPanel, workspace] ); - const hasSearchOrFilters = - !!dataViewState.debouncedSearchBar || dataViewState.debouncedColumnFilters.length > 0; - const isInitialEmpty = - virtualModelsWithId.length === 0 && !isFetching && !error && !hasSearchOrFilters; - - const emptyState = ( - - } - /> - - ); - return ( = ({ requestStatus: error ? 'error' : isFetching ? 'loading' : undefined, }, DataViewTableContent: { - renderEmptyState: () => - isInitialEmpty ? ( - emptyState - ) : ( - - Clear Filters - - } + renderEmptyState: ({ hasFiltersApplied, hasSearchApplied }) => + hasFiltersApplied || hasSearchApplied ? ( + + ) : ( + ), renderErrorState: () => ( void; onUploadFile: () => void; + onClearSearch: () => void; } export const FilesetFileExplorerEmptyState: FC = ({ - searchQuery, + hasSearchApplied, isReadWriteDataset, - onNewDirectory, onUploadFile, + onClearSearch, }) => ( - - Organize with folders or upload files by drag-and-drop or browsing.
Visit the - docs for setup instructions.{' '} - - Documentation - - - ) : ( - 'This fileset is read-only.' - ) - } - actions={ - searchQuery || !isReadWriteDataset ? null : ( - - - - - ) - } - /> + {hasSearchApplied ? ( + + ) : ( + + )}
); diff --git a/web/packages/studio/src/components/filesets/FilesetFileExplorer/index.test.tsx b/web/packages/studio/src/components/filesets/FilesetFileExplorer/index.test.tsx index 06a3a632d9..6ea35dc101 100644 --- a/web/packages/studio/src/components/filesets/FilesetFileExplorer/index.test.tsx +++ b/web/packages/studio/src/components/filesets/FilesetFileExplorer/index.test.tsx @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { ENTITY_EMPTY_STATES } from '@nemo/common/src/components/EntityEmptyState/registry'; import type { FilesetOutput } from '@nemo/sdk/generated/platform/schema'; import { FilesetFileExplorer } from '@studio/components/filesets/FilesetFileExplorer'; import { GITKEEP_FILENAME } from '@studio/components/FilesTable/utils'; @@ -110,7 +111,7 @@ describe('FilesetFileExplorer', () => { ], }); - expect(await screen.findByText('No Files')).toBeInTheDocument(); + expect(await screen.findByText(ENTITY_EMPTY_STATES.filesetFiles.heading)).toBeInTheDocument(); }); describe('read-only gating by storage.type', () => { @@ -169,29 +170,26 @@ describe('FilesetFileExplorer', () => { } ); - it('shows empty-state action buttons for local fileset', async () => { + it('shows the toolbar and empty-state upload actions for local fileset', async () => { stubRetrieve('local'); renderComponent({ filesList: [] }); - await screen.findByText('No Files'); - // Toolbar + empty-state both expose these buttons; wait for at least - // one of each since they only mount after the fileset query resolves. - expect( - (await screen.findAllByRole('button', { name: 'New Directory' })).length - ).toBeGreaterThan(0); + await screen.findByText(ENTITY_EMPTY_STATES.filesetFiles.heading); + expect(await screen.findByTestId('dataset-details-new-directory-button')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Upload File' })).toBeInTheDocument(); expect( - (await screen.findAllByRole('button', { name: 'Upload File' })).length - ).toBeGreaterThan(0); + screen.getByRole('button', { name: ENTITY_EMPTY_STATES.filesetFiles.createAction?.label }) + ).toBeInTheDocument(); }); - it('hides empty-state action buttons and shows read-only copy for external fileset', async () => { + it('hides create actions for external fileset', async () => { stubRetrieve('huggingface'); renderComponent({ filesList: [] }); - await screen.findByText('No Files'); - await waitFor(() => { - expect(screen.getByText('This fileset is read-only.')).toBeInTheDocument(); - }); - expect(screen.queryByRole('button', { name: 'New Directory' })).not.toBeInTheDocument(); + await screen.findByText(ENTITY_EMPTY_STATES.filesetFiles.heading); + expect(screen.queryByTestId('dataset-details-new-directory-button')).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Upload File' })).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: ENTITY_EMPTY_STATES.filesetFiles.createAction?.label }) + ).not.toBeInTheDocument(); }); }); diff --git a/web/packages/studio/src/components/filesets/FilesetFileExplorer/index.tsx b/web/packages/studio/src/components/filesets/FilesetFileExplorer/index.tsx index 05588c8729..d3cea9d1f8 100644 --- a/web/packages/studio/src/components/filesets/FilesetFileExplorer/index.tsx +++ b/web/packages/studio/src/components/filesets/FilesetFileExplorer/index.tsx @@ -251,10 +251,10 @@ export const FilesetFileExplorer: FC = ({ )} {!rowContents.length ? ( setNewDirectoryOpen(true)} onUploadFile={handleOpenUploadModal} + onClearSearch={() => handleSearchQueryChange('', clearSelectedItems)} /> ) : ( diff --git a/web/packages/studio/src/components/promoted/ErrorPanel.test.tsx b/web/packages/studio/src/components/promoted/ErrorPanel.test.tsx index e86cb4e49c..bc046f4f07 100644 --- a/web/packages/studio/src/components/promoted/ErrorPanel.test.tsx +++ b/web/packages/studio/src/components/promoted/ErrorPanel.test.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel, ErrorPanelProps } from '@nemo/common/src/components/ErrorPanel'; +import { ErrorPanelProps, RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { suppressConsoleError } from '@nemo/testing/utils/suppress-console'; import { mockUseNavigate } from '@studio/tests/util/mockUseParams'; import { render, screen, fireEvent } from '@testing-library/react'; @@ -42,7 +42,7 @@ const renderWithRouterError = ( { path: '/', element: , - errorElement: , + errorElement: , }, ], { initialEntries: ['/'] } @@ -51,7 +51,7 @@ const renderWithRouterError = ( return render(); }; -describe('ErrorPanel', () => { +describe('RouteErrorPanel', () => { describe('Error Display', () => { it('displays error UI when an error occurs', () => { renderWithRouterError({ title: 'Evaluator' }, new Error('Test error')); diff --git a/web/packages/studio/src/routes/DeploymentsListRoute/index.tsx b/web/packages/studio/src/routes/DeploymentsListRoute/index.tsx index bf2247d850..c5530904a8 100644 --- a/web/packages/studio/src/routes/DeploymentsListRoute/index.tsx +++ b/web/packages/studio/src/routes/DeploymentsListRoute/index.tsx @@ -144,16 +144,16 @@ export const DeploymentsListRoute: FC = () => { className="p-0" slotHeading="Deployments" slotDescription="Manage NIM deployments and their configurations." - slotActions={createDeploymentButton} - /> - {docsButton} {createDeploymentButton} } + /> + setIsCreateDeploymentOpen(true)} onDeploymentRowClick={(row) => navigate( getWorkspaceDeploymentDetailsRoute( diff --git a/web/packages/studio/src/routes/ExperimentRoute/index.tsx b/web/packages/studio/src/routes/ExperimentRoute/index.tsx index b1402b5c9d..e1bab18783 100644 --- a/web/packages/studio/src/routes/ExperimentRoute/index.tsx +++ b/web/packages/studio/src/routes/ExperimentRoute/index.tsx @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { getErrorMessage } from '@nemo/common/src/api/common/utils'; import type { WithFilterOperators } from '@nemo/common/src/api/filterOperators'; import { AccessibleTitle } from '@nemo/common/src/components/AccessibleTitle'; import { @@ -9,6 +10,8 @@ import { } from '@nemo/common/src/components/DataView/dateTimeFilter'; import * as DataView from '@nemo/common/src/components/DataView/internal'; import { StudioDataView } from '@nemo/common/src/components/DataView/StudioDataView'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; +import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { DEFAULT_PAGE_SIZE_OPTIONS } from '@nemo/common/src/constants/pagination'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { useListExperiments } from '@nemo/sdk/generated/platform/api'; @@ -16,7 +19,6 @@ import type { ExperimentFilter, ExperimentResponse } from '@nemo/sdk/generated/p import { Block, Button, - Flex, PageHeader, PaginationArrowButton, PaginationControlsGroup, @@ -27,7 +29,6 @@ import { PaginationPageInput, PaginationPageSizeSelect, Stack, - StatusMessage, Text, } from '@nvidia/foundations-react-core'; import { ExperimentCreateModal } from '@studio/components/ExperimentCreateModal'; @@ -37,7 +38,6 @@ import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath'; import { useBreadcrumbs } from '@studio/providers/breadcrumbs/useBreadcrumbs'; import { ExperimentCard } from '@studio/routes/ExperimentRoute/ExperimentCard'; import { keepPreviousData } from '@tanstack/react-query'; -import { CircleAlert } from 'lucide-react'; import { type ComponentProps, type FC, useMemo, useState } from 'react'; const DEFAULT_PAGE_SIZE = 10; @@ -148,24 +148,27 @@ export const ExperimentRoute: FC = () => { renderLoadingState={() => } - renderEmptyState={({ hasSearchApplied, hasFiltersApplied }) => ( - - - {hasSearchApplied || hasFiltersApplied - ? 'No experiments match your search or filters.' - : 'No experiments yet.'} - - - )} - renderErrorState={() => ( - - } - slotHeading="Error loading experiments" - slotSubheading={error?.message} + renderEmptyState={({ hasSearchApplied, hasFiltersApplied }) => + hasSearchApplied || hasFiltersApplied ? ( + + ) : ( + setIsCreateModalOpen(true)} /> - + ) + } + renderErrorState={() => ( + )} > {({ rows }) => ( diff --git a/web/packages/studio/src/routes/FilesetDetailRoute/index.test.tsx b/web/packages/studio/src/routes/FilesetDetailRoute/index.test.tsx index ff1ba2f42b..0ece672c2e 100644 --- a/web/packages/studio/src/routes/FilesetDetailRoute/index.test.tsx +++ b/web/packages/studio/src/routes/FilesetDetailRoute/index.test.tsx @@ -5,7 +5,7 @@ import { FilesetOutput, FilesetPurpose } from '@nemo/sdk/generated/platform/sche import { FilesetDetailRoute } from '@studio/routes/FilesetDetailRoute'; import { render } from '@studio/tests/util/render'; import { TestProviders } from '@studio/tests/util/TestProviders'; -import { render as rtlRender, screen } from '@testing-library/react'; +import { render as rtlRender, act, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { MemoryRouter } from 'react-router'; @@ -110,11 +110,19 @@ describe('FilesetDetailRoute', () => {
); - it('opens the Files tab when the initial URL has ?tab=files', () => { + it('opens the Files tab when the initial URL has ?tab=files', async () => { renderAtUrl('/?tab=files'); expect(screen.getByRole('tab', { name: 'Files' })).toHaveAttribute('aria-selected', 'true'); expect(screen.getByTestId('fileset-files-tab')).toBeInTheDocument(); + + // The empty Files tab's self-service help renders a CodeSnippet whose + // syntax highlighting resolves asynchronously after mount. Flush it here + // so the state update lands inside act() instead of leaking past this + // test's synchronous assertions. + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + }); }); it('falls back to the default tab when ?tab= is an unknown value', () => { diff --git a/web/packages/studio/src/routes/InferenceProvidersListRoute/index.tsx b/web/packages/studio/src/routes/InferenceProvidersListRoute/index.tsx index f047d33e80..716fb2d75e 100644 --- a/web/packages/studio/src/routes/InferenceProvidersListRoute/index.tsx +++ b/web/packages/studio/src/routes/InferenceProvidersListRoute/index.tsx @@ -67,7 +67,7 @@ export const InferenceProvidersListRoute: FC = () => { /> setIsCreatePanelOpen(true)} attributes={{ Stack: { className: 'flex-1 min-h-0', diff --git a/web/packages/studio/src/routes/SecretsListRoute/index.tsx b/web/packages/studio/src/routes/SecretsListRoute/index.tsx index de9cddc8f0..c72ec9a4dc 100644 --- a/web/packages/studio/src/routes/SecretsListRoute/index.tsx +++ b/web/packages/studio/src/routes/SecretsListRoute/index.tsx @@ -40,11 +40,7 @@ export const SecretsListRoute: FC = () => { /> setIsCreateModalOpen(true)}> - Create Secret - - } + onCreate={() => setIsCreateModalOpen(true)} attributes={{ Stack: { className: 'flex-1 min-h-0', diff --git a/web/packages/studio/src/routes/WorkspaceBaseModelsRoute/index.tsx b/web/packages/studio/src/routes/WorkspaceBaseModelsRoute/index.tsx index 9dd5c34533..e988a5abbb 100644 --- a/web/packages/studio/src/routes/WorkspaceBaseModelsRoute/index.tsx +++ b/web/packages/studio/src/routes/WorkspaceBaseModelsRoute/index.tsx @@ -10,6 +10,7 @@ * its affiliates is strictly prohibited. */ +import { getErrorMessage } from '@nemo/common/src/api/common/utils'; import { useBaseModels, type ModelEntityFilterInput, @@ -19,14 +20,14 @@ import { AccessibleTitle } from '@nemo/common/src/components/AccessibleTitle'; import { dateTimeFilter } from '@nemo/common/src/components/DataView/dateTimeFilter'; import * as DataView from '@nemo/common/src/components/DataView/internal'; import { StudioDataView } from '@nemo/common/src/components/DataView/StudioDataView'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; +import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { getModelEntityChatStatus } from '@nemo/common/src/utils/models'; import { getSortParam } from '@nemo/common/src/utils/query'; import { useModelsGetModel } from '@nemo/sdk/generated/platform/api'; import type { ModelEntity, ModelEntitySortField } from '@nemo/sdk/generated/platform/schema'; import { - Button, Checkbox, Flex, PageHeader, @@ -149,10 +150,6 @@ export const WorkspaceBaseModelsRoute: FC = () => { customizableFilter && Object.keys(customizableFilter).length > 0 ); - // Only blame the Customizable filter for an empty result when it's the *only* active filter — - // otherwise a name search or date filter could be the real cause and we'd misreport it. - const onlyCustomizableFilterActive = customizableFilterActive && !nameSearch && !apiColumnFilters; - const hasActiveFilters = !!nameSearch || !!apiColumnFilters || customizableFilterActive; const filter = useMemo(() => { @@ -169,6 +166,7 @@ export const WorkspaceBaseModelsRoute: FC = () => { models, isLoading, isError, + error, hasNextPage, fetchNextPage, isFetchingNextPage, @@ -388,28 +386,21 @@ export const WorkspaceBaseModelsRoute: FC = () => { )} - renderEmptyState={() => ( - - )} - renderErrorState={() => ( - - + hasActiveFilters ? ( + - - + ) : ( + + ) + } + renderErrorState={() => ( + )} > {({ rows }) => ( diff --git a/web/packages/studio/src/routes/agents/AgentEvaluationsRoute/AgentEvaluationsListRoute.test.tsx b/web/packages/studio/src/routes/agents/AgentEvaluationsRoute/AgentEvaluationsListRoute.test.tsx index d0413648ed..23eb01a090 100644 --- a/web/packages/studio/src/routes/agents/AgentEvaluationsRoute/AgentEvaluationsListRoute.test.tsx +++ b/web/packages/studio/src/routes/agents/AgentEvaluationsRoute/AgentEvaluationsListRoute.test.tsx @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { ENTITY_EMPTY_STATES } from '@nemo/common/src/components/EntityEmptyState/registry'; import { ROUTES } from '@studio/constants/routes'; import { workspace1 } from '@studio/mocks/entity-store/projects'; import { AgentEvaluationsListRoute } from '@studio/routes/agents/AgentEvaluationsRoute'; @@ -26,6 +27,8 @@ describe('AgentEvaluationsListRoute', () => { it('shows the empty state when no eval jobs are returned (default mock)', async () => { renderList(); - expect(await screen.findByText('No evaluation jobs yet')).toBeInTheDocument(); + expect( + await screen.findByText(ENTITY_EMPTY_STATES.agentEvaluations.heading) + ).toBeInTheDocument(); }); }); diff --git a/web/packages/studio/src/routes/agents/AgentMonitorRoute/components/InferenceLogsTable.tsx b/web/packages/studio/src/routes/agents/AgentMonitorRoute/components/InferenceLogsTable.tsx index 04bd0df826..41fe84836a 100644 --- a/web/packages/studio/src/routes/agents/AgentMonitorRoute/components/InferenceLogsTable.tsx +++ b/web/packages/studio/src/routes/agents/AgentMonitorRoute/components/InferenceLogsTable.tsx @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 import { StudioDataView } from '@nemo/common/src/components/DataView/StudioDataView'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { ErrorMessage } from '@nemo/common/src/components/ErrorMessage'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { Button, Stack, Text } from '@nvidia/foundations-react-core'; import type { RunSummary } from '@studio/routes/agents/AgentMonitorRoute/telemetry'; @@ -184,10 +184,7 @@ export const InferenceLogsTable: FC = ({ runs, isFetching, error, onRetry /> ), renderEmptyState: () => ( - + ), }, }} diff --git a/web/packages/studio/src/routes/groups/agentRoutes.tsx b/web/packages/studio/src/routes/groups/agentRoutes.tsx index 4ab7c8797d..fb565e226a 100644 --- a/web/packages/studio/src/routes/groups/agentRoutes.tsx +++ b/web/packages/studio/src/routes/groups/agentRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { AGENTS_ENABLED, MONITOR_ENABLED } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; import { iconColorClass } from '@studio/routes/constants'; @@ -52,31 +52,31 @@ export const agentRoutes: RouteObject[] = agentsRoutes([ { path: ROUTES.workspace.agentsList, element: AgentsListRoute ? : null, - errorElement: , + errorElement: , }, ...(MONITOR_ENABLED ? [ { path: ROUTES.workspace.agentMonitor, element: , - errorElement: , + errorElement: , }, ] : []), { path: ROUTES.workspace.agentEvaluationsList, element: AgentEvaluationsListRoute ? : null, - errorElement: , + errorElement: , }, { path: ROUTES.workspace.agentEvaluationDetail, element: AgentEvaluationDetailRoute ? : null, - errorElement: , + errorElement: , }, { path: ROUTES.workspace.agentDetail, element: AgentDetailRoute ? : null, - errorElement: , + errorElement: , }, ]); diff --git a/web/packages/studio/src/routes/groups/anonymizerRoutes.tsx b/web/packages/studio/src/routes/groups/anonymizerRoutes.tsx index ff477e7736..0257f40dc6 100644 --- a/web/packages/studio/src/routes/groups/anonymizerRoutes.tsx +++ b/web/packages/studio/src/routes/groups/anonymizerRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { ANONYMIZER_ENABLED } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; import { iconColorClass } from '@studio/routes/constants'; @@ -36,17 +36,17 @@ export const anonymizerRoutes: RouteObject[] = gateAnonymizerRoutes([ { path: ROUTES.workspace.anonymizer, element: AnonymizerListRoute ? : null, - errorElement: , + errorElement: , }, { path: ROUTES.workspace.anonymizerNew, element: AnonymizerBuilderRoute ? : null, - errorElement: , + errorElement: , }, { path: ROUTES.workspace.anonymizerJob, element: AnonymizerJobDetailRoute ? : null, - errorElement: , + errorElement: , }, ]); diff --git a/web/packages/studio/src/routes/groups/customizationRoutes.tsx b/web/packages/studio/src/routes/groups/customizationRoutes.tsx index 5ebc8a7d4d..392a23dad3 100644 --- a/web/packages/studio/src/routes/groups/customizationRoutes.tsx +++ b/web/packages/studio/src/routes/groups/customizationRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { CUSTOMIZER_ENABLED } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; import { iconColorClass } from '@studio/routes/constants'; @@ -38,22 +38,22 @@ export const customizationRoutes: RouteObject[] = gateCustomizationRoutes([ { path: ROUTES.workspace.newCustomizationJob, element: , - errorElement: , + errorElement: , }, { path: ROUTES.workspace.promptTuningForm, element: , - errorElement: , + errorElement: , }, { path: ROUTES.workspace.customizationJobList, element: , - errorElement: , + errorElement: , }, { path: ROUTES.workspace.customizationJobDetails, element: , - errorElement: , + errorElement: , }, ]); diff --git a/web/packages/studio/src/routes/groups/dashboardRoutes.tsx b/web/packages/studio/src/routes/groups/dashboardRoutes.tsx index d29c30aabd..e58b855b1e 100644 --- a/web/packages/studio/src/routes/groups/dashboardRoutes.tsx +++ b/web/packages/studio/src/routes/groups/dashboardRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { ASSISTANT_STUDIO_ENABLED, DASHBOARD_ENABLED } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; import { iconColorClass } from '@studio/routes/constants'; @@ -34,13 +34,13 @@ export const dashboardRoutes: RouteObject[] = gateDashboardRoutes([ { path: ROUTES.workspace.dashboard, element: ASSISTANT_STUDIO_ENABLED ? : , - errorElement: , + errorElement: , }, ...gateAssistantStudioRoutes([ { path: ROUTES.workspace.assistantChat, element: , - errorElement: , + errorElement: , }, ]), ]); diff --git a/web/packages/studio/src/routes/groups/dataDesignerRoutes.tsx b/web/packages/studio/src/routes/groups/dataDesignerRoutes.tsx index ac01ed398e..1a2e9c52ef 100644 --- a/web/packages/studio/src/routes/groups/dataDesignerRoutes.tsx +++ b/web/packages/studio/src/routes/groups/dataDesignerRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { DATA_DESIGNER_ENABLED } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; import { iconColorClass } from '@studio/routes/constants'; @@ -50,27 +50,27 @@ export const dataDesignerRoutes: RouteObject[] = gateDataDesignerRoutes([ { path: ROUTES.workspace.dataDesignerJobList, element: DataDesignerJobListRoute ? : null, - errorElement: , + errorElement: , }, { path: ROUTES.workspace.dataDesignerJobDetails, element: DataDesignerJobDetailsRoute ? : null, - errorElement: , + errorElement: , }, { path: ROUTES.workspace.dataDesignerJobNew, element: NewDataDesignerJobRoute ? : null, - errorElement: , + errorElement: , }, { path: ROUTES.workspace.dataDesignerJobBuild, element: DataDesignerJobBuildRoute ? : null, - errorElement: , + errorElement: , }, { path: ROUTES.workspace.dataDesignerJobNewLegacy, element: LegacyNewDataDesignerJobRoute ? : null, - errorElement: , + errorElement: , }, ]); diff --git a/web/packages/studio/src/routes/groups/deploymentRoutes.tsx b/web/packages/studio/src/routes/groups/deploymentRoutes.tsx index f19fbb623c..d9a5f2d58d 100644 --- a/web/packages/studio/src/routes/groups/deploymentRoutes.tsx +++ b/web/packages/studio/src/routes/groups/deploymentRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { DEPLOYMENTS_ENABLED } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; import { iconColorClass } from '@studio/routes/constants'; @@ -22,12 +22,12 @@ export const deploymentRoutes: RouteObject[] = gateDeploymentsRoutes([ { path: ROUTES.workspace.deployments, element: DeploymentsListRoute ? : null, - errorElement: , + errorElement: , }, { path: ROUTES.workspace.deploymentsDeployment, element: DeploymentsListRoute ? : null, - errorElement: , + errorElement: , }, ]); diff --git a/web/packages/studio/src/routes/groups/evaluationRoutes.tsx b/web/packages/studio/src/routes/groups/evaluationRoutes.tsx index 7906be7171..d9289e7ffa 100644 --- a/web/packages/studio/src/routes/groups/evaluationRoutes.tsx +++ b/web/packages/studio/src/routes/groups/evaluationRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { EVALUATOR_ENABLED } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; import { iconColorClass } from '@studio/routes/constants'; @@ -39,7 +39,7 @@ export const evaluationRoutes: RouteObject[] = gateEvaluationRoutes([ { path: ROUTES.workspace.evaluation, element: , - errorElement: , + errorElement: , children: [ { index: true, @@ -51,12 +51,12 @@ export const evaluationRoutes: RouteObject[] = gateEvaluationRoutes([ { path: ROUTES.workspace.evaluationResultDetails, element: , - errorElement: , + errorElement: , }, { path: ROUTES.workspace.evaluationResults, element: , - errorElement: , + errorElement: , children: [ { index: true, diff --git a/web/packages/studio/src/routes/groups/experimentRoutes.tsx b/web/packages/studio/src/routes/groups/experimentRoutes.tsx index 55c69ccb72..a3215f0726 100644 --- a/web/packages/studio/src/routes/groups/experimentRoutes.tsx +++ b/web/packages/studio/src/routes/groups/experimentRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { EXPERIMENT_ENABLED } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; import { iconColorClass } from '@studio/routes/constants'; @@ -35,22 +35,22 @@ export const experimentRoutes: RouteObject[] = gateExperimentRoutes([ { path: ROUTES.workspace.experiment, element: , - errorElement: , + errorElement: , }, { path: ROUTES.workspace.evaluationSessionDetail, element: , - errorElement: , + errorElement: , }, { path: ROUTES.workspace.experimentDetail, element: , - errorElement: , + errorElement: , }, { path: ROUTES.workspace.evaluationDetail, element: , - errorElement: , + errorElement: , }, ]); diff --git a/web/packages/studio/src/routes/groups/filesetRoutes.tsx b/web/packages/studio/src/routes/groups/filesetRoutes.tsx index 8a5b510249..0f6251e1d3 100644 --- a/web/packages/studio/src/routes/groups/filesetRoutes.tsx +++ b/web/packages/studio/src/routes/groups/filesetRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { DATASETS_ENABLED } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; import { iconColorClass } from '@studio/routes/constants'; @@ -33,7 +33,7 @@ export const filesetRoutes: RouteObject[] = gateDatasetsRoutes([ { path: ROUTES.workspace.filesets, element: , - errorElement: , + errorElement: , children: [ { path: ROUTES.workspace.filesetNew, @@ -53,7 +53,7 @@ export const filesetRoutes: RouteObject[] = gateDatasetsRoutes([ { path: ROUTES.workspace.filesetDetail, element: , - errorElement: , + errorElement: , }, ]), ]); diff --git a/web/packages/studio/src/routes/groups/guardrailsRoutes.tsx b/web/packages/studio/src/routes/groups/guardrailsRoutes.tsx index 2008f45886..556bcf27ff 100644 --- a/web/packages/studio/src/routes/groups/guardrailsRoutes.tsx +++ b/web/packages/studio/src/routes/groups/guardrailsRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { GUARDRAILS_ENABLED } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; import { iconColorClass } from '@studio/routes/constants'; @@ -39,12 +39,12 @@ export const guardrailsRoutes: RouteObject[] = gateGuardrailsRoutes([ { path: ROUTES.workspace.guardrails, element: , - errorElement: , + errorElement: , }, { path: ROUTES.workspace.guardrailDetail, element: , - errorElement: , + errorElement: , children: [ { index: true, diff --git a/web/packages/studio/src/routes/groups/inferenceProviderRoutes.tsx b/web/packages/studio/src/routes/groups/inferenceProviderRoutes.tsx index 1e8fedfe9a..ca6995a9cd 100644 --- a/web/packages/studio/src/routes/groups/inferenceProviderRoutes.tsx +++ b/web/packages/studio/src/routes/groups/inferenceProviderRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { ROUTES } from '@studio/constants/routes'; import { gateInferenceProviderRoutes } from '@studio/routes/utils'; import { lazy } from 'react'; @@ -17,6 +17,6 @@ export const inferenceProviderRoutes: RouteObject[] = gateInferenceProviderRoute { path: ROUTES.workspace.inferenceProviders, element: , - errorElement: , + errorElement: , }, ]); diff --git a/web/packages/studio/src/routes/groups/intakeRoutes.tsx b/web/packages/studio/src/routes/groups/intakeRoutes.tsx index c50ca9a822..0e4aa3c161 100644 --- a/web/packages/studio/src/routes/groups/intakeRoutes.tsx +++ b/web/packages/studio/src/routes/groups/intakeRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { Stack } from '@nvidia/foundations-react-core'; import { INTAKE_ENABLED } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; @@ -47,7 +47,7 @@ export const intakeRoutes: RouteObject[] = gateIntakeRoutes([ { path: ROUTES.workspace.intake, element: , - errorElement: , + errorElement: , children: [ { index: true, @@ -66,7 +66,7 @@ export const intakeRoutes: RouteObject[] = gateIntakeRoutes([ { path: ROUTES.workspace.intakeSession, element: , - errorElement: , + errorElement: , }, ]); diff --git a/web/packages/studio/src/routes/groups/jobRoutes.tsx b/web/packages/studio/src/routes/groups/jobRoutes.tsx index 75622658b2..e69efbbd85 100644 --- a/web/packages/studio/src/routes/groups/jobRoutes.tsx +++ b/web/packages/studio/src/routes/groups/jobRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { JOBS_ENABLED } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; import { iconColorClass } from '@studio/routes/constants'; @@ -25,12 +25,12 @@ export const jobRoutes: RouteObject[] = gateJobsRoutes([ { path: ROUTES.workspace.jobs, element: , - errorElement: , + errorElement: , }, { path: ROUTES.workspace.jobDetail, element: , - errorElement: , + errorElement: , }, ]); diff --git a/web/packages/studio/src/routes/groups/memberRoutes.tsx b/web/packages/studio/src/routes/groups/memberRoutes.tsx index a0492267a8..819ca3dc56 100644 --- a/web/packages/studio/src/routes/groups/memberRoutes.tsx +++ b/web/packages/studio/src/routes/groups/memberRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { ROUTES } from '@studio/constants/routes'; import { gateMembersRoutes } from '@studio/routes/utils'; import { lazy } from 'react'; @@ -17,6 +17,6 @@ export const memberRoutes: RouteObject[] = gateMembersRoutes([ { path: ROUTES.workspace.members, element: , - errorElement: , + errorElement: , }, ]); diff --git a/web/packages/studio/src/routes/groups/modelCompareRoutes.tsx b/web/packages/studio/src/routes/groups/modelCompareRoutes.tsx index 4d9ab95cd6..0e7ac05e22 100644 --- a/web/packages/studio/src/routes/groups/modelCompareRoutes.tsx +++ b/web/packages/studio/src/routes/groups/modelCompareRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { MODEL_COMPARE_ENABLED } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; import { iconColorClass } from '@studio/routes/constants'; @@ -22,7 +22,7 @@ export const modelCompareRoutes: RouteObject[] = gateModelCompareRoutes([ { path: ROUTES.workspace.modelCompare, element: ModelCompareRoute ? : null, - errorElement: , + errorElement: , }, ]); diff --git a/web/packages/studio/src/routes/groups/optimizerRoutes.tsx b/web/packages/studio/src/routes/groups/optimizerRoutes.tsx index f808482996..521f033904 100644 --- a/web/packages/studio/src/routes/groups/optimizerRoutes.tsx +++ b/web/packages/studio/src/routes/groups/optimizerRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { OPTIMIZER_ENABLED } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; import { iconColorClass } from '@studio/routes/constants'; @@ -32,12 +32,12 @@ export const optimizerRoutes: RouteObject[] = gateOptimizerRoutes( { path: ROUTES.workspace.optimizer, element: , - errorElement: , + errorElement: , }, { path: ROUTES.workspace.optimizerInsight, element: , - errorElement: , + errorElement: , }, ] : [] diff --git a/web/packages/studio/src/routes/groups/safeSynthesizerRoutes.tsx b/web/packages/studio/src/routes/groups/safeSynthesizerRoutes.tsx index 407a296767..b39852f298 100644 --- a/web/packages/studio/src/routes/groups/safeSynthesizerRoutes.tsx +++ b/web/packages/studio/src/routes/groups/safeSynthesizerRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { SAFE_SYNTHESIZER_ENABLED } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; import { iconColorClass } from '@studio/routes/constants'; @@ -43,22 +43,22 @@ export const safeSynthesizerRoutes: RouteObject[] = gateSafeSynthesizerRoutes([ { path: ROUTES.workspace.safeSynthesizer, element: SafeSynthesizerListRoute ? : null, - errorElement: , + errorElement: , }, { path: ROUTES.workspace.safeSynthesizerNew, element: SafeSynthesizerNewRoute ? : null, - errorElement: , + errorElement: , }, { path: ROUTES.workspace.safeSynthesizerJob, element: SafeSynthesizerJobDetailsRoute ? : null, - errorElement: , + errorElement: , }, { path: ROUTES.workspace.safeSynthesizerJobReport, element: SafeSynthesizerJobReportRoute ? : null, - errorElement: , + errorElement: , }, ]); diff --git a/web/packages/studio/src/routes/groups/secretsRoutes.tsx b/web/packages/studio/src/routes/groups/secretsRoutes.tsx index 3b22f67ceb..e8b5a9620e 100644 --- a/web/packages/studio/src/routes/groups/secretsRoutes.tsx +++ b/web/packages/studio/src/routes/groups/secretsRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { ROUTES } from '@studio/constants/routes'; import { gateSecretsRoutes } from '@studio/routes/utils'; import { lazy } from 'react'; @@ -15,6 +15,6 @@ export const secretsRoutes: RouteObject[] = gateSecretsRoutes([ { path: ROUTES.workspace.secrets, element: , - errorElement: , + errorElement: , }, ]); diff --git a/web/packages/studio/src/routes/groups/settingsRoutes.tsx b/web/packages/studio/src/routes/groups/settingsRoutes.tsx index b674ac2de3..a1f62ea5c7 100644 --- a/web/packages/studio/src/routes/groups/settingsRoutes.tsx +++ b/web/packages/studio/src/routes/groups/settingsRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { SETTINGS_ENABLED } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; import { iconColorClass } from '@studio/routes/constants'; @@ -20,7 +20,7 @@ export const settingsRoutes: RouteObject[] = gateSettingsRoutes([ { path: ROUTES.workspace.settings, element: , - errorElement: , + errorElement: , }, ]); diff --git a/web/packages/studio/src/routes/groups/virtualModelsRoutes.tsx b/web/packages/studio/src/routes/groups/virtualModelsRoutes.tsx index b607bc317a..e77ef76f81 100644 --- a/web/packages/studio/src/routes/groups/virtualModelsRoutes.tsx +++ b/web/packages/studio/src/routes/groups/virtualModelsRoutes.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { GUARDRAILS_ENABLED } from '@studio/constants/environment'; import { ROUTES } from '@studio/constants/routes'; import { iconColorClass } from '@studio/routes/constants'; @@ -20,7 +20,7 @@ export const virtualModelsRoutes: RouteObject[] = gateGuardrailsRoutes([ { path: ROUTES.workspace.virtualModels, element: , - errorElement: , + errorElement: , }, ]); diff --git a/web/packages/studio/src/routes/index.tsx b/web/packages/studio/src/routes/index.tsx index c6d2b6d267..439de8f8eb 100644 --- a/web/packages/studio/src/routes/index.tsx +++ b/web/packages/studio/src/routes/index.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { ErrorMessage } from '@nemo/common/src/components/ErrorMessage'; -import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; +import { RouteErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { Loading } from '@studio/components/Layouts/Loading'; import { ROUTES } from '@studio/constants/routes'; import { PluginProvider } from '@studio/plugins/PluginProvider'; @@ -105,7 +105,7 @@ export const routes: RouteObject[] = [ ), - errorElement: , + errorElement: , children: [ ...dashboardRoutes, ...baseModelsRoutes, @@ -129,7 +129,7 @@ export const routes: RouteObject[] = [ // The /* suffix allows the plugin to own sub-paths via its own internal router. path: `${ROUTES.workspace.plugin}/*`, element: , - errorElement: , + errorElement: , }), ...settingsRoutes, ...modelCompareRoutes, diff --git a/web/packages/studio/src/routes/optimizer/InsightTracesTable/index.test.tsx b/web/packages/studio/src/routes/optimizer/InsightTracesTable/index.test.tsx index 3d9d31c6bd..e947c3713d 100644 --- a/web/packages/studio/src/routes/optimizer/InsightTracesTable/index.test.tsx +++ b/web/packages/studio/src/routes/optimizer/InsightTracesTable/index.test.tsx @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { ENTITY_EMPTY_STATES } from '@nemo/common/src/components/EntityEmptyState/registry'; import { DEFAULT_WORKSPACE } from '@nemo/common/src/models/constants'; import type { Trace } from '@nemo/sdk/generated/platform/schema'; import { server } from '@studio/mocks/node'; @@ -93,6 +94,8 @@ describe('InsightTracesTable', () => { renderRoute(); expect(await screen.findByText('Error')).toBeInTheDocument(); - expect(screen.queryByText('This insight has no linked traces.')).not.toBeInTheDocument(); + expect( + screen.queryByText(ENTITY_EMPTY_STATES.insightTraces.subheading) + ).not.toBeInTheDocument(); }); }); diff --git a/web/packages/studio/src/routes/optimizer/InsightTracesTable/index.tsx b/web/packages/studio/src/routes/optimizer/InsightTracesTable/index.tsx index b5c0b6a421..568c161ec2 100644 --- a/web/packages/studio/src/routes/optimizer/InsightTracesTable/index.tsx +++ b/web/packages/studio/src/routes/optimizer/InsightTracesTable/index.tsx @@ -4,8 +4,8 @@ import { getErrorMessage } from '@nemo/common/src/api/common/utils'; import { withOperators } from '@nemo/common/src/api/filterOperators'; import { EditColumnsMenu } from '@nemo/common/src/components/DataView/internal'; -import { ErrorMessage } from '@nemo/common/src/components/ErrorMessage'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; +import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { useListTraces } from '@nemo/sdk/generated/platform/api'; import type { Trace, TraceFilter } from '@nemo/sdk/generated/platform/schema'; @@ -91,15 +91,10 @@ export const InsightTracesTable: FC = ({ workspace, tra requestStatus: error ? 'error' : isFetching ? 'loading' : undefined, }, DataViewTableContent: { - renderEmptyState: () => ( - - ), + renderEmptyState: () => , renderErrorState: () => ( - ), }, diff --git a/web/packages/studio/src/routes/optimizer/OptimizerInsightRoute/InsightExperiments.tsx b/web/packages/studio/src/routes/optimizer/OptimizerInsightRoute/InsightExperiments.tsx index 59666f9fc0..8ad354bfca 100644 --- a/web/packages/studio/src/routes/optimizer/OptimizerInsightRoute/InsightExperiments.tsx +++ b/web/packages/studio/src/routes/optimizer/OptimizerInsightRoute/InsightExperiments.tsx @@ -3,16 +3,15 @@ import * as DataView from '@nemo/common/src/components/DataView/internal'; import { useRowClick } from '@nemo/common/src/components/DataView/useRowClick'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { ErrorMessage } from '@nemo/common/src/components/ErrorMessage'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { DEFAULT_PAGE_SIZE_OPTIONS } from '@nemo/common/src/constants/pagination'; import { useListExperiments } from '@nemo/sdk/generated/platform/api'; import type { ExperimentResponse } from '@nemo/sdk/generated/platform/schema'; import { Button, Text } from '@nvidia/foundations-react-core'; import { getExperimentDetailRoute } from '@studio/routes/utils'; import { keepPreviousData } from '@tanstack/react-query'; -import { FlaskConical } from 'lucide-react'; import { type ComponentProps, type FC } from 'react'; import { useNavigate } from 'react-router'; @@ -99,20 +98,10 @@ export const InsightExperiments: FC = ({ className={`min-h-0 flex-1 overflow-auto bg-transparent [&_td]:!bg-transparent [&_thead]:!bg-transparent [&_thead_th]:!bg-transparent ${className}`} onClick={onClick} renderEmptyState={() => ( - } - actions={ - - } + )} renderErrorState={() => ( diff --git a/web/packages/studio/src/routes/optimizer/OptimizerInsightRoute/index.test.tsx b/web/packages/studio/src/routes/optimizer/OptimizerInsightRoute/index.test.tsx index 6dbbe9d7cf..e68d5fa78a 100644 --- a/web/packages/studio/src/routes/optimizer/OptimizerInsightRoute/index.test.tsx +++ b/web/packages/studio/src/routes/optimizer/OptimizerInsightRoute/index.test.tsx @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { ENTITY_EMPTY_STATES } from '@nemo/common/src/components/EntityEmptyState/registry'; import { getListEvaluationsQueryKey, getListExperimentsQueryKey, @@ -118,7 +119,9 @@ describe('OptimizerInsightRoute experiments', () => { const { unmount } = renderInsight(); expect(await screen.findByText('Failed to load experiments')).toBeInTheDocument(); - expect(screen.queryByText('No experiments for this insight.')).not.toBeInTheDocument(); + expect( + screen.queryByText(ENTITY_EMPTY_STATES.insightExperiments.subheading) + ).not.toBeInTheDocument(); unmount(); server.use( @@ -126,7 +129,9 @@ describe('OptimizerInsightRoute experiments', () => { ); renderInsight(); - expect(await screen.findByText('No experiments for this insight.')).toBeInTheDocument(); + expect( + await screen.findByText(ENTITY_EMPTY_STATES.insightExperiments.subheading) + ).toBeInTheDocument(); expect(screen.queryByText('Failed to load experiments')).not.toBeInTheDocument(); }); diff --git a/web/packages/studio/src/routes/optimizer/OptimizerRoute/index.tsx b/web/packages/studio/src/routes/optimizer/OptimizerRoute/index.tsx index 44c57d5f69..10bd19ee85 100644 --- a/web/packages/studio/src/routes/optimizer/OptimizerRoute/index.tsx +++ b/web/packages/studio/src/routes/optimizer/OptimizerRoute/index.tsx @@ -7,9 +7,9 @@ import { ROW_ACTIONS_COLUMN_SIZE, StudioDataView, } from '@nemo/common/src/components/DataView/StudioDataView'; +import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState'; import { ErrorPanel } from '@nemo/common/src/components/ErrorPanel'; import { RelativeTime } from '@nemo/common/src/components/RelativeTime'; -import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState'; import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState'; import { Flex, PageHeader, Stack, Tag, Text } from '@nvidia/foundations-react-core'; import { type InsightListItem, useOptimizerListInsights } from '@studio/api/optimizer'; @@ -19,7 +19,6 @@ import { useBreadcrumbs } from '@studio/providers/breadcrumbs/useBreadcrumbs'; import { insightStatusColor } from '@studio/routes/optimizer/insightStatus'; import { getOptimizerInsightRoute, getOptimizerRoute } from '@studio/routes/utils'; import { keepPreviousData } from '@tanstack/react-query'; -import { Lightbulb } from 'lucide-react'; import { type ComponentProps, type FC } from 'react'; import { useNavigate } from 'react-router'; @@ -154,13 +153,16 @@ export const OptimizerRoute: FC = () => { requestStatus: error ? 'error' : isFetching ? 'loading' : undefined, }, DataViewTableContent: { - renderEmptyState: () => ( - } - header="No insights yet" - emptyMessage="Run an optimizer analysis on an agent to surface insights here." - /> - ), + renderEmptyState: ({ hasFiltersApplied, hasSearchApplied }) => + hasFiltersApplied || hasSearchApplied ? ( + + ) : ( + + ), renderErrorState: () => (