From eece1ad6c44d7c410bd154fbd0508e96c27097b5 Mon Sep 17 00:00:00 2001 From: Wilson Neto Date: Wed, 29 Jan 2025 15:01:17 -0300 Subject: [PATCH] feat(listing-details): add and implements query inconsistency invalidator hook add hook to cache bust in case of inconsistency between listing and details closes #833 ``` --- .../TreeListingPage/TreeListingPage.tsx | 13 ++++-- .../components/TreeListingPage/TreeTable.tsx | 36 +++++++++++++-- .../hooks/useQueryInconsistencyInvalidator.ts | 46 +++++++++++++++++++ dashboard/src/main.tsx | 16 ++++++- .../pages/Hardware/HardwareListingPage.tsx | 8 ++-- .../src/pages/Hardware/HardwareTable.tsx | 20 ++++++-- .../src/pages/TreeDetails/TreeDetails.tsx | 34 +++++++++++++- .../pages/hardwareDetails/HardwareDetails.tsx | 37 ++++++++++++++- dashboard/src/types/general.ts | 6 ++- dashboard/src/types/hardware.ts | 10 +--- dashboard/src/utils/status.ts | 20 +++++++- 11 files changed, 214 insertions(+), 32 deletions(-) create mode 100644 dashboard/src/hooks/useQueryInconsistencyInvalidator.ts diff --git a/dashboard/src/components/TreeListingPage/TreeListingPage.tsx b/dashboard/src/components/TreeListingPage/TreeListingPage.tsx index 5ee51f77..85c1a972 100644 --- a/dashboard/src/components/TreeListingPage/TreeListingPage.tsx +++ b/dashboard/src/components/TreeListingPage/TreeListingPage.tsx @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import type { + TableTestStatus, Tree, TreeFastPathResponse, TreeTableBody, @@ -67,25 +68,27 @@ const TreeListingPage = ({ inputFilter }: ITreeListingPage): JSX.Element => { : undefined; const testStatus = isCompleteTree(tree) - ? { - done: tree.test_status.done, + ? ({ error: tree.test_status.error, fail: tree.test_status.fail, miss: tree.test_status.miss, pass: tree.test_status.pass, skip: tree.test_status.skip, - } + done: tree.test_status.done, + null: tree.test_status.null, + } satisfies TableTestStatus) : undefined; const bootStatus = isCompleteTree(tree) - ? { + ? ({ done: tree.boot_status.done, error: tree.boot_status.error, fail: tree.boot_status.fail, miss: tree.boot_status.miss, pass: tree.boot_status.pass, skip: tree.boot_status.skip, - } + null: tree.boot_status.null, + } satisfies TableTestStatus) : undefined; return { diff --git a/dashboard/src/components/TreeListingPage/TreeTable.tsx b/dashboard/src/components/TreeListingPage/TreeTable.tsx index a092935a..f7790862 100644 --- a/dashboard/src/components/TreeListingPage/TreeTable.tsx +++ b/dashboard/src/components/TreeListingPage/TreeTable.tsx @@ -24,7 +24,7 @@ import { TooltipDateTime } from '@/components/TooltipDateTime'; import type { TreeTableBody } from '@/types/tree/Tree'; import { RedirectFrom, zOrigin } from '@/types/general'; -import type { TFilter, TOrigins } from '@/types/general'; +import type { TFilter, TOrigins, BuildStatus } from '@/types/general'; import { formattedBreakLineValue } from '@/locales/messages'; @@ -62,6 +62,8 @@ import CopyButton from '@/components/Button/CopyButton'; import type { ListingTableColumnMeta } from '@/types/table'; +import { statusCountToRequiredStatusCount } from '@/utils/status'; + import { InputTime } from './InputTime'; const MemoizedInputTime = memo(InputTime); @@ -75,10 +77,6 @@ const getLinkProps = ( return { to: '/tree/$treeId', params: { treeId: row.original.id }, - state: { - id: row.original.id, - from: RedirectFrom.Tree, - }, search: previousSearch => ({ tableFilter: { bootsTable: possibleTestsTableFilter[0], @@ -97,6 +95,34 @@ const getLinkProps = ( }, intervalInDays: previousSearch.intervalInDays, }), + state: s => ({ + ...s, + id: row.original.id, + from: RedirectFrom.Tree, + treeStatusCount: { + buildsStatus: { + valid: row.original.buildStatus?.valid ?? 0, + invalid: row.original.buildStatus?.invalid ?? 0, + null: row.original.buildStatus?.null ?? 0, + } satisfies BuildStatus, + testStatus: statusCountToRequiredStatusCount({ + DONE: row.original.testStatus?.done, + PASS: row.original.testStatus?.pass, + FAIL: row.original.testStatus?.fail, + ERROR: row.original.testStatus?.error, + MISS: row.original.testStatus?.miss, + SKIP: row.original.testStatus?.skip, + }), + bootStatus: statusCountToRequiredStatusCount({ + DONE: row.original.bootStatus?.done, + PASS: row.original.bootStatus?.pass, + FAIL: row.original.bootStatus?.fail, + ERROR: row.original.bootStatus?.error, + MISS: row.original.bootStatus?.miss, + SKIP: row.original.bootStatus?.skip, + }), + }, + }), }; }; diff --git a/dashboard/src/hooks/useQueryInconsistencyInvalidator.ts b/dashboard/src/hooks/useQueryInconsistencyInvalidator.ts new file mode 100644 index 00000000..66399688 --- /dev/null +++ b/dashboard/src/hooks/useQueryInconsistencyInvalidator.ts @@ -0,0 +1,46 @@ +import { useQueryClient } from '@tanstack/react-query'; +import type { UseNavigateResult } from '@tanstack/react-router'; +import { useEffect } from 'react'; +import { isEqual } from 'lodash-es'; + +type ReferenceTable = + | Record | undefined> + | undefined; + +type QueryInconsistencyInvalidatorArgs = { + referenceData?: T; + comparedData?: T; + enabled?: boolean; + navigate: UseNavigateResult<'/tree/$treeId' | '/hardware/$hardwareId'>; +}; + +export const useQueryInconsistencyInvalidator = ({ + referenceData: referenceData, + comparedData: comparedData, + navigate, + enabled = true, +}: QueryInconsistencyInvalidatorArgs): void => { + const queryClient = useQueryClient(); + useEffect(() => { + if (!enabled || !referenceData || !comparedData) { + return; + } + + const shouldInvalidate = !isEqual(referenceData, comparedData); + + if (shouldInvalidate) { + queryClient.invalidateQueries().then(() => { + navigate({ + search: s => s, + state: s => { + return { + ...s, + treeStatusCount: undefined, + hardwareStatusCount: undefined, + }; + }, + }); + }); + } + }, [referenceData, comparedData, queryClient, navigate, enabled]); +}; diff --git a/dashboard/src/main.tsx b/dashboard/src/main.tsx index 5913f49b..10de71f8 100644 --- a/dashboard/src/main.tsx +++ b/dashboard/src/main.tsx @@ -20,7 +20,11 @@ import { routeTree } from './routeTree.gen'; import './index.css'; import { isDev } from './lib/utils/vite'; import { ToastProvider } from './components/ui/toast'; -import type { RedirectFrom } from './types/general'; +import type { + BuildStatus, + RedirectFrom, + RequiredStatusCount, +} from './types/general'; import { parseSearch, stringifySearch } from './utils/search'; declare global { @@ -40,6 +44,16 @@ declare module '@tanstack/react-router' { interface HistoryState { id?: string; from?: RedirectFrom; + treeStatusCount?: { + buildsStatus?: BuildStatus; + bootStatus?: RequiredStatusCount; + testStatus?: RequiredStatusCount; + }; + hardwareStatusCount?: { + builds: BuildStatus; + boot: RequiredStatusCount; + test: RequiredStatusCount; + }; } } diff --git a/dashboard/src/pages/Hardware/HardwareListingPage.tsx b/dashboard/src/pages/Hardware/HardwareListingPage.tsx index 624e239c..fca57060 100644 --- a/dashboard/src/pages/Hardware/HardwareListingPage.tsx +++ b/dashboard/src/pages/Hardware/HardwareListingPage.tsx @@ -15,6 +15,8 @@ import { dateObjectToTimestampInSeconds, daysToSeconds } from '@/utils/date'; import { MemoizedSectionError } from '@/components/DetailsPages/SectionError'; +import type { BuildStatus, StatusCount } from '@/types/general'; + import { HardwareTable } from './HardwareTable'; interface HardwareListingPageProps { @@ -77,13 +79,13 @@ const HardwareListingPage = ({ return hardware.hardware_name?.includes(inputFilter); }) .map((hardware): HardwareTableItem => { - const buildCount = { + const buildCount: BuildStatus = { valid: hardware.build_status_summary?.valid, invalid: hardware.build_status_summary?.invalid, null: hardware.build_status_summary?.null, }; - const testStatusCount = { + const testStatusCount: StatusCount = { DONE: hardware.test_status_summary.DONE, ERROR: hardware.test_status_summary.ERROR, FAIL: hardware.test_status_summary.FAIL, @@ -93,7 +95,7 @@ const HardwareListingPage = ({ NULL: hardware.test_status_summary.NULL, }; - const bootStatusCount = { + const bootStatusCount: StatusCount = { DONE: hardware.boot_status_summary.DONE, ERROR: hardware.boot_status_summary.ERROR, FAIL: hardware.boot_status_summary.FAIL, diff --git a/dashboard/src/pages/Hardware/HardwareTable.tsx b/dashboard/src/pages/Hardware/HardwareTable.tsx index 7703fe02..93d55fa7 100644 --- a/dashboard/src/pages/Hardware/HardwareTable.tsx +++ b/dashboard/src/pages/Hardware/HardwareTable.tsx @@ -40,7 +40,7 @@ import { PaginationInfo } from '@/components/Table/PaginationInfo'; import type { HardwareTableItem } from '@/types/hardware'; -import { sumStatus } from '@/utils/status'; +import { statusCountToRequiredStatusCount, sumStatus } from '@/utils/status'; import { usePaginationState } from '@/hooks/usePaginationState'; @@ -71,10 +71,6 @@ const getLinkProps = ( from: '/hardware', to: '/hardware/$hardwareId', params: { hardwareId: row.original.hardware_name }, - state: { - id: row.original.hardware_name, - from: RedirectFrom.Hardware, - }, search: previousSearch => ({ ...previousSearch, currentPageTab: zPossibleTabValidator.parse(tabTarget), @@ -82,6 +78,20 @@ const getLinkProps = ( endTimestampInSeconds, diffFilter: { ...previousSearch.diffFilter, ...newDiffFilter }, }), + state: s => ({ + ...s, + id: row.original.hardware_name, + from: RedirectFrom.Hardware, + hardwareStatusCount: { + builds: row.original.build_status_summary, + test: statusCountToRequiredStatusCount( + row.original.test_status_summary, + ), + boot: statusCountToRequiredStatusCount( + row.original.boot_status_summary, + ), + }, + }), }; }; diff --git a/dashboard/src/pages/TreeDetails/TreeDetails.tsx b/dashboard/src/pages/TreeDetails/TreeDetails.tsx index f5ad7442..b8ecae80 100644 --- a/dashboard/src/pages/TreeDetails/TreeDetails.tsx +++ b/dashboard/src/pages/TreeDetails/TreeDetails.tsx @@ -1,4 +1,9 @@ -import { useNavigate, useParams, useSearch } from '@tanstack/react-router'; +import { + useNavigate, + useParams, + useRouterState, + useSearch, +} from '@tanstack/react-router'; import { useCallback, useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; @@ -48,6 +53,10 @@ import { useTreeDetailsLazyLoadQuery } from '@/hooks/useTreeDetailsLazyLoadQuery import { LoadingCircle } from '@/components/ui/loading-circle'; +import { useQueryInconsistencyInvalidator } from '@/hooks/useQueryInconsistencyInvalidator'; + +import { statusCountToRequiredStatusCount } from '@/utils/status'; + import TreeDetailsFilter from './TreeDetailsFilter'; import type { TreeDetailsTabRightElement } from './Tabs/TreeDetailsTab'; import TreeDetailsTab from './Tabs/TreeDetailsTab'; @@ -139,6 +148,29 @@ function TreeDetails(): JSX.Element { status: summaryQueryStatus, } = treeDetailsLazyLoaded.summary; + const treeRouterStatus = useRouterState({ + select: s => s.location.state.treeStatusCount, + }); + + type TreeRouterStatus = typeof treeRouterStatus; + + const comparedData: TreeRouterStatus = useMemo(() => { + if (!data) return undefined; + + const { builds, tests, boots } = data.summary; + + return { + buildsStatus: builds.status, + testStatus: statusCountToRequiredStatusCount(tests.status), + bootStatus: statusCountToRequiredStatusCount(boots.status), + } satisfies TreeRouterStatus; + }, [data]); + + useQueryInconsistencyInvalidator({ + referenceData: treeRouterStatus, + comparedData: comparedData, + navigate: navigate, + }); const onFilterChange = useCallback( (newFilter: TFilter) => { navigate({ diff --git a/dashboard/src/pages/hardwareDetails/HardwareDetails.tsx b/dashboard/src/pages/hardwareDetails/HardwareDetails.tsx index 87ade434..cde99576 100644 --- a/dashboard/src/pages/hardwareDetails/HardwareDetails.tsx +++ b/dashboard/src/pages/hardwareDetails/HardwareDetails.tsx @@ -1,4 +1,9 @@ -import { useNavigate, useParams, useSearch } from '@tanstack/react-router'; +import { + useNavigate, + useParams, + useRouterState, + useSearch, +} from '@tanstack/react-router'; import { FormattedMessage } from 'react-intl'; @@ -45,6 +50,10 @@ import { MemoizedSectionError } from '@/components/DetailsPages/SectionError'; import { useHardwareDetailsLazyLoadQuery } from '@/hooks/useHardwareDetailsLazyLoadQuery'; +import { useQueryInconsistencyInvalidator } from '@/hooks/useQueryInconsistencyInvalidator'; + +import { statusCountToRequiredStatusCount } from '@/utils/status'; + import { HardwareHeader } from './HardwareDetailsHeaderTable'; import type { TreeDetailsTabRightElement } from './Tabs/HardwareDetailsTabs'; import HardwareDetailsTabs from './Tabs/HardwareDetailsTabs'; @@ -153,6 +162,32 @@ function HardwareDetails(): JSX.Element { treeIndexesLength: treeIndexesLength, }); + const hardwareStatusHistoryState = useRouterState({ + select: s => s.location.state.hardwareStatusCount, + }); + + type HardwareStatusComparedState = typeof hardwareStatusHistoryState; + + const hardwareDataPreparedForInconsistencyValidation: HardwareStatusComparedState = + useMemo(() => { + const { data } = summaryResponse; + if (!data) return; + + const { boots, builds, tests } = data; + + return { + boot: statusCountToRequiredStatusCount(boots.status), + test: statusCountToRequiredStatusCount(tests.status), + builds: builds.status, + } satisfies HardwareStatusComparedState; + }, [summaryResponse]); + + useQueryInconsistencyInvalidator({ + referenceData: hardwareStatusHistoryState, + comparedData: hardwareDataPreparedForInconsistencyValidation, + navigate: navigate, + }); + const hardwareTableForCommitHistory = useMemo(() => { const result: CommitHead[] = []; if (!summaryResponse.isLoading && summaryResponse.data) { diff --git a/dashboard/src/types/general.ts b/dashboard/src/types/general.ts index 89c744f2..2af125d1 100644 --- a/dashboard/src/types/general.ts +++ b/dashboard/src/types/general.ts @@ -85,7 +85,7 @@ export type BuildStatus = { null: number; }; -export interface StatusCount { +export type StatusCount = { PASS?: number; FAIL?: number; MISS?: number; @@ -93,7 +93,9 @@ export interface StatusCount { ERROR?: number; NULL?: number; DONE?: number; -} +}; + +export type RequiredStatusCount = Required; export type Architecture = Record< string, diff --git a/dashboard/src/types/hardware.ts b/dashboard/src/types/hardware.ts index 7e2ec71a..af9038bf 100644 --- a/dashboard/src/types/hardware.ts +++ b/dashboard/src/types/hardware.ts @@ -1,14 +1,8 @@ -import type { StatusCount } from './general'; - -interface BuildCount { - valid: number; - invalid: number; - null: number; -} +import type { BuildStatus, StatusCount } from './general'; export interface HardwareItem { hardware_name: string; - build_status_summary: BuildCount; + build_status_summary: BuildStatus; test_status_summary: StatusCount; boot_status_summary: StatusCount; } diff --git a/dashboard/src/utils/status.ts b/dashboard/src/utils/status.ts index db090786..f1bb5918 100644 --- a/dashboard/src/utils/status.ts +++ b/dashboard/src/utils/status.ts @@ -1,5 +1,9 @@ import type { Status } from '@/types/database'; -import type { StatusCount, BuildStatus } from '@/types/general'; +import type { + StatusCount, + BuildStatus, + RequiredStatusCount, +} from '@/types/general'; type StatusGroups = 'success' | 'failed' | 'inconclusive'; @@ -46,3 +50,17 @@ export function sumStatus(status: FlexibleStatus): number { 0, ); } + +export const statusCountToRequiredStatusCount = ( + statusCount: StatusCount, +): RequiredStatusCount => { + return { + PASS: statusCount.PASS ?? 0, + FAIL: statusCount.FAIL ?? 0, + MISS: statusCount.MISS ?? 0, + SKIP: statusCount.SKIP ?? 0, + ERROR: statusCount.ERROR ?? 0, + NULL: statusCount.NULL ?? 0, + DONE: statusCount.DONE ?? 0, + }; +};