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 1bd0b0cb..82363b82 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, @@ -28,14 +28,33 @@ class TestSummary(BaseModel): failed_platforms: List[str] -class TreeSummary(Summary): - hardware: Optional[Set[str]] +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 20cca1a1..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,20 +965,13 @@ 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: type: string type: array - uniqueItems: true - type: 'null' title: Hardware tree_url: @@ -974,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/api/treeDetails.ts b/dashboard/src/api/treeDetails.ts index 6d0c352c..4a7fc37b 100644 --- a/dashboard/src/api/treeDetails.ts +++ b/dashboard/src/api/treeDetails.ts @@ -6,8 +6,12 @@ import { useSearch } from '@tanstack/react-router'; import type { TTreeDetailsFilter, BuildCountsResponse, - TTreeTestsFullData, + TreeDetailsFullData, 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: TreeDetailsFullData; + 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/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/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..49f86e63 --- /dev/null +++ b/dashboard/src/hooks/useTreeDetailsLazyLoadQuery.ts @@ -0,0 +1,62 @@ +import type { UseQueryResult } from '@tanstack/react-query'; + +import type { UseTreeDetailsWithoutVariant } from '@/api/treeDetails'; +import { useTreeDetails } from '@/api/treeDetails'; +import type { + TreeDetailsFullData, + TreeDetailsSummary, +} from '@/types/tree/TreeDetails'; +import type { QuerySelectorStatus } from '@/components/QuerySwitcher/QuerySwitcher'; + +export type TreeDetailsLazyLoaded = { + summary: { + data?: TreeDetailsSummary; + isLoading: boolean; + status: QuerySelectorStatus; + error: UseQueryResult['error']; + isPlaceholderData: boolean; + }; + full: { + data?: TreeDetailsFullData; + isLoading: boolean; + status: QuerySelectorStatus; + }; + common: { + isAllReady: boolean; + isAnyLoading: boolean; + }; +}; + +export const useTreeDetailsLazyLoadQuery = ( + useTreeDetailsArgs: UseTreeDetailsWithoutVariant, +): TreeDetailsLazyLoaded => { + const summaryResult = useTreeDetails({ + ...useTreeDetailsArgs, + variant: 'summary', + }); + + const fullResult = useTreeDetails({ + ...useTreeDetailsArgs, + variant: 'full', + enabled: !!summaryResult.data, + }); + + return { + summary: { + data: summaryResult.data, + isLoading: summaryResult.isLoading, + status: summaryResult.status, + isPlaceholderData: summaryResult.isPlaceholderData, + error: summaryResult.error, + }, + full: { + data: fullResult.data, + isLoading: fullResult.isLoading, + status: fullResult.status, + }, + common: { + 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 aa0c8246..2b80e2ac 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,10 @@ const BootsTab = ({ reqFilter }: BootsTabProps): JSX.Element => { [navigate], ); - const { isLoading, data, error } = useTreeDetails({ - treeId: treeId ?? '', - filter: reqFilter, - }); + const { isLoading, data, error } = treeDetailsLazyLoaded.summary; + const { data: fullData, status: fullStatus } = treeDetailsLazyLoaded.full; + + const bootsData = fullData?.boots; const getRowLink = useCallback( (bootId: string): LinkProps => ({ @@ -124,7 +125,7 @@ const BootsTab = ({ reqFilter }: BootsTabProps): JSX.Element => { if (!data) return
; - if (data.boots.length < 1) { + if (bootsData?.length === 0) { return ( } @@ -211,15 +212,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..7cecd1ad 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,10 @@ const BuildTab = ({ treeDetailsData }: BuildTab): JSX.Element => { const { treeId } = useParams({ from: '/tree/$treeId' }); + 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 => { navigate({ @@ -66,6 +98,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), + }), + [ + buildsData, + summaryData?.architectures, + summaryData?.configs, + summaryData?.issues, + summaryData?.status, + summaryData?.unknown_issues, + ], + ); return (
@@ -131,15 +181,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..d5799408 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 { full: fullQuery, summary: summaryQuery } = treeDetailsLazyLoaded; + const { data, status, isLoading: fullIsLoading } = fullQuery; + 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 (!fullIsLoading && 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..37bd0bfc 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,14 @@ import { MemoizedSectionError } from '@/components/DetailsPages/SectionError'; 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'; -export interface ITreeDetails { - architectures: ISummaryItem[]; - configs: IListingItem[]; - buildsSummary: BuildStatus; - builds: AccordionItemBuilds[]; - buildsIssues: TIssue[]; - failedBuildsWithUnknownIssues?: number; -} - interface ITreeHeader { treeNames?: string; gitUrl?: string; @@ -145,11 +127,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 +170,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 +218,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?.common.hardware} + diffFilter={diffFilter} />
- -
- } - hardwareUsed={data?.summary.hardware} - diffFilter={diffFilter} - /> -
-
- {data?.summary.tree_url && ( -
-
+
+
+ {data ? ( -
+ ) : ( + + )}
- )} +
diff --git a/dashboard/src/pages/TreeDetails/TreeDetailsFilter.tsx b/dashboard/src/pages/TreeDetails/TreeDetailsFilter.tsx index b055d3bb..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.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)); - 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,13 +131,8 @@ const sectionTrees: ISectionItem[] = [ const TreeDetailsFilter = ({ paramFilter, treeUrl, + data, }: ITreeDetailsFilter): JSX.Element => { - const { treeId } = useParams({ from: '/tree/$treeId' }); - - const { data, isLoading } = useTreeDetails({ - treeId, - }); - const navigate = useNavigate({ from: '/tree/$treeId', }); @@ -191,25 +182,19 @@ const TreeDetailsFilter = ({ onOpenChange={handleOpenChange} onCancel={onClickCancel} > - {isLoading ? ( - - - - ) : ( - <> - - - - )} + <> + + + ); }; diff --git a/dashboard/src/types/tree/TreeDetails.tsx b/dashboard/src/types/tree/TreeDetails.tsx index d5cec039..e2535e4b 100644 --- a/dashboard/src/types/tree/TreeDetails.tsx +++ b/dashboard/src/types/tree/TreeDetails.tsx @@ -88,16 +88,56 @@ type TreeSummary = { boots: TestSummary; builds: BuildSummary; tests: TestSummary; +}; + +type TreeCommon = { hardware: string[]; tree_url: string; git_commit_tags: string[]; }; -export type TTreeTestsFullData = { +type TreeGlobalFilters = { + configs: string[]; + architectures: string[]; + compilers: string[]; +}; + +type TreeLocalFilters = { + issues: string[]; +}; + +type TreeFilters = { + all: TreeGlobalFilters; + builds: TreeLocalFilters; + boots: TreeLocalFilters; + tests: TreeLocalFilters; +}; + +export type TreeDetailsFullData = { builds: BuildsTabBuild[]; boots: TestHistory[]; tests: TestHistory[]; summary: TreeSummary; + common: TreeCommon; + filters: TreeFilters; +}; + +export type TreeDetailsSummary = { + summary: TreeSummary; + common: TreeCommon; + filters: TreeFilters; +}; + +export type TreeDetailsBuilds = { + builds: BuildsTabBuild[]; +}; + +export type TreeDetailsTests = { + tests: TestHistory[]; +}; + +export type TreeDetailsBoots = { + boots: TestHistory[]; }; export const possibleTabs = [ @@ -135,6 +175,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);