Skip to content

Commit 3321a7b

Browse files
maxiadlovskiidennisoelkers
authored andcommitted
Use endpoint to get list of predefined layout variants (#25947)
1 parent fa94a73 commit 3321a7b

17 files changed

Lines changed: 462 additions & 143 deletions

graylog2-web-interface/src/components/common/EntityDataTable/EntityDataTable.tsx

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import useAuthorizedColumnSchemas from 'components/common/EntityDataTable/hooks/
4646
import useIntersectionObserver from 'hooks/useIntersectionObserver';
4747
import { CELL_PADDING } from 'components/common/EntityDataTable/Constants';
4848
import ActiveSliceColContext from 'components/common/EntityDataTable/contexts/ActiveSliceColContext';
49+
import useInternalLayoutPreferences from 'components/common/EntityDataTable/hooks/useInternalLayoutPreferences';
4950

5051
import type {
5152
ColumnRenderers,
@@ -232,18 +233,7 @@ const EntityDataTable = <Entity extends EntityBase, Meta = unknown>({
232233
const scrolledToRightIndicator = useRef<HTMLDivElement>();
233234
const scrolledToRight = useIntersectionObserver(scrollContainerRef, scrolledToRightIndicator);
234235

235-
const [internalAttributeColumnOrder, setInternalAttributeColumnOrder] = useState<Array<string>>(
236-
layoutPreferences?.order ?? defaultColumnOrder,
237-
);
238-
const [internalColumnWidthPreferences, setInternalColumnWidthPreferences] = useState<{
239-
[attributeId: string]: number;
240-
}>(() =>
241-
Object.fromEntries(
242-
Object.entries(layoutPreferences?.attributes ?? {}).flatMap(([key, { width }]) =>
243-
typeof width === 'number' ? [[key, width]] : [],
244-
),
245-
),
246-
);
236+
const { setInternalAttributeColumnOrder, setInternalColumnWidthPreferences, internalColumnWidthPreferences, internalAttributeColumnOrder } = useInternalLayoutPreferences({ layoutPreferences, defaultColumnOrder });
247237

248238
const columnOrder = useVisibleColumnOrder(
249239
layoutPreferences?.attributes,
@@ -311,7 +301,7 @@ const EntityDataTable = <Entity extends EntityBase, Meta = unknown>({
311301
setInternalAttributeColumnOrder(defaultColumnOrder);
312302
setInternalColumnWidthPreferences({});
313303
});
314-
}, [defaultColumnOrder, onResetLayoutPreferences]);
304+
}, [defaultColumnOrder, onResetLayoutPreferences, setInternalAttributeColumnOrder, setInternalColumnWidthPreferences]);
315305

316306
return (
317307
<MetaDataProvider<Meta> meta={meta}>

graylog2-web-interface/src/components/common/EntityDataTable/hooks/useAttributeColumnDefinitions.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,11 @@ const useSortableCol = (colId: string, disabled: boolean) => {
7373
const cssTransform = CSS.Translate.toString(transform);
7474

7575
useLayoutEffect(() => {
76-
setColumnTransform((cur) => ({
77-
...cur,
78-
[colId]: cssTransform,
79-
}));
76+
setColumnTransform((cur) => {
77+
if (cur[colId] === cssTransform) return cur;
78+
79+
return { ...cur, [colId]: cssTransform };
80+
});
8081
}, [colId, setColumnTransform, cssTransform]);
8182

8283
return {

graylog2-web-interface/src/components/common/EntityDataTable/hooks/useColumnWidths.ts

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
* along with this program. If not, see
1515
* <http://www.mongodb.com/licensing/server-side-public-license>.
1616
*/
17-
import { useState, useLayoutEffect, useMemo } from 'react';
17+
import { useState, useLayoutEffect, useMemo, useRef } from 'react';
18+
import isEqual from 'lodash/isEqual';
1819

1920
import {
2021
DEFAULT_COL_MIN_WIDTH,
@@ -135,6 +136,7 @@ const useColumnWidths = <Entity extends EntityBase>({
135136
headerMinWidths: { [colId: string]: number };
136137
}) => {
137138
const [columnWidths, setColumnWidths] = useState({});
139+
const prevColumnWidthsRef = useRef<Record<string, number>>({});
138140
const staticColumnWidths = useMemo(
139141
() =>
140142
calculateStaticColumnWidths({
@@ -161,28 +163,21 @@ const useColumnWidths = <Entity extends EntityBase>({
161163
staticColumnWidths,
162164
});
163165

164-
// eslint-disable-next-line react-hooks/set-state-in-effect
165-
setColumnWidths(
166-
calculateColumnWidths({
167-
actionsColMinWidth,
168-
assignableWidth,
169-
attributeColumnIds: columnIds,
170-
attributeColumnRenderers: columnRenderersByAttribute,
171-
bulkSelectColWidth,
172-
staticColumnWidths,
173-
headerMinWidths,
174-
}),
175-
);
176-
}, [
177-
actionsColMinWidth,
178-
bulkSelectColWidth,
179-
columnRenderersByAttribute,
180-
columnIds,
181-
scrollContainerWidth,
182-
columnWidthPreferences,
183-
staticColumnWidths,
184-
headerMinWidths,
185-
]);
166+
const newColumnWidths = calculateColumnWidths({
167+
actionsColMinWidth,
168+
assignableWidth,
169+
attributeColumnIds: columnIds,
170+
attributeColumnRenderers: columnRenderersByAttribute,
171+
bulkSelectColWidth,
172+
staticColumnWidths,
173+
headerMinWidths,
174+
})
175+
176+
if (!isEqual(prevColumnWidthsRef.current, newColumnWidths)) {
177+
prevColumnWidthsRef.current = newColumnWidths;
178+
setColumnWidths(newColumnWidths);
179+
}
180+
}, [actionsColMinWidth, bulkSelectColWidth, columnRenderersByAttribute, columnIds, scrollContainerWidth, columnWidthPreferences, staticColumnWidths, headerMinWidths]);
186181

187182
return columnWidths;
188183
};
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
import { useState } from 'react';
18+
import isEqual from 'lodash/isEqual';
19+
20+
type ColumnWidthPreferences = {
21+
[attributeId: string]: number;
22+
};
23+
24+
type LayoutPreferences = {
25+
order?: Array<string>;
26+
attributes?: {
27+
[attributeId: string]: {
28+
width?: number;
29+
};
30+
};
31+
};
32+
33+
const getInitialColumnWidthPreferences = (
34+
layoutPreferences?: LayoutPreferences,
35+
): ColumnWidthPreferences => Object.fromEntries(
36+
Object.entries(layoutPreferences?.attributes ?? {}).flatMap(([key, { width }]) =>
37+
typeof width === 'number' ? [[key, width]] : [],
38+
),
39+
)
40+
41+
const useInternalLayoutPreferences = ({
42+
layoutPreferences,
43+
defaultColumnOrder,
44+
}: {
45+
layoutPreferences?: LayoutPreferences;
46+
defaultColumnOrder: Array<string>;
47+
}) => {
48+
const getInitialState = () => ({
49+
internalAttributeColumnOrder: layoutPreferences?.order ?? defaultColumnOrder,
50+
internalColumnWidthPreferences: getInitialColumnWidthPreferences(layoutPreferences),
51+
});
52+
53+
const [prevInitialState, setPrevInitialState] = useState(getInitialState);
54+
55+
const [internalAttributeColumnOrder, setInternalAttributeColumnOrder] = useState<Array<string>>(
56+
() => prevInitialState.internalAttributeColumnOrder,
57+
);
58+
59+
const [internalColumnWidthPreferences, setInternalColumnWidthPreferences] =
60+
useState<ColumnWidthPreferences>(() => prevInitialState.internalColumnWidthPreferences);
61+
62+
const nextInitialState = getInitialState();
63+
64+
if (
65+
!isEqual(nextInitialState, prevInitialState)
66+
) {
67+
setPrevInitialState(nextInitialState);
68+
setInternalAttributeColumnOrder(nextInitialState.internalAttributeColumnOrder);
69+
setInternalColumnWidthPreferences(nextInitialState.internalColumnWidthPreferences);
70+
}
71+
72+
return {
73+
internalAttributeColumnOrder,
74+
setInternalAttributeColumnOrder,
75+
internalColumnWidthPreferences,
76+
setInternalColumnWidthPreferences,
77+
};
78+
}
79+
80+
export default useInternalLayoutPreferences

graylog2-web-interface/src/components/common/EntityDataTable/hooks/useTableLayout.test.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,58 @@ describe('useTableLayout hook', () => {
9090
});
9191
});
9292

93+
it('should provide default slicing preferences when there are no user slicing preferences', async () => {
94+
const defaultSlicing = { sliceColumn: 'status', sortBy: 'risk_score', order: 'desc' as const };
95+
96+
asMock(useUserLayoutPreferences).mockReturnValue({ data: undefined, isInitialLoading: false, refetch: () => {} });
97+
98+
const { result } = renderHook(
99+
() =>
100+
useTableLayout({
101+
entityTableId: 'streams',
102+
...defaultLayout,
103+
defaultSlicing,
104+
}),
105+
{ wrapper },
106+
);
107+
108+
expect(result.current.layoutConfig).toEqual({
109+
attributes: undefined,
110+
order: undefined,
111+
sort: defaultLayout.defaultSort,
112+
pageSize: defaultLayout.defaultPageSize,
113+
slicing: defaultSlicing,
114+
});
115+
});
116+
117+
it('should allow user preferences to disable default slicing', async () => {
118+
const defaultSlicing = { sliceColumn: 'status', sortBy: 'risk_score', order: 'desc' as const };
119+
120+
asMock(useUserLayoutPreferences).mockReturnValue({
121+
data: { slicing: null },
122+
isInitialLoading: false,
123+
refetch: () => {},
124+
});
125+
126+
const { result } = renderHook(
127+
() =>
128+
useTableLayout({
129+
entityTableId: 'streams',
130+
...defaultLayout,
131+
defaultSlicing,
132+
}),
133+
{ wrapper },
134+
);
135+
136+
expect(result.current.layoutConfig).toEqual({
137+
attributes: undefined,
138+
order: undefined,
139+
sort: defaultLayout.defaultSort,
140+
pageSize: defaultLayout.defaultPageSize,
141+
slicing: undefined,
142+
});
143+
});
144+
93145
it('should merge user preferences with defaults', async () => {
94146
asMock(useUserLayoutPreferences).mockReturnValue({
95147
data: { perPage: layoutPreferences.perPage },

graylog2-web-interface/src/components/common/EntityDataTable/hooks/useTableLayout.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,35 +32,28 @@ const useTableLayout = ({
3232
entityTableId,
3333
layoutVariant,
3434
defaultSort,
35+
defaultSlicing,
3536
defaultPageSize,
3637
}: DefaultLayout): {
3738
isInitialLoading: boolean;
3839
layoutConfig: LayoutConfig;
3940
} => {
4041
const { data: userLayoutPreferences = {}, isInitialLoading } = useUserLayoutPreferences(entityTableId, layoutVariant);
4142

42-
return useMemo(
43-
() => ({
43+
return useMemo(() => {
44+
const hasSlicingPreference = Object.prototype.hasOwnProperty.call(userLayoutPreferences, 'slicing');
45+
46+
return {
4447
layoutConfig: {
4548
attributes: userLayoutPreferences?.attributes,
4649
order: userLayoutPreferences.order,
4750
pageSize: userLayoutPreferences.perPage ?? defaultPageSize,
48-
slicing: userLayoutPreferences.slicing,
51+
slicing: hasSlicingPreference ? (userLayoutPreferences.slicing ?? undefined) : defaultSlicing,
4952
sort: userLayoutPreferences.sort ?? defaultSort,
5053
},
5154
isInitialLoading,
52-
}),
53-
[
54-
defaultPageSize,
55-
defaultSort,
56-
isInitialLoading,
57-
userLayoutPreferences?.attributes,
58-
userLayoutPreferences.order,
59-
userLayoutPreferences.perPage,
60-
userLayoutPreferences.slicing,
61-
userLayoutPreferences.sort,
62-
],
63-
);
55+
};
56+
}, [defaultPageSize, defaultSlicing, defaultSort, isInitialLoading, userLayoutPreferences]);
6457
};
6558

6659
export default useTableLayout;

graylog2-web-interface/src/components/common/EntityDataTable/hooks/useUpdateUserLayoutPreferences.test.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,22 @@ describe('useUserSearchFilterQuery hook', () => {
7171
);
7272
});
7373

74+
it('should update null slicing preferences', async () => {
75+
const { result } = renderHook(() => useUpdateUserLayoutPreferences('streams'), { wrapper });
76+
77+
result.current.mutateAsync({
78+
...layoutPreferences,
79+
slicing: null,
80+
});
81+
82+
await waitFor(() =>
83+
expect(fetch).toHaveBeenCalledWith('POST', expect.stringContaining('/entitylists/preferences/streams'), {
84+
...layoutPreferencesJSON,
85+
slicing: null,
86+
}),
87+
);
88+
});
89+
7490
it('should update user layout preferences for a layout variant', async () => {
7591
const { result } = renderHook(() => useUpdateUserLayoutPreferences('streams', 'security-events'), { wrapper });
7692

graylog2-web-interface/src/components/common/EntityDataTable/hooks/useUpdateUserLayoutPreferences.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,31 @@ import { useMutation } from '@tanstack/react-query';
1818

1919
import fetch from 'logic/rest/FetchProvider';
2020
import { qualifyUrl } from 'util/URLUtils';
21-
import type { TableLayoutPreferences, TableLayoutPreferencesJSON } from 'components/common/EntityDataTable/types';
21+
import type {
22+
SlicingPreferences,
23+
SlicingPreferencesJSON,
24+
TableLayoutPreferences,
25+
TableLayoutPreferencesJSON,
26+
} from 'components/common/EntityDataTable/types';
2227
import UserNotification from 'util/UserNotification';
2328
import useUserLayoutPreferences from 'components/common/EntityDataTable/hooks/useUserLayoutPreferences';
2429

30+
const slicingToJSON = (slicing?: SlicingPreferences | null): SlicingPreferencesJSON | null | undefined => {
31+
if (slicing === null) {
32+
return null;
33+
}
34+
35+
if (!slicing) {
36+
return undefined;
37+
}
38+
39+
return {
40+
slice_column: slicing.sliceColumn,
41+
sort_by: slicing.sortBy,
42+
order: slicing.order,
43+
};
44+
};
45+
2546
const preferencesToJSON = <T>({
2647
attributes,
2748
sort,
@@ -33,13 +54,7 @@ const preferencesToJSON = <T>({
3354
attributes,
3455
sort: sort ? { order: sort.direction, field: sort.attributeId } : undefined,
3556
per_page: perPage,
36-
slicing: slicing
37-
? {
38-
slice_column: slicing.sliceColumn,
39-
sort_by: slicing.sortBy,
40-
order: slicing.order,
41-
}
42-
: undefined,
57+
slicing: slicingToJSON(slicing),
4358
custom_preferences: customPreferences,
4459
order,
4560
});

graylog2-web-interface/src/components/common/EntityDataTable/hooks/useUserLayoutPreferences.test.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,24 @@ describe('useUserSearchFilterQuery hook', () => {
7474
});
7575
});
7676

77+
it('should return null slicing preferences', async () => {
78+
asMock(fetch).mockImplementation(() =>
79+
Promise.resolve({
80+
...layoutPreferencesJSON,
81+
slicing: null,
82+
}),
83+
);
84+
const { result } = renderHook(() => useUserLayoutPreferences('streams'), { wrapper });
85+
86+
await waitFor(() => result.current.isInitialLoading);
87+
await waitFor(() => !result.current.isInitialLoading);
88+
89+
expect(result.current.data).toEqual({
90+
...layoutPreferences,
91+
slicing: null,
92+
});
93+
});
94+
7795
it('should fetch layout preferences for a layout variant', async () => {
7896
asMock(fetch).mockImplementation(() => Promise.resolve(layoutPreferencesJSON));
7997
const { result } = renderHook(() => useUserLayoutPreferences('streams', 'security-events'), { wrapper });

0 commit comments

Comments
 (0)