1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-28 17:55:15 +02:00
unleash.unleash/frontend/src/component/feature/FeatureToggleList/FeatureToggleListTable.tsx

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 &ldquo;
{query}
&rdquo;
</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 />;
};