From 7e2f0ecea3f17c61fa8a1994ff964007c08d4a49 Mon Sep 17 00:00:00 2001 From: Wilson Neto Date: Wed, 22 Jan 2025 11:28:18 -0300 Subject: [PATCH 1/5] Reapply "fix(TreeDetails): add lazy load" This reverts commit 565fdcaaee717bf5cb5fa51290a07543a0b6a597. --- .../kernelCI_app/typeModels/treeDetails.py | 4 +- backend/schema.yml | 1 - dashboard/src/api/treeDetails.ts | 73 +++++++-- .../components/Tabs/Builds/ConfigsCard.tsx | 5 +- .../src/components/Tabs/Builds/StatusCard.tsx | 5 +- .../src/hooks/useTreeDetailsLazyLoadQuery.ts | 138 ++++++++++++++++++ .../pages/TreeDetails/Tabs/Boots/BootsTab.tsx | 37 ++--- .../pages/TreeDetails/Tabs/Build/BuildTab.tsx | 68 +++++++-- .../pages/TreeDetails/Tabs/Tests/TestsTab.tsx | 75 +++++----- .../pages/TreeDetails/Tabs/TreeDetailsTab.tsx | 18 +-- .../src/pages/TreeDetails/TreeDetails.tsx | 81 ++++------ .../pages/TreeDetails/TreeDetailsFilter.tsx | 2 + dashboard/src/types/tree/TreeDetails.tsx | 18 +++ 13 files changed, 372 insertions(+), 153 deletions(-) create mode 100644 dashboard/src/hooks/useTreeDetailsLazyLoadQuery.ts diff --git a/backend/kernelCI_app/typeModels/treeDetails.py b/backend/kernelCI_app/typeModels/treeDetails.py index 1bd0b0cb..8e52987f 100644 --- a/backend/kernelCI_app/typeModels/treeDetails.py +++ b/backend/kernelCI_app/typeModels/treeDetails.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, Set +from typing import Dict, List, Optional from kernelCI_app.typeModels.commonDetails import ( Summary, @@ -29,7 +29,7 @@ class TestSummary(BaseModel): class TreeSummary(Summary): - hardware: Optional[Set[str]] + hardware: Optional[List[str]] tree_url: Optional[str] git_commit_tags: Optional[List[str]] diff --git a/backend/schema.yml b/backend/schema.yml index 20cca1a1..23a2c5c0 100644 --- a/backend/schema.yml +++ b/backend/schema.yml @@ -958,7 +958,6 @@ components: - items: type: string type: array - uniqueItems: true - type: 'null' title: Hardware tree_url: diff --git a/dashboard/src/api/treeDetails.ts b/dashboard/src/api/treeDetails.ts index 6d0c352c..e3cc172a 100644 --- a/dashboard/src/api/treeDetails.ts +++ b/dashboard/src/api/treeDetails.ts @@ -8,6 +8,10 @@ import type { BuildCountsResponse, TTreeTestsFullData, LogFilesResponse, + TreeDetailsSummary, + TreeDetailsBoots, + TreeDetailsBuilds, + TreeDetailsTests, } from '@/types/tree/TreeDetails'; import { getTargetFilter, type TFilter } from '@/types/general'; @@ -31,11 +35,27 @@ const useTreeSearchParameters = (): TreeSearchParameters => { return { origin, gitUrl, gitBranch }; }; -const fetchTreeDetails = async ( - treeId: string, - treeSearchParameters: TreeSearchParameters, - filter: TTreeDetailsFilter = {}, -): Promise => { +type TreeDetailsVariants = 'full' | 'builds' | 'boots' | 'tests' | 'summary'; + +type TreeDetailsResponseTable = { + full: TTreeTestsFullData; + summary: TreeDetailsSummary; + builds: TreeDetailsBuilds; + boots: TreeDetailsBoots; + tests: TreeDetailsTests; +}; + +const fetchTreeDetails = async ({ + treeId, + treeSearchParameters, + filter = {}, + variant, +}: { + treeId: string; + treeSearchParameters: TreeSearchParameters; + filter: TTreeDetailsFilter; + variant: TreeDetailsVariants; +}): Promise => { const backendCompatibleFilters = mapFiltersKeysToBackendCompatible(filter); const params = { @@ -45,22 +65,39 @@ const fetchTreeDetails = async ( ...backendCompatibleFilters, }; - const res = await http.get(`/api/tree/${treeId}/full`, { + const urlTable: Record = { + full: `/api/tree/${treeId}/full`, + builds: `/api/tree/${treeId}/builds`, + boots: `/api/tree/${treeId}/boots`, + tests: `/api/tree/${treeId}/tests`, + summary: `/api/tree/${treeId}/summary`, + }; + const res = await http.get(urlTable[variant], { params: params, }); return res.data; }; -export const useTreeDetails = ({ - treeId, - filter = {}, - enabled = true, -}: { +type TreeDetailsResponse = + TreeDetailsResponseTable[T]; + +export type UseTreeDetailsWithoutVariant = { treeId: string; filter?: TFilter; enabled?: boolean; -}): UseQueryResult => { +}; + +type UseTreeDetailsParameters = { + variant: T; +} & UseTreeDetailsWithoutVariant; + +export const useTreeDetails = ({ + treeId, + filter = {}, + enabled = true, + variant, +}: UseTreeDetailsParameters): UseQueryResult> => { const testFilter = getTargetFilter(filter, 'test'); const treeDetailsFilter = getTargetFilter(filter, 'treeDetails'); const treeSearchParameters = useTreeSearchParameters(); @@ -72,11 +109,17 @@ export const useTreeDetails = ({ treeSearchParameters, testFilter, treeDetailsFilter, + variant, ], queryFn: () => - fetchTreeDetails(treeId, treeSearchParameters, { - ...testFilter, - ...treeDetailsFilter, + fetchTreeDetails({ + treeId, + treeSearchParameters, + variant, + filter: { + ...testFilter, + ...treeDetailsFilter, + }, }), enabled, placeholderData: previousData => previousData, diff --git a/dashboard/src/components/Tabs/Builds/ConfigsCard.tsx b/dashboard/src/components/Tabs/Builds/ConfigsCard.tsx index 0439fa32..a3f1a02d 100644 --- a/dashboard/src/components/Tabs/Builds/ConfigsCard.tsx +++ b/dashboard/src/components/Tabs/Builds/ConfigsCard.tsx @@ -2,8 +2,6 @@ import { memo, useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; -import type { ITreeDetails } from '@/pages/TreeDetails/TreeDetails'; - import { DumbListingContent } from '@/components/ListingContent/ListingContent'; import ListingItem from '@/components/ListingItem/ListingItem'; import { BuildStatus } from '@/components/Status/Status'; @@ -11,9 +9,10 @@ import BaseCard from '@/components/Cards/BaseCard'; import FilterLink from '@/components/Tabs/FilterLink'; import type { TFilter, TFilterObjectsKeys } from '@/types/general'; +import type { IBuildsTab } from '@/pages/TreeDetails/Tabs/Build/BuildTab'; interface IConfigsCard { - configs: ITreeDetails['configs']; + configs: IBuildsTab['configs']; toggleFilterBySection: ( value: string, filterSection: TFilterObjectsKeys, diff --git a/dashboard/src/components/Tabs/Builds/StatusCard.tsx b/dashboard/src/components/Tabs/Builds/StatusCard.tsx index 4104914c..ebd1d6f4 100644 --- a/dashboard/src/components/Tabs/Builds/StatusCard.tsx +++ b/dashboard/src/components/Tabs/Builds/StatusCard.tsx @@ -2,16 +2,15 @@ import { memo, useMemo } from 'react'; import { useIntl } from 'react-intl'; -import type { ITreeDetails } from '@/pages/TreeDetails/TreeDetails'; - import BaseCard from '@/components/Cards/BaseCard'; import StatusChartMemoized, { Colors, } from '@/components/StatusChart/StatusCharts'; import type { TFilterObjectsKeys } from '@/types/general'; +import type { IBuildsTab } from '@/pages/TreeDetails/Tabs/Build/BuildTab'; interface IStatusCard { - buildsSummary?: ITreeDetails['buildsSummary']; + buildsSummary?: IBuildsTab['buildsSummary']; toggleFilterBySection: ( value: string, filterSection: TFilterObjectsKeys, diff --git a/dashboard/src/hooks/useTreeDetailsLazyLoadQuery.ts b/dashboard/src/hooks/useTreeDetailsLazyLoadQuery.ts new file mode 100644 index 00000000..241136d4 --- /dev/null +++ b/dashboard/src/hooks/useTreeDetailsLazyLoadQuery.ts @@ -0,0 +1,138 @@ +import { useSearch } from '@tanstack/react-router'; + +import { useEffect, useState } from 'react'; + +import type { UseQueryResult } from '@tanstack/react-query'; + +import type { UseTreeDetailsWithoutVariant } from '@/api/treeDetails'; +import { useTreeDetails } from '@/api/treeDetails'; +import type { + PossibleTabs, + TreeDetailsBoots, + TreeDetailsBuilds, + TreeDetailsSummary, + TreeDetailsTests, +} from '@/types/tree/TreeDetails'; +import type { QuerySelectorStatus } from '@/components/QuerySwitcher/QuerySwitcher'; + +const canWeFetchAll = ( + isSummaryRead: boolean, + currentPageTab: PossibleTabs, + statusTable: Record, +): boolean => { + if (!isSummaryRead) { + return false; + } + + return statusTable[currentPageTab] ?? false; +}; + +export type TreeDetailsLazyLoaded = { + summary: { + data?: TreeDetailsSummary; + isLoading: boolean; + status: QuerySelectorStatus; + error: UseQueryResult['error']; + isPlaceholderData: boolean; + }; + builds: { + data?: TreeDetailsBuilds; + isLoading: boolean; + status: QuerySelectorStatus; + }; + boots: { + data?: TreeDetailsBoots; + isLoading: boolean; + status: QuerySelectorStatus; + }; + tests: { + data?: TreeDetailsTests; + isLoading: boolean; + status: QuerySelectorStatus; + }; +}; + +export const useTreeDetailsLazyLoadQuery = ( + useTreeDetailsArgs: UseTreeDetailsWithoutVariant, +): TreeDetailsLazyLoaded => { + const [fetchAll, setFetchAll] = useState(false); + + const summaryResult = useTreeDetails({ + ...useTreeDetailsArgs, + variant: 'summary', + }); + + const { currentPageTab } = useSearch({ + from: '/tree/$treeId/', + }); + + const buildsResult = useTreeDetails({ + ...useTreeDetailsArgs, + variant: 'builds', + enabled: + (!!summaryResult.data && currentPageTab === 'global.builds') || fetchAll, + }); + + const bootsResult = useTreeDetails({ + ...useTreeDetailsArgs, + variant: 'boots', + enabled: + (!!summaryResult.data && currentPageTab === 'global.boots') || fetchAll, + }); + + const testsResult = useTreeDetails({ + ...useTreeDetailsArgs, + variant: 'tests', + enabled: + (!!summaryResult.data && currentPageTab === 'global.tests') || fetchAll, + }); + + useEffect(() => { + if ( + canWeFetchAll(!!summaryResult.data, currentPageTab, { + 'global.builds': !!buildsResult.data, + 'global.boots': !!bootsResult.data, + 'global.tests': !!testsResult.data, + }) + ) { + setFetchAll(true); + } + }, [ + bootsResult.data, + bootsResult.isLoading, + buildsResult.data, + buildsResult.isLoading, + currentPageTab, + fetchAll, + setFetchAll, + summaryResult.data, + summaryResult.isLoading, + testsResult.data, + testsResult.isLoading, + ]); + + return { + summary: { + data: summaryResult.data, + isLoading: summaryResult.isLoading, + status: summaryResult.status, + isPlaceholderData: summaryResult.isPlaceholderData, + error: summaryResult.error, + }, + builds: { + data: buildsResult.data, + isLoading: buildsResult.isLoading, + status: buildsResult.status, + }, + boots: { + data: bootsResult.data, + isLoading: bootsResult.isLoading, + status: bootsResult.status, + }, + tests: { + data: testsResult.data, + isLoading: testsResult.isLoading, + status: testsResult.status, + }, + }; +}; diff --git a/dashboard/src/pages/TreeDetails/Tabs/Boots/BootsTab.tsx b/dashboard/src/pages/TreeDetails/Tabs/Boots/BootsTab.tsx index aa0c8246..aa9a35f0 100644 --- a/dashboard/src/pages/TreeDetails/Tabs/Boots/BootsTab.tsx +++ b/dashboard/src/pages/TreeDetails/Tabs/Boots/BootsTab.tsx @@ -5,7 +5,6 @@ import { useNavigate, useParams, useSearch } from '@tanstack/react-router'; import { useCallback, useMemo } from 'react'; -import { useTreeDetails } from '@/api/treeDetails'; import BaseCard from '@/components/Cards/BaseCard'; import { Skeleton } from '@/components/Skeleton'; @@ -27,15 +26,17 @@ import MemoizedConfigList from '@/components/Tabs/Tests/ConfigsList'; import MemoizedErrorsSummary from '@/components/Tabs/Tests/ErrorsSummary'; import MemoizedStatusCard from '@/components/Tabs/Tests/StatusCard'; -import { RedirectFrom, type TFilter } from '@/types/general'; +import { RedirectFrom } from '@/types/general'; import TreeCommitNavigationGraph from '@/pages/TreeDetails/Tabs/TreeCommitNavigationGraph'; +import type { TreeDetailsLazyLoaded } from '@/hooks/useTreeDetailsLazyLoadQuery'; +import QuerySwitcher from '@/components/QuerySwitcher/QuerySwitcher'; interface BootsTabProps { - reqFilter: TFilter; + treeDetailsLazyLoaded: TreeDetailsLazyLoaded; } -const BootsTab = ({ reqFilter }: BootsTabProps): JSX.Element => { +const BootsTab = ({ treeDetailsLazyLoaded }: BootsTabProps): JSX.Element => { const { treeId } = useParams({ from: '/tree/$treeId', }); @@ -80,10 +81,8 @@ const BootsTab = ({ reqFilter }: BootsTabProps): JSX.Element => { [navigate], ); - const { isLoading, data, error } = useTreeDetails({ - treeId: treeId ?? '', - filter: reqFilter, - }); + const { isLoading, data, error } = treeDetailsLazyLoaded.summary; + const { data: bootsData, status: bootsStatus } = treeDetailsLazyLoaded.boots; const getRowLink = useCallback( (bootId: string): LinkProps => ({ @@ -124,7 +123,7 @@ const BootsTab = ({ reqFilter }: BootsTabProps): JSX.Element => { if (!data) return
; - if (data.boots.length < 1) { + if (bootsData?.boots?.length === 0) { return ( } @@ -211,15 +210,17 @@ const BootsTab = ({ reqFilter }: BootsTabProps): JSX.Element => {
- + + + ); }; diff --git a/dashboard/src/pages/TreeDetails/Tabs/Build/BuildTab.tsx b/dashboard/src/pages/TreeDetails/Tabs/Build/BuildTab.tsx index 06f51154..b130abe2 100644 --- a/dashboard/src/pages/TreeDetails/Tabs/Build/BuildTab.tsx +++ b/dashboard/src/pages/TreeDetails/Tabs/Build/BuildTab.tsx @@ -1,11 +1,9 @@ import { FormattedMessage } from 'react-intl'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useNavigate, useParams, useSearch } from '@tanstack/react-router'; -import type { ITreeDetails } from '@/pages/TreeDetails/TreeDetails'; - import TreeCommitNavigationGraph from '@/pages/TreeDetails/Tabs/TreeCommitNavigationGraph'; import MemoizedIssuesList from '@/components/Cards/IssuesList'; @@ -22,15 +20,45 @@ import { MobileGrid, } from '@/components/Tabs/TabGrid'; -import { RedirectFrom, type TFilterObjectsKeys } from '@/types/general'; +import type { TreeDetailsLazyLoaded } from '@/hooks/useTreeDetailsLazyLoadQuery'; + +import { + sanitizeArchs, + sanitizeConfigs, + sanitizeBuildsSummary, + sanitizeBuilds, +} from '@/utils/utils'; + +import QuerySwitcher from '@/components/QuerySwitcher/QuerySwitcher'; + +import type { ISummaryItem } from '@/components/Tabs/Summary'; +import type { IListingItem } from '@/components/ListingItem/ListingItem'; + +import { + RedirectFrom, + type BuildStatus, + type TFilterObjectsKeys, + type TIssue, +} from '@/types/general'; + +import type { AccordionItemBuilds } from '@/types/tree/TreeDetails'; import { TreeDetailsBuildsTable } from './TreeDetailsBuildsTable'; interface BuildTab { - treeDetailsData: ITreeDetails; + treeDetailsLazyLoaded: TreeDetailsLazyLoaded; +} + +export interface IBuildsTab { + architectures: ISummaryItem[]; + configs: IListingItem[]; + buildsSummary: BuildStatus; + buildsIssues: TIssue[]; + failedBuildsWithUnknownIssues?: number; + builds: AccordionItemBuilds[]; } -const BuildTab = ({ treeDetailsData }: BuildTab): JSX.Element => { +const BuildTab = ({ treeDetailsLazyLoaded }: BuildTab): JSX.Element => { const navigate = useNavigate({ from: '/tree/$treeId', }); @@ -41,6 +69,11 @@ const BuildTab = ({ treeDetailsData }: BuildTab): JSX.Element => { const { treeId } = useParams({ from: '/tree/$treeId' }); + const { builds: buildsQuery, summary: summaryQuery } = treeDetailsLazyLoaded; + + const summaryData = summaryQuery.data?.summary.builds; + const { data: buildsData, status: buildsStatus } = buildsQuery; + const toggleFilterBySection = useCallback( (filterSectionKey: string, filterSection: TFilterObjectsKeys): void => { navigate({ @@ -66,6 +99,24 @@ const BuildTab = ({ treeDetailsData }: BuildTab): JSX.Element => { [navigate], ); + const treeDetailsData: IBuildsTab = useMemo( + () => ({ + architectures: sanitizeArchs(summaryData?.architectures), + configs: sanitizeConfigs(summaryData?.configs), + buildsSummary: sanitizeBuildsSummary(summaryData?.status), + buildsIssues: summaryData?.issues || [], + failedBuildsWithUnknownIssues: summaryData?.unknown_issues, + builds: sanitizeBuilds(buildsData?.builds), + }), + [ + buildsData?.builds, + summaryData?.architectures, + summaryData?.configs, + summaryData?.issues, + summaryData?.status, + summaryData?.unknown_issues, + ], + ); return (
@@ -131,15 +182,14 @@ const BuildTab = ({ treeDetailsData }: BuildTab): JSX.Element => { /> - {treeDetailsData && ( +
-
- )} +
); }; diff --git a/dashboard/src/pages/TreeDetails/Tabs/Tests/TestsTab.tsx b/dashboard/src/pages/TreeDetails/Tabs/Tests/TestsTab.tsx index 66200f0e..72bf35f7 100644 --- a/dashboard/src/pages/TreeDetails/Tabs/Tests/TestsTab.tsx +++ b/dashboard/src/pages/TreeDetails/Tabs/Tests/TestsTab.tsx @@ -7,7 +7,6 @@ import { useCallback, useMemo } from 'react'; import { Skeleton } from '@/components/Skeleton'; -import { useTreeDetails } from '@/api/treeDetails'; import BaseCard from '@/components/Cards/BaseCard'; import { @@ -29,20 +28,23 @@ import MemoizedConfigList from '@/components/Tabs/Tests/ConfigsList'; import MemoizedErrorsSummary from '@/components/Tabs/Tests/ErrorsSummary'; import MemoizedStatusCard from '@/components/Tabs/Tests/StatusCard'; -import { RedirectFrom, type TFilter } from '@/types/general'; +import { RedirectFrom } from '@/types/general'; import TreeCommitNavigationGraph from '@/pages/TreeDetails/Tabs/TreeCommitNavigationGraph'; +import type { TreeDetailsLazyLoaded } from '@/hooks/useTreeDetailsLazyLoadQuery'; +import QuerySwitcher from '@/components/QuerySwitcher/QuerySwitcher'; interface TestsTabProps { - reqFilter: TFilter; + treeDetailsLazyLoaded: TreeDetailsLazyLoaded; } -const TestsTab = ({ reqFilter }: TestsTabProps): JSX.Element => { +const TestsTab = ({ treeDetailsLazyLoaded }: TestsTabProps): JSX.Element => { const { treeId } = useParams({ from: '/tree/$treeId' }); - const { isLoading, data, error } = useTreeDetails({ - treeId: treeId ?? '', - filter: reqFilter, - }); + + const { tests: testsQuery, summary: summaryQuery } = treeDetailsLazyLoaded; + const { data, status, isLoading: testsIsLoading } = testsQuery; + const { isLoading: isSummaryLoading, error: summaryError } = summaryQuery; + const summaryData = treeDetailsLazyLoaded.summary.data?.summary.tests; const { tableFilter, diffFilter } = useSearch({ from: '/tree/$treeId', @@ -101,15 +103,12 @@ const TestsTab = ({ reqFilter }: TestsTabProps): JSX.Element => { const hardwareData = useMemo(() => { return { - ...data?.summary.tests.environment_compatible, - ...data?.summary.tests.environment_misc, + ...summaryData?.environment_compatible, + ...summaryData?.environment_misc, }; - }, [ - data?.summary.tests.environment_compatible, - data?.summary.tests.environment_misc, - ]); + }, [summaryData?.environment_compatible, summaryData?.environment_misc]); - if (error || !treeId) { + if (summaryError || !treeId) { return (
@@ -117,16 +116,16 @@ const TestsTab = ({ reqFilter }: TestsTabProps): JSX.Element => { ); } - if (isLoading) + if (isSummaryLoading) return ( ); - if (!data) return
; + if (!summaryData) return
; - if (data.tests.length < 1) { + if (!testsIsLoading && data?.tests.length === 0) { return ( } @@ -145,22 +144,22 @@ const TestsTab = ({ reqFilter }: TestsTabProps): JSX.Element => {
} - statusCounts={data.summary.tests.status} + statusCounts={summaryData.status} /> } - configStatusCounts={data.summary.tests.configs} + configStatusCounts={summaryData.configs} diffFilter={diffFilter} /> } - archCompilerErrors={data.summary.tests.architectures} + archCompilerErrors={summaryData.architectures} diffFilter={diffFilter} /> } - issues={data.summary.tests.issues} - failedWithUnknownIssues={data.summary.tests.unknown_issues} + issues={summaryData.issues} + failedWithUnknownIssues={summaryData.unknown_issues} diffFilter={diffFilter} issueFilterSection="testIssue" detailsId={treeId} @@ -179,25 +178,25 @@ const TestsTab = ({ reqFilter }: TestsTabProps): JSX.Element => { } - statusCounts={data.summary.tests.status} + statusCounts={summaryData.status} />
} - configStatusCounts={data.summary.tests.configs} + configStatusCounts={summaryData.configs} diffFilter={diffFilter} /> } - archCompilerErrors={data.summary.tests.architectures} + archCompilerErrors={summaryData.architectures} diffFilter={diffFilter} /> } - issues={data.summary.tests.issues} - failedWithUnknownIssues={data.summary.tests.unknown_issues} + issues={summaryData.issues} + failedWithUnknownIssues={summaryData.unknown_issues} diffFilter={diffFilter} issueFilterSection="testIssue" detailsId={treeId} @@ -214,15 +213,17 @@ const TestsTab = ({ reqFilter }: TestsTabProps): JSX.Element => { - + + +
); }; diff --git a/dashboard/src/pages/TreeDetails/Tabs/TreeDetailsTab.tsx b/dashboard/src/pages/TreeDetails/Tabs/TreeDetailsTab.tsx index 0dc0efc6..bd05ed75 100644 --- a/dashboard/src/pages/TreeDetails/Tabs/TreeDetailsTab.tsx +++ b/dashboard/src/pages/TreeDetails/Tabs/TreeDetailsTab.tsx @@ -8,9 +8,7 @@ import Tabs from '@/components/Tabs/Tabs'; import { zPossibleTabValidator } from '@/types/tree/TreeDetails'; -import type { ITreeDetails } from '@/pages/TreeDetails/TreeDetails'; - -import type { TFilter } from '@/types/general'; +import type { TreeDetailsLazyLoaded } from '@/hooks/useTreeDetailsLazyLoadQuery'; import BuildTab from './Build'; import BootsTab from './Boots'; @@ -22,17 +20,15 @@ export type TreeDetailsTabRightElement = Record< >; export interface ITreeDetailsTab { - treeDetailsData: ITreeDetails; + treeDetailsLazyLoaded: TreeDetailsLazyLoaded; filterListElement?: JSX.Element; - reqFilter: TFilter; countElements: TreeDetailsTabRightElement; } const TreeDetailsTab = ({ - treeDetailsData, filterListElement, - reqFilter, countElements, + treeDetailsLazyLoaded, }: ITreeDetailsTab): JSX.Element => { const { currentPageTab } = useSearch({ from: '/tree/$treeId', @@ -42,24 +38,24 @@ const TreeDetailsTab = ({ () => [ { name: 'global.builds', - content: , + content: , disabled: false, rightElement: countElements['global.builds'], }, { name: 'global.boots', - content: , + content: , disabled: false, rightElement: countElements['global.boots'], }, { name: 'global.tests', - content: , + content: , disabled: false, rightElement: countElements['global.tests'], }, ], - [countElements, reqFilter, treeDetailsData], + [countElements, treeDetailsLazyLoaded], ); const onValueChange: (value: string) => void = useCallback( diff --git a/dashboard/src/pages/TreeDetails/TreeDetails.tsx b/dashboard/src/pages/TreeDetails/TreeDetails.tsx index a9ef229b..4cece500 100644 --- a/dashboard/src/pages/TreeDetails/TreeDetails.tsx +++ b/dashboard/src/pages/TreeDetails/TreeDetails.tsx @@ -3,8 +3,6 @@ import { useCallback, useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; -import type { IListingItem } from '@/components/ListingItem/ListingItem'; -import type { AccordionItemBuilds } from '@/types/tree/TreeDetails'; import { Breadcrumb, BreadcrumbItem, @@ -31,14 +29,7 @@ import { GroupedTestStatus, } from '@/components/Status/Status'; -import { - sanitizeArchs, - sanitizeBuilds, - sanitizeBuildsSummary, - sanitizeConfigs, -} from '@/utils/utils'; - -import type { BuildStatus, TFilter, TIssue } from '@/types/general'; +import type { TFilter } from '@/types/general'; import MemoizedHardwareUsed from '@/components/Cards/HardwareUsed'; @@ -46,10 +37,6 @@ import { mapFilterToReq } from '@/components/Tabs/Filters'; import DetailsFilterList from '@/components/Tabs/FilterList'; -import type { ISummaryItem } from '@/components/Tabs/Summary'; - -import { useTreeDetails } from '@/api/treeDetails'; - import { truncateUrl } from '@/lib/string'; import CopyButton from '@/components/Button/CopyButton'; @@ -57,19 +44,12 @@ import { MemoizedSectionError } from '@/components/DetailsPages/SectionError'; import { CommitTagTooltip } from '@/components/Tooltip/CommitTagTooltip'; +import { useTreeDetailsLazyLoadQuery } from '@/hooks/useTreeDetailsLazyLoadQuery'; + import TreeDetailsFilter from './TreeDetailsFilter'; import type { TreeDetailsTabRightElement } from './Tabs/TreeDetailsTab'; import TreeDetailsTab from './Tabs/TreeDetailsTab'; -export interface ITreeDetails { - architectures: ISummaryItem[]; - configs: IListingItem[]; - buildsSummary: BuildStatus; - builds: AccordionItemBuilds[]; - buildsIssues: TIssue[]; - failedBuildsWithUnknownIssues?: number; -} - interface ITreeHeader { treeNames?: string; gitUrl?: string; @@ -145,11 +125,18 @@ function TreeDetails(): JSX.Element { const reqFilter = mapFilterToReq(diffFilter); - const { isLoading, data, status, isPlaceholderData, error } = useTreeDetails({ + const treeDetailsLazyLoaded = useTreeDetailsLazyLoadQuery({ treeId: treeId ?? '', filter: reqFilter, }); + const { + data, + isLoading, + error, + status: summaryQueryStatus, + } = treeDetailsLazyLoaded.summary; + const onFilterChange = useCallback( (newFilter: TFilter) => { navigate({ @@ -181,10 +168,15 @@ function TreeDetails(): JSX.Element { filter={diffFilter} cleanFilters={cleanAll} navigate={onFilterChange} - isLoading={isPlaceholderData} + isLoading={treeDetailsLazyLoaded.summary.isPlaceholderData} /> ), - [cleanAll, diffFilter, isPlaceholderData, onFilterChange], + [ + cleanAll, + diffFilter, + onFilterChange, + treeDetailsLazyLoaded.summary.isPlaceholderData, + ], ); const tabsCounts: TreeDetailsTabRightElement = useMemo(() => { @@ -224,21 +216,9 @@ function TreeDetails(): JSX.Element { }; }, [data]); - const treeDetailsData: ITreeDetails = useMemo( - () => ({ - architectures: sanitizeArchs(data?.summary.builds.architectures), - configs: sanitizeConfigs(data?.summary.builds.configs), - builds: sanitizeBuilds(data?.builds), - buildsSummary: sanitizeBuildsSummary(data?.summary.builds.status), - buildsIssues: data?.summary.builds.issues || [], - failedBuildsWithUnknownIssues: data?.summary.builds.unknown_issues, - }), - [data], - ); - return (
- -
- } - hardwareUsed={data?.summary.hardware} - diffFilter={diffFilter} - /> -
-
+
+ } + hardwareUsed={data?.summary.hardware} + diffFilter={diffFilter} + /> +
{data?.summary.tree_url && (
@@ -307,9 +281,8 @@ function TreeDetails(): JSX.Element {
)}
diff --git a/dashboard/src/pages/TreeDetails/TreeDetailsFilter.tsx b/dashboard/src/pages/TreeDetails/TreeDetailsFilter.tsx index b055d3bb..8a03f7ef 100644 --- a/dashboard/src/pages/TreeDetails/TreeDetailsFilter.tsx +++ b/dashboard/src/pages/TreeDetails/TreeDetailsFilter.tsx @@ -140,6 +140,8 @@ const TreeDetailsFilter = ({ const { data, isLoading } = useTreeDetails({ treeId, + // TODO : use tree details summary + variant: 'full', }); const navigate = useNavigate({ diff --git a/dashboard/src/types/tree/TreeDetails.tsx b/dashboard/src/types/tree/TreeDetails.tsx index d5cec039..f2edaa53 100644 --- a/dashboard/src/types/tree/TreeDetails.tsx +++ b/dashboard/src/types/tree/TreeDetails.tsx @@ -100,6 +100,22 @@ export type TTreeTestsFullData = { summary: TreeSummary; }; +export type TreeDetailsSummary = { + summary: TreeSummary; +}; + +export type TreeDetailsBuilds = { + builds: BuildsTabBuild[]; +}; + +export type TreeDetailsTests = { + tests: TestHistory[]; +}; + +export type TreeDetailsBoots = { + boots: TestHistory[]; +}; + export const possibleTabs = [ 'global.builds', 'global.boots', @@ -135,6 +151,8 @@ export const zPossibleTabValidator = z .default(defaultValidadorValues.tab) .catch(defaultValidadorValues.tab); +export type PossibleTabs = z.infer; + export const zBuildsTableFilterValidator = z .enum(possibleBuildsTableFilter) .catch(defaultValidadorValues.buildsTableFilter); From 2a17bab02cfb85b6ff5c8081d039fcf7f45fe525 Mon Sep 17 00:00:00 2001 From: Wilson Neto Date: Wed, 22 Jan 2025 11:46:50 -0300 Subject: [PATCH 2/5] Reapply "feat: add loading state to TreeDetails component" This reverts commit b1efcea59cfc04d31d92d4bf4eb9834258bd8e7c. --- .../src/hooks/useTreeDetailsLazyLoadQuery.ts | 20 +++++++++++++++++++ .../src/pages/TreeDetails/TreeDetails.tsx | 16 ++++++++++----- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/dashboard/src/hooks/useTreeDetailsLazyLoadQuery.ts b/dashboard/src/hooks/useTreeDetailsLazyLoadQuery.ts index 241136d4..26789532 100644 --- a/dashboard/src/hooks/useTreeDetailsLazyLoadQuery.ts +++ b/dashboard/src/hooks/useTreeDetailsLazyLoadQuery.ts @@ -50,6 +50,10 @@ export type TreeDetailsLazyLoaded = { isLoading: boolean; status: QuerySelectorStatus; }; + common: { + isAllReady: boolean; + isAnyLoading: boolean; + }; }; export const useTreeDetailsLazyLoadQuery = ( @@ -87,6 +91,18 @@ export const useTreeDetailsLazyLoadQuery = ( (!!summaryResult.data && currentPageTab === 'global.tests') || fetchAll, }); + const isAllReady = + !!summaryResult.data && + !!buildsResult.data && + !!bootsResult.data && + !!testsResult.data; + + const isAnyLoading = + summaryResult.isLoading || + buildsResult.isLoading || + bootsResult.isLoading || + testsResult.isLoading; + useEffect(() => { if ( canWeFetchAll(!!summaryResult.data, currentPageTab, { @@ -134,5 +150,9 @@ export const useTreeDetailsLazyLoadQuery = ( isLoading: testsResult.isLoading, status: testsResult.status, }, + common: { + isAllReady, + isAnyLoading, + }, }; }; diff --git a/dashboard/src/pages/TreeDetails/TreeDetails.tsx b/dashboard/src/pages/TreeDetails/TreeDetails.tsx index 4cece500..5624f27c 100644 --- a/dashboard/src/pages/TreeDetails/TreeDetails.tsx +++ b/dashboard/src/pages/TreeDetails/TreeDetails.tsx @@ -46,6 +46,8 @@ import { CommitTagTooltip } from '@/components/Tooltip/CommitTagTooltip'; import { useTreeDetailsLazyLoadQuery } from '@/hooks/useTreeDetailsLazyLoadQuery'; +import { LoadingCircle } from '@/components/ui/loading-circle'; + import TreeDetailsFilter from './TreeDetailsFilter'; import type { TreeDetailsTabRightElement } from './Tabs/TreeDetailsTab'; import TreeDetailsTab from './Tabs/TreeDetailsTab'; @@ -130,6 +132,7 @@ function TreeDetails(): JSX.Element { filter: reqFilter, }); + const { isAllReady, isAnyLoading } = treeDetailsLazyLoaded.common; const { data, isLoading, @@ -270,16 +273,19 @@ function TreeDetails(): JSX.Element { />
- {data?.summary.tree_url && ( -
-
+
+
+ {data && isAllReady && !isAnyLoading && ( -
+ )} + {!isAllReady && isAnyLoading && ( + + )}
- )} +
Date: Tue, 21 Jan 2025 12:57:07 -0300 Subject: [PATCH 3/5] feat(tree-details): add filters field - Added `process_filter` function to helpers/treeDetails.py, the function responsible to get the value of all possible filters in the tree details response - Added 'common' and 'filters' fields to tree details summary response - Added 'common' and 'filters' fields to tree details response Part of #792 --- backend/kernelCI_app/helpers/treeDetails.py | 41 +++++++++ .../kernelCI_app/typeModels/treeDetails.py | 23 ++++- .../views/treeDetailsSummaryView.py | 35 +++++++- backend/kernelCI_app/views/treeDetailsView.py | 27 ++++++ backend/schema.yml | 86 ++++++++++++++++--- .../src/pages/TreeDetails/TreeDetails.tsx | 6 +- .../pages/TreeDetails/TreeDetailsFilter.tsx | 2 +- dashboard/src/types/tree/TreeDetails.tsx | 24 ++++++ 8 files changed, 222 insertions(+), 22 deletions(-) diff --git a/backend/kernelCI_app/helpers/treeDetails.py b/backend/kernelCI_app/helpers/treeDetails.py index 34dc4aa8..c340ad55 100644 --- a/backend/kernelCI_app/helpers/treeDetails.py +++ b/backend/kernelCI_app/helpers/treeDetails.py @@ -17,6 +17,7 @@ ) from kernelCI_app.cache import getQueryCache, setQueryCache from kernelCI_app.utils import is_boot +from kernelCI_app.constants.hardwareDetails import STATUS_FAILED_VALUE from django.db import connection @@ -510,3 +511,43 @@ def process_boots_summary(instance, row_data): ] += 1 else: instance.bootEnvironmentMisc[test_platform][test_status] += 1 + + +def process_filters(instance, row_data: dict) -> None: + if row_data["build_id"] is not None: + instance.global_configs.add(row_data["build_config_name"]) + instance.global_architectures.add(row_data["build_architecture"]) + instance.global_compilers.add(row_data["build_config_name"]) + + issue_id = row_data["issue_id"] + issue_version = row_data["issue_version"] + incident_test_id = row_data["incident_test_id"] + build_valid = row_data["build_valid"] + + if row_data["build_id"] is not None: + build_issue_id, is_build_issue = should_increment_build_issue( + issue_id=issue_id, + incident_test_id=incident_test_id, + build_valid=build_valid, + ) + + if build_issue_id is not None and issue_version is not None and is_build_issue: + instance.unfiltered_build_issues.add(build_issue_id) + elif build_valid is False: + instance.unfiltered_build_issues.add(UNKNOWN_STRING) + + if row_data["test_id"] is not None: + test_issue_id, is_test_issue = should_increment_test_issue( + issue_id=issue_id, + incident_test_id=incident_test_id, + ) + + if is_boot(row_data["test_path"]): + issue_set = instance.unfiltered_boot_issues + else: + issue_set = instance.unfiltered_test_issues + + if test_issue_id is not None and issue_version is not None and is_test_issue: + issue_set.add(test_issue_id) + elif row_data["test_status"] == STATUS_FAILED_VALUE: + issue_set.add(UNKNOWN_STRING) diff --git a/backend/kernelCI_app/typeModels/treeDetails.py b/backend/kernelCI_app/typeModels/treeDetails.py index 8e52987f..82363b82 100644 --- a/backend/kernelCI_app/typeModels/treeDetails.py +++ b/backend/kernelCI_app/typeModels/treeDetails.py @@ -28,14 +28,33 @@ class TestSummary(BaseModel): failed_platforms: List[str] -class TreeSummary(Summary): +class TreeCommon(BaseModel): hardware: Optional[List[str]] tree_url: Optional[str] git_commit_tags: Optional[List[str]] +class TreeGlobalFilters(BaseModel): + configs: List[str] + architectures: List[str] + compilers: List[str] + + +class TreeLocalFilters(BaseModel): + issues: List[str] + + +class TreeFilters(BaseModel): + all: TreeGlobalFilters + builds: TreeLocalFilters + boots: TreeLocalFilters + tests: TreeLocalFilters + + class SummaryResponse(BaseModel): - summary: TreeSummary + common: TreeCommon + summary: Summary + filters: TreeFilters class BootResponse(BaseModel): diff --git a/backend/kernelCI_app/views/treeDetailsSummaryView.py b/backend/kernelCI_app/views/treeDetailsSummaryView.py index 63495d48..ee325b7a 100644 --- a/backend/kernelCI_app/views/treeDetailsSummaryView.py +++ b/backend/kernelCI_app/views/treeDetailsSummaryView.py @@ -17,6 +17,7 @@ process_builds_issue, process_test_summary, process_tests_issue, + process_filters, ) from kernelCI_app.typeModels.treeDetails import SummaryResponse from kernelCI_app.utils import ( @@ -67,6 +68,13 @@ def __init__(self): self.tree_url = "" self.git_commit_tags = [] + self.global_configs = set() + self.global_architectures = set() + self.global_compilers = set() + self.unfiltered_test_issues = set() + self.unfiltered_boot_issues = set() + self.unfiltered_build_issues = set() + def _process_boots_test(self, row_data): test_id = row_data["test_id"] @@ -120,6 +128,7 @@ def _sanitize_rows(self, rows): call_based_on_compatible_and_misc_platform(row_data, self.hardwareUsed.add) process_tree_url(self, row_data) + process_filters(self, row_data) is_record_filter_out = decide_if_is_full_row_filtered_out(self, row_data) @@ -158,6 +167,11 @@ def get(self, request, commit_hash: str | None): self._sanitize_rows(rows) response = { + "common": { + "tree_url": self.tree_url, + "hardware": list(self.hardwareUsed), + "git_commit_tags": self.git_commit_tags, + }, "summary": { "builds": { "status": self.build_summary["builds"], @@ -188,10 +202,23 @@ def get(self, request, commit_hash: str | None): "fail_reasons": self.testFailReasons, "failed_platforms": list(self.testPlatformsWithErrors), }, - "hardware": list(self.hardwareUsed), - "tree_url": self.tree_url, - "git_commit_tags": self.git_commit_tags, - } + }, + "filters": { + "all": { + "configs": list(self.global_configs), + "architectures": list(self.global_architectures), + "compilers": list(self.global_compilers), + }, + "builds": { + "issues": list(self.unfiltered_build_issues), + }, + "boots": { + "issues": list(self.unfiltered_boot_issues), + }, + "tests": { + "issues": list(self.unfiltered_test_issues), + }, + }, } try: diff --git a/backend/kernelCI_app/views/treeDetailsView.py b/backend/kernelCI_app/views/treeDetailsView.py index fe6a60f0..0786a776 100644 --- a/backend/kernelCI_app/views/treeDetailsView.py +++ b/backend/kernelCI_app/views/treeDetailsView.py @@ -22,6 +22,7 @@ process_builds_issue, process_test_summary, process_tests_issue, + process_filters, ) from kernelCI_app.utils import ( Issue, @@ -72,6 +73,13 @@ def __init__(self): self.tree_url = "" self.git_commit_tags = [] + self.global_configs = set() + self.global_architectures = set() + self.global_compilers = set() + self.unfiltered_test_issues = set() + self.unfiltered_boot_issues = set() + self.unfiltered_build_issues = set() + def _process_boots_test(self, row_data): test_id = row_data["test_id"] history_item = row_data["history_item"] @@ -129,6 +137,7 @@ def _sanitize_rows(self, rows): call_based_on_compatible_and_misc_platform(row_data, self.hardwareUsed.add) process_tree_url(self, row_data) + process_filters(self, row_data) is_record_filter_out = decide_if_is_full_row_filtered_out(self, row_data) @@ -198,10 +207,28 @@ def get(self, request, commit_hash: str | None): "fail_reasons": self.testFailReasons, "failed_platforms": list(self.testPlatformsWithErrors), }, + }, + "common": { "hardware": list(self.hardwareUsed), "tree_url": self.tree_url, "git_commit_tags": self.git_commit_tags, }, + "filters": { + "all": { + "configs": list(self.global_configs), + "architectures": list(self.global_architectures), + "compilers": list(self.global_compilers), + }, + "builds": { + "issues": list(self.unfiltered_build_issues), + }, + "boots": { + "issues": list(self.unfiltered_boot_issues), + }, + "tests": { + "issues": list(self.unfiltered_test_issues), + }, + }, }, safe=False, ) diff --git a/backend/schema.yml b/backend/schema.yml index 23a2c5c0..cfb3ae32 100644 --- a/backend/schema.yml +++ b/backend/schema.yml @@ -663,12 +663,32 @@ components: - platform title: Misc type: object + Summary: + properties: + builds: + $ref: '#/components/schemas/BuildSummary' + boots: + $ref: '#/components/schemas/TestSummary' + tests: + $ref: '#/components/schemas/TestSummary' + required: + - builds + - boots + - tests + title: Summary + type: object SummaryResponse: properties: + common: + $ref: '#/components/schemas/TreeCommon' summary: - $ref: '#/components/schemas/TreeSummary' + $ref: '#/components/schemas/Summary' + filters: + $ref: '#/components/schemas/TreeFilters' required: + - common - summary + - filters title: SummaryResponse type: object TestArchSummaryItem: @@ -945,14 +965,8 @@ components: - selected_commit_status title: Tree type: object - TreeSummary: + TreeCommon: properties: - builds: - $ref: '#/components/schemas/BuildSummary' - boots: - $ref: '#/components/schemas/TestSummary' - tests: - $ref: '#/components/schemas/TestSummary' hardware: anyOf: - items: @@ -973,13 +987,61 @@ components: - type: 'null' title: Git Commit Tags required: - - builds - - boots - - tests - hardware - tree_url - git_commit_tags - title: TreeSummary + title: TreeCommon + type: object + TreeFilters: + properties: + all: + $ref: '#/components/schemas/TreeGlobalFilters' + builds: + $ref: '#/components/schemas/TreeLocalFilters' + boots: + $ref: '#/components/schemas/TreeLocalFilters' + tests: + $ref: '#/components/schemas/TreeLocalFilters' + required: + - all + - builds + - boots + - tests + title: TreeFilters + type: object + TreeGlobalFilters: + properties: + configs: + items: + type: string + title: Configs + type: array + architectures: + items: + type: string + title: Architectures + type: array + compilers: + items: + type: string + title: Compilers + type: array + required: + - configs + - architectures + - compilers + title: TreeGlobalFilters + type: object + TreeLocalFilters: + properties: + issues: + items: + type: string + title: Issues + type: array + required: + - issues + title: TreeLocalFilters type: object securitySchemes: basicAuth: diff --git a/dashboard/src/pages/TreeDetails/TreeDetails.tsx b/dashboard/src/pages/TreeDetails/TreeDetails.tsx index 5624f27c..529306f6 100644 --- a/dashboard/src/pages/TreeDetails/TreeDetails.tsx +++ b/dashboard/src/pages/TreeDetails/TreeDetails.tsx @@ -262,13 +262,13 @@ function TreeDetails(): JSX.Element { gitUrl={treeInfo?.gitUrl} commitHash={treeId} commitName={treeInfo?.commitName} - commitTags={data?.summary.git_commit_tags} + commitTags={data?.common.git_commit_tags} />
} - hardwareUsed={data?.summary.hardware} + hardwareUsed={data?.common.hardware} diffFilter={diffFilter} />
@@ -278,7 +278,7 @@ function TreeDetails(): JSX.Element { {data && isAllReady && !isAnyLoading && ( )} {!isAllReady && isAnyLoading && ( diff --git a/dashboard/src/pages/TreeDetails/TreeDetailsFilter.tsx b/dashboard/src/pages/TreeDetails/TreeDetailsFilter.tsx index 8a03f7ef..cbcb6e67 100644 --- a/dashboard/src/pages/TreeDetails/TreeDetailsFilter.tsx +++ b/dashboard/src/pages/TreeDetails/TreeDetailsFilter.tsx @@ -55,7 +55,7 @@ export const createFilter = (data: TTreeTestsFullData | undefined): TFilter => { compilers[b.compiler ?? 'Unknown'] = false; }); - data.summary.hardware.forEach(h => (hardware[h] = false)); + data.common.hardware.forEach(h => (hardware[h] = false)); data.summary.builds.issues.forEach(i => (buildIssue[i.id] = false)); data.summary.boots.issues.forEach(i => (bootIssue[i.id] = false)); diff --git a/dashboard/src/types/tree/TreeDetails.tsx b/dashboard/src/types/tree/TreeDetails.tsx index f2edaa53..d10b250b 100644 --- a/dashboard/src/types/tree/TreeDetails.tsx +++ b/dashboard/src/types/tree/TreeDetails.tsx @@ -88,20 +88,44 @@ type TreeSummary = { boots: TestSummary; builds: BuildSummary; tests: TestSummary; +}; + +type TreeCommon = { hardware: string[]; tree_url: string; git_commit_tags: string[]; }; +type TreeGlobalFilters = { + configs: string[]; + architectures: string[]; + compilers: string[]; +}; + +type TreeLocalFilters = { + issues: string[]; +}; + +type TreeFilters = { + all: TreeGlobalFilters; + builds: TreeLocalFilters; + boots: TreeLocalFilters; + tests: TreeLocalFilters; +}; + export type TTreeTestsFullData = { builds: BuildsTabBuild[]; boots: TestHistory[]; tests: TestHistory[]; summary: TreeSummary; + common: TreeCommon; + filters: TreeFilters; }; export type TreeDetailsSummary = { summary: TreeSummary; + common: TreeCommon; + filters: TreeFilters; }; export type TreeDetailsBuilds = { From 7dd51984de7a064c6d2cc114d82e4b670e4dd650 Mon Sep 17 00:00:00 2001 From: Murilo Geraldini Date: Tue, 21 Jan 2025 16:06:36 -0300 Subject: [PATCH 4/5] fix(tree-details-filters): remove extra query Closes #792 --- .../src/components/Checkbox/Checkbox.tsx | 10 ++- .../src/pages/TreeDetails/TreeDetails.tsx | 7 +- .../pages/TreeDetails/TreeDetailsFilter.tsx | 69 +++++++------------ 3 files changed, 38 insertions(+), 48 deletions(-) diff --git a/dashboard/src/components/Checkbox/Checkbox.tsx b/dashboard/src/components/Checkbox/Checkbox.tsx index 99e7db5c..c13619cb 100644 --- a/dashboard/src/components/Checkbox/Checkbox.tsx +++ b/dashboard/src/components/Checkbox/Checkbox.tsx @@ -1,5 +1,7 @@ import cls from 'classnames'; +import { isUrl, truncateBigText, truncateUrl } from '@/lib/string'; + interface ICheckbox { onToggle: () => void; isChecked?: boolean; @@ -10,12 +12,18 @@ interface ICheckbox { const containerClass = 'min-w-[300px] p-4 border-[2px] border-darkGray rounded cursor-pointer text-dimGray'; +const maxCheckboxLength = 30; + const Checkbox = ({ text, onToggle, className, isChecked = false, }: ICheckbox): JSX.Element => { + let truncatedText = text; + if (isUrl(text)) truncatedText = truncateUrl(text); + else truncatedText = truncateBigText(text, maxCheckboxLength); + return ( ); }; diff --git a/dashboard/src/pages/TreeDetails/TreeDetails.tsx b/dashboard/src/pages/TreeDetails/TreeDetails.tsx index 529306f6..37bd0bfc 100644 --- a/dashboard/src/pages/TreeDetails/TreeDetails.tsx +++ b/dashboard/src/pages/TreeDetails/TreeDetails.tsx @@ -132,7 +132,6 @@ function TreeDetails(): JSX.Element { filter: reqFilter, }); - const { isAllReady, isAnyLoading } = treeDetailsLazyLoaded.common; const { data, isLoading, @@ -275,13 +274,13 @@ function TreeDetails(): JSX.Element {
- {data && isAllReady && !isAnyLoading && ( + {data ? ( - )} - {!isAllReady && isAnyLoading && ( + ) : ( )}
diff --git a/dashboard/src/pages/TreeDetails/TreeDetailsFilter.tsx b/dashboard/src/pages/TreeDetails/TreeDetailsFilter.tsx index cbcb6e67..fa5ce8a9 100644 --- a/dashboard/src/pages/TreeDetails/TreeDetailsFilter.tsx +++ b/dashboard/src/pages/TreeDetails/TreeDetailsFilter.tsx @@ -1,16 +1,13 @@ import { useCallback, useMemo, useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { useNavigate, useParams } from '@tanstack/react-router'; +import { useNavigate } from '@tanstack/react-router'; import { status as testStatuses } from '@/utils/constants/database'; import type { IDrawerLink } from '@/components/Filter/Drawer'; import FilterDrawer from '@/components/Filter/Drawer'; -import type { TTreeTestsFullData } from '@/types/tree/TreeDetails'; +import type { TreeDetailsSummary } from '@/types/tree/TreeDetails'; import type { ISectionItem } from '@/components/Filter/CheckboxSection'; -import { Skeleton } from '@/components/Skeleton'; - import { MemoizedCheckboxSection, MemoizedTimeRangeSection, @@ -19,16 +16,15 @@ import { import { isTFilterObjectKeys, type TFilter } from '@/types/general'; import { cleanFalseFilters } from '@/components/Tabs/tabsUtils'; -import { useTreeDetails } from '@/api/treeDetails'; - type TFilterValues = Record; interface ITreeDetailsFilter { paramFilter: TFilter; treeUrl: string; + data: TreeDetailsSummary; } -export const createFilter = (data: TTreeTestsFullData | undefined): TFilter => { +export const createFilter = (data: TreeDetailsSummary | undefined): TFilter => { const buildStatus = { Success: false, Failed: false, Inconclusive: false }; const bootStatus: TFilterValues = {}; @@ -49,17 +45,17 @@ export const createFilter = (data: TTreeTestsFullData | undefined): TFilter => { const hardware: TFilterValues = {}; if (data) { - data.builds.forEach(b => { - configs[b.config_name ?? 'Unknown'] = false; - archs[b.architecture ?? 'Unknown'] = false; - compilers[b.compiler ?? 'Unknown'] = false; - }); + data.filters.all.configs.forEach(config => (configs[config] = false)); + data.filters.all.architectures.forEach(arch => (archs[arch] = false)); + data.filters.all.compilers.forEach( + compiler => (compilers[compiler] = false), + ); data.common.hardware.forEach(h => (hardware[h] = false)); - data.summary.builds.issues.forEach(i => (buildIssue[i.id] = false)); - data.summary.boots.issues.forEach(i => (bootIssue[i.id] = false)); - data.summary.tests.issues.forEach(i => (testIssue[i.id] = false)); + data.filters.builds.issues.forEach(i => (buildIssue[i] = false)); + data.filters.boots.issues.forEach(i => (bootIssue[i] = false)); + data.filters.tests.issues.forEach(i => (testIssue[i] = false)); } return { @@ -135,15 +131,8 @@ const sectionTrees: ISectionItem[] = [ const TreeDetailsFilter = ({ paramFilter, treeUrl, + data, }: ITreeDetailsFilter): JSX.Element => { - const { treeId } = useParams({ from: '/tree/$treeId' }); - - const { data, isLoading } = useTreeDetails({ - treeId, - // TODO : use tree details summary - variant: 'full', - }); - const navigate = useNavigate({ from: '/tree/$treeId', }); @@ -193,25 +182,19 @@ const TreeDetailsFilter = ({ onOpenChange={handleOpenChange} onCancel={onClickCancel} > - {isLoading ? ( - - - - ) : ( - <> - - - - )} + <> + + + ); }; From 0bc56836f77aac14b1ac512bb5a9bc0dafb99b07 Mon Sep 17 00:00:00 2001 From: Wilson Neto Date: Wed, 22 Jan 2025 11:12:37 -0300 Subject: [PATCH 5/5] feat(tree-details): lazy load summary to full Make the lazy load be tree details to full, so we don't get 4 requests, only two Renamed TTreeTestsFullData to TreeDetailsFullData across the codebase for better clarity and consistency. Updated all references and dependencies to use the new type name. Closes #791 --- dashboard/src/api/treeDetails.ts | 8 +- .../src/hooks/useTreeDetailsLazyLoadQuery.ts | 120 ++---------------- .../pages/TreeDetails/Tabs/Boots/BootsTab.tsx | 10 +- .../pages/TreeDetails/Tabs/Build/BuildTab.tsx | 13 +- .../pages/TreeDetails/Tabs/Tests/TestsTab.tsx | 6 +- dashboard/src/types/tree/TreeDetails.tsx | 2 +- 6 files changed, 32 insertions(+), 127 deletions(-) diff --git a/dashboard/src/api/treeDetails.ts b/dashboard/src/api/treeDetails.ts index e3cc172a..4a7fc37b 100644 --- a/dashboard/src/api/treeDetails.ts +++ b/dashboard/src/api/treeDetails.ts @@ -6,7 +6,7 @@ import { useSearch } from '@tanstack/react-router'; import type { TTreeDetailsFilter, BuildCountsResponse, - TTreeTestsFullData, + TreeDetailsFullData, LogFilesResponse, TreeDetailsSummary, TreeDetailsBoots, @@ -38,7 +38,7 @@ const useTreeSearchParameters = (): TreeSearchParameters => { type TreeDetailsVariants = 'full' | 'builds' | 'boots' | 'tests' | 'summary'; type TreeDetailsResponseTable = { - full: TTreeTestsFullData; + full: TreeDetailsFullData; summary: TreeDetailsSummary; builds: TreeDetailsBuilds; boots: TreeDetailsBoots; @@ -55,7 +55,7 @@ const fetchTreeDetails = async ({ treeSearchParameters: TreeSearchParameters; filter: TTreeDetailsFilter; variant: TreeDetailsVariants; -}): Promise => { +}): Promise => { const backendCompatibleFilters = mapFiltersKeysToBackendCompatible(filter); const params = { @@ -72,7 +72,7 @@ const fetchTreeDetails = async ({ tests: `/api/tree/${treeId}/tests`, summary: `/api/tree/${treeId}/summary`, }; - const res = await http.get(urlTable[variant], { + const res = await http.get(urlTable[variant], { params: params, }); diff --git a/dashboard/src/hooks/useTreeDetailsLazyLoadQuery.ts b/dashboard/src/hooks/useTreeDetailsLazyLoadQuery.ts index 26789532..49f86e63 100644 --- a/dashboard/src/hooks/useTreeDetailsLazyLoadQuery.ts +++ b/dashboard/src/hooks/useTreeDetailsLazyLoadQuery.ts @@ -1,32 +1,13 @@ -import { useSearch } from '@tanstack/react-router'; - -import { useEffect, useState } from 'react'; - import type { UseQueryResult } from '@tanstack/react-query'; import type { UseTreeDetailsWithoutVariant } from '@/api/treeDetails'; import { useTreeDetails } from '@/api/treeDetails'; import type { - PossibleTabs, - TreeDetailsBoots, - TreeDetailsBuilds, + TreeDetailsFullData, TreeDetailsSummary, - TreeDetailsTests, } from '@/types/tree/TreeDetails'; import type { QuerySelectorStatus } from '@/components/QuerySwitcher/QuerySwitcher'; -const canWeFetchAll = ( - isSummaryRead: boolean, - currentPageTab: PossibleTabs, - statusTable: Record, -): boolean => { - if (!isSummaryRead) { - return false; - } - - return statusTable[currentPageTab] ?? false; -}; - export type TreeDetailsLazyLoaded = { summary: { data?: TreeDetailsSummary; @@ -35,18 +16,8 @@ export type TreeDetailsLazyLoaded = { error: UseQueryResult['error']; isPlaceholderData: boolean; }; - builds: { - data?: TreeDetailsBuilds; - isLoading: boolean; - status: QuerySelectorStatus; - }; - boots: { - data?: TreeDetailsBoots; - isLoading: boolean; - status: QuerySelectorStatus; - }; - tests: { - data?: TreeDetailsTests; + full: { + data?: TreeDetailsFullData; isLoading: boolean; status: QuerySelectorStatus; }; @@ -59,74 +30,17 @@ export type TreeDetailsLazyLoaded = { export const useTreeDetailsLazyLoadQuery = ( useTreeDetailsArgs: UseTreeDetailsWithoutVariant, ): TreeDetailsLazyLoaded => { - const [fetchAll, setFetchAll] = useState(false); - const summaryResult = useTreeDetails({ ...useTreeDetailsArgs, variant: 'summary', }); - const { currentPageTab } = useSearch({ - from: '/tree/$treeId/', - }); - - const buildsResult = useTreeDetails({ - ...useTreeDetailsArgs, - variant: 'builds', - enabled: - (!!summaryResult.data && currentPageTab === 'global.builds') || fetchAll, - }); - - const bootsResult = useTreeDetails({ - ...useTreeDetailsArgs, - variant: 'boots', - enabled: - (!!summaryResult.data && currentPageTab === 'global.boots') || fetchAll, - }); - - const testsResult = useTreeDetails({ + const fullResult = useTreeDetails({ ...useTreeDetailsArgs, - variant: 'tests', - enabled: - (!!summaryResult.data && currentPageTab === 'global.tests') || fetchAll, + variant: 'full', + enabled: !!summaryResult.data, }); - const isAllReady = - !!summaryResult.data && - !!buildsResult.data && - !!bootsResult.data && - !!testsResult.data; - - const isAnyLoading = - summaryResult.isLoading || - buildsResult.isLoading || - bootsResult.isLoading || - testsResult.isLoading; - - useEffect(() => { - if ( - canWeFetchAll(!!summaryResult.data, currentPageTab, { - 'global.builds': !!buildsResult.data, - 'global.boots': !!bootsResult.data, - 'global.tests': !!testsResult.data, - }) - ) { - setFetchAll(true); - } - }, [ - bootsResult.data, - bootsResult.isLoading, - buildsResult.data, - buildsResult.isLoading, - currentPageTab, - fetchAll, - setFetchAll, - summaryResult.data, - summaryResult.isLoading, - testsResult.data, - testsResult.isLoading, - ]); - return { summary: { data: summaryResult.data, @@ -135,24 +49,14 @@ export const useTreeDetailsLazyLoadQuery = ( isPlaceholderData: summaryResult.isPlaceholderData, error: summaryResult.error, }, - builds: { - data: buildsResult.data, - isLoading: buildsResult.isLoading, - status: buildsResult.status, - }, - boots: { - data: bootsResult.data, - isLoading: bootsResult.isLoading, - status: bootsResult.status, - }, - tests: { - data: testsResult.data, - isLoading: testsResult.isLoading, - status: testsResult.status, + full: { + data: fullResult.data, + isLoading: fullResult.isLoading, + status: fullResult.status, }, common: { - isAllReady, - isAnyLoading, + isAllReady: !!summaryResult && !!fullResult, + isAnyLoading: summaryResult.isLoading || fullResult.isLoading, }, }; }; diff --git a/dashboard/src/pages/TreeDetails/Tabs/Boots/BootsTab.tsx b/dashboard/src/pages/TreeDetails/Tabs/Boots/BootsTab.tsx index aa9a35f0..2b80e2ac 100644 --- a/dashboard/src/pages/TreeDetails/Tabs/Boots/BootsTab.tsx +++ b/dashboard/src/pages/TreeDetails/Tabs/Boots/BootsTab.tsx @@ -82,7 +82,9 @@ const BootsTab = ({ treeDetailsLazyLoaded }: BootsTabProps): JSX.Element => { ); const { isLoading, data, error } = treeDetailsLazyLoaded.summary; - const { data: bootsData, status: bootsStatus } = treeDetailsLazyLoaded.boots; + const { data: fullData, status: fullStatus } = treeDetailsLazyLoaded.full; + + const bootsData = fullData?.boots; const getRowLink = useCallback( (bootId: string): LinkProps => ({ @@ -123,7 +125,7 @@ const BootsTab = ({ treeDetailsLazyLoaded }: BootsTabProps): JSX.Element => { if (!data) return
; - if (bootsData?.boots?.length === 0) { + if (bootsData?.length === 0) { return ( } @@ -210,12 +212,12 @@ const BootsTab = ({ treeDetailsLazyLoaded }: BootsTabProps): JSX.Element => {
- + { const { treeId } = useParams({ from: '/tree/$treeId' }); - const { builds: buildsQuery, summary: summaryQuery } = treeDetailsLazyLoaded; - - const summaryData = summaryQuery.data?.summary.builds; - const { data: buildsData, status: buildsStatus } = buildsQuery; + const summaryData = treeDetailsLazyLoaded.summary?.data?.summary.builds; + const { data: fullData, status: fullStatus } = treeDetailsLazyLoaded.full; + const buildsData = fullData?.builds; const toggleFilterBySection = useCallback( (filterSectionKey: string, filterSection: TFilterObjectsKeys): void => { @@ -106,10 +105,10 @@ const BuildTab = ({ treeDetailsLazyLoaded }: BuildTab): JSX.Element => { buildsSummary: sanitizeBuildsSummary(summaryData?.status), buildsIssues: summaryData?.issues || [], failedBuildsWithUnknownIssues: summaryData?.unknown_issues, - builds: sanitizeBuilds(buildsData?.builds), + builds: sanitizeBuilds(buildsData), }), [ - buildsData?.builds, + buildsData, summaryData?.architectures, summaryData?.configs, summaryData?.issues, @@ -182,7 +181,7 @@ const BuildTab = ({ treeDetailsLazyLoaded }: BuildTab): JSX.Element => { /> - +
diff --git a/dashboard/src/pages/TreeDetails/Tabs/Tests/TestsTab.tsx b/dashboard/src/pages/TreeDetails/Tabs/Tests/TestsTab.tsx index 72bf35f7..d5799408 100644 --- a/dashboard/src/pages/TreeDetails/Tabs/Tests/TestsTab.tsx +++ b/dashboard/src/pages/TreeDetails/Tabs/Tests/TestsTab.tsx @@ -41,8 +41,8 @@ interface TestsTabProps { const TestsTab = ({ treeDetailsLazyLoaded }: TestsTabProps): JSX.Element => { const { treeId } = useParams({ from: '/tree/$treeId' }); - const { tests: testsQuery, summary: summaryQuery } = treeDetailsLazyLoaded; - const { data, status, isLoading: testsIsLoading } = testsQuery; + const { full: fullQuery, summary: summaryQuery } = treeDetailsLazyLoaded; + const { data, status, isLoading: fullIsLoading } = fullQuery; const { isLoading: isSummaryLoading, error: summaryError } = summaryQuery; const summaryData = treeDetailsLazyLoaded.summary.data?.summary.tests; @@ -125,7 +125,7 @@ const TestsTab = ({ treeDetailsLazyLoaded }: TestsTabProps): JSX.Element => { if (!summaryData) return
; - if (!testsIsLoading && data?.tests.length === 0) { + if (!fullIsLoading && data?.tests.length === 0) { return ( } diff --git a/dashboard/src/types/tree/TreeDetails.tsx b/dashboard/src/types/tree/TreeDetails.tsx index d10b250b..e2535e4b 100644 --- a/dashboard/src/types/tree/TreeDetails.tsx +++ b/dashboard/src/types/tree/TreeDetails.tsx @@ -113,7 +113,7 @@ type TreeFilters = { tests: TreeLocalFilters; }; -export type TTreeTestsFullData = { +export type TreeDetailsFullData = { builds: BuildsTabBuild[]; boots: TestHistory[]; tests: TestHistory[];