mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-31 01:16:01 +02:00
parent
f00eac0881
commit
dbd897e3bd
@ -9,7 +9,7 @@ interface IBatchSelectionActionsBarProps {
|
|||||||
const StyledStickyContainer = styled('div')(({ theme }) => ({
|
const StyledStickyContainer = styled('div')(({ theme }) => ({
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
bottom: 50,
|
bottom: 50,
|
||||||
zIndex: theme.zIndex.mobileStepper,
|
zIndex: theme.zIndex.fab,
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import useProject, {
|
import useProject, {
|
||||||
useProjectNameOrId,
|
useProjectNameOrId,
|
||||||
} from 'hooks/api/getters/useProject/useProject';
|
} from 'hooks/api/getters/useProject/useProject';
|
||||||
@ -9,15 +9,15 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
|||||||
import { useLastViewedProject } from 'hooks/useLastViewedProject';
|
import { useLastViewedProject } from 'hooks/useLastViewedProject';
|
||||||
|
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
|
|
||||||
import {
|
import {
|
||||||
ISortingRules,
|
DEFAULT_PAGE_LIMIT,
|
||||||
|
useFeatureSearch,
|
||||||
|
} from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
|
||||||
|
import {
|
||||||
PaginatedProjectFeatureToggles,
|
PaginatedProjectFeatureToggles,
|
||||||
|
ProjectTableState,
|
||||||
} from '../ProjectFeatureToggles/PaginatedProjectFeatureToggles';
|
} from '../ProjectFeatureToggles/PaginatedProjectFeatureToggles';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useTableState } from 'hooks/useTableState';
|
||||||
|
|
||||||
import { PaginationBar } from 'component/common/PaginationBar/PaginationBar';
|
|
||||||
import { SortingRule } from 'react-table';
|
|
||||||
|
|
||||||
const refreshInterval = 15 * 1000;
|
const refreshInterval = 15 * 1000;
|
||||||
|
|
||||||
@ -40,26 +40,21 @@ const StyledContentContainer = styled(Box)(() => ({
|
|||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const DEFAULT_PAGE_LIMIT = 25;
|
|
||||||
|
|
||||||
const PaginatedProjectOverview = () => {
|
const PaginatedProjectOverview = () => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
|
||||||
const { project, loading: projectLoading } = useProject(projectId, {
|
const { project, loading: projectLoading } = useProject(projectId, {
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
});
|
});
|
||||||
const [pageLimit, setPageLimit] = useState(DEFAULT_PAGE_LIMIT);
|
|
||||||
const [currentOffset, setCurrentOffset] = useState(0);
|
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState(
|
const [tableState, setTableState] = useTableState<ProjectTableState>(
|
||||||
searchParams.get('search') || '',
|
{},
|
||||||
|
`project-features-${projectId}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [sortingRules, setSortingRules] = useState<ISortingRules>({
|
const page = parseInt(tableState.page || '1', 10);
|
||||||
sortBy: 'createdBy',
|
const pageSize = tableState?.pageSize
|
||||||
sortOrder: 'desc',
|
? parseInt(tableState.pageSize, 10) || DEFAULT_PAGE_LIMIT
|
||||||
isFavoritesPinned: false,
|
: DEFAULT_PAGE_LIMIT;
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
features: searchFeatures,
|
features: searchFeatures,
|
||||||
@ -68,28 +63,21 @@ const PaginatedProjectOverview = () => {
|
|||||||
loading,
|
loading,
|
||||||
initialLoad,
|
initialLoad,
|
||||||
} = useFeatureSearch(
|
} = useFeatureSearch(
|
||||||
currentOffset,
|
(page - 1) * pageSize,
|
||||||
pageLimit,
|
pageSize,
|
||||||
sortingRules,
|
{
|
||||||
|
sortBy: tableState.sortBy || 'createdAt',
|
||||||
|
sortOrder: tableState.sortOrder === 'desc' ? 'desc' : 'asc',
|
||||||
|
favoritesFirst: tableState.favorites === 'true',
|
||||||
|
},
|
||||||
projectId,
|
projectId,
|
||||||
searchValue,
|
tableState.search,
|
||||||
{
|
{
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { environments } = project;
|
const { environments } = project;
|
||||||
const fetchNextPage = () => {
|
|
||||||
if (!loading) {
|
|
||||||
setCurrentOffset(Math.min(total, currentOffset + pageLimit));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const fetchPrevPage = () => {
|
|
||||||
setCurrentOffset(Math.max(0, currentOffset - pageLimit));
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasPreviousPage = currentOffset > 0;
|
|
||||||
const hasNextPage = currentOffset + pageLimit < total;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
@ -97,35 +85,20 @@ const PaginatedProjectOverview = () => {
|
|||||||
<StyledProjectToggles>
|
<StyledProjectToggles>
|
||||||
<PaginatedProjectFeatureToggles
|
<PaginatedProjectFeatureToggles
|
||||||
key={
|
key={
|
||||||
loading && searchFeatures.length === 0
|
(loading || projectLoading) &&
|
||||||
|
searchFeatures.length === 0
|
||||||
? 'loading'
|
? 'loading'
|
||||||
: 'ready'
|
: 'ready'
|
||||||
}
|
}
|
||||||
features={searchFeatures}
|
|
||||||
style={{ width: '100%', margin: 0 }}
|
style={{ width: '100%', margin: 0 }}
|
||||||
|
features={searchFeatures || []}
|
||||||
environments={environments}
|
environments={environments}
|
||||||
initialLoad={initialLoad && searchFeatures.length === 0}
|
initialLoad={initialLoad && searchFeatures.length === 0}
|
||||||
loading={loading && searchFeatures.length === 0}
|
loading={loading && searchFeatures.length === 0}
|
||||||
onChange={refetch}
|
onChange={refetch}
|
||||||
total={total}
|
total={total}
|
||||||
searchValue={searchValue}
|
tableState={tableState}
|
||||||
setSearchValue={setSearchValue}
|
setTableState={setTableState}
|
||||||
sortingRules={sortingRules}
|
|
||||||
setSortingRules={setSortingRules}
|
|
||||||
paginationBar={
|
|
||||||
<StickyPaginationBar>
|
|
||||||
<PaginationBar
|
|
||||||
total={total}
|
|
||||||
hasNextPage={hasNextPage}
|
|
||||||
hasPreviousPage={hasPreviousPage}
|
|
||||||
fetchNextPage={fetchNextPage}
|
|
||||||
fetchPrevPage={fetchPrevPage}
|
|
||||||
currentOffset={currentOffset}
|
|
||||||
pageLimit={pageLimit}
|
|
||||||
setPageLimit={setPageLimit}
|
|
||||||
/>
|
|
||||||
</StickyPaginationBar>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</StyledProjectToggles>
|
</StyledProjectToggles>
|
||||||
</StyledContentContainer>
|
</StyledContentContainer>
|
||||||
@ -133,36 +106,6 @@ const PaginatedProjectOverview = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledStickyBar = styled('div')(({ theme }) => ({
|
|
||||||
position: 'sticky',
|
|
||||||
bottom: 0,
|
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
zIndex: 9999,
|
|
||||||
borderBottomLeftRadius: theme.shape.borderRadiusMedium,
|
|
||||||
borderBottomRightRadius: theme.shape.borderRadiusMedium,
|
|
||||||
borderTop: `1px solid ${theme.palette.divider}`,
|
|
||||||
boxShadow: `0px -2px 8px 0px rgba(32, 32, 33, 0.06)`,
|
|
||||||
height: '52px',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledStickyBarContentContainer = styled(Box)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
width: '100%',
|
|
||||||
minWidth: 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StickyPaginationBar: React.FC = ({ children }) => {
|
|
||||||
return (
|
|
||||||
<StyledStickyBar>
|
|
||||||
<StyledStickyBarContentContainer>
|
|
||||||
{children}
|
|
||||||
</StyledStickyBarContentContainer>
|
|
||||||
</StyledStickyBar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated remove when flag `featureSearchFrontend` is removed
|
* @deprecated remove when flag `featureSearchFrontend` is removed
|
||||||
*/
|
*/
|
||||||
|
@ -34,11 +34,8 @@ interface IColumnsMenuProps {
|
|||||||
dividerBefore?: string[];
|
dividerBefore?: string[];
|
||||||
dividerAfter?: string[];
|
dividerAfter?: string[];
|
||||||
isCustomized?: boolean;
|
isCustomized?: boolean;
|
||||||
setHiddenColumns: (
|
setHiddenColumns: (hiddenColumns: string[]) => void;
|
||||||
hiddenColumns:
|
onCustomize?: () => void;
|
||||||
| string[]
|
|
||||||
| ((previousHiddenColumns: string[]) => string[]),
|
|
||||||
) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnNameMap: Record<string, string> = {
|
const columnNameMap: Record<string, string> = {
|
||||||
@ -51,6 +48,7 @@ export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
|
|||||||
dividerBefore = [],
|
dividerBefore = [],
|
||||||
dividerAfter = [],
|
dividerAfter = [],
|
||||||
isCustomized = false,
|
isCustomized = false,
|
||||||
|
onCustomize,
|
||||||
setHiddenColumns,
|
setHiddenColumns,
|
||||||
}) => {
|
}) => {
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
@ -69,7 +67,7 @@ export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
|
|||||||
environmentsToShow: number = 0,
|
environmentsToShow: number = 0,
|
||||||
) => {
|
) => {
|
||||||
const visibleEnvColumns = allColumns
|
const visibleEnvColumns = allColumns
|
||||||
.filter(({ id }) => id.startsWith('environments.') !== false)
|
.filter(({ id }) => id.startsWith('environment:') !== false)
|
||||||
.map(({ id }) => id)
|
.map(({ id }) => id)
|
||||||
.slice(0, environmentsToShow);
|
.slice(0, environmentsToShow);
|
||||||
const hiddenColumns = allColumns
|
const hiddenColumns = allColumns
|
||||||
@ -160,9 +158,10 @@ export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
|
|||||||
show={<StyledDivider />}
|
show={<StyledDivider />}
|
||||||
/>,
|
/>,
|
||||||
<StyledMenuItem
|
<StyledMenuItem
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
column.toggleHidden(column.isVisible)
|
column.toggleHidden(column.isVisible);
|
||||||
}
|
onCustomize?.();
|
||||||
|
}}
|
||||||
disabled={staticColumns.includes(column.id)}
|
disabled={staticColumns.includes(column.id)}
|
||||||
>
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, {
|
||||||
|
type CSSProperties,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import {
|
import {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
IconButton,
|
IconButton,
|
||||||
@ -9,10 +15,10 @@ import {
|
|||||||
useTheme,
|
useTheme,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Add } from '@mui/icons-material';
|
import { Add } from '@mui/icons-material';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
SortingRule,
|
|
||||||
useFlexLayout,
|
useFlexLayout,
|
||||||
|
usePagination,
|
||||||
useRowSelect,
|
useRowSelect,
|
||||||
useSortBy,
|
useSortBy,
|
||||||
useTable,
|
useTable,
|
||||||
@ -32,7 +38,6 @@ import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/Fe
|
|||||||
import { IProject } from 'interfaces/project';
|
import { IProject } from 'interfaces/project';
|
||||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
|
||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
|
||||||
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
|
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
|
||||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||||
import { getColumnValues, includesFilter, useSearch } from 'hooks/useSearch';
|
import { getColumnValues, includesFilter, useSearch } from 'hooks/useSearch';
|
||||||
@ -40,17 +45,12 @@ import { Search } from 'component/common/Search/Search';
|
|||||||
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
||||||
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
||||||
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
||||||
import {
|
import { ProjectEnvironmentType } from './hooks/useEnvironmentsRef';
|
||||||
ProjectEnvironmentType,
|
|
||||||
useEnvironmentsRef,
|
|
||||||
} from './hooks/useEnvironmentsRef';
|
|
||||||
import { ActionsCell } from './ActionsCell/ActionsCell';
|
import { ActionsCell } from './ActionsCell/ActionsCell';
|
||||||
import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
|
import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
|
||||||
import { useStyles } from './ProjectFeatureToggles.styles';
|
import { useStyles } from './ProjectFeatureToggles.styles';
|
||||||
import { usePinnedFavorites } from 'hooks/usePinnedFavorites';
|
|
||||||
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
||||||
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
||||||
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
|
|
||||||
import FileDownload from '@mui/icons-material/FileDownload';
|
import FileDownload from '@mui/icons-material/FileDownload';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
||||||
@ -63,17 +63,22 @@ import { ListItemType } from './ProjectFeatureToggles.types';
|
|||||||
import { createFeatureToggleCell } from './FeatureToggleSwitch/createFeatureToggleCell';
|
import { createFeatureToggleCell } from './FeatureToggleSwitch/createFeatureToggleCell';
|
||||||
import { useFeatureToggleSwitch } from './FeatureToggleSwitch/useFeatureToggleSwitch';
|
import { useFeatureToggleSwitch } from './FeatureToggleSwitch/useFeatureToggleSwitch';
|
||||||
import useLoading from 'hooks/useLoading';
|
import useLoading from 'hooks/useLoading';
|
||||||
import { DEFAULT_PAGE_LIMIT } from '../ProjectOverview';
|
import { StickyPaginationBar } from '../StickyPaginationBar/StickyPaginationBar';
|
||||||
|
import { DEFAULT_PAGE_LIMIT } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
|
||||||
|
|
||||||
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export interface ISortingRules {
|
export type ProjectTableState = {
|
||||||
sortBy: string;
|
page?: string;
|
||||||
sortOrder: 'asc' | 'desc';
|
sortBy?: string;
|
||||||
isFavoritesPinned: boolean;
|
pageSize?: string;
|
||||||
}
|
sortOrder?: 'asc' | 'desc';
|
||||||
|
favorites?: 'true' | 'false';
|
||||||
|
columns?: string;
|
||||||
|
search?: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface IPaginatedProjectFeatureTogglesProps {
|
interface IPaginatedProjectFeatureTogglesProps {
|
||||||
features: IProject['features'];
|
features: IProject['features'];
|
||||||
@ -82,33 +87,23 @@ interface IPaginatedProjectFeatureTogglesProps {
|
|||||||
onChange: () => void;
|
onChange: () => void;
|
||||||
total?: number;
|
total?: number;
|
||||||
initialLoad: boolean;
|
initialLoad: boolean;
|
||||||
searchValue: string;
|
tableState: ProjectTableState;
|
||||||
setSearchValue: React.Dispatch<React.SetStateAction<string>>;
|
setTableState: (state: Partial<ProjectTableState>, quiet?: boolean) => void;
|
||||||
paginationBar: JSX.Element;
|
style?: CSSProperties;
|
||||||
sortingRules: ISortingRules;
|
|
||||||
setSortingRules: (sortingRules: ISortingRules) => void;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const staticColumns = ['Select', 'Actions', 'name', 'favorite'];
|
const staticColumns = ['Select', 'Actions', 'name', 'favorite'];
|
||||||
|
|
||||||
const defaultSort: SortingRule<string> & {
|
|
||||||
columns?: string[];
|
|
||||||
} = { id: 'createdAt', desc: true };
|
|
||||||
|
|
||||||
export const PaginatedProjectFeatureToggles = ({
|
export const PaginatedProjectFeatureToggles = ({
|
||||||
features,
|
features,
|
||||||
loading,
|
loading,
|
||||||
initialLoad,
|
initialLoad,
|
||||||
environments: newEnvironments = [],
|
environments,
|
||||||
onChange,
|
onChange,
|
||||||
total,
|
total,
|
||||||
searchValue,
|
tableState,
|
||||||
setSearchValue,
|
setTableState,
|
||||||
paginationBar,
|
style,
|
||||||
sortingRules,
|
|
||||||
setSortingRules,
|
|
||||||
style = {},
|
|
||||||
}: IPaginatedProjectFeatureTogglesProps) => {
|
}: IPaginatedProjectFeatureTogglesProps) => {
|
||||||
const { classes: styles } = useStyles();
|
const { classes: styles } = useStyles();
|
||||||
const bodyLoadingRef = useLoading(loading);
|
const bodyLoadingRef = useLoading(loading);
|
||||||
@ -122,30 +117,14 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
const [featureArchiveState, setFeatureArchiveState] = useState<
|
const [featureArchiveState, setFeatureArchiveState] = useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
|
const [isCustomColumns, setIsCustomColumns] = useState(
|
||||||
|
Boolean(tableState.columns),
|
||||||
|
);
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const { onToggle: onFeatureToggle, modals: featureToggleModals } =
|
const { onToggle: onFeatureToggle, modals: featureToggleModals } =
|
||||||
useFeatureToggleSwitch(projectId);
|
useFeatureToggleSwitch(projectId);
|
||||||
|
|
||||||
const { value: storedParams, setValue: setStoredParams } =
|
|
||||||
createLocalStorage(
|
|
||||||
`${projectId}:FeatureToggleListTable:v1`,
|
|
||||||
defaultSort,
|
|
||||||
);
|
|
||||||
const { value: globalStore, setValue: setGlobalStore } =
|
|
||||||
useGlobalLocalStorage();
|
|
||||||
const navigate = useNavigate();
|
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 { favorite, unfavorite } = useFavoriteFeaturesApi();
|
||||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||||
const [showExportDialog, setShowExportDialog] = useState(false);
|
const [showExportDialog, setShowExportDialog] = useState(false);
|
||||||
@ -191,8 +170,15 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
id: 'favorite',
|
id: 'favorite',
|
||||||
Header: (
|
Header: (
|
||||||
<FavoriteIconHeader
|
<FavoriteIconHeader
|
||||||
isActive={isFavoritesPinned}
|
isActive={tableState.favorites === 'true'}
|
||||||
onClick={onChangeIsFavoritePinned}
|
onClick={() =>
|
||||||
|
setTableState({
|
||||||
|
favorites:
|
||||||
|
tableState.favorites === 'true'
|
||||||
|
? undefined
|
||||||
|
: 'true',
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
accessor: 'favorite',
|
accessor: 'favorite',
|
||||||
@ -280,35 +266,40 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
Cell: DateCell,
|
Cell: DateCell,
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
},
|
},
|
||||||
...environments.map((value: ProjectEnvironmentType | string) => {
|
...environments.map(
|
||||||
const name =
|
(projectEnvironment: ProjectEnvironmentType | string) => {
|
||||||
typeof value === 'string'
|
const name =
|
||||||
? value
|
typeof projectEnvironment === 'string'
|
||||||
: (value as ProjectEnvironmentType).environment;
|
? projectEnvironment
|
||||||
const isChangeRequestEnabled = isChangeRequestConfigured(name);
|
: (projectEnvironment as ProjectEnvironmentType)
|
||||||
const FeatureToggleCell = createFeatureToggleCell(
|
.environment;
|
||||||
projectId,
|
const isChangeRequestEnabled =
|
||||||
name,
|
isChangeRequestConfigured(name);
|
||||||
isChangeRequestEnabled,
|
const FeatureToggleCell = createFeatureToggleCell(
|
||||||
onChange,
|
projectId,
|
||||||
onFeatureToggle,
|
name,
|
||||||
);
|
isChangeRequestEnabled,
|
||||||
|
onChange,
|
||||||
return {
|
onFeatureToggle,
|
||||||
Header: loading ? () => '' : name,
|
);
|
||||||
maxWidth: 90,
|
|
||||||
id: `environment:${name}`,
|
|
||||||
accessor: (row: ListItemType) =>
|
|
||||||
row.environments[name]?.enabled,
|
|
||||||
align: 'center',
|
|
||||||
Cell: FeatureToggleCell,
|
|
||||||
sortType: 'boolean',
|
|
||||||
filterName: name,
|
|
||||||
filterParsing: (value: boolean) =>
|
|
||||||
value ? 'enabled' : 'disabled',
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
Header: loading ? () => '' : name,
|
||||||
|
maxWidth: 90,
|
||||||
|
id: `environment:${name}`,
|
||||||
|
accessor: (row: ListItemType) => {
|
||||||
|
return row.environments?.[name]?.enabled;
|
||||||
|
},
|
||||||
|
align: 'center',
|
||||||
|
Cell: FeatureToggleCell,
|
||||||
|
sortType: 'boolean',
|
||||||
|
sortDescFirst: true,
|
||||||
|
filterName: name,
|
||||||
|
filterParsing: (value: boolean) =>
|
||||||
|
value ? 'enabled' : 'disabled',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
{
|
{
|
||||||
id: 'Actions',
|
id: 'Actions',
|
||||||
maxWidth: 56,
|
maxWidth: 56,
|
||||||
@ -332,7 +323,7 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[projectId, environments, loading],
|
[projectId, environments, loading, tableState.favorites, onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [showTitle, setShowTitle] = useState(true);
|
const [showTitle, setShowTitle] = useState(true);
|
||||||
@ -345,10 +336,10 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
environments.map((env) => {
|
environments.map((env) => {
|
||||||
const thisEnv = feature?.environments.find(
|
const thisEnv = feature?.environments.find(
|
||||||
(featureEnvironment) =>
|
(featureEnvironment) =>
|
||||||
featureEnvironment?.name === env,
|
featureEnvironment?.name === env.environment,
|
||||||
);
|
);
|
||||||
return [
|
return [
|
||||||
env,
|
typeof env === 'string' ? env : env.environment,
|
||||||
{
|
{
|
||||||
name: env,
|
name: env,
|
||||||
enabled: thisEnv?.enabled || false,
|
enabled: thisEnv?.enabled || false,
|
||||||
@ -374,89 +365,92 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
|
|
||||||
const { getSearchText, getSearchContext } = useSearch(
|
const { getSearchText, getSearchContext } = useSearch(
|
||||||
columns,
|
columns,
|
||||||
searchValue,
|
tableState.search || '',
|
||||||
featuresData,
|
featuresData,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const initialPageSize = tableState.pageSize
|
||||||
|
? parseInt(tableState.pageSize, 10) || DEFAULT_PAGE_LIMIT
|
||||||
|
: DEFAULT_PAGE_LIMIT;
|
||||||
|
|
||||||
|
const allColumnIds = columns
|
||||||
|
.map(
|
||||||
|
(column: any) =>
|
||||||
|
(column?.id as string) ||
|
||||||
|
(typeof column?.accessor === 'string'
|
||||||
|
? (column?.accessor as string)
|
||||||
|
: ''),
|
||||||
|
)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const initialState = useMemo(
|
||||||
|
() => ({
|
||||||
|
sortBy: [
|
||||||
|
{
|
||||||
|
id: tableState.sortBy || 'createdAt',
|
||||||
|
desc: tableState.sortOrder === 'desc',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
...(tableState.columns
|
||||||
|
? {
|
||||||
|
hiddenColumns: allColumnIds.filter(
|
||||||
|
(id) =>
|
||||||
|
!(tableState.columns?.split(',') || [])?.includes(
|
||||||
|
id,
|
||||||
|
) && !staticColumns.includes(id),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
pageSize: initialPageSize,
|
||||||
|
pageIndex: tableState.page ? parseInt(tableState.page, 10) - 1 : 0,
|
||||||
|
selectedRowIds: {},
|
||||||
|
}),
|
||||||
|
[initialLoad],
|
||||||
|
);
|
||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
if (initialLoad || loading) {
|
if (initialLoad || loading) {
|
||||||
const loadingData = Array(15)
|
const loadingData = Array(
|
||||||
|
parseInt(tableState.pageSize || `${initialPageSize}`, 10),
|
||||||
|
)
|
||||||
.fill(null)
|
.fill(null)
|
||||||
.map((_, index) => ({
|
.map((_, index) => ({
|
||||||
id: index, // Assuming `id` is a required property
|
id: index, // Assuming `id` is a required property
|
||||||
type: '-',
|
type: '-',
|
||||||
name: `Feature name ${index}`,
|
name: `Feature name ${index}`,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
environments: {
|
environments: [
|
||||||
production: {
|
{
|
||||||
name: 'production',
|
name: 'production',
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
}));
|
}));
|
||||||
// Coerce loading data to FeatureSchema[]
|
// Coerce loading data to FeatureSchema[]
|
||||||
return loadingData as unknown as FeatureSchema[];
|
return loadingData as unknown as typeof featuresData;
|
||||||
}
|
}
|
||||||
return featuresData;
|
return featuresData;
|
||||||
}, [loading, featuresData]);
|
}, [loading, featuresData]);
|
||||||
|
|
||||||
const initialState = useMemo(
|
const pageCount = useMemo(
|
||||||
() => {
|
() =>
|
||||||
const allColumnIds = columns
|
tableState.pageSize
|
||||||
.map(
|
? Math.ceil((total || 0) / parseInt(tableState.pageSize))
|
||||||
(column: any) =>
|
: 0,
|
||||||
(column?.id as string) ||
|
[total, tableState.pageSize],
|
||||||
(typeof column?.accessor === 'string'
|
|
||||||
? (column?.accessor as string)
|
|
||||||
: ''),
|
|
||||||
)
|
|
||||||
.filter(Boolean);
|
|
||||||
let hiddenColumns = environments
|
|
||||||
.filter((_, index) => index >= 3)
|
|
||||||
.map((environment) => `environments.${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 ||
|
|
||||||
sortingRules.sortBy,
|
|
||||||
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 getRowId = useCallback((row: any) => row.name, []);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
allColumns,
|
allColumns,
|
||||||
headerGroups,
|
headerGroups,
|
||||||
rows,
|
rows,
|
||||||
state: { selectedRowIds, sortBy, hiddenColumns },
|
state: { pageIndex, pageSize, hiddenColumns, selectedRowIds, sortBy },
|
||||||
|
canNextPage,
|
||||||
|
canPreviousPage,
|
||||||
|
previousPage,
|
||||||
|
nextPage,
|
||||||
|
setPageSize,
|
||||||
prepareRow,
|
prepareRow,
|
||||||
setHiddenColumns,
|
setHiddenColumns,
|
||||||
toggleAllRowsSelected,
|
toggleAllRowsSelected,
|
||||||
@ -465,74 +459,52 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
columns: columns as any[], // TODO: fix after `react-table` v8 update
|
columns: columns as any[], // TODO: fix after `react-table` v8 update
|
||||||
data,
|
data,
|
||||||
initialState,
|
initialState,
|
||||||
sortTypes,
|
|
||||||
autoResetHiddenColumns: false,
|
autoResetHiddenColumns: false,
|
||||||
autoResetSelectedRows: false,
|
autoResetSelectedRows: false,
|
||||||
disableSortRemove: true,
|
disableSortRemove: true,
|
||||||
autoResetSortBy: false,
|
autoResetSortBy: false,
|
||||||
|
manualSortBy: true,
|
||||||
|
manualPagination: true,
|
||||||
|
pageCount,
|
||||||
getRowId,
|
getRowId,
|
||||||
},
|
},
|
||||||
useFlexLayout,
|
useFlexLayout,
|
||||||
useSortBy,
|
useSortBy,
|
||||||
|
usePagination,
|
||||||
useRowSelect,
|
useRowSelect,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Refetching - https://github.com/TanStack/table/blob/v7/docs/src/pages/docs/faq.md#how-can-i-use-the-table-state-to-fetch-new-data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loading) {
|
setTableState({
|
||||||
return;
|
page: `${pageIndex + 1}`,
|
||||||
}
|
pageSize: `${pageSize}`,
|
||||||
const sortedByColumn = sortBy[0].id;
|
sortBy: sortBy[0]?.id || 'createdAt',
|
||||||
const sortOrder = sortBy[0].desc ? 'desc' : 'asc';
|
sortOrder: sortBy[0]?.desc ? 'desc' : 'asc',
|
||||||
const tableState: Record<string, string> = {};
|
|
||||||
tableState.sort = sortedByColumn;
|
|
||||||
if (sortBy[0].desc) {
|
|
||||||
tableState.order = sortOrder;
|
|
||||||
}
|
|
||||||
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(','),
|
|
||||||
}));
|
|
||||||
const favoritesPinned = Boolean(isFavoritesPinned);
|
|
||||||
setGlobalStore((params) => ({
|
|
||||||
...params,
|
|
||||||
favorites: favoritesPinned,
|
|
||||||
}));
|
|
||||||
setSortingRules({
|
|
||||||
sortBy: sortedByColumn,
|
|
||||||
sortOrder,
|
|
||||||
isFavoritesPinned: favoritesPinned,
|
|
||||||
});
|
});
|
||||||
|
}, [pageIndex, pageSize, sortBy]);
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
useEffect(() => {
|
||||||
}, [
|
if (!loading && isCustomColumns) {
|
||||||
loading,
|
setTableState(
|
||||||
sortBy,
|
{
|
||||||
hiddenColumns,
|
columns:
|
||||||
searchValue,
|
hiddenColumns !== undefined
|
||||||
setSearchParams,
|
? allColumnIds
|
||||||
isFavoritesPinned,
|
.filter(
|
||||||
]);
|
(id) =>
|
||||||
|
!hiddenColumns.includes(id) &&
|
||||||
|
!staticColumns.includes(id),
|
||||||
|
)
|
||||||
|
.join(',')
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
true, // Columns state is controllable by react-table - update only URL and storage, not state
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [loading, isCustomColumns, hiddenColumns]);
|
||||||
|
|
||||||
const showPaginationBar = Boolean(total && total > DEFAULT_PAGE_LIMIT);
|
const showPaginationBar = Boolean(total && total > pageSize);
|
||||||
const paginatedStyles = showPaginationBar
|
const paginatedStyles = showPaginationBar
|
||||||
? {
|
? {
|
||||||
borderBottomLeftRadius: 0,
|
borderBottomLeftRadius: 0,
|
||||||
@ -546,7 +518,7 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
disableLoading
|
disableLoading
|
||||||
disablePadding
|
disablePadding
|
||||||
className={styles.container}
|
className={styles.container}
|
||||||
style={{ ...style, ...paginatedStyles }}
|
style={{ ...paginatedStyles, ...style }}
|
||||||
header={
|
header={
|
||||||
<Box
|
<Box
|
||||||
ref={headerLoadingRef}
|
ref={headerLoadingRef}
|
||||||
@ -575,8 +547,12 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
data-loading
|
data-loading
|
||||||
placeholder='Search and Filter'
|
placeholder='Search and Filter'
|
||||||
expandable
|
expandable
|
||||||
initialValue={searchValue}
|
initialValue={tableState.search}
|
||||||
onChange={setSearchValue}
|
onChange={(value) => {
|
||||||
|
setTableState({
|
||||||
|
search: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
onFocus={() =>
|
onFocus={() =>
|
||||||
setShowTitle(false)
|
setShowTitle(false)
|
||||||
}
|
}
|
||||||
@ -596,10 +572,11 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
staticColumns={staticColumns}
|
staticColumns={staticColumns}
|
||||||
dividerAfter={['createdAt']}
|
dividerAfter={['createdAt']}
|
||||||
dividerBefore={['Actions']}
|
dividerBefore={['Actions']}
|
||||||
isCustomized={Boolean(
|
isCustomized={isCustomColumns}
|
||||||
storedParams.columns,
|
|
||||||
)}
|
|
||||||
setHiddenColumns={setHiddenColumns}
|
setHiddenColumns={setHiddenColumns}
|
||||||
|
onCustomize={() =>
|
||||||
|
setIsCustomColumns(true)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<PageHeader.Divider
|
<PageHeader.Divider
|
||||||
sx={{ marginLeft: 0 }}
|
sx={{ marginLeft: 0 }}
|
||||||
@ -652,8 +629,12 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
condition={isSmallScreen}
|
condition={isSmallScreen}
|
||||||
show={
|
show={
|
||||||
<Search
|
<Search
|
||||||
initialValue={searchValue}
|
initialValue={tableState.search}
|
||||||
onChange={setSearchValue}
|
onChange={(value) => {
|
||||||
|
setTableState({
|
||||||
|
search: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
hasFilters
|
hasFilters
|
||||||
getSearchContext={getSearchContext}
|
getSearchContext={getSearchContext}
|
||||||
id='projectFeatureToggles'
|
id='projectFeatureToggles'
|
||||||
@ -669,7 +650,9 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
aria-busy={loading}
|
aria-busy={loading}
|
||||||
aria-live='polite'
|
aria-live='polite'
|
||||||
>
|
>
|
||||||
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
<SearchHighlightProvider
|
||||||
|
value={getSearchText(tableState.search || '')}
|
||||||
|
>
|
||||||
<VirtualizedTable
|
<VirtualizedTable
|
||||||
rows={rows}
|
rows={rows}
|
||||||
headerGroups={headerGroups}
|
headerGroups={headerGroups}
|
||||||
@ -677,31 +660,35 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
/>
|
/>
|
||||||
</SearchHighlightProvider>
|
</SearchHighlightProvider>
|
||||||
|
|
||||||
<Box sx={{ padding: theme.spacing(3) }}>
|
<ConditionallyRender
|
||||||
<ConditionallyRender
|
condition={rows.length === 0}
|
||||||
condition={rows.length === 0}
|
show={
|
||||||
show={
|
<ConditionallyRender
|
||||||
<ConditionallyRender
|
condition={
|
||||||
condition={searchValue?.length > 0}
|
(tableState.search || '')?.length > 0
|
||||||
show={
|
}
|
||||||
|
show={
|
||||||
|
<Box sx={{ padding: theme.spacing(3) }}>
|
||||||
<TablePlaceholder>
|
<TablePlaceholder>
|
||||||
No feature toggles found matching
|
No feature toggles found matching
|
||||||
“
|
“
|
||||||
{searchValue}
|
{tableState.search}
|
||||||
”
|
”
|
||||||
</TablePlaceholder>
|
</TablePlaceholder>
|
||||||
}
|
</Box>
|
||||||
elseShow={
|
}
|
||||||
|
elseShow={
|
||||||
|
<Box sx={{ padding: theme.spacing(3) }}>
|
||||||
<TablePlaceholder>
|
<TablePlaceholder>
|
||||||
No feature toggles available. Get
|
No feature toggles available. Get
|
||||||
started by adding a new feature
|
started by adding a new feature
|
||||||
toggle.
|
toggle.
|
||||||
</TablePlaceholder>
|
</TablePlaceholder>
|
||||||
}
|
</Box>
|
||||||
/>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
}
|
||||||
</Box>
|
/>
|
||||||
<FeatureStaleDialog
|
<FeatureStaleDialog
|
||||||
isStale={featureStaleDialogState.stale === true}
|
isStale={featureStaleDialogState.stale === true}
|
||||||
isOpen={Boolean(featureStaleDialogState.featureId)}
|
isOpen={Boolean(featureStaleDialogState.featureId)}
|
||||||
@ -731,7 +718,9 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
showExportDialog={showExportDialog}
|
showExportDialog={showExportDialog}
|
||||||
data={data}
|
data={data}
|
||||||
onClose={() => setShowExportDialog(false)}
|
onClose={() => setShowExportDialog(false)}
|
||||||
environments={environments}
|
environments={environments.map(
|
||||||
|
({ environment }) => environment,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -740,7 +729,18 @@ export const PaginatedProjectFeatureToggles = ({
|
|||||||
</PageContent>
|
</PageContent>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={showPaginationBar}
|
condition={showPaginationBar}
|
||||||
show={paginationBar}
|
show={
|
||||||
|
<StickyPaginationBar
|
||||||
|
total={total || 0}
|
||||||
|
hasNextPage={canNextPage}
|
||||||
|
hasPreviousPage={canPreviousPage}
|
||||||
|
fetchNextPage={nextPage}
|
||||||
|
fetchPrevPage={previousPage}
|
||||||
|
currentOffset={pageIndex * pageSize}
|
||||||
|
pageLimit={pageSize}
|
||||||
|
setPageLimit={setPageSize}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<BatchSelectionActionsBar
|
<BatchSelectionActionsBar
|
||||||
count={Object.keys(selectedRowIds).length}
|
count={Object.keys(selectedRowIds).length}
|
||||||
|
@ -387,7 +387,7 @@ export const ProjectFeatureToggles = ({
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
let hiddenColumns = environments
|
let hiddenColumns = environments
|
||||||
.filter((_, index) => index >= 3)
|
.filter((_, index) => index >= 3)
|
||||||
.map((environment) => `environments.${environment}`);
|
.map((environment) => `environment:${environment}`);
|
||||||
|
|
||||||
if (searchParams.has('columns')) {
|
if (searchParams.has('columns')) {
|
||||||
const columnsInParams =
|
const columnsInParams =
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { FC, useEffect } from 'react';
|
||||||
import useProject, {
|
import useProject, {
|
||||||
useProjectNameOrId,
|
useProjectNameOrId,
|
||||||
} from 'hooks/api/getters/useProject/useProject';
|
} from 'hooks/api/getters/useProject/useProject';
|
||||||
@ -9,17 +9,17 @@ import { usePageTitle } from 'hooks/usePageTitle';
|
|||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { useLastViewedProject } from 'hooks/useLastViewedProject';
|
import { useLastViewedProject } from 'hooks/useLastViewedProject';
|
||||||
import { ProjectStats } from './ProjectStats/ProjectStats';
|
import { ProjectStats } from './ProjectStats/ProjectStats';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import { useFeatureSearch } from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
|
|
||||||
import {
|
import {
|
||||||
ISortingRules,
|
DEFAULT_PAGE_LIMIT,
|
||||||
|
useFeatureSearch,
|
||||||
|
} from 'hooks/api/getters/useFeatureSearch/useFeatureSearch';
|
||||||
|
import {
|
||||||
|
ProjectTableState,
|
||||||
PaginatedProjectFeatureToggles,
|
PaginatedProjectFeatureToggles,
|
||||||
} from './ProjectFeatureToggles/PaginatedProjectFeatureToggles';
|
} from './ProjectFeatureToggles/PaginatedProjectFeatureToggles';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { PaginationBar } from 'component/common/PaginationBar/PaginationBar';
|
import { useTableState } from 'hooks/useTableState';
|
||||||
import { SortingRule } from 'react-table';
|
|
||||||
|
|
||||||
const refreshInterval = 15 * 1000;
|
const refreshInterval = 15 * 1000;
|
||||||
|
|
||||||
@ -42,26 +42,24 @@ const StyledContentContainer = styled(Box)(() => ({
|
|||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const DEFAULT_PAGE_LIMIT = 25;
|
const PaginatedProjectOverview: FC<{
|
||||||
|
fullWidth?: boolean;
|
||||||
const PaginatedProjectOverview = () => {
|
storageKey?: string;
|
||||||
|
}> = ({ fullWidth, storageKey = 'project-overview' }) => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
|
||||||
const { project, loading: projectLoading } = useProject(projectId, {
|
const { project, loading: projectLoading } = useProject(projectId, {
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
});
|
});
|
||||||
const [pageLimit, setPageLimit] = useState(DEFAULT_PAGE_LIMIT);
|
|
||||||
const [currentOffset, setCurrentOffset] = useState(0);
|
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState(
|
const [tableState, setTableState] = useTableState<ProjectTableState>(
|
||||||
searchParams.get('search') || '',
|
{},
|
||||||
|
`${storageKey}-${projectId}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [sortingRules, setSortingRules] = useState<ISortingRules>({
|
const page = parseInt(tableState.page || '1', 10);
|
||||||
sortBy: 'createdBy',
|
const pageSize = tableState?.pageSize
|
||||||
sortOrder: 'desc',
|
? parseInt(tableState.pageSize, 10) || DEFAULT_PAGE_LIMIT
|
||||||
isFavoritesPinned: false,
|
: DEFAULT_PAGE_LIMIT;
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
features: searchFeatures,
|
features: searchFeatures,
|
||||||
@ -70,11 +68,15 @@ const PaginatedProjectOverview = () => {
|
|||||||
loading,
|
loading,
|
||||||
initialLoad,
|
initialLoad,
|
||||||
} = useFeatureSearch(
|
} = useFeatureSearch(
|
||||||
currentOffset,
|
(page - 1) * pageSize,
|
||||||
pageLimit,
|
pageSize,
|
||||||
sortingRules,
|
{
|
||||||
|
sortBy: tableState.sortBy || 'createdAt',
|
||||||
|
sortOrder: tableState.sortOrder === 'desc' ? 'desc' : 'asc',
|
||||||
|
favoritesFirst: tableState.favorites === 'true',
|
||||||
|
},
|
||||||
projectId,
|
projectId,
|
||||||
searchValue,
|
tableState.search,
|
||||||
{
|
{
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
},
|
},
|
||||||
@ -82,20 +84,9 @@ const PaginatedProjectOverview = () => {
|
|||||||
|
|
||||||
const { members, features, health, description, environments, stats } =
|
const { members, features, health, description, environments, stats } =
|
||||||
project;
|
project;
|
||||||
const fetchNextPage = () => {
|
|
||||||
if (!loading) {
|
|
||||||
setCurrentOffset(Math.min(total, currentOffset + pageLimit));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const fetchPrevPage = () => {
|
|
||||||
setCurrentOffset(Math.max(0, currentOffset - pageLimit));
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasPreviousPage = currentOffset > 0;
|
|
||||||
const hasNextPage = currentOffset + pageLimit < total;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer key={projectId}>
|
||||||
<ProjectInfo
|
<ProjectInfo
|
||||||
id={projectId}
|
id={projectId}
|
||||||
description={description}
|
description={description}
|
||||||
@ -108,35 +99,17 @@ const PaginatedProjectOverview = () => {
|
|||||||
<ProjectStats stats={project.stats} />
|
<ProjectStats stats={project.stats} />
|
||||||
<StyledProjectToggles>
|
<StyledProjectToggles>
|
||||||
<PaginatedProjectFeatureToggles
|
<PaginatedProjectFeatureToggles
|
||||||
key={
|
style={
|
||||||
loading && searchFeatures.length === 0
|
fullWidth ? { width: '100%', margin: 0 } : undefined
|
||||||
? 'loading'
|
|
||||||
: 'ready'
|
|
||||||
}
|
}
|
||||||
features={searchFeatures}
|
features={searchFeatures || []}
|
||||||
environments={environments}
|
environments={environments}
|
||||||
initialLoad={initialLoad && searchFeatures.length === 0}
|
initialLoad={initialLoad && searchFeatures.length === 0}
|
||||||
loading={loading && searchFeatures.length === 0}
|
loading={loading && searchFeatures.length === 0}
|
||||||
onChange={refetch}
|
onChange={refetch}
|
||||||
total={total}
|
total={total}
|
||||||
searchValue={searchValue}
|
tableState={tableState}
|
||||||
setSearchValue={setSearchValue}
|
setTableState={setTableState}
|
||||||
sortingRules={sortingRules}
|
|
||||||
setSortingRules={setSortingRules}
|
|
||||||
paginationBar={
|
|
||||||
<StickyPaginationBar>
|
|
||||||
<PaginationBar
|
|
||||||
total={total}
|
|
||||||
hasNextPage={hasNextPage}
|
|
||||||
hasPreviousPage={hasPreviousPage}
|
|
||||||
fetchNextPage={fetchNextPage}
|
|
||||||
fetchPrevPage={fetchPrevPage}
|
|
||||||
currentOffset={currentOffset}
|
|
||||||
pageLimit={pageLimit}
|
|
||||||
setPageLimit={setPageLimit}
|
|
||||||
/>
|
|
||||||
</StickyPaginationBar>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</StyledProjectToggles>
|
</StyledProjectToggles>
|
||||||
</StyledContentContainer>
|
</StyledContentContainer>
|
||||||
@ -144,37 +117,6 @@ const PaginatedProjectOverview = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledStickyBar = styled('div')(({ theme }) => ({
|
|
||||||
position: 'sticky',
|
|
||||||
bottom: 0,
|
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
marginLeft: theme.spacing(2),
|
|
||||||
zIndex: 9999,
|
|
||||||
borderBottomLeftRadius: theme.shape.borderRadiusMedium,
|
|
||||||
borderBottomRightRadius: theme.shape.borderRadiusMedium,
|
|
||||||
borderTop: `1px solid ${theme.palette.divider}`,
|
|
||||||
boxShadow: `0px -2px 8px 0px rgba(32, 32, 33, 0.06)`,
|
|
||||||
height: '52px',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledStickyBarContentContainer = styled(Box)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
width: '100%',
|
|
||||||
minWidth: 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StickyPaginationBar: React.FC = ({ children }) => {
|
|
||||||
return (
|
|
||||||
<StyledStickyBar>
|
|
||||||
<StyledStickyBarContentContainer>
|
|
||||||
{children}
|
|
||||||
</StyledStickyBarContentContainer>
|
|
||||||
</StyledStickyBar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated remove when flag `featureSearchFrontend` is removed
|
* @deprecated remove when flag `featureSearchFrontend` is removed
|
||||||
*/
|
*/
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
import { Box, styled } from '@mui/material';
|
||||||
|
import { PaginationBar } from 'component/common/PaginationBar/PaginationBar';
|
||||||
|
import { ComponentProps, FC } from 'react';
|
||||||
|
|
||||||
|
const StyledStickyBar = styled('div')(({ theme }) => ({
|
||||||
|
position: 'sticky',
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
marginLeft: theme.spacing(2),
|
||||||
|
zIndex: theme.zIndex.fab,
|
||||||
|
borderBottomLeftRadius: theme.shape.borderRadiusMedium,
|
||||||
|
borderBottomRightRadius: theme.shape.borderRadiusMedium,
|
||||||
|
borderTop: `1px solid ${theme.palette.divider}`,
|
||||||
|
boxShadow: `0px -2px 8px 0px rgba(32, 32, 33, 0.06)`,
|
||||||
|
height: '52px',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledStickyBarContentContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '100%',
|
||||||
|
minWidth: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StickyPaginationBar: FC<ComponentProps<typeof PaginationBar>> = ({
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<StyledStickyBar>
|
||||||
|
<StyledStickyBarContentContainer>
|
||||||
|
<PaginationBar {...props} />
|
||||||
|
</StyledStickyBarContentContainer>
|
||||||
|
</StyledStickyBar>
|
||||||
|
);
|
||||||
|
};
|
@ -4,7 +4,12 @@ import { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
|||||||
import { formatApiPath } from 'utils/formatPath';
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
import { translateToQueryParams } from './searchToQueryParams';
|
import { translateToQueryParams } from './searchToQueryParams';
|
||||||
import { ISortingRules } from 'component/project/Project/ProjectFeatureToggles/PaginatedProjectFeatureToggles';
|
|
||||||
|
type ISortingRules = {
|
||||||
|
sortBy: string;
|
||||||
|
sortOrder: string;
|
||||||
|
favoritesFirst: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type IFeatureSearchResponse = {
|
type IFeatureSearchResponse = {
|
||||||
features: IFeatureToggleListItem[];
|
features: IFeatureToggleListItem[];
|
||||||
@ -109,6 +114,8 @@ const createFeatureSearch = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_PAGE_LIMIT = 25;
|
||||||
|
|
||||||
export const useFeatureSearch = createFeatureSearch();
|
export const useFeatureSearch = createFeatureSearch();
|
||||||
|
|
||||||
const getFeatureSearchFetcher = (
|
const getFeatureSearchFetcher = (
|
||||||
@ -137,7 +144,7 @@ const getFeatureSearchFetcher = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const translateToSortQueryParams = (sortingRules: ISortingRules) => {
|
const translateToSortQueryParams = (sortingRules: ISortingRules) => {
|
||||||
const { sortBy, sortOrder, isFavoritesPinned } = sortingRules;
|
const { sortBy, sortOrder, favoritesFirst } = sortingRules;
|
||||||
const sortQueryParams = `sortBy=${sortBy}&sortOrder=${sortOrder}&favoritesFirst=${isFavoritesPinned}`;
|
const sortQueryParams = `sortBy=${sortBy}&sortOrder=${sortOrder}&favoritesFirst=${favoritesFirst}`;
|
||||||
return sortQueryParams;
|
return sortQueryParams;
|
||||||
};
|
};
|
||||||
|
@ -231,7 +231,7 @@ describe('useTableState', () => {
|
|||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useTableState<{
|
useTableState<{
|
||||||
[key: string]: string | string[];
|
[key: string]: string;
|
||||||
}>({}, 'test', ['saveOnlyThisToUrl'], ['page']),
|
}>({}, 'test', ['saveOnlyThisToUrl'], ['page']),
|
||||||
);
|
);
|
||||||
const setParams = result.current[1];
|
const setParams = result.current[1];
|
||||||
@ -245,7 +245,7 @@ describe('useTableState', () => {
|
|||||||
sortBy: 'type',
|
sortBy: 'type',
|
||||||
sortOrder: 'favorites',
|
sortOrder: 'favorites',
|
||||||
favorites: 'false',
|
favorites: 'false',
|
||||||
columns: ['test', 'id'],
|
columns: 'test,id',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -253,39 +253,16 @@ describe('useTableState', () => {
|
|||||||
expect(storageSetter).toHaveBeenCalledWith({ page: '2' });
|
expect(storageSetter).toHaveBeenCalledWith({ page: '2' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can reset state to the default instead of overwriting', () => {
|
it('can update query and storage without triggering a rerender', () => {
|
||||||
mockStorage.mockReturnValue({
|
|
||||||
value: { pageSize: 25 },
|
|
||||||
setValue: vi.fn(),
|
|
||||||
});
|
|
||||||
mockQuery.mockReturnValue([new URLSearchParams('page=4'), vi.fn()]);
|
|
||||||
|
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
useTableState<{
|
useTableState({ page: '1' }, 'test', [], []),
|
||||||
page: string;
|
|
||||||
pageSize?: string;
|
|
||||||
sortBy?: string;
|
|
||||||
}>({ page: '1', pageSize: '10' }, 'test'),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const setParams = result.current[1];
|
const setParams = result.current[1];
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
setParams({ sortBy: 'type' });
|
setParams({ page: '2' }, true);
|
||||||
});
|
|
||||||
expect(result.current[0]).toEqual({
|
|
||||||
page: '4',
|
|
||||||
pageSize: '10',
|
|
||||||
sortBy: 'type',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
expect(result.current[0]).toEqual({ page: '1' });
|
||||||
setParams({ pageSize: '50' }, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.current[0]).toEqual({
|
|
||||||
page: '1',
|
|
||||||
pageSize: '50',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { createLocalStorage } from '../utils/createLocalStorage';
|
import { createLocalStorage } from '../utils/createLocalStorage';
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ const defaultQueryKeys = [...defaultStoredKeys, 'page'];
|
|||||||
* @param queryKeys array of elements to be saved in the url
|
* @param queryKeys array of elements to be saved in the url
|
||||||
* @param storageKeys array of elements to be saved in local storage
|
* @param storageKeys array of elements to be saved in local storage
|
||||||
*/
|
*/
|
||||||
export const useTableState = <Params extends Record<string, string | string[]>>(
|
export const useTableState = <Params extends Record<string, string>>(
|
||||||
defaultParams: Params,
|
defaultParams: Params,
|
||||||
storageId: string,
|
storageId: string,
|
||||||
queryKeys?: Array<keyof Params>,
|
queryKeys?: Array<keyof Params>,
|
||||||
@ -52,31 +52,34 @@ export const useTableState = <Params extends Record<string, string | string[]>>(
|
|||||||
...searchQuery,
|
...searchQuery,
|
||||||
} as Params);
|
} as Params);
|
||||||
|
|
||||||
const updateParams = (value: Partial<Params>, reset = false) => {
|
const updateParams = useCallback(
|
||||||
const newState: Params = reset
|
(value: Partial<Params>, quiet = false) => {
|
||||||
? { ...defaultParams, ...value }
|
const newState: Params = {
|
||||||
: {
|
...params,
|
||||||
...params,
|
...value,
|
||||||
...value,
|
};
|
||||||
};
|
|
||||||
|
|
||||||
// remove keys with undefined values
|
// remove keys with undefined values
|
||||||
Object.keys(newState).forEach((key) => {
|
Object.keys(newState).forEach((key) => {
|
||||||
if (newState[key] === undefined) {
|
if (newState[key] === undefined) {
|
||||||
delete newState[key];
|
delete newState[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!quiet) {
|
||||||
|
setParams(newState);
|
||||||
}
|
}
|
||||||
});
|
setSearchParams(
|
||||||
|
filterObjectKeys(newState, queryKeys || defaultQueryKeys),
|
||||||
|
);
|
||||||
|
setStoredParams(
|
||||||
|
filterObjectKeys(newState, storageKeys || defaultStoredKeys),
|
||||||
|
);
|
||||||
|
|
||||||
setParams(newState);
|
return params;
|
||||||
setSearchParams(
|
},
|
||||||
filterObjectKeys(newState, queryKeys || defaultQueryKeys),
|
[setParams, setSearchParams, setStoredParams],
|
||||||
);
|
);
|
||||||
setStoredParams(
|
|
||||||
filterObjectKeys(newState, storageKeys || defaultStoredKeys),
|
|
||||||
);
|
|
||||||
|
|
||||||
return params;
|
|
||||||
};
|
|
||||||
|
|
||||||
return [params, updateParams] as const;
|
return [params, updateParams] as const;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user