diff --git a/cypress/integration/version/downstream_projects.ts b/cypress/integration/version/downstream_projects.ts
index 2cadc53ce4..e4cb8376be 100644
--- a/cypress/integration/version/downstream_projects.ts
+++ b/cypress/integration/version/downstream_projects.ts
@@ -5,16 +5,15 @@ describe("Downstream Projects Tab", () => {
cy.visit(DOWNSTREAM_ROUTE);
});
- it("shows the number of failed patches in the Downstream tab label", () => {
+ it("shows number of failed patches in the Downstream tab label", () => {
cy.dataCy("downstream-tab-badge").should("exist");
cy.dataCy("downstream-tab-badge").should("contain.text", "1");
});
- it("shows the child patches", () => {
+ it("renders child patches", () => {
cy.dataCy("project-accordion").should("have.length", 3);
cy.dataCy("project-title").should("have.length", 3);
- // On CI, none of the child patches failed, so no tables should be visible.
- cy.dataCy("tasks-table").should("not.be.visible");
+ cy.dataCy("downstream-tasks-table").should("have.length", 3);
});
it("links to base commit", () => {
@@ -28,15 +27,15 @@ describe("Downstream Projects Tab", () => {
});
it("filters by test name", () => {
- cy.dataCy("accordion-toggle").first().click();
- cy.get("tbody").first().children().should("have.length", 1);
- cy.toggleTableFilter(1);
- cy.dataCy("taskname-input-wrapper")
+ cy.dataCy("task-name-filter").eq(1).click();
+ cy.dataCy("task-name-filter-wrapper")
.find("input")
- .focus()
- .type("filter")
- .type("{enter}");
- cy.get("tbody").first().contains("No Data");
+ .as("testnameInputWrapper");
+ cy.get("@testnameInputWrapper").focus();
+ cy.get("@testnameInputWrapper").type("generate-lint");
+ cy.get("@testnameInputWrapper").type("{enter}");
+ cy.location("search").should("not.contain", "generate-lint"); // Should not update the URL.
+ cy.contains("generate-lint").should("be.visible");
});
it("does not push query params to the URL", () => {
diff --git a/src/analytics/patch/usePatchAnalytics.ts b/src/analytics/patch/usePatchAnalytics.ts
index 2a62c3e4ad..9cdf21a970 100644
--- a/src/analytics/patch/usePatchAnalytics.ts
+++ b/src/analytics/patch/usePatchAnalytics.ts
@@ -18,13 +18,13 @@ type Action =
| TaskSortCategory.BaseStatus
| TaskSortCategory.Variant;
}
+ | {
+ name: "Filter Downstream Tasks Table";
+ filterBy: string | string[];
+ }
| {
name: "Sort Downstream Tasks Table";
- sortBy:
- | TaskSortCategory.Name
- | TaskSortCategory.Status
- | TaskSortCategory.BaseStatus
- | TaskSortCategory.Variant;
+ sortBy: TaskSortCategory | TaskSortCategory[];
}
| { name: "Restart"; abort: boolean }
| { name: "Schedule" }
diff --git a/src/analytics/version/useVersionAnalytics.ts b/src/analytics/version/useVersionAnalytics.ts
index fd14ff691a..921e3a0a54 100644
--- a/src/analytics/version/useVersionAnalytics.ts
+++ b/src/analytics/version/useVersionAnalytics.ts
@@ -18,13 +18,13 @@ type Action =
| TaskSortCategory.BaseStatus
| TaskSortCategory.Variant;
}
+ | {
+ name: "Filter Downstream Tasks Table";
+ filterBy: string | string[];
+ }
| {
name: "Sort Downstream Tasks Table";
- sortBy:
- | TaskSortCategory.Name
- | TaskSortCategory.Status
- | TaskSortCategory.BaseStatus
- | TaskSortCategory.Variant;
+ sortBy: TaskSortCategory | TaskSortCategory[];
}
| { name: "Restart"; abort: boolean }
| { name: "Schedule" }
diff --git a/src/components/TasksTable/Columns.tsx b/src/components/TasksTable/Columns.tsx
index 326b95e3f6..d2f5736080 100644
--- a/src/components/TasksTable/Columns.tsx
+++ b/src/components/TasksTable/Columns.tsx
@@ -46,7 +46,7 @@ export const getColumnsTemplate = ({
),
meta: {
search: {
- "data-cy": "task-name-filter-popover",
+ "data-cy": "task-name-filter",
placeholder: "Task name regex",
},
},
@@ -87,7 +87,7 @@ export const getColumnsTemplate = ({
},
meta: {
treeSelect: {
- "data-cy": "status-filter-popover",
+ "data-cy": "status-filter",
options: statusOptions,
},
},
@@ -113,7 +113,7 @@ export const getColumnsTemplate = ({
),
meta: {
treeSelect: {
- "data-cy": "base-status-filter-popover",
+ "data-cy": "base-status-filter",
options: baseStatusOptions,
},
},
@@ -145,7 +145,7 @@ export const getColumnsTemplate = ({
),
meta: {
search: {
- "data-cy": "variant-filter-popover",
+ "data-cy": "variant-filter",
placeholder: "Variant name regex",
},
},
diff --git a/src/pages/version/downstreamTasks/DownstreamProjectAccordion.tsx b/src/pages/version/downstreamTasks/DownstreamProjectAccordion.tsx
index a2b68f1f8b..13cc2c3a97 100644
--- a/src/pages/version/downstreamTasks/DownstreamProjectAccordion.tsx
+++ b/src/pages/version/downstreamTasks/DownstreamProjectAccordion.tsx
@@ -2,15 +2,9 @@ import { useReducer } from "react";
import { useQuery } from "@apollo/client";
import styled from "@emotion/styled";
import { InlineCode } from "@leafygreen-ui/typography";
-import { Skeleton } from "antd";
-import { TableProps } from "antd/es/table";
-import { Link, useParams } from "react-router-dom";
-import { useVersionAnalytics } from "analytics";
+import { Link } from "react-router-dom";
import { Accordion } from "components/Accordion";
import { PatchStatusBadge } from "components/PatchStatusBadge";
-import TableControl from "components/Table/TableControl";
-import TableWrapper from "components/Table/TableWrapper";
-import TasksTable from "components/TasksTable";
import { getVersionRoute } from "constants/routes";
import { size } from "constants/tokens";
import { useToastContext } from "context/toast";
@@ -18,21 +12,31 @@ import {
Parameter,
SortDirection,
SortOrder,
- Task,
TaskSortCategory,
VersionTasksQuery,
VersionTasksQueryVariables,
} from "gql/generated/types";
import { VERSION_TASKS } from "gql/queries";
-import { usePolling, useTaskStatuses } from "hooks";
+import { usePolling } from "hooks";
import { PatchStatus } from "types/patch";
-import { queryString, string } from "utils";
+import { string } from "utils";
import { ParametersModal } from "../ParametersModal";
+import { DownstreamTasksTable } from "./DownstreamTasksTable";
import { reducer } from "./reducer";
-const { parseSortString, toSortString } = queryString;
const { shortenGithash } = string;
+const defaultSorts: SortOrder[] = [
+ {
+ Key: TaskSortCategory.Status,
+ Direction: SortDirection.Asc,
+ },
+ {
+ Key: TaskSortCategory.BaseStatus,
+ Direction: SortDirection.Desc,
+ },
+];
+
interface DownstreamProjectAccordionProps {
baseVersionID: string;
githash: string;
@@ -56,14 +60,6 @@ export const DownstreamProjectAccordion: React.FC<
}) => {
const dispatchToast = useToastContext();
- const { id } = useParams<{ id: string }>();
- const { sendEvent } = useVersionAnalytics(id);
-
- const defaultSort: SortOrder = {
- Key: TaskSortCategory.Status,
- Direction: SortDirection.Asc,
- };
-
const [state, dispatch] = useReducer(reducer, {
baseStatuses: [],
limit: 10,
@@ -71,107 +67,52 @@ export const DownstreamProjectAccordion: React.FC<
statuses: [],
taskName: "",
variant: "",
- baseStatusesInputVal: [],
- currentStatusesInputVal: [],
- taskNameInputVal: "",
- variantInputVal: "",
- sorts: [defaultSort],
+ sorts: defaultSorts,
});
const { baseStatuses, limit, page, sorts, statuses, taskName, variant } =
state;
- const variables = {
- versionId: childPatchId,
- taskFilterOptions: {
- limit,
- page,
- statuses,
- taskName,
- variant,
- sorts,
- baseStatuses,
- },
- };
-
- const { baseStatusesInputVal, currentStatusesInputVal } = state;
- const { baseStatuses: currentBaseStatuses, currentStatuses } =
- useTaskStatuses({
- versionId: childPatchId,
- });
-
- const taskNameInputProps = {
- placeholder: "Task name",
- value: state.taskNameInputVal,
- onChange: ({ target }) =>
- dispatch({ type: "onChangeTaskNameInput", task: target.value }),
- onFilter: () => dispatch({ type: "onFilterTaskNameInput" }),
- };
-
- const variantInputProps = {
- placeholder: "Variant name",
- value: state.variantInputVal,
- onChange: ({ target }) =>
- dispatch({
- type: "onChangeVariantInput",
- variant: target.value,
- }),
- onFilter: () => dispatch({ type: "onFilterVariantInput" }),
- };
-
- const baseStatusSelectorProps = {
- state: baseStatusesInputVal,
- tData: currentBaseStatuses,
- onChange: (s: string[]) =>
- dispatch({ type: "setAndSubmitBaseStatusesSelector", baseStatuses: s }),
- };
-
- const statusSelectorProps = {
- state: currentStatusesInputVal,
- tData: currentStatuses,
- onChange: (s: string[]) =>
- dispatch({
- type: "setAndSubmitStatusesSelector",
- statuses: s,
- }),
- };
-
const { data, refetch, startPolling, stopPolling } = useQuery<
VersionTasksQuery,
VersionTasksQueryVariables
>(VERSION_TASKS, {
- variables,
+ variables: {
+ versionId: childPatchId,
+ taskFilterOptions: {
+ baseStatuses,
+ limit,
+ page,
+ sorts,
+ statuses,
+ taskName,
+ variant,
+ },
+ },
fetchPolicy: "cache-and-network",
onError: (err) => {
dispatchToast.error(`Error fetching downstream tasks ${err}`);
},
});
usePolling({ startPolling, stopPolling, refetch });
+
const showSkeleton = !data;
const { version } = data || {};
const { isPatch, tasks } = version || {};
const { count = 0, data: tasksData = [] } = tasks || {};
- const variantTitle = (
- <>
-
- {projectName}
-
-
- >
- );
-
- const tableChangeHandler: TableProps["onChange"] = (...[, , sorter]) =>
- dispatch({
- type: "onSort",
- sorts: parseSortString(toSortString(sorter)),
- });
-
return (
+
+ {projectName}
+
+
+ >
+ }
titleTag={FlexContainer}
subtitle={
- dispatch({ type: "clearAllFilters" })}
- onPageChange={(p) => {
- dispatch({ type: "onChangePagination", page: p });
- }}
- onPageSizeChange={(l) => {
- dispatch({ type: "onChangeLimit", limit: l });
- }}
- limit={limit}
- page={page}
- />
- }
- >
- {showSkeleton ? (
-
- ) : (
-
- sendEvent({
- name: "Sort Downstream Tasks Table",
- sortBy: sortField,
- })
- }
- sorts={sorts}
- statusSelectorProps={statusSelectorProps}
- tableChangeHandler={tableChangeHandler}
- taskNameInputProps={taskNameInputProps}
- tasks={tasksData}
- variantInputProps={variantInputProps}
- />
- )}
-
+
);
};
+
interface DownstreamMetadataProps {
baseVersionID: string;
githash: string;
diff --git a/src/pages/version/downstreamTasks/DownstreamTasksTable.tsx b/src/pages/version/downstreamTasks/DownstreamTasksTable.tsx
new file mode 100644
index 0000000000..a5839d90a1
--- /dev/null
+++ b/src/pages/version/downstreamTasks/DownstreamTasksTable.tsx
@@ -0,0 +1,156 @@
+import { useRef, useMemo } from "react";
+import { LeafyGreenTable, useLeafyGreenTable } from "@leafygreen-ui/table";
+import {
+ ColumnFiltersState,
+ Filters,
+ Sorting,
+ SortingState,
+} from "@tanstack/react-table";
+import { useParams } from "react-router-dom";
+import { usePatchAnalytics, useVersionAnalytics } from "analytics";
+import { BaseTable } from "components/Table/BaseTable";
+import TableControl from "components/Table/TableControl";
+import { TablePlaceholder } from "components/Table/TablePlaceholder";
+import TableWrapper from "components/Table/TableWrapper";
+import { onChangeHandler } from "components/Table/utils";
+import { getColumnsTemplate } from "components/TasksTable/Columns";
+import { TaskTableInfo } from "components/TasksTable/types";
+import { SortDirection, TaskSortCategory } from "gql/generated/types";
+import { useTaskStatuses } from "hooks";
+import { Action } from "./reducer";
+
+const { getDefaultOptions: getDefaultFiltering } = Filters;
+const { getDefaultOptions: getDefaultSorting } = Sorting;
+
+interface DownstreamTasksTableProps {
+ childPatchId: string;
+ count: number;
+ dispatch: (action: Action) => void;
+ isPatch: boolean;
+ limit: number;
+ loading: boolean;
+ page: number;
+ taskCount: number;
+ tasks: TaskTableInfo[];
+}
+
+export const DownstreamTasksTable: React.FC = ({
+ childPatchId,
+ count,
+ dispatch,
+ isPatch,
+ limit,
+ loading,
+ page,
+ taskCount,
+ tasks,
+}) => {
+ const { id: versionId } = useParams<{ id: string }>();
+ const { sendEvent } = (isPatch ? usePatchAnalytics : useVersionAnalytics)(
+ versionId,
+ );
+
+ const { baseStatuses: baseStatusOptions, currentStatuses: statusOptions } =
+ useTaskStatuses({ versionId: childPatchId });
+
+ const onFilterChange = (filterState: ColumnFiltersState) => {
+ filterState.forEach(({ id, value }) => {
+ if (id === TaskSortCategory.Name) {
+ dispatch({ type: "setTaskName", task: value as string });
+ } else if (id === TaskSortCategory.Status) {
+ dispatch({ type: "setStatuses", statuses: value as string[] });
+ } else if (id === TaskSortCategory.BaseStatus) {
+ dispatch({ type: "setBaseStatuses", baseStatuses: value as string[] });
+ } else if (id === TaskSortCategory.Variant) {
+ dispatch({ type: "setVariant", variant: value as string });
+ }
+ });
+ sendEvent({
+ name: "Filter Downstream Tasks Table",
+ filterBy: Object.keys(filterState),
+ });
+ };
+
+ const onSortingChange = (sortingState: SortingState) => {
+ const updatedSorts = sortingState.map(({ desc, id }) => ({
+ Key: id as TaskSortCategory,
+ Direction: desc ? SortDirection.Desc : SortDirection.Asc,
+ }));
+ dispatch({ type: "setSorts", sorts: updatedSorts });
+ sendEvent({
+ name: "Sort Downstream Tasks Table",
+ sortBy: sortingState.map(({ id }) => id as TaskSortCategory),
+ });
+ };
+
+ const columns = useMemo(
+ () =>
+ getColumnsTemplate({
+ baseStatusOptions,
+ statusOptions,
+ isPatch,
+ }),
+ [baseStatusOptions, statusOptions, isPatch],
+ );
+
+ const tableContainerRef = useRef(null);
+ const table: LeafyGreenTable =
+ useLeafyGreenTable({
+ columns,
+ containerRef: tableContainerRef,
+ data: tasks ?? [],
+ defaultColumn: {
+ enableMultiSort: true,
+ sortDescFirst: false, // Handle bug in sorting order (https://github.com/TanStack/table/issues/4289)
+ },
+ initialState: {
+ sorting: [
+ { id: TaskSortCategory.Status, desc: false },
+ { id: TaskSortCategory.BaseStatus, desc: true },
+ ],
+ },
+ isMultiSortEvent: () => true, // Override default requirement for shift-click to multisort.
+ maxMultiSortColCount: 2,
+ manualFiltering: true,
+ manualPagination: true,
+ manualSorting: true,
+ onColumnFiltersChange: onChangeHandler(
+ (f) => getDefaultFiltering(table).onColumnFiltersChange(f),
+ onFilterChange,
+ ),
+ onSortingChange: onChangeHandler(
+ (s) => getDefaultSorting(table).onSortingChange(s),
+ onSortingChange,
+ ),
+ getSubRows: (row) => row.executionTasksFull || [],
+ });
+
+ return (
+ {
+ dispatch({ type: "clearAllFilters" });
+ table.reset();
+ }}
+ onPageChange={(p) => dispatch({ type: "setPage", page: p })}
+ onPageSizeChange={(l) => dispatch({ type: "setLimit", limit: l })}
+ page={page}
+ totalCount={taskCount}
+ />
+ }
+ >
+ }
+ loading={loading}
+ shouldAlternateRowColor
+ table={table}
+ />
+
+ );
+};
diff --git a/src/pages/version/downstreamTasks/reducer.ts b/src/pages/version/downstreamTasks/reducer.ts
index d6a21b902a..8b6d53ac51 100644
--- a/src/pages/version/downstreamTasks/reducer.ts
+++ b/src/pages/version/downstreamTasks/reducer.ts
@@ -10,59 +10,41 @@ interface QueryParamState {
variant: string;
}
-interface InputValueState {
- baseStatusesInputVal: string[];
- currentStatusesInputVal: string[];
- taskNameInputVal: string;
- variantInputVal: string;
-}
-
export type Action =
- | { type: "onChangeTaskNameInput"; task: string }
- | { type: "onChangeVariantInput"; variant: string }
- | { type: "onFilterTaskNameInput" }
- | { type: "onFilterVariantInput" }
- | { type: "setAndSubmitBaseStatusesSelector"; baseStatuses: string[] }
- | { type: "setAndSubmitStatusesSelector"; statuses: string[] }
+ | { type: "setTaskName"; task: string }
+ | { type: "setVariant"; variant: string }
+ | { type: "setBaseStatuses"; baseStatuses: string[] }
+ | { type: "setStatuses"; statuses: string[] }
| { type: "clearAllFilters" }
- | { type: "onSort"; sorts: SortOrder[] }
- | { type: "onChangePagination"; page: number }
- | { type: "onChangeLimit"; limit: number };
+ | { type: "setSorts"; sorts: SortOrder[] }
+ | { type: "setPage"; page: number }
+ | { type: "setLimit"; limit: number };
-export type State = QueryParamState & InputValueState;
+export type State = QueryParamState;
const resetPage = { page: 0 };
export const reducer = (state: State, action: Action) => {
switch (action.type) {
- case "onChangeTaskNameInput":
- return {
- ...state,
- taskNameInputVal: action.task,
- };
- case "onChangeVariantInput":
- return { ...state, variantInputVal: action.variant };
- case "onFilterTaskNameInput":
+ case "setTaskName":
return {
...state,
- taskName: state.taskNameInputVal,
+ taskName: action.task,
...resetPage,
};
- case "onFilterVariantInput":
+ case "setVariant":
return {
...state,
- variant: state.variantInputVal,
+ variant: action.variant,
...resetPage,
};
- case "setAndSubmitBaseStatusesSelector":
+ case "setBaseStatuses":
return {
...state,
- baseStatusesInputVal: action.baseStatuses,
baseStatuses: action.baseStatuses,
...resetPage,
};
- case "setAndSubmitStatusesSelector":
+ case "setStatuses":
return {
...state,
- currentStatusesInputVal: action.statuses,
statuses: action.statuses,
...resetPage,
};
@@ -70,27 +52,23 @@ export const reducer = (state: State, action: Action) => {
return {
...state,
...resetPage,
- currentStatusesInputVal: [],
statuses: [],
- variantInputVal: "",
variant: "",
- baseStatusesInputVal: [],
baseStatuses: [],
- taskNameInputVal: "",
taskName: "",
};
- case "onSort":
+ case "setSorts":
return {
...state,
...resetPage,
sorts: action.sorts,
};
- case "onChangePagination":
+ case "setPage":
return {
...state,
page: action.page < 0 ? 0 : action.page,
};
- case "onChangeLimit":
+ case "setLimit":
return {
...state,
...resetPage,