Skip to content

Commit dc33784

Browse files
committed
Update ModelKindDropdown to be more self-contained
1 parent c4396b7 commit dc33784

File tree

4 files changed

+118
-59
lines changed

4 files changed

+118
-59
lines changed

extensions/ql-vscode/src/view/model-editor/MethodRow.tsx

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { vscode } from "../vscode-api";
1212
import { Method } from "../../model-editor/method";
1313
import { ModeledMethod } from "../../model-editor/modeled-method";
1414
import { ModelKindDropdown } from "./ModelKindDropdown";
15-
import { extensiblePredicateDefinitions } from "../../model-editor/predicates";
1615
import { Mode } from "../../model-editor/shared/mode";
1716
import { MethodClassifications } from "./MethodClassifications";
1817
import {
@@ -73,31 +72,11 @@ export const MethodRow = (props: MethodRowProps) => {
7372
function ModelableMethodRow(props: MethodRowProps) {
7473
const { method, modeledMethod, methodIsUnsaved, mode, onChange } = props;
7574

76-
const handleKindChange = useCallback(
77-
(kind: string) => {
78-
if (!modeledMethod) {
79-
return;
80-
}
81-
82-
onChange(method, {
83-
...modeledMethod,
84-
kind,
85-
});
86-
},
87-
[onChange, method, modeledMethod],
88-
);
89-
9075
const jumpToUsage = useCallback(
9176
() => sendJumpToUsageMessage(method),
9277
[method],
9378
);
9479

95-
const predicate =
96-
modeledMethod?.type && modeledMethod.type !== "none"
97-
? extensiblePredicateDefinitions[modeledMethod.type]
98-
: undefined;
99-
const showKindCell = predicate?.supportedKinds;
100-
10180
const modelingStatus = getModelingStatus(modeledMethod, methodIsUnsaved);
10281

10382
return (
@@ -155,11 +134,9 @@ function ModelableMethodRow(props: MethodRowProps) {
155134
</VSCodeDataGridCell>
156135
<VSCodeDataGridCell gridColumn={5}>
157136
<ModelKindDropdown
158-
kinds={predicate?.supportedKinds || []}
159-
value={modeledMethod?.kind}
160-
disabled={!showKindCell}
161-
onChange={handleKindChange}
162-
aria-label="Kind"
137+
method={method}
138+
modeledMethod={modeledMethod}
139+
onChange={onChange}
163140
/>
164141
</VSCodeDataGridCell>
165142
</>
Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,84 @@
11
import * as React from "react";
22
import { ChangeEvent, useCallback, useEffect, useMemo } from "react";
3-
import type { ModeledMethodKind } from "../../model-editor/modeled-method";
3+
import type {
4+
ModeledMethod,
5+
ModeledMethodKind,
6+
} from "../../model-editor/modeled-method";
47
import { Dropdown } from "../common/Dropdown";
8+
import { Method } from "../../model-editor/method";
9+
import { extensiblePredicateDefinitions } from "../../model-editor/predicates";
510

611
type Props = {
7-
kinds: ModeledMethodKind[];
8-
9-
value: ModeledMethodKind | undefined;
10-
disabled?: boolean;
11-
onChange: (value: ModeledMethodKind) => void;
12-
13-
"aria-label"?: string;
12+
method: Method;
13+
modeledMethod: ModeledMethod | undefined;
14+
onChange: (method: Method, modeledMethod: ModeledMethod) => void;
1415
};
1516

1617
export const ModelKindDropdown = ({
17-
kinds,
18-
value,
19-
disabled,
18+
method,
19+
modeledMethod,
2020
onChange,
21-
...props
2221
}: Props) => {
22+
const predicate = useMemo(() => {
23+
return modeledMethod?.type && modeledMethod.type !== "none"
24+
? extensiblePredicateDefinitions[modeledMethod.type]
25+
: undefined;
26+
}, [modeledMethod?.type]);
27+
28+
const kinds = useMemo(() => predicate?.supportedKinds || [], [predicate]);
29+
30+
const disabled = useMemo(
31+
() => !predicate?.supportedKinds,
32+
[predicate?.supportedKinds],
33+
);
34+
2335
const options = useMemo(
2436
() => kinds.map((kind) => ({ value: kind, label: kind })),
2537
[kinds],
2638
);
2739

28-
const handleInput = useCallback(
40+
const onChangeKind = useCallback(
41+
(kind: ModeledMethodKind) => {
42+
if (!modeledMethod) {
43+
return;
44+
}
45+
46+
onChange(method, {
47+
...modeledMethod,
48+
kind,
49+
});
50+
},
51+
[method, modeledMethod, onChange],
52+
);
53+
54+
const handleChange = useCallback(
2955
(e: ChangeEvent<HTMLSelectElement>) => {
3056
const target = e.target as HTMLSelectElement;
57+
const kind = target.value;
3158

32-
onChange(target.value);
59+
onChangeKind(kind);
3360
},
34-
[onChange],
61+
[onChangeKind],
3562
);
3663

3764
useEffect(() => {
65+
const value = modeledMethod?.kind;
3866
if (value === undefined && kinds.length > 0) {
39-
onChange(kinds[0]);
67+
onChangeKind(kinds[0]);
4068
}
4169

4270
if (value !== undefined && !kinds.includes(value)) {
43-
onChange(kinds[0]);
71+
onChangeKind(kinds[0]);
4472
}
45-
}, [value, kinds, onChange]);
73+
}, [modeledMethod?.kind, kinds, onChangeKind]);
4674

4775
return (
4876
<Dropdown
49-
value={value}
77+
value={modeledMethod?.kind}
5078
options={options}
5179
disabled={disabled}
52-
onChange={handleInput}
53-
{...props}
80+
onChange={handleChange}
81+
aria-label="Kind"
5482
/>
5583
);
5684
};

extensions/ql-vscode/src/view/model-editor/__tests__/ModelKindDropdown.spec.tsx

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,61 +2,95 @@ import * as React from "react";
22
import { render, screen } from "@testing-library/react";
33
import { ModelKindDropdown } from "../ModelKindDropdown";
44
import userEvent from "@testing-library/user-event";
5+
import { createMethod } from "../../../../test/factories/model-editor/method-factories";
6+
import { createModeledMethod } from "../../../../test/factories/model-editor/modeled-method-factories";
57

68
describe(ModelKindDropdown.name, () => {
79
const onChange = jest.fn();
10+
const method = createMethod();
811

912
beforeEach(() => {
1013
onChange.mockReset();
1114
});
1215

1316
it("allows changing the kind", async () => {
17+
const modeledMethod = createModeledMethod({
18+
type: "source",
19+
kind: "local",
20+
});
21+
1422
render(
1523
<ModelKindDropdown
16-
kinds={["local", "remote"]}
17-
value="local"
24+
method={method}
25+
modeledMethod={modeledMethod}
1826
onChange={onChange}
1927
/>,
2028
);
2129

2230
expect(screen.getByRole("combobox")).toHaveValue("local");
2331
await userEvent.selectOptions(screen.getByRole("combobox"), "remote");
24-
expect(onChange).toHaveBeenCalledWith("remote");
32+
expect(onChange).toHaveBeenCalledWith(
33+
method,
34+
expect.objectContaining({
35+
kind: "remote",
36+
}),
37+
);
2538
});
2639

2740
it("resets the kind when changing the supported kinds", () => {
41+
const method = createMethod();
42+
const modeledMethod = createModeledMethod({
43+
type: "source",
44+
kind: "local",
45+
});
46+
2847
const { rerender } = render(
2948
<ModelKindDropdown
30-
kinds={["local", "remote"]}
31-
value={"local"}
49+
method={method}
50+
modeledMethod={modeledMethod}
3251
onChange={onChange}
3352
/>,
3453
);
3554

3655
expect(screen.getByRole("combobox")).toHaveValue("local");
3756
expect(onChange).not.toHaveBeenCalled();
3857

58+
// Changing the type to sink should update the supported kinds
59+
const updatedModeledMethod = createModeledMethod({
60+
type: "sink",
61+
});
62+
3963
rerender(
4064
<ModelKindDropdown
41-
kinds={["sql-injection", "log-injection", "url-redirection"]}
42-
value="local"
65+
method={method}
66+
modeledMethod={updatedModeledMethod}
4367
onChange={onChange}
4468
/>,
4569
);
46-
expect(screen.getByRole("combobox")).toHaveValue("sql-injection");
47-
expect(onChange).toHaveBeenCalledWith("sql-injection");
70+
71+
expect(screen.getByRole("combobox")).toHaveValue("code-injection");
4872
});
4973

5074
it("sets the kind when value is undefined", () => {
75+
const method = createMethod();
76+
const modeledMethod = createModeledMethod({
77+
type: "source",
78+
});
79+
5180
render(
5281
<ModelKindDropdown
53-
kinds={["local", "remote"]}
54-
value={undefined}
82+
method={method}
83+
modeledMethod={modeledMethod}
5584
onChange={onChange}
5685
/>,
5786
);
5887

5988
expect(screen.getByRole("combobox")).toHaveValue("local");
60-
expect(onChange).toHaveBeenCalledWith("local");
89+
expect(onChange).toHaveBeenCalledWith(
90+
method,
91+
expect.objectContaining({
92+
kind: "local",
93+
}),
94+
);
6195
});
6296
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ModeledMethod } from "../../../src/model-editor/modeled-method";
2+
3+
export function createModeledMethod(
4+
data: Partial<ModeledMethod> = {},
5+
): ModeledMethod {
6+
return {
7+
libraryVersion: "1.6.0",
8+
signature: "org.sql2o.Connection#createQuery(String)",
9+
packageName: "org.sql2o",
10+
typeName: "Connection",
11+
methodName: "createQuery",
12+
methodParameters: "(String)",
13+
type: "sink",
14+
input: "Argument[0]",
15+
output: "",
16+
kind: "jndi-injection",
17+
provenance: "manual",
18+
...data,
19+
};
20+
}

0 commit comments

Comments
 (0)