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
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { CJobCancellableStatuses } from '@nemo/common/src/constants/query';
import {
getAnonymizerListRunJobsQueryKey,
useAnonymizerCancelRunJob,
} from '@nemo/sdk/generated/anonymizer/api';
import type { RunJob as AnonymizerJob } from '@nemo/sdk/generated/anonymizer/schema';
import { DeleteJobModal } from '@studio/components/dataViews/AnonymizerJobsDataView/DeleteJobModal';
import {
type QuickActionItem,
QuickActionsMenuRoot,
} from '@studio/components/QuickActionsMenu/QuickActionsMenuRoot';
import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath';
import { getAnonymizerJobRoute } from '@studio/routes/utils';
import { useQueryClient } from '@tanstack/react-query';
import { type FC, useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';

interface AnonymizerJobActionsMenuProps {
job: AnonymizerJob;
includeViewDetails?: boolean;
onDeleted?: () => void;
onCancelError?: (message: string | undefined) => void;
}

export const AnonymizerJobActionsMenu: FC<AnonymizerJobActionsMenuProps> = ({
job,
includeViewDetails = false,
onDeleted,
onCancelError,
}) => {
const navigate = useNavigate();
const workspace = useWorkspaceFromPath();
const queryClient = useQueryClient();
const [showDeleteModal, setShowDeleteModal] = useState(false);

const cancelJobMutation = useAnonymizerCancelRunJob({
mutation: {
onSuccess: () => {
queryClient.resetQueries({
queryKey: getAnonymizerListRunJobsQueryKey(workspace),
});
onCancelError?.(undefined);
},
onError: (error) => {
onCancelError?.(error instanceof Error ? error.message : 'Failed to cancel job');
},
},
});

const { mutateAsync: cancelJob } = cancelJobMutation;

const handleCancel = useCallback(async () => {
if (!job.workspace || !job.name) return;
try {
onCancelError?.(undefined);
await cancelJob({ workspace: job.workspace, name: job.name });
} catch {
// Error is surfaced via the mutation's onError callback.
}
}, [job.workspace, job.name, cancelJob, onCancelError]);

const isCancellable = job.status != null && CJobCancellableStatuses.includes(job.status);

const actions = useMemo<QuickActionItem[]>(
() => [
...(includeViewDetails
? [
{
label: 'View details',
onSelect: () => {
if (job.name) {
navigate(getAnonymizerJobRoute(workspace, job.name));
}
},
},
]
: []),
...(isCancellable
? [
{
label: 'Cancel',
onSelect: handleCancel,
},
]
: []),
{
label: 'Delete',
onSelect: () => setShowDeleteModal(true),
danger: true,
},
],
[includeViewDetails, isCancellable, handleCancel, navigate, workspace, job.name]
);

return (
<>
<QuickActionsMenuRoot actions={actions} />
{showDeleteModal && (
<DeleteJobModal
jobs={[job]}
onClose={() => setShowDeleteModal(false)}
onDeleted={onDeleted}
/>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import {
getAnonymizerListRunJobsQueryKey,
useAnonymizerDeleteRunJob,
} from '@nemo/sdk/generated/anonymizer/api';
import type { RunJob as AnonymizerJob } from '@nemo/sdk/generated/anonymizer/schema';
import { BulkDeleteModal } from '@studio/components/BulkDeleteModal';
import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath';
import { useQueryClient } from '@tanstack/react-query';
import type { FC } from 'react';

interface DeleteJobModalProps {
jobs: AnonymizerJob[];
onClose: () => void;
onDeleted?: () => void;
}

export const DeleteJobModal: FC<DeleteJobModalProps> = ({ jobs, onClose, onDeleted }) => {
const queryClient = useQueryClient();
const workspace = useWorkspaceFromPath();

const deleteJobMutation = useAnonymizerDeleteRunJob({
mutation: {
onSuccess: () =>
queryClient.resetQueries({
queryKey: getAnonymizerListRunJobsQueryKey(workspace),
}),
},
});

const handleDelete = async (jobsToDelete: AnonymizerJob[]) => {
const invalid = jobsToDelete.filter((job) => !job.workspace || !job.name);
if (invalid.length > 0) {
throw new Error(
`Cannot delete ${invalid.length} job${invalid.length !== 1 ? 's' : ''}: missing workspace or name.`
);
}
await Promise.all(
jobsToDelete.map(async (job) => {
try {
await deleteJobMutation.mutateAsync({ workspace: job.workspace!, name: job.name });
} catch (error) {
throw new Error(
`Failed to delete job "${job.name}": ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
})
);
onDeleted?.();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

return (
<BulkDeleteModal
items={jobs}
open={jobs.length > 0}
onDelete={handleDelete}
title={(count) => `Delete ${count} Anonymizer Job${count !== 1 ? 's' : ''}`}
onClose={onClose}
/>
);
};
Loading
Loading