Skip to content

Commit

Permalink
[4346] Improve the performance of the Query view with large sets of data
Browse files Browse the repository at this point in the history
Bug: #4346
Signed-off-by: Stéphane Bégaudeau <[email protected]>
  • Loading branch information
sbegaudeau committed Jan 6, 2025
1 parent 979953f commit 9a03eca
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 25 deletions.
46 changes: 46 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"react-router-dom": "6.26.0",
"react-window": "1.8.11",
"@xyflow/react": "12.2.1",
"tss-react": "4.9.7",
"xstate": "4.32.1"
Expand Down Expand Up @@ -116,6 +117,7 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"react-router-dom": "6.26.0",
"react-window": "1.8.11",
"rollup-plugin-peer-deps-external": "2.2.4",
"xstate": "4.32.1",
"typescript": "5.4.5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { SxProps, Theme, useTheme } from '@mui/material/styles';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import { ComponentType, useState } from 'react';
import { FixedSizeList, ListChildComponentProps } from 'react-window';
import {
ExpressionAreaProps,
ExpressionAreaState,
Expand All @@ -37,23 +38,25 @@ import {
GQLObjectsExpressionResult,
GQLStringExpressionResult,
} from './useEvaluateExpression.types';
import { useResultAreaSize } from './useResultAreaSize';

export const QueryView = ({ editingContextId, readOnly }: WorkbenchViewComponentProps) => {
const interpreterStyle: SxProps<Theme> = (theme) => ({
const queryViewStyle: SxProps<Theme> = (theme) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
paddingX: theme.spacing(1),
});

const { evaluateExpression, loading, result } = useEvaluateExpression();

const handleEvaluateExpression = (expression: string) => evaluateExpression(editingContextId, expression);

const { ref, width, height } = useResultAreaSize();

return (
<Box data-representation-kind="interpreter" sx={interpreterStyle}>
<Box data-representation-kind="query" sx={queryViewStyle} ref={ref}>
<ExpressionArea onEvaluateExpression={handleEvaluateExpression} disabled={loading || readOnly} />
<ResultArea loading={loading} payload={result} />
<ResultArea loading={loading} payload={result} width={width} height={height} />
</Box>
);
};
Expand Down Expand Up @@ -84,7 +87,7 @@ const ExpressionArea = ({ onEvaluateExpression, disabled }: ExpressionAreaProps)
};

return (
<div>
<div data-role="expression">
<Box sx={expressionAreaToolbarStyle}>
<Typography variant="subtitle2">Expression</Typography>

Expand Down Expand Up @@ -143,35 +146,51 @@ const ObjectExpressionResultViewer = ({ result }: ExpressionResultViewerProps) =
);
};

const ObjectsExpressionResultViewer = ({ result }: ExpressionResultViewerProps) => {
const ObjectRow = ({ data, index, style }: ListChildComponentProps) => {
const listItemStyle: SxProps<Theme> = (theme) => ({
gap: theme.spacing(2),
});
const listItemIconStyle: SxProps<Theme> = () => ({
minWidth: '0px',
});

const object = data[index];

return (
<ListItem key={index} style={style} sx={listItemStyle}>
<ListItemIcon sx={listItemIconStyle}>
<IconOverlay iconURL={object.iconURLs} alt="Icon of the object" />
</ListItemIcon>
<ListItemText primary={object.label} />
</ListItem>
);
};

const ObjectsExpressionResultViewer = ({ result, width, height }: ExpressionResultViewerProps) => {
if (result.__typename !== 'ObjectsExpressionResult') {
return null;
}

const { objectsValue } = result as GQLObjectsExpressionResult;

const listItemStyle: SxProps<Theme> = (theme) => ({
gap: theme.spacing(2),
});
const listItemIconStyle: SxProps<Theme> = () => ({
minWidth: '0px',
const listStyle: SxProps<Theme> = (theme) => ({
border: `1px solid ${theme.palette.divider}`,
});

return (
<Box>
<Typography variant="body2" gutterBottom>
A collection of {objectsValue.length} object{objectsValue.length > 1 ? 's' : ''} has been returned
</Typography>
<List dense>
{objectsValue.map((object) => {
return (
<ListItem key={object.id} sx={listItemStyle}>
<ListItemIcon sx={listItemIconStyle}>
<IconOverlay iconURL={object.iconURLs} alt="Icon of the object" />
</ListItemIcon>
<ListItemText primary={object.label} />
</ListItem>
);
})}
<List dense disablePadding sx={listStyle}>
<FixedSizeList
height={height}
width={width}
itemData={objectsValue}
itemCount={objectsValue.length}
itemSize={34}>
{ObjectRow}
</FixedSizeList>
</List>
</Box>
);
Expand Down Expand Up @@ -276,7 +295,7 @@ const LoadingViewer = () => {
);
};

const ResultArea = ({ loading, payload }: ResultAreaProps) => {
const ResultArea = ({ loading, payload, width, height }: ResultAreaProps) => {
const resultAreaToolbarStyle: SxProps<Theme> = {
display: 'flex',
flexDirection: 'row',
Expand All @@ -295,12 +314,12 @@ const ResultArea = ({ loading, payload }: ResultAreaProps) => {
} else if (payload) {
const Viewer = resultType2viewer[payload.result.__typename];
if (Viewer) {
content = <Viewer result={payload.result} />;
content = <Viewer result={payload.result} width={width} height={height} />;
}
}

return (
<div>
<div data-role="result">
<Box sx={resultAreaToolbarStyle}>
<Box sx={titleAreaStyle}>
<Typography variant="subtitle2">Evaluation result</Typography>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ export interface ExpressionAreaState {
export interface ResultAreaProps {
loading: boolean;
payload: GQLEvaluateExpressionSuccessPayload | null;
height: number | null;
width: number | null;
}

export interface ExpressionResultViewerProps {
result: GQLExpressionResult;
height: number | null;
width: number | null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*******************************************************************************
* Copyright (c) 2025 Obeo.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/

import { useEffect, useRef, useState } from 'react';
import { UseResultAreaSizeState, UseResultAreaSizeValue } from './useResultAreaSize.types';

export const useResultAreaSize = (): UseResultAreaSizeValue => {
const [state, setState] = useState<UseResultAreaSizeState>({
height: null,
width: null,
});

const ref = useRef<HTMLDivElement | null>(null);

useEffect(() => {
if (!ref.current) {
return () => {};
}
const parentDiv = ref.current;
const expressionDiv: HTMLDivElement | null = parentDiv.querySelector('[data-role="expression"]');
const resultDiv: HTMLDivElement | null = parentDiv.querySelector('[data-role="result"]');

const resizeObserver = new ResizeObserver(() => {
if (expressionDiv && resultDiv) {
const height = (parentDiv.offsetHeight - expressionDiv.offsetHeight) * 0.85;
const width = expressionDiv.offsetWidth;

setState((prevState) => ({
...prevState,
height,
width,
}));
}
});

resizeObserver.observe(ref.current);

return () => resizeObserver.disconnect();
}, [ref.current]);

return {
ref,
height: state.height,
width: state.width,
};
};
Loading

0 comments on commit 9a03eca

Please sign in to comment.