1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-18 00:19:49 +01:00

feat: Align switches in table actions ()

* 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
frontend/src
component
addons/AddonList
AddonIcon
AvailableAddons
ConfiguredAddons
common/Table/cells/ActionCell
environments
EnvironmentCard
EnvironmentTable
EnvironmentActionCell
EnvironmentIconCell
EnvironmentNameCell
EnvironmentRow
EnvironmentTable.tsx
strategies/StrategiesList
hooks/api/actions/useStrategiesApi

View File

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

View File

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

View File

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

View File

@ -1,7 +1,9 @@
import { Visibility, VisibilityOff, Edit, Delete } from '@mui/icons-material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Edit, Delete } from '@mui/icons-material';
import { Tooltip } from '@mui/material';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch';
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
import { useOptimisticUpdate } from 'component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/hooks/useOptimisticUpdate';
import {
UPDATE_ADDON,
DELETE_ADDON,
@ -23,19 +25,32 @@ export const ConfiguredAddonsActionsCell = ({
original,
}: IConfiguredAddonsActionsCellProps) => {
const navigate = useNavigate();
const [isEnabled, setIsEnabled, rollbackIsChecked] =
useOptimisticUpdate<boolean>(original.enabled);
const onClick = () => {
setIsEnabled(!isEnabled);
toggleAddon(original).catch(rollbackIsChecked);
};
return (
<ActionCell>
<PermissionIconButton
permission={UPDATE_ADDON}
onClick={() => toggleAddon(original)}
tooltipProps={{ title: 'Toggle addon' }}
<Tooltip
title={
isEnabled
? `Disable addon ${original.provider}`
: `Enable addon ${original.provider}`
}
arrow
describeChild
>
<ConditionallyRender
condition={original.enabled}
show={<Visibility />}
elseShow={<VisibilityOff />}
<PermissionSwitch
permission={UPDATE_ADDON}
checked={isEnabled}
onClick={onClick}
/>
</PermissionIconButton>
</Tooltip>
<ActionCell.Divider />
<PermissionIconButton
permission={UPDATE_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 { ReactNode } from 'react';
import { Box, Divider } from '@mui/material';
import { FC, VFC } from 'react';
import { useStyles } from './ActionCell.styles';
interface IContextActionsCellProps {
children: ReactNode;
}
export const ActionCell = ({ children }: IContextActionsCellProps) => {
const ActionCellDivider: VFC = () => {
const { classes } = useStyles();
return (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', px: 2 }}>
{children}
</Box>
<Divider
className={classes.divider}
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: {
marginTop: '1rem',
display: 'flex',
justifyContent: 'space-between',
justifyContent: 'space-around',
},
infoInnerContainer: {
textAlign: 'center',

View File

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

View File

@ -11,11 +11,6 @@ import {
import { useCallback } from 'react';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
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 useToast from 'hooks/useToast';
import useEnvironmentApi, {
@ -23,6 +18,10 @@ import useEnvironmentApi, {
} from 'hooks/api/actions/useEnvironmentApi/useEnvironmentApi';
import { formatUnknownError } from 'utils/formatUnknownError';
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';
const StyledAlert = styled(Alert)(({ theme }) => ({
@ -137,7 +136,7 @@ const COLUMNS = [
{
id: 'Icon',
width: '1%',
Cell: () => <IconCell icon={<CloudCircle color="disabled" />} />,
Cell: () => <EnvironmentIconCell />,
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 { IconButton, Tooltip, Box } from '@mui/material';
import {
Delete,
Edit,
Extension,
Visibility,
VisibilityOff,
} from '@mui/icons-material';
import {
DELETE_STRATEGY,
UPDATE_STRATEGY,
} from 'component/providers/AccessProvider/permissions';
import { Box } from '@mui/material';
import { Extension } from '@mui/icons-material';
import {
Table,
SortableTableHeader,
@ -20,11 +10,11 @@ import {
TableRow,
TablePlaceholder,
} from 'component/common/Table';
import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
import { formatStrategyName } from 'utils/strategyNames';
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
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 { AddStrategyButton } from './AddStrategyButton/AddStrategyButton';
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';
interface IDialogueMetaData {
@ -78,6 +71,85 @@ export const StrategiesList = () => {
);
}, [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(
() => [
{
@ -86,7 +158,7 @@ export const StrategiesList = () => {
<Box
data-loading
sx={{
pl: 2,
pl: 3,
pr: 1,
display: 'flex',
alignItems: 'center',
@ -134,18 +206,22 @@ export const StrategiesList = () => {
id: 'Actions',
align: 'center',
Cell: ({ row: { original } }: any) => (
<Box
sx={{ display: 'flex', justifyContent: 'flex-end' }}
data-loading
>
<ConditionallyRender
condition={original.deprecated}
show={reactivateButton(original)}
elseShow={deprecateButton(original)}
<ActionCell>
<StrategySwitch
deprecated={original.deprecated}
onToggle={onToggle(original)}
disabled={original.name === 'default'}
/>
{editButton(original)}
{deleteButton(original)}
</Box>
<ActionCell.Divider />
<StrategyEditButton
strategy={original}
onClick={() => onEditStrategy(original)}
/>
<StrategyDeleteButton
strategy={original}
onClick={() => onDeleteStrategy(original)}
/>
</ActionCell>
),
width: 150,
disableGlobalFilter: true,
@ -161,8 +237,7 @@ export const StrategiesList = () => {
sortType: 'number',
},
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
[onToggle, onEditStrategy, onDeleteStrategy]
);
const initialState = useMemo(
@ -195,152 +270,6 @@ export const StrategiesList = () => {
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 = () => {
dialogueMetaData?.onConfirm();
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 { useCallback } from 'react';
import useAPI from '../useApi/useApi';
const URI = 'api/admin/strategies';
const useStrategiesApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({
propagateErrors: true,
});
const URI = 'api/admin/strategies';
const createStrategy = async (strategy: IStrategyPayload) => {
const req = createRequest(URI, {
method: 'POST',
body: JSON.stringify(strategy),
});
const createStrategy = useCallback(
async (strategy: IStrategyPayload) => {
const req = createRequest(URI, {
method: 'POST',
body: JSON.stringify(strategy),
});
try {
const res = await makeRequest(req.caller, req.id);
return makeRequest(req.caller, req.id);
},
[createRequest, makeRequest]
);
return res;
} catch (e) {
throw e;
}
};
const updateStrategy = useCallback(
async (strategy: IStrategyPayload) => {
const path = `${URI}/${strategy.name}`;
const req = createRequest(path, {
method: 'PUT',
body: JSON.stringify(strategy),
});
const updateStrategy = async (strategy: IStrategyPayload) => {
const path = `${URI}/${strategy.name}`;
const req = createRequest(path, {
method: 'PUT',
body: JSON.stringify(strategy),
});
try {
const res = await makeRequest(req.caller, req.id);
try {
const res = await makeRequest(req.caller, req.id);
return res;
} catch (e) {
throw e;
}
},
[createRequest, makeRequest]
);
return res;
} catch (e) {
throw e;
}
};
const removeStrategy = useCallback(
async (strategy: IStrategyPayload) => {
const path = `${URI}/${strategy.name}`;
const req = createRequest(path, { method: 'DELETE' });
const removeStrategy = async (strategy: IStrategyPayload) => {
const path = `${URI}/${strategy.name}`;
const req = createRequest(path, { method: 'DELETE' });
try {
const res = await makeRequest(req.caller, req.id);
try {
const res = await makeRequest(req.caller, req.id);
return res;
} catch (e) {
throw e;
}
},
[createRequest, makeRequest]
);
return res;
} catch (e) {
throw e;
}
};
const deprecateStrategy = useCallback(
async (strategy: IStrategyPayload) => {
const path = `${URI}/${strategy.name}/deprecate`;
const req = createRequest(path, {
method: 'POST',
});
const deprecateStrategy = async (strategy: IStrategyPayload) => {
const path = `${URI}/${strategy.name}/deprecate`;
const req = createRequest(path, {
method: 'POST',
});
try {
const res = await makeRequest(req.caller, req.id);
try {
const res = await makeRequest(req.caller, req.id);
return res;
} catch (e) {
throw e;
}
},
[createRequest, makeRequest]
);
return res;
} catch (e) {
throw e;
}
};
const reactivateStrategy = useCallback(
async (strategy: IStrategyPayload) => {
const path = `${URI}/${strategy.name}/reactivate`;
const req = createRequest(path, { method: 'POST' });
const reactivateStrategy = async (strategy: IStrategyPayload) => {
const path = `${URI}/${strategy.name}/reactivate`;
const req = createRequest(path, { method: 'POST' });
try {
const res = await makeRequest(req.caller, req.id);
try {
const res = await makeRequest(req.caller, req.id);
return res;
} catch (e) {
throw e;
}
};
return res;
} catch (e) {
throw e;
}
},
[createRequest, makeRequest]
);
return {
createStrategy,