Skip to content

Commit 21c33b7

Browse files
authored
Added filtering and sorting in the model alerts view (#3509)
1 parent 8b6a935 commit 21c33b7

File tree

7 files changed

+461
-18
lines changed

7 files changed

+461
-18
lines changed

extensions/ql-vscode/src/model-editor/model-alerts/alert-processor.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,22 @@ export function calculateModelAlerts(
2929
}
3030

3131
for (const [i, repoResult] of repoResults.entries()) {
32+
const results = repoResult.interpretedResults || [];
33+
const repository = {
34+
id: repoResult.repositoryId,
35+
fullName: repoMap.get(repoResult.repositoryId) || "",
36+
};
37+
38+
const alerts = results.map(() => {
39+
return {
40+
alert: createMockAlert(),
41+
repository,
42+
};
43+
});
44+
3245
modelAlerts.push({
3346
model: createModeledMethod(i.toString()),
34-
alerts: [
35-
{
36-
alert: createMockAlert(),
37-
repository: {
38-
id: repoResult.repositoryId,
39-
fullName: repoMap.get(repoResult.repositoryId) || "",
40-
},
41-
},
42-
],
47+
alerts,
4348
});
4449
}
4550

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { ModelAlerts } from "../model-alerts/model-alerts";
2+
3+
export enum SortKey {
4+
Alphabetically = "alphabetically",
5+
NumberOfResults = "numberOfResults",
6+
}
7+
8+
export type ModelAlertsFilterSortState = {
9+
modelSearchValue: string;
10+
repositorySearchValue: string;
11+
sortKey: SortKey;
12+
};
13+
14+
export const defaultFilterSortState: ModelAlertsFilterSortState = {
15+
modelSearchValue: "",
16+
repositorySearchValue: "",
17+
sortKey: SortKey.NumberOfResults,
18+
};
19+
20+
export function filterAndSort(
21+
modelAlerts: ModelAlerts[],
22+
filterSortState: ModelAlertsFilterSortState,
23+
): ModelAlerts[] {
24+
if (!modelAlerts || modelAlerts.length === 0) {
25+
return [];
26+
}
27+
28+
return modelAlerts
29+
.filter((item) => matchesFilter(item, filterSortState))
30+
.sort((a, b) => {
31+
switch (filterSortState.sortKey) {
32+
case SortKey.Alphabetically:
33+
return a.model.signature.localeCompare(b.model.signature);
34+
case SortKey.NumberOfResults:
35+
return (b.alerts.length || 0) - (a.alerts.length || 0);
36+
default:
37+
return 0;
38+
}
39+
});
40+
}
41+
42+
function matchesFilter(
43+
item: ModelAlerts,
44+
filterSortState: ModelAlertsFilterSortState | undefined,
45+
): boolean {
46+
if (!filterSortState) {
47+
return true;
48+
}
49+
50+
return (
51+
matchesRepository(item, filterSortState.repositorySearchValue) &&
52+
matchesModel(item, filterSortState.modelSearchValue)
53+
);
54+
}
55+
56+
function matchesRepository(
57+
item: ModelAlerts,
58+
repositorySearchValue: string,
59+
): boolean {
60+
// We may want to only return alerts that have a repository match
61+
// but for now just return true if the model has any alerts
62+
// with a matching repo.
63+
64+
return item.alerts.some((alert) =>
65+
alert.repository.fullName
66+
.toLowerCase()
67+
.includes(repositorySearchValue.toLowerCase()),
68+
);
69+
}
70+
71+
function matchesModel(item: ModelAlerts, modelSearchValue: string): boolean {
72+
return item.model.signature
73+
.toLowerCase()
74+
.includes(modelSearchValue.toLowerCase());
75+
}

extensions/ql-vscode/src/stories/model-alerts/ModelAlerts.stories.tsx

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import type { Meta, StoryFn } from "@storybook/react";
22

33
import { ModelAlerts as ModelAlertsComponent } from "../../view/model-alerts/ModelAlerts";
44
import { createMockVariantAnalysis } from "../../../test/factories/variant-analysis/shared/variant-analysis";
5+
import { VariantAnalysisRepoStatus } from "../../variant-analysis/shared/variant-analysis";
56
import type { VariantAnalysisScannedRepositoryResult } from "../../variant-analysis/shared/variant-analysis";
7+
import { createMockAnalysisAlert } from "../../../test/factories/variant-analysis/shared/analysis-alert";
68

79
export default {
810
title: "Model Alerts/Model Alerts",
@@ -24,15 +26,79 @@ const variantAnalysis = createMockVariantAnalysis({
2426
path: "/path/to/model-pack-2",
2527
},
2628
],
29+
scannedRepos: [
30+
{
31+
repository: {
32+
id: 1,
33+
fullName: "org/repo1",
34+
private: false,
35+
stargazersCount: 100,
36+
updatedAt: new Date().toISOString(),
37+
},
38+
analysisStatus: VariantAnalysisRepoStatus.InProgress,
39+
resultCount: 0,
40+
artifactSizeInBytes: 0,
41+
},
42+
{
43+
repository: {
44+
id: 2,
45+
fullName: "org/repo2",
46+
private: false,
47+
stargazersCount: 100,
48+
updatedAt: new Date().toISOString(),
49+
},
50+
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
51+
resultCount: 0,
52+
artifactSizeInBytes: 0,
53+
},
54+
{
55+
repository: {
56+
id: 3,
57+
fullName: "org/repo3",
58+
private: false,
59+
stargazersCount: 100,
60+
updatedAt: new Date().toISOString(),
61+
},
62+
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
63+
resultCount: 1,
64+
artifactSizeInBytes: 0,
65+
},
66+
{
67+
repository: {
68+
id: 4,
69+
fullName: "org/repo4",
70+
private: false,
71+
stargazersCount: 100,
72+
updatedAt: new Date().toISOString(),
73+
},
74+
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
75+
resultCount: 3,
76+
artifactSizeInBytes: 0,
77+
},
78+
],
2779
});
2880

