Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
96d00c3
Use endpoint to get list of predefined layout variants
maxiadlovskii May 8, 2026
49b314c
Merge branch 'master' into feat/connect-use-layout-vatiants-endpoint
maxiadlovskii May 8, 2026
4e660d9
Use endpoint to get list of predefined layout variants
maxiadlovskii May 8, 2026
d6b8c19
Merge branch 'master' into feat/connect-use-layout-vatiants-endpoint
maxiadlovskii May 8, 2026
5161bf3
fix reset behaviour
maxiadlovskii May 8, 2026
39e6100
add proper reset
maxiadlovskii May 8, 2026
e05d102
Merge branch 'master' into feat/connect-use-layout-vatiants-endpoint
maxiadlovskii May 18, 2026
9e17623
ad license
maxiadlovskii May 18, 2026
bc31242
Merge branch 'master' into feat/connect-use-layout-vatiants-endpoint
maxiadlovskii May 18, 2026
ea4e6e1
remove reset buttons from layout variants and remove sorting from loc…
maxiadlovskii May 18, 2026
87c2363
Merge branch 'master' into feat/connect-use-layout-vatiants-endpoint
maxiadlovskii May 18, 2026
796cfb2
fix issue with state update loop
maxiadlovskii May 20, 2026
ed948a7
remove console.log
maxiadlovskii May 20, 2026
12c51c3
fix issue with updates
maxiadlovskii May 22, 2026
01c4010
Merge branch 'master' into feat/connect-use-layout-vatiants-endpoint
maxiadlovskii May 27, 2026
0ccf296
use server-api
maxiadlovskii May 27, 2026
37a8246
Merge branch 'master' into feat/connect-use-layout-vatiants-endpoint
maxiadlovskii May 27, 2026
9742112
add static timerange to get metrics
maxiadlovskii May 28, 2026
c631fa4
add temporary solution to fix type issue
maxiadlovskii May 28, 2026
1eb7d63
use normal keyword
maxiadlovskii May 28, 2026
ba1e727
Merge branch 'master' into feat/connect-use-layout-vatiants-endpoint
maxiadlovskii May 28, 2026
958fa09
move static timerange to alert ans events
maxiadlovskii May 28, 2026
bb945dd
Merge branch 'master' into feat/connect-use-layout-vatiants-endpoint
maxiadlovskii May 28, 2026
f8a6c0e
Merge branch 'master' into feat/connect-use-layout-vatiants-endpoint
maxiadlovskii May 29, 2026
9988fdc
Fixing issue with set state loop
linuspahl May 29, 2026
3b2ad92
remove no longer needed todo
maxiadlovskii May 29, 2026
821e93f
Merge branch 'master' into feat/connect-use-layout-vatiants-endpoint
maxiadlovskii May 29, 2026
3ee5755
remove unused eslint-disable
maxiadlovskii Jun 1, 2026
a33575a
Merge branch 'master' into feat/connect-use-layout-vatiants-endpoint
maxiadlovskii Jun 1, 2026
d608a05
fix issue with slicing
maxiadlovskii Jun 1, 2026
7224e9c
fix test
maxiadlovskii Jun 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import { useState, useLayoutEffect, useMemo } from 'react';
import isEqual from 'lodash/isEqual';

import {
DEFAULT_COL_MIN_WIDTH,
Expand Down Expand Up @@ -161,28 +162,19 @@ const useColumnWidths = <Entity extends EntityBase>({
staticColumnWidths,
});

const newColumnWidths = calculateColumnWidths({
actionsColMinWidth,
assignableWidth,
attributeColumnIds: columnIds,
attributeColumnRenderers: columnRenderersByAttribute,
bulkSelectColWidth,
staticColumnWidths,
headerMinWidths,
})

// eslint-disable-next-line react-hooks/set-state-in-effect
setColumnWidths(
calculateColumnWidths({
actionsColMinWidth,
assignableWidth,
attributeColumnIds: columnIds,
attributeColumnRenderers: columnRenderersByAttribute,
bulkSelectColWidth,
staticColumnWidths,
headerMinWidths,
}),
);
}, [
actionsColMinWidth,
bulkSelectColWidth,
columnRenderersByAttribute,
columnIds,
scrollContainerWidth,
columnWidthPreferences,
staticColumnWidths,
headerMinWidths,
]);
setColumnWidths((cur) => isEqual(cur, newColumnWidths) ? cur : newColumnWidths)
}, [actionsColMinWidth, bulkSelectColWidth, columnRenderersByAttribute, columnIds, scrollContainerWidth, columnWidthPreferences, staticColumnWidths, headerMinWidths]);

return columnWidths;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,58 @@ describe('useTableLayout hook', () => {
});
});

