mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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