From b2e457bc6c4f7a5c717ce6b91b3ac27e34899c4b Mon Sep 17 00:00:00 2001 From: Arjun Patel Date: Mon, 18 Dec 2023 13:23:22 -0500 Subject: [PATCH 1/2] v3.0.184 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 78ed40cfc5..35ebe76697 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "spruce", - "version": "3.0.183", + "version": "3.0.184", "private": true, "scripts": { "bootstrap-logkeeper": "./scripts/bootstrap-logkeeper.sh", From c765193787ef26c0e1611403ee98366853730921 Mon Sep 17 00:00:00 2001 From: Sophie Stadler Date: Mon, 18 Dec 2023 13:50:04 -0500 Subject: [PATCH 2/2] DEVPROD-3049: Remove LeafyGreen column utils (#2181) --- cypress.config.ts | 5 +- cypress/integration/version/task_duration.ts | 12 +- src/analytics/version/useVersionAnalytics.ts | 2 +- src/components/Table/BaseTable.tsx | 37 +-- src/components/Table/LGFilters.tsx | 119 ---------- src/pages/hosts/HostsTable.tsx | 44 +++- .../notificationTab/UserSubscriptions.tsx | 32 ++- .../taskDuration/TaskDurationTable.tsx | 212 ++++++++++++------ 8 files changed, 227 insertions(+), 236 deletions(-) delete mode 100644 src/components/Table/LGFilters.tsx diff --git a/cypress.config.ts b/cypress.config.ts index 4e9c84e8d9..98390ef519 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -3,7 +3,10 @@ import { execSync } from "child_process"; export default defineConfig({ e2e: { - retries: 3, + retries: { + runMode: 3, + openMode: 0, + }, baseUrl: "http://localhost:3000", projectId: "yshv48", reporterOptions: { diff --git a/cypress/integration/version/task_duration.ts b/cypress/integration/version/task_duration.ts index 5ea833505b..84fe44a5d0 100644 --- a/cypress/integration/version/task_duration.ts +++ b/cypress/integration/version/task_duration.ts @@ -31,7 +31,7 @@ describe("Task Duration Tab", () => { cy.dataCy("leafygreen-table-row").should("have.length", 3); cy.location("search").should( "include", - "duration=DESC&page=0&statuses=running-umbrella,started,dispatched" + "duration=DESC&page=0&statuses=running-umbrella%2Cstarted%2Cdispatched" ); // Clear status filter. cy.dataCy("status-filter-popover").click(); @@ -61,14 +61,15 @@ describe("Task Duration Tab", () => { }); it("updates URL appropriately when sort is changing", () => { + const durationSortControl = "button[aria-label='Sort by Task Duration']"; // The default sort (DURATION DESC) should be applied cy.location("search").should("include", "duration=DESC"); const longestTask = "test-thirdparty"; cy.contains(longestTask).should("be.visible"); cy.dataCy("leafygreen-table-row").first().should("contain", longestTask); - cy.dataCy("duration-sort-icon").click(); + cy.get(durationSortControl).click(); cy.location("search").should("not.include", "duration"); - cy.dataCy("duration-sort-icon").click(); + cy.get(durationSortControl).click(); cy.location("search").should("include", "duration=ASC"); const shortestTask = "test-auth"; cy.contains(shortestTask).should("be.visible"); @@ -76,9 +77,10 @@ describe("Task Duration Tab", () => { }); it("clearing all filters resets to the default sort", () => { - cy.dataCy("duration-sort-icon").click(); + const durationSortControl = "button[aria-label='Sort by Task Duration']"; + cy.get(durationSortControl).click(); cy.location("search").should("not.include", "duration"); - cy.dataCy("duration-sort-icon").click(); + cy.get(durationSortControl).click(); cy.location("search").should("include", "duration=ASC"); cy.contains("Clear all filters").click(); cy.location("search").should("include", "duration=DESC"); diff --git a/src/analytics/version/useVersionAnalytics.ts b/src/analytics/version/useVersionAnalytics.ts index 712ae92a7e..e812de8d18 100644 --- a/src/analytics/version/useVersionAnalytics.ts +++ b/src/analytics/version/useVersionAnalytics.ts @@ -9,7 +9,7 @@ import { import { VERSION } from "gql/queries"; type Action = - | { name: "Filter Tasks"; filterBy: string } + | { name: "Filter Tasks"; filterBy: string | string[] } | { name: "Sort Tasks Table"; sortBy: diff --git a/src/components/Table/BaseTable.tsx b/src/components/Table/BaseTable.tsx index 23ba7cd491..06fb85aa67 100644 --- a/src/components/Table/BaseTable.tsx +++ b/src/components/Table/BaseTable.tsx @@ -27,14 +27,15 @@ import TableLoader from "./TableLoader"; declare module "@tanstack/table-core" { // eslint-disable-next-line @typescript-eslint/no-unused-vars interface ColumnMeta { - filterComponent?: (column: any) => JSX.Element; search?: { "data-cy"?: string; placeholder?: string; }; - sortComponent?: (column: any) => JSX.Element; treeSelect?: { "data-cy"?: string; + // Configures whether or not the tree select should be filtered to only represent values found in the table. + // Note that this may not be very performant for large tables. + filterOptions?: boolean; options: TreeDataEntry[]; }; // Overcome react-table's column width limitations @@ -67,41 +68,43 @@ export const BaseTable = ({ {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { - const { columnDef } = header.column; + const { columnDef } = header.column ?? {}; + const { meta } = columnDef; return ( {flexRender(columnDef.header, header.getContext())} - {columnDef?.meta?.sortComponent?.({ - column: header.column, - })} - {columnDef?.meta?.filterComponent?.({ - column: header.column, - })} {header.column.getCanFilter() && - (columnDef?.meta?.treeSelect ? ( + (meta?.treeSelect ? ( header.column.setFilterValue(value) } - options={columnDef?.meta?.treeSelect?.options} + options={ + meta.treeSelect?.filterOptions + ? meta.treeSelect.options.filter( + ({ value }) => + !!header.column + .getFacetedUniqueValues() + .get(value) + ) + : meta.treeSelect.options + } value={ (header?.column?.getFilterValue() as string[]) ?? [] } /> ) : ( header.column.setFilterValue(value) } - placeholder={columnDef?.meta?.search?.placeholder} + placeholder={meta?.search?.placeholder} value={ (header?.column?.getFilterValue() as string) ?? "" } diff --git a/src/components/Table/LGFilters.tsx b/src/components/Table/LGFilters.tsx deleted file mode 100644 index 2563f8ed8b..0000000000 --- a/src/components/Table/LGFilters.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { LeafyGreenTableRow } from "@leafygreen-ui/table"; -import { - TableFilterPopover, - TableSearchPopover, - TableSortIcon, -} from "components/TablePopover"; -import { TreeDataEntry } from "components/TreeSelect"; - -type TreeSelectFilterProps = { - "data-cy"?: string; - onConfirm?: ({ id, value }: { id: string; value: string[] }) => void; - tData: TreeDataEntry[]; -}; - -/* - * @deprecated Use react-table's onColumnFiltersChange prop. - */ -export const getColumnTreeSelectFilterProps = ({ - "data-cy": dataCy, - onConfirm = () => {}, - tData, -}: TreeSelectFilterProps) => ({ - enableColumnFilter: false, - meta: { - filterComponent: ({ column }) => { - const filteredOptions = tData.filter( - ({ value }) => !!column.getFacetedUniqueValues().get(value) - ); - return ( - { - column.setFilterValue(newValue); - onConfirm({ id: column.id, value: newValue }); - }} - options={filteredOptions.length ? filteredOptions : tData} - value={column?.getFilterValue() ?? []} - /> - ); - }, - }, - filterFn: ( - row: LeafyGreenTableRow, - columnId: string, - filterValue: string[] - ) => { - // If no filter is specified, show all rows. - if (!filterValue.length) { - return true; - } - return filterValue.includes(row.getValue(columnId)); - }, -}); - -type InputFilterProps = { - "data-cy"?: string; - onConfirm?: ({ id, value }: { id: string; value: string }) => void; -}; - -/* - * @deprecated Use react-table's onColumnFiltersChange prop. - */ -export const getColumnInputFilterProps = ({ - "data-cy": dataCy, - onConfirm = () => {}, -}: InputFilterProps) => ({ - enableColumnFilter: false, - meta: { - filterComponent: ({ column }) => ( - { - column.setFilterValue(newValue); - onConfirm({ id: column.id, value: newValue }); - }} - value={column?.getFilterValue() ?? ""} - /> - ), - }, - filterFn: ( - row: LeafyGreenTableRow, - columnId: string, - filterValue: string - ) => { - // If no filter is specified, show all rows. - if (!filterValue.length) { - return true; - } - return (row.getValue(columnId) as string) - .toLowerCase() - .includes(filterValue.toLowerCase()); - }, -}); - -type SortProps = { - "data-cy"?: string; - onToggle?: ({ id, value }: { id: string; value: string }) => void; -}; - -/* - * @deprecated Use react-table's onSortingChange prop. - */ -export const getColumnSortProps = ({ - "data-cy": dataCy, - onToggle = () => {}, -}: SortProps) => ({ - meta: { - sortComponent: ({ column }) => ( - { - column.toggleSorting(); - onToggle({ id: column.id, value: newValue }); - }} - value={column.getIsSorted().toString()} - /> - ), - }, -}); diff --git a/src/pages/hosts/HostsTable.tsx b/src/pages/hosts/HostsTable.tsx index 72ac3c0e86..675c646566 100644 --- a/src/pages/hosts/HostsTable.tsx +++ b/src/pages/hosts/HostsTable.tsx @@ -2,7 +2,9 @@ import { useRef, useState } from "react"; import { useLeafyGreenTable } from "@leafygreen-ui/table"; import { ColumnFiltersState, + Filters, RowSelectionState, + Sorting, SortingState, } from "@tanstack/react-table"; import { formatDistanceToNow } from "date-fns"; @@ -15,11 +17,14 @@ import { getHostRoute, getTaskRoute } from "constants/routes"; import { HostSortBy, HostsQuery } from "gql/generated/types"; import { useTableSort } from "hooks"; import { useQueryParams } from "hooks/useQueryParam"; -import { mapIdToFilterParam } from "types/host"; +import { HostsTableFilterParams, mapIdToFilterParam } from "types/host"; import { Unpacked } from "types/utils"; type Host = Unpacked; +const { getDefaultOptions: getDefaultFiltering } = Filters; +const { getDefaultOptions: getDefaultSorting } = Sorting; + interface Props { initialFilters: ColumnFiltersState; initialSorting: SortingState; @@ -39,14 +44,8 @@ export const HostsTable: React.FC = ({ }) => { const { sendEvent } = useHostsTableAnalytics(); - const tableSortHandler = useTableSort({ - sendAnalyticsEvents: () => sendEvent({ name: "Sort Hosts" }), - }); - - const [, setQueryParams] = useQueryParams(); + const [queryParams, setQueryParams] = useQueryParams(); - const [filters, setFilters] = useState(initialFilters); - const [sorting, setSorting] = useState(initialSorting); const [rowSelection, setRowSelection] = useState({}); const updateRowSelection = (rowState: RowSelectionState) => { @@ -56,8 +55,22 @@ export const HostsTable: React.FC = ({ setSelectedHosts(selectedHosts); }; + const setSorting = (s: SortingState) => + getDefaultSorting(table).onSortingChange(s); + + const tableSortHandler = useTableSort({ + sendAnalyticsEvents: () => sendEvent({ name: "Sort Hosts" }), + }); + + const setFilters = (f: ColumnFiltersState) => + getDefaultFiltering(table).onColumnFiltersChange(f); + const updateFilters = (filterState: ColumnFiltersState) => { - const updatedParams = { page: "0" }; + const updatedParams = { + ...queryParams, + page: "0", + ...emptyFilterQueryParams, + }; filterState.forEach(({ id, value }) => { const key = mapIdToFilterParam[id]; @@ -80,15 +93,17 @@ export const HostsTable: React.FC = ({ // https://github.com/TanStack/table/issues/4289 sortDescFirst: false, }, + initialState: { + columnFilters: initialFilters, + sorting: initialSorting, + }, state: { - columnFilters: filters, rowSelection, - sorting, }, hasSelectableRows: true, manualFiltering: true, - manualSorting: true, manualPagination: true, + manualSorting: true, onColumnFiltersChange: onChangeHandler( setFilters, (updatedState) => { @@ -121,6 +136,11 @@ export const HostsTable: React.FC = ({ ); }; +const emptyFilterQueryParams = Object.values(HostsTableFilterParams).reduce( + (a, v) => ({ ...a, [v]: undefined }), + {} +); + const columns = [ { header: "ID", diff --git a/src/pages/preferences/preferencesTabs/notificationTab/UserSubscriptions.tsx b/src/pages/preferences/preferencesTabs/notificationTab/UserSubscriptions.tsx index 56d8d37ab8..acf889cd5f 100644 --- a/src/pages/preferences/preferencesTabs/notificationTab/UserSubscriptions.tsx +++ b/src/pages/preferences/preferencesTabs/notificationTab/UserSubscriptions.tsx @@ -8,12 +8,12 @@ import { useLeafyGreenTable } from "@leafygreen-ui/table"; import { getFacetedUniqueValues, getFilteredRowModel, + filterFns, } from "@tanstack/react-table"; import Icon from "components/Icon"; import { SettingsCard, SettingsCardTitle } from "components/SettingsCard"; import { ShortenedRouterLink } from "components/styles"; import { BaseTable } from "components/Table/BaseTable"; -import { getColumnTreeSelectFilterProps } from "components/Table/LGFilters"; import { getSubscriberText } from "constants/subscription"; import { size } from "constants/tokens"; import { @@ -77,15 +77,21 @@ export const UserSubscriptions: React.FC<{}> = () => { () => [ { accessorKey: "resourceType", + id: "resourceType", cell: ({ getValue }) => { const resourceType = getValue(); return resourceTypeToCopy[resourceType] ?? resourceType; }, + enableColumnFilter: true, + filterFn: filterFns.arrIncludesSome, header: "Type", - ...getColumnTreeSelectFilterProps({ - "data-cy": "status-filter-popover", - tData: resourceTypeTreeData, - }), + meta: { + treeSelect: { + "data-cy": "status-filter-popover", + filterOptions: true, + options: resourceTypeTreeData, + }, + }, }, { header: "ID", @@ -113,10 +119,15 @@ export const UserSubscriptions: React.FC<{}> = () => { { accessorKey: "trigger", header: "Event", - ...getColumnTreeSelectFilterProps({ - "data-cy": "trigger-filter-popover", - tData: triggerTreeData, - }), + enableColumnFilter: true, + filterFn: filterFns.arrIncludesSome, + meta: { + treeSelect: { + "data-cy": "trigger-filter-popover", + filterOptions: true, + options: triggerTreeData, + }, + }, cell: ({ getValue }) => { const trigger = getValue(); return triggerToCopy[trigger] ?? trigger; @@ -149,6 +160,9 @@ export const UserSubscriptions: React.FC<{}> = () => { columns, containerRef: tableContainerRef, data: subscriptions ?? [], + defaultColumn: { + enableColumnFilter: false, + }, getFacetedUniqueValues: getFacetedUniqueValues(), getFilteredRowModel: getFilteredRowModel(), hasSelectableRows: true, diff --git a/src/pages/version/taskDuration/TaskDurationTable.tsx b/src/pages/version/taskDuration/TaskDurationTable.tsx index 36e0aea9c2..6eab78f023 100644 --- a/src/pages/version/taskDuration/TaskDurationTable.tsx +++ b/src/pages/version/taskDuration/TaskDurationTable.tsx @@ -1,24 +1,28 @@ -import { useCallback, useMemo, useRef } from "react"; +import { useMemo, useRef } from "react"; import { useLeafyGreenTable } from "@leafygreen-ui/table"; -import { getFacetedMinMaxValues } from "@tanstack/react-table"; +import { + ColumnFiltersState, + Filters, + Sorting, + SortingState, + getFacetedMinMaxValues, +} from "@tanstack/react-table"; import { useParams } from "react-router-dom"; import { useVersionAnalytics } from "analytics"; import { BaseTable } from "components/Table/BaseTable"; -import { - getColumnInputFilterProps, - getColumnTreeSelectFilterProps, - getColumnSortProps, -} from "components/Table/LGFilters"; import { TablePlaceholder } from "components/Table/TablePlaceholder"; +import { onChangeHandler } from "components/Table/utils"; import { TaskLink } from "components/TasksTable/TaskLink"; import TaskStatusBadge from "components/TaskStatusBadge"; import { VersionTaskDurationsQuery, SortDirection } from "gql/generated/types"; import { useTaskStatuses } from "hooks"; import { useQueryParams } from "hooks/useQueryParam"; -import { useUpdateURLQueryParams } from "hooks/useUpdateURLQueryParams"; import { PatchTasksQueryParams } from "types/task"; import { TaskDurationCell } from "./TaskDurationCell"; +const { getDefaultOptions: getDefaultFiltering } = Filters; +const { getDefaultOptions: getDefaultSorting } = Sorting; + interface Props { tasks: VersionTaskDurationsQuery["version"]["tasks"]["data"]; loading: boolean; @@ -34,46 +38,49 @@ export const TaskDurationTable: React.FC = ({ const { sendEvent } = useVersionAnalytics(versionId); const { currentStatuses: statusOptions } = useTaskStatuses({ versionId }); - const [ - { - [PatchTasksQueryParams.TaskName]: taskName = "", - [PatchTasksQueryParams.Statuses]: statuses = [], - [PatchTasksQueryParams.Variant]: variant = "", - [PatchTasksQueryParams.Duration]: duration = "", - }, - ] = useQueryParams(); + const [queryParams, setQueryParams] = useQueryParams(); - const filters = useMemo( - () => [ - { id: PatchTasksQueryParams.TaskName, value: taskName }, - { - id: PatchTasksQueryParams.Statuses, - value: Array.isArray(statuses) ? statuses : [statuses], - }, - { id: PatchTasksQueryParams.Variant, value: variant }, - ], - [taskName, statuses, variant] + const { initialFilters, initialSort } = useMemo( + () => getInitialParams(queryParams), + [] // eslint-disable-line react-hooks/exhaustive-deps ); - const sorting = useMemo( - () => [ - ...(duration && [ - { - id: PatchTasksQueryParams.Duration, - desc: duration === SortDirection.Desc, - }, - ]), - ], - [duration] - ); + const setFilters = (f: ColumnFiltersState) => + getDefaultFiltering(table).onColumnFiltersChange(f); - const updateQueryParams = useUpdateURLQueryParams(); - const updateUrl = useCallback( - ({ id, value }) => { - updateQueryParams({ [id]: value || undefined, page: "0" }); - }, - [updateQueryParams] - ); + const updateFilters = (filterState: ColumnFiltersState) => { + const updatedParams = { + ...queryParams, + page: "0", + [PatchTasksQueryParams.TaskName]: undefined, + [PatchTasksQueryParams.Statuses]: undefined, + [PatchTasksQueryParams.Variant]: undefined, + }; + + filterState.forEach(({ id, value }) => { + updatedParams[id] = value; + }); + + setQueryParams(updatedParams); + sendEvent({ name: "Filter Tasks", filterBy: Object.keys(filterState) }); + }; + + const setSorting = (s: SortingState) => + getDefaultSorting(table).onSortingChange(s); + + const updateSort = (sortState: SortingState) => { + const updatedParams = { + ...queryParams, + page: "0", + [PatchTasksQueryParams.Duration]: undefined, + }; + + sortState.forEach(({ desc, id }) => { + updatedParams[id] = desc ? SortDirection.Desc : SortDirection.Asc; + }); + + setQueryParams(updatedParams); + }; const columns = useMemo( () => [ @@ -82,53 +89,51 @@ export const TaskDurationTable: React.FC = ({ accessorKey: "displayName", header: "Task Name", size: 250, + enableColumnFilter: true, cell: ({ getValue, row: { original: { id }, }, }) => , - ...getColumnInputFilterProps({ - "data-cy": "task-name-filter-popover", - onConfirm: (filter) => { - updateUrl(filter); - sendEvent({ name: "Filter Tasks", filterBy: filter.id }); + meta: { + search: { + "data-cy": "task-name-filter-popover", }, - }), + }, }, { id: PatchTasksQueryParams.Statuses, accessorKey: "status", header: "Status", size: 120, + enableColumnFilter: true, cell: ({ getValue }) => , - ...getColumnTreeSelectFilterProps({ - "data-cy": "status-filter-popover", - tData: statusOptions, - onConfirm: (filter) => { - updateUrl(filter); - sendEvent({ name: "Filter Tasks", filterBy: filter.id }); + meta: { + treeSelect: { + "data-cy": "status-filter-popover", + options: statusOptions, }, - }), + }, }, { id: PatchTasksQueryParams.Variant, accessorKey: "buildVariantDisplayName", header: "Build Variant", size: 150, - ...getColumnInputFilterProps({ - "data-cy": "build-variant-filter-popover", - onConfirm: (filter) => { - updateUrl(filter); - sendEvent({ name: "Filter Tasks", filterBy: filter.id }); + enableColumnFilter: true, + meta: { + search: { + "data-cy": "build-variant-filter-popover", }, - }), + }, }, { id: PatchTasksQueryParams.Duration, accessorKey: "timeTaken", header: "Task Duration", enableColumnFilter: false, + enableSorting: true, size: 250, cell: ({ column, @@ -143,13 +148,9 @@ export const TaskDurationTable: React.FC = ({ timeTaken={getValue()} /> ), - ...getColumnSortProps({ - "data-cy": "duration-sort-icon", - onToggle: updateUrl, - }), }, ], - [statusOptions, sendEvent, updateUrl] + [statusOptions] ); const tableContainerRef = useRef(null); @@ -159,14 +160,33 @@ export const TaskDurationTable: React.FC = ({ columns, containerRef: tableContainerRef, data: tasks ?? [], - state: { - columnFilters: filters, - sorting, + defaultColumn: { + // Handle bug in sorting order + // https://github.com/TanStack/table/issues/4289 + sortDescFirst: false, }, getFacetedMinMaxValues: getFacetedMinMaxValues(), + initialState: { + columnFilters: initialFilters, + sorting: initialSort, + }, manualFiltering: true, - manualSorting: true, manualPagination: true, + manualSorting: true, + onColumnFiltersChange: onChangeHandler( + setFilters, + (updatedState) => { + updateFilters(updatedState); + table.resetRowSelection(); + } + ), + onSortingChange: onChangeHandler( + setSorting, + (updatedState) => { + updateSort(updatedState); + table.resetRowSelection(); + } + ), }); return ( @@ -181,3 +201,51 @@ export const TaskDurationTable: React.FC = ({ /> ); }; + +const getInitialParams = (queryParams: { + [key: string]: any; +}): { + initialFilters: ColumnFiltersState; + initialSort: SortingState; +} => { + const { + [PatchTasksQueryParams.TaskName]: taskName, + [PatchTasksQueryParams.Statuses]: statuses, + [PatchTasksQueryParams.Variant]: variant, + [PatchTasksQueryParams.Duration]: duration, + } = queryParams; + + const initialFilters = []; + if (taskName) { + initialFilters.push({ + id: PatchTasksQueryParams.TaskName, + value: taskName, + }); + } + if (statuses) { + initialFilters.push({ + id: PatchTasksQueryParams.Statuses, + value: Array.isArray(statuses) ? statuses : [statuses], + }); + } + if (variant) { + initialFilters.push({ id: PatchTasksQueryParams.Variant, value: variant }); + } + + return { + initialFilters, + initialSort: duration + ? [ + { + id: PatchTasksQueryParams.Duration, + desc: duration === SortDirection.Desc, + }, + ] + : [ + { + id: PatchTasksQueryParams.Duration, + desc: true, + }, + ], + }; +};