diff --git a/extensions/ql-vscode/src/stories/results/AlertTable.stories.tsx b/extensions/ql-vscode/src/stories/results/AlertTable.stories.tsx new file mode 100644 index 000000000..4a559597b --- /dev/null +++ b/extensions/ql-vscode/src/stories/results/AlertTable.stories.tsx @@ -0,0 +1,443 @@ +import * as React from "react"; + +import { Meta, StoryFn } from "@storybook/react"; + +import { AlertTable as AlertTableComponent } from "../../view/results/AlertTable"; + +import "../../view/results/resultsView.css"; + +export default { + title: "Results/Alert Table", + component: AlertTableComponent, +} as Meta; + +const Template: StoryFn = (args) => ( + +); + +export const WithoutCodeFlows = Template.bind({}); +WithoutCodeFlows.args = { + resultSet: { + t: "InterpretedResultSet", + schema: { name: "alerts", rows: 1, columns: [] }, + name: "alerts", + interpretation: { + data: { + version: "2.1.0", + runs: [ + { + tool: { driver: { name: "" } }, + results: [ + { + ruleId: "java/example/empty-block", + ruleIndex: 0, + rule: { id: "java/example/empty-block", index: 0 }, + message: { text: "This is a empty block." }, + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: "gson/src/main/java/com/google/gson/internal/Streams.java", + uriBaseId: "%SRCROOT%", + index: 0, + }, + region: { startLine: 98, startColumn: 35, endColumn: 37 }, + }, + }, + ], + partialFingerprints: { + primaryLocationLineHash: "1d25c2fbd979cbb:1", + primaryLocationStartColumnFingerprint: "30", + }, + }, + { + ruleId: "java/example/empty-block", + ruleIndex: 0, + rule: { id: "java/example/empty-block", index: 0 }, + message: { text: "This is a empty block." }, + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: "gson/src/main/java/com/google/gson/internal/Streams.java", + uriBaseId: "%SRCROOT%", + index: 0, + }, + region: { startLine: 99, startColumn: 35, endColumn: 37 }, + }, + }, + ], + partialFingerprints: { + primaryLocationLineHash: "5c5ed8d70236498a:1", + primaryLocationStartColumnFingerprint: "30", + }, + }, + { + ruleId: "java/example/empty-block", + ruleIndex: 0, + rule: { id: "java/example/empty-block", index: 0 }, + message: { text: "This is a empty block." }, + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: "gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java", + uriBaseId: "%SRCROOT%", + index: 1, + }, + region: { + startLine: 66, + startColumn: 33, + endLine: 68, + endColumn: 6, + }, + }, + }, + ], + partialFingerprints: { + primaryLocationLineHash: "bd306a1ab438981d:1", + primaryLocationStartColumnFingerprint: "28", + }, + }, + { + ruleId: "java/example/empty-block", + ruleIndex: 0, + rule: { id: "java/example/empty-block", index: 0 }, + message: { text: "This is a empty block." }, + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: "gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java", + uriBaseId: "%SRCROOT%", + index: 1, + }, + region: { + startLine: 91, + startColumn: 33, + endLine: 93, + endColumn: 6, + }, + }, + }, + ], + partialFingerprints: { + primaryLocationLineHash: "b91980e3f3ee2a16:1", + primaryLocationStartColumnFingerprint: "28", + }, + }, + { + ruleId: "java/example/empty-block", + ruleIndex: 0, + rule: { id: "java/example/empty-block", index: 0 }, + message: { text: "This is a empty block." }, + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: "gson/src/main/java/com/google/gson/internal/ReflectionAccessFilterHelper.java", + uriBaseId: "%SRCROOT%", + index: 2, + }, + region: { + startLine: 100, + startColumn: 49, + endLine: 102, + endColumn: 10, + }, + }, + }, + ], + partialFingerprints: { + primaryLocationLineHash: "e4d69f1851f45b95:1", + primaryLocationStartColumnFingerprint: "40", + }, + }, + { + ruleId: "java/example/empty-block", + ruleIndex: 0, + rule: { id: "java/example/empty-block", index: 0 }, + message: { text: "This is a empty block." }, + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: "gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java", + uriBaseId: "%SRCROOT%", + index: 1, + }, + region: { + startLine: 112, + startColumn: 33, + endLine: 114, + endColumn: 6, + }, + }, + }, + ], + partialFingerprints: { + primaryLocationLineHash: "f3fb11daf511ebdb:1", + primaryLocationStartColumnFingerprint: "28", + }, + }, + { + ruleId: "java/example/empty-block", + ruleIndex: 0, + rule: { id: "java/example/empty-block", index: 0 }, + message: { text: "This is a empty block." }, + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: "gson/src/main/java/com/google/gson/internal/bind/DateTypeAdapter.java", + uriBaseId: "%SRCROOT%", + index: 3, + }, + region: { + startLine: 84, + startColumn: 42, + endLine: 86, + endColumn: 10, + }, + }, + }, + ], + partialFingerprints: { + primaryLocationLineHash: "65a5e0f08a26f7fd:1", + primaryLocationStartColumnFingerprint: "33", + }, + }, + { + ruleId: "java/example/empty-block", + ruleIndex: 0, + rule: { id: "java/example/empty-block", index: 0 }, + message: { text: "This is a empty block." }, + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: "gson/src/main/java/com/google/gson/internal/bind/DefaultDateTypeAdapter.java", + uriBaseId: "%SRCROOT%", + index: 4, + }, + region: { + startLine: 157, + startColumn: 42, + endLine: 159, + endColumn: 10, + }, + }, + }, + ], + partialFingerprints: { + primaryLocationLineHash: "c7647299ca3416a7:1", + primaryLocationStartColumnFingerprint: "33", + }, + }, + { + ruleId: "java/example/empty-block", + ruleIndex: 0, + rule: { id: "java/example/empty-block", index: 0 }, + message: { text: "This is a empty block." }, + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: "gson/src/main/java/com/google/gson/internal/bind/JsonTreeWriter.java", + uriBaseId: "%SRCROOT%", + index: 5, + }, + region: { + startLine: 227, + startColumn: 52, + endLine: 228, + endColumn: 4, + }, + }, + }, + ], + partialFingerprints: { + primaryLocationLineHash: "d86e48478bd5f82f:1", + primaryLocationStartColumnFingerprint: "49", + }, + }, + { + ruleId: "java/example/empty-block", + ruleIndex: 0, + rule: { id: "java/example/empty-block", index: 0 }, + message: { text: "This is a empty block." }, + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: "gson/src/main/java/com/google/gson/stream/JsonReader.java", + uriBaseId: "%SRCROOT%", + index: 6, + }, + region: { + startLine: 969, + startColumn: 47, + endLine: 971, + endColumn: 8, + }, + }, + }, + ], + partialFingerprints: { + primaryLocationLineHash: "3bc8c477478d1d94:1", + primaryLocationStartColumnFingerprint: "40", + }, + }, + { + ruleId: "java/example/empty-block", + ruleIndex: 0, + rule: { id: "java/example/empty-block", index: 0 }, + message: { text: "This is a empty block." }, + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: "gson/src/main/java/com/google/gson/stream/JsonReader.java", + uriBaseId: "%SRCROOT%", + index: 6, + }, + region: { + startLine: 1207, + startColumn: 47, + endLine: 1209, + endColumn: 8, + }, + }, + }, + ], + partialFingerprints: { + primaryLocationLineHash: "3bc8c477478d1d94:2", + primaryLocationStartColumnFingerprint: "40", + }, + }, + ], + }, + ], + t: "SarifInterpretationData", + }, + sourceLocationPrefix: "/home/runner/work/gson/gson", + numTruncatedResults: 0, + numTotalResults: 11, + }, + }, + databaseUri: "file:///a/b/c/java", + resultsPath: "file:///a/b/c/results.sarif", + nonemptyRawResults: true, + offset: 0, +}; + +export const WithCodeFlows = Template.bind({}); +WithCodeFlows.args = { + resultSet: { + t: "InterpretedResultSet", + schema: { name: "alerts", rows: 1, columns: [] }, + name: "alerts", + interpretation: { + data: { + version: "2.1.0", + runs: [ + { + tool: { driver: { name: "" } }, + results: [ + { + ruleId: "java/sql-injection", + ruleIndex: 0, + rule: { id: "java/sql-injection", index: 0 }, + message: { + text: "This query depends on a [user-provided value](1).", + }, + locations: [ + { + physicalLocation: { + artifactLocation: { + uri: "src/main/java/org/example/HelloController.java", + uriBaseId: "%SRCROOT%", + index: 0, + }, + region: { startLine: 15, startColumn: 29, endColumn: 56 }, + }, + }, + ], + partialFingerprints: { + primaryLocationLineHash: "87e2d3cc5b365094:1", + primaryLocationStartColumnFingerprint: "16", + }, + codeFlows: [ + { + threadFlows: [ + { + locations: [ + { + location: { + physicalLocation: { + artifactLocation: { + uri: "src/main/java/org/example/HelloController.java", + uriBaseId: "%SRCROOT%", + index: 0, + }, + region: { + startLine: 13, + startColumn: 25, + endColumn: 54, + }, + }, + message: { text: "id : String" }, + }, + }, + { + location: { + physicalLocation: { + artifactLocation: { + uri: "src/main/java/org/example/HelloController.java", + uriBaseId: "%SRCROOT%", + index: 0, + }, + region: { + startLine: 15, + startColumn: 29, + endColumn: 56, + }, + }, + message: { text: "... + ..." }, + }, + }, + ], + }, + ], + }, + ], + relatedLocations: [ + { + id: 1, + physicalLocation: { + artifactLocation: { + uri: "src/main/java/org/example/HelloController.java", + uriBaseId: "%SRCROOT%", + index: 0, + }, + region: { startLine: 13, startColumn: 25, endColumn: 54 }, + }, + message: { text: "user-provided value" }, + }, + ], + }, + ], + }, + ], + t: "SarifInterpretationData", + }, + sourceLocationPrefix: "/home/runner/work/sql2o-example/sql2o-example", + numTruncatedResults: 0, + numTotalResults: 1, + }, + }, + databaseUri: "file:///a/b/c/java", + resultsPath: "file:///a/b/c/results.sarif", + nonemptyRawResults: true, + offset: 0, +}; diff --git a/extensions/ql-vscode/src/view/common/TextButton.tsx b/extensions/ql-vscode/src/view/common/TextButton.tsx index 32ae3fe7f..7760955c6 100644 --- a/extensions/ql-vscode/src/view/common/TextButton.tsx +++ b/extensions/ql-vscode/src/view/common/TextButton.tsx @@ -8,7 +8,8 @@ const StyledButton = styled.button<{ size: Size }>` color: var(--vscode-textLink-foreground); border: none; cursor: pointer; - font-size: ${(props) => props.size}; + font-size: ${(props) => props.size ?? "1em"}; + padding: 0; `; const TextButton = ({ @@ -16,8 +17,8 @@ const TextButton = ({ onClick, children, }: { - size: Size; - onClick: () => void; + size?: Size; + onClick: (e: React.MouseEvent) => void; children: React.ReactNode; }) => ( diff --git a/extensions/ql-vscode/src/view/results/AlertTable.tsx b/extensions/ql-vscode/src/view/results/AlertTable.tsx index 4f8e1d404..f54d1735d 100644 --- a/extensions/ql-vscode/src/view/results/AlertTable.tsx +++ b/extensions/ql-vscode/src/view/results/AlertTable.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import * as Sarif from "sarif"; import * as Keys from "./result-keys"; -import { chevronDown, chevronRight, info, listUnordered } from "./octicons"; +import { info, listUnordered } from "./octicons"; import { className, ResultTableProps, @@ -22,6 +22,7 @@ import { AlertTableHeader } from "./AlertTableHeader"; import { SarifMessageWithLocations } from "./locations/SarifMessageWithLocations"; import { SarifLocation } from "./locations/SarifLocation"; import { EmptyQueryResultsMessage } from "./EmptyQueryResultsMessage"; +import { AlertTableDropdownIndicatorCell } from "./AlertTableDropdownIndicatorCell"; type AlertTableProps = ResultTableProps & { resultSet: InterpretedResultSet; @@ -137,7 +138,6 @@ export class AlertTable extends React.Component< const currentResultExpanded = this.state.expanded.has( Keys.keyToString(resultKey), ); - const indicator = currentResultExpanded ? chevronDown : chevronRight; const location = result.locations !== undefined && result.locations.length > 0 && ( - {/* - eslint-disable-next-line - jsx-a11y/no-noninteractive-element-interactions - */} - - {indicator} - + {listUnordered} {msg} {locationCells} @@ -206,9 +200,6 @@ export class AlertTable extends React.Component< Keys.keyToString(pathKey), ); if (currentResultExpanded) { - const indicator = currentPathExpanded - ? chevronDown - : chevronRight; const isPathSpecificallySelected = Keys.equalsNotUndefined( pathKey, selectedItem, @@ -225,16 +216,10 @@ export class AlertTable extends React.Component< - {/* - eslint-disable-next-line - jsx-a11y/no-noninteractive-element-interactions - */} - - {indicator} - + Path diff --git a/extensions/ql-vscode/src/view/results/AlertTableDropdownIndicatorCell.tsx b/extensions/ql-vscode/src/view/results/AlertTableDropdownIndicatorCell.tsx new file mode 100644 index 000000000..3db621b7e --- /dev/null +++ b/extensions/ql-vscode/src/view/results/AlertTableDropdownIndicatorCell.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; +import TextButton from "../common/TextButton"; +import { chevronDown, chevronRight } from "./octicons"; + +type Props = { + expanded: boolean; + onClick: (e: React.MouseEvent) => void; +}; + +export function AlertTableDropdownIndicatorCell({ expanded, onClick }: Props) { + const indicator = expanded ? chevronDown : chevronRight; + + return ( + + {indicator} + + ); +} diff --git a/extensions/ql-vscode/src/view/results/resultsView.css b/extensions/ql-vscode/src/view/results/resultsView.css index fdb35ba73..1208ea322 100644 --- a/extensions/ql-vscode/src/view/results/resultsView.css +++ b/extensions/ql-vscode/src/view/results/resultsView.css @@ -84,8 +84,15 @@ .vscode-codeql__result-table th { border-top: 1px solid rgba(88, 96, 105, 0.25); border-bottom: 1px solid rgba(88, 96, 105, 0.25); - font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, - sans-serif, Apple Color Emoji, Segoe UI Emoji; + font-family: + -apple-system, + BlinkMacSystemFont, + Segoe UI, + Helvetica, + Arial, + sans-serif, + Apple Color Emoji, + Segoe UI Emoji; background: rgba(225, 228, 232, 0.25); padding: 0.25em 0.5em; text-align: center; @@ -167,10 +174,6 @@ td.vscode-codeql__icon-cell { width: 24px; } -td.vscode-codeql__dropdown-cell:hover { - cursor: pointer; -} - td.vscode-codeql__path-index-cell { text-align: right; }