Skip to content

Commit 2004314

Browse files
claudeironAiken2
authored andcommitted
fix(FR-2820): add Active/Inactive tabs and consolidate controls in Project page
Resolves #7254(FR-2820) - Add Active/Inactive tab split to the Project page BAICard, replacing the single "Project" tab. The active tab drives an `is_active` filter merged into the GraphQL query, and the redundant `is_active` BAIPropertyFilter entry is removed. - Consolidate per-row actions (edit, deactivate/restore, purge) from the standalone `controls` column into the `name` column using `BAINameActionCell`, matching the convention on other listing pages. - Add a Restore action (via Popconfirm, since restore is reversible) that calls `modify_group` with `is_active: true` for inactive projects. - Switch the purge confirmation from `modal.confirm` to `BAIConfirmModalWithInput` with the project name as the typed confirmation string, per the destructive-confirmation rule. - Remove the `is_active` and `Controls` columns from the table since tabs and BAINameActionCell handle those respectively. - Add i18n keys for Restore, RestoreProject, EditProject, etc. across all 21 locale files.
1 parent 624b2cd commit 2004314

23 files changed

Lines changed: 279 additions & 171 deletions

File tree

packages/backend.ai-ui/src/components/fragments/BAIProjectTable.tsx

Lines changed: 152 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,21 @@ import {
33
BAIProjectTableFragment$data,
44
BAIProjectTableFragment$key,
55
} from '../../__generated__/BAIProjectTableFragment.graphql';
6+
import { BAIProjectTableModifyMutation } from '../../__generated__/BAIProjectTableModifyMutation.graphql';
67
import { BAIProjectTablePurgeMutation } from '../../__generated__/BAIProjectTablePurgeMutation.graphql';
78
import { toLocalId } from '../../helper';
89
import { useErrorMessageResolver } from '../../hooks';
9-
import BAIButton from '../BAIButton';
1010
import BAIDeleteConfirmModal from '../BAIDeleteConfirmModal';
11-
import BAIFlex from '../BAIFlex';
1211
import BAIResourceNumberWithIcon from '../BAIResourceNumberWithIcon';
13-
import BAITag from '../BAITag';
1412
import BAIText from '../BAIText';
1513
import { BAIColumnsType, BAITable, BAITableProps } from '../Table';
14+
import BAINameActionCell from '../Table/BAINameActionCell';
1615
import AllowedVfolderHostsWithPermission from './BAIAllowedVfolderHostsWithPermission';
1716
import { DeleteFilled, SettingOutlined } from '@ant-design/icons';
18-
import { App, Popconfirm, Tag, theme } from 'antd';
17+
import { App, Tag } from 'antd';
1918
import dayjs from 'dayjs';
2019
import * as _ from 'lodash-es';
21-
import { BanIcon } from 'lucide-react';
20+
import { BanIcon, UndoIcon } from 'lucide-react';
2221
import { useState } from 'react';
2322
import { useTranslation } from 'react-i18next';
2423
import { graphql, useFragment, useMutation } from 'react-relay';
@@ -53,19 +52,22 @@ export interface BAIProjectTableProps extends Omit<
5352
) => void;
5453
onClickProjectEditButton: (project: Project) => void;
5554
updateFetchKey?: () => void;
55+
isActiveTab?: boolean;
5656
}
5757

