Skip to content

Commit

Permalink
feat: session renaming in the session detail panel
Browse files Browse the repository at this point in the history
  • Loading branch information
yomybaby committed Jan 15, 2025
1 parent a84073a commit f4e4a4f
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 71 deletions.
137 changes: 103 additions & 34 deletions react/src/components/ComputeSessionNodeItems/EditableSessionName.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { useCurrentUserInfo } from '../../hooks/backendai';
import { getSessionNameRules } from '../SessionNameFormItem';
import { EditableSessionNameFragment$key } from './__generated__/EditableSessionNameFragment.graphql';
import { EditableSessionNameMutation } from './__generated__/EditableSessionNameMutation.graphql';
import { theme } from 'antd';
import { theme, Form, Input } from 'antd';
import Text, { TextProps } from 'antd/es/typography/Text';
import Title, { TitleProps } from 'antd/es/typography/Title';
import graphql from 'babel-plugin-relay/macro';
import { CornerDownLeftIcon } from 'lucide-react';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useFragment, useMutation } from 'react-relay';

type EditableSessionNameProps = {
Expand All @@ -17,21 +21,25 @@ type EditableSessionNameProps = {
const EditableSessionName: React.FC<EditableSessionNameProps> = ({
component: Component = Text,
sessionFrgmt,
editable: editableOfProps,
style,
...otherProps
}) => {
const session = useFragment(
graphql`
fragment EditableSessionNameFragment on ComputeSessionNode {
id
row_id
name
priority
user_id
status
}
`,
sessionFrgmt,
);
const [optimisticName, setOptimisticName] = useState(session.name);
const { token } = theme.useToken();
const [userInfo] = useCurrentUserInfo();
const [commitEditMutation, isPendingEditMutation] =
useMutation<EditableSessionNameMutation>(graphql`
mutation EditableSessionNameMutation($input: ModifyComputeSessionInput!) {
Expand All @@ -43,41 +51,102 @@ const EditableSessionName: React.FC<EditableSessionNameProps> = ({
}
}
`);

const { t } = useTranslation();
const { token } = theme.useToken();
const [isEditing, setIsEditing] = useState(false);

const isNotPreparingCategoryStatus = ![
'RESTARTING',
'PREPARING',
'PREPARED',
'CREATING',
'PULLING',
].includes(session.status || '');

const isEditingAllowed =
editableOfProps &&
userInfo.uuid === session.user_id &&
isNotPreparingCategoryStatus;

return (
session && (
<Component
editable={
isPendingEditMutation
? undefined
: {
onChange: (newName) => {
setOptimisticName(newName);
commitEditMutation({
variables: {
input: {
id: session.id,
name: newName,
// TODO: Setting the priority is not needed here. However, due to an API bug, we will keep it.
priority: session.priority,
},
},
onCompleted(response, errors) {},
onError(error) {},
});
<>
{(!isEditing || isPendingEditMutation) && (
<Component
editable={
isEditingAllowed && !isPendingEditMutation
? {
onStart: () => {
setIsEditing(true);
},
triggerType: ['icon', 'text'],
}
: false
}
copyable
style={{
...style,
color: isPendingEditMutation
? token.colorTextTertiary
: style?.color,
}}
{...otherProps}
>
{isPendingEditMutation ? optimisticName : session.name}
</Component>
)}
{isEditing && !isPendingEditMutation && (
<Form
onFinish={(values) => {
setIsEditing(false);
setOptimisticName(values.sessionName);
commitEditMutation({
variables: {
input: {
id: session.id,
name: values.sessionName,
},
triggerType: ['icon', 'text'],
},
onCompleted(response, errors) {},
onError(error) {},
});
}}
initialValues={{
sessionName: session.name,
}}
style={{
flex: 1,
}}
>
<Form.Item
name="sessionName"
rules={getSessionNameRules(t)}
style={{
margin: 0,
}}
>
<Input
size="large"
suffix={
<CornerDownLeftIcon
style={{
fontSize: '0.8em',
color: token.colorTextTertiary,
}}
/>
}
}
copyable
style={{
...style,
color: isPendingEditMutation ? token.colorTextTertiary : style?.color,
}}
{...otherProps}
>
{isPendingEditMutation ? optimisticName : session.name}
</Component>
)
autoFocus
onKeyDown={(e) => {
// when press escape key, cancel editing
if (e.key === 'Escape') {
setIsEditing(false);
}
}}
/>
</Form.Item>
</Form>
)}
</>
);
};

Expand Down
4 changes: 2 additions & 2 deletions react/src/components/SessionDetailContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,16 +139,16 @@ const SessionDetailContent: React.FC<{
style={{
alignSelf: 'stretch',
}}
gap={'sm'}
>
<EditableSessionName
sessionFrgmt={session}
component={Title}
level={3}
style={{
margin: 0,
flex: 1,
}}
editable={false}
editable
/>
<Button.Group size="large">
<SessionActionButtons sessionFrgmt={session} />
Expand Down
1 change: 1 addition & 0 deletions react/src/components/SessionDetailDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const SessionDetailDrawer: React.FC<SessionDetailDrawerProps> = ({
<Drawer
title={t('session.SessionInfo')}
width={800}
keyboard={false}
{...drawerProps}
open={!!sessionId}
onClose={(e) => {
Expand Down
72 changes: 37 additions & 35 deletions react/src/components/SessionNameFormItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Form, FormItemProps, Input } from 'antd';
import { TFunction } from 'i18next';
import _ from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
Expand All @@ -8,6 +9,41 @@ interface SessionNameFormItemProps extends FormItemProps {}
export interface SessionNameFormItemValue {
sessionName: string;
}

export const getSessionNameRules = (t: TFunction): FormItemProps['rules'] => [
{
min: 4,
message: t('session.validation.SessionNameTooShort'),
},
{
max: 64,
message: t('session.validation.SessionNameTooLong64'),
},
{
validator(f, value) {
if (_.isEmpty(value)) {
return Promise.resolve();
}
if (!/^\w/.test(value)) {
return Promise.reject(
t('session.validation.SessionNameShouldStartWith'),
);
}

if (!/^[\w.-]*$/.test(value)) {
return Promise.reject(
t('session.validation.SessionNameInvalidCharacter'),
);
}

if (!/\w$/.test(value) && value.length >= 4) {
return Promise.reject(t('session.validation.SessionNameShouldEndWith'));
}
return Promise.resolve();
},
},
];

const SessionNameFormItem: React.FC<SessionNameFormItemProps> = ({
...formItemProps
}) => {
Expand All @@ -19,41 +55,7 @@ const SessionNameFormItem: React.FC<SessionNameFormItemProps> = ({
name="sessionName"
// Original rule : /^(?=.{4,64}$)\w[\w.-]*\w$/
// https://github.com/lablup/backend.ai/blob/main/src/ai/backend/manager/api/session.py#L355-L356
rules={[
{
min: 4,
message: t('session.validation.SessionNameTooShort'),
},
{
max: 64,
message: t('session.validation.SessionNameTooLong64'),
},
{
validator(f, value) {
if (_.isEmpty(value)) {
return Promise.resolve();
}
if (!/^\w/.test(value)) {
return Promise.reject(
t('session.validation.SessionNameShouldStartWith'),
);
}

if (!/^[\w.-]*$/.test(value)) {
return Promise.reject(
t('session.validation.SessionNameInvalidCharacter'),
);
}

if (!/\w$/.test(value) && value.length >= 4) {
return Promise.reject(
t('session.validation.SessionNameShouldEndWith'),
);
}
return Promise.resolve();
},
},
]}
rules={getSessionNameRules(t)}
{...formItemProps}
>
<Input allowClear autoComplete="off" />
Expand Down
2 changes: 2 additions & 0 deletions react/src/hooks/backendai.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export const useResourceSlotsDetails = (resourceGroupName?: string) => {
interface UserInfo {
full_name: string;
email: string;
uuid: string;
}

type mutationOptions<T> = {
Expand All @@ -146,6 +147,7 @@ export const useCurrentUserInfo = () => {
const [userInfo, _setUserInfo] = useState<UserInfo>({
full_name: baiClient.full_name,
email: baiClient.email,
uuid: baiClient.user_uuid,
});

const getUsername = () => {
Expand Down
2 changes: 2 additions & 0 deletions src/components/backend-ai-login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1744,6 +1744,7 @@ export default class BackendAILogin extends BackendAIPage {
'domain_name',
'groups {name, id}',
'need_password_change',
'uuid',
];
const q = `query { user{ ${fields.join(' ')} } }`;
const v = { uuid: this.user };
Expand Down Expand Up @@ -1783,6 +1784,7 @@ export default class BackendAILogin extends BackendAIPage {
const role = response['user'].role;
this.domain_name = response['user'].domain_name;
globalThis.backendaiclient.email = this.email;
globalThis.backendaiclient.user_uuid = response['user'].uuid;
globalThis.backendaiclient.full_name = response['user'].full_name;
globalThis.backendaiclient.is_admin = false;
globalThis.backendaiclient.is_superadmin = false;
Expand Down
5 changes: 5 additions & 0 deletions src/lib/backend.ai-client-esm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@ class Client {
static ERR_SERVER: any;
static ERR_UNKNOWN: any;

// Additional info related to current login user
public email: string;
public full_name: string;
public user_uuid: string;

/**
* The client API wrapper.
*
Expand Down

0 comments on commit f4e4a4f

Please sign in to comment.