Skip to content
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
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;
17 changes: 17 additions & 0 deletions frontend/src/components/form/Toogle/index.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@use '@cloudscape-design/design-tokens/index' as awsui;

.labelWithInfo {
display: inline-flex;
align-items: center;
}

.divider {
display: inline-block;
height: 16px;
border-left: 1px solid awsui.$color-border-divider-default;
margin: 0 8px;
}

.info {
display: inline-flex;
}
78 changes: 78 additions & 0 deletions frontend/src/components/form/Toogle/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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';

import styles from './index.module.scss';

export const FormToggle = <T extends FieldValues>({
name,
control,
rules,
label,
info,
constraintText,
description,
secondaryControl,
stretch,
leftContent,
toggleLabel,
onChange: onChangeProp,
toggleDescription,
toggleInfo,
...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 || toggleInfo) && (
<span className={styles.labelWithInfo}>
{toggleLabel}
{toggleLabel && toggleInfo && <span aria-hidden="true" className={styles.divider} />}
{toggleInfo && (
<span
className={styles.info}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
{toggleInfo}
</span>
)}
</span>
)}
</ToggleCSD>
</FormField>
);
}}
/>
);
};
13 changes: 13 additions & 0 deletions frontend/src/components/form/Toogle/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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;
toggleInfo?: ReactNode;
};
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
Loading