mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-28 17:55:15 +02:00
409 lines
16 KiB
TypeScript
409 lines
16 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState, VFC } from 'react';
|
|
import {
|
|
Box,
|
|
IconButton,
|
|
Link,
|
|
Tooltip,
|
|
useMediaQuery,
|
|
useTheme,
|
|
} from '@mui/material';
|
|
import { Link as RouterLink } from 'react-router-dom';
|
|
import { createColumnHelper, useReactTable } from '@tanstack/react-table';
|
|
import { PaginatedTable, TablePlaceholder } from 'component/common/Table';
|
|
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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
|
import { FeatureSchema, FeatureSearchResponseSchema } from 'openapi';
|
|
import { FeatureStaleCell } from './FeatureStaleCell/FeatureStaleCell';
|
|
import { Search } from 'component/common/Search/Search';
|
|
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 { 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';
|
|
import { FeatureToggleFilters } from './FeatureToggleFilters/FeatureToggleFilters';
|
|
import {
|
|
DEFAULT_PAGE_LIMIT,
|
|
useFeatureSearch,
|
|
} from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
|
|
import mapValues from 'lodash.mapvalues';
|
|
import {
|
|
BooleansStringParam,
|
|
FilterItemParam,
|
|
} from 'utils/serializeQueryParams';
|
|
import {
|
|
encodeQueryParams,
|
|
NumberParam,
|
|
StringParam,
|
|
withDefault,
|
|
} from 'use-query-params';
|
|
import { withTableState } from 'utils/withTableState';
|
|
import { usePersistentTableState } from 'hooks/usePersistentTableState';
|
|
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
|
import { FeatureSegmentCell } from 'component/common/Table/cells/FeatureSegmentCell/FeatureSegmentCell';
|
|
import { useUiFlag } from 'hooks/useUiFlag';
|
|
import { FeatureToggleListTable as LegacyFeatureToggleListTable } from './LegacyFeatureToggleListTable';
|
|
import { FeatureToggleListActions } from './FeatureToggleListActions/FeatureToggleListActions';
|
|
import useLoading from 'hooks/useLoading';
|
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
|
import { useFeedback } from '../../feedbackNew/useFeedback';
|
|
import { ReviewsOutlined } from '@mui/icons-material';
|
|
import { useUserSubmittedFeedback } from 'hooks/useSubmittedFeedback';
|
|
|
|
export const featuresPlaceholder = Array(15).fill({
|
|
name: 'Name of the feature',
|
|
description: 'Short description of the feature',
|
|
type: '-',
|
|
createdAt: new Date(2022, 1, 1),
|
|
project: 'projectID',
|
|
});
|
|
|
|
const columnHelper = createColumnHelper<FeatureSearchResponseSchema>();
|
|
const feedbackCategory = 'search';
|
|
|
|
const FeatureToggleListTableComponent: VFC = () => {
|
|
const theme = useTheme();
|
|
const { openFeedback } = useFeedback(feedbackCategory, 'automatic');
|
|
const { trackEvent } = usePlausibleTracker();
|
|
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 { setToastApiError } = useToast();
|
|
const { uiConfig } = useUiConfig();
|
|
const featureSearchFeedback = useUiFlag('featureSearchFeedback');
|
|
|
|
const stateConfig = {
|
|
offset: withDefault(NumberParam, 0),
|
|
limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT),
|
|
query: StringParam,
|
|
favoritesFirst: withDefault(BooleansStringParam, true),
|
|
sortBy: withDefault(StringParam, 'createdAt'),
|
|
sortOrder: withDefault(StringParam, 'desc'),
|
|
project: FilterItemParam,
|
|
tag: FilterItemParam,
|
|
state: FilterItemParam,
|
|
segment: FilterItemParam,
|
|
createdAt: FilterItemParam,
|
|
};
|
|
const [tableState, setTableState] = usePersistentTableState(
|
|
'features-list-table',
|
|
stateConfig,
|
|
);
|
|
const {
|
|
offset,
|
|
limit,
|
|
query,
|
|
favoritesFirst,
|
|
sortBy,
|
|
sortOrder,
|
|
...filterState
|
|
} = tableState;
|
|
|
|
const {
|
|
features = [],
|
|
total,
|
|
loading,
|
|
refetch: refetchFeatures,
|
|
initialLoad,
|
|
} = useFeatureSearch(
|
|
mapValues(encodeQueryParams(stateConfig, tableState), (value) =>
|
|
value ? `${value}` : undefined,
|
|
),
|
|
);
|
|
const bodyLoadingRef = useLoading(loading);
|
|
const { favorite, unfavorite } = useFavoriteFeaturesApi();
|
|
const onFavorite = useCallback(
|
|
async (feature: FeatureSchema) => {
|
|
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(
|
|
() => [
|
|
columnHelper.accessor('favorite', {
|
|
header: () => (
|
|
<FavoriteIconHeader
|
|
isActive={favoritesFirst}
|
|
onClick={() =>
|
|
setTableState({
|
|
favoritesFirst: !favoritesFirst,
|
|
})
|
|
}
|
|
/>
|
|
),
|
|
cell: ({ getValue, row }) => (
|
|
<>
|
|
<FavoriteIconCell
|
|
value={getValue()}
|
|
onClick={() => onFavorite(row.original)}
|
|
/>
|
|
</>
|
|
),
|
|
enableSorting: false,
|
|
}),
|
|
columnHelper.accessor('lastSeenAt', {
|
|
header: 'Seen',
|
|
cell: ({ row }) => (
|
|
<FeatureEnvironmentSeenCell feature={row.original} />
|
|
),
|
|
meta: {
|
|
align: 'center',
|
|
},
|
|
}),
|
|
columnHelper.accessor('type', {
|
|
header: 'Type',
|
|
cell: ({ getValue }) => <FeatureTypeCell value={getValue()} />,
|
|
meta: {
|
|
align: 'center',
|
|
},
|
|
}),
|
|
columnHelper.accessor('name', {
|
|
header: 'Name',
|
|
// cell: (cell) => <FeatureNameCell value={cell.row} />,
|
|
cell: ({ row }) => (
|
|
<LinkCell
|
|
title={row.original.name}
|
|
subtitle={row.original.description || undefined}
|
|
to={`/projects/${row.original.project}/features/${row.original.name}`}
|
|
/>
|
|
),
|
|
}),
|
|
columnHelper.accessor((row) => row.segments?.join('\n') || '', {
|
|
header: 'Segments',
|
|
cell: ({ getValue, row }) => (
|
|
<FeatureSegmentCell value={getValue()} row={row} />
|
|
),
|
|
enableSorting: false,
|
|
}),
|
|
columnHelper.accessor(
|
|
(row) =>
|
|
row.tags
|
|
?.map(({ type, value }) => `${type}:${value}`)
|
|
.join('\n') || '',
|
|
{
|
|
header: 'Tags',
|
|
cell: FeatureTagCell,
|
|
enableSorting: false,
|
|
},
|
|
),
|
|
columnHelper.accessor('createdAt', {
|
|
header: 'Created',
|
|
cell: ({ getValue }) => <DateCell value={getValue()} />,
|
|
}),
|
|
columnHelper.accessor('project', {
|
|
header: 'Project ID',
|
|
cell: ({ getValue }) => (
|
|
<LinkCell
|
|
title={getValue()}
|
|
to={`/projects/${getValue()}`}
|
|
/>
|
|
),
|
|
}),
|
|
columnHelper.accessor('stale', {
|
|
header: 'State',
|
|
cell: ({ getValue }) => <FeatureStaleCell value={getValue()} />,
|
|
}),
|
|
],
|
|
[favoritesFirst],
|
|
);
|
|
|
|
const data = useMemo(
|
|
() =>
|
|
features?.length === 0 && loading ? featuresPlaceholder : features,
|
|
[initialLoad, features, loading],
|
|
);
|
|
|
|
const table = useReactTable(
|
|
withTableState(tableState, setTableState, {
|
|
columns,
|
|
data,
|
|
}),
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (isSmallScreen) {
|
|
table.setColumnVisibility({
|
|
type: false,
|
|
createdAt: false,
|
|
tags: false,
|
|
lastSeenAt: false,
|
|
stale: false,
|
|
});
|
|
} else if (isMediumScreen) {
|
|
table.setColumnVisibility({
|
|
lastSeenAt: false,
|
|
stale: false,
|
|
});
|
|
} else {
|
|
table.setColumnVisibility({});
|
|
}
|
|
}, [isSmallScreen, isMediumScreen]);
|
|
|
|
const setSearchValue = (query = '') => setTableState({ query });
|
|
|
|
const rows = table.getRowModel().rows;
|
|
|
|
if (!(environments.length > 0)) {
|
|
return null;
|
|
}
|
|
|
|
const createFeedbackContext = () => {
|
|
openFeedback({
|
|
title: 'How easy was it to use search and filters?',
|
|
positiveLabel: 'What do you like most about search and filters?',
|
|
areasForImprovementsLabel:
|
|
'What should be improved in search and filters page?',
|
|
});
|
|
};
|
|
|
|
return (
|
|
<PageContent
|
|
bodyClass='no-padding'
|
|
header={
|
|
<PageHeader
|
|
title='Search'
|
|
actions={
|
|
<>
|
|
<ConditionallyRender
|
|
condition={!isSmallScreen}
|
|
show={
|
|
<>
|
|
<Search
|
|
placeholder='Search'
|
|
expandable
|
|
initialValue={query || ''}
|
|
onChange={setSearchValue}
|
|
id='globalFeatureToggles'
|
|
/>
|
|
<PageHeader.Divider />
|
|
</>
|
|
}
|
|
/>
|
|
<Link
|
|
component={RouterLink}
|
|
to='/archive'
|
|
underline='always'
|
|
sx={{ marginRight: 2, ...focusable(theme) }}
|
|
onClick={() => {
|
|
trackEvent('search-feature-buttons', {
|
|
props: {
|
|
action: 'archive',
|
|
},
|
|
});
|
|
}}
|
|
>
|
|
View archive
|
|
</Link>
|
|
<FeatureToggleListActions
|
|
onExportClick={() => setShowExportDialog(true)}
|
|
/>
|
|
<ConditionallyRender
|
|
condition={featureSearchFeedback}
|
|
show={
|
|
<Tooltip title='Provide feedback' arrow>
|
|
<IconButton
|
|
onClick={createFeedbackContext}
|
|
size='large'
|
|
>
|
|
<ReviewsOutlined />
|
|
</IconButton>
|
|
</Tooltip>
|
|
}
|
|
/>
|
|
</>
|
|
}
|
|
>
|
|
<ConditionallyRender
|
|
condition={isSmallScreen}
|
|
show={
|
|
<Search
|
|
initialValue={query || ''}
|
|
onChange={setSearchValue}
|
|
id='globalFeatureToggles'
|
|
/>
|
|
}
|
|
/>
|
|
</PageHeader>
|
|
}
|
|
>
|
|
<FeatureToggleFilters
|
|
onChange={setTableState}
|
|
state={filterState}
|
|
/>
|
|
<SearchHighlightProvider value={query || ''}>
|
|
<div ref={bodyLoadingRef}>
|
|
<PaginatedTable tableInstance={table} totalItems={total} />
|
|
</div>
|
|
</SearchHighlightProvider>
|
|
<ConditionallyRender
|
|
condition={rows.length === 0}
|
|
show={
|
|
<Box sx={(theme) => ({ padding: theme.spacing(0, 2, 2) })}>
|
|
<ConditionallyRender
|
|
condition={(query || '')?.length > 0}
|
|
show={
|
|
<TablePlaceholder>
|
|
No feature toggles found matching “
|
|
{query}
|
|
”
|
|
</TablePlaceholder>
|
|
}
|
|
elseShow={
|
|
<TablePlaceholder>
|
|
No feature toggles found matching your
|
|
criteria. Get started by adding a new
|
|
feature toggle.
|
|
</TablePlaceholder>
|
|
}
|
|
/>
|
|
</Box>
|
|
}
|
|
/>
|
|
<ConditionallyRender
|
|
condition={Boolean(uiConfig?.flags?.featuresExportImport)}
|
|
show={
|
|
<ExportDialog
|
|
showExportDialog={showExportDialog}
|
|
data={data}
|
|
onClose={() => setShowExportDialog(false)}
|
|
environments={enabledEnvironments}
|
|
/>
|
|
}
|
|
/>
|
|
</PageContent>
|
|
);
|
|
};
|
|
|
|
export const FeatureToggleListTable: VFC = () => {
|
|
const featureSearchFrontend = useUiFlag('featureSearchFrontend');
|
|
|
|
if (featureSearchFrontend) return <FeatureToggleListTableComponent />;
|
|
|
|
return <LegacyFeatureToggleListTable />;
|
|
};
|