Added filtering and sorting in the model alerts view (#3509)
This commit is contained in:
Родитель
8b6a9352f6
Коммит
21c33b762c
|
@ -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]]);
|
||||
});
|
||||
});
|
Загрузка…
Ссылка в новой задаче