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:
parent
7495b07df6
commit
40185e9066
@ -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={
|
||||||
|
@ -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}
|
||||||
|
@ -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 you’re 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 don’t have these problems. They’re 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -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 “
|
condition={globalFilter?.length > 0}
|
||||||
{globalFilter}
|
show={
|
||||||
”
|
<TablePlaceholder>
|
||||||
</TablePlaceholder>
|
No predefined strategies found matching
|
||||||
}
|
“
|
||||||
elseShow={
|
{globalFilter}
|
||||||
<TablePlaceholder>
|
”
|
||||||
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
|
||||||
|
“
|
||||||
|
{globalFilter}
|
||||||
|
”
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
elseShow={<CustomStrategyInfo />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</SearchHighlightProvider>
|
||||||
|
|
||||||
<Dialogue
|
<Dialogue
|
||||||
open={dialogueMetaData.show}
|
open={dialogueMetaData.show}
|
||||||
|
Loading…
Reference in New Issue
Block a user