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 ${
disabled ? 'enable' : 'disable'
} 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}
primaryButtonText={

View File

@ -10,6 +10,7 @@ import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
import { formatUnknownError } from 'utils/formatUnknownError';
import { CreateButton } from 'component/common/CreateButton/CreateButton';
import { GO_BACK } from 'constants/navigate';
import { CustomStrategyInfo } from '../CustomStrategyInfo/CustomStrategyInfo';
export const CreateStrategy = () => {
const { setToastData, setToastApiError } = useToast();
@ -78,6 +79,7 @@ export const CreateStrategy = () => {
documentationLinkLabel="Custom strategies documentation"
formatApiCode={formatApiCode}
>
<CustomStrategyInfo alert />
<StrategyForm
errors={errors}
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')}
permission={CREATE_STRATEGY}
size="large"
tooltipProps={{ title: 'New strategy type' }}
tooltipProps={{ title: 'New custom strategy' }}
>
<Add />
</PermissionIconButton>
@ -32,7 +32,7 @@ export const AddStrategyButton = () => {
permission={CREATE_STRATEGY}
data-testid={ADD_NEW_STRATEGY_ID}
>
New strategy type
New custom strategy
</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 { Box, styled } from '@mui/material';
import { Box, Link, Typography, styled } from '@mui/material';
import { Extension } from '@mui/icons-material';
import {
Table,
@ -31,6 +31,8 @@ import { StrategyEditButton } from './StrategyEditButton/StrategyEditButton';
import { StrategyDeleteButton } from './StrategyDeleteButton/StrategyDeleteButton';
import { Search } from 'component/common/Search/Search';
import { Badge } from 'component/common/Badge/Badge';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
import { CustomStrategyInfo } from '../CustomStrategyInfo/CustomStrategyInfo';
interface IDialogueMetaData {
show: boolean;
@ -43,6 +45,62 @@ const StyledBadge = styled(Badge)(({ theme }) => ({
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 = () => {
const navigate = useNavigate();
const [dialogueMetaData, setDialogueMetaData] = useState<IDialogueMetaData>(
@ -60,13 +118,18 @@ export const StrategiesList = () => {
const data = useMemo(() => {
if (loading) {
return Array(5).fill({
const mock = Array(5).fill({
name: 'Context name',
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,
@ -74,6 +137,11 @@ export const StrategiesList = () => {
deprecated,
})
);
return {
all,
predefined: all.filter(strategy => !strategy.editable),
custom: all.filter(strategy => strategy.editable),
};
}, [strategies, loading]);
const onToggle = useCallback(
@ -181,24 +249,21 @@ export const StrategiesList = () => {
width: '90%',
Cell: ({
row: {
original: { name, description, deprecated, editable },
original: { name, description, deprecated },
},
}: any) => {
const subTitleText = deprecated
? `${description} (deprecated)`
: description;
return (
<LinkCell
data-loading
title={formatStrategyName(name)}
subtitle={subTitleText}
subtitle={description}
to={`/strategies/${name}`}
>
<ConditionallyRender
condition={!editable}
condition={deprecated}
show={() => (
<StyledBadge color="success">
Predefined
<StyledBadge color="disabled">
Disabled
</StyledBadge>
)}
/>
@ -217,18 +282,28 @@ export const StrategiesList = () => {
deprecated={original.deprecated}
onToggle={onToggle(original)}
/>
<ActionCell.Divider />
<StrategyEditButton
strategy={original}
onClick={() => onEditStrategy(original)}
/>
<StrategyDeleteButton
strategy={original}
onClick={() => onDeleteStrategy(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,
disableGlobalFilter: true,
disableSortBy: true,
},
@ -264,7 +339,28 @@ export const StrategiesList = () => {
} = useTable(
{
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,
sortTypes,
autoResetGlobalFilter: false,
@ -292,58 +388,99 @@ export const StrategiesList = () => {
<PageHeader
title={`Strategy types (${strategyTypeCount})`}
actions={
<>
<Search
initialValue={globalFilter}
onChange={setGlobalFilter}
/>
<PageHeader.Divider />
<AddStrategyButton />
</>
<Search
initialValue={globalFilter}
onChange={(...props) => {
setGlobalFilter(...props);
customSetGlobalFilter(...props);
}}
/>
}
/>
}
>
<SearchHighlightProvider value={globalFilter}>
<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>
</SearchHighlightProvider>
<ConditionallyRender
condition={rows.length === 0}
show={
<Box sx={theme => ({ paddingBottom: theme.spacing(4) })}>
<PredefinedStrategyTitle />
<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={globalFilter?.length > 0}
condition={rows.length === 0}
show={
<TablePlaceholder>
No strategies found matching &ldquo;
{globalFilter}
&rdquo;
</TablePlaceholder>
}
elseShow={
<TablePlaceholder>
No strategies available. Get started by adding
one.
</TablePlaceholder>
<ConditionallyRender
condition={globalFilter?.length > 0}
show={
<TablePlaceholder>
No predefined strategies found matching
&ldquo;
{globalFilter}
&rdquo;
</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
open={dialogueMetaData.show}