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: () => (