1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-06 00:07:44 +01:00
unleash.unleash/frontend/src/component/strategies/StrategiesList/StrategiesList.tsx

495 lines
17 KiB
TypeScript
Raw Normal View History

import { useState, useMemo, useCallback, type FC } from 'react';
import { useNavigate } from 'react-router-dom';
import { Alert, Box, Link, Typography, styled } from '@mui/material';
import Extension from '@mui/icons-material/Extension';
import {
Table,
SortableTableHeader,
TableBody,
TableCell,
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 { formatStrategyName } from 'utils/strategyNames';
feat: add new feature strategy create/edit pages (#739) * refactor: add param helper hooks * refactor: remove first add strategy link * refactor: add more types to useStrategies * refactor: port strategy utils to TS * refactor: replace rollout strategy icon * refactor: use a named export for useFeature * refactor: add more types to useFeature * refactor: adjust code box styles * refactor: add missing PermissionButton variant prop * refactor: add missing button icon label * refactor: move common feature components * refactor: fix StrategyConstraints error prop types * refactor: fix GeneralStrategy prop types * feat: add new feature strategy create/edit pages * refactor: remove feature strategies page * refactor: fix types in GeneralStrategy * refactor: use ConstraintAccordion on the new pages * refactor: use ConditionallyRender for remove button * refactor: rename FeatureStrategyForm component * refactor: use the Edit icon for feature strategies * refactor: fix initial edit mode for new constraints * refactor: add FeatureStrategyMenu to closed accordions * refactor: allow editing multiple constraints * refactor: show single-valued constraint value * refactor: increase feature overview strategy width * refactor: add remove button to feature overview strategies * refactor: move createEmptyConstraint to own file * refactor: disable submit button for invalid constraints * refactor: fix nested paragraphs on the metrics page * refactor: move create/edit feature strategy to modal * refactor: always open new constraints in edit mode * refactor: use a PermissionButton for the save button * refactor: remvoe unsaved constraints on cancel * refactor: clarify useConstraintsValidation logic * refactor: remove unused strategy descriptions * refactor: restore Rollout icon * refactor: remove sidebar modal slide animation * refactor: avoid constraint accordion toggle on edit/delete * refactor: truncate long strategy names * refactor: find the correct remove button
2022-03-09 14:59:24 +01:00
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
Feat/custom strategy screen (#722) * feat: setup new screen structure * refactor: strategyParameter * feat: add strategy input errors for required fields * feat: add create strategy to routes * feat: add EditStrategy component * feat: edit strategy view and EditStrategy component * feat: update EditStrategy component * test: update snapshots * fix: styles * test: update snapshots * refactor: rename StrategyForm and fix ts errors * test: update snapshots * fix: remove test route * fix: update PR based on feedback * fix: update PR based on feedback * refactor: restore feature settings (#712) * refactor: resotre feature settings * fix: update PR based on feedback * feat: add feature information in Metadata container * fix: update PR based on feedback * fix: update PR based on feedback Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com> * chore(deps): update dependency @types/react-dom to v17.0.13 * refactor: expect existing TS errors (#767) * refactor: expect existing TS errors * refactor: fail build on new TS errors * fix: styles * refactor: rename StrategyForm and fix ts errors * fix: update PR based on feedback * fix: cleaning up * fix: remove errors and warnings * fix: remove ts-expect-error and fix errors * fix: ts errors * Update src/component/strategies/StrategyView/StrategyView.tsx * Update src/component/strategies/StrategyView/StrategyView.tsx Co-authored-by: Fredrik Strand Oseberg <fredrik.no@gmail.com> Co-authored-by: Renovate Bot <bot@renovateapp.com> Co-authored-by: olav <mail@olav.io>
2022-03-04 23:39:41 +01:00
import useStrategiesApi from 'hooks/api/actions/useStrategiesApi/useStrategiesApi';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import type { IStrategy } from 'interfaces/strategy';
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { sortTypes } from 'utils/sortTypes';
import { useTable, useSortBy } from 'react-table';
import { StrategySwitch } from './StrategySwitch/StrategySwitch';
import { StrategyEditButton } from './StrategyEditButton/StrategyEditButton';
import { StrategyDeleteButton } from './StrategyDeleteButton/StrategyDeleteButton';
import { Badge } from 'component/common/Badge/Badge';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
import { CustomStrategyInfo } from '../CustomStrategyInfo/CustomStrategyInfo';
import { AddStrategyButton } from './AddStrategyButton/AddStrategyButton';
interface IDialogueMetaData {
show: boolean;
title: string;
onConfirm: () => void;
}
const StyledBox = styled(Box)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
}));
const StyledBadge = styled(Badge)(({ theme }) => ({
marginLeft: theme.spacing(1),
display: 'inline-block',
}));
const StyledTypography = styled(Typography)(({ theme }) => ({
display: 'flex',
fontSize: theme.fontSizes.mainHeader,
}));
const Subtitle: FC<{
title: string;
description: string;
link: string;
}> = ({ title, description, link }) => (
<StyledTypography>
{title}
<HelpIcon
htmlTooltip
tooltip={
<>
<Typography
variant='body2'
component='p'
sx={(theme) => ({ marginBottom: theme.spacing(1) })}
>
{description}
</Typography>
<Link href={link} target='_blank' variant='body2'>
Read more in the documentation
</Link>
</>
}
/>
</StyledTypography>
);
const CustomStrategyTitle: FC = () => (
<Box
sx={(theme) => ({
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: theme.spacing(1.5),
})}
>
<Subtitle
title='Custom strategies'
description='Custom activation strategies let you define your own activation strategies to use with Unleash.'
link='https://docs.getunleash.io/reference/custom-activation-strategies'
/>
<AddStrategyButton />
</Box>
);
const PredefinedStrategyTitle = () => (
<Box>
<Subtitle
title='Predefined strategies'
description='Activation strategies let you enable a feature only for a specified audience. Different strategies use different parameters. Predefined strategies are bundled with Unleash.'
link='https://docs.getunleash.io/reference/activation-strategies'
/>
</Box>
);
const StrategyDeprecationWarning = () => (
<Alert severity='warning' sx={{ mb: 2 }}>
Custom strategies are deprecated and may be removed in a future major
version. We recommend not using custom strategies going forward and
instead using the predefined strategies with{' '}
<Link
href={'https://docs.getunleash.io/reference/strategy-constraints'}
target='_blank'
variant='body2'
>
constraints
</Link>
. If you have a need for custom strategies that you cannot support with
constraints, please reach out to us.
</Alert>
);
export const StrategiesList = () => {
const navigate = useNavigate();
const [dialogueMetaData, setDialogueMetaData] = useState<IDialogueMetaData>(
refactor: fix misc TS errors (#729) * refactor: update test deps * refactor: remove unused ts-expect-error annotations * refactor: add missing arg and return types * refactor: the loading prop is optional * refactor: add missing arg and return types * reafactor: fix value arg type * refactor: fix missing array type * refactor: the parameters field is an array * refactor: use undefined instead of null in state * refactor: add missing params type * refactor: add missing children prop * refactor: add missing array type * refactor: add missing React imports * refactor: use correct IProjectEnvironment type * refactor: type errors as unknown * refactor: the index prop is required * refactor: fix date prop type * refactor: fix tooltip placement prop type * refactor: fix environments state type * refactor: add missing arg types * refactor: add guard for undefined field * refactor: fix ChangePassword prop types * refactor: fix MUI import paths * refactor: add missing arg type * refactor: fix showDialog prop type * refactor: remove unused openUpdateDialog prop * refactor: add missing non-null assertion * refactor: remove unused types prop * refactor: stricten API error handler types * refactor: add missing undefined check * refactor: add missing IProject id field * refactor: fix ConditionallyRender condition prop types * refactor: remove unused args * refactor: add AddVariant prop types * refactor: add types to UIContext * refactor: fix event arg type * refactor: add missing default impressionData field * refactor: fix handleDeleteEnvironment prop args * refactor: fix IFeatureMetrics field requirements * refactor: add missing element types to ConditionallyRender * refactor: remove unused ProjectAccess projectId prop * refactor: add missing undefined check * refactor: fix getCreateTogglePath arg type * refactor: add missing IStrategyPayload import * refactor: remove unused user arg * refactor: add missing event arg type * refactor: add missing style object types * refactor: improve userApiErrors prop type * refactor: the Dialogue onClose prop is optional * refactor: fix the AddonEvents setEventValue prop type
2022-02-25 10:55:39 +01:00
{
show: false,
title: '',
onConfirm: () => {},
},
);
const { strategies, refetchStrategies, loading } = useStrategies();
const { removeStrategy, deprecateStrategy, reactivateStrategy } =
useStrategiesApi();
const { setToastData, setToastApiError } = useToast();
const data = useMemo(() => {
if (loading) {
const mock = Array(5).fill({
name: 'Context name',
description: 'Context description when loading',
});
return {
all: mock,
predefined: mock,
custom: mock,
};
}
const all = strategies.map(
({ name, description, editable, deprecated }) => ({
name,
description,
editable,
deprecated,
}),
);
return {
all,
predefined: all.filter((strategy) => !strategy.editable),
custom: all.filter((strategy) => strategy.editable),
};
}, [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(
() => [
{
id: 'Icon',
Cell: () => (
<Box
data-loading
sx={{
pl: 3,
pr: 1,
display: 'flex',
alignItems: 'center',
}}
>
<Extension color='disabled' />
</Box>
),
},
{
2022-10-04 11:41:43 +02:00
id: 'Name',
Header: 'Name',
2022-10-04 11:41:43 +02:00
accessor: (row: any) => formatStrategyName(row.name),
width: '90%',
Cell: ({
row: {
original: { name, description, deprecated },
},
}: any) => {
return (
<LinkCell
data-loading
title={formatStrategyName(name)}
subtitle={description}
to={`/strategies/${name}`}
>
<ConditionallyRender
condition={deprecated}
show={() => (
<StyledBadge color='disabled'>
Disabled
</StyledBadge>
)}
/>
</LinkCell>
);
},
sortType: 'alphanumeric',
},
{
Header: 'Actions',
id: 'Actions',
align: 'center',
Cell: ({ row: { original } }: any) => (
<ActionCell>
<StrategySwitch
deprecated={original.deprecated}
onToggle={onToggle(original)}
/>
<ConditionallyRender
condition={original.editable}
show={
<>
<ActionCell.Divider />
<StrategyEditButton
strategy={original}
onClick={() => onEditStrategy(original)}
/>
<StrategyDeleteButton
strategy={original}
onClick={() =>
onDeleteStrategy(original)
}
/>
</>
}
/>
</ActionCell>
),
width: 150,
minWidth: 120,
disableSortBy: true,
},
{
accessor: 'description',
disableSortBy: true,
},
{
accessor: 'sortOrder',
sortType: 'number',
},
],
[onToggle, onEditStrategy, onDeleteStrategy],
);
const initialState = useMemo(
() => ({
2022-10-04 11:41:43 +02:00
sortBy: [{ id: 'Name', desc: false }],
hiddenColumns: ['description', 'sortOrder'],
}),
[],
);
const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
useTable(
{
columns: columns as any[], // TODO: fix after `react-table` v8 update
data: data.predefined,
initialState,
sortTypes,
autoResetSortBy: false,
disableSortRemove: true,
autoResetHiddenColumns: false,
},
useSortBy,
);
const {
getTableProps: customGetTableProps,
getTableBodyProps: customGetTableBodyProps,
headerGroups: customHeaderGroups,
rows: customRows,
prepareRow: customPrepareRow,
} = useTable(
{
columns: columns as any[], // TODO: fix after `react-table` v8 update
data: data.custom,
initialState,
sortTypes,
autoResetSortBy: false,
disableSortRemove: true,
autoResetHiddenColumns: false,
},
useSortBy,
);
const onDialogConfirm = () => {
dialogueMetaData?.onConfirm();
setDialogueMetaData((prev: IDialogueMetaData) => ({
...prev,
show: false,
}));
};
return (
<StyledBox>
<PageContent
isLoading={loading}
header={
<PageHeader>
<PredefinedStrategyTitle />
</PageHeader>
}
>
<Box>
<Table {...getTableProps()}>
<SortableTableHeader headerGroups={headerGroups} />
<TableBody {...getTableBodyProps()}>
{rows.map((row) => {
prepareRow(row);
return (
<TableRow hover {...row.getRowProps()}>
{row.cells.map((cell) => (
<TableCell {...cell.getCellProps()}>
{cell.render('Cell')}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
<ConditionallyRender
condition={rows.length === 0}
show={
<TablePlaceholder>
No strategies available.
</TablePlaceholder>
}
/>
</Box>
<Dialogue
open={dialogueMetaData.show}
onClick={onDialogConfirm}
title={dialogueMetaData?.title}
onClose={() =>
setDialogueMetaData((prev: IDialogueMetaData) => ({
...prev,
show: false,
}))
}
/>
</PageContent>
<PageContent
isLoading={loading}
header={
<PageHeader>
<CustomStrategyTitle />
</PageHeader>
}
>
<Box>
<StrategyDeprecationWarning />
<Table {...customGetTableProps()}>
<SortableTableHeader
headerGroups={customHeaderGroups}
/>
<TableBody {...customGetTableBodyProps()}>
{customRows.map((row) => {
customPrepareRow(row);
return (
<TableRow hover {...row.getRowProps()}>
{row.cells.map((cell) => (
<TableCell {...cell.getCellProps()}>
{cell.render('Cell')}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
<ConditionallyRender
condition={customRows.length === 0}
show={<CustomStrategyInfo />}
/>
</Box>
<Dialogue
open={dialogueMetaData.show}
onClick={onDialogConfirm}
title={dialogueMetaData?.title}
onClose={() =>
setDialogueMetaData((prev: IDialogueMetaData) => ({
...prev,
show: false,
}))
}
/>
</PageContent>
</StyledBox>
);
};