Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 49 additions & 0 deletions extensions/ql-vscode/src/view/common/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import * as React from "react";
import { useCallback, useEffect } from "react";
import styled from "styled-components";
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react";
import { ChangeEvent, useCallback, useEffect, useMemo } from "react";

import type { ModeledMethod } from "../../data-extensions-editor/modeled-method";

const Dropdown = styled(VSCodeDropdown)`
width: 100%;
`;
import { Dropdown } from "../common/Dropdown";

type Props = {
kinds: Array<ModeledMethod["kind"]>;

value: ModeledMethod["kind"] | undefined;
disabled?: boolean;
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(
(e: InputEvent) => {
(e: ChangeEvent<HTMLSelectElement>) => {
const target = e.target as HTMLSelectElement;

onChange(target.value as ModeledMethod["kind"]);
Expand All @@ -37,12 +38,11 @@ export const KindInput = ({ kinds, value, onChange }: Props) => {
}, [value, kinds, onChange]);

return (
<Dropdown value={value} onInput={handleInput}>
{kinds.map((kind) => (
<VSCodeOption key={kind} value={kind}>
{kind}
</VSCodeOption>
))}
</Dropdown>
<Dropdown
value={value}
options={options}
disabled={disabled}
onChange={handleInput}
/>
);
};
223 changes: 99 additions & 124 deletions extensions/ql-vscode/src/view/data-extensions-editor/MethodRow.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
VSCodeCheckbox,
VSCodeDataGridCell,
VSCodeDataGridRow,
VSCodeDropdown,
VSCodeOption,
VSCodeLink,
} from "@vscode/webview-ui-toolkit/react";
import * as React from "react";
import { useCallback, useMemo } from "react";
import { ChangeEvent, useCallback, useMemo } from "react";
import styled from "styled-components";
import { vscode } from "../vscode-api";

Expand All @@ -18,52 +18,27 @@ import {
import { KindInput } from "./KindInput";
import { extensiblePredicateDefinitions } from "../../data-extensions-editor/predicates";
import { Mode } from "../../data-extensions-editor/shared/mode";
import { Dropdown } from "../common/Dropdown";

const Dropdown = styled(VSCodeDropdown)`
width: 100%;
`;

type SupportedUnsupportedSpanProps = {
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 ApiOrMethodCell = styled(VSCodeDataGridCell)`
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5em;
`;

const UsagesButton = styled.button`
color: var(--vscode-editor-foreground);
background-color: transparent;
background-color: var(--vscode-input-background);
border: none;
border-radius: 40%;
cursor: pointer;
`;

const ViewLink = styled(VSCodeLink)`
white-space: nowrap;
`;

type Props = {
externalApiUsage: ExternalApiUsage;
modeledMethod: ModeledMethod | undefined;
Expand All @@ -90,9 +65,7 @@ export const MethodRow = ({
}, [externalApiUsage.methodParameters]);

const handleTypeInput = useCallback(
(e: InputEvent) => {
const target = e.target as HTMLSelectElement;

(e: ChangeEvent<HTMLSelectElement>) => {
let newProvenance: Provenance = "manual";
if (modeledMethod?.provenance === "df-generated") {
newProvenance = "df-manual";
Expand All @@ -106,14 +79,14 @@ export const MethodRow = ({
output: "ReturnType",
kind: "value",
...modeledMethod,
type: target.value as ModeledMethodType,
type: e.target.value as ModeledMethodType,
provenance: newProvenance,
});
},
[onChange, externalApiUsage, modeledMethod, argumentsList],
);
const handleInputInput = useCallback(
(e: InputEvent) => {
(e: ChangeEvent<HTMLSelectElement>) => {
if (!modeledMethod) {
return;
}
Expand All @@ -128,7 +101,7 @@ export const MethodRow = ({
[onChange, externalApiUsage, modeledMethod],
);
const handleOutputInput = useCallback(
(e: InputEvent) => {
(e: ChangeEvent<HTMLSelectElement>) => {
if (!modeledMethod) {
return;
}
Expand Down Expand Up @@ -169,94 +142,96 @@ export const MethodRow = ({
? extensiblePredicateDefinitions[modeledMethod.type]
: 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 (
<VSCodeDataGridRow>
<VSCodeDataGridCell gridColumn={1}>
<SupportSpan
supported={externalApiUsage.supported}
modeled={modeledMethod}
>
{externalApiUsage.packageName}.{externalApiUsage.typeName}
</SupportSpan>
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={2}>
<ApiOrMethodCell gridColumn={1}>
<VSCodeCheckbox />
<span>
{externalApiUsage.packageName}.{externalApiUsage.typeName}.
{externalApiUsage.methodName}
{externalApiUsage.methodParameters}
</span>
{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}>
{externalApiUsage.usages.length}
</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={5}>
{modeledMethod?.type &&
["sink", "summary"].includes(modeledMethod?.type) && (
<Dropdown value={modeledMethod?.input} onInput={handleInputInput}>
<VSCodeOption value="Argument[this]">Argument[this]</VSCodeOption>
{argumentsList.map((argument, index) => (
<VSCodeOption key={argument} value={`Argument[${index}]`}>
Argument[{index}]: {argument}
</VSCodeOption>
))}
</Dropdown>
)}
<VSCodeDataGridCell gridColumn={3}>
<Dropdown
value={modeledMethod?.input}
options={inputOptions}
disabled={!showInputCell}
onChange={handleInputInput}
/>
</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 gridColumn={4}>
<Dropdown
value={modeledMethod?.output}
options={outputOptions}
disabled={!showOutputCell}
onChange={handleOutputInput}
/>
</VSCodeDataGridCell>
<VSCodeDataGridCell gridColumn={7}>
{predicate?.supportedKinds && (
<KindInput
kinds={predicate.supportedKinds}
value={modeledMethod?.kind}
onChange={handleKindChange}
/>
)}
<VSCodeDataGridCell gridColumn={5}>
<KindInput
kinds={predicate?.supportedKinds || []}
value={modeledMethod?.kind}
disabled={!showKindCell}
onChange={handleKindChange}
/>
</VSCodeDataGridCell>
</VSCodeDataGridRow>
);
Expand Down
Loading