Skip to content
Open
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
1 change: 1 addition & 0 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export const API = {
// Fleets
FLEETS: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/fleets/list`,
FLEETS_DETAILS: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/fleets/get`,
FLEETS_APPLY: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/fleets/apply`,
FLEETS_DELETE: (projectName: IProject['project_name']) => `${API.BASE()}/project/${projectName}/fleets/delete`,
FLEET_INSTANCES_DELETE: (projectName: IProject['project_name']) =>
`${API.BASE()}/project/${projectName}/fleets/delete_instances`,
Expand Down
20 changes: 16 additions & 4 deletions frontend/src/components/ButtonWithConfirmation/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import Box from '@cloudscape-design/components/box';

import { Button } from '../Button';
Expand All @@ -13,20 +14,31 @@ export const ButtonWithConfirmation: React.FC<IProps> = ({
confirmButtonLabel,
...props
}) => {
const { t } = useTranslation();
const [showDeleteConfirm, setShowConfirmDelete] = useState(false);

const toggleDeleteConfirm = () => {
setShowConfirmDelete((val) => !val);
};

const content = typeof confirmContent === 'string' ? <Box variant="span">{confirmContent}</Box> : confirmContent;

const onConfirm = () => {
if (onClick) onClick();

setShowConfirmDelete(false);
};

const getContent = () => {
if (!confirmContent) {
return <Box variant="span">{t('confirm_dialog.message')}</Box>;
}

if (typeof confirmContent === 'string') {
return <Box variant="span">{confirmContent}</Box>;
}

return confirmContent;
};

return (
<>
<Button {...props} onClick={toggleDeleteConfirm} />
Expand All @@ -36,8 +48,8 @@ export const ButtonWithConfirmation: React.FC<IProps> = ({
onDiscard={toggleDeleteConfirm}
onConfirm={onConfirm}
title={confirmTitle}
content={content}
confirmButtonLabel={confirmButtonLabel}
content={getContent()}
confirmButtonLabel={confirmButtonLabel ?? t('common.delete')}
/>
</>
);
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/components/ConfirmationDialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { IProps } from './types';

export const ConfirmationDialog: React.FC<IProps> = ({
title: titleProp,
content: contentProp,
content,
visible = false,
onDiscard,
onConfirm,
Expand All @@ -18,9 +18,8 @@ export const ConfirmationDialog: React.FC<IProps> = ({
}) => {
const { t } = useTranslation();
const title = titleProp ?? t('confirm_dialog.title');
const content = contentProp ?? <Box variant="span">{t('confirm_dialog.message')}</Box>;
const cancelButtonLabel = cancelButtonLabelProp ?? t('common.cancel');
const confirmButtonLabel = confirmButtonLabelProp ?? t('common.delete');
const confirmButtonLabel = confirmButtonLabelProp ?? t('common.ok');

return (
<Modal
Expand Down
34 changes: 34 additions & 0 deletions frontend/src/components/ConfirmationDialog/slice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { RootState } from 'store';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

import { IProps as ConfirmationDialogProps } from './types';

type ConfirmationDialogPropsWithUuid = ConfirmationDialogProps & { uuid: string };

type ConfirmationDialogsStata = {
dialogs: Array<ConfirmationDialogPropsWithUuid>;
};

const initialState: ConfirmationDialogsStata = {
dialogs: [],
};

export const confirmationSlice = createSlice({
name: 'confirmation',
initialState,

reducers: {
open: (state, action: PayloadAction<ConfirmationDialogPropsWithUuid>) => {
state.dialogs = [...state.dialogs, action.payload];
},
close: (state, action: PayloadAction<ConfirmationDialogPropsWithUuid['uuid']>) => {
state.dialogs = state.dialogs.filter((i) => i.uuid !== action.payload);
},
},
});

export const { open, close } = confirmationSlice.actions;

export const selectConfirmationDialogs = (state: RootState) => state.confirmation.dialogs;

export default confirmationSlice.reducer;
59 changes: 59 additions & 0 deletions frontend/src/components/form/Toogle/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import { Controller, FieldValues } from 'react-hook-form';
import FormField from '@cloudscape-design/components/form-field';
import ToggleCSD from '@cloudscape-design/components/toggle';

import { FormToggleProps } from './types';

export const FormToggle = <T extends FieldValues>({
name,
control,
rules,
label,
info,
constraintText,
description,
secondaryControl,
stretch,
leftContent,
toggleLabel,
onChange: onChangeProp,
toggleDescription,
...props
}: FormToggleProps<T>) => {
return (
<Controller
name={name}
control={control}
rules={rules}
render={({ field: { onChange, value, ...fieldRest }, fieldState: { error } }) => {
return (
<FormField
description={description}
label={label}
info={info}
stretch={stretch}
constraintText={constraintText}
secondaryControl={secondaryControl}
errorText={error?.message}
>
{leftContent}

<ToggleCSD
{...fieldRest}
{...props}
checked={value}
onChange={(event) => {
onChange(event.detail.checked);
onChangeProp?.(event);
}}
description={toggleDescription}
>
{toggleLabel}
</ToggleCSD>
</FormField>
);
}}
/>
);
};
12 changes: 12 additions & 0 deletions frontend/src/components/form/Toogle/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ReactNode } from 'react';
import { ControllerProps, FieldValues } from 'react-hook-form';
import { FormFieldProps } from '@cloudscape-design/components/form-field';
import { ToggleProps } from '@cloudscape-design/components/toggle';

export type FormToggleProps<T extends FieldValues> = Omit<ToggleProps, 'value' | 'checked' | 'name'> &
Omit<FormFieldProps, 'errorText'> &
Pick<ControllerProps<T>, 'control' | 'name' | 'rules'> & {
toggleDescription?: ReactNode;
leftContent?: ReactNode;
toggleLabel?: ReactNode | string;
};
1 change: 1 addition & 0 deletions frontend/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export { ListEmptyMessage } from './ListEmptyMessage';
export { DetailsHeader } from './DetailsHeader';
export { Loader } from './Loader';
export { FormCheckbox } from './form/Checkbox';
export { FormToggle } from './form/Toogle';
export { FormInput } from './form/Input';
export { FormMultiselect } from './form/Multiselect';
export { FormSelect } from './form/Select';
Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { default as useAppDispatch } from './useAppDispatch';
export { default as useAppSelector } from './useAppSelector';
export { useBreadcrumbs } from './useBreadcrumbs';
export { useNotifications } from './useNotifications';
export { useConfirmationDialog } from './useConfirmationDialog';
export { useHelpPanel } from './useHelpPanel';
export { usePermissionGuard } from './usePermissionGuard';
export { useInfiniteScroll } from './useInfiniteScroll';
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/hooks/useConfirmationDialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { close, open } from 'components/ConfirmationDialog/slice';
import { IProps as ConfirmationDialogProps } from 'components/ConfirmationDialog/types';

import { getUid } from '../libs';
import useAppDispatch from './useAppDispatch';

export const useConfirmationDialog = () => {
const dispatch = useAppDispatch();

const onDiscard = (uuid: string) => {
dispatch(close(uuid));
};

const openConfirmationDialog = (props: Omit<ConfirmationDialogProps, 'onDiscard'>) => {
const uuid = getUid();

dispatch(
open({
uuid,
...props,
onDiscard: () => onDiscard(uuid),
}),
);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmation dialog doesn't close after user confirms

High Severity

The useConfirmationDialog hook wraps onDiscard to close the dialog but doesn't wrap onConfirm. When the user clicks confirm, the original onConfirm callback runs but the dialog remains visible because close(uuid) is never dispatched. The dialog will stay open until the user clicks Cancel or dismisses it manually.

Fix in Cursor Fix in Web

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onConfirm was implemented in ConfirmationDialof componentd


return [openConfirmationDialog];
};
1 change: 1 addition & 0 deletions frontend/src/hooks/useNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const NOTIFICATION_LIFE_TIME = 6000;
type TUseNotificationsArgs = { temporary?: boolean; liveTime?: number } | undefined;

const defaultArgs: NonNullable<Required<TUseNotificationsArgs>> = { temporary: true, liveTime: NOTIFICATION_LIFE_TIME };

export const useNotifications = (args: TUseNotificationsArgs = defaultArgs) => {
const dispatch = useAppDispatch();
const notificationIdsSet = useRef(new Set<ReturnType<typeof getUid>>());
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/layouts/AppLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
AppLayout as GenericAppLayout,
AppLayoutProps as GenericAppLayoutProps,
BreadcrumbGroup,
ConfirmationDialog,
HelpPanel,
Notifications,
SideNavigation,
Expand All @@ -35,6 +36,7 @@ import {
setToolsTab,
} from 'App/slice';

import { selectConfirmationDialogs } from '../../components/ConfirmationDialog/slice';
import { AnnotationContext } from './AnnotationContext';
import { useSideNavigation } from './hooks';
import { TallyComponent } from './Tally';
Expand Down Expand Up @@ -71,6 +73,7 @@ const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const helpPanelContent = useAppSelector(selectHelpPanelContent);
const dispatch = useAppDispatch();
const { navLinks, activeHref } = useSideNavigation();
const confirmationDialogs = useAppSelector(selectConfirmationDialogs);

const onFollowHandler: SideNavigationProps['onFollow'] = (event) => {
event.preventDefault();
Expand Down Expand Up @@ -254,6 +257,10 @@ const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
/>

<TallyComponent />

{confirmationDialogs.map(({ uuid, ...props }) => (
<ConfirmationDialog key={uuid} {...props} visible />
))}
</AnnotationContext>
);
};
Expand Down
21 changes: 14 additions & 7 deletions frontend/src/locale/en.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{
"dstack": "Dstack",
"common": {
"ok": "OK",
"loading": "Loading",
"add": "Add",
"yes": "Yes",
"no": "No",
"create": "Create {{text}}",
"create": "Create",
"create_wit_text": "Create {{text}}",
"edit": "Edit",
"delete": "Delete",
"remove": "Remove",
Expand Down Expand Up @@ -205,14 +207,19 @@
"backends": "Backends",
"base_backends_description": "dstack will automatically collect offers from the following providers. Deselect providers you don’t want to use.",
"backends_description": "The following backends can be configured with your own cloud credentials in the project settings after the project is created.",
"default_fleet": "Create default fleet",
"default_fleet_description": "You can create default fleet for project",
"fleet_name": "Fleet name",
"fleet_name_description": "Only latin characters, dashes, underscores, and digits",
"default_fleet": "Create a default fleet",
"default_fleet_description": "At least one fleet is required to create dev environments, submit tasks, or run services",
"fleet_name": "Name",
"fleet_name_description": "The name of the fleet, e.g. 'my-fleet'",
"fleet_name_placeholder": "Optional",
"fleet_name_constraint": "If not specified, generated automatically",
"fleet_min_instances": "Min number of instances",
"fleet_min_instances_description": "Only digits",
"fleet_min_instances_description": "Specify \"0\" if you want instances to be created on demand",
"fleet_max_instances": "Max number of instances",
"fleet_max_instances_description": "Only digits",
"fleet_max_instances_description": "Specify it only if you want to limit the maximum number of instances",
"fleet_max_instances_placeholder": "Optional",
"fleet_idle_duration": "Idle duration",
"fleet_idle_duration_description": "For how long instances should be kept idle before termination, e.g. \"0s\", \"1m\", \"1h\"",
"is_public": "Make project public",
"is_public_description": "Public projects can be accessed by any user without being a member",
"backend": "Backend",
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/Project/Add/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const ProjectAdd: React.FC = () => {
href: ROUTES.PROJECT.LIST,
},
{
text: t('common.create', { text: t('navigation.project') }),
text: t('common.create_wit_text', { text: t('navigation.project') }),
href: ROUTES.PROJECT.ADD,
},
]);
Expand Down
13 changes: 0 additions & 13 deletions frontend/src/pages/Project/CreateWizard/constants.ts

This file was deleted.

42 changes: 42 additions & 0 deletions frontend/src/pages/Project/CreateWizard/constants.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';

export const projectTypeOptions = [
{
label: 'GPU marketplace',
description:
'Find the cheapest GPUs available in our marketplace. Enjoy $5 in free credits, and easily top up your balance with a credit card.',
value: 'gpu_marketplace',
},
{
label: 'Your cloud accounts',
description: 'Connect and manage your cloud accounts. dstack supports all major GPU cloud providers.',
value: 'own_cloud',
},
];

export const FLEET_MIN_INSTANCES_INFO = {
header: <h2>Min number of instances</h2>,
body: (
<>
<p>Some text</p>
</>
),
};

export const FLEET_MAX_INSTANCES_INFO = {
header: <h2>Max number of instances</h2>,
body: (
<>
<p>Some text</p>
</>
),
};

export const FLEET_IDLE_DURATION_INFO = {
header: <h2>Idle duration</h2>,
body: (
<>
<p>Some text</p>
</>
),
};
Loading