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 ${
|
||||
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={
|
||||
|
@ -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}
|
||||
|
@ -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')}
|
||||
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>
|
||||
}
|
||||
/>
|
||||
|
@ -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 “
|
||||
{globalFilter}
|
||||
”
|
||||
</TablePlaceholder>
|
||||
}
|
||||
elseShow={
|
||||
<TablePlaceholder>
|
||||
No strategies available. Get started by adding
|
||||
one.
|
||||
</TablePlaceholder>
|
||||
<ConditionallyRender
|
||||
condition={globalFilter?.length > 0}
|
||||
show={
|
||||
<TablePlaceholder>
|
||||
No predefined strategies found matching
|
||||
“
|
||||
{globalFilter}
|
||||
”
|
||||
</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
|
||||
open={dialogueMetaData.show}
|
||||
|
Loading…
Reference in New Issue
Block a user