diff --git a/packages/backend.ai-ui/src/components/BAIConfirmModalWithInput.stories.tsx b/packages/backend.ai-ui/src/components/BAIConfirmModalWithInput.stories.tsx deleted file mode 100644 index ba09d73613..0000000000 --- a/packages/backend.ai-ui/src/components/BAIConfirmModalWithInput.stories.tsx +++ /dev/null @@ -1,169 +0,0 @@ -'use memo'; - -import BAIButton from './BAIButton'; -import BAIConfirmModalWithInput from './BAIConfirmModalWithInput'; -import BAIText from './BAIText'; -import { DeleteFilled } from '@ant-design/icons'; -import type { Meta, StoryObj } from '@storybook/react-vite'; -import { theme } from 'antd'; -import { useState } from 'react'; - -const meta: Meta = { - title: 'Modal/BAIConfirmModalWithInput', - component: BAIConfirmModalWithInput, - tags: ['autodocs'], - parameters: { - layout: 'padded', - docs: { - description: { - component: ` -**BAIConfirmModalWithInput** is a specialized modal component for dangerous actions that require explicit user confirmation. It extends [BAIModal](/?path=/docs/components-baimodal--docs) and enforces users to type a confirmation text before allowing the action. - -## Key Features -- **Type-to-Confirm**: OK button is disabled until user types the exact confirmation text -- **Danger Mode**: OK button is styled as danger (red) by default -- **Form Validation**: Built-in form validation with error messages -- **Auto-Reset**: Form resets on modal close/cancel - -## BAI-Specific Props -| Prop | Type | Required | Description | -|------|------|----------|-------------| -| \`confirmText\` | \`string\` | Yes | The exact text user must type to enable confirmation | -| \`content\` | \`ReactNode\` | Yes | Content displayed above the input field | -| \`title\` | \`ReactNode\` | Yes | Modal title | -| \`icon\` | \`ReactNode\` | No | Custom icon (defaults to warning icon) | -| \`inputLabel\` | \`ReactNode\` | No | Label displayed above the input field | -| \`inputProps\` | \`InputProps\` | No | Additional props for the input field | -| \`okButtonProps\` | \`Omit\` | No | OK button props (disabled/danger are controlled internally) | - -For inherited props, refer to [BAIModal](/?path=/docs/components-baimodal--docs). - `, - }, - }, - }, - argTypes: { - confirmText: { - control: { type: 'text' }, - description: 'The exact text user must type to enable confirmation', - }, - title: { - control: { type: 'text' }, - description: 'Modal title', - }, - inputLabel: { - control: { type: 'text' }, - description: 'Label displayed above the input field', - }, - }, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - name: 'Basic', - args: { - confirmText: 'DELETE', - title: 'Delete Project', - inputLabel: ( - <> - Please type DELETE to confirm. - - ), - }, - render: (args) => { - const [open, setOpen] = useState(false); - return ( - <> - setOpen(true)}> - Open Modal - - - This action cannot be undone. This will - permanently delete the project and all its associated data. - - } - onOk={() => setOpen(false)} - onCancel={() => setOpen(false)} - /> - - ); - }, -}; - -export const CustomIcon: Story = { - args: { - confirmText: 'DELETE', - title: 'Delete Files', - inputLabel: ( - <> - Type DELETE to confirm. - - ), - }, - render: (args) => { - const [open, setOpen] = useState(false); - const { token } = theme.useToken(); - return ( - <> - setOpen(true)}> - Open Modal - - - } - content={ - - You are about to delete 15 files. This action cannot be undone. - - } - onOk={() => setOpen(false)} - onCancel={() => setOpen(false)} - /> - - ); - }, -}; - -export const WithInputProps: Story = { - args: { - confirmText: 'CONFIRM', - title: 'Dangerous Action', - inputLabel: ( - <> - Type CONFIRM to proceed. - - ), - }, - render: (args) => { - const [open, setOpen] = useState(false); - return ( - <> - setOpen(true)}> - Open Modal - - - This is a high-risk operation that will affect multiple resources. - - } - inputProps={{ - placeholder: `Type ${args.confirmText} here...`, - }} - onOk={() => setOpen(false)} - onCancel={() => setOpen(false)} - /> - - ); - }, -}; diff --git a/packages/backend.ai-ui/src/components/BAIConfirmModalWithInput.tsx b/packages/backend.ai-ui/src/components/BAIConfirmModalWithInput.tsx deleted file mode 100644 index c2f6a97599..0000000000 --- a/packages/backend.ai-ui/src/components/BAIConfirmModalWithInput.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import BAIFlex from './BAIFlex'; -import BAIModal, { type BAIModalProps } from './BAIModal'; -import { ExclamationCircleFilled } from '@ant-design/icons'; -import { Form, Input, theme, Typography, type InputProps } from 'antd'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -const { Text } = Typography; - -export interface BAIConfirmModalWithInputProps extends Omit< - BAIModalProps, - 'icon' | 'okButtonProps' -> { - confirmText: string; - content: React.ReactNode; - title: React.ReactNode; - icon?: React.ReactNode; - inputLabel?: React.ReactNode; - okButtonProps?: Omit; - inputProps?: InputProps; -} - -const BAIConfirmModalWithInput: React.FC = ({ - confirmText, - title, - content, - icon, - onOk, - onCancel, - inputProps, - inputLabel, - ...modalProps -}) => { - const { t } = useTranslation(); - const { token } = theme.useToken(); - const [form] = Form.useForm(); - const typedText = Form.useWatch('confirmText', form); - - return ( - - - {icon ?? ( - - )} - {title} - - - } - onOk={(e) => { - form.resetFields(); - onOk?.(e); - }} - onCancel={(e) => { - form.resetFields(); - onCancel?.(e); - }} - {...modalProps} - okButtonProps={{ - ...modalProps.okButtonProps, - disabled: confirmText !== typedText, - danger: true, - }} - > - - {content} -
- { - if (value === confirmText) { - return Promise.resolve(); - } - return Promise.reject(); - }, - }, - ]} - > - { - e.preventDefault(); - e.stopPropagation(); - inputProps?.onClick?.(e); - }} - /> - -
-
-
- ); -}; - -export default BAIConfirmModalWithInput; diff --git a/packages/backend.ai-ui/src/components/BAIDeleteConfirmModal.stories.tsx b/packages/backend.ai-ui/src/components/BAIDeleteConfirmModal.stories.tsx index 862ec2fbbf..63ea1a1abf 100644 --- a/packages/backend.ai-ui/src/components/BAIDeleteConfirmModal.stories.tsx +++ b/packages/backend.ai-ui/src/components/BAIDeleteConfirmModal.stories.tsx @@ -27,6 +27,7 @@ const meta: Meta = { ## Key Features - Accepts \`React.ReactNode\` for item labels (icons, tags, custom rendering) - Scrollable item list for multi-item selections +- \`target\` prop produces a resource-type-aware default description ("Are you sure you want to permanently delete {target}?") - \`extraContent\` slot for domain-specific additions (checkboxes, warnings) - Built on \`BAIModal\` `, @@ -93,6 +94,39 @@ export const SingleItemWithInput: Story = { }, }; +export const WithTarget: Story = { + parameters: { + docs: { + description: { + story: + 'Resource-typed deletion using the `target` prop. The default description becomes "Are you sure you want to permanently delete {target}?", surfacing the resource type (e.g. "Resource Preset", "Resource Policy") in the dialog copy. Typically paired with `requireConfirmInput` for irreversible deletes.', + }, + }, + }, + render: () => { + const [open, setOpen] = useState(false); + const itemName = 'gpu-large-preset'; + return ( + <> + } onClick={() => setOpen(true)}> + Delete Resource Preset + + setOpen(false)} + onCancel={() => setOpen(false)} + /> + + ); + }, +}; + export const MultipleItems: Story = { parameters: { docs: { diff --git a/packages/backend.ai-ui/src/components/BAIDeleteConfirmModal.tsx b/packages/backend.ai-ui/src/components/BAIDeleteConfirmModal.tsx index b38fc7fdfb..87ada64261 100644 --- a/packages/backend.ai-ui/src/components/BAIDeleteConfirmModal.tsx +++ b/packages/backend.ai-ui/src/components/BAIDeleteConfirmModal.tsx @@ -23,8 +23,13 @@ export interface BAIDeleteConfirmModalProps extends Omit< items: BAIDeleteConfirmModalItem[]; /** Custom modal title. Defaults to "Delete" / "Delete N items". */ title?: React.ReactNode; - /** Description shown above the item list. Defaults to "Are you sure you want to delete?" */ + /** Description shown above the item list. If omitted, falls back to a `target`-based or generic default. */ description?: React.ReactNode; + /** + * Resource type label (e.g. "Credential", "Project"). When provided and `description` is not, + * the default description becomes "Are you sure you want to permanently delete {target}?". + */ + target?: React.ReactNode; /** Force text-input confirmation even for a single item. Default: false */ requireConfirmInput?: boolean; /** @@ -54,6 +59,7 @@ const BAIDeleteConfirmModal: React.FC = ({ items, title, description, + target, requireConfirmInput = false, confirmText: confirmTextProp, inputLabel, @@ -83,15 +89,20 @@ const BAIDeleteConfirmModal: React.FC = ({ }) : t('comp:BAIDeleteConfirmModal.DeleteItem')); - const resolvedDescription = - description ?? t('comp:BAIDeleteConfirmModal.AreYouSureToDelete'); - const resolvedConfirmText = confirmTextProp ?? (items.length === 1 ? (extractTextFromNode(items[0]?.label) ?? t('general.button.Delete')) : t('general.button.Delete')); + const resolvedDescription = + description ?? + (target + ? t('comp:BAIDeleteConfirmModal.AreYouSureToPermanentlyDeleteTarget', { + target, + }) + : t('comp:BAIDeleteConfirmModal.AreYouSureToDelete')); + const resolvedOkText = okText ?? t('general.button.Delete'); const resolvedInputLabel = inputLabel ?? ( @@ -159,7 +170,7 @@ const BAIDeleteConfirmModal: React.FC = ({ }} > - {resolvedDescription} + {resolvedDescription && {resolvedDescription}} {items.length > 1 && itemListContent}
= ({ onCancel={onCancel} > - {resolvedDescription} + {resolvedDescription && {resolvedDescription}} {itemListContent} {t('comp:BAIDeleteConfirmModal.CannotBeUndone')} diff --git a/packages/backend.ai-ui/src/components/baiClient/FileExplorer/DeleteSelectedItemsModal.tsx b/packages/backend.ai-ui/src/components/baiClient/FileExplorer/DeleteSelectedItemsModal.tsx index 7a480ee040..20dcd7f0f1 100644 --- a/packages/backend.ai-ui/src/components/baiClient/FileExplorer/DeleteSelectedItemsModal.tsx +++ b/packages/backend.ai-ui/src/components/baiClient/FileExplorer/DeleteSelectedItemsModal.tsx @@ -1,10 +1,9 @@ -import BAIConfirmModalWithInput from '../../BAIConfirmModalWithInput'; -import BAIFlex from '../../BAIFlex'; +import BAIDeleteConfirmModal from '../../BAIDeleteConfirmModal'; import useConnectedBAIClient from '../../provider/BAIClientProvider/hooks/useConnectedBAIClient'; import { VFolderFile } from '../../provider/BAIClientProvider/types'; import { FolderInfoContext } from './BAIFileExplorer'; import { useMutation } from '@tanstack/react-query'; -import { Alert, App, theme, Typography, type ModalProps } from 'antd'; +import { App, type ModalProps } from 'antd'; import * as _ from 'lodash-es'; import { use } from 'react'; import { useTranslation } from 'react-i18next'; @@ -26,7 +25,6 @@ const DeleteSelectedItemsModal: React.FC = ({ ...modalProps }) => { const { t } = useTranslation(); - const { token } = theme.useToken(); const { message } = App.useApp(); const { currentPath, targetVFolderId } = use(FolderInfoContext); const baiClient = useConnectedBAIClient(); @@ -79,59 +77,18 @@ const DeleteSelectedItemsModal: React.FC = ({ }; return ( - ({ + key: item.name + item.created, + label: item.name, + }))} + requireConfirmInput + okButtonProps={{ loading: deleteFilesMutation.isPending }} inputProps={{ disabled: deleteFilesMutation.isPending }} onOk={handleDelete} onCancel={() => onRequestClose(false)} - confirmText={ - selectedFiles.length > 1 - ? t('general.button.Delete') - : selectedFiles[0]?.name - } - content={ - - - {selectedFiles.length > 1 ? ( - - - {t('general.modal.ItemSelectedWithCount', { - count: selectedFiles.length, - })} - - - {_.map(selectedFiles, (item) => ( - - {item.name} - - ))} - - - {t('comp:FileExplorer.TypeDeleteToConfirm')} - - - ) : ( - - - {t('comp:FileExplorer.TypeFolderNameToDelete')} - - {selectedFiles[0]?.name} - - - - )} - - } {...modalProps} /> ); diff --git a/packages/backend.ai-ui/src/components/fragments/BAIProjectTable.tsx b/packages/backend.ai-ui/src/components/fragments/BAIProjectTable.tsx index 9e3b96178c..86ea7757d9 100644 --- a/packages/backend.ai-ui/src/components/fragments/BAIProjectTable.tsx +++ b/packages/backend.ai-ui/src/components/fragments/BAIProjectTable.tsx @@ -7,6 +7,7 @@ import { BAIProjectTablePurgeMutation } from '../../__generated__/BAIProjectTabl import { toLocalId } from '../../helper'; import { useErrorMessageResolver } from '../../hooks'; import BAIButton from '../BAIButton'; +import BAIDeleteConfirmModal from '../BAIDeleteConfirmModal'; import BAIFlex from '../BAIFlex'; import BAIResourceNumberWithIcon from '../BAIResourceNumberWithIcon'; import BAITag from '../BAITag'; @@ -18,6 +19,7 @@ import { App, Popconfirm, Tag, theme } from 'antd'; import dayjs from 'dayjs'; import * as _ from 'lodash-es'; import { BanIcon } from 'lucide-react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { graphql, useFragment, useMutation } from 'react-relay'; @@ -62,8 +64,9 @@ const BAIProjectTable = ({ }: BAIProjectTableProps) => { const { t } = useTranslation(); const { token } = theme.useToken(); - const { modal, message } = App.useApp(); + const { message } = App.useApp(); const { getErrorMessage } = useErrorMessageResolver(); + const [purgingProject, setPurgingProject] = useState(null); const projects = useFragment( graphql` @@ -219,50 +222,7 @@ const BAIProjectTable = ({ /> } onClick={() => { - modal.confirm({ - title: t('comp:BAIProjectTable.PurgeProject'), - content: t('comp:BAIProjectTable.AreYouSureToPurgeProject', { - projectName: value?.name, - }), - okButtonProps: { - danger: true, - }, - okText: t('comp:BAIProjectTable.Purge'), - onOk: () => { - if (!record?.row_id) { - return; - } - commitPurgeGroup({ - variables: { - gid: record.row_id, - }, - onCompleted: (response, errors) => { - if (errors && errors.length > 0) { - errors.forEach((error) => { - message.error( - getErrorMessage( - error, - t('comp:BAIProjectTable.FailedToPurgeProject'), - ), - ); - }); - return; - } - if (response.purge_group?.ok) { - message.success( - t('comp:BAIProjectTable.ProjectPurged'), - ); - updateFetchKey?.(); - } else { - message.error( - response.purge_group?.msg || - t('comp:BAIProjectTable.FailedToPurgeProject'), - ); - } - }, - }); - }, - }); + setPurgingProject(record); }} disabled={_.get(record, 'type') === 'MODEL_STORE'} /> @@ -386,18 +346,70 @@ const BAIProjectTable = ({ }, ]; return ( - - scroll={{ x: 'max-content' }} - {...tableProps} - rowKey={(record) => record.id} - dataSource={projects} - columns={columns} - onChangeOrder={(order) => { - onChangeOrder?.( - (order as (typeof availableProjectSorterValues)[number]) || null, - ); - }} - /> + <> + + scroll={{ x: 'max-content' }} + {...tableProps} + rowKey={(record) => record.id} + dataSource={projects} + columns={columns} + onChangeOrder={(order) => { + onChangeOrder?.( + (order as (typeof availableProjectSorterValues)[number]) || null, + ); + }} + /> + { + if (!purgingProject?.row_id) return; + return new Promise((resolve) => { + commitPurgeGroup({ + variables: { gid: purgingProject.row_id! }, + onCompleted: (response, errors) => { + if (errors && errors.length > 0) { + errors.forEach((error) => { + message.error( + getErrorMessage( + error, + t('comp:BAIProjectTable.FailedToPurgeProject'), + ), + ); + }); + resolve(); + return; + } + if (response.purge_group?.ok) { + message.success(t('comp:BAIProjectTable.ProjectPurged')); + setPurgingProject(null); + updateFetchKey?.(); + } else { + message.error( + response.purge_group?.msg || + t('comp:BAIProjectTable.FailedToPurgeProject'), + ); + } + resolve(); + }, + }); + }); + }} + onCancel={() => setPurgingProject(null)} + /> + ); }; diff --git a/packages/backend.ai-ui/src/components/index.ts b/packages/backend.ai-ui/src/components/index.ts index dc54f4737a..d4c7f41d3b 100644 --- a/packages/backend.ai-ui/src/components/index.ts +++ b/packages/backend.ai-ui/src/components/index.ts @@ -54,8 +54,6 @@ export type { } from './BAINotificationItem'; export { default as BAIModal } from './BAIModal'; export type { BAIModalProps } from './BAIModal'; -export { default as BAIConfirmModalWithInput } from './BAIConfirmModalWithInput'; -export type { BAIConfirmModalWithInputProps } from './BAIConfirmModalWithInput'; export { default as BAIDeleteConfirmModal } from './BAIDeleteConfirmModal'; export type { BAIDeleteConfirmModalProps, diff --git a/packages/backend.ai-ui/src/locale/de.json b/packages/backend.ai-ui/src/locale/de.json index f4105542f5..fb8349d4ef 100644 --- a/packages/backend.ai-ui/src/locale/de.json +++ b/packages/backend.ai-ui/src/locale/de.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "Sind Sie sicher, dass Sie löschen möchten?", + "AreYouSureToPermanentlyDeleteTarget": "Möchten Sie {{target}} wirklich dauerhaft löschen?", "CannotBeUndone": "Diese Aktion kann nicht rückgängig gemacht werden.", "DeleteItem": "Löschen", "DeleteNItems": "{{count}} Elemente löschen", @@ -402,8 +403,10 @@ }, "general": { "DeselectAll": "Alle abwählen", + "File": "Datei", "NSelected": "{{count}} ausgewählt", "Optional": "Optional", + "Project": "Projekt", "TotalItems": "Total {{total}} Elemente", "button": { "Cancel": "Stornieren", diff --git a/packages/backend.ai-ui/src/locale/el.json b/packages/backend.ai-ui/src/locale/el.json index 9b59af141e..199fbcfb1f 100644 --- a/packages/backend.ai-ui/src/locale/el.json +++ b/packages/backend.ai-ui/src/locale/el.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "Είστε σίγουροι ότι θέλετε να διαγράψετε;", + "AreYouSureToPermanentlyDeleteTarget": "Είστε βέβαιοι ότι θέλετε να διαγράψετε οριστικά {{target}};", "CannotBeUndone": "Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", "DeleteItem": "Διαγραφή", "DeleteNItems": "Διαγραφή {{count}} στοιχείων", @@ -402,8 +403,10 @@ }, "general": { "DeselectAll": "Αποεπιλογή όλων", + "File": "Αρχείο", "NSelected": "{{count}} Επιλεγμένη", "Optional": "Προαιρετικό", + "Project": "Έργο", "TotalItems": "Σύνολο {{total}} στοιχεία", "button": { "Cancel": "Ματαίωση", diff --git a/packages/backend.ai-ui/src/locale/en.json b/packages/backend.ai-ui/src/locale/en.json index 4853d636b1..bd297f9e47 100644 --- a/packages/backend.ai-ui/src/locale/en.json +++ b/packages/backend.ai-ui/src/locale/en.json @@ -96,6 +96,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "Are you sure you want to delete?", + "AreYouSureToPermanentlyDeleteTarget": "Are you sure you want to permanently delete {{target}}?", "CannotBeUndone": "This action cannot be undone.", "DeleteItem": "Delete", "DeleteNItems": "Delete {{count}} items", @@ -408,8 +409,10 @@ }, "general": { "DeselectAll": "Deselect all", + "File": "File", "NSelected": "{{count}} selected", "Optional": "Optional", + "Project": "Project", "TotalItems": "Total {{total}} items", "button": { "Cancel": "Cancel", diff --git a/packages/backend.ai-ui/src/locale/es.json b/packages/backend.ai-ui/src/locale/es.json index 27ddf30957..a008689557 100644 --- a/packages/backend.ai-ui/src/locale/es.json +++ b/packages/backend.ai-ui/src/locale/es.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "¿Está seguro de que desea eliminar?", + "AreYouSureToPermanentlyDeleteTarget": "¿Está seguro de que desea eliminar permanentemente {{target}}?", "CannotBeUndone": "Esta acción no se puede deshacer.", "DeleteItem": "Eliminar", "DeleteNItems": "Eliminar {{count}} elementos", @@ -402,8 +403,10 @@ }, "general": { "DeselectAll": "Deseleccionar todo", + "File": "Archivo", "NSelected": "{{count}} seleccionado", "Optional": "Opcional", + "Project": "Proyecto", "TotalItems": "Total {{total}} elementos", "button": { "Cancel": "Cancelar", diff --git a/packages/backend.ai-ui/src/locale/fi.json b/packages/backend.ai-ui/src/locale/fi.json index 08bfc25e40..241fea0488 100644 --- a/packages/backend.ai-ui/src/locale/fi.json +++ b/packages/backend.ai-ui/src/locale/fi.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "Haluatko varmasti poistaa?", + "AreYouSureToPermanentlyDeleteTarget": "Haluatko varmasti poistaa {{target}} pysyvästi?", "CannotBeUndone": "Tätä toimintoa ei voi peruuttaa.", "DeleteItem": "Poista", "DeleteNItems": "Poista {{count}} kohdetta", @@ -402,8 +403,10 @@ }, "general": { "DeselectAll": "Poista kaikki valinnat", + "File": "Tiedosto", "NSelected": "{{count}} valittu", "Optional": "Valinnainen", + "Project": "Projekti", "TotalItems": "Yhteensä {{total}} kohteet", "button": { "Cancel": "Peruuttaa", diff --git a/packages/backend.ai-ui/src/locale/fr.json b/packages/backend.ai-ui/src/locale/fr.json index 3dd09ffd77..25631df953 100644 --- a/packages/backend.ai-ui/src/locale/fr.json +++ b/packages/backend.ai-ui/src/locale/fr.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "Êtes-vous sûr de vouloir supprimer ?", + "AreYouSureToPermanentlyDeleteTarget": "Voulez-vous vraiment supprimer définitivement {{target}} ?", "CannotBeUndone": "Cette action est irréversible.", "DeleteItem": "Supprimer", "DeleteNItems": "Supprimer {{count}} éléments", @@ -402,8 +403,10 @@ }, "general": { "DeselectAll": "Tout désélectionner", + "File": "Fichier", "NSelected": "{{count}} sélectionné", "Optional": "Facultatif", + "Project": "Projet", "TotalItems": "Éléments totaux {{total}}", "button": { "Cancel": "Annuler", diff --git a/packages/backend.ai-ui/src/locale/id.json b/packages/backend.ai-ui/src/locale/id.json index 55cc3bcc92..206960540f 100644 --- a/packages/backend.ai-ui/src/locale/id.json +++ b/packages/backend.ai-ui/src/locale/id.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "Apakah Anda yakin ingin menghapus?", + "AreYouSureToPermanentlyDeleteTarget": "Apakah Anda yakin ingin menghapus {{target}} secara permanen?", "CannotBeUndone": "Tindakan ini tidak dapat dibatalkan.", "DeleteItem": "Hapus", "DeleteNItems": "Hapus {{count}} item", @@ -402,8 +403,10 @@ }, "general": { "DeselectAll": "Batalkan semua pilihan", + "File": "Berkas", "NSelected": "{{count}} dipilih", "Optional": "Opsional", + "Project": "Proyek", "TotalItems": "Total {{total}} item", "button": { "Cancel": "Membatalkan", diff --git a/packages/backend.ai-ui/src/locale/it.json b/packages/backend.ai-ui/src/locale/it.json index 1eccb1c523..a7b7b03139 100644 --- a/packages/backend.ai-ui/src/locale/it.json +++ b/packages/backend.ai-ui/src/locale/it.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "Sei sicuro di voler eliminare?", + "AreYouSureToPermanentlyDeleteTarget": "Sei sicuro di voler eliminare definitivamente {{target}}?", "CannotBeUndone": "Questa azione non può essere annullata.", "DeleteItem": "Elimina", "DeleteNItems": "Elimina {{count}} elementi", @@ -402,8 +403,10 @@ }, "general": { "DeselectAll": "Deseleziona tutto", + "File": "File", "NSelected": "{{count}} selezionato", "Optional": "Opzionale", + "Project": "Progetto", "TotalItems": "Totali {{total}} elementi", "button": { "Cancel": "Cancellare", diff --git a/packages/backend.ai-ui/src/locale/ja.json b/packages/backend.ai-ui/src/locale/ja.json index 6da8342ef4..cb3010598a 100644 --- a/packages/backend.ai-ui/src/locale/ja.json +++ b/packages/backend.ai-ui/src/locale/ja.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "本当に削除しますか?", + "AreYouSureToPermanentlyDeleteTarget": "{{target}}を完全に削除してもよろしいですか?", "CannotBeUndone": "この操作は元に戻せません。", "DeleteItem": "削除", "DeleteNItems": "{{count}} 件を削除", @@ -402,8 +403,10 @@ }, "general": { "DeselectAll": "すべて選択解除", + "File": "ファイル", "NSelected": "{{count}}選択", "Optional": "任意", + "Project": "プロジェクト", "TotalItems": "Total {{total}}アイテム", "button": { "Cancel": "キャンセル", diff --git a/packages/backend.ai-ui/src/locale/ko.json b/packages/backend.ai-ui/src/locale/ko.json index 5fe3933db1..9a3a948508 100644 --- a/packages/backend.ai-ui/src/locale/ko.json +++ b/packages/backend.ai-ui/src/locale/ko.json @@ -96,10 +96,11 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "정말 삭제하시겠습니까?", + "AreYouSureToPermanentlyDeleteTarget": "{{target}}을(를) 영구 삭제하시겠습니까?", "CannotBeUndone": "이 작업은 되돌릴 수 없습니다.", "DeleteItem": "삭제", "DeleteNItems": "{{count}}개 항목 삭제", - "TypeToConfirm": "삭제를 위해 {{confirmText}}을(를) 입력하세요." + "TypeToConfirm": "{{confirmText}}을(를) 입력해주세요." }, "comp:BAIDeploymentSelect": { "SelectDeployment": "배포를 선택해주세요" @@ -405,8 +406,10 @@ }, "general": { "DeselectAll": "선택 취소", + "File": "파일", "NSelected": "{{count}}개 선택됨", "Optional": "선택", + "Project": "프로젝트", "TotalItems": "총 {{total}} 항목", "button": { "Cancel": "취소", diff --git a/packages/backend.ai-ui/src/locale/mn.json b/packages/backend.ai-ui/src/locale/mn.json index 2fa80336e6..bf7c202069 100644 --- a/packages/backend.ai-ui/src/locale/mn.json +++ b/packages/backend.ai-ui/src/locale/mn.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "Та устгахдаа итгэлтэй байна уу?", + "AreYouSureToPermanentlyDeleteTarget": "{{target}}-г бүрмөсөн устгахдаа итгэлтэй байна уу?", "CannotBeUndone": "Энэ үйлдлийг буцаах боломжгүй.", "DeleteItem": "Устгах", "DeleteNItems": "{{count}} зүйл устгах", @@ -402,8 +403,10 @@ }, "general": { "DeselectAll": "Бүгдийг сонголтоос хасах", + "File": "Файл", "NSelected": "{{count}} сонгогдсон", "Optional": "Заавал биш", + "Project": "Төсөл", "TotalItems": "Нийт {{total}} зүйлүүд", "button": { "Cancel": "Цуаах", diff --git a/packages/backend.ai-ui/src/locale/ms.json b/packages/backend.ai-ui/src/locale/ms.json index 0466f59e07..fcd2c5936e 100644 --- a/packages/backend.ai-ui/src/locale/ms.json +++ b/packages/backend.ai-ui/src/locale/ms.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "Adakah anda pasti mahu memadam?", + "AreYouSureToPermanentlyDeleteTarget": "Adakah anda pasti mahu memadam {{target}} secara kekal?", "CannotBeUndone": "Tindakan ini tidak dapat dibatalkan.", "DeleteItem": "Padam", "DeleteNItems": "Padam {{count}} item", @@ -402,8 +403,10 @@ }, "general": { "DeselectAll": "Nyahpilih semua", + "File": "Fail", "NSelected": "{{count}} dipilih", "Optional": "Pilihan", + "Project": "Projek", "TotalItems": "Jumlah {{total}} item", "button": { "Cancel": "Batalkan", diff --git a/packages/backend.ai-ui/src/locale/pl.json b/packages/backend.ai-ui/src/locale/pl.json index 8ad1efb8e2..de43ceeac4 100644 --- a/packages/backend.ai-ui/src/locale/pl.json +++ b/packages/backend.ai-ui/src/locale/pl.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "Czy na pewno chcesz usunąć?", + "AreYouSureToPermanentlyDeleteTarget": "Czy na pewno chcesz trwale usunąć {{target}}?", "CannotBeUndone": "Tej operacji nie można cofnąć.", "DeleteItem": "Usuń", "DeleteNItems": "Usuń {{count}} elementów", @@ -402,8 +403,10 @@ }, "general": { "DeselectAll": "Odznacz wszystko", + "File": "Plik", "NSelected": "{{count}}", "Optional": "Opcjonalne", + "Project": "Projekt", "TotalItems": "Total {{total}}", "button": { "Cancel": "Anulować", diff --git a/packages/backend.ai-ui/src/locale/pt-BR.json b/packages/backend.ai-ui/src/locale/pt-BR.json index d956c7a544..5082d02e75 100644 --- a/packages/backend.ai-ui/src/locale/pt-BR.json +++ b/packages/backend.ai-ui/src/locale/pt-BR.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "Tem certeza que deseja excluir?", + "AreYouSureToPermanentlyDeleteTarget": "Tem certeza de que deseja excluir permanentemente {{target}}?", "CannotBeUndone": "Esta ação não pode ser desfeita.", "DeleteItem": "Excluir", "DeleteNItems": "Excluir {{count}} itens", @@ -399,8 +400,10 @@ }, "general": { "DeselectAll": "Desmarcar tudo", + "File": "Arquivo", "NSelected": "{{count}} selecionado", "Optional": "Opcional", + "Project": "Projeto", "TotalItems": "Total {{total}} itens", "button": { "Cancel": "Cancelar", diff --git a/packages/backend.ai-ui/src/locale/pt.json b/packages/backend.ai-ui/src/locale/pt.json index a90dac8d96..303e7bbeb0 100644 --- a/packages/backend.ai-ui/src/locale/pt.json +++ b/packages/backend.ai-ui/src/locale/pt.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "Tem a certeza de que pretende eliminar?", + "AreYouSureToPermanentlyDeleteTarget": "Tem certeza de que deseja excluir permanentemente {{target}}?", "CannotBeUndone": "Esta ação não pode ser anulada.", "DeleteItem": "Eliminar", "DeleteNItems": "Eliminar {{count}} itens", @@ -402,8 +403,10 @@ }, "general": { "DeselectAll": "Desmarcar tudo", + "File": "Arquivo", "NSelected": "{{count}} selecionado", "Optional": "Opcional", + "Project": "Projeto", "TotalItems": "Total {{total}} itens", "button": { "Cancel": "Cancelar", diff --git a/packages/backend.ai-ui/src/locale/ru.json b/packages/backend.ai-ui/src/locale/ru.json index 96c3ebb287..68d24634b8 100644 --- a/packages/backend.ai-ui/src/locale/ru.json +++ b/packages/backend.ai-ui/src/locale/ru.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "Вы уверены, что хотите удалить?", + "AreYouSureToPermanentlyDeleteTarget": "Вы действительно хотите окончательно удалить {{target}}?", "CannotBeUndone": "Это действие нельзя отменить.", "DeleteItem": "Удалить", "DeleteNItems": "Удалить {{count}} элементов", @@ -402,8 +403,10 @@ }, "general": { "DeselectAll": "Снять выделение", + "File": "Файл", "NSelected": "{{count}} выбрал", "Optional": "Необязательно", + "Project": "Проект", "TotalItems": "Total {{total}} элементы", "button": { "Cancel": "Отмена", diff --git a/packages/backend.ai-ui/src/locale/th.json b/packages/backend.ai-ui/src/locale/th.json index f251d82086..ba2c1c1bc4 100644 --- a/packages/backend.ai-ui/src/locale/th.json +++ b/packages/backend.ai-ui/src/locale/th.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "คุณแน่ใจหรือไม่ว่าต้องการลบ?", + "AreYouSureToPermanentlyDeleteTarget": "คุณแน่ใจหรือไม่ว่าต้องการลบ {{target}} อย่างถาวร?", "CannotBeUndone": "การกระทำนี้ไม่สามารถยกเลิกได้", "DeleteItem": "ลบ", "DeleteNItems": "ลบ {{count}} รายการ", @@ -402,8 +403,10 @@ }, "general": { "DeselectAll": "ยกเลิกการเลือกทั้งหมด", + "File": "ไฟล์", "NSelected": "{{count}} เลือก", "Optional": "ไม่บังคับ", + "Project": "โครงการ", "TotalItems": "รวม {{total}} รายการ", "button": { "Cancel": "ยกเลิก", diff --git a/packages/backend.ai-ui/src/locale/tr.json b/packages/backend.ai-ui/src/locale/tr.json index 68513ca74f..737a25ec53 100644 --- a/packages/backend.ai-ui/src/locale/tr.json +++ b/packages/backend.ai-ui/src/locale/tr.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "Silmek istediğinizden emin misiniz?", + "AreYouSureToPermanentlyDeleteTarget": "{{target}} kalıcı olarak silinsin mi?", "CannotBeUndone": "Bu işlem geri alınamaz.", "DeleteItem": "Sil", "DeleteNItems": "{{count}} öğeyi sil", @@ -397,8 +398,10 @@ }, "general": { "DeselectAll": "Tüm seçimi kaldır", + "File": "Dosya", "NSelected": "{{count}} seçildi", "Optional": "İsteğe bağlı", + "Project": "Proje", "TotalItems": "Total {{total}} öğeleri", "button": { "Cancel": "İptal etmek", diff --git a/packages/backend.ai-ui/src/locale/vi.json b/packages/backend.ai-ui/src/locale/vi.json index c3aec5e900..e00d8fbe90 100644 --- a/packages/backend.ai-ui/src/locale/vi.json +++ b/packages/backend.ai-ui/src/locale/vi.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "Bạn có chắc chắn muốn xóa không?", + "AreYouSureToPermanentlyDeleteTarget": "Bạn có chắc muốn xóa vĩnh viễn {{target}} không?", "CannotBeUndone": "Hành động này không thể hoàn tác.", "DeleteItem": "Xóa", "DeleteNItems": "Xóa {{count}} mục", @@ -402,8 +403,10 @@ }, "general": { "DeselectAll": "Bỏ chọn tất cả", + "File": "Tệp", "NSelected": "{{count}} Đã chọn", "Optional": "Không bắt buộc", + "Project": "Dự án", "TotalItems": "Tổng số {{total}} các mục", "button": { "Cancel": "Hủy bỏ", diff --git a/packages/backend.ai-ui/src/locale/zh-CN.json b/packages/backend.ai-ui/src/locale/zh-CN.json index c6b54eb9cf..4fc854b399 100644 --- a/packages/backend.ai-ui/src/locale/zh-CN.json +++ b/packages/backend.ai-ui/src/locale/zh-CN.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "您确定要删除吗?", + "AreYouSureToPermanentlyDeleteTarget": "确定要永久删除{{target}}吗?", "CannotBeUndone": "此操作无法撤销。", "DeleteItem": "删除", "DeleteNItems": "删除 {{count}} 个项目", @@ -402,8 +403,10 @@ }, "general": { "DeselectAll": "取消全选", + "File": "文件", "NSelected": "{{count}}选择", "Optional": "可选", + "Project": "项目", "TotalItems": "总计{{total}}项目", "button": { "Cancel": "取消", diff --git a/packages/backend.ai-ui/src/locale/zh-TW.json b/packages/backend.ai-ui/src/locale/zh-TW.json index c5902d56ef..2c10e993df 100644 --- a/packages/backend.ai-ui/src/locale/zh-TW.json +++ b/packages/backend.ai-ui/src/locale/zh-TW.json @@ -93,6 +93,7 @@ }, "comp:BAIDeleteConfirmModal": { "AreYouSureToDelete": "您確定要刪除嗎?", + "AreYouSureToPermanentlyDeleteTarget": "確定要永久刪除{{target}}嗎?", "CannotBeUndone": "此操作無法復原。", "DeleteItem": "刪除", "DeleteNItems": "刪除 {{count}} 個項目", @@ -402,8 +403,10 @@ }, "general": { "DeselectAll": "取消全選", + "File": "檔案", "NSelected": "{{count}}選擇", "Optional": "選填", + "Project": "專案", "TotalItems": "總計{{total}}項目", "button": { "Cancel": "取消", diff --git a/react/src/components/AutoScalingRuleList.tsx b/react/src/components/AutoScalingRuleList.tsx index f5a0ca6111..102bcd5414 100644 --- a/react/src/components/AutoScalingRuleList.tsx +++ b/react/src/components/AutoScalingRuleList.tsx @@ -16,6 +16,7 @@ import QuestionIconWithTooltip from './QuestionIconWithTooltip'; import { DeleteFilled, PlusOutlined, SettingOutlined } from '@ant-design/icons'; import { App, Button, Tag, Tooltip, Typography } from 'antd'; import { + BAIDeleteConfirmModal, BAIFetchKeyButton, BAIFlex, BAIGraphQLPropertyFilter, @@ -401,12 +402,16 @@ const AutoScalingRuleList: React.FC = ({ }) => { 'use memo'; const { t } = useTranslation(); - const { message, modal } = App.useApp(); + const { message } = App.useApp(); const [isPendingRefetch, startRefetchTransition] = useTransition(); const [fetchKey, updateFetchKey] = useFetchKey(); const [editingRuleId, setEditingRuleId] = useState(null); const [isOpenEditorModal, setIsOpenEditorModal] = useState(false); + const [deletingRule, setDeletingRule] = useState<{ + id: string; + metricName: string; + } | null>(null); useImperativeHandle( ref, @@ -553,29 +558,7 @@ const AutoScalingRuleList: React.FC = ({ }; const handleDeleteRule = (ruleId: string, metricName: string) => { - modal.confirm({ - title: t('dialog.warning.CannotBeUndone'), - content: t('autoScalingRule.ConfirmDeleteAutoScalingRule', { - autoScalingRule: metricName, - }), - okText: t('button.Delete'), - okButtonProps: { danger: true }, - onOk: () => - commitDeleteMutation({ input: { id: toLocalId(ruleId) } }) - .then(() => { - handleRefetch(); - message.success({ - key: 'autoscaling-rule-deleted', - content: t('autoScalingRule.SuccessfullyDeleted'), - }); - }) - .catch((error) => { - const errors = Array.isArray(error) ? error : [error]; - for (const err of errors) { - message.error(err?.message || t('dialog.ErrorOccurred')); - } - }), - }); + setDeletingRule({ id: ruleId, metricName }); }; return ( @@ -687,6 +670,47 @@ const AutoScalingRuleList: React.FC = ({ }} /> + { + if (!deletingRule) return; + return commitDeleteMutation({ + input: { id: toLocalId(deletingRule.id) }, + }) + .then(() => { + setDeletingRule(null); + handleRefetch(); + message.success({ + key: 'autoscaling-rule-deleted', + content: t('autoScalingRule.SuccessfullyDeleted'), + }); + }) + .catch((error) => { + const errors = Array.isArray(error) ? error : [error]; + for (const err of errors) { + message.error(err?.message || t('dialog.ErrorOccurred')); + } + }); + }} + onCancel={() => setDeletingRule(null)} + /> ); }; diff --git a/react/src/components/AutoScalingRuleListLegacy.tsx b/react/src/components/AutoScalingRuleListLegacy.tsx index f6c60aceb8..75669c9f4b 100644 --- a/react/src/components/AutoScalingRuleListLegacy.tsx +++ b/react/src/components/AutoScalingRuleListLegacy.tsx @@ -8,17 +8,14 @@ import AutoScalingRuleEditorModalLegacy, { COMPARATOR_LABELS, } from './AutoScalingRuleEditorModalLegacy'; import { DeleteFilled, PlusOutlined, SettingOutlined } from '@ant-design/icons'; +import { App, Button, Tag, Tooltip, Typography, theme } from 'antd'; import { - App, - Button, - Card, - Popconfirm, - Tag, - Tooltip, - Typography, - theme, -} from 'antd'; -import { BAIFlex, BAITable, BAIUnmountAfterClose } from 'backend.ai-ui'; + BAICard, + BAIDeleteConfirmModal, + BAIFlex, + BAITable, + BAIUnmountAfterClose, +} from 'backend.ai-ui'; import { default as dayjs } from 'dayjs'; import * as _ from 'lodash-es'; import { CircleArrowDownIcon, CircleArrowUpIcon } from 'lucide-react'; @@ -102,6 +99,9 @@ const AutoScalingRuleListLegacy: React.FC = ({ useState(null); const [isOpenAutoScalingRuleModal, setIsOpenAutoScalingRuleModal] = useState(false); + const [deletingRule, setDeletingRule] = useState | null>( + null, + ); const [ commitDeleteAutoScalingRuleMutation, @@ -117,7 +117,7 @@ const AutoScalingRuleListLegacy: React.FC = ({ return ( <> - = ({ {t('modelService.AddRules')} } + styles={{ body: { paddingTop: 0 } }} > = ({ } }} /> - { - if (autoScalingRules) { - commitDeleteAutoScalingRuleMutation({ - variables: { - id: row?.id as string, - }, - onCompleted: (res, errors) => { - if ( - !res?.delete_endpoint_auto_scaling_rule_node?.ok - ) { - message.error( - res?.delete_endpoint_auto_scaling_rule_node - ?.msg, - ); - } else if (errors && errors.length > 0) { - const errorMsgList = _.map( - errors, - (error) => - error.message || t('dialog.ErrorOccurred'), - ); - for (const error of errorMsgList) { - message.error(error); +