Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a component for filtering repositories based on their results #2343

Merged
merged 13 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions extensions/ql-vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [UNRELEASED]

- Added ability to filter repositories for a variant analysis to only those that have results [#2343](https://github.com/github/vscode-codeql/pull/2343)
- Add new configuration option to allow downloading databases from http, non-secure servers. [#2332](https://github.com/github/vscode-codeql/pull/2332)

## 1.8.2 - 12 April 2023
Expand Down
58 changes: 48 additions & 10 deletions extensions/ql-vscode/src/pure/variant-analysis-filter-sort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import {
RepositoryWithMetadata,
} from "../variant-analysis/shared/repository";
import { parseDate } from "./date";
import { assertNever } from "./helpers-pure";

export enum FilterKey {
All = "all",
WithResults = "withResults",
}

export enum SortKey {
Name = "name",
Expand All @@ -13,6 +19,7 @@ export enum SortKey {

export type RepositoriesFilterSortState = {
searchValue: string;
filterKey: FilterKey;
sortKey: SortKey;
};

Expand All @@ -22,20 +29,43 @@ export type RepositoriesFilterSortStateWithIds = RepositoriesFilterSortState & {

export const defaultFilterSortState: RepositoriesFilterSortState = {
searchValue: "",
filterKey: FilterKey.All,
sortKey: SortKey.Name,
};

export function matchesFilter(
repo: Pick<Repository, "fullName">,
item: FilterAndSortableResult,
filterSortState: RepositoriesFilterSortState | undefined,
): boolean {
if (!filterSortState) {
return true;
}

return repo.fullName
.toLowerCase()
.includes(filterSortState.searchValue.toLowerCase());
return (
matchesSearch(item.repository, filterSortState.searchValue) &&
matchesFilterKey(item.resultCount, filterSortState.filterKey)
);
}

function matchesSearch(
repository: SortableRepository,
searchValue: string,
): boolean {
return repository.fullName.toLowerCase().includes(searchValue.toLowerCase());
}

function matchesFilterKey(
resultCount: number | undefined,
filterKey: FilterKey,
): boolean {
switch (filterKey) {
case FilterKey.All:
return true;
case FilterKey.WithResults:
return resultCount !== undefined && resultCount > 0;
default:
assertNever(filterKey);
}
}

type SortableRepository = Pick<Repository, "fullName"> &
Expand Down Expand Up @@ -71,17 +101,22 @@ export function compareRepository(
};
}

type SortableResult = {
type FilterAndSortableResult = {
repository: SortableRepository;
resultCount?: number;
};

type FilterAndSortableResultWithIds = {
repository: SortableRepository & Pick<Repository, "id">;
resultCount?: number;
};

export function compareWithResults(
filterSortState: RepositoriesFilterSortState | undefined,
): (left: SortableResult, right: SortableResult) => number {
): (left: FilterAndSortableResult, right: FilterAndSortableResult) => number {
const fallbackSort = compareRepository(filterSortState);

return (left: SortableResult, right: SortableResult) => {
return (left: FilterAndSortableResult, right: FilterAndSortableResult) => {
// Highest to lowest
if (filterSortState?.sortKey === SortKey.ResultsCount) {
const resultCount = (right.resultCount ?? 0) - (left.resultCount ?? 0);
Expand All @@ -95,7 +130,7 @@ export function compareWithResults(
}

export function filterAndSortRepositoriesWithResultsByName<
T extends SortableResult,
T extends FilterAndSortableResult,
>(
repositories: T[] | undefined,
filterSortState: RepositoriesFilterSortState | undefined,
Expand All @@ -105,18 +140,21 @@ export function filterAndSortRepositoriesWithResultsByName<
}

return repositories
.filter((repo) => matchesFilter(repo.repository, filterSortState))
.filter((repo) => matchesFilter(repo, filterSortState))
.sort(compareWithResults(filterSortState));
}

export function filterAndSortRepositoriesWithResults<T extends SortableResult>(
export function filterAndSortRepositoriesWithResults<
T extends FilterAndSortableResultWithIds,
>(
repositories: T[] | undefined,
filterSortState: RepositoriesFilterSortStateWithIds | undefined,
): T[] | undefined {
if (!repositories) {
return undefined;
}

// If repository IDs are given, then ignore the search value and filter key
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Just a question around IDs, since I don't really understand where they come in to play with filtering 😅 When would repository IDs "be given"? As in, where do they come from?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! I believe the answer is whenever you're operating on a list of repos selected by the user, such as when exporting results or copying a repo list to the clipboard. A user can use the tickboxes to select repos, and we pass that list into here using the repository IDs.

if (
filterSortState?.repositoryIds &&
filterSortState.repositoryIds.length > 0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from "react";
import { useState } from "react";

import { ComponentMeta } from "@storybook/react";

import { RepositoriesFilter as RepositoriesFilterComponent } from "../../view/variant-analysis/RepositoriesFilter";
import { FilterKey } from "../../pure/variant-analysis-filter-sort";

export default {
title: "Variant Analysis/Repositories Filter",
component: RepositoriesFilterComponent,
argTypes: {
value: {
control: {
disable: true,
},
},
},
} as ComponentMeta<typeof RepositoriesFilterComponent>;

export const RepositoriesFilter = () => {
const [value, setValue] = useState(FilterKey.All);

return <RepositoriesFilterComponent value={value} onChange={setValue} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from "react";
import { useCallback } from "react";
import styled from "styled-components";
import { VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react";
import { Codicon } from "../common";
import { FilterKey } from "../../pure/variant-analysis-filter-sort";

const Dropdown = styled(VSCodeDropdown)`
width: 100%;
`;

type Props = {
value: FilterKey;
onChange: (value: FilterKey) => void;

className?: string;
};

export const RepositoriesFilter = ({ value, onChange, className }: Props) => {
const handleInput = useCallback(
(e: InputEvent) => {
const target = e.target as HTMLSelectElement;

onChange(target.value as FilterKey);
},
[onChange],
);

return (
<Dropdown value={value} onInput={handleInput} className={className}>
<Codicon name="list-filter" label="Filter..." slot="indicator" />
<VSCodeOption value={FilterKey.All}>All</VSCodeOption>
<VSCodeOption value={FilterKey.WithResults}>With results</VSCodeOption>
</Dropdown>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import * as React from "react";
import { Dispatch, SetStateAction, useCallback } from "react";
import styled from "styled-components";
import {
FilterKey,
RepositoriesFilterSortState,
SortKey,
} from "../../pure/variant-analysis-filter-sort";
import { RepositoriesSearch } from "./RepositoriesSearch";
import { RepositoriesSort } from "./RepositoriesSort";
import { RepositoriesFilter } from "./RepositoriesFilter";

type Props = {
value: RepositoriesFilterSortState;
Expand All @@ -25,6 +27,10 @@ const RepositoriesSearchColumn = styled(RepositoriesSearch)`
flex: 3;
`;

const RepositoriesFilterColumn = styled(RepositoriesFilter)`
flex: 1;
`;

const RepositoriesSortColumn = styled(RepositoriesSort)`
flex: 1;
`;
Expand All @@ -40,6 +46,16 @@ export const RepositoriesSearchSortRow = ({ value, onChange }: Props) => {
[onChange],
);

const handleFilterKeyChange = useCallback(
(filterKey: FilterKey) => {
onChange((oldValue) => ({
...oldValue,
filterKey,
}));
},
[onChange],
);

const handleSortKeyChange = useCallback(
(sortKey: SortKey) => {
onChange((oldValue) => ({
Expand All @@ -56,6 +72,10 @@ export const RepositoriesSearchSortRow = ({ value, onChange }: Props) => {
value={value.searchValue}
onChange={handleSearchValueChange}
/>
<RepositoriesFilterColumn
value={value.filterKey}
onChange={handleFilterKeyChange}
/>
<RepositoriesSortColumn
value={value.sortKey}
onChange={handleSortKeyChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ export const VariantAnalysisSkippedRepositoriesTab = ({
}: VariantAnalysisSkippedRepositoriesTabProps) => {
const repositories = useMemo(() => {
return skippedRepositoryGroup.repositories
?.filter((repo) => {
return matchesFilter(repo, filterSortState);
?.filter((repository) => {
return matchesFilter({ repository }, filterSortState);
})
?.sort(compareRepository(filterSortState));
}, [filterSortState, skippedRepositoryGroup.repositories]);
Expand Down
Loading