Skip to content
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
2 changes: 2 additions & 0 deletions config.example.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ module.exports = {
name: "owner/otherRepo",
requiredStatuses: ["tests", "build", "codeClimate"],
ignoredStatuses: ["coverage"],
// Hides pulls on this repo on page load, users can unhide them with the repo filter
hideByDefault: true,
},
],

Expand Down
91 changes: 74 additions & 17 deletions frontend/src/filter-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,37 @@ import {
MenuDivider,
MenuOptionGroup,
MenuItemOption,
chakra,
} from "@chakra-ui/react";
import { useEffect, useMemo } from "react";
import { countBy } from "lodash-es";

// Map from value to number of pulls that have that value
type ValueGetter = (pull: Pull) => string;

const SHOWALL = "SHOWALL";

type FilterMenuProps = {
urlParam: string;
buttonText: string;
extractValueFromPull: ValueGetter;
defaultExculdedValues?: string[];
};

export function FilterMenu({
urlParam,
buttonText,
extractValueFromPull,
defaultExculdedValues,
}: FilterMenuProps) {
const pulls = useAllOpenPulls();
// Default is empty array that implies show all pulls (no filtering)
const [selectedValues, setSelectedValues] = useArrayUrlState(urlParam, []);
// Nothing selected == show everything, otherwise, it'd be empty
const showAll = selectedValues.length === 0;
// Nothing selected == show the default values (everything except the excluded values)
const showDefault = selectedValues.length === 0;
// Show every single value if the magic SHOWALL string is selected
const showAll = notEmpty(defaultExculdedValues) ? selectedValues.includes(SHOWALL) : selectedValues.length === 0;

// List from url may contain values we have no pulls for
const urlValues = useConst(() => new Set(selectedValues));
const setPullFilter = useSetFilter();
Expand All @@ -41,50 +49,87 @@ export function FilterMenu({
const allValues = useMemo(() => {
// All values of open pulls
const pullValues = new Set<string>(pulls.map(extractValueFromPull));
return sortValues([...new Set([...pullValues, ...urlValues])]);
const allValuesSet = new Set([...pullValues, ...urlValues]);
allValuesSet.delete(SHOWALL);
return sortValues([...allValuesSet]);
}, [pulls]);

const valueToPullCount = useMemo(
() => countBy(pulls, extractValueFromPull),
[pulls]
);

const defaultSelectedValues = arrayDiff(allValues, defaultExculdedValues || []);

useEffect(() => {
const selectedValuesSet = new Set(selectedValues);
setPullFilter(
urlParam,
selectedValuesSet.size === 0
showAll
? null
: (pull) => selectedValuesSet.has(extractValueFromPull(pull))
: (showDefault ? (pull) => !defaultExculdedValues?.includes(extractValueFromPull(pull))
: (pull) => selectedValuesSet.has(extractValueFromPull(pull)))
);
}, [selectedValues]);
}, [selectedValues, defaultExculdedValues]);

const numberText = showDefault ? "" : (showAll ? allValues.length : selectedValues.length);

return (
<Menu closeOnSelect={false}>
<MenuButton
as={Button}
colorScheme="blue"
size="sm"
variant={showAll ? "outline" : null}
variant={showDefault ? "outline" : null}
>
{buttonText} {selectedValues.length ? `(${selectedValues.length})` : ""}
{buttonText} {numberText ? `(${numberText})` : ""}
</MenuButton>
<MenuList minWidth="240px">
<MenuItemOption
key="Show All"
onClick={() => setSelectedValues([])}
isChecked={showAll}
>
Show All
</MenuItemOption>
{notEmpty(defaultExculdedValues) && (
<>
<MenuItemOption
key="Show All"
onClick={() => setSelectedValues([SHOWALL])}
>
Show All
</MenuItemOption>
<MenuItemOption
key="Show Default"
onClick={() => setSelectedValues([])}
>
Show Default
</MenuItemOption>
</>)
}
{empty(defaultExculdedValues) &&
<MenuItemOption
key="Show All"
onClick={() => setSelectedValues([])}
>
Show All
</MenuItemOption>
}
<MenuDivider />
<MenuOptionGroup
type="checkbox"
value={showAll ? [] : selectedValues}
value={showAll ? allValues : (showDefault ? defaultSelectedValues : selectedValues)}
onChange={setSelectedValues}
>
{allValues.map((value) => (
<MenuItemOption key={value} value={value}>
<MenuItemOption className="filterOption" key={value} value={value}>
{value} ({valueToPullCount[value] || 0})
<chakra.span
visibility="hidden"
float="right"
_hover={{textDecoration:"underline"}}
mt={1}
fontSize="xs"
sx={{'.filterOption:hover &': {visibility: 'visible'}}}
onClick={(e)=> {
setSelectedValues([value]);
e.stopPropagation();
}
}>only</chakra.span>
</MenuItemOption>
))}
</MenuOptionGroup>
Expand All @@ -98,3 +143,15 @@ function sortValues(values: string[]): string[] {
a.localeCompare(b, undefined, { sensitivity: "base" })
);
}

function arrayDiff<T>(a: T[], b: T[]): T[] {
return a.filter((x) => !b.includes(x));
}

function empty<T>(array: T[] | undefined): boolean {
return !array || array.length === 0;
}

function notEmpty<T>(array: T[] | undefined): boolean {
return !!array && array.length > 0;
}
8 changes: 7 additions & 1 deletion frontend/src/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
useFilteredOpenPulls,
useAllOpenPulls,
useSetFilter,
useRepoSpecs,
} from "./pulldasher/pulls-context";
import { Pull } from "./pull";
import {
Expand All @@ -21,7 +22,7 @@ import {
MenuList,
Text
} from "@chakra-ui/react";
import { useRef, useEffect, useCallback } from "react";
import { useRef, useEffect, useCallback, useMemo } from "react";
import { useBoolUrlState } from "./use-url-state";
import { NotificationRequest } from "./notifications";
import { useConnectionState, ConnectionState } from "./backend/socket";
Expand Down Expand Up @@ -51,6 +52,10 @@ export function Navbar(props: NavBarProps) {
const pulls: Set<Pull> = useFilteredOpenPulls();
const allOpenPulls: Pull[] = useAllOpenPulls();
const setPullFilter = useSetFilter();
const repoSpecs = useRepoSpecs();
const reposToHide = useMemo(() =>
repoSpecs.filter((repo) => repo.hideByDefault).map((repo) => repo.name.replace(/.*\//g, "")),
[repoSpecs]);
const { toggleColorMode } = useColorMode();
const [showCryo, toggleShowCryo] = useBoolUrlState("cryo", false);
const [showExtBlocked, toggleShowExtBlocked] = useBoolUrlState(
Expand Down Expand Up @@ -174,6 +179,7 @@ export function Navbar(props: NavBarProps) {
<FilterMenu
urlParam="repo"
buttonText="Repo"
defaultExculdedValues={reposToHide}
extractValueFromPull={(pull: Pull) => pull.getRepoName()}
/>
</Box>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pull-card/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,9 @@ const formatDate = (dateStr: string | null) => {
return dateStr ? formatter.format(new Date(dateStr)) : null;
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function highlightOnChange(
ref: RefObject<HTMLElement>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
dependencies: Array<any>
) {
// Animate a highlight when pull.received_at changes
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/pulldasher/pulls-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "./filtered-pulls-state";
import { usePullsState } from "./pulls-state";
import { Pull } from "../pull";
import { RepoSpec } from "../types";
import { defaultCompare } from "./sort";

interface PullContextProps {
Expand All @@ -19,6 +20,8 @@ interface PullContextProps {
filteredOpenPulls: Set<Pull>;
// Changes the filter function
setFilter: FilterFunctionSetter;
// RepoSpecs (.repos) from the config
repoSpecs: RepoSpec[];
}

const defaultProps = {
Expand All @@ -29,6 +32,7 @@ const defaultProps = {
// Default implementation is a no-op, just so there's
// something there until the provider is used
setFilter: (name: string, filter: FilterFunction) => filter,
repoSpecs: [],
};
const PullsContext = createContext<PullContextProps>(defaultProps);

Expand All @@ -52,12 +56,16 @@ export function useSetFilter(): FilterFunctionSetter {
return useContext(PullsContext).setFilter;
}

export function useRepoSpecs(): RepoSpec[] {
return useContext(PullsContext).repoSpecs;
}

export const PullsProvider = function ({
children,
}: {
children: React.ReactNode;
}) {
const unfilteredPulls = usePullsState();
const {pullState: unfilteredPulls, repoSpecs} = usePullsState();
const [filteredPulls, setFilter] = useFilteredPullsState(unfilteredPulls);
const openPulls = unfilteredPulls.filter(isOpen);
const contextValue = {
Expand All @@ -66,6 +74,7 @@ export const PullsProvider = function ({
filteredPulls: filteredPulls,
allPulls: unfilteredPulls,
setFilter,
repoSpecs,
};
return (
<PullsContext.Provider value={contextValue}>
Expand Down
21 changes: 14 additions & 7 deletions frontend/src/pulldasher/pulls-state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@ import { createPullSocket } from "../backend/pull-socket";
import { PullData, RepoSpec } from "../types";
import { Pull } from "../pull";

function onPullsChanged(pullsChanged: (pulls: Pull[]) => void) {
function onPullsChanged(pullsChanged: (pulls: Pull[], repoSpecs: RepoSpec[]) => void) {
const pulls: Record<string, Pull> = {};
const pullRefresh = () => pullsChanged(Object.values(pulls));
const pullRefresh = () => pullsChanged(Object.values(pulls), repoSpecs);
const throttledPullRefresh: () => void = throttle(pullRefresh, 500);
let repoSpecs: RepoSpec[] = [];

createPullSocket((pullDatas: PullData[], repoSpecs: RepoSpec[]) => {
createPullSocket((pullDatas: PullData[], newRepoSpecs: RepoSpec[]) => {
pullDatas.forEach((pullData: PullData) => {
pullData.repoSpec =
repoSpecs.find((repo) => repo.name == pullData.repo) || null;
newRepoSpecs.find((repo) => repo.name == pullData.repo) || null;
pullData.received_at = new Date();
const pull: Pull = new Pull(pullData);
pulls[pull.getKey()] = pull;
});

repoSpecs = newRepoSpecs || [];
throttledPullRefresh();
});
}
Expand All @@ -25,16 +28,20 @@ function onPullsChanged(pullsChanged: (pulls: Pull[]) => void) {
* Note: This is only meant to be used in one component
*/
let socketInitialized = false;
export function usePullsState(): Pull[] {
export function usePullsState() {
const [pullState, setPullsState] = useState<Pull[]>([]);
const [repoSpecs, setRepoSpecs] = useState<RepoSpec[]>([]);
useEffect(() => {
if (socketInitialized) {
throw new Error(
"usePullsState() connects to socket-io and is only meant to be used in the PullsProvider component, see useFilteredOpenPulls() instead."
);
}
socketInitialized = true;
onPullsChanged(setPullsState);
onPullsChanged((pulls, repoSpecs) => {
setPullsState(pulls);
setRepoSpecs(repoSpecs);
});
}, []);
return pullState;
return {pullState, repoSpecs};
}
1 change: 1 addition & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface SignatureUser {
export interface RepoSpec {
requiredStatuses?: string[];
ignoredStatuses?: string[];
hideByDefault?: boolean;
name: string;
}

Expand Down
Loading