1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-22 01:16:07 +02:00

feat: Align switches in table actions (#1082)

* feat: strateges state changing switch

* refactor: ActionCell for table

* fix: remove image clipping for webhook icons

* feat: align addons switch in table

* feat: align enviromnemnts table switch

* fix: disallow turning off protected environment

* refactor: move environment table sub-components

* feat: add predefined badge to default environment

* feat: environment reorder handle hightlight

* fix: environment table padding when searching

* Update src/hooks/api/actions/useStrategiesApi/useStrategiesApi.ts

Co-authored-by: olav <mail@olav.io>

* refactor: toggle addon promise

* remove dragging highlight

* fix: strategy switch tooltip

* fix: switch tooltips

Co-authored-by: olav <mail@olav.io>
This commit is contained in:
Tymoteusz Czech 2022-06-14 14:32:16 +02:00 committed by GitHub
parent 407e3a5f55
commit 51e5939f68
17 changed files with 443 additions and 302 deletions

View File

@ -12,7 +12,6 @@ const style: React.CSSProperties = {
width: '32.5px', width: '32.5px',
height: '32.5px', height: '32.5px',
marginRight: '16px', marginRight: '16px',
borderRadius: '50%',
}; };
interface IAddonIconProps { interface IAddonIconProps {

View File

@ -87,7 +87,6 @@ export const AvailableAddons = ({
sortType: 'alphanumeric', sortType: 'alphanumeric',
}, },
{ {
Header: 'Actions',
id: 'Actions', id: 'Actions',
align: 'center', align: 'center',
Cell: ({ row: { original } }: any) => ( Cell: ({ row: { original } }: any) => (

View File

@ -52,10 +52,13 @@ export const ConfiguredAddons = () => {
setToastData({ setToastData({
type: 'success', type: 'success',
title: 'Success', title: 'Success',
text: 'Addon state switched successfully', text: !addon.enabled
? 'Addon is now active'
: 'Addon is now disabled',
}); });
} catch (error: unknown) { } catch (error: unknown) {
setToastApiError(formatUnknownError(error)); setToastApiError(formatUnknownError(error));
throw error; // caught by optimistic update
} }
}, },
[setToastApiError, refetchAddons, setToastData, updateAddon] [setToastApiError, refetchAddons, setToastData, updateAddon]
@ -96,12 +99,16 @@ export const ConfiguredAddons = () => {
Header: 'Actions', Header: 'Actions',
id: 'Actions', id: 'Actions',
align: 'center', align: 'center',
Cell: ({ row: { original } }: any) => ( Cell: ({
row: { original },
}: {
row: { original: IAddon };
}) => (
<ConfiguredAddonsActionsCell <ConfiguredAddonsActionsCell
setShowDelete={setShowDelete} setShowDelete={setShowDelete}
toggleAddon={toggleAddon} toggleAddon={toggleAddon}
setDeletedAddon={setDeletedAddon} setDeletedAddon={setDeletedAddon}
original={original as IAddon} original={original}
/> />
), ),
width: 150, width: 150,

View File

@ -1,7 +1,9 @@
import { Visibility, VisibilityOff, Edit, Delete } from '@mui/icons-material'; import { Edit, Delete } from '@mui/icons-material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { Tooltip } from '@mui/material';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch';
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell'; import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
import { useOptimisticUpdate } from 'component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/hooks/useOptimisticUpdate';
import { import {
UPDATE_ADDON, UPDATE_ADDON,
DELETE_ADDON, DELETE_ADDON,
@ -23,19 +25,32 @@ export const ConfiguredAddonsActionsCell = ({
original, original,
}: IConfiguredAddonsActionsCellProps) => { }: IConfiguredAddonsActionsCellProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [isEnabled, setIsEnabled, rollbackIsChecked] =
useOptimisticUpdate<boolean>(original.enabled);
const onClick = () => {
setIsEnabled(!isEnabled);
toggleAddon(original).catch(rollbackIsChecked);
};
return ( return (
<ActionCell> <ActionCell>
<PermissionIconButton <Tooltip
permission={UPDATE_ADDON} title={
onClick={() => toggleAddon(original)} isEnabled
tooltipProps={{ title: 'Toggle addon' }} ? `Disable addon ${original.provider}`
: `Enable addon ${original.provider}`
}
arrow
describeChild
> >
<ConditionallyRender <PermissionSwitch
condition={original.enabled} permission={UPDATE_ADDON}
show={<Visibility />} checked={isEnabled}
elseShow={<VisibilityOff />} onClick={onClick}
/> />
</PermissionIconButton> </Tooltip>
<ActionCell.Divider />
<PermissionIconButton <PermissionIconButton
permission={UPDATE_ADDON} permission={UPDATE_ADDON}
tooltipProps={{ title: 'Edit Addon' }} tooltipProps={{ title: 'Edit Addon' }}

View File

@ -0,0 +1,15 @@
import { makeStyles } from 'tss-react/mui';
export const useStyles = makeStyles()(theme => ({
container: {
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
padding: theme.spacing(0, 1.5),
},
divider: {
borderColor: theme.palette.dividerAlternative,
height: theme.spacing(3),
margin: theme.spacing(0, 2),
},
}));

View File

@ -1,14 +1,26 @@
import { Box } from '@mui/material'; import { Box, Divider } from '@mui/material';
import { ReactNode } from 'react'; import { FC, VFC } from 'react';
import { useStyles } from './ActionCell.styles';
interface IContextActionsCellProps { const ActionCellDivider: VFC = () => {
children: ReactNode; const { classes } = useStyles();
}
export const ActionCell = ({ children }: IContextActionsCellProps) => {
return ( return (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', px: 2 }}> <Divider
{children} className={classes.divider}
</Box> orientation="vertical"
variant="middle"
/>
); );
}; };
const ActionCellComponent: FC & {
Divider: typeof ActionCellDivider;
} = ({ children }) => {
const { classes } = useStyles();
return <Box className={classes.container}>{children}</Box>;
};
ActionCellComponent.Divider = ActionCellDivider;
export const ActionCell = ActionCellComponent;

View File

@ -22,7 +22,7 @@ export const useStyles = makeStyles()(theme => ({
infoContainer: { infoContainer: {
marginTop: '1rem', marginTop: '1rem',
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-around',
}, },
infoInnerContainer: { infoInnerContainer: {
textAlign: 'center', textAlign: 'center',

View File

@ -3,12 +3,7 @@ import {
DELETE_ENVIRONMENT, DELETE_ENVIRONMENT,
UPDATE_ENVIRONMENT, UPDATE_ENVIRONMENT,
} from 'component/providers/AccessProvider/permissions'; } from 'component/providers/AccessProvider/permissions';
import { import { Edit, Delete } from '@mui/icons-material';
Edit,
Delete,
DragIndicator,
PowerSettingsNew,
} from '@mui/icons-material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { IconButton, Tooltip } from '@mui/material'; import { IconButton, Tooltip } from '@mui/material';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@ -16,14 +11,14 @@ import AccessContext from 'contexts/AccessContext';
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import { IEnvironment } from 'interfaces/environments'; import { IEnvironment } from 'interfaces/environments';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import EnvironmentToggleConfirm from '../EnvironmentToggleConfirm/EnvironmentToggleConfirm'; import EnvironmentToggleConfirm from '../../EnvironmentToggleConfirm/EnvironmentToggleConfirm';
import EnvironmentDeleteConfirm from '../EnvironmentDeleteConfirm/EnvironmentDeleteConfirm'; import EnvironmentDeleteConfirm from '../../EnvironmentDeleteConfirm/EnvironmentDeleteConfirm';
import useEnvironmentApi from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi'; import useEnvironmentApi from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
import useProjectRolePermissions from 'hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions'; import useProjectRolePermissions from 'hooks/api/getters/useProjectRolePermissions/useProjectRolePermissions';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { useId } from 'hooks/useId'; import { useId } from 'hooks/useId';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch';
interface IEnvironmentTableActionsProps { interface IEnvironmentTableActionsProps {
environment: IEnvironment; environment: IEnvironment;
@ -35,7 +30,6 @@ export const EnvironmentActionCell = ({
const navigate = useNavigate(); const navigate = useNavigate();
const { hasAccess } = useContext(AccessContext); const { hasAccess } = useContext(AccessContext);
const updatePermission = hasAccess(UPDATE_ENVIRONMENT); const updatePermission = hasAccess(UPDATE_ENVIRONMENT);
const { searchQuery } = useSearchHighlightContext();
const { setToastApiError, setToastData } = useToast(); const { setToastApiError, setToastData } = useToast();
const { refetchEnvironments } = useEnvironments(); const { refetchEnvironments } = useEnvironments();
@ -73,8 +67,8 @@ export const EnvironmentActionCell = ({
const handleToggleEnvironmentOn = async () => { const handleToggleEnvironmentOn = async () => {
try { try {
await toggleEnvironmentOn(environment.name);
setToggleModal(false); setToggleModal(false);
await toggleEnvironmentOn(environment.name);
setToastData({ setToastData({
type: 'success', type: 'success',
title: 'Project environment enabled', title: 'Project environment enabled',
@ -88,8 +82,8 @@ export const EnvironmentActionCell = ({
const handleToggleEnvironmentOff = async () => { const handleToggleEnvironmentOff = async () => {
try { try {
await toggleEnvironmentOff(environment.name);
setToggleModal(false); setToggleModal(false);
await toggleEnvironmentOff(environment.name);
setToastData({ setToastData({
type: 'success', type: 'success',
title: 'Project environment disabled', title: 'Project environment disabled',
@ -102,37 +96,28 @@ export const EnvironmentActionCell = ({
}; };
const toggleIconTooltip = environment.enabled const toggleIconTooltip = environment.enabled
? 'Disable environment' ? `Disable environment ${environment.name}`
: 'Enable environment'; : `Enable environment ${environment.name}`;
const editId = useId(); const editId = useId();
const deleteId = useId(); const deleteId = useId();
// Allow drag and drop if the user is permitted to reorder environments.
// Disable drag and drop while searching since some rows may be hidden.
const enableDragAndDrop = updatePermission && !searchQuery;
return ( return (
<ActionCell> <ActionCell>
<ConditionallyRender
condition={enableDragAndDrop}
show={
<IconButton size="large">
<DragIndicator titleAccess="Drag" cursor="grab" />
</IconButton>
}
/>
<ConditionallyRender <ConditionallyRender
condition={updatePermission} condition={updatePermission}
show={ show={
<Tooltip title={toggleIconTooltip} arrow> <>
<IconButton <Tooltip title={toggleIconTooltip} arrow describeChild>
onClick={() => setToggleModal(true)} <PermissionSwitch
size="large" permission={UPDATE_ENVIRONMENT}
> checked={environment.enabled}
<PowerSettingsNew /> onClick={() => setToggleModal(true)}
</IconButton> disabled={environment.protected}
</Tooltip> />
</Tooltip>
<ActionCell.Divider />
</>
} }
/> />
<ConditionallyRender <ConditionallyRender
@ -141,7 +126,7 @@ export const EnvironmentActionCell = ({
<Tooltip <Tooltip
title={ title={
environment.protected environment.protected
? 'You cannot edit environment' ? 'You cannot edit protected environment'
: 'Edit environment' : 'Edit environment'
} }
arrow arrow
@ -169,9 +154,10 @@ export const EnvironmentActionCell = ({
<Tooltip <Tooltip
title={ title={
environment.protected environment.protected
? 'You cannot delete environment' ? 'You cannot delete protected environment'
: 'Delete environment' : 'Delete environment'
} }
describeChild
arrow arrow
> >
<span id={deleteId}> <span id={deleteId}>

View File

@ -0,0 +1,42 @@
import { useContext, VFC } from 'react';
import { styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useSearchHighlightContext } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { Box, IconButton } from '@mui/material';
import { CloudCircle, DragIndicator } from '@mui/icons-material';
import { UPDATE_ENVIRONMENT } from 'component/providers/AccessProvider/permissions';
import AccessContext from 'contexts/AccessContext';
const DragIcon = styled(IconButton)(
({ theme }) => `
padding: ${theme.spacing(0, 1, 0, 0)};
cursor: inherit;
transition: color 0.2s ease-in-out;
`
);
export const EnvironmentIconCell: VFC = () => {
const { hasAccess } = useContext(AccessContext);
const updatePermission = hasAccess(UPDATE_ENVIRONMENT);
const { searchQuery } = useSearchHighlightContext();
// Allow drag and drop if the user is permitted to reorder environments.
// Disable drag and drop while searching since some rows may be hidden.
const enableDragAndDrop = updatePermission && !searchQuery;
return (
<Box sx={{ display: 'flex', alignItems: 'center', pl: 2 }}>
<ConditionallyRender
condition={enableDragAndDrop}
show={
<DragIcon size="large" disableRipple disabled>
<DragIndicator
titleAccess="Drag to reorder"
cursor="grab"
/>
</DragIcon>
}
/>
<CloudCircle color="disabled" />
</Box>
);
};

View File

@ -21,6 +21,10 @@ export const EnvironmentNameCell = ({
condition={!environment.enabled} condition={!environment.enabled}
show={<StatusBadge severity="warning">Disabled</StatusBadge>} show={<StatusBadge severity="warning">Disabled</StatusBadge>}
/> />
<ConditionallyRender
condition={environment.protected}
show={<StatusBadge severity="success">Predefined</StatusBadge>}
/>
</TextCell> </TextCell>
); );
}; };

View File

@ -11,11 +11,6 @@ import {
import { useCallback } from 'react'; import { useCallback } from 'react';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { Alert, styled, TableBody } from '@mui/material'; import { Alert, styled, TableBody } from '@mui/material';
import { CloudCircle } from '@mui/icons-material';
import { IconCell } from 'component/common/Table/cells/IconCell/IconCell';
import { EnvironmentActionCell } from 'component/environments/EnvironmentActionCell/EnvironmentActionCell';
import { EnvironmentNameCell } from 'component/environments/EnvironmentNameCell/EnvironmentNameCell';
import { EnvironmentRow } from 'component/environments/EnvironmentRow/EnvironmentRow';
import { MoveListItem } from 'hooks/useDragItem'; import { MoveListItem } from 'hooks/useDragItem';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import useEnvironmentApi, { import useEnvironmentApi, {
@ -23,6 +18,10 @@ import useEnvironmentApi, {
} from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi'; } from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { EnvironmentRow } from './EnvironmentRow/EnvironmentRow';
import { EnvironmentNameCell } from './EnvironmentNameCell/EnvironmentNameCell';
import { EnvironmentActionCell } from './EnvironmentActionCell/EnvironmentActionCell';
import { EnvironmentIconCell } from './EnvironmentIconCell/EnvironmentIconCell';
import { Search } from 'component/common/Search/Search'; import { Search } from 'component/common/Search/Search';
const StyledAlert = styled(Alert)(({ theme }) => ({ const StyledAlert = styled(Alert)(({ theme }) => ({
@ -137,7 +136,7 @@ const COLUMNS = [
{ {
id: 'Icon', id: 'Icon',
width: '1%', width: '1%',
Cell: () => <IconCell icon={<CloudCircle color="disabled" />} />, Cell: () => <EnvironmentIconCell />,
disableGlobalFilter: true, disableGlobalFilter: true,
}, },
{ {

View File

@ -1,17 +1,7 @@
import { useState, useMemo } from 'react'; import { useState, useMemo, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { IconButton, Tooltip, Box } from '@mui/material'; import { Box } from '@mui/material';
import { import { Extension } from '@mui/icons-material';
Delete,
Edit,
Extension,
Visibility,
VisibilityOff,
} from '@mui/icons-material';
import {
DELETE_STRATEGY,
UPDATE_STRATEGY,
} from 'component/providers/AccessProvider/permissions';
import { import {
Table, Table,
SortableTableHeader, SortableTableHeader,
@ -20,11 +10,11 @@ import {
TableRow, TableRow,
TablePlaceholder, TablePlaceholder,
} from 'component/common/Table'; } from 'component/common/Table';
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader'; import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { Dialogue } from 'component/common/Dialogue/Dialogue';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { formatStrategyName } from 'utils/strategyNames'; import { formatStrategyName } from 'utils/strategyNames';
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies'; import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
import useStrategiesApi from 'hooks/api/actions/useStrategiesApi/useStrategiesApi'; import useStrategiesApi from 'hooks/api/actions/useStrategiesApi/useStrategiesApi';
@ -37,6 +27,9 @@ import { sortTypes } from 'utils/sortTypes';
import { useTable, useGlobalFilter, useSortBy } from 'react-table'; import { useTable, useGlobalFilter, useSortBy } from 'react-table';
import { AddStrategyButton } from './AddStrategyButton/AddStrategyButton'; import { AddStrategyButton } from './AddStrategyButton/AddStrategyButton';
import { StatusBadge } from 'component/common/StatusBadge/StatusBadge'; import { StatusBadge } from 'component/common/StatusBadge/StatusBadge';
import { StrategySwitch } from './StrategySwitch/StrategySwitch';
import { StrategyEditButton } from './StrategyEditButton/StrategyEditButton';
import { StrategyDeleteButton } from './StrategyDeleteButton/StrategyDeleteButton';
import { Search } from 'component/common/Search/Search'; import { Search } from 'component/common/Search/Search';
interface IDialogueMetaData { interface IDialogueMetaData {
@ -78,6 +71,85 @@ export const StrategiesList = () => {
); );
}, [strategies, loading]); }, [strategies, loading]);
const onToggle = useCallback(
(strategy: IStrategy) => (deprecated: boolean) => {
if (deprecated) {
setDialogueMetaData({
show: true,
title: 'Really reactivate strategy?',
onConfirm: async () => {
try {
await reactivateStrategy(strategy);
refetchStrategies();
setToastData({
type: 'success',
title: 'Success',
text: 'Strategy reactivated successfully',
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
},
});
} else {
setDialogueMetaData({
show: true,
title: 'Really deprecate strategy?',
onConfirm: async () => {
try {
await deprecateStrategy(strategy);
refetchStrategies();
setToastData({
type: 'success',
title: 'Success',
text: 'Strategy deprecated successfully',
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
},
});
}
},
[
deprecateStrategy,
reactivateStrategy,
refetchStrategies,
setToastApiError,
setToastData,
]
);
const onDeleteStrategy = useCallback(
(strategy: IStrategy) => {
setDialogueMetaData({
show: true,
title: 'Really delete strategy?',
onConfirm: async () => {
try {
await removeStrategy(strategy);
refetchStrategies();
setToastData({
type: 'success',
title: 'Success',
text: 'Strategy deleted successfully',
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
},
});
},
[removeStrategy, refetchStrategies, setToastApiError, setToastData]
);
const onEditStrategy = useCallback(
(strategy: IStrategy) => {
navigate(`/strategies/${strategy.name}/edit`);
},
[navigate]
);
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ {
@ -86,7 +158,7 @@ export const StrategiesList = () => {
<Box <Box
data-loading data-loading
sx={{ sx={{
pl: 2, pl: 3,
pr: 1, pr: 1,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@ -134,18 +206,22 @@ export const StrategiesList = () => {
id: 'Actions', id: 'Actions',
align: 'center', align: 'center',
Cell: ({ row: { original } }: any) => ( Cell: ({ row: { original } }: any) => (
<Box <ActionCell>
sx={{ display: 'flex', justifyContent: 'flex-end' }} <StrategySwitch
data-loading deprecated={original.deprecated}
> onToggle={onToggle(original)}
<ConditionallyRender disabled={original.name === 'default'}
condition={original.deprecated}
show={reactivateButton(original)}
elseShow={deprecateButton(original)}
/> />
{editButton(original)} <ActionCell.Divider />
{deleteButton(original)} <StrategyEditButton
</Box> strategy={original}
onClick={() => onEditStrategy(original)}
/>
<StrategyDeleteButton
strategy={original}
onClick={() => onDeleteStrategy(original)}
/>
</ActionCell>
), ),
width: 150, width: 150,
disableGlobalFilter: true, disableGlobalFilter: true,
@ -161,8 +237,7 @@ export const StrategiesList = () => {
sortType: 'number', sortType: 'number',
}, },
], ],
// eslint-disable-next-line react-hooks/exhaustive-deps [onToggle, onEditStrategy, onDeleteStrategy]
[]
); );
const initialState = useMemo( const initialState = useMemo(
@ -195,152 +270,6 @@ export const StrategiesList = () => {
useSortBy useSortBy
); );
const onReactivateStrategy = (strategy: IStrategy) => {
setDialogueMetaData({
show: true,
title: 'Really reactivate strategy?',
onConfirm: async () => {
try {
await reactivateStrategy(strategy);
refetchStrategies();
setToastData({
type: 'success',
title: 'Success',
text: 'Strategy reactivated successfully',
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
},
});
};
const onDeprecateStrategy = (strategy: IStrategy) => {
setDialogueMetaData({
show: true,
title: 'Really deprecate strategy?',
onConfirm: async () => {
try {
await deprecateStrategy(strategy);
refetchStrategies();
setToastData({
type: 'success',
title: 'Success',
text: 'Strategy deprecated successfully',
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
},
});
};
const onDeleteStrategy = (strategy: IStrategy) => {
setDialogueMetaData({
show: true,
title: 'Really delete strategy?',
onConfirm: async () => {
try {
await removeStrategy(strategy);
refetchStrategies();
setToastData({
type: 'success',
title: 'Success',
text: 'Strategy deleted successfully',
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
},
});
};
const reactivateButton = (strategy: IStrategy) => (
<PermissionIconButton
onClick={() => onReactivateStrategy(strategy)}
permission={UPDATE_STRATEGY}
tooltipProps={{ title: 'Reactivate activation strategy' }}
>
<VisibilityOff />
</PermissionIconButton>
);
const deprecateButton = (strategy: IStrategy) => (
<ConditionallyRender
condition={strategy.name === 'default'}
show={
<Tooltip title="You cannot deprecate the default strategy">
<div>
<IconButton disabled size="large">
<Visibility titleAccess="Deprecate strategy" />
</IconButton>
</div>
</Tooltip>
}
elseShow={
<div>
<PermissionIconButton
onClick={() => onDeprecateStrategy(strategy)}
permission={UPDATE_STRATEGY}
tooltipProps={{ title: 'Deprecate strategy' }}
>
<Visibility />
</PermissionIconButton>
</div>
}
/>
);
const editButton = (strategy: IStrategy) => (
<ConditionallyRender
condition={strategy?.editable}
show={
<PermissionIconButton
onClick={() =>
navigate(`/strategies/${strategy?.name}/edit`)
}
permission={UPDATE_STRATEGY}
tooltipProps={{ title: 'Edit strategy' }}
>
<Edit />
</PermissionIconButton>
}
elseShow={
<Tooltip title="You cannot edit a built-in strategy" arrow>
<div>
<IconButton disabled size="large">
<Edit titleAccess="Edit strategy" />
</IconButton>
</div>
</Tooltip>
}
/>
);
const deleteButton = (strategy: IStrategy) => (
<ConditionallyRender
condition={strategy?.editable}
show={
<PermissionIconButton
onClick={() => onDeleteStrategy(strategy)}
permission={DELETE_STRATEGY}
tooltipProps={{ title: 'Delete strategy' }}
>
<Delete />
</PermissionIconButton>
}
elseShow={
<Tooltip title="You cannot delete a built-in strategy" arrow>
<div>
<IconButton disabled size="large">
<Delete titleAccess="Delete strategy" />
</IconButton>
</div>
</Tooltip>
}
/>
);
const onDialogConfirm = () => { const onDialogConfirm = () => {
dialogueMetaData?.onConfirm(); dialogueMetaData?.onConfirm();
setDialogueMetaData((prev: IDialogueMetaData) => ({ setDialogueMetaData((prev: IDialogueMetaData) => ({

View File

@ -0,0 +1,41 @@
import { VFC } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { Delete } from '@mui/icons-material';
import { IconButton, Tooltip } from '@mui/material';
import { IStrategy } from 'interfaces/strategy';
import { DELETE_STRATEGY } from 'component/providers/AccessProvider/permissions';
interface IStrategyDeleteButtonProps {
strategy: IStrategy;
onClick: () => void;
}
export const StrategyDeleteButton: VFC<IStrategyDeleteButtonProps> = ({
strategy,
onClick,
}) => {
return (
<ConditionallyRender
condition={strategy?.editable}
show={
<PermissionIconButton
onClick={onClick}
permission={DELETE_STRATEGY}
tooltipProps={{ title: 'Delete strategy' }}
>
<Delete />
</PermissionIconButton>
}
elseShow={
<Tooltip title="You cannot delete a built-in strategy" arrow>
<div>
<IconButton disabled size="large">
<Delete titleAccess="Delete strategy" />
</IconButton>
</div>
</Tooltip>
}
/>
);
};

View File

@ -0,0 +1,39 @@
import { VFC } from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { Edit } from '@mui/icons-material';
import { IconButton, Tooltip } from '@mui/material';
import { UPDATE_STRATEGY } from 'component/providers/AccessProvider/permissions';
import { IStrategy } from 'interfaces/strategy';
interface IStrategyEditButtonProps {
strategy: IStrategy;
onClick: () => void;
}
export const StrategyEditButton: VFC<IStrategyEditButtonProps> = ({
strategy,
onClick,
}) => (
<ConditionallyRender
condition={strategy?.editable}
show={
<PermissionIconButton
onClick={onClick}
permission={UPDATE_STRATEGY}
tooltipProps={{ title: 'Edit strategy' }}
>
<Edit />
</PermissionIconButton>
}
elseShow={
<Tooltip title="You cannot edit a built-in strategy" arrow>
<div>
<IconButton disabled size="large">
<Edit titleAccess="Edit strategy" />
</IconButton>
</div>
</Tooltip>
}
/>
);

View File

@ -0,0 +1,43 @@
import { VFC } from 'react';
import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch';
import { UPDATE_STRATEGY } from 'component/providers/AccessProvider/permissions';
import { Tooltip } from '@mui/material';
import { useId } from 'hooks/useId';
interface IStrategySwitchProps {
deprecated: boolean;
onToggle: (state: boolean) => void;
disabled?: boolean;
}
export const StrategySwitch: VFC<IStrategySwitchProps> = ({
deprecated,
disabled,
onToggle,
}) => {
const onClick = () => {
onToggle(deprecated);
};
const id = useId();
const title = deprecated
? 'Excluded from strategy list'
: 'Included in strategy list';
return (
<Tooltip
title={disabled ? 'You cannot disable default strategy' : title}
describeChild
arrow
>
<div id={id} role="tooltip">
<PermissionSwitch
checked={!deprecated}
permission={UPDATE_STRATEGY}
onClick={onClick}
disabled={disabled}
/>
</div>
</Tooltip>
);
};

View File

@ -1,83 +1,94 @@
import { IStrategyPayload } from 'interfaces/strategy'; import { IStrategyPayload } from 'interfaces/strategy';
import { useCallback } from 'react';
import useAPI from '../useApi/useApi'; import useAPI from '../useApi/useApi';
const URI = 'api/admin/strategies';
const useStrategiesApi = () => { const useStrategiesApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({ const { makeRequest, createRequest, errors, loading } = useAPI({
propagateErrors: true, propagateErrors: true,
}); });
const URI = 'api/admin/strategies';
const createStrategy = async (strategy: IStrategyPayload) => { const createStrategy = useCallback(
const req = createRequest(URI, { async (strategy: IStrategyPayload) => {
method: 'POST', const req = createRequest(URI, {
body: JSON.stringify(strategy), method: 'POST',
}); body: JSON.stringify(strategy),
});
try { return makeRequest(req.caller, req.id);
const res = await makeRequest(req.caller, req.id); },
[createRequest, makeRequest]
);
return res; const updateStrategy = useCallback(
} catch (e) { async (strategy: IStrategyPayload) => {
throw e; const path = `${URI}/${strategy.name}`;
} const req = createRequest(path, {
}; method: 'PUT',
body: JSON.stringify(strategy),
});
const updateStrategy = async (strategy: IStrategyPayload) => { try {
const path = `${URI}/${strategy.name}`; const res = await makeRequest(req.caller, req.id);
const req = createRequest(path, {
method: 'PUT',
body: JSON.stringify(strategy),
});
try { return res;
const res = await makeRequest(req.caller, req.id); } catch (e) {
throw e;
}
},
[createRequest, makeRequest]
);
return res; const removeStrategy = useCallback(
} catch (e) { async (strategy: IStrategyPayload) => {
throw e; const path = `${URI}/${strategy.name}`;
} const req = createRequest(path, { method: 'DELETE' });
};
const removeStrategy = async (strategy: IStrategyPayload) => { try {
const path = `${URI}/${strategy.name}`; const res = await makeRequest(req.caller, req.id);
const req = createRequest(path, { method: 'DELETE' });
try { return res;
const res = await makeRequest(req.caller, req.id); } catch (e) {
throw e;
}
},
[createRequest, makeRequest]
);
return res; const deprecateStrategy = useCallback(
} catch (e) { async (strategy: IStrategyPayload) => {
throw e; const path = `${URI}/${strategy.name}/deprecate`;
} const req = createRequest(path, {
}; method: 'POST',
});
const deprecateStrategy = async (strategy: IStrategyPayload) => { try {
const path = `${URI}/${strategy.name}/deprecate`; const res = await makeRequest(req.caller, req.id);
const req = createRequest(path, {
method: 'POST',
});
try { return res;
const res = await makeRequest(req.caller, req.id); } catch (e) {
throw e;
}
},
[createRequest, makeRequest]
);
return res; const reactivateStrategy = useCallback(
} catch (e) { async (strategy: IStrategyPayload) => {
throw e; const path = `${URI}/${strategy.name}/reactivate`;
} const req = createRequest(path, { method: 'POST' });
};
const reactivateStrategy = async (strategy: IStrategyPayload) => { try {
const path = `${URI}/${strategy.name}/reactivate`; const res = await makeRequest(req.caller, req.id);
const req = createRequest(path, { method: 'POST' });
try { return res;
const res = await makeRequest(req.caller, req.id); } catch (e) {
throw e;
return res; }
} catch (e) { },
throw e; [createRequest, makeRequest]
} );
};
return { return {
createStrategy, createStrategy,