Merge pull request #2582 from github/robertbrignull/data-table

Update data extensions modelling table to new designs
This commit is contained in:
Robert
2023-07-06 16:27:06 +01:00
committed by GitHub
4 changed files with 170 additions and 154 deletions

View File

@@ -0,0 +1,49 @@
import * as React from "react";
import { ChangeEvent } from "react";
import styled from "styled-components";
const StyledDropdown = styled.select`
width: 100%;
height: calc(var(--input-height) * 1px);
background: var(--vscode-dropdown-background);
color: var(--vscode-foreground);
border: none;
padding: 2px 6px 2px 8px;
`;
type Props = {
value: string | undefined;
options: Array<{ value: string; label: string }>;
disabled?: boolean;
onChange: (event: ChangeEvent<HTMLSelectElement>) => void;
};
/**
* A dropdown implementation styled to look like `VSCodeDropdown`.
*
* The reason for doing this is that `VSCodeDropdown` doesn't handle fitting into
* available space and truncating content, and this leads to breaking the
* `VSCodeDataGrid` layout. This version using `select` directly will truncate the
* content as necessary and fit into whatever space is available.
* See https://github.com/github/vscode-codeql/pull/2582#issuecomment-1622164429
* for more info on the problem and other potential solutions.
*/
export function Dropdown({ value, options, disabled, onChange }: Props) {
return (
<StyledDropdown
value={disabled ? undefined : value}
disabled={disabled}
onChange={onChange}
>
{!disabled && (
<>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</>
)}
</StyledDropdown>
);
}

View File

