1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

Update strategies table (#3811)

- split strategies table in two
  - one for predefined strategies
  - one for custom strategies
- move custom strategy button
- add guidance why custom strategies are not recommended

https://linear.app/unleash/issue/1-894/improve-the-list-of-strategies
This commit is contained in:
Tymoteusz Czech 2023-05-22 12:46:27 +02:00 committed by GitHub
parent 7495b07df6
commit 40185e9066
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 279 additions and 67 deletions

View File

@ -47,7 +47,9 @@ export const DisableEnableStrategyDialog = ({
? `Add ${ ? `Add ${
disabled ? 'enable' : 'disable' disabled ? 'enable' : 'disable'
} strategy to change request?` } strategy to change request?`
: 'Are you sure you want to enable this strategy?' : `Are you sure you want to ${
disabled ? 'enable' : 'disable'
} this strategy?`
} }
open={isOpen} open={isOpen}
primaryButtonText={ primaryButtonText={

View File

@ -10,6 +10,7 @@ import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { CreateButton } from 'component/common/CreateButton/CreateButton'; import { CreateButton } from 'component/common/CreateButton/CreateButton';
import { GO_BACK } from 'constants/navigate'; import { GO_BACK } from 'constants/navigate';
import { CustomStrategyInfo } from '../CustomStrategyInfo/CustomStrategyInfo';
export const CreateStrategy = () => { export const CreateStrategy = () => {
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
@ -78,6 +79,7 @@ export const CreateStrategy = () => {
documentationLinkLabel="Custom strategies documentation" documentationLinkLabel="Custom strategies documentation"
formatApiCode={formatApiCode} formatApiCode={formatApiCode}
> >
<CustomStrategyInfo alert />
<StrategyForm <StrategyForm
errors={errors} errors={errors}
handleSubmit={handleSubmit} handleSubmit={handleSubmit}

View File

@ -0,0 +1,71 @@
import { Alert, Box, Typography } from '@mui/material';
import { FC } from 'react';
const Paragraph: FC = ({ children }) => (
<Typography
variant="body2"
sx={theme => ({
marginBottom: theme.spacing(2),
})}
>
{children}
</Typography>
);
export const CustomStrategyInfo: FC<{ alert?: boolean }> = ({ alert }) => {
const content = (
<>
<Paragraph>
We recommend you to use the predefined strategies like Gradual
rollout with constraints instead of creating a custom strategy.
</Paragraph>
<Paragraph>
If you decide to create a custom strategy be aware of:
<ul>
<li>
They require writing custom code and deployments for
each SDK youre using.
</li>
<li>
Differing implementation in each SDK will cause toggles
to evaluate differently
</li>
<li>
Requires a lot of configuration in both Unleash admin UI
and the SDK.
</li>
</ul>
</Paragraph>
<Paragraph>
Constraints dont have these problems. Theyre configured once
in the admin UI and behave in the same way in each SDK without
further configuration.
</Paragraph>
</>
);
if (alert) {
return (
<Alert
severity="info"
sx={theme => ({
marginBottom: theme.spacing(3),
})}
>
{content}
</Alert>
);
}
return (
<Box
sx={theme => ({
maxWidth: '720px',
padding: theme.spacing(4, 2),
margin: '0 auto',
})}
>
{content}
</Box>
);
};

View File

@ -20,7 +20,7 @@ export const AddStrategyButton = () => {
onClick={() => navigate('/strategies/create')} onClick={() => navigate('/strategies/create')}
permission={CREATE_STRATEGY} permission={CREATE_STRATEGY}
size="large" size="large"
tooltipProps={{ title: 'New strategy type' }} tooltipProps={{ title: 'New custom strategy' }}
> >
<Add /> <Add />
</PermissionIconButton> </PermissionIconButton>
@ -32,7 +32,7 @@ export const AddStrategyButton = () => {
permission={CREATE_STRATEGY} permission={CREATE_STRATEGY}
data-testid={ADD_NEW_STRATEGY_ID} data-testid={ADD_NEW_STRATEGY_ID}
> >
New strategy type New custom strategy
</PermissionButton> </PermissionButton>
} }
/> />

View File

@ -1,6 +1,6 @@
import { useState, useMemo, useCallback } from 'react'; import { useState, useMemo, useCallback, FC } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Box, styled } from '@mui/material'; import { Box, Link, Typography, styled } from '@mui/material';
import { Extension } from '@mui/icons-material'; import { Extension } from '@mui/icons-material';
import { import {
Table, Table,
@ -31,6 +31,8 @@ import { StrategyEditButton } from './StrategyEditButton/StrategyEditButton';
import { StrategyDeleteButton } from './StrategyDeleteButton/StrategyDeleteButton'; import { StrategyDeleteButton } from './StrategyDeleteButton/StrategyDeleteButton';
import { Search } from 'component/common/Search/Search'; import { Search } from 'component/common/Search/Search';
import { Badge } from 'component/common/Badge/Badge'; import { Badge } from 'component/common/Badge/Badge';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
import { CustomStrategyInfo } from '../CustomStrategyInfo/CustomStrategyInfo';
interface IDialogueMetaData { interface IDialogueMetaData {
show: boolean; show: boolean;
@ -43,6 +45,62 @@ const StyledBadge = styled(Badge)(({ theme }) => ({
display: 'inline-block', display: 'inline-block',
})); }));
const Subtitle: FC<{
title: string;
description: string;
link: string;
}> = ({ title, description, link }) => (
<Typography component="h2" variant="subtitle1" sx={{ display: 'flex' }}>
{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>
</>
}
/>
</Typography>
);
const PredefinedStrategyTitle = () => (
<Box sx={theme => ({ marginBottom: theme.spacing(1.5) })}>
<Subtitle
title="Predefined strategies"
description="The next level of control comes when you are able to enable a feature for specific users or enable it for a small subset of users. We achieve this level of control with the help of activation strategies."
link="https://docs.getunleash.io/reference/activation-strategies"
/>
</Box>
);
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>
);
export const StrategiesList = () => { export const StrategiesList = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [dialogueMetaData, setDialogueMetaData] = useState<IDialogueMetaData>( const [dialogueMetaData, setDialogueMetaData] = useState<IDialogueMetaData>(
@ -60,13 +118,18 @@ export const StrategiesList = () => {
const data = useMemo(() => { const data = useMemo(() => {
if (loading) { if (loading) {
return Array(5).fill({ const mock = Array(5).fill({
name: 'Context name', name: 'Context name',
description: 'Context description when loading', description: 'Context description when loading',
}); });
return {
all: mock,
predefined: mock,
custom: mock,
};
} }
return strategies.map( const all = strategies.map(
({ name, description, editable, deprecated }) => ({ ({ name, description, editable, deprecated }) => ({
name, name,
description, description,
@ -74,6 +137,11 @@ export const StrategiesList = () => {
deprecated, deprecated,
}) })
); );
return {
all,
predefined: all.filter(strategy => !strategy.editable),
custom: all.filter(strategy => strategy.editable),
};
}, [strategies, loading]); }, [strategies, loading]);
const onToggle = useCallback( const onToggle = useCallback(
@ -181,24 +249,21 @@ export const StrategiesList = () => {
width: '90%', width: '90%',
Cell: ({ Cell: ({
row: { row: {
original: { name, description, deprecated, editable }, original: { name, description, deprecated },
}, },
}: any) => { }: any) => {
const subTitleText = deprecated
? `${description} (deprecated)`
: description;
return ( return (
<LinkCell <LinkCell
data-loading data-loading
title={formatStrategyName(name)} title={formatStrategyName(name)}
subtitle={subTitleText} subtitle={description}
to={`/strategies/${name}`} to={`/strategies/${name}`}
> >
<ConditionallyRender <ConditionallyRender
condition={!editable} condition={deprecated}
show={() => ( show={() => (
<StyledBadge color="success"> <StyledBadge color="disabled">
Predefined Disabled
</StyledBadge> </StyledBadge>
)} )}
/> />
@ -217,18 +282,28 @@ export const StrategiesList = () => {
deprecated={original.deprecated} deprecated={original.deprecated}
onToggle={onToggle(original)} onToggle={onToggle(original)}
/> />
<ActionCell.Divider /> <ConditionallyRender
<StrategyEditButton condition={original.editable}
strategy={original} show={
onClick={() => onEditStrategy(original)} <>
/> <ActionCell.Divider />
<StrategyDeleteButton <StrategyEditButton
strategy={original} strategy={original}
onClick={() => onDeleteStrategy(original)} onClick={() => onEditStrategy(original)}
/>
<StrategyDeleteButton
strategy={original}
onClick={() =>
onDeleteStrategy(original)
}
/>
</>
}
/> />
</ActionCell> </ActionCell>
), ),
width: 150, width: 150,
minWidth: 120,
disableGlobalFilter: true, disableGlobalFilter: true,
disableSortBy: true, disableSortBy: true,
}, },
@ -264,7 +339,28 @@ export const StrategiesList = () => {
} = useTable( } = useTable(
{ {
columns: columns as any[], // TODO: fix after `react-table` v8 update columns: columns as any[], // TODO: fix after `react-table` v8 update
data, data: data.predefined,
initialState,
sortTypes,
autoResetGlobalFilter: false,
autoResetSortBy: false,
disableSortRemove: true,
},
useGlobalFilter,
useSortBy
);
const {
getTableProps: customGetTableProps,
getTableBodyProps: customGetTableBodyProps,
headerGroups: customHeaderGroups,
rows: customRows,
prepareRow: customPrepareRow,
setGlobalFilter: customSetGlobalFilter,
} = useTable(
{
columns: columns as any[], // TODO: fix after `react-table` v8 update
data: data.custom,
initialState, initialState,
sortTypes, sortTypes,
autoResetGlobalFilter: false, autoResetGlobalFilter: false,
@ -292,58 +388,99 @@ export const StrategiesList = () => {
<PageHeader <PageHeader
title={`Strategy types (${strategyTypeCount})`} title={`Strategy types (${strategyTypeCount})`}
actions={ actions={
<> <Search
<Search initialValue={globalFilter}
initialValue={globalFilter} onChange={(...props) => {
onChange={setGlobalFilter} setGlobalFilter(...props);
/> customSetGlobalFilter(...props);
<PageHeader.Divider /> }}
<AddStrategyButton /> />
</>
} }
/> />
} }
> >
<SearchHighlightProvider value={globalFilter}> <SearchHighlightProvider value={globalFilter}>
<Table {...getTableProps()}> <Box sx={theme => ({ paddingBottom: theme.spacing(4) })}>
<SortableTableHeader headerGroups={headerGroups} /> <PredefinedStrategyTitle />
<TableBody {...getTableBodyProps()}> <Table {...getTableProps()}>
{rows.map(row => { <SortableTableHeader headerGroups={headerGroups} />
prepareRow(row); <TableBody {...getTableBodyProps()}>
return ( {rows.map(row => {
<TableRow hover {...row.getRowProps()}> prepareRow(row);
{row.cells.map(cell => ( return (
<TableCell {...cell.getCellProps()}> <TableRow hover {...row.getRowProps()}>
{cell.render('Cell')} {row.cells.map(cell => (
</TableCell> <TableCell {...cell.getCellProps()}>
))} {cell.render('Cell')}
</TableRow> </TableCell>
); ))}
})} </TableRow>
</TableBody> );
</Table> })}
</SearchHighlightProvider> </TableBody>
<ConditionallyRender </Table>
condition={rows.length === 0}
show={
<ConditionallyRender <ConditionallyRender
condition={globalFilter?.length > 0} condition={rows.length === 0}
show={ show={
<TablePlaceholder> <ConditionallyRender
No strategies found matching &ldquo; condition={globalFilter?.length > 0}
{globalFilter} show={
&rdquo; <TablePlaceholder>
</TablePlaceholder> No predefined strategies found matching
} &ldquo;
elseShow={ {globalFilter}
<TablePlaceholder> &rdquo;
No strategies available. Get started by adding </TablePlaceholder>
one. }
</TablePlaceholder> elseShow={
<TablePlaceholder>
No strategies available.
</TablePlaceholder>
}
/>
} }
/> />
} </Box>
/> <Box>
<CustomStrategyTitle />
<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={
<ConditionallyRender
condition={globalFilter?.length > 0}
show={
<TablePlaceholder>
No custom strategies found matching
&ldquo;
{globalFilter}
&rdquo;
</TablePlaceholder>
}
elseShow={<CustomStrategyInfo />}
/>
}
/>
</Box>
</SearchHighlightProvider>
<Dialogue <Dialogue
open={dialogueMetaData.show} open={dialogueMetaData.show}