mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
feat: rebrand feature toggle list as search (#5675)
This commit is contained in:
parent
5603e8683d
commit
8e09f08a05
@ -0,0 +1,23 @@
|
|||||||
|
import { screen } from '@testing-library/react';
|
||||||
|
import { render } from 'utils/testRenderer';
|
||||||
|
import { FeatureToggleListActions } from './FeatureToggleListActions';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
||||||
|
|
||||||
|
const server = testServerSetup();
|
||||||
|
test('all options are drawn', async () => {
|
||||||
|
testServerRoute(server, '/api/admin/ui-config', {
|
||||||
|
flags: {
|
||||||
|
featuresExportImport: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<FeatureToggleListActions onExportClick={() => {}} />);
|
||||||
|
|
||||||
|
const batchReviveButton = await screen.findByTitle('Group actions');
|
||||||
|
|
||||||
|
await userEvent.click(batchReviveButton!);
|
||||||
|
|
||||||
|
await screen.findByText('New feature toggle');
|
||||||
|
await screen.findByText('Export');
|
||||||
|
});
|
@ -0,0 +1,134 @@
|
|||||||
|
import { FC, useState } from 'react';
|
||||||
|
import {
|
||||||
|
IconButton,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
Popover,
|
||||||
|
styled,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { Add, FileDownload, MoreVert } from '@mui/icons-material';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import { PermissionHOC } from 'component/common/PermissionHOC/PermissionHOC';
|
||||||
|
import { useCreateFeaturePath } from 'component/feature/CreateFeatureButton/useCreateFeaturePath';
|
||||||
|
|
||||||
|
const StyledActions = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledPopover = styled(Popover)(({ theme }) => ({
|
||||||
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
|
padding: theme.spacing(1, 1.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IFeatureToggleListActions {
|
||||||
|
onExportClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeatureToggleListActions: FC<IFeatureToggleListActions> = ({
|
||||||
|
onExportClick,
|
||||||
|
}: IFeatureToggleListActions) => {
|
||||||
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
const featuresExportImport = useUiFlag('featuresExportImport');
|
||||||
|
const createFeature = useCreateFeaturePath({
|
||||||
|
query: '',
|
||||||
|
project: 'default',
|
||||||
|
});
|
||||||
|
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
setAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
const handleClose = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const id = `feature-toggle-list-actions`;
|
||||||
|
const menuId = `${id}-menu`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledActions
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip title='Group actions' arrow describeChild>
|
||||||
|
<IconButton
|
||||||
|
id={id}
|
||||||
|
aria-controls={open ? menuId : undefined}
|
||||||
|
aria-haspopup='true'
|
||||||
|
aria-expanded={open ? 'true' : undefined}
|
||||||
|
onClick={handleClick}
|
||||||
|
type='button'
|
||||||
|
>
|
||||||
|
<MoreVert />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<StyledPopover
|
||||||
|
id={menuId}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
transformOrigin={{
|
||||||
|
horizontal: 'right',
|
||||||
|
vertical: 'top',
|
||||||
|
}}
|
||||||
|
anchorOrigin={{
|
||||||
|
horizontal: 'right',
|
||||||
|
vertical: 'bottom',
|
||||||
|
}}
|
||||||
|
disableScrollLock={true}
|
||||||
|
>
|
||||||
|
<MenuList aria-labelledby={id}>
|
||||||
|
<PermissionHOC permission={CREATE_FEATURE}>
|
||||||
|
{({ hasAccess }) => (
|
||||||
|
<MenuItem
|
||||||
|
onClick={handleClose}
|
||||||
|
component={Link}
|
||||||
|
disabled={!hasAccess}
|
||||||
|
to={createFeature!.path}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Add />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>
|
||||||
|
<Typography variant='body2'>
|
||||||
|
New feature toggle
|
||||||
|
</Typography>
|
||||||
|
</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</PermissionHOC>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={featuresExportImport}
|
||||||
|
show={
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
onExportClick();
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<FileDownload />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>
|
||||||
|
<Typography variant='body2'>
|
||||||
|
Export
|
||||||
|
</Typography>
|
||||||
|
</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</MenuList>
|
||||||
|
</StyledPopover>
|
||||||
|
</StyledActions>
|
||||||
|
);
|
||||||
|
};
|
@ -80,7 +80,7 @@ const setupApi = (features: APIFeature[], projects: APIProject[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const verifyTableFeature = async (feature: Partial<UIFeature>) => {
|
const verifyTableFeature = async (feature: Partial<UIFeature>) => {
|
||||||
await screen.findByText('Feature toggles');
|
await screen.findByText('Search');
|
||||||
await screen.findByText('Add Filter');
|
await screen.findByText('Add Filter');
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.values(feature).map((value) => screen.findByText(value)),
|
Object.values(feature).map((value) => screen.findByText(value)),
|
||||||
|
@ -1,12 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState, VFC } from 'react';
|
import { useCallback, useEffect, useMemo, useState, VFC } from 'react';
|
||||||
import {
|
import { Box, Link, useMediaQuery, useTheme } from '@mui/material';
|
||||||
Box,
|
|
||||||
IconButton,
|
|
||||||
Link,
|
|
||||||
Tooltip,
|
|
||||||
useMediaQuery,
|
|
||||||
useTheme,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
import { createColumnHelper, useReactTable } from '@tanstack/react-table';
|
import { createColumnHelper, useReactTable } from '@tanstack/react-table';
|
||||||
import { PaginatedTable, TablePlaceholder } from 'component/common/Table';
|
import { PaginatedTable, TablePlaceholder } from 'component/common/Table';
|
||||||
@ -18,13 +11,11 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
|||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import { FeatureSchema, FeatureSearchResponseSchema } from 'openapi';
|
import { FeatureSchema, FeatureSearchResponseSchema } from 'openapi';
|
||||||
import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton';
|
|
||||||
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
|
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
|
||||||
import { Search } from 'component/common/Search/Search';
|
import { Search } from 'component/common/Search/Search';
|
||||||
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
||||||
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
||||||
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
||||||
import FileDownload from '@mui/icons-material/FileDownload';
|
|
||||||
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
|
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
|
||||||
import { ExportDialog } from './ExportDialog';
|
import { ExportDialog } from './ExportDialog';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
@ -53,6 +44,7 @@ import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/Feat
|
|||||||
import { FeatureSegmentCell } from 'component/common/Table/cells/FeatureSegmentCell/FeatureSegmentCell';
|
import { FeatureSegmentCell } from 'component/common/Table/cells/FeatureSegmentCell/FeatureSegmentCell';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import { FeatureToggleListTable as LegacyFeatureToggleListTable } from './LegacyFeatureToggleListTable';
|
import { FeatureToggleListTable as LegacyFeatureToggleListTable } from './LegacyFeatureToggleListTable';
|
||||||
|
import { FeatureToggleListActions } from './FeatureToggleListActions/FeatureToggleListActions';
|
||||||
|
|
||||||
export const featuresPlaceholder = Array(15).fill({
|
export const featuresPlaceholder = Array(15).fill({
|
||||||
name: 'Name of the feature',
|
name: 'Name of the feature',
|
||||||
@ -268,7 +260,7 @@ const FeatureToggleListTableComponent: VFC = () => {
|
|||||||
bodyClass='no-padding'
|
bodyClass='no-padding'
|
||||||
header={
|
header={
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title='Feature toggles'
|
title='Search'
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
@ -296,32 +288,8 @@ const FeatureToggleListTableComponent: VFC = () => {
|
|||||||
>
|
>
|
||||||
View archive
|
View archive
|
||||||
</Link>
|
</Link>
|
||||||
<ConditionallyRender
|
<FeatureToggleListActions
|
||||||
condition={Boolean(
|
onExportClick={() => setShowExportDialog(true)}
|
||||||
uiConfig?.flags?.featuresExportImport,
|
|
||||||
)}
|
|
||||||
show={
|
|
||||||
<Tooltip
|
|
||||||
title='Export current selection'
|
|
||||||
arrow
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
onClick={() =>
|
|
||||||
setShowExportDialog(true)
|
|
||||||
}
|
|
||||||
sx={(theme) => ({
|
|
||||||
marginRight: theme.spacing(2),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<FileDownload />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CreateFeatureButton
|
|
||||||
loading={false}
|
|
||||||
filter={{ query: '', project: 'default' }}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
@ -123,6 +123,7 @@ const Header: VFC = () => {
|
|||||||
const [configRef, setConfigRef] = useState<HTMLButtonElement | null>(null);
|
const [configRef, setConfigRef] = useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
const disableNotifications = useUiFlag('disableNotifications');
|
const disableNotifications = useUiFlag('disableNotifications');
|
||||||
|
const hasSearch = useUiFlag('featureSearchFrontend');
|
||||||
const { uiConfig, isOss } = useUiConfig();
|
const { uiConfig, isOss } = useUiConfig();
|
||||||
const smallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
const smallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const [openDrawer, setOpenDrawer] = useState(false);
|
const [openDrawer, setOpenDrawer] = useState(false);
|
||||||
@ -191,9 +192,17 @@ const Header: VFC = () => {
|
|||||||
<StyledNav>
|
<StyledNav>
|
||||||
<StyledLinks>
|
<StyledLinks>
|
||||||
<StyledLink to='/projects'>Projects</StyledLink>
|
<StyledLink to='/projects'>Projects</StyledLink>
|
||||||
<StyledLink to={'/features'}>
|
<ConditionallyRender
|
||||||
Feature toggles
|
condition={hasSearch}
|
||||||
</StyledLink>
|
show={
|
||||||
|
<StyledLink to={'/search'}>Search</StyledLink>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<StyledLink to={'/features'}>
|
||||||
|
Feature toggles
|
||||||
|
</StyledLink>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<StyledLink to='/playground'>Playground</StyledLink>
|
<StyledLink to='/playground'>Playground</StyledLink>
|
||||||
<StyledAdvancedNavButton
|
<StyledAdvancedNavButton
|
||||||
onClick={(e) => setConfigRef(e.currentTarget)}
|
onClick={(e) => setConfigRef(e.currentTarget)}
|
||||||
|
@ -119,10 +119,21 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"menu": {
|
"menu": {
|
||||||
"mobile": true,
|
"mobile": true,
|
||||||
},
|
},
|
||||||
|
"notFlag": "featureSearchFrontend",
|
||||||
"path": "/features",
|
"path": "/features",
|
||||||
"title": "Feature toggles",
|
"title": "Feature toggles",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"component": [Function],
|
||||||
|
"flag": "featureSearchFrontend",
|
||||||
|
"menu": {
|
||||||
|
"mobile": true,
|
||||||
|
},
|
||||||
|
"path": "/search",
|
||||||
|
"title": "Search",
|
||||||
|
"type": "protected",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"component": {
|
"component": {
|
||||||
"$$typeof": Symbol(react.lazy),
|
"$$typeof": Symbol(react.lazy),
|
||||||
|
@ -147,6 +147,15 @@ export const routes: IRoute[] = [
|
|||||||
component: FeatureToggleListTable,
|
component: FeatureToggleListTable,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
menu: { mobile: true },
|
menu: { mobile: true },
|
||||||
|
notFlag: 'featureSearchFrontend',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/search',
|
||||||
|
title: 'Search',
|
||||||
|
component: FeatureToggleListTable,
|
||||||
|
type: 'protected',
|
||||||
|
menu: { mobile: true },
|
||||||
|
flag: 'featureSearchFrontend',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Playground
|
// Playground
|
||||||
|
Loading…
Reference in New Issue
Block a user