Added filtering and sorting in the model alerts view (#3509)

This commit is contained in:
Charis Kyriakou 2024-03-25 17:18:09 +00:00 коммит произвёл GitHub
Родитель 8b6a9352f6
Коммит 21c33b762c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
7 изменённых файлов: 461 добавлений и 18 удалений

Просмотреть файл

@ -29,17 +29,22 @@ export function calculateModelAlerts(
}
for (const [i, repoResult] of repoResults.entries()) {
const results = repoResult.interpretedResults || [];
const repository = {
id: repoResult.repositoryId,
fullName: repoMap.get(repoResult.repositoryId) || "",
};
const alerts = results.map(() => {
return {
alert: createMockAlert(),
repository,
};
});
modelAlerts.push({
model: createModeledMethod(i.toString()),
alerts: [
{
alert: createMockAlert(),
repository: {
id: repoResult.repositoryId,
fullName: repoMap.get(repoResult.repositoryId) || "",
},
},
],
alerts,
});
}

Просмотреть файл

@ -0,0 +1,75 @@
import type { ModelAlerts } from "../model-alerts/model-alerts";
export enum SortKey {
Alphabetically = "alphabetically",
NumberOfResults = "numberOfResults",
}
export type ModelAlertsFilterSortState = {
modelSearchValue: string;
repositorySearchValue: string;
sortKey: SortKey;
};
export const defaultFilterSortState: ModelAlertsFilterSortState = {
modelSearchValue: "",
repositorySearchValue: "",
sortKey: SortKey.NumberOfResults,
};
export function filterAndSort(
modelAlerts: ModelAlerts[],
filterSortState: ModelAlertsFilterSortState,
): ModelAlerts[] {
if (!modelAlerts || modelAlerts.length === 0) {
return [];
}
return modelAlerts
.filter((item) => matchesFilter(item, filterSortState))
.sort((a, b) => {
switch (filterSortState.sortKey) {
case SortKey.Alphabetically:
return a.model.signature.localeCompare(b.model.signature);
case SortKey.NumberOfResults:
return (b.alerts.length || 0) - (a.alerts.length || 0);
default:
return 0;
}
});
}
function matchesFilter(
item: ModelAlerts,
filterSortState: ModelAlertsFilterSortState | undefined,
): boolean {
if (!filterSortState) {
return true;
}
return (
matchesRepository(item, filterSortState.repositorySearchValue) &&
matchesModel(item, filterSortState.modelSearchValue)
);
}
function matchesRepository(
item: ModelAlerts,
repositorySearchValue: string,
): boolean {
// We may want to only return alerts that have a repository match
// but for now just return true if the model has any alerts
// with a matching repo.
return item.alerts.some((alert) =>
alert.repository.fullName
.toLowerCase()
.includes(repositorySearchValue.toLowerCase()),
);
}
function matchesModel(item: ModelAlerts, modelSearchValue: string): boolean {
return item.model.signature
.toLowerCase()
.includes(modelSearchValue.toLowerCase());
}

Просмотреть файл

@ -2,7 +2,9 @@ import type { Meta, StoryFn } from "@storybook/react";
import { ModelAlerts as ModelAlertsComponent } from "../../view/model-alerts/ModelAlerts";
import { createMockVariantAnalysis } from "../../../test/factories/variant-analysis/shared/variant-analysis";
import { VariantAnalysisRepoStatus } from "../../variant-analysis/shared/variant-analysis";
import type { VariantAnalysisScannedRepositoryResult } from "../../variant-analysis/shared/variant-analysis";
import { createMockAnalysisAlert } from "../../../test/factories/variant-analysis/shared/analysis-alert";
export default {
title: "Model Alerts/Model Alerts",
@ -24,15 +26,79 @@ const variantAnalysis = createMockVariantAnalysis({
path: "/path/to/model-pack-2",
},
],
scannedRepos: [
{
repository: {
id: 1,
fullName: "org/repo1",
private: false,
stargazersCount: 100,
updatedAt: new Date().toISOString(),
},
analysisStatus: VariantAnalysisRepoStatus.InProgress,
resultCount: 0,
artifactSizeInBytes: 0,
},
{
repository: {
id: 2,
fullName: "org/repo2",
private: false,
stargazersCount: 100,
updatedAt: new Date().toISOString(),
},
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
resultCount: 0,
artifactSizeInBytes: 0,
},
{
repository: {
id: 3,
fullName: "org/repo3",
private: false,
stargazersCount: 100,
updatedAt: new Date().toISOString(),
},
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
resultCount: 1,
artifactSizeInBytes: 0,
},
{
repository: {
id: 4,
fullName: "org/repo4",
private: false,
stargazersCount: 100,
updatedAt: new Date().toISOString(),
},
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
resultCount: 3,
artifactSizeInBytes: 0,
},
],
});
const repoResults: VariantAnalysisScannedRepositoryResult[] = (
variantAnalysis.scannedRepos || []
).map((repo) => ({
variantAnalysisId: variantAnalysis.id,
repositoryId: repo.repository.id,
interpretedResults: [],
}));
const repoResults: VariantAnalysisScannedRepositoryResult[] = [
{
variantAnalysisId: variantAnalysis.id,
repositoryId: 2,
interpretedResults: [createMockAnalysisAlert(), createMockAnalysisAlert()],
},
{
variantAnalysisId: variantAnalysis.id,
repositoryId: 3,
interpretedResults: [
createMockAnalysisAlert(),
createMockAnalysisAlert(),
createMockAnalysisAlert(),
],
},
{
variantAnalysisId: variantAnalysis.id,
repositoryId: 4,
interpretedResults: [createMockAnalysisAlert()],
},
];
export const ModelAlerts = Template.bind({});
ModelAlerts.args = {

Просмотреть файл

@ -11,6 +11,12 @@ import { vscode } from "../vscode-api";
import { ModelAlertsResults } from "./ModelAlertsResults";
import type { ModelAlerts } from "../../model-editor/model-alerts/model-alerts";
import { calculateModelAlerts } from "../../model-editor/model-alerts/alert-processor";
import { ModelAlertsSearchSortRow } from "./ModelAlertsSearchSortRow";
import {
defaultFilterSortState,
filterAndSort,
} from "../../model-editor/shared/model-alerts-filter-sort";
import type { ModelAlertsFilterSortState } from "../../model-editor/shared/model-alerts-filter-sort";
type Props = {
initialViewState?: ModelAlertsViewState;
@ -53,6 +59,9 @@ export function ModelAlerts({
const [repoResults, setRepoResults] =
useState<VariantAnalysisScannedRepositoryResult[]>(initialRepoResults);
const [filterSortValue, setFilterSortValue] =
useState<ModelAlertsFilterSortState>(defaultFilterSortState);
useEffect(() => {
const listener = (evt: MessageEvent) => {
if (evt.origin === window.origin) {
@ -97,8 +106,10 @@ export function ModelAlerts({
return [];
}
return calculateModelAlerts(variantAnalysis, repoResults);
}, [variantAnalysis, repoResults]);
const modelAlerts = calculateModelAlerts(variantAnalysis, repoResults);
return filterAndSort(modelAlerts, filterSortValue);
}, [filterSortValue, variantAnalysis, repoResults]);
if (viewState === undefined || variantAnalysis === undefined) {
return <></>;
@ -125,6 +136,10 @@ export function ModelAlerts({
></ModelAlertsHeader>
<div>
<SectionTitle>Model alerts</SectionTitle>
<ModelAlertsSearchSortRow
filterSortValue={filterSortValue}
onFilterSortChange={setFilterSortValue}
/>
<div>
{modelAlerts.map((alerts, i) => (
// We're using the index as the key here which is not recommended.

Просмотреть файл

@ -0,0 +1,87 @@
import { useCallback } from "react";
import type { Dispatch, SetStateAction } from "react";
import { styled } from "styled-components";
import type {
ModelAlertsFilterSortState,
SortKey,
} from "../../model-editor/shared/model-alerts-filter-sort";
import { SearchBox } from "../common/SearchBox";
import { ModelAlertsSort } from "./ModelAlertsSort";
type Props = {
filterSortValue: ModelAlertsFilterSortState;
onFilterSortChange: Dispatch<SetStateAction<ModelAlertsFilterSortState>>;
};
const Container = styled.div`
display: flex;
gap: 1em;
width: 100%;
margin-bottom: 1em;
`;
const ModelsSearchColumn = styled(SearchBox)`
flex: 2;
`;
const RepositoriesSearchColumn = styled(SearchBox)`
flex: 2;
`;
const SortColumn = styled(ModelAlertsSort)`
flex: 1;
`;
export const ModelAlertsSearchSortRow = ({
filterSortValue,
onFilterSortChange,
}: Props) => {
const handleModelSearchValueChange = useCallback(
(searchValue: string) => {
onFilterSortChange((oldValue) => ({
...oldValue,
modelSearchValue: searchValue,
}));
},
[onFilterSortChange],
);
const handleRepositorySearchValueChange = useCallback(
(searchValue: string) => {
onFilterSortChange((oldValue) => ({
...oldValue,
repositorySearchValue: searchValue,
}));
},
[onFilterSortChange],
);
const handleSortKeyChange = useCallback(
(sortKey: SortKey) => {
onFilterSortChange((oldValue) => ({
...oldValue,
sortKey,
}));
},
[onFilterSortChange],
);
return (
<Container>
<ModelsSearchColumn
placeholder="Filter by model"
value={filterSortValue.modelSearchValue}
onChange={handleModelSearchValueChange}
/>
<RepositoriesSearchColumn
placeholder="Filter by repository owner/name"
value={filterSortValue.repositorySearchValue}
onChange={handleRepositorySearchValueChange}
/>
<SortColumn
value={filterSortValue.sortKey}
onChange={handleSortKeyChange}
/>
</Container>
);
};

Просмотреть файл

@ -0,0 +1,37 @@
import { useCallback } from "react";
import { styled } from "styled-components";
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react";
import { SortKey } from "../../model-editor/shared/model-alerts-filter-sort";
import { Codicon } from "../common";
const Dropdown = styled(VSCodeDropdown)`
width: 100%;
`;
type Props = {
value: SortKey;
onChange: (value: SortKey) => void;
className?: string;
};
export const ModelAlertsSort = ({ value, onChange, className }: Props) => {
const handleInput = useCallback(
(e: InputEvent) => {
const target = e.target as HTMLSelectElement;
onChange(target.value as SortKey);
},
[onChange],
);
return (
<Dropdown value={value} onInput={handleInput} className={className}>
<Codicon name="sort-precedence" label="Sort..." slot="indicator" />
<VSCodeOption value={SortKey.Alphabetically}>Alphabetically</VSCodeOption>
<VSCodeOption value={SortKey.NumberOfResults}>
Number of results
</VSCodeOption>
</Dropdown>
);
};

Просмотреть файл

@ -0,0 +1,158 @@
import type { ModelAlerts } from "../../../../src/model-editor/model-alerts/model-alerts";
import type { ModelAlertsFilterSortState } from "../../../../src/model-editor/shared/model-alerts-filter-sort";
import {
SortKey,
filterAndSort,
} from "../../../../src/model-editor/shared/model-alerts-filter-sort";
import { createSinkModeledMethod } from "../../../factories/model-editor/modeled-method-factories";
import { createMockAnalysisAlert } from "../../../factories/variant-analysis/shared/analysis-alert";
import { shuffle } from "../../../vscode-tests/utils/list-helpers";
describe("model alerts filter sort", () => {
const modelAlerts: ModelAlerts[] = [
{
model: createSinkModeledMethod({
signature: "foo.m1",
}),
alerts: [
{
alert: createMockAnalysisAlert(),
repository: {
id: 1,
fullName: "r1",
},
},
{
alert: createMockAnalysisAlert(),
repository: {
id: 2,
fullName: "r2",
},
},
{
alert: createMockAnalysisAlert(),
repository: {
id: 3,
fullName: "r3",
},
},
{
alert: createMockAnalysisAlert(),
repository: {
id: 4,
fullName: "r4",
},
},
],
},
{
model: createSinkModeledMethod({
signature: "foo.m2",
}),
alerts: [
{
alert: createMockAnalysisAlert(),
repository: {
id: 1,
fullName: "r1",
},
},
{
alert: createMockAnalysisAlert(),
repository: {
id: 2,
fullName: "r2",
},
},
],
},
{
model: createSinkModeledMethod({
signature: "bar.m1",
}),
alerts: [
{
alert: createMockAnalysisAlert(),
repository: {
id: 1,
fullName: "r1",
},
},
],
},
];
it("should return an empty array if no model alerts", () => {
const filterSortState: ModelAlertsFilterSortState = {
modelSearchValue: "",
repositorySearchValue: "",
sortKey: SortKey.Alphabetically,
};
const result = filterAndSort([], filterSortState);
expect(result).toEqual([]);
});
it("should filter model alerts based on the model search value", () => {
const filterSortState: ModelAlertsFilterSortState = {
modelSearchValue: "m1",
repositorySearchValue: "",
sortKey: SortKey.Alphabetically,
};
const result = filterAndSort(modelAlerts, filterSortState);
expect(result.includes(modelAlerts[0])).toBeTruthy();
expect(result.includes(modelAlerts[2])).toBeTruthy();
});
it("should filter model alerts based on the repository search value", () => {
const filterSortState: ModelAlertsFilterSortState = {
modelSearchValue: "",
repositorySearchValue: "r2",
sortKey: SortKey.Alphabetically,
};
const result = filterAndSort(modelAlerts, filterSortState);
expect(result.includes(modelAlerts[0])).toBeTruthy();
expect(result.includes(modelAlerts[1])).toBeTruthy();
});
it("should sort model alerts alphabetically", () => {
const filterSortState: ModelAlertsFilterSortState = {
modelSearchValue: "",
repositorySearchValue: "",
sortKey: SortKey.Alphabetically,
};
const result = filterAndSort(shuffle([...modelAlerts]), filterSortState);
expect(result).toEqual([modelAlerts[2], modelAlerts[0], modelAlerts[1]]);
});
it("should sort model alerts by number of results", () => {
const filterSortState: ModelAlertsFilterSortState = {
modelSearchValue: "",
repositorySearchValue: "",
sortKey: SortKey.NumberOfResults,
};
const result = filterAndSort(shuffle([...modelAlerts]), filterSortState);
expect(result).toEqual([modelAlerts[0], modelAlerts[1], modelAlerts[2]]);
});
it("should filter and sort model alerts", () => {
const filterSortState: ModelAlertsFilterSortState = {
modelSearchValue: "m1",
repositorySearchValue: "r1",
sortKey: SortKey.NumberOfResults,
};
const result = filterAndSort(shuffle([...modelAlerts]), filterSortState);
expect(result).toEqual([modelAlerts[0], modelAlerts[2]]);
});
});