29-
const repoResults: VariantAnalysisScannedRepositoryResult[] = (
30-
variantAnalysis.scannedRepos || []
31-
).map((repo) => ({
32-
variantAnalysisId: variantAnalysis.id,
33-
repositoryId: repo.repository.id,
34-
interpretedResults: [],
35-
}));
81+
const repoResults: VariantAnalysisScannedRepositoryResult[] = [
82+
{
83+
variantAnalysisId: variantAnalysis.id,
84+
repositoryId: 2,
85+
interpretedResults: [createMockAnalysisAlert(), createMockAnalysisAlert()],
86+
},
87+
{
88+
variantAnalysisId: variantAnalysis.id,
89+
repositoryId: 3,
90+
interpretedResults: [
91+
createMockAnalysisAlert(),
92+
createMockAnalysisAlert(),
93+
createMockAnalysisAlert(),
94+
],
95+
},
96+
{
97+
variantAnalysisId: variantAnalysis.id,
98+
repositoryId: 4,
99+
interpretedResults: [createMockAnalysisAlert()],
100+
},
101+
];
36102

37103
export const ModelAlerts = Template.bind({});
38104
ModelAlerts.args = {

extensions/ql-vscode/src/view/model-alerts/ModelAlerts.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ import { vscode } from "../vscode-api";
1111
import { ModelAlertsResults } from "./ModelAlertsResults";
1212
import type { ModelAlerts } from "../../model-editor/model-alerts/model-alerts";
1313
import { calculateModelAlerts } from "../../model-editor/model-alerts/alert-processor";
14+
import { ModelAlertsSearchSortRow } from "./ModelAlertsSearchSortRow";
15+
import {
16+
defaultFilterSortState,
17+
filterAndSort,
18+
} from "../../model-editor/shared/model-alerts-filter-sort";
19+
import type { ModelAlertsFilterSortState } from "../../model-editor/shared/model-alerts-filter-sort";
1420

1521
type Props = {
1622
initialViewState?: ModelAlertsViewState;
@@ -53,6 +59,9 @@ export function ModelAlerts({
5359
const [repoResults, setRepoResults] =
5460
useState<VariantAnalysisScannedRepositoryResult[]>(initialRepoResults);
5561

62+
const [filterSortValue, setFilterSortValue] =
63+
useState<ModelAlertsFilterSortState>(defaultFilterSortState);
64+
5665
useEffect(() => {
5766
const listener = (evt: MessageEvent) => {
5867
if (evt.origin === window.origin) {
@@ -97,8 +106,10 @@ export function ModelAlerts({
97106
return [];
98107
}
99108

100-
return calculateModelAlerts(variantAnalysis, repoResults);
101-
}, [variantAnalysis, repoResults]);
109+
const modelAlerts = calculateModelAlerts(variantAnalysis, repoResults);
110+
111+
return filterAndSort(modelAlerts, filterSortValue);
112+
}, [filterSortValue, variantAnalysis, repoResults]);
102113

103114
if (viewState === undefined || variantAnalysis === undefined) {
104115
return <></>;
@@ -125,6 +136,10 @@ export function ModelAlerts({
125136
></ModelAlertsHeader>
126137
<div>
127138
<SectionTitle>Model alerts</SectionTitle>
139+
<ModelAlertsSearchSortRow
140+
filterSortValue={filterSortValue}
141+
onFilterSortChange={setFilterSortValue}
142+
/>
128143
<div>
129144
{modelAlerts.map((alerts, i) => (
130145
// We're using the index as the key here which is not recommended.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { useCallback } from "react";
2+
import type { Dispatch, SetStateAction } from "react";
3+
import { styled } from "styled-components";
4+
import type {
5+
ModelAlertsFilterSortState,
6+
SortKey,
7+
} from "../../model-editor/shared/model-alerts-filter-sort";
8+
import { SearchBox } from "../common/SearchBox";
9+
import { ModelAlertsSort } from "./ModelAlertsSort";
10+
11+
type Props = {
12+
filterSortValue: ModelAlertsFilterSortState;
13+
onFilterSortChange: Dispatch<SetStateAction<ModelAlertsFilterSortState>>;
14+
};
15+
16+
const Container = styled.div`
17+
display: flex;
18+
gap: 1em;
19+
width: 100%;
20+
margin-bottom: 1em;
21+
`;
22+
23+
const ModelsSearchColumn = styled(SearchBox)`
24+
flex: 2;
25+
`;
26+
27+
const RepositoriesSearchColumn = styled(SearchBox)`
28+
flex: 2;
29+
`;
30+
31+
const SortColumn = styled(ModelAlertsSort)`
32+
flex: 1;
33+
`;
34+
35+
export const ModelAlertsSearchSortRow = ({
36+
filterSortValue,
37+
onFilterSortChange,
38+
}: Props) => {
39+
const handleModelSearchValueChange = useCallback(
40+
(searchValue: string) => {
41+
onFilterSortChange((oldValue) => ({
42+
...oldValue,
43+
modelSearchValue: searchValue,
44+
}));
45+
},
46+
[onFilterSortChange],
47+
);
48+
49+
const handleRepositorySearchValueChange = useCallback(
50+
(searchValue: string) => {
51+
onFilterSortChange((oldValue) => ({
52+
...oldValue,
53+
repositorySearchValue: searchValue,
54+
}));
55+
},
56+
[onFilterSortChange],
57+
);
58+
59+
const handleSortKeyChange = useCallback(
60+
(sortKey: SortKey) => {
61+
onFilterSortChange((oldValue) => ({
62+
...oldValue,
63+
sortKey,
64+
}));
65+
},
66+
[onFilterSortChange],
67+
);
68+
69+
return (
70+
<Container>
71+
<ModelsSearchColumn
72+
placeholder="Filter by model"
73+
value={filterSortValue.modelSearchValue}
74+
onChange={handleModelSearchValueChange}
75+
/>
76+
<RepositoriesSearchColumn
77+
placeholder="Filter by repository owner/name"
78+
value={filterSortValue.repositorySearchValue}
79+
onChange={handleRepositorySearchValueChange}
80+
/>
81+
<SortColumn
82+
value={filterSortValue.sortKey}
83+
onChange={handleSortKeyChange}
84+
/>
85+
</Container>
86+
);
87+
};
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useCallback } from "react";
2+
import { styled } from "styled-components";
3+
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react";
4+
import { SortKey } from "../../model-editor/shared/model-alerts-filter-sort";
5+
import { Codicon } from "../common";
6+
7+
const Dropdown = styled(VSCodeDropdown)`
8+
width: 100%;
9+
`;
10+
11+
type Props = {
12+
value: SortKey;
13+
onChange: (value: SortKey) => void;
14+
15+
className?: string;
16+
};
17+
18+
export const ModelAlertsSort = ({ value, onChange, className }: Props) => {
19+
const handleInput = useCallback(
20+
(e: InputEvent) => {
21+
const target = e.target as HTMLSelectElement;
22+
23+
onChange(target.value as SortKey);
24+
},
25+
[onChange],
26+
);
27+
28+
return (
29+
<Dropdown value={value} onInput={handleInput} className={className}>
30+
<Codicon name="sort-precedence" label="Sort..." slot="indicator" />
31+
<VSCodeOption value={SortKey.Alphabetically}>Alphabetically</VSCodeOption>
32+
<VSCodeOption value={SortKey.NumberOfResults}>
33+
Number of results
34+
</VSCodeOption>
35+
</Dropdown>
36+
);
37+
};

0 commit comments

Comments
 (0)