Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(FR-244): session renaming in the session detail panel #3034

Merged
merged 1 commit into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading