Skip to content

Commit 049dc28

Browse files
committed
feat: improve tree performance
Signed-off-by: Maxim Stykow <maxim.stykow@tngtech.com>
1 parent 4e557d9 commit 049dc28

File tree

18 files changed

+323
-405
lines changed

18 files changed

+323
-405
lines changed

src/Frontend/Components/CardList/CardList.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { LIST_CARD_HEIGHT } from '../ListCard/ListCard';
1010

1111
const MAX_NUMBER_OF_CARDS = 4;
1212

13-
export const CardList = styled(List<string>)(({ data }) => {
13+
export const CardList = styled(List)(({ data }) => {
1414
const height =
1515
Math.min(MAX_NUMBER_OF_CARDS, data?.length ?? 0) * (LIST_CARD_HEIGHT + 1) +
1616
1;

src/Frontend/Components/GroupedList/GroupedList.tsx

+7-7
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,18 @@ import { LoadingMask } from '../LoadingMask/LoadingMask';
2323
import { NoResults } from '../NoResults/NoResults';
2424
import { GroupContainer, StyledLinearProgress } from './GroupedList.style';
2525

26-
export interface GroupedListProps<D> {
26+
export interface GroupedListProps {
2727
className?: string;
28-
grouped: Record<string, ReadonlyArray<D>> | null;
28+
grouped: Record<string, ReadonlyArray<string>> | null;
2929
loading?: boolean;
3030
renderGroupName?: (key: string) => React.ReactNode;
31-
renderItemContent: (datum: D, index: number) => React.ReactNode;
32-
selected?: D;
31+
renderItemContent: (datum: string, index: number) => React.ReactNode;
32+
selected?: string;
3333
sx?: SxProps;
3434
testId?: string;
3535
}
3636

37-
export function GroupedList<D>({
37+
export function GroupedList({
3838
className,
3939
grouped,
4040
loading,
@@ -44,7 +44,7 @@ export function GroupedList<D>({
4444
sx,
4545
testId,
4646
...props
47-
}: GroupedListProps<D> & Omit<GroupedVirtuosoProps<D, unknown>, 'selected'>) {
47+
}: GroupedListProps & Omit<GroupedVirtuosoProps<string, unknown>, 'selected'>) {
4848
const ref = useRef<GroupedVirtuosoHandle>(null);
4949
const [{ startIndex, endIndex }, setRange] = useState<{
5050
startIndex: number;
@@ -62,7 +62,7 @@ export function GroupedList<D>({
6262
ids: flattened,
6363
keys: Object.keys(grouped),
6464
counts: Object.values(grouped).map((group) => group.length),
65-
selectedIndex: flattened.findIndex((id) => id === selected),
65+
selectedIndex: flattened.findIndex((datum) => datum === selected),
6666
};
6767
}, [grouped, selected]);
6868

src/Frontend/Components/List/List.tsx

+8-8
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,25 @@
33
//
44
// SPDX-License-Identifier: Apache-2.0
55
import { SxProps } from '@mui/system';
6-
import { defer, isEqual } from 'lodash';
6+
import { defer } from 'lodash';
77
import { useEffect, useMemo, useRef } from 'react';
88
import { Virtuoso, VirtuosoHandle, VirtuosoProps } from 'react-virtuoso';
99

1010
import { LoadingMask } from '../LoadingMask/LoadingMask';
1111
import { NoResults } from '../NoResults/NoResults';
1212
import { StyledLinearProgress } from './List.style';
1313

14-
export interface ListProps<D> {
14+
export interface ListProps {
1515
className?: string;
16-
data: ReadonlyArray<D> | null;
16+
data: ReadonlyArray<string> | null;
1717
loading?: boolean;
18-
renderItemContent: (datum: D, index: number) => React.ReactNode;
19-
selected?: D;
18+
renderItemContent: (datum: string, index: number) => React.ReactNode;
19+
selected?: string;
2020
sx?: SxProps;
2121
testId?: string;
2222
}
2323

24-
export function List<D>({
24+
export function List({
2525
className,
2626
data,
2727
loading,
@@ -30,15 +30,15 @@ export function List<D>({
3030
sx,
3131
testId,
3232
...props
33-
}: ListProps<D> & Omit<VirtuosoProps<D, unknown>, 'data' | 'selected'>) {
33+
}: ListProps & Omit<VirtuosoProps<string, unknown>, 'data' | 'selected'>) {
3434
const ref = useRef<VirtuosoHandle>(null);
3535

3636
const selectedIndex = useMemo(() => {
3737
if (!data) {
3838
return undefined;
3939
}
4040

41-
return data.findIndex((datum) => isEqual(datum, selected));
41+
return data.findIndex((datum) => datum === selected);
4242
}, [data, selected]);
4343

4444
useEffect(() => {

src/Frontend/Components/ResourceBrowser/LinkedResourcesTree/GeneralTreeItemLabel/GeneralTreeItemLabel.tsx

-46
This file was deleted.

src/Frontend/Components/ResourceBrowser/LinkedResourcesTree/GeneralTreeItemLabel/__tests__/GeneralTreeItemLabel.test.tsx

-56
This file was deleted.

src/Frontend/Components/ResourceBrowser/LinkedResourcesTree/LinkedResourcesTree.tsx

+3-17
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,13 @@ import { SxProps } from '@mui/system';
66
import { remove } from 'lodash';
77
import { useCallback, useEffect, useState } from 'react';
88

9-
import { ROOT_PATH } from '../../../shared-constants';
109
import { OpossumColors } from '../../../shared-styles';
1110
import { navigateToSelectedPathOrOpenUnsavedPopup } from '../../../state/actions/popup-actions/popup-actions';
1211
import { getInitialExpandedIds } from '../../../state/helpers/resources-helpers';
1312
import { useAppDispatch, useAppSelector } from '../../../state/hooks';
14-
import {
15-
getAttributionBreakpoints,
16-
getFilesWithChildren,
17-
getSelectedResourceId,
18-
} from '../../../state/selectors/resource-selectors';
13+
import { getSelectedResourceId } from '../../../state/selectors/resource-selectors';
1914
import { VirtualizedTree } from '../../VirtualizedTree/VirtualizedTree';
20-
import { GeneralTreeItemLabel } from './GeneralTreeItemLabel/GeneralTreeItemLabel';
15+
import { LinkedResourcesTreeNode } from './LinkedResourcesTreeNode/LinkedResourcesTreeNode';
2116

2217
interface Props {
2318
disableHighlightSelected?: boolean;
@@ -33,8 +28,6 @@ export function LinkedResourcesTree({
3328
sx,
3429
}: Props) {
3530
const dispatch = useAppDispatch();
36-
const filesWithChildren = useAppSelector(getFilesWithChildren);
37-
const attributionBreakpoints = useAppSelector(getAttributionBreakpoints);
3831
const selectedResourceId = useAppSelector(getSelectedResourceId);
3932

4033
const [expandedIds, setExpandedIds] = useState<Array<string>>([]);
@@ -80,14 +73,7 @@ export function LinkedResourcesTree({
8073
resourceIds={resourceIds}
8174
selectedNodeId={disableHighlightSelected ? '' : selectedResourceId}
8275
readOnly={readOnly}
83-
getTreeNodeLabel={({ node, nodeId, nodeName }) => (
84-
<GeneralTreeItemLabel
85-
labelText={nodeName || ROOT_PATH}
86-
canHaveChildren={node !== 1}
87-
isAttributionBreakpoint={attributionBreakpoints.has(nodeId)}
88-
showFolderIcon={node !== 1 && !filesWithChildren.has(nodeId)}
89-
/>
90-
)}
76+
TreeNodeLabel={LinkedResourcesTreeNode}
9177
testId={'linked-resources-tree'}
9278
/>
9379
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
2+
// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
3+
//
4+
// SPDX-License-Identifier: Apache-2.0
5+
import MuiBox from '@mui/material/Box';
6+
import MuiTypography from '@mui/material/Typography';
7+
import { ReactElement } from 'react';
8+
9+
import { ROOT_PATH } from '../../../../shared-constants';
10+
import { treeItemClasses } from '../../../../shared-styles';
11+
import { useAppSelector } from '../../../../state/hooks';
12+
import {
13+
getAttributionBreakpoints,
14+
getFilesWithChildren,
15+
} from '../../../../state/selectors/resource-selectors';
16+
import { BreakpointIcon, DirectoryIcon, FileIcon } from '../../../Icons/Icons';
17+
import { TreeNode } from '../../../VirtualizedTree/VirtualizedTreeNode/VirtualizedTreeNode';
18+
19+
const labelDetail = 'without information';
20+
21+
export function LinkedResourcesTreeNode({
22+
node,
23+
nodeId,
24+
nodeName,
25+
}: TreeNode): ReactElement {
26+
const attributionBreakpoints = useAppSelector(getAttributionBreakpoints);
27+
const filesWithChildren = useAppSelector(getFilesWithChildren);
28+
29+
const isAttributionBreakpoint = attributionBreakpoints.has(nodeId);
30+
const showFolderIcon = node !== 1 && !filesWithChildren.has(nodeId);
31+
const labelText = nodeName || ROOT_PATH;
32+
33+
return (
34+
<MuiBox sx={treeItemClasses.labelRoot}>
35+
{showFolderIcon ? (
36+
isAttributionBreakpoint ? (
37+
<BreakpointIcon />
38+
) : (
39+
<DirectoryIcon
40+
sx={treeItemClasses.resourceWithoutInformation}
41+
labelDetail={labelDetail}
42+
/>
43+
)
44+
) : (
45+
<FileIcon
46+
sx={treeItemClasses.resourceWithoutInformation}
47+
labelDetail={labelDetail}
48+
/>
49+
)}
50+
<MuiTypography
51+
sx={{
52+
...treeItemClasses.text,
53+
...(isAttributionBreakpoint && treeItemClasses.breakpoint),
54+
}}
55+
>
56+
{labelText}
57+
</MuiTypography>
58+
</MuiBox>
59+
);
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
2+
// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
3+
//
4+
// SPDX-License-Identifier: Apache-2.0
5+
import { screen } from '@testing-library/react';
6+
7+
import { faker } from '../../../../../../testing/Faker';
8+
import { setAttributionBreakpoints } from '../../../../../state/actions/resource-actions/all-views-simple-actions';
9+
import { renderComponent } from '../../../../../test-helpers/render';
10+
import { LinkedResourcesTreeNode } from '../LinkedResourcesTreeNode';
11+
12+
describe('LinkedResourcesTreeNode', () => {
13+
it('renders a file without information', () => {
14+
renderComponent(
15+
<LinkedResourcesTreeNode
16+
nodeName={'Test label'}
17+
node={1}
18+
nodeId={faker.system.filePath()}
19+
/>,
20+
);
21+
22+
expect(screen.getByText('Test label')).toBeInTheDocument();
23+
expect(screen.queryByLabelText('Attribution icon')).not.toBeInTheDocument();
24+
expect(
25+
screen.getByLabelText('File icon without information'),
26+
).toBeInTheDocument();
27+
});
28+
29+
it('renders a folder without information', () => {
30+
renderComponent(
31+
<LinkedResourcesTreeNode
32+
nodeName={'Test label'}
33+
node={faker.opossum.resources()}
34+
nodeId={faker.system.filePath()}
35+
/>,
36+
);
37+
38+
expect(screen.getByText('Test label')).toBeInTheDocument();
39+
expect(
40+
screen.getByLabelText('Directory icon without information'),
41+
).toBeInTheDocument();
42+
});
43+
44+
it('renders a breakpoint', () => {
45+
const nodeId = faker.system.filePath();
46+
renderComponent(
47+
<LinkedResourcesTreeNode
48+
nodeName={'Test label'}
49+
node={faker.opossum.resources()}
50+
nodeId={nodeId}
51+
/>,
52+
{ actions: [setAttributionBreakpoints(new Set([nodeId]))] },
53+
);
54+
55+
expect(screen.getByText('Test label')).toBeInTheDocument();
56+
expect(screen.getByLabelText('Breakpoint icon')).toBeInTheDocument();
57+
});
58+
});

0 commit comments

Comments
 (0)