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
11 changes: 7 additions & 4 deletions web/packages/common/plugin-types/plugin.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

95 changes: 95 additions & 0 deletions web/packages/common/src/components/ActionsMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import {
Button,
Divider,
DropdownContent,
DropdownDividerItemEntry,
DropdownItem,
DropdownRoot,
DropdownTrigger,
Flex,
} from '@nvidia/foundations-react-core';
import { Sparkles } from 'lucide-react';
import React, { type FC, type ReactElement, type ReactNode } from 'react';

export interface ActionMenuItem {
label: string;
onSelect: () => void;
icon?: ReactElement<{ size?: number; fill?: string; className?: string }>;
disabled?: boolean;
danger?: boolean;
divider?: Omit<DropdownDividerItemEntry, 'kind'>;
}

interface ActionsMenuProps {
actions: ActionMenuItem[];
/** Trigger label. Omit for an icon-only trigger. */
label?: ReactNode;
/** Trigger icon. Defaults to a sparkles icon when a label is shown. */
icon?: ReactElement;
kind?: 'primary' | 'secondary' | 'tertiary';
disabled?: boolean;
ariaLabel?: string;
'data-testid'?: string;
}

/*
* ActionsMenu renders a dropdown of actions behind a single trigger button. Use it to consolidate
* page-level actions instead of lining up individual buttons. Pass a `label` for a labeled
* "Actions" trigger, or omit it for an icon-only trigger.
*/
export const ActionsMenu: FC<ActionsMenuProps> = ({
actions,
label,
icon,
kind = 'tertiary',
disabled,
ariaLabel = 'Open actions menu',
'data-testid': testId = 'actions-menu',
}) => {
const triggerIcon = icon ?? (label ? <Sparkles /> : undefined);

return (
<DropdownRoot>
<DropdownTrigger
asChild
data-testid={`${testId}-trigger`}
onClick={(e) => e.stopPropagation()}
showChevron={false}
>
<Button kind={kind} disabled={disabled} aria-label={ariaLabel}>
{triggerIcon}
{label}
</Button>
</DropdownTrigger>
<DropdownContent
align="end"
side="bottom"
data-testid={`${testId}-content`}
className="w-[180px] min-w-[180px]"
>
{actions.map((action, key) => (
<React.Fragment key={`action-${key}`}>
<DropdownItem
data-testid={`${testId}-item`}
disabled={action.disabled}
onClick={(e) => {
e.stopPropagation();
action.onSelect();
}}
danger={action.danger}
>
<Flex align="center" gap="density-sm" className="pr-6">
{action.icon && React.cloneElement(action.icon, { size: 20, fill: 'solid' })}
{action.label}
</Flex>
</DropdownItem>
{action.divider && <Divider width={action.divider.width} />}
</React.Fragment>
))}
</DropdownContent>
</DropdownRoot>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ interface FormFields {

type SubmitButtonColor = NonNullable<ComponentProps<typeof LoadingButton>['color']>;

export interface ConfirmationModalProps extends Pick<FormModalProps, 'open' | 'onClose'> {
export interface ConfirmationModalProps extends Pick<
FormModalProps,
'open' | 'onClose' | 'cancelButtonText'
> {
/** Return true when the action succeeded (shows success toast); false shows error toast. */
onConfirm: () => boolean | Promise<boolean>;
title: string;
Expand Down Expand Up @@ -50,6 +53,7 @@ export const ConfirmationModal: FC<ConfirmationModalProps> = ({
successText = 'Done.',
errorText = 'Something went wrong. Please try again.',
submitButtonText = 'Confirm',
cancelButtonText,
submitButtonColor,
suppressResultToasts = false,
onNotify,
Expand Down Expand Up @@ -100,6 +104,7 @@ export const ConfirmationModal: FC<ConfirmationModalProps> = ({
open={open}
title={title}
submitButtonText={submitButtonText}
cancelButtonText={cancelButtonText}
disabled={isPending}
submitDisabled={simpleConfirm ? false : !isValid}
loading={isPending}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,75 +1,24 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import {
Button,
Divider,
DropdownContent,
DropdownDividerItemEntry,
DropdownItem,
DropdownRoot,
DropdownTrigger,
Flex,
} from '@nvidia/foundations-react-core';
import { ActionsMenu, type ActionMenuItem } from '@nemo/common/src/components/ActionsMenu';
import { EllipsisVertical } from 'lucide-react';
import React, { FC } from 'react';
import type { FC } from 'react';

export interface QuickActionItem {
label: string;
onSelect: () => void;
icon?: React.ReactElement<{ size?: number; fill?: string; className?: string }>;
disabled?: boolean;
danger?: boolean;
divider?: Omit<DropdownDividerItemEntry, 'kind'>;
}
export type QuickActionItem = ActionMenuItem;

interface QuickActionsMenuProps {
actions: QuickActionItem[];
}

/*
* QuickActionsMenu is a wrapper around Kaizen's Menu component that standardizes
* the alignment of menus and other sizes/behaviours.
* QuickActionsMenu is the icon-only preset of ActionsMenu, used for row- and card-level menus.
*/
export const QuickActionsMenuRoot: FC<QuickActionsMenuProps> = ({ actions }) => {
return (
<DropdownRoot>
<DropdownTrigger
asChild
data-testid="quick-actions-menu-trigger"
onClick={(e) => e.stopPropagation()}
showChevron={false}
>
<Button kind="tertiary" aria-label="Open quick actions menu">
<EllipsisVertical />
</Button>
</DropdownTrigger>
<DropdownContent
align="end"
side="bottom"
data-testid="quick-actions-menu-content"
className="w-[180px] min-w-[180px]"
>
{actions.map((action, key) => (
<React.Fragment key={`action-${key}`}>
<DropdownItem
data-testid="quick-actions-menu-item"
disabled={action.disabled}
onClick={(e) => {
e.stopPropagation();
action.onSelect();
}}
danger={action.danger}
>
<Flex align="center" gap="density-sm" className="pr-6">
{action.icon && React.cloneElement(action.icon, { size: 20, fill: 'solid' })}
{action.label}
</Flex>
</DropdownItem>
{action.divider && <Divider width={action.divider.width} />}
</React.Fragment>
))}
</DropdownContent>
</DropdownRoot>
);
};
export const QuickActionsMenuRoot: FC<QuickActionsMenuProps> = ({ actions }) => (
<ActionsMenu
actions={actions}
icon={<EllipsisVertical />}
ariaLabel="Open quick actions menu"
data-testid="quick-actions-menu"
/>
);
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import {
type QuickActionItem,
QuickActionsMenuRoot,
} from '@nemo/common/src/components/QuickActionsMenu/QuickActionsMenuRoot';
import { type ActionMenuItem, ActionsMenu } from '@nemo/common/src/components/ActionsMenu';
import { QuickActionsMenuRoot } from '@nemo/common/src/components/QuickActionsMenu/QuickActionsMenuRoot';
import { CJobCancellableStatuses } from '@nemo/common/src/constants/query';
import {
getDataDesignerListCreateJobsQueryKey,
Expand All @@ -21,6 +19,10 @@ import { useNavigate } from 'react-router';

interface DataDesignerJobActionsMenuProps {
job: DataDesignerJob;
/** 'icon' renders the ellipsis trigger (tables); 'labeled' renders an "Actions" button (details page). */
variant?: 'icon' | 'labeled';
/** Page-specific actions listed above the job-level ones, e.g. Transform and Split. */
additionalActions?: ActionMenuItem[];
/** Include a "View details" entry. Used in the table row, omitted on the details page. */
includeViewDetails?: boolean;
/** When provided, adds a "View config" entry that invokes this callback. */
Expand All @@ -32,12 +34,14 @@ interface DataDesignerJobActionsMenuProps {
}

/**
* Quick-actions menu for a single Data Designer job: View details (optional), Clone, Cancel
* (when cancellable), and Delete. Shared by the jobs table and the job details page so both
* Actions menu for a single Data Designer job: any caller-supplied actions, View details
* (optional), Clone, Cancel (when cancellable), and Delete. Shared by the jobs table and the job details page so both
* expose the same actions. Owns its own delete modal; cancel errors are surfaced via callback.
*/
export const DataDesignerJobActionsMenu: FC<DataDesignerJobActionsMenuProps> = ({
job,
variant = 'icon',
additionalActions = [],
includeViewDetails = false,
onViewConfig,
onDeleted,
Expand Down Expand Up @@ -82,7 +86,8 @@ export const DataDesignerJobActionsMenu: FC<DataDesignerJobActionsMenuProps> = (

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

const actions: QuickActionItem[] = [
const actions: ActionMenuItem[] = [
...additionalActions,
...(includeViewDetails
? [
{
Expand Down Expand Up @@ -124,7 +129,11 @@ export const DataDesignerJobActionsMenu: FC<DataDesignerJobActionsMenuProps> = (

return (
<>
<QuickActionsMenuRoot actions={actions} />
{variant === 'labeled' ? (
<ActionsMenu actions={actions} label="Actions" />
) : (
<QuickActionsMenuRoot actions={actions} />
)}
{showDeleteModal && (
<DeleteJobModal
jobs={[job]}
Expand Down
Loading