mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-08 01:15:49 +02:00
chore: remove featureSearchFrontend flag (#6066)
This commit is contained in:
parent
b2c127c058
commit
c6a2303026
@ -14,8 +14,7 @@ describe('project overview', () => {
|
|||||||
const featureToggleName = `${featureTogglePrefix}-${randomId}`;
|
const featureToggleName = `${featureTogglePrefix}-${randomId}`;
|
||||||
const projectName = `unleash-e2e-project-overview-${randomId}`;
|
const projectName = `unleash-e2e-project-overview-${randomId}`;
|
||||||
const baseUrl = Cypress.config().baseUrl;
|
const baseUrl = Cypress.config().baseUrl;
|
||||||
const selectAll =
|
const selectAll = '[title="Select all rows"] input[type="checkbox"]';
|
||||||
'[title="Toggle All Rows Selected"] input[type="checkbox"]';
|
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.runBefore();
|
cy.runBefore();
|
||||||
|
@ -35,12 +35,6 @@ const setupNoFeaturesReturned = () =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
const setupApi = (features: APIFeature[], projects: APIProject[]) => {
|
const setupApi = (features: APIFeature[], projects: APIProject[]) => {
|
||||||
testServerRoute(server, '/api/admin/ui-config', {
|
|
||||||
flags: {
|
|
||||||
featureSearchFrontend: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
testServerRoute(server, '/api/admin/projects', {
|
testServerRoute(server, '/api/admin/projects', {
|
||||||
projects,
|
projects,
|
||||||
});
|
});
|
||||||
@ -146,6 +140,6 @@ test('Filter table by project', async () => {
|
|||||||
'No feature toggles found matching your criteria. Get started by adding a new feature toggle.',
|
'No feature toggles found matching your criteria. Get started by adding a new feature toggle.',
|
||||||
);
|
);
|
||||||
expect(window.location.href).toContain(
|
expect(window.location.href).toContain(
|
||||||
'?sort=createdAt&order=desc&offset=0&columns=&project=IS%3Aproject-b',
|
'?offset=0&columns=&project=IS%3Aproject-b',
|
||||||
);
|
);
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
@ -50,7 +50,6 @@ import { usePersistentTableState } from 'hooks/usePersistentTableState';
|
|||||||
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
||||||
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 { FeatureToggleListActions } from './FeatureToggleListActions/FeatureToggleListActions';
|
import { FeatureToggleListActions } from './FeatureToggleListActions/FeatureToggleListActions';
|
||||||
import useLoading from 'hooks/useLoading';
|
import useLoading from 'hooks/useLoading';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
@ -68,7 +67,7 @@ export const featuresPlaceholder = Array(15).fill({
|
|||||||
const columnHelper = createColumnHelper<FeatureSearchResponseSchema>();
|
const columnHelper = createColumnHelper<FeatureSearchResponseSchema>();
|
||||||
const feedbackCategory = 'search';
|
const feedbackCategory = 'search';
|
||||||
|
|
||||||
const FeatureToggleListTableComponent: VFC = () => {
|
export const FeatureToggleListTable: VFC = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { openFeedback } = useFeedback(feedbackCategory, 'automatic');
|
const { openFeedback } = useFeedback(feedbackCategory, 'automatic');
|
||||||
const { trackEvent } = usePlausibleTracker();
|
const { trackEvent } = usePlausibleTracker();
|
||||||
@ -428,11 +427,3 @@ const FeatureToggleListTableComponent: VFC = () => {
|
|||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FeatureToggleListTable: VFC = () => {
|
|
||||||
const featureSearchFrontend = useUiFlag('featureSearchFrontend');
|
|
||||||
|
|
||||||
if (featureSearchFrontend) return <FeatureToggleListTableComponent />;
|
|
||||||
|
|
||||||
return <LegacyFeatureToggleListTable />;
|
|
||||||
};
|
|
||||||
|
@ -1,413 +0,0 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState, VFC } from 'react';
|
|
||||||
import {
|
|
||||||
IconButton,
|
|
||||||
Link,
|
|
||||||
Tooltip,
|
|
||||||
useMediaQuery,
|
|
||||||
useTheme,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { Link as RouterLink, useSearchParams } from 'react-router-dom';
|
|
||||||
import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table';
|
|
||||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
|
||||||
import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures';
|
|
||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
|
||||||
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
|
||||||
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
|
||||||
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
|
|
||||||
import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
|
||||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
|
||||||
import { FeatureSchema } from 'openapi';
|
|
||||||
import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton';
|
|
||||||
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
|
|
||||||
import { useSearch } from 'hooks/useSearch';
|
|
||||||
import { Search } from 'component/common/Search/Search';
|
|
||||||
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
|
||||||
import { usePinnedFavorites } from 'hooks/usePinnedFavorites';
|
|
||||||
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
|
||||||
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
|
||||||
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
|
||||||
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
|
|
||||||
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
|
|
||||||
import FileDownload from '@mui/icons-material/FileDownload';
|
|
||||||
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
|
|
||||||
import { ExportDialog } from './ExportDialog';
|
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
import { focusable } from 'themes/themeStyles';
|
|
||||||
import { FeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
|
||||||
import useToast from 'hooks/useToast';
|
|
||||||
|
|
||||||
export const featuresPlaceholder: FeatureSchema[] = Array(15).fill({
|
|
||||||
name: 'Name of the feature',
|
|
||||||
description: 'Short description of the feature',
|
|
||||||
type: '-',
|
|
||||||
createdAt: new Date(2022, 1, 1),
|
|
||||||
project: 'projectID',
|
|
||||||
});
|
|
||||||
|
|
||||||
export type PageQueryType = Partial<
|
|
||||||
Record<'sort' | 'order' | 'search' | 'favorites', string>
|
|
||||||
>;
|
|
||||||
|
|
||||||
const defaultSort: SortingRule<string> = { id: 'createdAt', desc: true };
|
|
||||||
|
|
||||||
const { value: storedParams, setValue: setStoredParams } = createLocalStorage(
|
|
||||||
'FeatureToggleListTable:v1',
|
|
||||||
defaultSort,
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated remove with flag `featureSearchFrontend`
|
|
||||||
*/
|
|
||||||
export const FeatureToggleListTable: VFC = () => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const { environments } = useEnvironments();
|
|
||||||
const enabledEnvironments = environments
|
|
||||||
.filter((env) => env.enabled)
|
|
||||||
.map((env) => env.name);
|
|
||||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
|
||||||
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
|
||||||
const [showExportDialog, setShowExportDialog] = useState(false);
|
|
||||||
const { features = [], loading, refetchFeatures } = useFeatures();
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
|
||||||
const { setToastApiError } = useToast();
|
|
||||||
const { uiConfig } = useUiConfig();
|
|
||||||
|
|
||||||
const [initialState] = useState(() => ({
|
|
||||||
sortBy: [
|
|
||||||
{
|
|
||||||
id: searchParams.get('sort') || storedParams.id,
|
|
||||||
desc: searchParams.has('order')
|
|
||||||
? searchParams.get('order') === 'desc'
|
|
||||||
: storedParams.desc,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
hiddenColumns: ['description'],
|
|
||||||
globalFilter: searchParams.get('search') || '',
|
|
||||||
}));
|
|
||||||
const { value: globalStore, setValue: setGlobalStore } =
|
|
||||||
useGlobalLocalStorage();
|
|
||||||
const { isFavoritesPinned, sortTypes, onChangeIsFavoritePinned } =
|
|
||||||
usePinnedFavorites(
|
|
||||||
searchParams.has('favorites')
|
|
||||||
? searchParams.get('favorites') === 'true'
|
|
||||||
: globalStore.favorites,
|
|
||||||
);
|
|
||||||
const [searchValue, setSearchValue] = useState(initialState.globalFilter);
|
|
||||||
const { favorite, unfavorite } = useFavoriteFeaturesApi();
|
|
||||||
const onFavorite = useCallback(
|
|
||||||
async (feature: any) => {
|
|
||||||
try {
|
|
||||||
if (feature?.favorite) {
|
|
||||||
await unfavorite(feature.project, feature.name);
|
|
||||||
} else {
|
|
||||||
await favorite(feature.project, feature.name);
|
|
||||||
}
|
|
||||||
refetchFeatures();
|
|
||||||
} catch (error) {
|
|
||||||
setToastApiError(
|
|
||||||
'Something went wrong, could not update favorite',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[favorite, refetchFeatures, unfavorite, setToastApiError],
|
|
||||||
);
|
|
||||||
|
|
||||||
const columns = useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
Header: (
|
|
||||||
<FavoriteIconHeader
|
|
||||||
isActive={isFavoritesPinned}
|
|
||||||
onClick={onChangeIsFavoritePinned}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
accessor: 'favorite',
|
|
||||||
Cell: ({ row: { original: feature } }: any) => (
|
|
||||||
<FavoriteIconCell
|
|
||||||
value={feature?.favorite}
|
|
||||||
onClick={() => onFavorite(feature)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
maxWidth: 50,
|
|
||||||
disableSortBy: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'Seen',
|
|
||||||
accessor: 'lastSeenAt',
|
|
||||||
Cell: ({ value, row: { original: feature } }: any) => {
|
|
||||||
return <FeatureEnvironmentSeenCell feature={feature} />;
|
|
||||||
},
|
|
||||||
align: 'center',
|
|
||||||
maxWidth: 80,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'Type',
|
|
||||||
accessor: 'type',
|
|
||||||
Cell: FeatureTypeCell,
|
|
||||||
align: 'center',
|
|
||||||
maxWidth: 85,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'Name',
|
|
||||||
accessor: 'name',
|
|
||||||
minWidth: 150,
|
|
||||||
Cell: FeatureNameCell,
|
|
||||||
sortType: 'alphanumeric',
|
|
||||||
searchable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tags',
|
|
||||||
Header: 'Tags',
|
|
||||||
accessor: (row: FeatureSchema) =>
|
|
||||||
row.tags
|
|
||||||
?.map(({ type, value }) => `${type}:${value}`)
|
|
||||||
.join('\n') || '',
|
|
||||||
Cell: FeatureTagCell,
|
|
||||||
width: 80,
|
|
||||||
searchable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'Created',
|
|
||||||
accessor: 'createdAt',
|
|
||||||
Cell: DateCell,
|
|
||||||
maxWidth: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'Project ID',
|
|
||||||
accessor: 'project',
|
|
||||||
Cell: ({ value }: { value: string }) => (
|
|
||||||
<LinkCell title={value} to={`/projects/${value}`} />
|
|
||||||
),
|
|
||||||
sortType: 'alphanumeric',
|
|
||||||
maxWidth: 150,
|
|
||||||
filterName: 'project',
|
|
||||||
searchable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'State',
|
|
||||||
accessor: 'stale',
|
|
||||||
Cell: FeatureStaleCell,
|
|
||||||
sortType: 'boolean',
|
|
||||||
maxWidth: 120,
|
|
||||||
filterName: 'state',
|
|
||||||
filterParsing: (value: any) => (value ? 'stale' : 'active'),
|
|
||||||
},
|
|
||||||
// Always hidden -- for search
|
|
||||||
{
|
|
||||||
accessor: 'description',
|
|
||||||
Header: 'Description',
|
|
||||||
searchable: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[isFavoritesPinned],
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: searchedData,
|
|
||||||
getSearchText,
|
|
||||||
getSearchContext,
|
|
||||||
} = useSearch(columns, searchValue, features);
|
|
||||||
|
|
||||||
const data = useMemo(
|
|
||||||
() =>
|
|
||||||
searchedData?.length === 0 && loading
|
|
||||||
? featuresPlaceholder
|
|
||||||
: searchedData,
|
|
||||||
[searchedData, loading],
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
headerGroups,
|
|
||||||
rows,
|
|
||||||
prepareRow,
|
|
||||||
state: { sortBy },
|
|
||||||
setHiddenColumns,
|
|
||||||
} = useTable(
|
|
||||||
{
|
|
||||||
columns: columns as any[],
|
|
||||||
data,
|
|
||||||
initialState,
|
|
||||||
sortTypes,
|
|
||||||
autoResetHiddenColumns: false,
|
|
||||||
autoResetSortBy: false,
|
|
||||||
disableSortRemove: true,
|
|
||||||
disableMultiSort: true,
|
|
||||||
},
|
|
||||||
useSortBy,
|
|
||||||
useFlexLayout,
|
|
||||||
);
|
|
||||||
|
|
||||||
useConditionallyHiddenColumns(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
condition: !features.some(({ tags }) => tags?.length),
|
|
||||||
columns: ['tags'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: isSmallScreen,
|
|
||||||
columns: ['type', 'createdAt', 'tags'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
condition: isMediumScreen,
|
|
||||||
columns: ['lastSeenAt', 'stale'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
setHiddenColumns,
|
|
||||||
columns,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const tableState: PageQueryType = {};
|
|
||||||
tableState.sort = sortBy[0].id;
|
|
||||||
if (sortBy[0].desc) {
|
|
||||||
tableState.order = 'desc';
|
|
||||||
}
|
|
||||||
if (searchValue) {
|
|
||||||
tableState.search = searchValue;
|
|
||||||
}
|
|
||||||
if (isFavoritesPinned) {
|
|
||||||
tableState.favorites = 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
setSearchParams(tableState, {
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
setStoredParams({
|
|
||||||
id: sortBy[0].id,
|
|
||||||
desc: sortBy[0].desc || false,
|
|
||||||
});
|
|
||||||
setGlobalStore((params) => ({
|
|
||||||
...params,
|
|
||||||
favorites: Boolean(isFavoritesPinned),
|
|
||||||
}));
|
|
||||||
}, [sortBy, searchValue, setSearchParams, isFavoritesPinned]);
|
|
||||||
|
|
||||||
if (!(environments.length > 0)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent
|
|
||||||
isLoading={loading}
|
|
||||||
header={
|
|
||||||
<PageHeader
|
|
||||||
title={`Feature toggles (${
|
|
||||||
rows.length < data.length
|
|
||||||
? `${rows.length} of ${data.length}`
|
|
||||||
: data.length
|
|
||||||
})`}
|
|
||||||
actions={
|
|
||||||
<>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={!isSmallScreen}
|
|
||||||
show={
|
|
||||||
<>
|
|
||||||
<Search
|
|
||||||
placeholder='Search and Filter'
|
|
||||||
expandable
|
|
||||||
initialValue={searchValue}
|
|
||||||
onChange={setSearchValue}
|
|
||||||
hasFilters
|
|
||||||
getSearchContext={getSearchContext}
|
|
||||||
/>
|
|
||||||
<PageHeader.Divider />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Link
|
|
||||||
component={RouterLink}
|
|
||||||
to='/archive'
|
|
||||||
underline='always'
|
|
||||||
sx={{ marginRight: 2, ...focusable(theme) }}
|
|
||||||
>
|
|
||||||
View archive
|
|
||||||
</Link>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(
|
|
||||||
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' }}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={isSmallScreen}
|
|
||||||
show={
|
|
||||||
<Search
|
|
||||||
initialValue={searchValue}
|
|
||||||
onChange={setSearchValue}
|
|
||||||
hasFilters
|
|
||||||
getSearchContext={getSearchContext}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PageHeader>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
|
||||||
<VirtualizedTable
|
|
||||||
rows={rows}
|
|
||||||
headerGroups={headerGroups}
|
|
||||||
prepareRow={prepareRow}
|
|
||||||
/>
|
|
||||||
</SearchHighlightProvider>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={rows.length === 0}
|
|
||||||
show={
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={searchValue?.length > 0}
|
|
||||||
show={
|
|
||||||
<TablePlaceholder>
|
|
||||||
No feature toggles found matching “
|
|
||||||
{searchValue}
|
|
||||||
”
|
|
||||||
</TablePlaceholder>
|
|
||||||
}
|
|
||||||
elseShow={
|
|
||||||
<TablePlaceholder>
|
|
||||||
No feature toggles available. Get started by
|
|
||||||
adding a new feature toggle.
|
|
||||||
</TablePlaceholder>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(uiConfig?.flags?.featuresExportImport)}
|
|
||||||
show={
|
|
||||||
<ExportDialog
|
|
||||||
showExportDialog={showExportDialog}
|
|
||||||
data={data}
|
|
||||||
onClose={() => setShowExportDialog(false)}
|
|
||||||
environments={enabledEnvironments}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
};
|
|
@ -157,7 +157,6 @@ 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);
|
||||||
@ -249,17 +248,7 @@ const Header: VFC = () => {
|
|||||||
<StyledNav>
|
<StyledNav>
|
||||||
<StyledLinks>
|
<StyledLinks>
|
||||||
<StyledLink to='/projects'>Projects</StyledLink>
|
<StyledLink to='/projects'>Projects</StyledLink>
|
||||||
<ConditionallyRender
|
<StyledLink to={'/search'}>Search</StyledLink>
|
||||||
condition={hasSearch}
|
|
||||||
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,17 +119,6 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"menu": {
|
"menu": {
|
||||||
"mobile": true,
|
"mobile": true,
|
||||||
},
|
},
|
||||||
"notFlag": "featureSearchFrontend",
|
|
||||||
"path": "/features",
|
|
||||||
"title": "Feature toggles",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"component": [Function],
|
|
||||||
"flag": "featureSearchFrontend",
|
|
||||||
"menu": {
|
|
||||||
"mobile": true,
|
|
||||||
},
|
|
||||||
"path": "/search",
|
"path": "/search",
|
||||||
"title": "Search",
|
"title": "Search",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
|
@ -5,6 +5,6 @@ test('returns all baseRoutes', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('getRoute() returns named route', () => {
|
test('getRoute() returns named route', () => {
|
||||||
const featuresRoute = getRoute('/features');
|
const featuresRoute = getRoute('/search');
|
||||||
expect(featuresRoute?.path).toEqual('/features');
|
expect(featuresRoute?.path).toEqual('/search');
|
||||||
});
|
});
|
||||||
|
@ -142,21 +142,12 @@ export const routes: IRoute[] = [
|
|||||||
type: 'protected',
|
type: 'protected',
|
||||||
menu: {},
|
menu: {},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/features',
|
|
||||||
title: 'Feature toggles',
|
|
||||||
component: FeatureToggleListTable,
|
|
||||||
type: 'protected',
|
|
||||||
menu: { mobile: true },
|
|
||||||
notFlag: 'featureSearchFrontend',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/search',
|
path: '/search',
|
||||||
title: 'Search',
|
title: 'Search',
|
||||||
component: FeatureToggleListTable,
|
component: FeatureToggleListTable,
|
||||||
type: 'protected',
|
type: 'protected',
|
||||||
menu: { mobile: true },
|
menu: { mobile: true },
|
||||||
flag: 'featureSearchFrontend',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Playground
|
// Playground
|
||||||
|
@ -64,7 +64,7 @@ const formatEnvironmentColumnId = (environment: string) =>
|
|||||||
const columnHelper = createColumnHelper<FeatureSearchResponseSchema>();
|
const columnHelper = createColumnHelper<FeatureSearchResponseSchema>();
|
||||||
const getRowId = (row: { name: string }) => row.name;
|
const getRowId = (row: { name: string }) => row.name;
|
||||||
|
|
||||||
export const PaginatedProjectFeatureToggles = ({
|
export const ProjectFeatureToggles = ({
|
||||||
environments,
|
environments,
|
||||||
refreshInterval = 15 * 1000,
|
refreshInterval = 15 * 1000,
|
||||||
storageKey = 'project-feature-toggles-v2',
|
storageKey = 'project-feature-toggles-v2',
|
@ -4,8 +4,8 @@ import { flexRow } from 'themes/themeStyles';
|
|||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import VariantsWarningTooltip from 'component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning';
|
import VariantsWarningTooltip from 'component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning';
|
||||||
import { FeatureToggleSwitch } from './FeatureToggleSwitch';
|
import { FeatureToggleSwitch } from './FeatureToggleSwitch';
|
||||||
import type { ListItemType } from '../ProjectFeatureToggles.types';
|
|
||||||
import type { UseFeatureToggleSwitchType } from './FeatureToggleSwitch.types';
|
import type { UseFeatureToggleSwitchType } from './FeatureToggleSwitch.types';
|
||||||
|
import { ListItemType } from '../../PaginatedProjectFeatureToggles/ProjectFeatureToggles.types';
|
||||||
|
|
||||||
const StyledSwitchContainer = styled('div', {
|
const StyledSwitchContainer = styled('div', {
|
||||||
shouldForwardProp: (prop) => prop !== 'hasWarning',
|
shouldForwardProp: (prop) => prop !== 'hasWarning',
|
||||||
|
@ -1,700 +0,0 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
import {
|
|
||||||
Checkbox,
|
|
||||||
IconButton,
|
|
||||||
styled,
|
|
||||||
Tooltip,
|
|
||||||
useMediaQuery,
|
|
||||||
useTheme,
|
|
||||||
Box,
|
|
||||||
} from '@mui/material';
|
|
||||||
import { Add } from '@mui/icons-material';
|
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
||||||
import {
|
|
||||||
SortingRule,
|
|
||||||
useFlexLayout,
|
|
||||||
useRowSelect,
|
|
||||||
useSortBy,
|
|
||||||
useTable,
|
|
||||||
} from 'react-table';
|
|
||||||
import type { FeatureSchema } from 'openapi';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
|
||||||
import ResponsiveButton from 'component/common/ResponsiveButton/ResponsiveButton';
|
|
||||||
import { getCreateTogglePath } from 'utils/routePathHelpers';
|
|
||||||
import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
|
||||||
import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
|
|
||||||
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
|
|
||||||
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
|
|
||||||
import { IProject } from 'interfaces/project';
|
|
||||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
|
||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
|
||||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
|
||||||
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
|
|
||||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
|
||||||
import { getColumnValues, includesFilter, useSearch } from 'hooks/useSearch';
|
|
||||||
import { Search } from 'component/common/Search/Search';
|
|
||||||
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
|
||||||
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
|
||||||
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
|
||||||
import {
|
|
||||||
ProjectEnvironmentType,
|
|
||||||
useEnvironmentsRef,
|
|
||||||
} from './hooks/useEnvironmentsRef';
|
|
||||||
import { ActionsCell } from './ActionsCell/ActionsCell';
|
|
||||||
import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
|
|
||||||
import { useStyles } from './ProjectFeatureToggles.styles';
|
|
||||||
import { usePinnedFavorites } from 'hooks/usePinnedFavorites';
|
|
||||||
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
|
||||||
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
|
||||||
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
|
|
||||||
import FileDownload from '@mui/icons-material/FileDownload';
|
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
|
||||||
import { MemoizedRowSelectCell } from './RowSelectCell/RowSelectCell';
|
|
||||||
import { BatchSelectionActionsBar } from '../../../common/BatchSelectionActionsBar/BatchSelectionActionsBar';
|
|
||||||
import { ProjectFeaturesBatchActions } from './ProjectFeaturesBatchActions/ProjectFeaturesBatchActions';
|
|
||||||
import { MemoizedFeatureEnvironmentSeenCell } from '../../../common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
|
||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
|
||||||
import { ListItemType } from './ProjectFeatureToggles.types';
|
|
||||||
import { createFeatureToggleCell } from './FeatureToggleSwitch/createFeatureToggleCell';
|
|
||||||
import { useFeatureToggleSwitch } from './FeatureToggleSwitch/useFeatureToggleSwitch';
|
|
||||||
import useToast from 'hooks/useToast';
|
|
||||||
|
|
||||||
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface IProjectFeatureTogglesProps {
|
|
||||||
features: IProject['features'];
|
|
||||||
environments: IProject['environments'];
|
|
||||||
loading: boolean;
|
|
||||||
onChange: () => void;
|
|
||||||
total?: number;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
const staticColumns = ['Select', 'Actions', 'name', 'favorite'];
|
|
||||||
|
|
||||||
const defaultSort: SortingRule<string> & {
|
|
||||||
columns?: string[];
|
|
||||||
} = { id: 'createdAt', desc: true };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated remove when flag `featureSearchFrontend` is removed
|
|
||||||
*/
|
|
||||||
export const ProjectFeatureToggles = ({
|
|
||||||
features,
|
|
||||||
loading,
|
|
||||||
environments: newEnvironments = [],
|
|
||||||
onChange,
|
|
||||||
total,
|
|
||||||
style = {},
|
|
||||||
}: IProjectFeatureTogglesProps) => {
|
|
||||||
const { classes: styles } = useStyles();
|
|
||||||
const theme = useTheme();
|
|
||||||
const { setToastApiError } = useToast();
|
|
||||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
|
||||||
const [featureStaleDialogState, setFeatureStaleDialogState] = useState<{
|
|
||||||
featureId?: string;
|
|
||||||
stale?: boolean;
|
|
||||||
}>({});
|
|
||||||
const [featureArchiveState, setFeatureArchiveState] = useState<
|
|
||||||
string | undefined
|
|
||||||
>();
|
|
||||||
const projectId = useRequiredPathParam('projectId');
|
|
||||||
const { onToggle: onFeatureToggle, modals: featureToggleModals } =
|
|
||||||
useFeatureToggleSwitch(projectId);
|
|
||||||
|
|
||||||
const { value: storedParams, setValue: setStoredParams } =
|
|
||||||
createLocalStorage(
|
|
||||||
`${projectId}:FeatureToggleListTable:v1`,
|
|
||||||
defaultSort,
|
|
||||||
);
|
|
||||||
const { value: globalStore, setValue: setGlobalStore } =
|
|
||||||
useGlobalLocalStorage();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
|
||||||
const environments = useEnvironmentsRef(
|
|
||||||
loading
|
|
||||||
? [{ environment: 'a' }, { environment: 'b' }, { environment: 'c' }]
|
|
||||||
: newEnvironments,
|
|
||||||
);
|
|
||||||
const { isFavoritesPinned, sortTypes, onChangeIsFavoritePinned } =
|
|
||||||
usePinnedFavorites(
|
|
||||||
searchParams.has('favorites')
|
|
||||||
? searchParams.get('favorites') === 'true'
|
|
||||||
: globalStore.favorites,
|
|
||||||
);
|
|
||||||
const { favorite, unfavorite } = useFavoriteFeaturesApi();
|
|
||||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
|
||||||
const [showExportDialog, setShowExportDialog] = useState(false);
|
|
||||||
const { uiConfig } = useUiConfig();
|
|
||||||
|
|
||||||
const onFavorite = useCallback(
|
|
||||||
async (feature: IFeatureToggleListItem) => {
|
|
||||||
try {
|
|
||||||
if (feature?.favorite) {
|
|
||||||
await unfavorite(projectId, feature.name);
|
|
||||||
} else {
|
|
||||||
await favorite(projectId, feature.name);
|
|
||||||
}
|
|
||||||
onChange();
|
|
||||||
} catch (error) {
|
|
||||||
setToastApiError(
|
|
||||||
'Something went wrong, could not update favorite',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[projectId, onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const showTagsColumn = useMemo(
|
|
||||||
() => features.some((feature) => feature?.tags?.length),
|
|
||||||
[features],
|
|
||||||
);
|
|
||||||
|
|
||||||
const columns = useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
id: 'Select',
|
|
||||||
Header: ({ getToggleAllRowsSelectedProps }: any) => (
|
|
||||||
<Checkbox {...getToggleAllRowsSelectedProps()} />
|
|
||||||
),
|
|
||||||
Cell: ({ row }: any) => (
|
|
||||||
<MemoizedRowSelectCell
|
|
||||||
{...row?.getToggleRowSelectedProps?.()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
maxWidth: 50,
|
|
||||||
disableSortBy: true,
|
|
||||||
hideInMenu: true,
|
|
||||||
styles: {
|
|
||||||
borderRadius: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'favorite',
|
|
||||||
Header: (
|
|
||||||
<FavoriteIconHeader
|
|
||||||
isActive={isFavoritesPinned}
|
|
||||||
onClick={onChangeIsFavoritePinned}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
accessor: 'favorite',
|
|
||||||
Cell: ({ row: { original: feature } }: any) => (
|
|
||||||
<FavoriteIconCell
|
|
||||||
value={feature?.favorite}
|
|
||||||
onClick={() => onFavorite(feature)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
maxWidth: 50,
|
|
||||||
disableSortBy: true,
|
|
||||||
hideInMenu: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'Seen',
|
|
||||||
accessor: 'lastSeenAt',
|
|
||||||
Cell: ({ value, row: { original: feature } }: any) => {
|
|
||||||
return (
|
|
||||||
<MemoizedFeatureEnvironmentSeenCell feature={feature} />
|
|
||||||
);
|
|
||||||
},
|
|
||||||
align: 'center',
|
|
||||||
maxWidth: 80,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'Type',
|
|
||||||
accessor: 'type',
|
|
||||||
Cell: FeatureTypeCell,
|
|
||||||
align: 'center',
|
|
||||||
filterName: 'type',
|
|
||||||
maxWidth: 80,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'Name',
|
|
||||||
accessor: 'name',
|
|
||||||
Cell: ({ value }: { value: string }) => (
|
|
||||||
<Tooltip title={value} arrow describeChild>
|
|
||||||
<span>
|
|
||||||
<LinkCell
|
|
||||||
title={value}
|
|
||||||
to={`/projects/${projectId}/features/${value}`}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
),
|
|
||||||
minWidth: 100,
|
|
||||||
sortType: 'alphanumeric',
|
|
||||||
searchable: true,
|
|
||||||
},
|
|
||||||
...(showTagsColumn
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
id: 'tags',
|
|
||||||
Header: 'Tags',
|
|
||||||
accessor: (row: IFeatureToggleListItem) =>
|
|
||||||
row.tags
|
|
||||||
?.map(({ type, value }) => `${type}:${value}`)
|
|
||||||
.join('\n') || '',
|
|
||||||
Cell: FeatureTagCell,
|
|
||||||
width: 80,
|
|
||||||
searchable: true,
|
|
||||||
filterName: 'tags',
|
|
||||||
filterBy(
|
|
||||||
row: IFeatureToggleListItem,
|
|
||||||
values: string[],
|
|
||||||
) {
|
|
||||||
return includesFilter(
|
|
||||||
getColumnValues(this, row),
|
|
||||||
values,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
Header: 'Created',
|
|
||||||
accessor: 'createdAt',
|
|
||||||
Cell: DateCell,
|
|
||||||
minWidth: 120,
|
|
||||||
},
|
|
||||||
...environments.map((value: ProjectEnvironmentType | string) => {
|
|
||||||
const name =
|
|
||||||
typeof value === 'string'
|
|
||||||
? value
|
|
||||||
: (value as ProjectEnvironmentType).environment;
|
|
||||||
const isChangeRequestEnabled = isChangeRequestConfigured(name);
|
|
||||||
const FeatureToggleCell = createFeatureToggleCell(
|
|
||||||
projectId,
|
|
||||||
name,
|
|
||||||
isChangeRequestEnabled,
|
|
||||||
onChange,
|
|
||||||
onFeatureToggle,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
Header: loading ? () => '' : name,
|
|
||||||
maxWidth: 90,
|
|
||||||
id: `environments.${name}`,
|
|
||||||
accessor: (row: ListItemType) =>
|
|
||||||
row.environments[name]?.enabled,
|
|
||||||
align: 'center',
|
|
||||||
Cell: FeatureToggleCell,
|
|
||||||
sortType: 'boolean',
|
|
||||||
filterName: name,
|
|
||||||
filterParsing: (value: boolean) =>
|
|
||||||
value ? 'enabled' : 'disabled',
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
|
|
||||||
{
|
|
||||||
id: 'Actions',
|
|
||||||
maxWidth: 56,
|
|
||||||
width: 56,
|
|
||||||
Cell: (props: { row: { original: ListItemType } }) => (
|
|
||||||
<ActionsCell
|
|
||||||
projectId={projectId}
|
|
||||||
onOpenArchiveDialog={setFeatureArchiveState}
|
|
||||||
onOpenStaleDialog={setFeatureStaleDialogState}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
disableSortBy: true,
|
|
||||||
hideInMenu: true,
|
|
||||||
styles: {
|
|
||||||
borderRadius: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[projectId, environments, loading],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState(
|
|
||||||
searchParams.get('search') || '',
|
|
||||||
);
|
|
||||||
|
|
||||||
const [showTitle, setShowTitle] = useState(true);
|
|
||||||
|
|
||||||
const featuresData = useMemo(
|
|
||||||
() =>
|
|
||||||
features.map((feature) => ({
|
|
||||||
...feature,
|
|
||||||
environments: Object.fromEntries(
|
|
||||||
environments.map((env) => {
|
|
||||||
const thisEnv = feature?.environments.find(
|
|
||||||
(featureEnvironment) =>
|
|
||||||
featureEnvironment?.name === env,
|
|
||||||
);
|
|
||||||
return [
|
|
||||||
env,
|
|
||||||
{
|
|
||||||
name: env,
|
|
||||||
enabled: thisEnv?.enabled || false,
|
|
||||||
variantCount: thisEnv?.variantCount || 0,
|
|
||||||
lastSeenAt: thisEnv?.lastSeenAt,
|
|
||||||
type: thisEnv?.type,
|
|
||||||
hasStrategies: thisEnv?.hasStrategies,
|
|
||||||
hasEnabledStrategies:
|
|
||||||
thisEnv?.hasEnabledStrategies,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
someEnabledEnvironmentHasVariants:
|
|
||||||
feature.environments?.some(
|
|
||||||
(featureEnvironment) =>
|
|
||||||
featureEnvironment.variantCount > 0 &&
|
|
||||||
featureEnvironment.enabled,
|
|
||||||
) || false,
|
|
||||||
})),
|
|
||||||
[features, environments],
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: searchedData,
|
|
||||||
getSearchText,
|
|
||||||
getSearchContext,
|
|
||||||
} = useSearch(columns, searchValue, featuresData);
|
|
||||||
|
|
||||||
const data = useMemo(() => {
|
|
||||||
if (loading) {
|
|
||||||
return Array(6).fill({
|
|
||||||
type: '-',
|
|
||||||
name: 'Feature name',
|
|
||||||
createdAt: new Date(),
|
|
||||||
environments: {
|
|
||||||
production: { name: 'production', enabled: false },
|
|
||||||
},
|
|
||||||
}) as FeatureSchema[];
|
|
||||||
}
|
|
||||||
return searchedData;
|
|
||||||
}, [loading, searchedData]);
|
|
||||||
|
|
||||||
const initialState = useMemo(
|
|
||||||
() => {
|
|
||||||
const allColumnIds = columns
|
|
||||||
.map(
|
|
||||||
(column: any) =>
|
|
||||||
(column?.id as string) ||
|
|
||||||
(typeof column?.accessor === 'string'
|
|
||||||
? (column?.accessor as string)
|
|
||||||
: ''),
|
|
||||||
)
|
|
||||||
.filter(Boolean);
|
|
||||||
let hiddenColumns = environments
|
|
||||||
.filter((_, index) => index >= 3)
|
|
||||||
.map((environment) => `environment:${environment}`);
|
|
||||||
|
|
||||||
if (searchParams.has('columns')) {
|
|
||||||
const columnsInParams =
|
|
||||||
searchParams.get('columns')?.split(',') || [];
|
|
||||||
const visibleColumns = [...staticColumns, ...columnsInParams];
|
|
||||||
hiddenColumns = allColumnIds.filter(
|
|
||||||
(columnId) => !visibleColumns.includes(columnId),
|
|
||||||
);
|
|
||||||
} else if (storedParams.columns) {
|
|
||||||
const visibleColumns = [
|
|
||||||
...staticColumns,
|
|
||||||
...storedParams.columns,
|
|
||||||
];
|
|
||||||
hiddenColumns = allColumnIds.filter(
|
|
||||||
(columnId) => !visibleColumns.includes(columnId),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
sortBy: [
|
|
||||||
{
|
|
||||||
id:
|
|
||||||
searchParams.get('sort') ||
|
|
||||||
storedParams.id ||
|
|
||||||
'createdAt',
|
|
||||||
desc: searchParams.has('order')
|
|
||||||
? searchParams.get('order') === 'desc'
|
|
||||||
: storedParams.desc,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
hiddenColumns,
|
|
||||||
selectedRowIds: {},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[environments], // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
);
|
|
||||||
|
|
||||||
const getRowId = useCallback((row: any) => row.name, []);
|
|
||||||
const {
|
|
||||||
allColumns,
|
|
||||||
headerGroups,
|
|
||||||
rows,
|
|
||||||
state: { selectedRowIds, sortBy, hiddenColumns },
|
|
||||||
prepareRow,
|
|
||||||
setHiddenColumns,
|
|
||||||
toggleAllRowsSelected,
|
|
||||||
} = useTable(
|
|
||||||
{
|
|
||||||
columns: columns as any[], // TODO: fix after `react-table` v8 update
|
|
||||||
data,
|
|
||||||
initialState,
|
|
||||||
sortTypes,
|
|
||||||
autoResetHiddenColumns: false,
|
|
||||||
autoResetSelectedRows: false,
|
|
||||||
disableSortRemove: true,
|
|
||||||
autoResetSortBy: false,
|
|
||||||
getRowId,
|
|
||||||
},
|
|
||||||
useFlexLayout,
|
|
||||||
useSortBy,
|
|
||||||
useRowSelect,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tableState: Record<string, string> = {};
|
|
||||||
tableState.sort = sortBy[0].id;
|
|
||||||
if (sortBy[0].desc) {
|
|
||||||
tableState.order = 'desc';
|
|
||||||
}
|
|
||||||
if (searchValue) {
|
|
||||||
tableState.search = searchValue;
|
|
||||||
}
|
|
||||||
if (isFavoritesPinned) {
|
|
||||||
tableState.favorites = 'true';
|
|
||||||
}
|
|
||||||
tableState.columns = allColumns
|
|
||||||
.map(({ id }) => id)
|
|
||||||
.filter(
|
|
||||||
(id) =>
|
|
||||||
!staticColumns.includes(id) && !hiddenColumns?.includes(id),
|
|
||||||
)
|
|
||||||
.join(',');
|
|
||||||
|
|
||||||
setSearchParams(tableState, {
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
setStoredParams((params) => ({
|
|
||||||
...params,
|
|
||||||
id: sortBy[0].id,
|
|
||||||
desc: sortBy[0].desc || false,
|
|
||||||
columns: tableState.columns.split(','),
|
|
||||||
}));
|
|
||||||
setGlobalStore((params) => ({
|
|
||||||
...params,
|
|
||||||
favorites: Boolean(isFavoritesPinned),
|
|
||||||
}));
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [
|
|
||||||
loading,
|
|
||||||
sortBy,
|
|
||||||
hiddenColumns,
|
|
||||||
searchValue,
|
|
||||||
setSearchParams,
|
|
||||||
isFavoritesPinned,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PageContent
|
|
||||||
isLoading={loading}
|
|
||||||
disablePadding
|
|
||||||
className={styles.container}
|
|
||||||
style={style}
|
|
||||||
header={
|
|
||||||
<Box
|
|
||||||
sx={(theme) => ({
|
|
||||||
padding: `${theme.spacing(2.5)} ${theme.spacing(
|
|
||||||
3.125,
|
|
||||||
)}`,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<PageHeader
|
|
||||||
titleElement={
|
|
||||||
showTitle
|
|
||||||
? `Feature toggles (${
|
|
||||||
total || rows.length
|
|
||||||
})`
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
actions={
|
|
||||||
<>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={!isSmallScreen}
|
|
||||||
show={
|
|
||||||
<Search
|
|
||||||
placeholder='Search and Filter'
|
|
||||||
expandable
|
|
||||||
initialValue={searchValue}
|
|
||||||
onChange={setSearchValue}
|
|
||||||
onFocus={() =>
|
|
||||||
setShowTitle(false)
|
|
||||||
}
|
|
||||||
onBlur={() =>
|
|
||||||
setShowTitle(true)
|
|
||||||
}
|
|
||||||
hasFilters
|
|
||||||
getSearchContext={
|
|
||||||
getSearchContext
|
|
||||||
}
|
|
||||||
id='projectFeatureToggles'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ColumnsMenu
|
|
||||||
allColumns={allColumns}
|
|
||||||
staticColumns={staticColumns}
|
|
||||||
dividerAfter={['createdAt']}
|
|
||||||
dividerBefore={['Actions']}
|
|
||||||
isCustomized={Boolean(
|
|
||||||
storedParams.columns,
|
|
||||||
)}
|
|
||||||
setHiddenColumns={setHiddenColumns}
|
|
||||||
/>
|
|
||||||
<PageHeader.Divider
|
|
||||||
sx={{ marginLeft: 0 }}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(
|
|
||||||
uiConfig?.flags
|
|
||||||
?.featuresExportImport,
|
|
||||||
)}
|
|
||||||
show={
|
|
||||||
<Tooltip
|
|
||||||
title='Export toggles visible in the table below'
|
|
||||||
arrow
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
onClick={() =>
|
|
||||||
setShowExportDialog(
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
sx={(theme) => ({
|
|
||||||
marginRight:
|
|
||||||
theme.spacing(2),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<FileDownload />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<StyledResponsiveButton
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
getCreateTogglePath(projectId),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
maxWidth='960px'
|
|
||||||
Icon={Add}
|
|
||||||
projectId={projectId}
|
|
||||||
permission={CREATE_FEATURE}
|
|
||||||
data-testid='NAVIGATE_TO_CREATE_FEATURE'
|
|
||||||
>
|
|
||||||
New feature toggle
|
|
||||||
</StyledResponsiveButton>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={isSmallScreen}
|
|
||||||
show={
|
|
||||||
<Search
|
|
||||||
initialValue={searchValue}
|
|
||||||
onChange={setSearchValue}
|
|
||||||
hasFilters
|
|
||||||
getSearchContext={getSearchContext}
|
|
||||||
id='projectFeatureToggles'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PageHeader>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
|
||||||
<VirtualizedTable
|
|
||||||
rows={rows}
|
|
||||||
headerGroups={headerGroups}
|
|
||||||
prepareRow={prepareRow}
|
|
||||||
/>
|
|
||||||
</SearchHighlightProvider>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={rows.length === 0}
|
|
||||||
show={
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={searchValue?.length > 0}
|
|
||||||
show={
|
|
||||||
<Box sx={{ padding: theme.spacing(3) }}>
|
|
||||||
<TablePlaceholder>
|
|
||||||
No feature toggles found matching
|
|
||||||
“
|
|
||||||
{searchValue}
|
|
||||||
”
|
|
||||||
</TablePlaceholder>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
elseShow={
|
|
||||||
<Box sx={{ padding: theme.spacing(3) }}>
|
|
||||||
<TablePlaceholder>
|
|
||||||
No feature toggles found matching your
|
|
||||||
criteria. Get started by adding a new
|
|
||||||
feature toggle.
|
|
||||||
</TablePlaceholder>
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<FeatureStaleDialog
|
|
||||||
isStale={featureStaleDialogState.stale === true}
|
|
||||||
isOpen={Boolean(featureStaleDialogState.featureId)}
|
|
||||||
onClose={() => {
|
|
||||||
setFeatureStaleDialogState({});
|
|
||||||
onChange();
|
|
||||||
}}
|
|
||||||
featureId={featureStaleDialogState.featureId || ''}
|
|
||||||
projectId={projectId}
|
|
||||||
/>
|
|
||||||
<FeatureArchiveDialog
|
|
||||||
isOpen={Boolean(featureArchiveState)}
|
|
||||||
onConfirm={onChange}
|
|
||||||
onClose={() => {
|
|
||||||
setFeatureArchiveState(undefined);
|
|
||||||
}}
|
|
||||||
featureIds={[featureArchiveState || '']}
|
|
||||||
projectId={projectId}
|
|
||||||
/>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={
|
|
||||||
Boolean(uiConfig?.flags?.featuresExportImport) &&
|
|
||||||
!loading
|
|
||||||
}
|
|
||||||
show={
|
|
||||||
<ExportDialog
|
|
||||||
showExportDialog={showExportDialog}
|
|
||||||
data={data}
|
|
||||||
onClose={() => setShowExportDialog(false)}
|
|
||||||
environments={environments}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{featureToggleModals}
|
|
||||||
</PageContent>
|
|
||||||
<BatchSelectionActionsBar
|
|
||||||
count={Object.keys(selectedRowIds).length}
|
|
||||||
>
|
|
||||||
<ProjectFeaturesBatchActions
|
|
||||||
selectedIds={Object.keys(selectedRowIds)}
|
|
||||||
data={features}
|
|
||||||
projectId={projectId}
|
|
||||||
onResetSelection={() => toggleAllRowsSelected(false)}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
</BatchSelectionActionsBar>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,18 +1,14 @@
|
|||||||
import { FC, useEffect } from 'react';
|
import { FC, useEffect } from 'react';
|
||||||
import useProject, {
|
|
||||||
useProjectNameOrId,
|
|
||||||
} from 'hooks/api/getters/useProject/useProject';
|
|
||||||
import { Box, styled } from '@mui/material';
|
import { Box, styled } from '@mui/material';
|
||||||
import { ProjectFeatureToggles } from './ProjectFeatureToggles/ProjectFeatureToggles';
|
|
||||||
import ProjectInfo from './ProjectInfo/ProjectInfo';
|
import ProjectInfo from './ProjectInfo/ProjectInfo';
|
||||||
import { usePageTitle } from 'hooks/usePageTitle';
|
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { useLastViewedProject } from 'hooks/useLastViewedProject';
|
|
||||||
import { ProjectStats } from './ProjectStats/ProjectStats';
|
import { ProjectStats } from './ProjectStats/ProjectStats';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { ProjectFeatureToggles } from './PaginatedProjectFeatureToggles/ProjectFeatureToggles';
|
||||||
import { PaginatedProjectFeatureToggles } from './PaginatedProjectFeatureToggles/PaginatedProjectFeatureToggles';
|
import useProjectOverview, {
|
||||||
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
useProjectOverviewNameOrId,
|
||||||
import { type FeatureTypeCount } from '../../../interfaces/project';
|
} from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||||
|
import { usePageTitle } from 'hooks/usePageTitle';
|
||||||
|
import { useLastViewedProject } from 'hooks/useLastViewedProject';
|
||||||
|
|
||||||
const refreshInterval = 15 * 1000;
|
const refreshInterval = 15 * 1000;
|
||||||
|
|
||||||
@ -37,13 +33,19 @@ const StyledContentContainer = styled(Box)(({ theme }) => ({
|
|||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const PaginatedProjectOverview: FC<{
|
const ProjectOverview: FC<{
|
||||||
storageKey?: string;
|
storageKey?: string;
|
||||||
}> = ({ storageKey = 'project-overview-v2' }) => {
|
}> = ({ storageKey = 'project-overview-v2' }) => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
const projectName = useProjectOverviewNameOrId(projectId);
|
||||||
const { project } = useProjectOverview(projectId, {
|
const { project } = useProjectOverview(projectId, {
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
});
|
});
|
||||||
|
usePageTitle(`Project overview – ${projectName}`);
|
||||||
|
const { setLastViewed } = useLastViewedProject();
|
||||||
|
useEffect(() => {
|
||||||
|
setLastViewed(projectId);
|
||||||
|
}, [projectId, setLastViewed]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
members,
|
members,
|
||||||
@ -67,7 +69,7 @@ const PaginatedProjectOverview: FC<{
|
|||||||
<StyledContentContainer>
|
<StyledContentContainer>
|
||||||
<ProjectStats stats={project.stats} />
|
<ProjectStats stats={project.stats} />
|
||||||
<StyledProjectToggles>
|
<StyledProjectToggles>
|
||||||
<PaginatedProjectFeatureToggles
|
<ProjectFeatureToggles
|
||||||
environments={environments}
|
environments={environments}
|
||||||
refreshInterval={refreshInterval}
|
refreshInterval={refreshInterval}
|
||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
@ -78,66 +80,4 @@ const PaginatedProjectOverview: FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated remove when flag `featureSearchFrontend` is removed
|
|
||||||
*/
|
|
||||||
const ProjectOverview = () => {
|
|
||||||
const projectId = useRequiredPathParam('projectId');
|
|
||||||
const projectName = useProjectNameOrId(projectId);
|
|
||||||
const { project, loading, refetch } = useProject(projectId, {
|
|
||||||
refreshInterval,
|
|
||||||
});
|
|
||||||
const { members, features, health, description, environments, stats } =
|
|
||||||
project;
|
|
||||||
usePageTitle(`Project overview – ${projectName}`);
|
|
||||||
const { setLastViewed } = useLastViewedProject();
|
|
||||||
const featureSearchFrontend = useUiFlag('featureSearchFrontend');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLastViewed(projectId);
|
|
||||||
}, [projectId, setLastViewed]);
|
|
||||||
|
|
||||||
if (featureSearchFrontend) return <PaginatedProjectOverview />;
|
|
||||||
|
|
||||||
const featureTypeCounts = features.reduce(
|
|
||||||
(acc: FeatureTypeCount[], feature) => {
|
|
||||||
const existingEntry = acc.find(
|
|
||||||
(entry) => entry.type === feature.type,
|
|
||||||
);
|
|
||||||
if (existingEntry) {
|
|
||||||
existingEntry.count += 1;
|
|
||||||
} else {
|
|
||||||
acc.push({ type: feature.type, count: 1 });
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledContainer>
|
|
||||||
<ProjectInfo
|
|
||||||
id={projectId}
|
|
||||||
description={description}
|
|
||||||
memberCount={members}
|
|
||||||
health={health}
|
|
||||||
featureTypeCounts={featureTypeCounts}
|
|
||||||
stats={stats}
|
|
||||||
/>
|
|
||||||
<StyledContentContainer>
|
|
||||||
<ProjectStats stats={project.stats} />
|
|
||||||
<StyledProjectToggles>
|
|
||||||
<ProjectFeatureToggles
|
|
||||||
key={loading ? 'loading' : 'ready'}
|
|
||||||
features={features}
|
|
||||||
environments={environments}
|
|
||||||
loading={loading}
|
|
||||||
onChange={refetch}
|
|
||||||
/>
|
|
||||||
</StyledProjectToggles>
|
|
||||||
</StyledContentContainer>
|
|
||||||
</StyledContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProjectOverview;
|
export default ProjectOverview;
|
||||||
|
@ -65,7 +65,6 @@ export type UiFlags = {
|
|||||||
dependentFeatures?: boolean;
|
dependentFeatures?: boolean;
|
||||||
scheduledConfigurationChanges?: boolean;
|
scheduledConfigurationChanges?: boolean;
|
||||||
featureSearchAPI?: boolean;
|
featureSearchAPI?: boolean;
|
||||||
featureSearchFrontend?: boolean;
|
|
||||||
newStrategyConfiguration?: boolean;
|
newStrategyConfiguration?: boolean;
|
||||||
incomingWebhooks?: boolean;
|
incomingWebhooks?: boolean;
|
||||||
automatedActions?: boolean;
|
automatedActions?: boolean;
|
||||||
|
@ -95,10 +95,9 @@ exports[`should create default config 1`] = `
|
|||||||
"executiveDashboard": false,
|
"executiveDashboard": false,
|
||||||
"extendedUsageMetrics": false,
|
"extendedUsageMetrics": false,
|
||||||
"extendedUsageMetricsUI": false,
|
"extendedUsageMetricsUI": false,
|
||||||
"featureSearchAPI": false,
|
"featureSearchAPI": true,
|
||||||
"featureSearchFeedback": false,
|
"featureSearchFeedback": false,
|
||||||
"featureSearchFeedbackPosting": false,
|
"featureSearchFeedbackPosting": false,
|
||||||
"featureSearchFrontend": false,
|
|
||||||
"featuresExportImport": true,
|
"featuresExportImport": true,
|
||||||
"feedbackComments": {
|
"feedbackComments": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
|
@ -28,7 +28,6 @@ export type IFlagKey =
|
|||||||
| 'customRootRolesKillSwitch'
|
| 'customRootRolesKillSwitch'
|
||||||
| 'disableMetrics'
|
| 'disableMetrics'
|
||||||
| 'featureSearchAPI'
|
| 'featureSearchAPI'
|
||||||
| 'featureSearchFrontend'
|
|
||||||
| 'scheduledConfigurationChanges'
|
| 'scheduledConfigurationChanges'
|
||||||
| 'detectSegmentUsageInChangeRequests'
|
| 'detectSegmentUsageInChangeRequests'
|
||||||
| 'stripClientHeadersOn304'
|
| 'stripClientHeadersOn304'
|
||||||
@ -133,11 +132,7 @@ const flags: IFlags = {
|
|||||||
),
|
),
|
||||||
featureSearchAPI: parseEnvVarBoolean(
|
featureSearchAPI: parseEnvVarBoolean(
|
||||||
process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_API,
|
process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_API,
|
||||||
false,
|
true,
|
||||||
),
|
|
||||||
featureSearchFrontend: parseEnvVarBoolean(
|
|
||||||
process.env.UNLEASH_EXPERIMENTAL_FEATURE_SEARCH_FRONTEND,
|
|
||||||
false,
|
|
||||||
),
|
),
|
||||||
scheduledConfigurationChanges: parseEnvVarBoolean(
|
scheduledConfigurationChanges: parseEnvVarBoolean(
|
||||||
process.env.UNLEASH_EXPERIMENTAL_SCHEDULED_CONFIGURATION_CHANGES,
|
process.env.UNLEASH_EXPERIMENTAL_SCHEDULED_CONFIGURATION_CHANGES,
|
||||||
|
@ -41,7 +41,6 @@ process.nextTick(async () => {
|
|||||||
anonymiseEventLog: false,
|
anonymiseEventLog: false,
|
||||||
responseTimeWithAppNameKillSwitch: false,
|
responseTimeWithAppNameKillSwitch: false,
|
||||||
featureSearchAPI: true,
|
featureSearchAPI: true,
|
||||||
featureSearchFrontend: true,
|
|
||||||
stripClientHeadersOn304: true,
|
stripClientHeadersOn304: true,
|
||||||
newStrategyConfiguration: true,
|
newStrategyConfiguration: true,
|
||||||
stripHeadersOnAPI: true,
|
stripHeadersOnAPI: true,
|
||||||
|
Loading…
Reference in New Issue
Block a user