diff --git a/frontend/src/component/common/Table/PaginatedTable/PaginatedTable.tsx b/frontend/src/component/common/Table/PaginatedTable/PaginatedTable.tsx index 34556e7de8f..fa1a88550ee 100644 --- a/frontend/src/component/common/Table/PaginatedTable/PaginatedTable.tsx +++ b/frontend/src/component/common/Table/PaginatedTable/PaginatedTable.tsx @@ -28,8 +28,7 @@ const HeaderCell = (header: Header) => { onClick={() => column.toggleSorting()} styles={{ borderRadius: '0px', - paddingTop: 0, - paddingBottom: 0, + padding: 0, width, maxWidth: fixedWidth, minWidth: fixedWidth, diff --git a/frontend/src/component/common/Table/cells/FavoriteIconCell/FavoriteIconCell.tsx b/frontend/src/component/common/Table/cells/FavoriteIconCell/FavoriteIconCell.tsx index 8a706355a89..8015fd21101 100644 --- a/frontend/src/component/common/Table/cells/FavoriteIconCell/FavoriteIconCell.tsx +++ b/frontend/src/component/common/Table/cells/FavoriteIconCell/FavoriteIconCell.tsx @@ -5,12 +5,12 @@ import StarBorderIcon from '@mui/icons-material/StarBorder'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; const StyledCell = styled(Box)(({ theme }) => ({ - paddingLeft: theme.spacing(1.25), + paddingRight: theme.spacing(0.5), })); const StyledIconButton = styled(IconButton)(({ theme }) => ({ color: theme.palette.primary.main, - padding: theme.spacing(1.25), + paddingRight: theme.spacing(0.5), })); const StyledIconButtonInactive = styled(StyledIconButton)({ diff --git a/frontend/src/component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell.tsx b/frontend/src/component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell.tsx index fc5e10b8b13..3b9d7afbbcd 100644 --- a/frontend/src/component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell.tsx +++ b/frontend/src/component/common/Table/cells/FeatureOverviewCell/FeatureOverviewCell.tsx @@ -1,6 +1,6 @@ import type { FC } from 'react'; import type { FeatureSearchResponseSchema } from '../../../../../openapi'; -import { Box, styled, Tooltip } from '@mui/material'; +import { Box, styled } from '@mui/material'; import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes'; import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons'; import { useSearchHighlightContext } from '../../SearchHighlightContext/SearchHighlightContext'; @@ -59,13 +59,13 @@ const CappedDescription: FC<{ text: string; searchQuery: string }> = ({ placement='bottom-start' arrow > - + {text} } elseShow={ - + {text} } @@ -73,11 +73,26 @@ const CappedDescription: FC<{ text: string; searchQuery: string }> = ({ ); }; +const CappedTag: FC<{ tag: string }> = ({ tag }) => { + return ( + 30} + show={ + + {tag} + + } + elseShow={{tag}} + /> + ); +}; + const Container = styled(Box)(({ theme }) => ({ display: 'flex', flexDirection: 'column', gap: theme.spacing(0.5), - margin: theme.spacing(1, 0, 1, 0), + margin: theme.spacing(1.25, 0, 1, 0), + paddingLeft: theme.spacing(2), })); const FeatureNameAndType = styled(Box)(({ theme }) => ({ @@ -108,7 +123,6 @@ const FeatureName: FC<{ ({ fontWeight: theme.typography.fontWeightBold })}> = ({ tags }) => { return ( - {tag1 && {tag1}} - {tag2 && {tag2}} - {tag3 && {tag3}} + {tag1 && } + {tag2 && } + {tag3 && } 0} show={} @@ -152,56 +166,58 @@ const PrimaryFeatureInfo: FC<{ feature: string; searchQuery: string; type: string; -}> = ({ project, feature, type, searchQuery }) => { + dependencyType: string; +}> = ({ project, feature, type, searchQuery, dependencyType }) => { const { featureTypes } = useFeatureTypes(); const IconComponent = getFeatureTypeIcons(type); const typeName = featureTypes.find( (featureType) => featureType.id === type, )?.name; - const title = `This is a "${typeName || type}" flag`; + const title = `${typeName || type} flag`; const TypeIcon = () => ( - - ({ fontSize: theme.spacing(2) })} - data-loading - /> - + + ({ fontSize: theme.spacing(2) })} /> + ); return ( - + + + {dependencyType} + + } + /> ); }; const SecondaryFeatureInfo: FC<{ - dependencyType: string; description: string; searchQuery: string; -}> = ({ dependencyType, description, searchQuery }) => { +}> = ({ description, searchQuery }) => { return ( ({ display: 'flex', gap: theme.spacing(1) })} > - - {dependencyType} - = ({ row }) => { feature={row.original.name} searchQuery={searchQuery} type={row.original.type || ''} + dependencyType={row.original.dependencyType || ''} /> diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/OldProjectFeatureToggles.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/OldProjectFeatureToggles.tsx new file mode 100644 index 00000000000..80e8860d4fe --- /dev/null +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/OldProjectFeatureToggles.tsx @@ -0,0 +1,495 @@ +import { useCallback, useMemo, useState } from 'react'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { PageContent } from 'component/common/PageContent/PageContent'; +import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; +import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; +import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell'; +import { PaginatedTable } from 'component/common/Table'; +import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; +import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader'; +import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell'; +import { ActionsCell } from '../ProjectFeatureToggles/ActionsCell/ActionsCell'; +import { ExperimentalColumnsMenu as ColumnsMenu } from './ExperimentalColumnsMenu/ExperimentalColumnsMenu'; +import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi'; +import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog'; +import { MemoizedRowSelectCell } from '../ProjectFeatureToggles/RowSelectCell/RowSelectCell'; +import { BatchSelectionActionsBar } from 'component/common/BatchSelectionActionsBar/BatchSelectionActionsBar'; +import { ProjectFeaturesBatchActions } from '../ProjectFeatureToggles/ProjectFeaturesBatchActions/ProjectFeaturesBatchActions'; +import { MemoizedFeatureEnvironmentSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell'; +import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; +import { useFeatureToggleSwitch } from '../ProjectFeatureToggles/FeatureToggleSwitch/useFeatureToggleSwitch'; +import useLoading from 'hooks/useLoading'; +import { + DEFAULT_PAGE_LIMIT, + useFeatureSearch, +} from 'hooks/api/getters/useFeatureSearch/useFeatureSearch'; +import mapValues from 'lodash.mapvalues'; +import { usePersistentTableState } from 'hooks/usePersistentTableState'; +import { + BooleansStringParam, + FilterItemParam, +} from 'utils/serializeQueryParams'; +import { + NumberParam, + StringParam, + ArrayParam, + withDefault, + encodeQueryParams, +} from 'use-query-params'; +import { ProjectFeatureTogglesHeader } from './ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader'; +import { createColumnHelper, useReactTable } from '@tanstack/react-table'; +import { withTableState } from 'utils/withTableState'; +import type { FeatureSearchResponseSchema } from 'openapi'; +import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell'; +import { FeatureToggleCell } from './FeatureToggleCell/FeatureToggleCell'; +import { ProjectOverviewFilters } from './ProjectOverviewFilters'; +import { useDefaultColumnVisibility } from './hooks/useDefaultColumnVisibility'; +import { TableEmptyState } from './TableEmptyState/TableEmptyState'; +import { useRowActions } from './hooks/useRowActions'; +import { useUiFlag } from 'hooks/useUiFlag'; +import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell'; +import { useSelectedData } from './hooks/useSelectedData'; + +interface IPaginatedProjectFeatureTogglesProps { + environments: string[]; + refreshInterval?: number; + storageKey?: string; +} + +const formatEnvironmentColumnId = (environment: string) => + `environment:${environment}`; + +const columnHelper = createColumnHelper(); +const getRowId = (row: { name: string }) => row.name; + +export const OldProjectFeatureToggles = ({ + environments, + refreshInterval = 15 * 1000, + storageKey = 'project-feature-toggles-v2', +}: IPaginatedProjectFeatureTogglesProps) => { + const projectId = useRequiredPathParam('projectId'); + + const featuresExportImport = useUiFlag('featuresExportImport'); + + 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'), + columns: ArrayParam, + tag: FilterItemParam, + createdAt: FilterItemParam, + }; + const [tableState, setTableState] = usePersistentTableState( + `${storageKey}-${projectId}`, + stateConfig, + ); + + const filterState = { + tag: tableState.tag, + createdAt: tableState.createdAt, + }; + + const { features, total, refetch, loading, initialLoad } = useFeatureSearch( + mapValues( + { + ...encodeQueryParams(stateConfig, tableState), + project: `IS:${projectId}`, + }, + (value) => (value ? `${value}` : undefined), + ), + { + refreshInterval, + }, + ); + + const { favorite, unfavorite } = useFavoriteFeaturesApi(); + const onFavorite = useCallback( + async (feature: FeatureSearchResponseSchema) => { + if (feature?.favorite) { + await unfavorite(projectId, feature.name); + } else { + await favorite(projectId, feature.name); + } + refetch(); + }, + [projectId, refetch], + ); + const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); + const { onToggle: onFeatureToggle, modals: featureToggleModals } = + useFeatureToggleSwitch(projectId); + const { + rowActionsDialogs, + setFeatureArchiveState, + setFeatureStaleDialogState, + } = useRowActions(refetch, projectId); + const [showExportDialog, setShowExportDialog] = useState(false); + + const columns = useMemo( + () => [ + columnHelper.display({ + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ), + meta: { + width: '1%', + }, + enableHiding: false, + }), + columnHelper.accessor('favorite', { + id: 'favorite', + header: () => ( + + setTableState({ + favoritesFirst: !tableState.favoritesFirst, + }) + } + /> + ), + cell: ({ row: { original: feature } }) => ( + onFavorite(feature)} + /> + ), + enableSorting: false, + enableHiding: false, + meta: { + align: 'center', + width: '1%', + }, + }), + columnHelper.accessor('lastSeenAt', { + id: 'lastSeenAt', + header: 'Last seen', + cell: ({ row: { original } }) => ( + + ), + size: 50, + meta: { + align: 'center', + width: '1%', + }, + }), + columnHelper.accessor('type', { + id: 'type', + header: 'Type', + cell: FeatureTypeCell, + meta: { + align: 'center', + width: '1%', + }, + }), + columnHelper.accessor('name', { + id: 'name', + header: 'Name', + cell: FeatureNameCell, + enableHiding: false, + meta: { + width: '50%', + }, + }), + columnHelper.accessor('tags', { + id: 'tags', + header: 'Tags', + enableSorting: false, + cell: FeatureTagCell, + meta: { + width: '1%', + }, + }), + columnHelper.accessor('createdAt', { + id: 'createdAt', + header: 'Created', + cell: DateCell, + }), + ...environments.map((name: string) => { + const isChangeRequestEnabled = isChangeRequestConfigured(name); + + return columnHelper.accessor( + (row) => ({ + featureId: row.name, + environment: row.environments?.find( + (featureEnvironment) => + featureEnvironment.name === name, + ), + someEnabledEnvironmentHasVariants: + row.environments?.some( + (featureEnvironment) => + featureEnvironment.variantCount && + featureEnvironment.variantCount > 0 && + featureEnvironment.enabled, + ) || false, + }), + { + id: formatEnvironmentColumnId(name), + header: name, + meta: { + align: 'center', + width: 90, + }, + cell: ({ getValue }) => { + const { + featureId, + environment, + someEnabledEnvironmentHasVariants, + } = getValue(); + + return ( + + ); + }, + }, + ); + }), + columnHelper.display({ + id: 'actions', + header: '', + cell: ({ row }) => ( + + ), + enableSorting: false, + enableHiding: false, + meta: { + align: 'right', + width: '1%', + }, + }), + ], + [projectId, environments, tableState.favoritesFirst, refetch], + ); + + const placeholderData = useMemo( + () => + Array(tableState.limit) + .fill(null) + .map((_, index) => ({ + id: index, + type: '-', + name: `Feature name ${index}`, + createdAt: new Date().toISOString(), + dependencyType: null, + favorite: false, + impressionData: false, + project: 'project', + segments: [], + stale: false, + environments: [ + { + name: 'production', + enabled: false, + }, + { + name: 'production', + enabled: false, + }, + ], + })), + [tableState.limit], + ); + + const isPlaceholder = Boolean(initialLoad || (loading && total)); + const bodyLoadingRef = useLoading(isPlaceholder); + + const data = useMemo(() => { + if (isPlaceholder) { + return placeholderData; + } + return features; + }, [isPlaceholder, features]); + const allColumnIds = useMemo( + () => columns.map((column) => column.id).filter(Boolean) as string[], + [columns], + ); + + const defaultColumnVisibility = useDefaultColumnVisibility(allColumnIds); + + const table = useReactTable( + withTableState(tableState, setTableState, { + columns, + data, + enableRowSelection: true, + state: { + columnVisibility: defaultColumnVisibility, + }, + getRowId, + }), + ); + + const { columnVisibility, rowSelection } = table.getState(); + const onToggleColumnVisibility = useCallback( + (columnId) => { + const isVisible = columnVisibility[columnId]; + const newColumnVisibility: Record = { + ...columnVisibility, + [columnId]: !isVisible, + }; + setTableState({ + columns: Object.keys(newColumnVisibility).filter( + (columnId) => + newColumnVisibility[columnId] && + !columnId.includes(','), + ), + }); + }, + [columnVisibility, setTableState], + ); + + const selectedData = useSelectedData(features, rowSelection); + + return ( + <> + { + setTableState({ query }); + }} + dataToExport={data} + environmentsToExport={environments} + actions={ + ({ + header: environment, + id: formatEnvironmentColumnId( + environment, + ), + isVisible: + columnVisibility[ + formatEnvironmentColumnId( + environment, + ) + ], + })), + ]} + onToggle={onToggleColumnVisibility} + /> + } + /> + } + bodyClass='noop' + style={{ cursor: 'inherit' }} + > +
+ + + + + + } + /> + {rowActionsDialogs} + + setShowExportDialog(false)} + environments={environments} + /> + } + /> + {featureToggleModals} +
+
+ + + + + ); +}; diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx index d904332bb30..a39767dd295 100644 --- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureToggles.tsx @@ -3,7 +3,6 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit import { PageContent } from 'component/common/PageContent/PageContent'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; -import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell'; import { PaginatedTable } from 'component/common/Table'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader'; @@ -30,25 +29,24 @@ import { FilterItemParam, } from 'utils/serializeQueryParams'; import { + ArrayParam, + encodeQueryParams, NumberParam, StringParam, - ArrayParam, withDefault, - encodeQueryParams, } from 'use-query-params'; import { ProjectFeatureTogglesHeader } from './ProjectFeatureTogglesHeader/ProjectFeatureTogglesHeader'; import { createColumnHelper, useReactTable } from '@tanstack/react-table'; import { withTableState } from 'utils/withTableState'; import type { FeatureSearchResponseSchema } from 'openapi'; -import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/FeatureNameCell'; import { FeatureToggleCell } from './FeatureToggleCell/FeatureToggleCell'; import { ProjectOverviewFilters } from './ProjectOverviewFilters'; import { useDefaultColumnVisibility } from './hooks/useDefaultColumnVisibility'; import { TableEmptyState } from './TableEmptyState/TableEmptyState'; import { useRowActions } from './hooks/useRowActions'; import { useUiFlag } from 'hooks/useUiFlag'; -import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell'; import { useSelectedData } from './hooks/useSelectedData'; +import { FeatureOverviewCell } from '../../../common/Table/cells/FeatureOverviewCell/FeatureOverviewCell'; interface IPaginatedProjectFeatureTogglesProps { environments: string[]; @@ -133,7 +131,6 @@ export const ProjectFeatureToggles = ({ id: 'select', header: ({ table }) => ( ( { const isChangeRequestEnabled = isChangeRequestConfigured(name); @@ -395,32 +376,22 @@ export const ProjectFeatureToggles = ({ actions={ void; checked: boolean; title: string; - noPadding?: boolean; } const StyledBoxCell = styled(Box)(({ theme }) => ({ display: 'flex', justifyContent: 'center', + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(1), })); export const RowSelectCell: FC = ({ onChange, checked, title, - noPadding, }) => ( - ({ paddingLeft: noPadding ? 0 : theme.spacing(2) })} - > + - environment.environment, )} refreshInterval={refreshInterval}