Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions web/packages/studio/src/components/FilesetActionMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { getEntityReference } from '@nemo/common/src/namedEntity';
import { useFilesDeleteFileset } from '@nemo/sdk/generated/platform/api';
import type { FilesetOutput } from '@nemo/sdk/generated/platform/schema';
import {
Button,
DropdownContent,
DropdownItem,
DropdownRoot,
DropdownTrigger,
} from '@nvidia/foundations-react-core';
import { invalidateDatasetCaches } from '@studio/api/datasets/invalidateDatasetCaches';
import { DatasetCreateModal } from '@studio/components/DatasetCreateModal';
import { DatasetCreateModalMode } from '@studio/components/DatasetCreateModal/constants';
import { DeleteConfirmationModal } from '@studio/components/DeleteConfirmationModal';
import { EllipsisVertical } from 'lucide-react';
import { type FC, useState } from 'react';

export interface FilesetActionMenuProps {
dataset: FilesetOutput;
onNavigateToDetails?: (dataset: FilesetOutput) => void;
onDatasetUpdated?: (dataset: FilesetOutput) => void;
onDatasetDeleted?: (dataset: FilesetOutput) => void;
}

export const FilesetActionMenu: FC<FilesetActionMenuProps> = ({
dataset,
onNavigateToDetails,
onDatasetUpdated,
onDatasetDeleted,
}) => {
const [modalOpen, setModalOpen] = useState<'edit' | 'delete' | undefined>(undefined);
const { mutateAsync: deleteDataset } = useFilesDeleteFileset({
mutation: {
onSuccess: (_data, variables) => {
invalidateDatasetCaches(variables.workspace, variables.name, ['list']);
},
},
});

const handleDeleteDataset = async (): Promise<boolean> => {
try {
if (!dataset?.workspace || !dataset?.name) {
throw new Error('Dataset workspace or name is undefined');
}
await deleteDataset({ workspace: dataset.workspace, name: dataset.name });
onDatasetDeleted?.(dataset);
return true;
} catch (error) {
console.error('Failed to delete dataset:', error);
return false;
}
};

const handleModalClose = () => {
setModalOpen(undefined);
};

const handleDatasetUpdated = (updatedDataset: FilesetOutput) => {
onDatasetUpdated?.(updatedDataset);
handleModalClose();
};

return (
<>
<DropdownRoot>
<DropdownTrigger asChild showChevron={false} data-testid="quick-actions-menu-trigger">
<Button kind="tertiary" aria-label="Open dataset actions menu">
<EllipsisVertical />
Comment thread
steramae-nvidia marked this conversation as resolved.
Outdated
</Button>
</DropdownTrigger>
<DropdownContent align="end" className="w-[180px]">
{onNavigateToDetails && (
<DropdownItem onClick={() => onNavigateToDetails(dataset)}>View</DropdownItem>
)}
<DropdownItem onClick={() => setModalOpen('edit')}>Edit</DropdownItem>
<DropdownItem onClick={() => setModalOpen('delete')} danger>
Delete
</DropdownItem>
</DropdownContent>
</DropdownRoot>

{modalOpen === 'edit' && (
<DatasetCreateModal
dataset={dataset}
mode={DatasetCreateModalMode.Edit}
onClose={handleModalClose}
onDatasetUpdated={handleDatasetUpdated}
open={modalOpen === 'edit'}
/>
)}

{modalOpen === 'delete' && (
<DeleteConfirmationModal
open={modalOpen === 'delete'}
onClose={handleModalClose}
onDelete={handleDeleteDataset}
title={`Delete Dataset: ${dataset.name}`}
confirmationText={dataset.name ?? getEntityReference(dataset)}
simpleConfirm
successText="Dataset deleted successfully"
/>
)}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import type { FilesetFileOutput, FilesetOutput } from '@nemo/sdk/generated/platform/schema';
import { Grid, GridItem, Stack, Text } from '@nvidia/foundations-react-core';
import { Grid, GridItem, Stack } from '@nvidia/foundations-react-core';
import { useDatasetFileContent } from '@studio/api/datasets/useDatasetFileContent';
import { ReadmeBody } from '@studio/routes/FilesetDetailRoute/FilesetCard/ReadmeBody';
import { FilesetMetadataPanel } from '@studio/routes/FilesetDetailRoute/FilesetMetadataPanel';
Expand Down Expand Up @@ -52,19 +52,14 @@ export const FilesetCard: FC<FilesetCardProps> = ({
<Grid
cols={{ base: 1, xl: 12 }}
gap="density-xl"
className="w-full items-start"
className="max-h-full w-full items-start"
data-testid="fileset-card"
>
<GridItem
cols={{ lg: 8 }}
className="min-w-0 overflow-hidden rounded-lg border border-base bg-surface-raised p-density-xl"
className="min-w-0 max-h-full overflow-auto rounded-lg border border-base bg-surface-raised p-density-xl"
>
<Stack gap="density-md">
{fileset.description && (
<Text kind="body/regular/md" data-testid="fileset-card-description">
{fileset.description}
</Text>
)}
<ReadmeBody
isFilesError={isFilesError}
readmePath={readmePath}
Expand Down
34 changes: 28 additions & 6 deletions web/packages/studio/src/routes/FilesetDetailRoute/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
Text,
} from '@nvidia/foundations-react-core';
import { AccessibleTitle } from '@studio/components/AccessibleTitle';
import { FilesetActionMenu } from '@studio/components/FilesetActionMenu';
import { Loading } from '@studio/components/Layouts/Loading';
import { ROUTE_PARAMS } from '@studio/constants/routes';
import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath';
Expand All @@ -32,9 +33,11 @@ import { getModelSource, isRootReadme } from '@studio/routes/FilesetDetailRoute/
import { getWorkspaceFilesetsRoute } from '@studio/routes/utils';
import { useRequiredPathParams } from '@studio/util/hooks/useRequiredPathParams';
import type { FC } from 'react';
import { useNavigate } from 'react-router-dom';

export const FilesetDetailRoute: FC = () => {
const workspace = useWorkspaceFromPath();
const navigate = useNavigate();
const { filesetName } = useRequiredPathParams([ROUTE_PARAMS.filesetName]);
const filesetId = getEntityReference({ namespace: workspace, name: filesetName });

Expand Down Expand Up @@ -105,17 +108,36 @@ export const FilesetDetailRoute: FC = () => {
// The source line (HF/NGC origin) is meaningful for model/generic filesets;
// datasets don't surface it.
const source = isDataset ? undefined : getModelSource(fileset);
const description = source ? (
<Flex gap="density-sm" align="center">
{creatorToIcon(source.creatorSlug, { className: 'w-4 h-4 flex-shrink-0' })}
<span>{source.path}</span>
</Flex>
const descriptionText = <Text kind="body/regular/md">{fileset.description}</Text>;
const description = isDataset ? (
descriptionText
) : source ? (
<Stack gap="1">
<Flex gap="1" align="center">
{creatorToIcon(source.creatorSlug, { className: 'w-4 h-4 flex-shrink-0' })}
<span>{source.path}</span>
</Flex>
{descriptionText}
</Stack>
) : undefined;

return (
<AccessibleTitle title={`${typeLabel} ${filesetName}`}>
<Stack className="w-full h-full min-h-0 p-density-2xl" gap="density-xl">
<PageHeader slotHeading={filesetName} slotDescription={description} />
<PageHeader
slotHeading={
<Stack gap="density-sm">
<Flex gap="density-sm" align="center" justify="between">
<span>{filesetName}</span>
<FilesetActionMenu
dataset={fileset}
onDatasetDeleted={() => navigate(getWorkspaceFilesetsRoute(workspace))}
/>
</Flex>
</Stack>
}
slotDescription={description}
/>
<TabsRoot
className="flex-1 min-h-0 flex flex-col"
value={currentTab}
Expand Down
Loading