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",
|
||||
"jsdom": "^19.0.0",
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
"msw": "0.42.0",
|
||||
"msw": "0.42.1",
|
||||
"pkginfo": "^0.4.1",
|
||||
"plausible-tracker": "0.3.8",
|
||||
"prettier": "2.6.2",
|
||||
|
@ -22,7 +22,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import { sortTypes } from 'utils/sortTypes';
|
||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||
import { useVirtualizedRange } from 'hooks/useVirtualizedRange';
|
||||
import { FeatureSchema } from 'openapi';
|
||||
import { CreateFeatureButton } from '../CreateFeatureButton/CreateFeatureButton';
|
||||
@ -101,21 +101,32 @@ const columns = [
|
||||
|
||||
const defaultSort: SortingRule<string> = { id: 'createdAt', desc: true };
|
||||
|
||||
const { value: storedParams, setValue: setStoredParams } = createLocalStorage(
|
||||
'FeatureToggleListTable:v1',
|
||||
defaultSort
|
||||
);
|
||||
|
||||
export const FeatureToggleListTable: VFC = () => {
|
||||
const theme = useTheme();
|
||||
const rowHeight = theme.shape.tableRowHeight;
|
||||
const { classes } = useStyles();
|
||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const isMediumScreen = useMediaQuery(theme.breakpoints.down('lg'));
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [storedParams, setStoredParams] = useLocalStorage(
|
||||
'FeatureToggleListTable:v1',
|
||||
defaultSort
|
||||
);
|
||||
const { features = [], loading } = useFeatures();
|
||||
const [searchValue, setSearchValue] = useState(
|
||||
searchParams.get('search') || ''
|
||||
);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
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 {
|
||||
data: searchedData,
|
||||
@ -131,18 +142,6 @@ export const FeatureToggleListTable: VFC = () => {
|
||||
[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 {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
@ -190,7 +189,7 @@ export const FeatureToggleListTable: VFC = () => {
|
||||
replace: true,
|
||||
});
|
||||
setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false });
|
||||
}, [sortBy, searchValue, setSearchParams, setStoredParams]);
|
||||
}, [sortBy, searchValue, setSearchParams]);
|
||||
|
||||
const [firstRenderedIndex, lastRenderedIndex] =
|
||||
useVirtualizedRange(rowHeight);
|
||||
|
@ -30,7 +30,6 @@ interface IColumnsMenuProps {
|
||||
dividerBefore?: string[];
|
||||
dividerAfter?: string[];
|
||||
isCustomized?: boolean;
|
||||
onCustomize?: (columns: string[]) => void;
|
||||
setHiddenColumns: (
|
||||
hiddenColumns:
|
||||
| string[]
|
||||
@ -44,7 +43,6 @@ export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
|
||||
dividerBefore = [],
|
||||
dividerAfter = [],
|
||||
isCustomized = false,
|
||||
onCustomize = () => {},
|
||||
setHiddenColumns,
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
@ -96,17 +94,6 @@ export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
|
||||
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 id = `columns-menu`;
|
||||
const menuId = `columns-menu-list-${id}`;
|
||||
@ -162,7 +149,9 @@ export const ColumnsMenu: VFC<IColumnsMenuProps> = ({
|
||||
show={<Divider className={classes.divider} />}
|
||||
/>,
|
||||
<MenuItem
|
||||
onClick={() => onItemClick(column)}
|
||||
onClick={() =>
|
||||
column.toggleHidden(column.isVisible)
|
||||
}
|
||||
disabled={staticColumns.includes(column.id)}
|
||||
className={classes.menuItem}
|
||||
>
|
||||
|
@ -29,7 +29,7 @@ import {
|
||||
} from 'component/common/Table';
|
||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||
import useProject from 'hooks/api/getters/useProject/useProject';
|
||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||
import { useVirtualizedRange } from 'hooks/useVirtualizedRange';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors';
|
||||
@ -93,7 +93,14 @@ export const ProjectFeatureToggles = ({
|
||||
string | undefined
|
||||
>();
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
|
||||
const { value: storedParams, setValue: setStoredParams } =
|
||||
createLocalStorage(
|
||||
`${projectId}:FeatureToggleListTable:v1`,
|
||||
defaultSort
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const environments = useEnvironmentsRef(
|
||||
loading ? ['a', 'b', 'c'] : newEnvironments
|
||||
@ -235,12 +242,6 @@ export const ProjectFeatureToggles = ({
|
||||
[projectId, environments, onToggle, loading]
|
||||
);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [storedParams, setStoredParams] = useLocalStorage(
|
||||
`${projectId}:ProjectFeatureToggles`,
|
||||
defaultSort
|
||||
);
|
||||
|
||||
const [searchValue, setSearchValue] = useState(
|
||||
searchParams.get('search') || ''
|
||||
);
|
||||
@ -300,6 +301,7 @@ export const ProjectFeatureToggles = ({
|
||||
|
||||
const initialState = useMemo(
|
||||
() => {
|
||||
const searchParams = new URLSearchParams();
|
||||
const allColumnIds = columns.map(
|
||||
(column: any) => column?.accessor || column?.id
|
||||
);
|
||||
@ -384,27 +386,17 @@ export const ProjectFeatureToggles = ({
|
||||
setSearchParams(tableState, {
|
||||
replace: true,
|
||||
});
|
||||
setStoredParams({
|
||||
setStoredParams(params => ({
|
||||
...params,
|
||||
id: sortBy[0].id,
|
||||
desc: sortBy[0].desc || false,
|
||||
columns: tableState.columns.split(','),
|
||||
});
|
||||
}));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [loading, sortBy, hiddenColumns, searchValue, setSearchParams]);
|
||||
|
||||
const onCustomizeColumns = useCallback(
|
||||
visibleColumns => {
|
||||
setStoredParams(storedParams => ({
|
||||
...storedParams,
|
||||
columns: visibleColumns,
|
||||
}));
|
||||
},
|
||||
[setStoredParams]
|
||||
);
|
||||
const [firstRenderedIndex, lastRenderedIndex] = useVirtualizedRange(
|
||||
rowHeight,
|
||||
20
|
||||
);
|
||||
const [firstRenderedIndex, lastRenderedIndex] =
|
||||
useVirtualizedRange(rowHeight);
|
||||
|
||||
return (
|
||||
<PageContent
|
||||
@ -436,7 +428,6 @@ export const ProjectFeatureToggles = ({
|
||||
dividerAfter={['createdAt']}
|
||||
dividerBefore={['Actions']}
|
||||
isCustomized={Boolean(storedParams.columns)}
|
||||
onCustomize={onCustomizeColumns}
|
||||
setHiddenColumns={setHiddenColumns}
|
||||
/>
|
||||
<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"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
msw@0.42.0:
|
||||
version "0.42.0"
|
||||
resolved "https://registry.yarnpkg.com/msw/-/msw-0.42.0.tgz#2286aefad82808888914e2bc5e40666e82b2824b"
|
||||
integrity sha512-vB9rzgiGHoQGfkKpp3QZHxobzfuuQOJk+0bff0wtbK8k3P3CaUSt8bCwvExours682AY4mUfTjIkCsxy0JoS3w==
|
||||
msw@0.42.1:
|
||||
version "0.42.1"
|
||||
resolved "https://registry.yarnpkg.com/msw/-/msw-0.42.1.tgz#2496d3e191754b68686e2530de459a2e102f85c4"
|
||||
integrity sha512-LZZuz7VddL45gCBgfBWHyXj6a4W7OTJY0mZPoipJ3P/xwbuJwrtwB3IJrWlqBM8aink/eTKlRxwzmtIAwCj5yQ==
|
||||
dependencies:
|
||||
"@mswjs/cookies" "^0.2.0"
|
||||
"@mswjs/interceptors" "^0.16.3"
|
||||
|
Loading…
Reference in New Issue
Block a user