5858
const BAIProjectTable = ({
5959
projectFragment,
6060
onChangeOrder,
6161
onClickProjectEditButton,
6262
updateFetchKey,
63+
isActiveTab = true,
6364
...tableProps
6465
}: BAIProjectTableProps) => {
66+
'use memo';
6567
const { t } = useTranslation();
66-
const { token } = theme.useToken();
6768
const { message } = App.useApp();
6869
const { getErrorMessage } = useErrorMessageResolver();
70+
6971
const [purgingProject, setPurgingProject] = useState<Project | null>(null);
7072

7173
const projects = useFragment<BAIProjectTableFragment$key>(
@@ -109,119 +111,158 @@ const BAIProjectTable = ({
109111
}
110112
`);
111113

114+
const [commitModifyGroup] = useMutation<BAIProjectTableModifyMutation>(
115+
graphql`
116+
mutation BAIProjectTableModifyMutation(
117+
$gid: UUID!
118+
$props: ModifyGroupInput!
119+
) {
120+
modify_group(gid: $gid, props: $props) {
121+
ok
122+
msg
123+
}
124+
}
125+
`,
126+
);
127+
112128
const columns: BAIColumnsType<Project> = [
113129
{
114130
key: 'name',
115131
title: t('comp:BAIProjectTable.Name'),
116132
dataIndex: 'name',
117133
fixed: 'left',
118134
sorter: isEnableSorter('name'),
119-
},
120-
{
121-
key: 'controls',
122-
title: t('comp:BAIProjectTable.Controls'),
123-
fixed: 'left',
124-
render: (value, record) => {
135+
render: (_value, record) => {
136+
const isModelStore = record.type === 'MODEL_STORE';
125137
return (
126-
<BAIFlex>
127-
<BAIButton
128-
type="text"
129-
icon={
130-
<SettingOutlined
131-
style={{
132-
color:
133-
_.get(record, 'type') === 'MODEL_STORE'
134-
? token.colorTextDisabled
135-
: token.colorInfo,
136-
}}
137-
/>
138-
}
139-
disabled={_.get(record, 'type') === 'MODEL_STORE'}
140-
onClick={() => {
141-
onClickProjectEditButton(value);
142-
}}
143-
/>
144-
<Popconfirm
145-
title={t('comp:BAIProjectTable.DeactivateProject')}
146-
description={value?.name}
147-
okButtonProps={{
148-
danger: true,
149-
loading: isInFlightCommitDeleteGroup,
150-
}}
151-
okText={t('comp:BAIProjectTable.Deactivate')}
152-
onConfirm={() => {
153-
if (!record?.row_id) {
154-
return;
155-
}
156-
commitDeleteGroup({
157-
variables: {
158-
gid: record.row_id,
159-
},
160-
onCompleted: (response, errors) => {
161-
if (errors && errors.length > 0) {
162-
errors.forEach((error) => {
163-
message.error(
164-
getErrorMessage(
165-
error,
166-
t('comp:BAIProjectTable.FailedToDeactivateProject'),
167-
),
168-
);
169-
});
170-
return;
171-
}
172-
if (response.delete_group?.ok) {
173-
message.success(
174-
t('comp:BAIProjectTable.ProjectDeactivated'),
175-
);
176-
updateFetchKey?.();
177-
} else {
178-
message.error(
179-
response.delete_group?.msg ||
180-
t('comp:BAIProjectTable.FailedToDeactivateProject'),
181-
);
182-
}
183-
},
184-
});
185-
}}
186-
>
187-
<BAIButton
188-
type="text"
189-
danger
190-
icon={
191-
<BanIcon
192-
size={token.fontSize}
193-
style={{
194-
color:
195-
_.get(record, 'type') === 'MODEL_STORE'
196-
? token.colorTextDisabled
197-
: undefined,
198-
}}
199-
/>
200-
}
201-
disabled={
202-
_.get(record, 'type') === 'MODEL_STORE' ||
203-
_.get(record, 'is_active') === false
204-
}
205-
/>
206-
</Popconfirm>
207-
<BAIButton
208-
type="text"
209-
icon={
210-
<DeleteFilled
211-
style={{
212-
color:
213-
_.get(record, 'type') === 'MODEL_STORE'
214-
? token.colorTextDisabled
215-
: token.colorError,
216-
}}
217-
/>
218-
}
219-
onClick={() => {
220-
setPurgingProject(record);
221-
}}
222-
disabled={_.get(record, 'type') === 'MODEL_STORE'}
223-
/>
224-
</BAIFlex>
138+
<BAINameActionCell
139+
title={record.name}
140+
showActions="always"
141+
actions={[
142+
{
143+
key: 'edit',
144+
title: t('comp:BAIProjectTable.EditProject'),
145+
icon: <SettingOutlined />,
146+
disabled: isModelStore,
147+
onClick: () => {
148+
onClickProjectEditButton(record);
149+
},
150+
},
151+
...(isActiveTab
152+
? [
153+
{
154+
key: 'deactivate',
155+
title: t('comp:BAIProjectTable.Deactivate'),
156+
icon: <BanIcon />,
157+
type: 'danger' as const,
158+
disabled: isModelStore,
159+
popConfirm: {
160+
title: t('comp:BAIProjectTable.DeactivateProject'),
161+
description: record.name,
162+
okButtonProps: {
163+
danger: true,
164+
loading: isInFlightCommitDeleteGroup,
165+
},
166+
okText: t('comp:BAIProjectTable.Deactivate'),
167+
onConfirm: () => {
168+
if (!record.row_id) return;
169+
commitDeleteGroup({
170+
variables: { gid: record.row_id },
171+
onCompleted: (response, errors) => {
172+
if (errors && errors.length > 0) {
173+
errors.forEach((error) => {
174+
message.error(
175+
getErrorMessage(
176+
error,
177+
t(
178+
'comp:BAIProjectTable.FailedToDeactivateProject',
179+
),
180+
),
181+
);
182+
});
183+
return;
184+
}
185+
if (response.delete_group?.ok) {
186+
message.success(
187+
t('comp:BAIProjectTable.ProjectDeactivated'),
188+
);
189+
updateFetchKey?.();
190+
} else {
191+
message.error(
192+
response.delete_group?.msg ||
193+
t(
194+
'comp:BAIProjectTable.FailedToDeactivateProject',
195+
),
196+
);
197+
}
198+
},
199+
});
200+
},
201+
},
202+
},
203+
]
204+
: [
205+
{
206+
key: 'restore',
207+
title: t('comp:BAIProjectTable.Restore'),
208+
icon: <UndoIcon />,
209+
disabled: isModelStore,
210+
popConfirm: {
211+
title: t('comp:BAIProjectTable.RestoreProject'),
212+
description: record.name,
213+
okText: t('comp:BAIProjectTable.Restore'),
214+
onConfirm: () => {
215+
if (!record.row_id) return;
216+
commitModifyGroup({
217+
variables: {
218+
gid: record.row_id,
219+
props: { is_active: true },
220+
},
221+
onCompleted: (response, errors) => {
222+
if (errors && errors.length > 0) {
223+
errors.forEach((error) => {
224+
message.error(
225+
getErrorMessage(
226+
error,
227+
t(
228+
'comp:BAIProjectTable.FailedToRestoreProject',
229+
),
230+
),
231+
);
232+
});
233+
return;
234+
}
235+
if (response.modify_group?.ok) {
236+
message.success(
237+
t('comp:BAIProjectTable.ProjectRestored'),
238+
);
239+
updateFetchKey?.();
240+
} else {
241+
message.error(
242+
response.modify_group?.msg ||
243+
t(
244+
'comp:BAIProjectTable.FailedToRestoreProject',
245+
),
246+
);
247+
}
248+
},
249+
});
250+
},
251+
},
252+
},
253+
]),
254+
{
255+
key: 'purge',
256+
title: t('comp:BAIProjectTable.Purge'),
257+
icon: <DeleteFilled />,
258+
type: 'danger' as const,
259+
disabled: isModelStore,
260+
onClick: () => {
261+
setPurgingProject(record);
262+
},
263+
},
264+
]}
265+
/>
225266
);
226267
},
227268
},
@@ -244,14 +285,6 @@ const BAIProjectTable = ({
244285
render: (value) => dayjs(value).format('lll'),
245286
sorter: isEnableSorter('created_at'),
246287
},
247-
{
248-
key: 'is_active',
249-
title: t('comp:BAIProjectTable.IsActive'),
250-
dataIndex: 'is_active',
251-
render: (value) =>
252-
value ? <BAITag color="green">true</BAITag> : <BAITag>false</BAITag>,
253-
sorter: isEnableSorter('is_active'),
254-
},
255288
{
256289
key: 'type',
257290
title: t('comp:BAIProjectTable.Type'),

packages/backend.ai-ui/src/locale/de.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,25 +213,28 @@
213213
"comp:BAIProjectTable": {
214214
"AreYouSureToPurgeProject": "Sind Sie sicher, dass Sie {{projectName}} endgültig löschen möchten? Diese Aktion ist unwiderruflich.",
215215
"ContainerRegistry": "Container-Registry",
216-
"Controls": "Steuerelemente",
217216
"CreatedAt": "Erstellt am",
218217
"Deactivate": "Deaktivieren",
219218
"DeactivateProject": "Projekt deaktivieren",
220219
"Description": "Beschreibung",
221220
"Domain": "Domäne",
221+
"EditProject": "Edit",
222222
"FailedToDeactivateProject": "Fehler beim Deaktivieren des Projekts.",
223223
"FailedToPurgeProject": "Fehler beim Bereinigen des Projekts.",
224+
"FailedToRestoreProject": "Failed to restore the project.",
224225
"IntegrationID": "Integrations‑ID",
225-
"IsActive": "Aktiv",
226226
"Name": "Name",
227227
"Project": "Projekt",
228228
"ProjectDeactivated": "Das Projekt wurde deaktiviert.",
229229
"ProjectID": "Projekt‑ID",
230230
"ProjectPurged": "Das Projekt wurde endgültig gelöscht.",
231+
"ProjectRestored": "The project has been restored.",
231232
"Purge": "Bereinigen",
232233
"PurgeProject": "Projekt bereinigen",
233234
"Registry": "Registry",
234235
"ResourcePolicy": "Ressourcenrichtlinie",
236+
"Restore": "Restore",
237+
"RestoreProject": "Restore Project",
235238
"ScalingGroups": "Ressourcengruppen",
236239
"StorageNodes": "Speicherknoten",
237240
"TotalResourceSlots": "Gesamtanzahl der Ressourcen-Slots",

packages/backend.ai-ui/src/locale/el.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,25 +213,28 @@
213213
"comp:BAIProjectTable": {
214214
"AreYouSureToPurgeProject": "Είστε βέβαιοι ότι θέλετε να διαγράψετε οριστικά το {{projectName}}? Αυτή η ενέργεια είναι μη αναστρέψιμη.",
215215
"ContainerRegistry": "Αποθετήριο κοντέινερ",
216-
"Controls": "Έλεγχοι",
217216
"CreatedAt": "Δημιουργήθηκε στις",
218217
"Deactivate": "Απενεργοποίηση",
219218
"DeactivateProject": "Απενεργοποίηση έργου",
220219
"Description": "Περιγραφή",
221220
"Domain": "Τομέας",
221+
"EditProject": "Edit",
222222
"FailedToDeactivateProject": "Η απενεργοποίηση του έργου απέτυχε.",
223223
"FailedToPurgeProject": "Αποτυχία εκκαθάρισης του έργου.",
224+
"FailedToRestoreProject": "Failed to restore the project.",
224225
"IntegrationID": "ID ενσωμάτωσης",
225-
"IsActive": "Ενεργό",
226226
"Name": "Όνομα",
227227
"Project": "Έργο",
228228
"ProjectDeactivated": "Το έργο έχει απενεργοποιηθεί.",
229229
"ProjectID": "ID έργου",
230230
"ProjectPurged": "Το projecth έχει διαγραφεί.",
231+
"ProjectRestored": "The project has been restored.",
231232
"Purge": "Εκκαθάριση",
232233
"PurgeProject": "Μόνιμη διαγραφή έργου",
233234
"Registry": "Μητρώο",
234235
"ResourcePolicy": "Πολιτική πόρων",
236+
"Restore": "Restore",
237+
"RestoreProject": "Restore Project",
235238
"ScalingGroups": "Ομάδες πόρων",
236239
"StorageNodes": "Κόμβοι Αποθήκευσης",
237240
"TotalResourceSlots": "Συνολικές θέσεις πόρων",

0 commit comments

Comments
 (0)