From 3294919e1bd5afd656350dfb132822ab5c4e2b7d Mon Sep 17 00:00:00 2001 From: Jong Eun Lee Date: Mon, 27 May 2024 14:59:19 +0800 Subject: [PATCH 01/18] fix: do not apply preset when disabled presets (#2429) ### What changed? After this PR, the resource allocation form items do not apply when the preset is disabled. (bug report from [Teams](https://teams.microsoft.com/l/message/19:14c484402d874dafb15806d093b95a82@thread.skype/1716362564395?tenantId=13c6a44d-9b52-4b9e-aa34-0513ee7131f2&groupId=74ae2c4d-ec4d-4fdf-b2c2-f5041d1e8631&parentMessageId=1716359409725&teamName=devops&channelName=Frontend&createdTime=1716362564395)) ### How to test? - Create two model services with different CPU numbers. - Click the modify button and check if the CPU number is preserved or not. ### Why make this change? This change was made to improve the user experience and meet the requirements of the project. --- **Checklist:** (if applicable) - [ ] Mention to the original issue - [ ] Documentation - [ ] Minium required manager version - [ ] Specific setting for review (eg., KB link, endpoint or how to setup) - [ ] Minimum requirements to check during review - [ ] Test case(s) to demonstrate the difference of before/after --- react/src/components/ResourceAllocationFormItems.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react/src/components/ResourceAllocationFormItems.tsx b/react/src/components/ResourceAllocationFormItems.tsx index e547e5ae19..37848c2e69 100644 --- a/react/src/components/ResourceAllocationFormItems.tsx +++ b/react/src/components/ResourceAllocationFormItems.tsx @@ -218,7 +218,7 @@ const ResourceAllocationFormItems: React.FC< allocatablePresetNames.includes(form.getFieldValue('allocationPreset')) ) { // if the current preset is available in the current resource group, do nothing. - } else if (allocatablePresetNames[0]) { + } else if (enableResourcePresets && allocatablePresetNames[0]) { const autoSelectedPreset = _.sortBy(allocatablePresetNames, 'name')[0]; form.setFieldsValue({ allocationPreset: autoSelectedPreset, From 95620b7afcbd3e69f922eee91241d590d4963f5c Mon Sep 17 00:00:00 2001 From: Jong Eun Lee Date: Mon, 27 May 2024 17:24:00 +0800 Subject: [PATCH 02/18] fix: `InputNumber` component does not update the value correctly at first (#2428) ### What changed? This PR fixes the bug related to first update of `InputNumber` in Form.Item. - InputNumberWithSlider - DynamicUnitInputNumberWithSlider - DynamicInputNumber ### How to test? - Change the CPU number in the Neo session launcher. - Refresh the browser. - If the CPU number is preserved after the refresh, it works fine. --- **Checklist:** (if applicable) - [ ] Mention to the original issue - [ ] Documentation - [ ] Minium required manager version - [ ] Specific setting for review (eg., KB link, endpoint or how to setup) - [ ] Minimum requirements to check during review - [ ] Test case(s) to demonstrate the difference of before/after --- react/src/components/DynamicStepInputNumber.tsx | 13 ++++++++++++- .../DynamicUnitInputNumberWithSlider.tsx | 14 +++++++++++++- react/src/components/InputNumberWithSlider.tsx | 12 ++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/react/src/components/DynamicStepInputNumber.tsx b/react/src/components/DynamicStepInputNumber.tsx index 0b487c34e6..fa2a52439e 100644 --- a/react/src/components/DynamicStepInputNumber.tsx +++ b/react/src/components/DynamicStepInputNumber.tsx @@ -1,7 +1,8 @@ +import { useUpdatableState } from '../hooks'; import { useControllableValue } from 'ahooks'; import { InputNumber, InputNumberProps } from 'antd'; import _ from 'lodash'; -import React from 'react'; +import React, { useEffect } from 'react'; export interface DynamicInputNumberProps extends Omit { @@ -24,9 +25,19 @@ const DynamicInputNumber: React.FC = ({ defaultValue: dynamicSteps[0], }); + // FIXME: this is a workaround to fix the issue that the value is not updated when the value is controlled + const [key, updateKey] = useUpdatableState('first'); + useEffect(() => { + setTimeout(() => { + updateKey(value); + }, 0); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( { // @ts-ignore diff --git a/react/src/components/DynamicUnitInputNumberWithSlider.tsx b/react/src/components/DynamicUnitInputNumberWithSlider.tsx index 734ef8bcc0..5fab632e12 100644 --- a/react/src/components/DynamicUnitInputNumberWithSlider.tsx +++ b/react/src/components/DynamicUnitInputNumberWithSlider.tsx @@ -1,4 +1,5 @@ import { compareNumberWithUnits, iSizeToSize } from '../helper'; +import { useUpdatableState } from '../hooks'; import DynamicUnitInputNumber, { DynamicUnitInputNumberProps, } from './DynamicUnitInputNumber'; @@ -7,7 +8,7 @@ import { useControllableValue } from 'ahooks'; import { Slider, theme } from 'antd'; import { SliderMarks } from 'antd/es/slider'; import _ from 'lodash'; -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; export interface DynamicUnitInputNumberWithSliderProps extends DynamicUnitInputNumberProps { @@ -45,6 +46,16 @@ const DynamicUnitInputNumberWithSlider: React.FC< // : undefined; // }, [warn, maxGiB?.number]); // console.log('##marks', marks); + + // FIXME: this is a workaround to fix the issue that the value is not updated when the value is controlled + const [key, updateKey] = useUpdatableState('first'); + useEffect(() => { + setTimeout(() => { + updateKey(value); + }, 0); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( = ({ } // eslint-disable-next-line react-hooks/exhaustive-deps }, [step]); + + // FIXME: this is a workaround to fix the issue that the value is not updated when the value is controlled + const [key, updateKey] = useUpdatableState('first'); + useEffect(() => { + setTimeout(() => { + updateKey(value); + }, 0); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( = ({ direction="column" > Date: Mon, 27 May 2024 17:29:21 +0800 Subject: [PATCH 03/18] hotfix: replace ellipsis in Notification with `_.turuncate` (#2434) To prevent flickering issues in notifications when using ellipsis in Typography, we temporarily replace it with "truncated". **Checklist:** (if applicable) - [ ] Mention to the original issue - [ ] Documentation - [ ] Minium required manager version - [ ] Specific setting for review (eg., KB link, endpoint or how to setup) - [ ] Minimum requirements to check during review - [ ] Test case(s) to demonstrate the difference of before/after --- react/src/components/BAINotificationItem.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/react/src/components/BAINotificationItem.tsx b/react/src/components/BAINotificationItem.tsx index 47a82a8886..125c978eac 100644 --- a/react/src/components/BAINotificationItem.tsx +++ b/react/src/components/BAINotificationItem.tsx @@ -56,14 +56,21 @@ const BAINotificationItem: React.FC<{ style={{ fontWeight: 500, }} - ellipsis={{ rows: 3 }} > - {notification.message} + {_.isString(notification.message) + ? _.truncate(notification.message, { + length: 200, + }) + : notification.message} - - {notification.description} + + {_.isString(notification.description) + ? _.truncate(notification.description, { + length: 300, + }) + : notification.description} {notification.to ? ( From 821dd5efddfb483a532c9ee8f72a75027ef97a44 Mon Sep 17 00:00:00 2001 From: Sujin Kim Date: Tue, 28 May 2024 10:24:00 +0900 Subject: [PATCH 04/18] feat: User resource policy page (#2421) follows #2357 Add User Resource Policy Page ## Description - Add User resource policy CRUD. - Add `FormItemProps` props to `FormItemWithUnlimited`. - Add simple error handler to `KeypairResourcePolicySettingModal`. - Remove unused component, `ResourcePolicyCard`. ## Checklist - [ ] List Page: name, max folder count, max session count per model session, max quota scope size, id, created at - [ ] Each item will be shown/hidden according to the server version. - [ ] In particular, it is stated in the schema that `max_vfolder_count` is supported from version 24.03.1, but this is incorrectly documented, and it follows https://github.com/lablup/backend.ai/pull/1993. - [ ] `max_session_count_per_model_session` is supported from version 23.09.10 as follows https://github.com/lablup/backend.ai/pull/1993 too. - [ ] Create/Modal: Create or modify user resource policy. - [ ] Version compatibility is the same as the list. - [ ] Unlimited value of `Max Folder Count` is 0. - [ ] Unlimited value of `Max Folder Size` is -1. ## Screenshots
24.03.0b1 (10.82.230.101) 24.09.0dev1 (main)
List ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/2HueYSdFvL8pOB5mgrUQ/7c23ba67-e22f-4770-8a4a-dfef8856ba96.png) ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/2HueYSdFvL8pOB5mgrUQ/c42974ac-fa28-4abb-a7a9-e2c6f67af335.png)
Create Modal ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/2HueYSdFvL8pOB5mgrUQ/da4d4c9d-a5cb-48fc-88c2-ca801642fde7.png) ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/2HueYSdFvL8pOB5mgrUQ/887306a4-00b9-4758-a72f-df897c60d634.png)
Modify Modal ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/2HueYSdFvL8pOB5mgrUQ/88c9f6e7-26fd-4780-b9fd-ce242724fcf1.png) ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/2HueYSdFvL8pOB5mgrUQ/af6c825c-5fb1-4417-a21c-f946e0f9ad3a.png)
Table Setting ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/2HueYSdFvL8pOB5mgrUQ/5a6dc107-9ff2-4b14-87d2-badd419b242d.png) ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/2HueYSdFvL8pOB5mgrUQ/ec04ff59-5f3f-47a8-a3a5-4b192c5528c5.png)
**Checklist:** (if applicable) - [x] Mention to the original issue - [ ] Documentation - [x] Minium required manager version - [x] Specific setting for review (eg., KB link, endpoint or how to setup) - [x] Minimum requirements to check during review - [ ] Test case(s) to demonstrate the difference of before/after --- .../src/components/FormItemWithUnlimited.tsx | 23 +- .../components/KeypairResourcePolicyList.tsx | 10 +- .../KeypairResourcePolicySettingModal.tsx | 175 ++++----- react/src/components/ResourcePolicyCard.tsx | 247 ------------ .../src/components/UserResourcePolicyList.tsx | 334 +++++++++++++++- .../UserResourcePolicySettingModal.tsx | 357 ++++++++++++------ react/src/helper/index.test.tsx | 29 ++ react/src/helper/index.tsx | 14 + react/src/pages/ResourcePolicyPage.tsx | 8 +- resources/i18n/de.json | 9 +- resources/i18n/el.json | 9 +- resources/i18n/en.json | 9 +- resources/i18n/es.json | 9 +- resources/i18n/fi.json | 9 +- resources/i18n/fr.json | 9 +- resources/i18n/id.json | 9 +- resources/i18n/it.json | 9 +- resources/i18n/ja.json | 9 +- resources/i18n/ko.json | 9 +- resources/i18n/mn.json | 9 +- resources/i18n/ms.json | 9 +- resources/i18n/pl.json | 9 +- resources/i18n/pt-BR.json | 9 +- resources/i18n/pt.json | 9 +- resources/i18n/ru.json | 9 +- resources/i18n/tr.json | 9 +- resources/i18n/vi.json | 9 +- resources/i18n/zh-CN.json | 9 +- resources/i18n/zh-TW.json | 9 +- src/components/backend-ai-credential-view.ts | 2 +- src/lib/backend.ai-client-esm.ts | 5 + 31 files changed, 891 insertions(+), 493 deletions(-) delete mode 100644 react/src/components/ResourcePolicyCard.tsx diff --git a/react/src/components/FormItemWithUnlimited.tsx b/react/src/components/FormItemWithUnlimited.tsx index 5f5ea00ce1..36c9db3fd5 100644 --- a/react/src/components/FormItemWithUnlimited.tsx +++ b/react/src/components/FormItemWithUnlimited.tsx @@ -1,26 +1,18 @@ import Flex from './Flex'; -import { Form, Checkbox } from 'antd'; +import { Form, Checkbox, FormItemProps } from 'antd'; import { CheckboxChangeEvent } from 'antd/es/checkbox'; -import { NamePath } from 'antd/es/form/interface'; -import React, { - cloneElement, - PropsWithChildren, - useEffect, - useState, -} from 'react'; +import React, { cloneElement, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -interface FormItemWithUnlimitedProps extends PropsWithChildren { - name: NamePath; +interface FormItemWithUnlimitedProps extends FormItemProps { unlimitedValue?: number | string; - label?: string; } const FormItemWithUnlimited: React.FC = ({ name, unlimitedValue, - label, children, + ...formItemPropsWithoutNameAndChildren }) => { const { t } = useTranslation(); const [isUnlimited, setIsUnlimited] = useState(false); @@ -51,14 +43,17 @@ const FormItemWithUnlimited: React.FC = ({ {isUnlimited ? ( - + {childrenWithUndefinedValue} ) : null} diff --git a/react/src/components/KeypairResourcePolicyList.tsx b/react/src/components/KeypairResourcePolicyList.tsx index a9c17c61b6..9ad9ae40f9 100644 --- a/react/src/components/KeypairResourcePolicyList.tsx +++ b/react/src/components/KeypairResourcePolicyList.tsx @@ -1,4 +1,4 @@ -import { localeCompare } from '../helper'; +import { localeCompare, numberSorterWithInfinityValue } from '../helper'; import { useSuspendedBackendaiClient, useUpdatableState } from '../hooks'; import Flex from './Flex'; import KeypairResourcePolicySettingModal, { @@ -158,9 +158,11 @@ const KeypairResourcePolicyList: React.FC = ( dataIndex: 'max_session_lifetime', key: 'max_session_lifetime', sorter: (a, b) => - a?.max_session_lifetime && b?.max_session_lifetime - ? a.max_session_lifetime - b.max_session_lifetime - : 1, + numberSorterWithInfinityValue( + a?.max_session_lifetime, + b?.max_session_lifetime, + 0, + ), render: (text) => (text ? text : '∞'), }, { diff --git a/react/src/components/KeypairResourcePolicySettingModal.tsx b/react/src/components/KeypairResourcePolicySettingModal.tsx index 3582e3aa08..7feab1ab26 100644 --- a/react/src/components/KeypairResourcePolicySettingModal.tsx +++ b/react/src/components/KeypairResourcePolicySettingModal.tsx @@ -147,95 +147,98 @@ const KeypairResourcePolicySettingModal: React.FC< ]); const handleOk = () => { - return form.validateFields().then((values) => { - let totalResourceSlots = _.mapValues( - values?.parsedTotalResourceSlots, - (value, key) => { - if (key === 'mem') { - return iSizeToSize(value, 'b', 0)?.numberFixed; - } - return value; - }, - ); - // Remove undefined values - totalResourceSlots = _.pickBy( - totalResourceSlots, - _.negate(_.isUndefined), - ); - - const parsedAllowedVfolderHosts: Record = - keypairResourcePolicy - ? JSON.parse(keypairResourcePolicy.allowed_vfolder_hosts || '{}') - : {}; - const allowedVfolderHosts: Record = - _.fromPairs( - _.map(values.allowedVfolderHostNames, (hostName) => { - const permissions = _.get( - parsedAllowedVfolderHosts, - hostName, - // TODO: Comment out if allow all permissions by default - // vfolder_host_permissions?.vfolder_host_permission_list, - [], // Default value if undefined - ); - return [hostName, permissions]; - }), + return form + .validateFields() + .then((values) => { + let totalResourceSlots = _.mapValues( + values?.parsedTotalResourceSlots, + (value, key) => { + if (key === 'mem') { + return iSizeToSize(value, 'b', 0)?.numberFixed; + } + return value; + }, + ); + // Remove undefined values + totalResourceSlots = _.pickBy( + totalResourceSlots, + _.negate(_.isUndefined), ); - const props: - | CreateKeyPairResourcePolicyInput - | ModifyKeyPairResourcePolicyInput = { - default_for_unspecified: 'UNLIMITED', - total_resource_slots: JSON.stringify(totalResourceSlots || '{}'), - max_session_lifetime: values?.max_session_lifetime, - max_concurrent_sessions: values?.max_concurrent_sessions, - max_containers_per_session: values?.max_containers_per_session, - idle_timeout: values?.idle_timeout, - allowed_vfolder_hosts: JSON.stringify(allowedVfolderHosts || '{}'), - }; - if (!isDeprecatedMaxVfolderCountInKeypairResourcePolicy) { - props.max_vfolder_count = values?.max_vfolder_count; - } + const parsedAllowedVfolderHosts: Record = + keypairResourcePolicy + ? JSON.parse(keypairResourcePolicy.allowed_vfolder_hosts || '{}') + : {}; + const allowedVfolderHosts: Record = + _.fromPairs( + _.map(values.allowedVfolderHostNames, (hostName) => { + const permissions = _.get( + parsedAllowedVfolderHosts, + hostName, + // TODO: Comment out if allow all permissions by default + // vfolder_host_permissions?.vfolder_host_permission_list, + [], // Default value if undefined + ); + return [hostName, permissions]; + }), + ); - if (keypairResourcePolicy === null) { - commitCreateKeypairResourcePolicy({ - variables: { - name: values?.name, - props: props as CreateKeyPairResourcePolicyInput, - }, - onCompleted: (res, errors) => { - if (!res?.create_keypair_resource_policy?.ok || errors) { - message.error(res?.create_keypair_resource_policy?.msg); - onRequestClose(); - } else { - message.success(t('resourcePolicy.SuccessfullyCreated')); - onRequestClose(true); - } - }, - onError(err) { - message.error(err.message); - }, - }); - } else { - commitModifyKeypairResourcePolicy({ - variables: { - name: values?.name, - props: props as ModifyKeyPairResourcePolicyInput, - }, - onCompleted: (res, errors) => { - if (!res?.modify_keypair_resource_policy?.ok || errors) { - message.error(res?.modify_keypair_resource_policy?.msg); - onRequestClose(); - } else { - message.success(t('resourcePolicy.SuccessfullyUpdated')); - onRequestClose(true); - } - }, - onError(err) { - message.error(err.message); - }, - }); - } - }); + const props: + | CreateKeyPairResourcePolicyInput + | ModifyKeyPairResourcePolicyInput = { + default_for_unspecified: 'UNLIMITED', + total_resource_slots: JSON.stringify(totalResourceSlots || '{}'), + max_session_lifetime: values?.max_session_lifetime, + max_concurrent_sessions: values?.max_concurrent_sessions, + max_containers_per_session: values?.max_containers_per_session, + idle_timeout: values?.idle_timeout, + allowed_vfolder_hosts: JSON.stringify(allowedVfolderHosts || '{}'), + }; + if (!isDeprecatedMaxVfolderCountInKeypairResourcePolicy) { + props.max_vfolder_count = values?.max_vfolder_count; + } + + if (keypairResourcePolicy === null) { + commitCreateKeypairResourcePolicy({ + variables: { + name: values?.name, + props: props as CreateKeyPairResourcePolicyInput, + }, + onCompleted: (res, errors) => { + if (!res?.create_keypair_resource_policy?.ok || errors) { + message.error(res?.create_keypair_resource_policy?.msg); + onRequestClose(); + } else { + message.success(t('resourcePolicy.SuccessfullyCreated')); + onRequestClose(true); + } + }, + onError(err) { + message.error(err.message); + }, + }); + } else { + commitModifyKeypairResourcePolicy({ + variables: { + name: values?.name, + props: props as ModifyKeyPairResourcePolicyInput, + }, + onCompleted: (res, errors) => { + if (!res?.modify_keypair_resource_policy?.ok || errors) { + message.error(res?.modify_keypair_resource_policy?.msg); + onRequestClose(); + } else { + message.success(t('resourcePolicy.SuccessfullyUpdated')); + onRequestClose(true); + } + }, + onError(err) { + message.error(err.message); + }, + }); + } + }) + .catch(() => {}); }; return ( diff --git a/react/src/components/ResourcePolicyCard.tsx b/react/src/components/ResourcePolicyCard.tsx deleted file mode 100644 index d859ed5747..0000000000 --- a/react/src/components/ResourcePolicyCard.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import { humanReadableDecimalSize } from '../helper/index'; -import Flex from './Flex'; -import ProjectResourcePolicySettingModal from './ProjectResourcePolicySettingModal'; -import UserResourcePolicySettingModal from './UserResourcePolicySettingModal'; -import { ResourcePolicyCardModifyProjectMutation } from './__generated__/ResourcePolicyCardModifyProjectMutation.graphql'; -import { ResourcePolicyCardModifyUserMutation } from './__generated__/ResourcePolicyCardModifyUserMutation.graphql'; -import { ResourcePolicyCard_project_resource_policy$key } from './__generated__/ResourcePolicyCard_project_resource_policy.graphql'; -import { ResourcePolicyCard_user_resource_policy$key } from './__generated__/ResourcePolicyCard_user_resource_policy.graphql'; -import { - EditFilled, - CloseOutlined, - ExclamationCircleOutlined, -} from '@ant-design/icons'; -import { useToggle } from 'ahooks'; -import { - Button, - Card, - CardProps, - Descriptions, - Modal, - message, - theme, -} from 'antd'; -import graphql from 'babel-plugin-relay/macro'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { useFragment, useMutation } from 'react-relay'; - -interface Props extends CardProps { - projectResourcePolicyFrgmt: ResourcePolicyCard_project_resource_policy$key | null; - userResourcePolicyFrgmt: ResourcePolicyCard_user_resource_policy$key | null; - onChangePolicy: () => void; -} -const ResourcePolicyCard: React.FC = ({ - projectResourcePolicyFrgmt, - userResourcePolicyFrgmt, - onChangePolicy, - ...props -}) => { - const { t } = useTranslation(); - const { token } = theme.useToken(); - - const [modal, contextHolder] = Modal.useModal(); - - const [ - visibleProjectResourcePolicySettingModal, - { toggle: toggleProjectResourcePolicySettingModal }, - ] = useToggle(false); - const [ - visibleUserResourcePolicySettingModal, - { toggle: toggleUserResourcePolicySettingModal }, - ] = useToggle(false); - - const project_resource_policy = useFragment( - graphql` - fragment ResourcePolicyCard_project_resource_policy on ProjectResourcePolicy { - id - name - max_quota_scope_size - ...ProjectResourcePolicySettingModalFragment - } - `, - projectResourcePolicyFrgmt, - ); - const user_resource_policy = useFragment( - graphql` - fragment ResourcePolicyCard_user_resource_policy on UserResourcePolicy { - id - name - max_quota_scope_size - ...UserResourcePolicySettingModalFragment - } - `, - userResourcePolicyFrgmt, - ); - - const [ - commitModifyProjectResourcePolicy, - // isInFlightCommitModifyProjectResourcePolicy, - ] = useMutation(graphql` - mutation ResourcePolicyCardModifyProjectMutation( - $name: String! - $props: ModifyProjectResourcePolicyInput! - ) { - modify_project_resource_policy(name: $name, props: $props) { - ok - msg - } - } - `); - - const [ - commitModifyUserResourcePolicy, - // isInFlightCommitModifyUserResourcePolicy, - ] = useMutation(graphql` - mutation ResourcePolicyCardModifyUserMutation( - $name: String! - $props: ModifyUserResourcePolicyInput! - ) { - modify_user_resource_policy(name: $name, props: $props) { - ok - msg - } - } - `); - - const confirmUnsetResourcePolicy = () => { - modal.confirm({ - title: t('storageHost.UnsetResourcePolicy'), - content: t('storageHost.DoYouWantToUseDefaultValue'), - icon: , - okText: t('button.Confirm'), - onOk() { - if (project_resource_policy) { - commitModifyProjectResourcePolicy({ - variables: { - name: project_resource_policy.name, - props: { - // max_vfolder_count: 0, - max_quota_scope_size: -1, - }, - }, - onCompleted(response) { - if (!response?.modify_project_resource_policy?.ok) { - message.error(response?.modify_project_resource_policy?.msg); - } else { - onChangePolicy(); - message.success( - t('storageHost.ResourcePolicySuccessfullyUpdated'), - ); - } - }, - onError(error) { - message.error(error?.message); - }, - }); - } else if (user_resource_policy) { - commitModifyUserResourcePolicy({ - variables: { - name: user_resource_policy.name, - props: { - // max_vfolder_count: 0, - max_quota_scope_size: -1, - }, - }, - onCompleted(response) { - if (!response?.modify_user_resource_policy?.ok) { - message.error(response?.modify_user_resource_policy?.msg); - } else { - onChangePolicy(); - message.success( - t('storageHost.ResourcePolicySuccessfullyUpdated'), - ); - } - }, - onError(error) { - message.error(error?.message); - }, - }); - } else { - message.error(t('storageHost.SelectProjectOrUserFirst')); - } - }, - }); - }; - - return ( - <> - - - - - ) : null - } - title={t('storageHost.ResourcePolicy')} - // bordered={false} - headStyle={{ borderBottom: 'none' }} - style={{ marginBottom: 10 }} - > - {project_resource_policy || user_resource_policy ? ( - - - {project_resource_policy - ? project_resource_policy && - project_resource_policy?.max_quota_scope_size !== -1 - ? humanReadableDecimalSize( - project_resource_policy?.max_quota_scope_size, - ) - : '-' - : user_resource_policy && - user_resource_policy?.max_quota_scope_size !== -1 - ? humanReadableDecimalSize( - user_resource_policy?.max_quota_scope_size, - ) - : '-'} - - - ) : null} - - { - onChangePolicy(); - toggleProjectResourcePolicySettingModal(); - }} - projectResourcePolicyFrgmt={project_resource_policy || null} - /> - { - onChangePolicy(); - toggleUserResourcePolicySettingModal(); - }} - /> - {contextHolder} - - ); -}; - -export default ResourcePolicyCard; diff --git a/react/src/components/UserResourcePolicyList.tsx b/react/src/components/UserResourcePolicyList.tsx index 363eb4a532..d78ac9fd87 100644 --- a/react/src/components/UserResourcePolicyList.tsx +++ b/react/src/components/UserResourcePolicyList.tsx @@ -1,15 +1,331 @@ -import React from 'react'; +import { + bytesToGB, + localeCompare, + numberSorterWithInfinityValue, +} from '../helper'; +import { useSuspendedBackendaiClient, useUpdatableState } from '../hooks'; +import Flex from './Flex'; +import TableColumnsSettingModal from './TableColumnsSettingModal'; +import UserResourcePolicySettingModal from './UserResourcePolicySettingModal'; +import { UserResourcePolicyListMutation } from './__generated__/UserResourcePolicyListMutation.graphql'; +import { + UserResourcePolicyListQuery, + UserResourcePolicyListQuery$data, +} from './__generated__/UserResourcePolicyListQuery.graphql'; +import { UserResourcePolicySettingModalFragment$key } from './__generated__/UserResourcePolicySettingModalFragment.graphql'; +import { + DeleteOutlined, + PlusOutlined, + ReloadOutlined, + SettingOutlined, +} from '@ant-design/icons'; +import { useLocalStorageState } from 'ahooks'; +import { App, Button, Popconfirm, Table, theme, Typography } from 'antd'; +import { AnyObject } from 'antd/es/_util/type'; +import { ColumnType } from 'antd/es/table'; +import graphql from 'babel-plugin-relay/macro'; +import dayjs from 'dayjs'; +import _ from 'lodash'; +import React, { useState, useTransition } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLazyLoadQuery, useMutation } from 'react-relay'; -interface UserResourcePolicyListProps { - // Define your prop types here -} +type UserResourcePolicies = NonNullable< + UserResourcePolicyListQuery$data['user_resource_policies'] +>[number]; -const UserResourcePolicyList: React.FC = ( - props, -) => { - // Component logic goes here +interface UserResourcePolicyListProps {} - return
User
; +const UserResourcePolicyList: React.FC = () => { + const { token } = theme.useToken(); + const { t } = useTranslation(); + const { message } = App.useApp(); + + const [isRefetchPending, startRefetchTransition] = useTransition(); + const [userResourcePolicyFetchKey, updateUserResourcePolicyFetchKey] = + useUpdatableState('initial-fetch'); + const [isCreatingPolicySetting, setIsCreatingPolicySetting] = useState(false); + const [isOpenColumnsSetting, setIsOpenColumnsSetting] = useState(false); + const [inFlightResourcePolicyName, setInFlightResourcePolicyName] = + useState(); + const [editingUserResourcePolicy, setEditingUserResourcePolicy] = + useState(); + + const baiClient = useSuspendedBackendaiClient(); + const supportMaxVfolderCount = baiClient?.supports( + 'max-vfolder-count-in-user-and-project-resource-policy', + ); + const supportMaxQuotaScopeSize = baiClient?.supports('max-quota-scope-size'); + const supportMaxSessionCountPerModelSession = baiClient?.supports( + 'max-session-count-per-model-session', + ); + const supportMaxCustomizedImageCount = baiClient?.supports( + 'max-customized-image-count', + ); + + const { user_resource_policies } = + useLazyLoadQuery( + graphql` + query UserResourcePolicyListQuery { + user_resource_policies { + id + name + created_at + # follows version of https://github.com/lablup/backend.ai/pull/1993 + # --------------- START -------------------- + max_vfolder_count @since(version: "23.09.6") + max_session_count_per_model_session @since(version: "23.09.10") + max_quota_scope_size @since(version: "23.09.2") + # ---------------- END --------------------- + max_customized_image_count @since(version: "24.03.0") + ...UserResourcePolicySettingModalFragment + } + } + `, + {}, + { + fetchPolicy: + userResourcePolicyFetchKey === 'initial-fetch' + ? 'store-and-network' + : 'network-only', + fetchKey: userResourcePolicyFetchKey, + }, + ); + + const [commitDelete, isInflightDelete] = + useMutation(graphql` + mutation UserResourcePolicyListMutation($name: String!) { + delete_user_resource_policy(name: $name) { + ok + msg + } + } + `); + + const columns = _.filter>([ + { + title: t('resourcePolicy.Name'), + dataIndex: 'name', + key: 'name', + fixed: 'left', + sorter: (a, b) => localeCompare(a?.name, b?.name), + }, + supportMaxVfolderCount && { + title: t('resourcePolicy.MaxVFolderCount'), + dataIndex: 'max_vfolder_count', + render: (text) => (_.toNumber(text) === 0 ? '∞' : text), + sorter: (a, b) => + numberSorterWithInfinityValue( + a?.max_vfolder_count, + b?.max_vfolder_count, + 0, + ), + }, + supportMaxSessionCountPerModelSession && { + title: t('resourcePolicy.MaxSessionCountPerModelSession'), + dataIndex: 'max_session_count_per_model_session', + sorter: (a, b) => + (a?.max_session_count_per_model_session ?? 0) - + (b?.max_session_count_per_model_session ?? 0), + }, + supportMaxQuotaScopeSize && { + title: t('resourcePolicy.MaxQuotaScopeSize'), + dataIndex: 'max_quota_scope_size', + render: (text) => (text === -1 ? '∞' : bytesToGB(text)), + sorter: (a, b) => + numberSorterWithInfinityValue( + a?.max_quota_scope_size, + b?.max_quota_scope_size, + -1, + ), + }, + supportMaxCustomizedImageCount && { + title: t('resourcePolicy.MaxCustomizedImageCount'), + dataIndex: 'max_customized_image_count', + sorter: (a, b) => + (a?.max_customized_image_count ?? 0) - + (b?.max_customized_image_count ?? 0), + }, + { + title: 'ID', + dataIndex: 'id', + sorter: (a, b) => localeCompare(a?.id, b?.id), + }, + { + title: t('resourcePolicy.CreatedAt'), + dataIndex: 'created_at', + render: (text) => dayjs(text).format('lll'), + sorter: (a, b) => localeCompare(a?.created_at, b?.created_at), + }, + { + title: t('general.Control'), + fixed: 'right', + render: (text: any, row: UserResourcePolicies) => ( + + + +
+
+ + displayedColumnKeys?.includes(_.toString(column.key)), + ) as ColumnType[] + } + dataSource={user_resource_policies as readonly AnyObject[] | undefined} + scroll={{ x: 'max-content' }} + pagination={false} + /> + +
24.03.0b1 (10.82.230.101) 24.09.0dev1 (main)
List ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/2HueYSdFvL8pOB5mgrUQ/bc9a3d28-9119-47f7-b697-f1d54935c020.png) ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/2HueYSdFvL8pOB5mgrUQ/21e03b17-1d18-4fb7-8683-7bcdfe009beb.png)
Create Modal ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/2HueYSdFvL8pOB5mgrUQ/93f5f421-3c09-47f6-ab7e-9404c4bdbb8c.png) ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/2HueYSdFvL8pOB5mgrUQ/8c958f14-b3e7-427d-997c-aa5a331343a2.png)
Modify Modal ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/2HueYSdFvL8pOB5mgrUQ/81144f9b-a55a-4136-b298-58038f2f224b.png) ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/2HueYSdFvL8pOB5mgrUQ/32d1f8d6-9fa9-4377-aee0-0427183fa573.png)
Table Setting ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/2HueYSdFvL8pOB5mgrUQ/fc7e605b-b957-47fd-95ec-c92381931d0a.png) ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/2HueYSdFvL8pOB5mgrUQ/11659c5b-cb04-4a5f-97fc-d1122913e1a5.png)
--- **Checklist:** (if applicable) - [x] Mention to the original issue - [ ] Documentation - [x] Minium required manager version - [x] Specific setting for review (eg., KB link, endpoint or how to setup) - [x] Minimum requirements to check during review - [ ] Test case(s) to demonstrate the difference of before/after --- .../components/ProjectResourcePolicyList.tsx | 315 ++++++++++++++++- .../ProjectResourcePolicySettingModal.tsx | 316 ++++++++++++------ react/src/pages/ResourcePolicyPage.tsx | 8 +- resources/i18n/de.json | 3 +- resources/i18n/el.json | 3 +- resources/i18n/en.json | 3 +- resources/i18n/es.json | 3 +- resources/i18n/fi.json | 3 +- resources/i18n/fr.json | 3 +- resources/i18n/id.json | 3 +- resources/i18n/it.json | 3 +- resources/i18n/ja.json | 3 +- resources/i18n/ko.json | 3 +- resources/i18n/mn.json | 3 +- resources/i18n/ms.json | 3 +- resources/i18n/pl.json | 3 +- resources/i18n/pt-BR.json | 3 +- resources/i18n/pt.json | 3 +- resources/i18n/ru.json | 3 +- resources/i18n/tr.json | 3 +- resources/i18n/vi.json | 3 +- resources/i18n/zh-CN.json | 3 +- resources/i18n/zh-TW.json | 3 +- 23 files changed, 560 insertions(+), 139 deletions(-) diff --git a/react/src/components/ProjectResourcePolicyList.tsx b/react/src/components/ProjectResourcePolicyList.tsx index 4956d0f87f..a6f9c55405 100644 --- a/react/src/components/ProjectResourcePolicyList.tsx +++ b/react/src/components/ProjectResourcePolicyList.tsx @@ -1,15 +1,312 @@ -import React from 'react'; +import { + bytesToGB, + localeCompare, + numberSorterWithInfinityValue, +} from '../helper'; +import { useSuspendedBackendaiClient, useUpdatableState } from '../hooks'; +import Flex from './Flex'; +import ProjectResourcePolicySettingModal from './ProjectResourcePolicySettingModal'; +import TableColumnsSettingModal from './TableColumnsSettingModal'; +import { ProjectResourcePolicyListMutation } from './__generated__/ProjectResourcePolicyListMutation.graphql'; +import { + ProjectResourcePolicyListQuery, + ProjectResourcePolicyListQuery$data, +} from './__generated__/ProjectResourcePolicyListQuery.graphql'; +import { ProjectResourcePolicySettingModalFragment$key } from './__generated__/ProjectResourcePolicySettingModalFragment.graphql'; +import { + DeleteOutlined, + PlusOutlined, + ReloadOutlined, + SettingOutlined, +} from '@ant-design/icons'; +import { useLocalStorageState } from 'ahooks'; +import { Button, message, Popconfirm, Table, theme, Typography } from 'antd'; +import { AnyObject } from 'antd/es/_util/type'; +import { ColumnType } from 'antd/es/table'; +import graphql from 'babel-plugin-relay/macro'; +import dayjs from 'dayjs'; +import _ from 'lodash'; +import React, { useState, useTransition } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLazyLoadQuery, useMutation } from 'react-relay'; -type ProjectResourcePolicyListProps = { - // Define the props for your component here -}; +type ProjectResourcePolicies = NonNullable< + ProjectResourcePolicyListQuery$data['project_resource_policies'] +>[number]; + +interface ProjectResourcePolicyListProps {} + +const ProjectResourcePolicyList: React.FC< + ProjectResourcePolicyListProps +> = () => { + const { token } = theme.useToken(); + const { t } = useTranslation(); + const [isRefetchPending, startRefetchTransition] = useTransition(); + const [projectResourcePolicyFetchKey, updateProjectResourcePolicyFetchKey] = + useUpdatableState('initial-fetch'); + const [isCreatingPolicySetting, setIsCreatingPolicySetting] = useState(false); + const [isOpenColumnsSetting, setIsOpenColumnsSetting] = useState(false); + const [inFlightResourcePolicyName, setInFlightResourcePolicyName] = + useState(); + const [editingProjectResourcePolicy, setEditingProjectResourcePolicy] = + useState(); + + const baiClient = useSuspendedBackendaiClient(); + const supportMaxVfolderCount = baiClient?.supports( + 'max-vfolder-count-in-user-and-project-resource-policy', + ); + const supportMaxQuotaScopeSize = baiClient?.supports('max-quota-scope-size'); + + const { project_resource_policies } = + useLazyLoadQuery( + graphql` + query ProjectResourcePolicyListQuery { + project_resource_policies { + id + name + created_at + # follows version of https://github.com/lablup/backend.ai/pull/1993 + # --------------- START -------------------- + max_vfolder_count @since(version: "23.09.6") + max_quota_scope_size @since(version: "23.09.2") + # ---------------- END --------------------- + ...ProjectResourcePolicySettingModalFragment + } + } + `, + {}, + { + fetchPolicy: + projectResourcePolicyFetchKey === 'initial-fetch' + ? 'store-and-network' + : 'network-only', + fetchKey: projectResourcePolicyFetchKey, + }, + ); + + const [commitDelete, isInflightDelete] = + useMutation(graphql` + mutation ProjectResourcePolicyListMutation($name: String!) { + delete_project_resource_policy(name: $name) { + ok + msg + } + } + `); + + const columns = _.filter>([ + { + title: t('resourcePolicy.Name'), + dataIndex: 'name', + key: 'name', + fixed: 'left', + sorter: (a, b) => localeCompare(a?.name, b?.name), + }, + supportMaxVfolderCount && { + title: t('resourcePolicy.MaxVFolderCount'), + dataIndex: 'max_vfolder_count', + render: (text: ProjectResourcePolicies) => + _.toNumber(text) === 0 ? '∞' : text, + sorter: (a, b) => + numberSorterWithInfinityValue( + a?.max_vfolder_count, + b?.max_vfolder_count, + 0, + ), + }, + supportMaxQuotaScopeSize && { + title: t('resourcePolicy.MaxQuotaScopeSize'), + dataIndex: 'max_quota_scope_size', + render: (text) => (text === -1 ? '∞' : bytesToGB(text)), + sorter: (a, b) => + numberSorterWithInfinityValue( + a?.max_quota_scope_size, + b?.max_quota_scope_size, + -1, + ), + }, + { + title: 'ID', + dataIndex: 'id', + sorter: (a, b) => localeCompare(a?.id, b?.id), + }, + { + title: t('resourcePolicy.CreatedAt'), + dataIndex: 'created_at', + render: (text) => dayjs(text).format('lll'), + sorter: (a, b) => localeCompare(a?.created_at, b?.created_at), + }, + { + title: t('general.Control'), + fixed: 'right', + render: (text: any, row: ProjectResourcePolicies) => ( + + + +
+
+ + displayedColumnKeys?.includes(_.toString(column.key)), + ) as ColumnType[] + } + dataSource={ + project_resource_policies as readonly AnyObject[] | undefined + } + scroll={{ x: 'max-content' }} + pagination={false} + /> + +