Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
fileset: FilesetOutput;
onNavigateToDetails?: (fileset: FilesetOutput) => void;
onFilesetUpdated?: (fileset: FilesetOutput) => void;
onFilesetDeleted?: (fileset: FilesetOutput) => void;
}

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

const handleDeleteFileset = async (): Promise<boolean> => {
try {
if (!fileset?.workspace || !fileset?.name) {
throw new Error('Fileset workspace or name is undefined');
}
await deleteFileset({ workspace: fileset.workspace, name: fileset.name });
onFilesetDeleted?.(fileset);
return true;
} catch (error) {
console.error('Failed to delete fileset:', error);
return false;
}
};

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

const handleFilesetUpdated = (updatedFileset: FilesetOutput) => {
onFilesetUpdated?.(updatedFileset);
handleModalClose();
};

return (
<>
<DropdownRoot>
<DropdownTrigger asChild showChevron={false} data-testid="quick-actions-menu-trigger">
<Button kind="tertiary" aria-label="Open fileset actions menu">
<EllipsisVertical />
</Button>
</DropdownTrigger>
<DropdownContent align="end" className="w-[180px]">
{onNavigateToDetails && (
<DropdownItem onClick={() => onNavigateToDetails(fileset)}>View</DropdownItem>
)}
<DropdownItem onClick={() => setModalOpen('edit')}>Edit</DropdownItem>
<DropdownItem onClick={() => setModalOpen('delete')} danger>
Delete
</DropdownItem>
</DropdownContent>
</DropdownRoot>

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

{modalOpen === 'delete' && (
<DeleteConfirmationModal
open={modalOpen === 'delete'}
onClose={handleModalClose}
onDelete={handleDeleteFileset}
title={`Delete Fileset: ${fileset.name}`}
confirmationText={fileset.name ?? getEntityReference(fileset)}
simpleConfirm
successText="Fileset deleted successfully"
/>
)}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
type FilesetFileOutput,
type FilesetOutput,
} from '@nemo/sdk/generated/platform/schema';
import { Stack, Text } from '@nvidia/foundations-react-core';
import { Stack } from '@nvidia/foundations-react-core';
import { useDatasetFileContent } from '@studio/api/datasets/useDatasetFileContent';
import { DatasetSamplePanel } from '@studio/routes/FilesetDetailRoute/FilesetCard/DatasetSamplePanel';
import { ReadmeBody } from '@studio/routes/FilesetDetailRoute/FilesetCard/ReadmeBody';
Expand Down Expand Up @@ -62,11 +62,6 @@ export const FilesetCard: FC<FilesetCardProps> = ({
>
<div className="lg:col-span-2">
<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 min-h-full 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
fileset={fileset}
onFilesetDeleted={() => navigate(getWorkspaceFilesetsRoute(workspace))}
/>
</Flex>
</Stack>
}
slotDescription={description}
/>
<TabsRoot className="flex flex-col" value={currentTab} onValueChange={handleTabChange}>
<TabsList>
<TabsTrigger value={FilesetDetailTab.Card}>{cardLabel}</TabsTrigger>
Expand Down
Loading