mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
Merge remote-tracking branch 'origin/archive_table' into archive_table
This commit is contained in:
commit
efa66b2ab2
@ -76,7 +76,7 @@
|
|||||||
"immer": "9.0.14",
|
"immer": "9.0.14",
|
||||||
"jsdom": "^19.0.0",
|
"jsdom": "^19.0.0",
|
||||||
"lodash.clonedeep": "4.5.0",
|
"lodash.clonedeep": "4.5.0",
|
||||||
"msw": "0.42.0",
|
"msw": "0.42.1",
|
||||||
"pkginfo": "^0.4.1",
|
"pkginfo": "^0.4.1",
|
||||||
"plausible-tracker": "0.3.8",
|
"plausible-tracker": "0.3.8",
|
||||||
"prettier": "2.6.2",
|
"prettier": "2.6.2",
|
||||||
|
@ -22,7 +22,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
|||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import { sortTypes } from 'utils/sortTypes';
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||||
import { useVirtualizedRange } from 'hooks/useVirtualizedRange';
|
import { useVirtualizedRange } from 'hooks/useVirtualizedRange';
|
||||||
import { FeatureSchema } from 'openapi';
|
import { FeatureSchema } from 'openapi';
|
||||||
import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton';
|
import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton';
|
||||||
@ -101,21 +101,32 @@ const columns = [
|
|||||||
|
|
||||||
const defaultSort: SortingRule<string> = { id: 'createdAt', desc: true };
|
const defaultSort: SortingRule<string> = { id: 'createdAt', desc: true };
|
||||||
|
|
||||||
|
const { value: storedParams, setValue: setStoredParams } = createLocalStorage(
|
||||||
|
'FeatureToggleListTable:v1',
|
||||||
|
defaultSort
|
||||||
|
);
|
||||||
|
|
||||||
export const FeatureToggleListTable: VFC = () => {
|
export const FeatureToggleListTable: VFC = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const rowHeight = theme.shape.tableRowHeight;
|
const rowHeight = theme.shape.tableRowHeight;
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
|
||||||
const [storedParams, setStoredParams] = useLocalStorage(
|
|
||||||
'FeatureToggleListTable:v1',
|
|
||||||
defaultSort
|
|
||||||
);
|
|
||||||
const { features = [], loading } = useFeatures();
|
const { features = [], loading } = useFeatures();
|
||||||
const [searchValue, setSearchValue] = useState(
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
searchParams.get('search') || ''
|
const [initialState] = useState(() => ({
|
||||||
);
|
sortBy: [
|
||||||
|
{
|
||||||
|
id: searchParams.get('sort') || storedParams.id,
|
||||||
|
desc: searchParams.has('order')
|
||||||
|
? searchParams.get('order') === 'desc'
|
||||||
|
: storedParams.desc,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hiddenColumns: ['description'],
|
||||||
|
globalFilter: searchParams.get('search') || '',
|
||||||
|
}));
|
||||||
|
const [searchValue, setSearchValue] = useState(initialState.globalFilter);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: searchedData,
|
data: searchedData,
|
||||||
@ -131,18 +142,6 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
[searchedData, loading]
|
[searchedData, loading]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [initialState] = useState(() => ({
|
|
||||||
sortBy: [
|
|
||||||
{
|
|
||||||
id: searchParams.get('sort') || storedParams.id,
|
|
||||||
desc: searchParams.has('order')
|
|
||||||
? searchParams.get('order') === 'desc'
|
|
||||||
: storedParams.desc,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
hiddenColumns: ['description'],
|
|
||||||
}));
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getTableProps,
|
getTableProps,
|
||||||
getTableBodyProps,
|
getTableBodyProps,
|
||||||
@ -190,7 +189,7 @@ export const FeatureToggleListTable: VFC = () => {
|
|||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false });
|
setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false });
|
||||||
}, [sortBy, searchValue, setSearchParams, setStoredParams]);
|
}, [sortBy, searchValue, setSearchParams]);
|
||||||
|
|
||||||
const [firstRenderedIndex, lastRenderedIndex] =
|
const [firstRenderedIndex, lastRenderedIndex] =
|
||||||
useVirtualizedRange(rowHeight);
|
useVirtualizedRange(rowHeight);
|
||||||
|
@ -30,7 +30,6 @@ interface IColumnsMenuProps {
|
|||||||
dividerBefore?: string[];
|
dividerBefore?: string[];
|
||||||
dividerAfter?: string[];
|
dividerAfter?: string[];
|
||||||
isCustomized?: boolean;
|
isCustomized?: boolean;
|
||||||
onCustomize?: (columns: string[]) => void;
|
|
||||||
setHiddenColumns: (
|
setHiddenColumns: (
|
||||||
hiddenColumns:
|
hiddenColumns:
|
||||||
| string[]
|
| string[]
|
||||||
@ -44,7 +43,6 @@ 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);
|
||||||
@ -96,17 +94,6 @@ export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
|
|||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onItemClick = (column: typeof allColumns[number]) => {
|
|
||||||
onCustomize([
|
|
||||||
...allColumns
|
|
||||||
.filter(({ isVisible }) => isVisible)
|
|
||||||
.map(({ id }) => id)
|
|
||||||
.filter(id => !staticColumns.includes(id) && id !== column.id),
|
|
||||||
...(!column.isVisible ? [column.id] : []),
|
|
||||||
]);
|
|
||||||
column.toggleHidden(column.isVisible);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isOpen = Boolean(anchorEl);
|
const isOpen = Boolean(anchorEl);
|
||||||
const id = `columns-menu`;
|
const id = `columns-menu`;
|
||||||
const menuId = `columns-menu-list-${id}`;
|
const menuId = `columns-menu-list-${id}`;
|
||||||
@ -162,7 +149,9 @@ export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
|
|||||||
show={<Divider className={classes.divider} />}
|
show={<Divider className={classes.divider} />}
|
||||||
/>,
|
/>,
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => onItemClick(column)}
|
onClick={() =>
|
||||||
|
column.toggleHidden(column.isVisible)
|
||||||
|
}
|
||||||
disabled={staticColumns.includes(column.id)}
|
disabled={staticColumns.includes(column.id)}
|
||||||
className={classes.menuItem}
|
className={classes.menuItem}
|
||||||
>
|
>
|
||||||
|
@ -29,7 +29,7 @@ import {
|
|||||||
} from 'component/common/Table';
|
} from 'component/common/Table';
|
||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
import useProject from 'hooks/api/getters/useProject/useProject';
|
import useProject from 'hooks/api/getters/useProject/useProject';
|
||||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||||
import { useVirtualizedRange } from 'hooks/useVirtualizedRange';
|
import { useVirtualizedRange } from 'hooks/useVirtualizedRange';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors';
|
import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors';
|
||||||
@ -93,7 +93,14 @@ export const ProjectFeatureToggles = ({
|
|||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
|
||||||
|
const { value: storedParams, setValue: setStoredParams } =
|
||||||
|
createLocalStorage(
|
||||||
|
`${projectId}:FeatureToggleListTable:v1`,
|
||||||
|
defaultSort
|
||||||
|
);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
const environments = useEnvironmentsRef(
|
const environments = useEnvironmentsRef(
|
||||||
loading ? ['a', 'b', 'c'] : newEnvironments
|
loading ? ['a', 'b', 'c'] : newEnvironments
|
||||||
@ -235,12 +242,6 @@ export const ProjectFeatureToggles = ({
|
|||||||
[projectId, environments, onToggle, loading]
|
[projectId, environments, onToggle, loading]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
|
||||||
const [storedParams, setStoredParams] = useLocalStorage(
|
|
||||||
`${projectId}:ProjectFeatureToggles`,
|
|
||||||
defaultSort
|
|
||||||
);
|
|
||||||
|
|
||||||
const [searchValue, setSearchValue] = useState(
|
const [searchValue, setSearchValue] = useState(
|
||||||
searchParams.get('search') || ''
|
searchParams.get('search') || ''
|
||||||
);
|
);
|
||||||
@ -300,6 +301,7 @@ export const ProjectFeatureToggles = ({
|
|||||||
|
|
||||||
const initialState = useMemo(
|
const initialState = useMemo(
|
||||||
() => {
|
() => {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
const allColumnIds = columns.map(
|
const allColumnIds = columns.map(
|
||||||
(column: any) => column?.accessor || column?.id
|
(column: any) => column?.accessor || column?.id
|
||||||
);
|
);
|
||||||
@ -384,27 +386,17 @@ export const ProjectFeatureToggles = ({
|
|||||||
setSearchParams(tableState, {
|
setSearchParams(tableState, {
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
setStoredParams({
|
setStoredParams(params => ({
|
||||||
|
...params,
|
||||||
id: sortBy[0].id,
|
id: sortBy[0].id,
|
||||||
desc: sortBy[0].desc || false,
|
desc: sortBy[0].desc || false,
|
||||||
columns: tableState.columns.split(','),
|
columns: tableState.columns.split(','),
|
||||||
});
|
}));
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [loading, sortBy, hiddenColumns, searchValue, setSearchParams]);
|
}, [loading, sortBy, hiddenColumns, searchValue, setSearchParams]);
|
||||||
|
|
||||||
const onCustomizeColumns = useCallback(
|
const [firstRenderedIndex, lastRenderedIndex] =
|
||||||
visibleColumns => {
|
useVirtualizedRange(rowHeight);
|
||||||
setStoredParams(storedParams => ({
|
|
||||||
...storedParams,
|
|
||||||
columns: visibleColumns,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
[setStoredParams]
|
|
||||||
);
|
|
||||||
const [firstRenderedIndex, lastRenderedIndex] = useVirtualizedRange(
|
|
||||||
rowHeight,
|
|
||||||
20
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<PageContent
|
||||||
@ -436,7 +428,6 @@ export const ProjectFeatureToggles = ({
|
|||||||
dividerAfter={['createdAt']}
|
dividerAfter={['createdAt']}
|
||||||
dividerBefore={['Actions']}
|
dividerBefore={['Actions']}
|
||||||
isCustomized={Boolean(storedParams.columns)}
|
isCustomized={Boolean(storedParams.columns)}
|
||||||
onCustomize={onCustomizeColumns}
|
|
||||||
setHiddenColumns={setHiddenColumns}
|
setHiddenColumns={setHiddenColumns}
|
||||||
/>
|
/>
|
||||||
<PageHeader.Divider sx={{ marginLeft: 0 }} />
|
<PageHeader.Divider sx={{ marginLeft: 0 }} />
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
import { vi } from 'vitest';
|
|
||||||
import { useLocalStorage } from './useLocalStorage';
|
|
||||||
import { act, renderHook } from '@testing-library/react-hooks';
|
|
||||||
|
|
||||||
describe('useLocalStorage', () => {
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return an object with data and mutate properties', () => {
|
|
||||||
vi.spyOn(Storage.prototype, 'getItem').mockImplementationOnce(() =>
|
|
||||||
JSON.stringify(undefined)
|
|
||||||
);
|
|
||||||
const { result } = renderHook(() => useLocalStorage('key', {}));
|
|
||||||
|
|
||||||
expect(result.current).toEqual([{}, expect.any(Function)]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns default value', () => {
|
|
||||||
vi.spyOn(Storage.prototype, 'getItem').mockImplementationOnce(() =>
|
|
||||||
JSON.stringify(undefined)
|
|
||||||
);
|
|
||||||
const { result } = renderHook(() =>
|
|
||||||
useLocalStorage('key', { key: 'value' })
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.current).toEqual([
|
|
||||||
{ key: 'value' },
|
|
||||||
expect.any(Function),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns a value from local storage', async () => {
|
|
||||||
vi.spyOn(Storage.prototype, 'getItem').mockImplementationOnce(() =>
|
|
||||||
JSON.stringify({ key: 'value' })
|
|
||||||
);
|
|
||||||
|
|
||||||
const { result, waitFor } = renderHook(() =>
|
|
||||||
useLocalStorage('test-key', {})
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(result.current).toEqual([
|
|
||||||
{ key: 'value' },
|
|
||||||
expect.any(Function),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets new value to local storage', async () => {
|
|
||||||
const setItem = vi.spyOn(Storage.prototype, 'setItem');
|
|
||||||
const { result } = renderHook(() =>
|
|
||||||
useLocalStorage('test-key', { key: 'initial-value' })
|
|
||||||
);
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current[1]({ key: 'new-value' });
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(setItem).toHaveBeenCalledWith(
|
|
||||||
':test-key:useLocalStorage:v1',
|
|
||||||
JSON.stringify({ key: 'new-value' })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,34 +0,0 @@
|
|||||||
import { Dispatch, SetStateAction, useCallback, useState } from 'react';
|
|
||||||
import { basePath } from 'utils/formatPath';
|
|
||||||
import { getLocalStorageItem, setLocalStorageItem } from '../utils/storage';
|
|
||||||
|
|
||||||
export const useLocalStorage = <T extends object>(
|
|
||||||
key: string,
|
|
||||||
initialValue: T
|
|
||||||
) => {
|
|
||||||
const internalKey = `${basePath}:${key}:useLocalStorage:v1`;
|
|
||||||
const [value, setValue] = useState<T>(() => {
|
|
||||||
const state = getLocalStorageItem<T>(internalKey);
|
|
||||||
if (state === undefined) {
|
|
||||||
return initialValue;
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
});
|
|
||||||
|
|
||||||
const onUpdate = useCallback<Dispatch<SetStateAction<T>>>(
|
|
||||||
value => {
|
|
||||||
if (value instanceof Function) {
|
|
||||||
setValue(prev => {
|
|
||||||
const output = value(prev);
|
|
||||||
setLocalStorageItem(internalKey, output);
|
|
||||||
return output;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setLocalStorageItem(internalKey, value);
|
|
||||||
setValue(value);
|
|
||||||
},
|
|
||||||
[internalKey]
|
|
||||||
);
|
|
||||||
|
|
||||||
return [value, onUpdate] as const;
|
|
||||||
};
|
|
30
frontend/src/utils/createLocalStorage.ts
Normal file
30
frontend/src/utils/createLocalStorage.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { basePath } from './formatPath';
|
||||||
|
import { getLocalStorageItem, setLocalStorageItem } from './storage';
|
||||||
|
|
||||||
|
export const createLocalStorage = <T extends object>(
|
||||||
|
key: string,
|
||||||
|
initialValue: T
|
||||||
|
) => {
|
||||||
|
const internalKey = `${basePath}:${key}:localStorage:v2`;
|
||||||
|
const value = (() => {
|
||||||
|
const state = getLocalStorageItem<T>(internalKey);
|
||||||
|
if (state === undefined) {
|
||||||
|
return initialValue;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const onUpdate = (newValue: T | ((prev: T) => T)): T => {
|
||||||
|
if (newValue instanceof Function) {
|
||||||
|
const previousValue = getLocalStorageItem<T>(internalKey);
|
||||||
|
const output = newValue(previousValue ?? initialValue);
|
||||||
|
setLocalStorageItem(internalKey, output);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocalStorageItem(internalKey, newValue);
|
||||||
|
return newValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { value, setValue: onUpdate };
|
||||||
|
};
|
@ -4715,10 +4715,10 @@ ms@^2.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||||
|
|
||||||
msw@0.42.0:
|
msw@0.42.1:
|
||||||
version "0.42.0"
|
version "0.42.1"
|
||||||
resolved "https://registry.yarnpkg.com/msw/-/msw-0.42.0.tgz#2286aefad82808888914e2bc5e40666e82b2824b"
|
resolved "https://registry.yarnpkg.com/msw/-/msw-0.42.1.tgz#2496d3e191754b68686e2530de459a2e102f85c4"
|
||||||
integrity sha512-vB9rzgiGHoQGfkKpp3QZHxobzfuuQOJk+0bff0wtbK8k3P3CaUSt8bCwvExours682AY4mUfTjIkCsxy0JoS3w==
|
integrity sha512-LZZuz7VddL45gCBgfBWHyXj6a4W7OTJY0mZPoipJ3P/xwbuJwrtwB3IJrWlqBM8aink/eTKlRxwzmtIAwCj5yQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@mswjs/cookies" "^0.2.0"
|
"@mswjs/cookies" "^0.2.0"
|
||||||
"@mswjs/interceptors" "^0.16.3"
|
"@mswjs/interceptors" "^0.16.3"
|
||||||
|
Loading…
Reference in New Issue
Block a user