diff --git a/extensions/ql-vscode/src/view/common/Dropdown.tsx b/extensions/ql-vscode/src/view/common/Dropdown.tsx new file mode 100644 index 000000000..b281325a5 --- /dev/null +++ b/extensions/ql-vscode/src/view/common/Dropdown.tsx @@ -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) => 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 ( + + {!disabled && ( + <> + {options.map((option) => ( + + ))} + + )} + + ); +} diff --git a/extensions/ql-vscode/src/view/data-extensions-editor/KindInput.tsx b/extensions/ql-vscode/src/view/data-extensions-editor/KindInput.tsx index 52f49f6b6..43fdf1a4d 100644 --- a/extensions/ql-vscode/src/view/data-extensions-editor/KindInput.tsx +++ b/extensions/ql-vscode/src/view/data-extensions-editor/KindInput.tsx @@ -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; 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) => { const target = e.target as HTMLSelectElement; onChange(target.value as ModeledMethod["kind"]); @@ -37,12 +38,11 @@ export const KindInput = ({ kinds, value, onChange }: Props) => { }, [value, kinds, onChange]); return ( - - {kinds.map((kind) => ( - - {kind} - - ))} - + ); }; diff --git a/extensions/ql-vscode/src/view/data-extensions-editor/MethodRow.tsx b/extensions/ql-vscode/src/view/data-extensions-editor/MethodRow.tsx index 4ba3ca1b5..6f1a9ef4b 100644 --- a/extensions/ql-vscode/src/view/data-extensions-editor/MethodRow.tsx +++ b/extensions/ql-vscode/src/view/data-extensions-editor/MethodRow.tsx @@ -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"; @@ -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` - 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` - 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; @@ -90,9 +65,7 @@ export const MethodRow = ({ }, [externalApiUsage.methodParameters]); const handleTypeInput = useCallback( - (e: InputEvent) => { - const target = e.target as HTMLSelectElement; - + (e: ChangeEvent) => { let newProvenance: Provenance = "manual"; if (modeledMethod?.provenance === "df-generated") { newProvenance = "df-manual"; @@ -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) => { if (!modeledMethod) { return; } @@ -128,7 +101,7 @@ export const MethodRow = ({ [onChange, externalApiUsage, modeledMethod], ); const handleOutputInput = useCallback( - (e: InputEvent) => { + (e: ChangeEvent) => { if (!modeledMethod) { return; } @@ -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 ( - - - {externalApiUsage.packageName}.{externalApiUsage.typeName} - - - + + + + {externalApiUsage.packageName}.{externalApiUsage.typeName}. + {externalApiUsage.methodName} + {externalApiUsage.methodParameters} + {mode === Mode.Application && ( - - {externalApiUsage.methodName} - {externalApiUsage.methodParameters} - - )} - {mode === Mode.Framework && ( - - {externalApiUsage.methodName} - {externalApiUsage.methodParameters} - - )} - - {mode === Mode.Application && ( - {externalApiUsage.usages.length} - - )} - - {(!externalApiUsage.supported || - (modeledMethod && modeledMethod?.type !== "none")) && ( - - Unmodeled - Source - Sink - Flow summary - Neutral - )} + View + + + + + + + + + - {modeledMethod?.type && - ["sink", "summary"].includes(modeledMethod?.type) && ( - - Argument[this] - {argumentsList.map((argument, index) => ( - - Argument[{index}]: {argument} - - ))} - - )} - - - {modeledMethod?.type && - ["source", "summary"].includes(modeledMethod?.type) && ( - - ReturnValue - Argument[this] - {argumentsList.map((argument, index) => ( - - Argument[{index}]: {argument} - - ))} - - )} - - - {predicate?.supportedKinds && ( - - )} + ); diff --git a/extensions/ql-vscode/src/view/data-extensions-editor/ModeledMethodDataGrid.tsx b/extensions/ql-vscode/src/view/data-extensions-editor/ModeledMethodDataGrid.tsx index c1328a4ca..5100fa4a3 100644 --- a/extensions/ql-vscode/src/view/data-extensions-editor/ModeledMethodDataGrid.tsx +++ b/extensions/ql-vscode/src/view/data-extensions-editor/ModeledMethodDataGrid.tsx @@ -33,29 +33,21 @@ export const ModeledMethodDataGrid = ({ ); return ( - + - Type + API or method - Method - - {mode === Mode.Application && ( - - Usages - - )} - Model type - + Input - + Output - + Kind