Use custom grid element instead of VSCodeDataGrid

This commit is contained in:
Robert
2023-10-17 15:00:01 +01:00
parent a7f8019bf4
commit 54db867d15
8 changed files with 285 additions and 156 deletions

View File

@@ -5,7 +5,6 @@ import { Meta, StoryFn } from "@storybook/react";
import { MethodRow as MethodRowComponent } from "../../view/model-editor/MethodRow";
import { CallClassification, Method } from "../../model-editor/method";
import { ModeledMethod } from "../../model-editor/modeled-method";
import { VSCodeDataGrid } from "@vscode/webview-ui-toolkit/react";
import {
MULTIPLE_MODELS_GRID_TEMPLATE_COLUMNS,
SINGLE_MODEL_GRID_TEMPLATE_COLUMNS,
@@ -13,6 +12,7 @@ import {
import { ModelEditorViewState } from "../../model-editor/shared/view-state";
import { createMockExtensionPack } from "../../../test/factories/model-editor/extension-pack";
import { Mode } from "../../model-editor/shared/mode";
import { DataGrid } from "../../view/common/DataGrid";
export default {
title: "CodeQL Model Editor/Method Row",
@@ -24,9 +24,9 @@ const Template: StoryFn<typeof MethodRowComponent> = (args) => {
? MULTIPLE_MODELS_GRID_TEMPLATE_COLUMNS
: SINGLE_MODEL_GRID_TEMPLATE_COLUMNS;
return (
<VSCodeDataGrid gridTemplateColumns={gridTemplateColumns}>
<DataGrid gridTemplateColumns={gridTemplateColumns}>
<MethodRowComponent {...args} />
</VSCodeDataGrid>
</DataGrid>
);
};

View File

@@ -0,0 +1,79 @@
import * as React from "react";
import { styled } from "styled-components";
/*
* A drop-in replacement for the VSCodeDataGrid family of components.
*
* The difference is that the `display: grid` styling is applied to `DataGrid`, whereas
* in the VS Code version that styling is applied to `VSCodeDataGridRow`. This gives
* column alignment across rows in situation with dynamic contents. It also allows
* for cells to span multiple rows and all the other features of data grids.
*/
const StyledDataGrid = styled.div<{ $gridTemplateColumns: string | number }>`
display: grid;
grid-template-columns: ${(props) => props.$gridTemplateColumns};
padding: calc((var(--design-unit) / 4) * 1px) 0;
box-sizing: border-box;
width: 100%;
background: transparent;
`;
interface DataGridProps {
gridTemplateColumns: string | number;
children: React.ReactNode;
}
export function DataGrid(props: DataGridProps) {
const { gridTemplateColumns, children } = props;
return (
<StyledDataGrid
aria-label="DataGrid"
$gridTemplateColumns={gridTemplateColumns}
>
{children}
</StyledDataGrid>
);
}
export const DataGridRow = styled.div<{ $focused?: boolean }>`
display: contents;
&:hover > * {
background-color: var(--list-hover-background);
}
& > * {
// Use !important to override the background color set by the hover state
background-color: ${(props) =>
props.$focused
? "var(--vscode-editor-selectionBackground) !important"
: "inherit"};
}
`;
const StyledDataGridCell = styled.div<{
$gridRow: string | number;
$gridColumn: string | number;
}>`
grid-row: ${(props) => props.$gridRow};
grid-column: ${(props) => props.$gridColumn};
padding: calc(var(--design-unit) * 1px) calc(var(--design-unit) * 3px);
`;
interface DataGridCellProps {
gridRow: string | number;
gridColumn: string | number;
children: React.ReactNode;
}
export function DataGridCell(props: DataGridCellProps) {
const { gridRow, gridColumn, children } = props;
return (
<StyledDataGridCell $gridRow={gridRow} $gridColumn={gridColumn}>
{children}
</StyledDataGridCell>
);
}

View File

@@ -1,35 +1,41 @@
import {
VSCodeDataGridCell,
VSCodeDataGridRow,
} from "@vscode/webview-ui-toolkit/react";
import * as React from "react";
import { styled } from "styled-components";
import { pluralize } from "../../common/word";
import { DataGridCell, DataGridRow } from "../common/DataGrid";
import { ModelEditorViewState } from "../../model-editor/shared/view-state";
const HiddenMethodsCell = styled(VSCodeDataGridCell)`
const HiddenMethodsText = styled.div`
text-align: center;
`;
interface Props {
gridRow: number;
numHiddenMethods: number;
someMethodsAreVisible: boolean;
viewState: ModelEditorViewState;
}
export function HiddenMethodsRow({
gridRow,
numHiddenMethods,
someMethodsAreVisible,
viewState,
}: Props) {
if (numHiddenMethods === 0) {
return null;
}
const gridColumn = viewState.showMultipleModels ? "span 6" : "span 5";
return (
<VSCodeDataGridRow>
<HiddenMethodsCell gridColumn="span 5">
{someMethodsAreVisible && "And "}
{pluralize(numHiddenMethods, "method", "methods")} modeled in other
CodeQL packs
</HiddenMethodsCell>
</VSCodeDataGridRow>
<DataGridRow>
<DataGridCell gridRow={gridRow} gridColumn={gridColumn}>
<HiddenMethodsText>
{someMethodsAreVisible && "And "}
{pluralize(numHiddenMethods, "method", "methods")} modeled in other
CodeQL packs
</HiddenMethodsText>
</DataGridCell>
</DataGridRow>
);
}

View File

@@ -4,6 +4,7 @@ import { Method } from "../../model-editor/method";
const Name = styled.span`
font-family: var(--vscode-editor-font-family);
word-break: break-all;
`;
export const MethodName = (method: Method): JSX.Element => {

View File

@@ -1,7 +1,5 @@
import {
VSCodeButton,
VSCodeDataGridCell,
VSCodeDataGridRow,
VSCodeLink,
VSCodeProgressRing,
} from "@vscode/webview-ui-toolkit/react";
@@ -25,8 +23,9 @@ import { ModelOutputDropdown } from "./ModelOutputDropdown";
import { ModelEditorViewState } from "../../model-editor/shared/view-state";
import { Codicon } from "../common";
import { canAddNewModeledMethod } from "../../model-editor/shared/multiple-modeled-methods";
import { DataGridCell, DataGridRow } from "../common/DataGrid";
const MultiModelColumn = styled(VSCodeDataGridCell)`
const MultiModelColumn = styled.div`
display: flex;
flex-direction: column;
gap: 0.5em;
@@ -63,12 +62,8 @@ const CodiconRow = styled(VSCodeButton)`
align-items: center;
`;
const DataGridRow = styled(VSCodeDataGridRow)<{ focused?: boolean }>`
outline: ${(props) =>
props.focused ? "1px solid var(--vscode-focusBorder)" : "none"};
`;
export type MethodRowProps = {
gridRow: number;
method: Method;
methodCanBeModeled: boolean;
modeledMethods: ModeledMethod[];
@@ -103,6 +98,7 @@ export const MethodRow = (props: MethodRowProps) => {
const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
(props, ref) => {
const {
gridRow,
method,
modeledMethods: modeledMethodsProp,
methodIsUnsaved,
@@ -166,9 +162,9 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
<DataGridRow
data-testid="modelable-method-row"
ref={ref}
focused={revealedMethodSignature === method.signature}
$focused={revealedMethodSignature === method.signature}
>
<VSCodeDataGridCell gridColumn={1}>
<DataGridCell gridRow={gridRow} gridColumn={1}>
<ApiOrMethodRow>
<ModelingStatusIndicator status={modelingStatus} />
<MethodClassifications method={method} />
@@ -181,97 +177,107 @@ const ModelableMethodRow = forwardRef<HTMLElement | undefined, MethodRowProps>(
<ViewLink onClick={jumpToMethod}>View</ViewLink>
{props.modelingInProgress && <ProgressRing />}
</ApiOrMethodRow>
</VSCodeDataGridCell>
</DataGridCell>
{props.modelingInProgress && (
<>
<VSCodeDataGridCell gridColumn={2}>
<DataGridCell gridRow={gridRow} gridColumn={2}>
<InProgressDropdown />
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={3}>
</DataGridCell>
<DataGridCell gridRow={gridRow} gridColumn={3}>
<InProgressDropdown />
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={4}>
</DataGridCell>
<DataGridCell gridRow={gridRow} gridColumn={4}>
<InProgressDropdown />
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={5}>
</DataGridCell>
<DataGridCell gridRow={gridRow} gridColumn={5}>
<InProgressDropdown />
</VSCodeDataGridCell>
</DataGridCell>
{viewState.showMultipleModels && (
<VSCodeDataGridCell gridColumn={6}>
<DataGridCell gridRow={gridRow} gridColumn={6}>
<CodiconRow appearance="icon" disabled={true}>
<Codicon name="add" label="Add new model" />
</CodiconRow>
</VSCodeDataGridCell>
</DataGridCell>
)}
</>
)}
{!props.modelingInProgress && (
<>
<MultiModelColumn gridColumn={2}>
{modeledMethods.map((modeledMethod, index) => (
<ModelTypeDropdown
key={index}
method={method}
modeledMethod={modeledMethod}
onChange={modeledMethodChangedHandlers[index]}
/>
))}
</MultiModelColumn>
<MultiModelColumn gridColumn={3}>
{modeledMethods.map((modeledMethod, index) => (
<ModelInputDropdown
key={index}
method={method}
modeledMethod={modeledMethod}
onChange={modeledMethodChangedHandlers[index]}
/>
))}
</MultiModelColumn>
<MultiModelColumn gridColumn={4}>
{modeledMethods.map((modeledMethod, index) => (
<ModelOutputDropdown
key={index}
method={method}
modeledMethod={modeledMethod}
onChange={modeledMethodChangedHandlers[index]}
/>
))}
</MultiModelColumn>
<MultiModelColumn gridColumn={5}>
{modeledMethods.map((modeledMethod, index) => (
<ModelKindDropdown
key={index}
method={method}
modeledMethod={modeledMethod}
onChange={modeledMethodChangedHandlers[index]}
/>
))}
</MultiModelColumn>
{viewState.showMultipleModels && (
<MultiModelColumn gridColumn={6}>
{modeledMethods.map((_, index) =>
index === modeledMethods.length - 1 ? (
<CodiconRow
key={index}
appearance="icon"
aria-label="Add new model"
onClick={handleAddModelClick}
disabled={addModelButtonDisabled}
>
<Codicon name="add" />
</CodiconRow>
) : (
<CodiconRow
key={index}
appearance="icon"
aria-label="Remove model"
onClick={removeModelClickedHandlers[index]}
>
<Codicon name="trash" />
</CodiconRow>
),
)}
<DataGridCell gridRow={gridRow} gridColumn={2}>
<MultiModelColumn>
{modeledMethods.map((modeledMethod, index) => (
<ModelTypeDropdown
key={index}
method={method}
modeledMethod={modeledMethod}
onChange={modeledMethodChangedHandlers[index]}
/>
))}
</MultiModelColumn>
</DataGridCell>
<DataGridCell gridRow={gridRow} gridColumn={3}>
<MultiModelColumn>
{modeledMethods.map((modeledMethod, index) => (
<ModelInputDropdown
key={index}
method={method}
modeledMethod={modeledMethod}
onChange={modeledMethodChangedHandlers[index]}
/>
))}
</MultiModelColumn>
</DataGridCell>
<DataGridCell gridRow={gridRow} gridColumn={4}>
<MultiModelColumn>
{modeledMethods.map((modeledMethod, index) => (
<ModelOutputDropdown
key={index}
method={method}
modeledMethod={modeledMethod}
onChange={modeledMethodChangedHandlers[index]}
/>
))}
</MultiModelColumn>
</DataGridCell>
<DataGridCell gridRow={gridRow} gridColumn={5}>
<MultiModelColumn>
{modeledMethods.map((modeledMethod, index) => (
<ModelKindDropdown
key={index}
method={method}
modeledMethod={modeledMethod}
onChange={modeledMethodChangedHandlers[index]}
/>
))}
</MultiModelColumn>
</DataGridCell>
{viewState.showMultipleModels && (
<DataGridCell gridRow={gridRow} gridColumn={6}>
<MultiModelColumn>
{modeledMethods.map((_, index) =>
index === modeledMethods.length - 1 ? (
<CodiconRow
key={index}
appearance="icon"
aria-label="Add new model"
onClick={handleAddModelClick}
disabled={addModelButtonDisabled}
>
<Codicon name="add" />
</CodiconRow>
) : (
<CodiconRow
key={index}
appearance="icon"
aria-label="Remove model"
onClick={removeModelClickedHandlers[index]}
>
<Codicon name="trash" />
</CodiconRow>
),
)}
</MultiModelColumn>
</DataGridCell>
)}
</>
)}
@@ -285,7 +291,7 @@ const UnmodelableMethodRow = forwardRef<
HTMLElement | undefined,
MethodRowProps
>((props, ref) => {
const { method, viewState, revealedMethodSignature } = props;
const { gridRow, method, viewState, revealedMethodSignature } = props;
const jumpToMethod = useCallback(
() => sendJumpToMethodMessage(method),
@@ -296,9 +302,9 @@ const UnmodelableMethodRow = forwardRef<
<DataGridRow
data-testid="unmodelable-method-row"
ref={ref}
focused={revealedMethodSignature === method.signature}
$focused={revealedMethodSignature === method.signature}
>
<VSCodeDataGridCell gridColumn={1}>
<DataGridCell gridRow={gridRow} gridColumn={1}>
<ApiOrMethodRow>
<ModelingStatusIndicator status="saved" />
<MethodName {...props.method} />
@@ -310,10 +316,10 @@ const UnmodelableMethodRow = forwardRef<
<ViewLink onClick={jumpToMethod}>View</ViewLink>
<MethodClassifications method={method} />
</ApiOrMethodRow>
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn="span 4">
</DataGridCell>
<DataGridCell gridRow={gridRow} gridColumn="span 4">
Method already modeled
</VSCodeDataGridCell>
</DataGridCell>
</DataGridRow>
);
});

View File

@@ -1,9 +1,4 @@
import * as React from "react";
import {
VSCodeDataGrid,
VSCodeDataGridCell,
VSCodeDataGridRow,
} from "@vscode/webview-ui-toolkit/react";
import { MethodRow } from "./MethodRow";
import { Method, canMethodBeModeled } from "../../model-editor/method";
import { ModeledMethod } from "../../model-editor/modeled-method";
@@ -12,6 +7,7 @@ import { sortMethods } from "../../model-editor/shared/sorting";
import { HiddenMethodsRow } from "./HiddenMethodsRow";
import { ModelEditorViewState } from "../../model-editor/shared/view-state";
import { ScreenReaderOnly } from "../common/ScreenReaderOnly";
import { DataGrid, DataGridCell } from "../common/DataGrid";
export const SINGLE_MODEL_GRID_TEMPLATE_COLUMNS =
"0.5fr 0.125fr 0.125fr 0.125fr 0.125fr";
@@ -72,53 +68,56 @@ export const ModeledMethodDataGrid = ({
: SINGLE_MODEL_GRID_TEMPLATE_COLUMNS;
return (
<VSCodeDataGrid gridTemplateColumns={gridTemplateColumns}>
<DataGrid gridTemplateColumns={gridTemplateColumns}>
{someMethodsAreVisible && (
<>
<VSCodeDataGridRow rowType="header">
<VSCodeDataGridCell cellType="columnheader" gridColumn={1}>
API or method
</VSCodeDataGridCell>
<VSCodeDataGridCell cellType="columnheader" gridColumn={2}>
Model type
</VSCodeDataGridCell>
<VSCodeDataGridCell cellType="columnheader" gridColumn={3}>
Input
</VSCodeDataGridCell>
<VSCodeDataGridCell cellType="columnheader" gridColumn={4}>
Output
</VSCodeDataGridCell>
<VSCodeDataGridCell cellType="columnheader" gridColumn={5}>
Kind
</VSCodeDataGridCell>
{viewState.showMultipleModels && (
<VSCodeDataGridCell cellType="columnheader" gridColumn={6}>
<ScreenReaderOnly>Add or remove models</ScreenReaderOnly>
</VSCodeDataGridCell>
)}
</VSCodeDataGridRow>
{methodsWithModelability.map(({ method, methodCanBeModeled }) => {
const modeledMethods = modeledMethodsMap[method.signature] ?? [];
return (
<MethodRow
key={method.signature}
method={method}
methodCanBeModeled={methodCanBeModeled}
modeledMethods={modeledMethods}
methodIsUnsaved={modifiedSignatures.has(method.signature)}
modelingInProgress={inProgressMethods.has(method.signature)}
viewState={viewState}
revealedMethodSignature={revealedMethodSignature}
onChange={onChange}
/>
);
})}
<DataGridCell gridRow={1} gridColumn={1}>
API or method
</DataGridCell>
<DataGridCell gridRow={1} gridColumn={2}>
Model type
</DataGridCell>
<DataGridCell gridRow={1} gridColumn={3}>
Input
</DataGridCell>
<DataGridCell gridRow={1} gridColumn={4}>
Output
</DataGridCell>
<DataGridCell gridRow={1} gridColumn={5}>
Kind
</DataGridCell>
{viewState.showMultipleModels && (
<DataGridCell gridRow={1} gridColumn={6}>
<ScreenReaderOnly>Add or remove models</ScreenReaderOnly>
</DataGridCell>
)}
{methodsWithModelability.map(
({ method, methodCanBeModeled }, index) => {
const modeledMethods = modeledMethodsMap[method.signature] ?? [];
return (
<MethodRow
key={method.signature}
gridRow={index + 2}
method={method}
methodCanBeModeled={methodCanBeModeled}
modeledMethods={modeledMethods}
methodIsUnsaved={modifiedSignatures.has(method.signature)}
modelingInProgress={inProgressMethods.has(method.signature)}
viewState={viewState}
revealedMethodSignature={revealedMethodSignature}
onChange={onChange}
/>
);
},
)}
</>
)}
<HiddenMethodsRow
gridRow={methodsWithModelability.length + 2}
numHiddenMethods={numHiddenMethods}
someMethodsAreVisible={someMethodsAreVisible}
viewState={viewState}
/>
</VSCodeDataGrid>
</DataGrid>
);
};

View File

@@ -1,11 +1,28 @@
import * as React from "react";
import { render, screen } from "@testing-library/react";
import { HiddenMethodsRow } from "../HiddenMethodsRow";
import { createMockExtensionPack } from "../../../../test/factories/model-editor/extension-pack";
import { ModelEditorViewState } from "../../../model-editor/shared/view-state";
import { Mode } from "../../../model-editor/shared/mode";
describe(HiddenMethodsRow.name, () => {
const viewState: ModelEditorViewState = {
mode: Mode.Application,
showFlowGeneration: false,
showLlmButton: false,
showMultipleModels: false,
extensionPack: createMockExtensionPack(),
sourceArchiveAvailable: true,
};
it("does not render with 0 hidden methods", () => {
const { container } = render(
<HiddenMethodsRow numHiddenMethods={0} someMethodsAreVisible={true} />,
<HiddenMethodsRow
gridRow={1}
numHiddenMethods={0}
someMethodsAreVisible={true}
viewState={viewState}
/>,
);
expect(container).toBeEmptyDOMElement();
@@ -13,7 +30,12 @@ describe(HiddenMethodsRow.name, () => {
it("renders with 1 hidden methods and no visible methods", () => {
render(
<HiddenMethodsRow numHiddenMethods={1} someMethodsAreVisible={false} />,
<HiddenMethodsRow
gridRow={1}
numHiddenMethods={1}
someMethodsAreVisible={false}
viewState={viewState}
/>,
);
expect(
@@ -23,7 +45,12 @@ describe(HiddenMethodsRow.name, () => {
it("renders with 1 hidden methods and visible methods", () => {
render(
<HiddenMethodsRow numHiddenMethods={1} someMethodsAreVisible={true} />,
<HiddenMethodsRow
gridRow={1}
numHiddenMethods={1}
someMethodsAreVisible={true}
viewState={viewState}
/>,
);
expect(
@@ -33,7 +60,12 @@ describe(HiddenMethodsRow.name, () => {
it("renders with 3 hidden methods and no visible methods", () => {
render(
<HiddenMethodsRow numHiddenMethods={3} someMethodsAreVisible={false} />,
<HiddenMethodsRow
gridRow={1}
numHiddenMethods={3}
someMethodsAreVisible={false}
viewState={viewState}
/>,
);
expect(
@@ -43,7 +75,12 @@ describe(HiddenMethodsRow.name, () => {
it("renders with 3 hidden methods and visible methods", () => {
render(
<HiddenMethodsRow numHiddenMethods={3} someMethodsAreVisible={true} />,
<HiddenMethodsRow
gridRow={1}
numHiddenMethods={3}
someMethodsAreVisible={true}
viewState={viewState}
/>,
);
expect(

View File

@@ -45,6 +45,7 @@ describe(MethodRow.name, () => {
const render = (props: Partial<MethodRowProps> = {}) =>
reactRender(
<MethodRow
gridRow={1}
method={method}
methodCanBeModeled={true}
modeledMethods={[modeledMethod]}