it('should provide default slicing preferences when there are no user slicing preferences', async () => {
const defaultSlicing = { sliceColumn: 'status', sortBy: 'risk_score', order: 'desc' as const };

asMock(useUserLayoutPreferences).mockReturnValue({ data: undefined, isInitialLoading: false, refetch: () => {} });

const { result } = renderHook(
() =>
useTableLayout({
entityTableId: 'streams',
...defaultLayout,
defaultSlicing,
}),
{ wrapper },
);

expect(result.current.layoutConfig).toEqual({
attributes: undefined,
order: undefined,
sort: defaultLayout.defaultSort,
pageSize: defaultLayout.defaultPageSize,
slicing: defaultSlicing,
});
});

it('should allow user preferences to disable default slicing', async () => {
const defaultSlicing = { sliceColumn: 'status', sortBy: 'risk_score', order: 'desc' as const };

asMock(useUserLayoutPreferences).mockReturnValue({
data: { slicing: null },
isInitialLoading: false,
refetch: () => {},
});

const { result } = renderHook(
() =>
useTableLayout({
entityTableId: 'streams',
...defaultLayout,
defaultSlicing,
}),
{ wrapper },
);

expect(result.current.layoutConfig).toEqual({
attributes: undefined,
order: undefined,
sort: defaultLayout.defaultSort,
pageSize: defaultLayout.defaultPageSize,
slicing: undefined,
});
});