@@ -1,24 +1,25 @@
import * as React from "react"; import * as React from "react";
import { useCallback, useEffect } from "react"; import { ChangeEvent, useCallback, useEffect, useMemo } from "react";
import styled from "styled-components";
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react";
import type { ModeledMethod } from "../../data-extensions-editor/modeled-method"; import type { ModeledMethod } from "../../data-extensions-editor/modeled-method";
import { Dropdown } from "../common/Dropdown";
const Dropdown = styled(VSCodeDropdown)`
width: 100%;
`;
type Props = { type Props = {
kinds: Array<ModeledMethod["kind"]>; kinds: Array<ModeledMethod["kind"]>;
value: ModeledMethod["kind"] | undefined; value: ModeledMethod["kind"] | undefined;
disabled?: boolean;
onChange: (value: ModeledMethod["kind"]) => void; onChange: (value: ModeledMethod["kind"]) => void;
}; };
export const KindInput = ({ kinds, value, onChange }: Props) => { export const KindInput = ({ kinds, value, disabled, onChange }: Props) => {
const options = useMemo(
() => kinds.map((kind) => ({ value: kind, label: kind })),
[kinds],
);
const handleInput = useCallback( const handleInput = useCallback(
(e: InputEvent) => { (e: ChangeEvent<HTMLSelectElement>) => {
const target = e.target as HTMLSelectElement; const target = e.target as HTMLSelectElement;
onChange(target.value as ModeledMethod["kind"]); onChange(target.value as ModeledMethod["kind"]);
@@ -37,12 +38,11 @@ export const KindInput = ({ kinds, value, onChange }: Props) => {
}, [value, kinds, onChange]); }, [value, kinds, onChange]);
return ( return (
<Dropdown value={value} onInput={handleInput}> <Dropdown
{kinds.map((kind) => ( value={value}
<VSCodeOption key={kind} value={kind}> options={options}
{kind} disabled={disabled}
</VSCodeOption> onChange={handleInput}
))} />
</Dropdown>
); );
}; };

View File

@@ -1,11 +1,11 @@
import { import {
VSCodeCheckbox,
VSCodeDataGridCell, VSCodeDataGridCell,
VSCodeDataGridRow, VSCodeDataGridRow,
VSCodeDropdown, VSCodeLink,
VSCodeOption,
} from "@vscode/webview-ui-toolkit/react"; } from "@vscode/webview-ui-toolkit/react";
import * as React from "react"; import * as React from "react";
import { useCallback, useMemo } from "react"; import { ChangeEvent, useCallback, useMemo } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { vscode } from "../vscode-api"; import { vscode } from "../vscode-api";
@@ -18,52 +18,27 @@ import {
import { KindInput } from "./KindInput"; import { KindInput } from "./KindInput";
import { extensiblePredicateDefinitions } from "../../data-extensions-editor/predicates"; import { extensiblePredicateDefinitions } from "../../data-extensions-editor/predicates";
import { Mode } from "../../data-extensions-editor/shared/mode"; import { Mode } from "../../data-extensions-editor/shared/mode";
import { Dropdown } from "../common/Dropdown";
const Dropdown = styled(VSCodeDropdown)` const ApiOrMethodCell = styled(VSCodeDataGridCell)`
width: 100%; display: flex;
`; flex-direction: row;
align-items: center;
type SupportedUnsupportedSpanProps = { gap: 0.5em;
supported: boolean;
modeled: ModeledMethod | undefined;
};
const SupportSpan = styled.span<SupportedUnsupportedSpanProps>`
color: ${(props) => {
if (!props.supported && props.modeled && props.modeled?.type !== "none") {
return "orange";
} else {
return props.supported ? "green" : "red";
}
}};
`;
type SupportedUnsupportedLinkProps = {
supported: boolean;
modeled: ModeledMethod | undefined;
};
const SupportLink = styled.button<SupportedUnsupportedLinkProps>`
color: ${(props) => {
if (!props.supported && props.modeled && props.modeled?.type !== "none") {
return "orange";
} else {
return props.supported ? "green" : "red";
}
}};
background-color: transparent;
border: none;
cursor: pointer;
padding: 0;
`; `;
const UsagesButton = styled.button` const UsagesButton = styled.button`
color: var(--vscode-editor-foreground); color: var(--vscode-editor-foreground);
background-color: transparent; background-color: var(--vscode-input-background);
border: none; border: none;
border-radius: 40%;
cursor: pointer; cursor: pointer;
`; `;
const ViewLink = styled(VSCodeLink)`
white-space: nowrap;
`;
type Props = { type Props = {
externalApiUsage: ExternalApiUsage; externalApiUsage: ExternalApiUsage;
modeledMethod: ModeledMethod | undefined; modeledMethod: ModeledMethod | undefined;
@@ -90,9 +65,7 @@ export const MethodRow = ({
}, [externalApiUsage.methodParameters]); }, [externalApiUsage.methodParameters]);
const handleTypeInput = useCallback( const handleTypeInput = useCallback(
(e: InputEvent) => { (e: ChangeEvent<HTMLSelectElement>) => {
const target = e.target as HTMLSelectElement;
let newProvenance: Provenance = "manual"; let newProvenance: Provenance = "manual";
if (modeledMethod?.provenance === "df-generated") { if (modeledMethod?.provenance === "df-generated") {
newProvenance = "df-manual"; newProvenance = "df-manual";
@@ -106,14 +79,14 @@ export const MethodRow = ({
output: "ReturnType", output: "ReturnType",
kind: "value", kind: "value",
...modeledMethod, ...modeledMethod,
type: target.value as ModeledMethodType, type: e.target.value as ModeledMethodType,
provenance: newProvenance, provenance: newProvenance,
}); });
}, },
[onChange, externalApiUsage, modeledMethod, argumentsList], [onChange, externalApiUsage, modeledMethod, argumentsList],
); );
const handleInputInput = useCallback( const handleInputInput = useCallback(
(e: InputEvent) => { (e: ChangeEvent<HTMLSelectElement>) => {
if (!modeledMethod) { if (!modeledMethod) {
return; return;
} }
@@ -128,7 +101,7 @@ export const MethodRow = ({
[onChange, externalApiUsage, modeledMethod], [onChange, externalApiUsage, modeledMethod],
); );
const handleOutputInput = useCallback( const handleOutputInput = useCallback(
(e: InputEvent) => { (e: ChangeEvent<HTMLSelectElement>) => {
if (!modeledMethod) { if (!modeledMethod) {
return; return;
} }
@@ -169,94 +142,96 @@ export const MethodRow = ({
? extensiblePredicateDefinitions[modeledMethod.type] ? extensiblePredicateDefinitions[modeledMethod.type]
: undefined; : undefined;
const showModelTypeCell =
!externalApiUsage.supported ||
(modeledMethod && modeledMethod?.type !== "none");
const modelTypeOptions = useMemo(
() => [
{ value: "none", label: "Unmodeled" },
{ value: "source", label: "Source" },
{ value: "sink", label: "Sink" },
{ value: "summary", label: "Flow summary" },
{ value: "neutral", label: "Neutral" },
],
[],
);
const showInputCell =
modeledMethod?.type && ["sink", "summary"].includes(modeledMethod?.type);
const inputOptions = useMemo(
() => [
{ value: "Argument[this]", label: "Argument[this]" },
...argumentsList.map((argument, index) => ({
value: `Argument[${index}]`,
label: `Argument[${index}]: ${argument}`,
})),
],
[argumentsList],
);
const showOutputCell =
modeledMethod?.type && ["source", "summary"].includes(modeledMethod?.type);
const outputOptions = useMemo(
() => [
{ value: "ReturnValue", label: "ReturnValue" },
{ value: "Argument[this]", label: "Argument[this]" },
...argumentsList.map((argument, index) => ({
value: `Argument[${index}]`,
label: `Argument[${index}]: ${argument}`,
})),
],
[argumentsList],
);
const showKindCell = predicate?.supportedKinds;
return ( return (
<VSCodeDataGridRow> <VSCodeDataGridRow>
<VSCodeDataGridCell gridColumn={1}> <ApiOrMethodCell gridColumn={1}>
<SupportSpan <VSCodeCheckbox />
supported={externalApiUsage.supported} <span>
modeled={modeledMethod} {externalApiUsage.packageName}.{externalApiUsage.typeName}.
> {externalApiUsage.methodName}
{externalApiUsage.packageName}.{externalApiUsage.typeName} {externalApiUsage.methodParameters}
</SupportSpan> </span>
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={2}>
{mode === Mode.Application && ( {mode === Mode.Application && (
<SupportSpan
supported={externalApiUsage.supported}
modeled={modeledMethod}
>
{externalApiUsage.methodName}
{externalApiUsage.methodParameters}
</SupportSpan>
)}
{mode === Mode.Framework && (
<SupportLink
supported={externalApiUsage.supported}
modeled={modeledMethod}
onClick={jumpToUsage}
>
{externalApiUsage.methodName}
{externalApiUsage.methodParameters}
</SupportLink>
)}
</VSCodeDataGridCell>
{mode === Mode.Application && (
<VSCodeDataGridCell gridColumn={3}>
<UsagesButton onClick={jumpToUsage}> <UsagesButton onClick={jumpToUsage}>
{externalApiUsage.usages.length} {externalApiUsage.usages.length}
</UsagesButton> </UsagesButton>
</VSCodeDataGridCell>
)}
<VSCodeDataGridCell gridColumn={4}>
{(!externalApiUsage.supported ||
(modeledMethod && modeledMethod?.type !== "none")) && (
<Dropdown
value={modeledMethod?.type ?? "none"}
onInput={handleTypeInput}
>
<VSCodeOption value="none">Unmodeled</VSCodeOption>
<VSCodeOption value="source">Source</VSCodeOption>
<VSCodeOption value="sink">Sink</VSCodeOption>
<VSCodeOption value="summary">Flow summary</VSCodeOption>
<VSCodeOption value="neutral">Neutral</VSCodeOption>
</Dropdown>
)} )}
<ViewLink onClick={jumpToUsage}>View</ViewLink>
</ApiOrMethodCell>
<VSCodeDataGridCell gridColumn={2}>
<Dropdown
value={modeledMethod?.type ?? "none"}
options={modelTypeOptions}
disabled={!showModelTypeCell}
onChange={handleTypeInput}
/>
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={3}>
<Dropdown
value={modeledMethod?.input}
options={inputOptions}
disabled={!showInputCell}
onChange={handleInputInput}
/>
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={4}>
<Dropdown
value={modeledMethod?.output}
options={outputOptions}
disabled={!showOutputCell}
onChange={handleOutputInput}
/>
</VSCodeDataGridCell> </VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={5}> <VSCodeDataGridCell gridColumn={5}>
{modeledMethod?.type && <KindInput
["sink", "summary"].includes(modeledMethod?.type) && ( kinds={predicate?.supportedKinds || []}
<Dropdown value={modeledMethod?.input} onInput={handleInputInput}> value={modeledMethod?.kind}
<VSCodeOption value="Argument[this]">Argument[this]</VSCodeOption> disabled={!showKindCell}
{argumentsList.map((argument, index) => ( onChange={handleKindChange}
<VSCodeOption key={argument} value={`Argument[${index}]`}> />
Argument[{index}]: {argument}
</VSCodeOption>
))}
</Dropdown>
)}
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={6}>
{modeledMethod?.type &&
["source", "summary"].includes(modeledMethod?.type) && (
<Dropdown value={modeledMethod?.output} onInput={handleOutputInput}>
<VSCodeOption value="ReturnValue">ReturnValue</VSCodeOption>
<VSCodeOption value="Argument[this]">Argument[this]</VSCodeOption>
{argumentsList.map((argument, index) => (
<VSCodeOption key={argument} value={`Argument[${index}]`}>
Argument[{index}]: {argument}
</VSCodeOption>
))}
</Dropdown>
)}
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={7}>
{predicate?.supportedKinds && (
<KindInput
kinds={predicate.supportedKinds}
value={modeledMethod?.kind}
onChange={handleKindChange}
/>
)}
</VSCodeDataGridCell> </VSCodeDataGridCell>
</VSCodeDataGridRow> </VSCodeDataGridRow>
); );

View File

@@ -33,29 +33,21 @@ export const ModeledMethodDataGrid = ({
); );
return ( return (
<VSCodeDataGrid> <VSCodeDataGrid gridTemplateColumns="0.4fr 0.15fr 0.15fr 0.15fr 0.15fr">
<VSCodeDataGridRow rowType="header"> <VSCodeDataGridRow rowType="header">
<VSCodeDataGridCell cellType="columnheader" gridColumn={1}> <VSCodeDataGridCell cellType="columnheader" gridColumn={1}>
Type API or method
</VSCodeDataGridCell> </VSCodeDataGridCell>
<VSCodeDataGridCell cellType="columnheader" gridColumn={2}> <VSCodeDataGridCell cellType="columnheader" gridColumn={2}>
Method
</VSCodeDataGridCell>
{mode === Mode.Application && (
<VSCodeDataGridCell cellType="columnheader" gridColumn={3}>
Usages
</VSCodeDataGridCell>
)}
<VSCodeDataGridCell cellType="columnheader" gridColumn={4}>
Model type Model type
</VSCodeDataGridCell> </VSCodeDataGridCell>
<VSCodeDataGridCell cellType="columnheader" gridColumn={5}> <VSCodeDataGridCell cellType="columnheader" gridColumn={3}>
Input Input
</VSCodeDataGridCell> </VSCodeDataGridCell>
<VSCodeDataGridCell cellType="columnheader" gridColumn={6}> <VSCodeDataGridCell cellType="columnheader" gridColumn={4}>
Output Output
</VSCodeDataGridCell> </VSCodeDataGridCell>
<VSCodeDataGridCell cellType="columnheader" gridColumn={7}> <VSCodeDataGridCell cellType="columnheader" gridColumn={5}>
Kind Kind
</VSCodeDataGridCell> </VSCodeDataGridCell>
</VSCodeDataGridRow> </VSCodeDataGridRow>