it('should merge user preferences with defaults', async () => {
asMock(useUserLayoutPreferences).mockReturnValue({
data: { perPage: layoutPreferences.perPage },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,35 +32,28 @@ const useTableLayout = ({
entityTableId,
layoutVariant,
defaultSort,
defaultSlicing,
defaultPageSize,
}: DefaultLayout): {
isInitialLoading: boolean;
layoutConfig: LayoutConfig;
} => {
const { data: userLayoutPreferences = {}, isInitialLoading } = useUserLayoutPreferences(entityTableId, layoutVariant);

return useMemo(
() => ({
return useMemo(() => {
const hasSlicingPreference = Object.prototype.hasOwnProperty.call(userLayoutPreferences, 'slicing');

return {
layoutConfig: {
attributes: userLayoutPreferences?.attributes,
order: userLayoutPreferences.order,
pageSize: userLayoutPreferences.perPage ?? defaultPageSize,
slicing: userLayoutPreferences.slicing,
slicing: hasSlicingPreference ? (userLayoutPreferences.slicing ?? undefined) : defaultSlicing,
sort: userLayoutPreferences.sort ?? defaultSort,
},
isInitialLoading,
}),
[
defaultPageSize,
defaultSort,
isInitialLoading,
userLayoutPreferences?.attributes,
userLayoutPreferences.order,
userLayoutPreferences.perPage,
userLayoutPreferences.slicing,
userLayoutPreferences.sort,
],
);
};
}, [defaultPageSize, defaultSlicing, defaultSort, isInitialLoading, userLayoutPreferences]);
};

export default useTableLayout;
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,22 @@ describe('useUserSearchFilterQuery hook', () => {
);
});

it('should update null slicing preferences', async () => {
const { result } = renderHook(() => useUpdateUserLayoutPreferences('streams'), { wrapper });

result.current.mutateAsync({
...layoutPreferences,
slicing: null,
});

await waitFor(() =>
expect(fetch).toHaveBeenCalledWith('POST', expect.stringContaining('/entitylists/preferences/streams'), {
...layoutPreferencesJSON,
slicing: null,
}),
);
});

it('should update user layout preferences for a layout variant', async () => {
const { result } = renderHook(() => useUpdateUserLayoutPreferences('streams', 'security-events'), { wrapper });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,31 @@ import { useMutation } from '@tanstack/react-query';

import fetch from 'logic/rest/FetchProvider';
import { qualifyUrl } from 'util/URLUtils';
import type { TableLayoutPreferences, TableLayoutPreferencesJSON } from 'components/common/EntityDataTable/types';
import type {
SlicingPreferences,
SlicingPreferencesJSON,
TableLayoutPreferences,
TableLayoutPreferencesJSON,
} from 'components/common/EntityDataTable/types';
import UserNotification from 'util/UserNotification';
import useUserLayoutPreferences from 'components/common/EntityDataTable/hooks/useUserLayoutPreferences';

const slicingToJSON = (slicing?: SlicingPreferences | null): SlicingPreferencesJSON | null | undefined => {
if (slicing === null) {
return null;
}

if (!slicing) {
return undefined;
}

return {
slice_column: slicing.sliceColumn,
sort_by: slicing.sortBy,
order: slicing.order,
};
};

const preferencesToJSON = <T>({
attributes,
sort,
Expand All @@ -33,13 +54,7 @@ const preferencesToJSON = <T>({
attributes,
sort: sort ? { order: sort.direction, field: sort.attributeId } : undefined,
per_page: perPage,
slicing: slicing
? {
slice_column: slicing.sliceColumn,
sort_by: slicing.sortBy,
order: slicing.order,
}
: undefined,
slicing: slicingToJSON(slicing),
custom_preferences: customPreferences,
order,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,24 @@ describe('useUserSearchFilterQuery hook', () => {
});
});

it('should return null slicing preferences', async () => {
asMock(fetch).mockImplementation(() =>
Promise.resolve({
...layoutPreferencesJSON,
slicing: null,
}),
);
const { result } = renderHook(() => useUserLayoutPreferences('streams'), { wrapper });

await waitFor(() => result.current.isInitialLoading);
await waitFor(() => !result.current.isInitialLoading);

expect(result.current.data).toEqual({
...layoutPreferences,
slicing: null,
});
});

it('should fetch layout preferences for a layout variant', async () => {
asMock(fetch).mockImplementation(() => Promise.resolve(layoutPreferencesJSON));
const { result } = renderHook(() => useUserLayoutPreferences('streams', 'security-events'), { wrapper });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,30 @@ import { defaultOnError } from 'util/conditional/onError';

const INITIAL_DATA = {};

const preferencesFromJSON = ({
attributes,
sort,
per_page,
slicing,
custom_preferences,
order,
}: TableLayoutPreferencesJSON): TableLayoutPreferences => ({
attributes,
sort: sort ? { attributeId: sort.field, direction: sort.order } : undefined,
perPage: per_page,
slicing: slicing
? {
sliceColumn: slicing.slice_column,
sortBy: slicing.sort_by,
order: slicing.order,
}
: undefined,
customPreferences: custom_preferences,
order,
});
const preferencesFromJSON = (preferences: TableLayoutPreferencesJSON): TableLayoutPreferences => {
const { attributes, sort, per_page, slicing, custom_preferences, order } = preferences;
const hasSlicingPreference = Object.prototype.hasOwnProperty.call(preferences, 'slicing');

const result: TableLayoutPreferences = {
attributes,
sort: sort ? { attributeId: sort.field, direction: sort.order } : undefined,
perPage: per_page,
customPreferences: custom_preferences,
order,
};

if (hasSlicingPreference) {
result.slicing = slicing
? {
sliceColumn: slicing.slice_column,
sortBy: slicing.sort_by,
order: slicing.order,
}
: null;
}

return result;
};
const preferencesUrl = (entityId: string, layoutVariant?: string) => {
const params = layoutVariant ? `?layout_variant=${encodeURIComponent(layoutVariant)}` : '';

Expand All @@ -57,7 +60,7 @@ const useUserLayoutPreferences = <T>(
entityId: string,
layoutVariant?: string,
): { data: TableLayoutPreferences<T>; isInitialLoading: boolean; refetch: () => void } => {
const { data, isInitialLoading, refetch } = useQuery({
const { data, isLoading: isInitialLoading, refetch } = useQuery({
queryKey: ['table-layout', entityId, layoutVariant],
queryFn: () =>
defaultOnError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export type TableLayoutPreferences<T = { [key: string]: unknown }> = {
sort?: Sort;
perPage?: number;
order?: Array<string>;
slicing?: SlicingPreferences;
slicing?: SlicingPreferences | null;
customPreferences?: T;
};

Expand All @@ -95,7 +95,7 @@ export type TableLayoutPreferencesJSON<T = { [key: string]: unknown }> = {
order: 'asc' | 'desc';
};
per_page?: number;
slicing?: SlicingPreferencesJSON;
slicing?: SlicingPreferencesJSON | null;
custom_preferences?: T;
order?: Array<string>;
};
Expand All @@ -117,6 +117,7 @@ export type DefaultLayout = {
entityTableId: string;
layoutVariant?: string;
defaultSort: Sort;
defaultSlicing?: SlicingPreferences;
defaultDisplayedAttributes: Array<string>;
defaultPageSize: number;
defaultColumnOrder: Array<string>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ const PaginatedEntityTableInner = <T extends EntityBase, M = unknown>({
}

updateTableLayout({
slicing: newSliceCol ? defaultSlicingPreferences(newSliceCol, columnSchemas) : undefined,
slicing: newSliceCol ? defaultSlicingPreferences(newSliceCol, columnSchemas) : null,
});
},
[columnSchemas, fetchOptions.sliceCol, onChangeSlicingFilter, updateTableLayout],
Expand Down Expand Up @@ -318,6 +318,7 @@ const PaginatedEntityTableInner = <T extends EntityBase, M = unknown>({
) : (
<EntityDataTable<T, M>
entities={list}
key={`${tableLayout?.entityTableId}${tableLayout?.layoutVariant}`}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would really like to avoid this, because it unmounts the complete table.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's rendered when we change the layout. Since the layout is likely different, it's not a big problem to rerender it. As we need to reset the inner state of the component (columns order and columns width), that is one of the React recommendations

defaultDisplayedColumns={tableLayout.defaultDisplayedAttributes}
layoutPreferences={{
attributes: layoutConfig.attributes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,9 @@ const useLayoutVariant = () => {
[activeLayoutVariant, setActiveLayout],
);

const resetLayoutVariant = useCallback(() => {
setActiveLayout(undefined);
}, [setActiveLayout]);

return {
activeLayoutVariant,
selectLayoutVariant,
resetLayoutVariant,
};
};

Expand Down
Loading
Loading