diff --git a/backend/kernelCI_app/helpers/filters.py b/backend/kernelCI_app/helpers/filters.py index e4f2e82c..7de461ac 100644 --- a/backend/kernelCI_app/helpers/filters.py +++ b/backend/kernelCI_app/helpers/filters.py @@ -1,9 +1,7 @@ -from typing import Optional, Dict, List +from typing import Optional, Dict, List, TypedDict, Literal, Any from django.http import HttpResponseBadRequest import re -from kernelCI_app.utils import ( - getErrorResponseBody -) +from kernelCI_app.utils import getErrorResponseBody UNKNOWN_STRING = "Unknown" NULL_STRINGS = set(["null", UNKNOWN_STRING, "NULL"]) @@ -96,7 +94,12 @@ class FilterParams: string_like_filters = ["boot.path", "test.path"] - def __init__(self, data: Dict, process_body=False): + class ParsedFilter(TypedDict): + field: str + value: Any # TODO: correctly type this field + comparison_op: Literal["exact", "in", "gt", "gte", "lt", "lte", "like"] + + def __init__(self, data: Dict, process_body=False) -> None: self.filterTestDurationMin, self.filterTestDurationMax = None, None self.filterBootDurationMin, self.filterBootDurationMax = None, None self.filterBuildDurationMin, self.filterBuildDurationMax = None, None @@ -109,10 +112,11 @@ def __init__(self, data: Dict, process_body=False): self.filterTestPath = "" self.filterBootPath = "" self.filterBuildValid = set() - self.filterIssues = { + self.filterIssues = {"build": set(), "boot": set(), "test": set()} + self.filterPlatforms = { "build": set(), "boot": set(), - "test": set() + "test": set(), } self.filter_handlers = { @@ -131,9 +135,12 @@ def __init__(self, data: Dict, process_body=False): "build.issue": self._handle_issues, "boot.issue": self._handle_issues, "test.issue": self._handle_issues, + "build.platform": self._handle_platforms, + "boot.platform": self._handle_platforms, + "test.platform": self._handle_platforms, } - self.filters = [] + self.filters: List[FilterParams.ParsedFilter] = [] if process_body: self.create_filters_from_body(data) else: @@ -141,10 +148,10 @@ def __init__(self, data: Dict, process_body=False): self._processFilters() - def _handle_boot_status(self, current_filter: Dict): + def _handle_boot_status(self, current_filter: ParsedFilter) -> None: self.filterBootStatus.add(current_filter["value"]) - def _handle_boot_duration(self, current_filter: Dict): + def _handle_boot_duration(self, current_filter: ParsedFilter) -> None: value = current_filter["value"] operation = current_filter["comparison_op"] if operation == "lte": @@ -152,10 +159,10 @@ def _handle_boot_duration(self, current_filter: Dict): else: self.filterBootDurationMin = toIntOrDefault(value, None) - def _handle_test_status(self, current_filter: Dict): + def _handle_test_status(self, current_filter: ParsedFilter) -> None: self.filterTestStatus.add(current_filter["value"]) - def _handle_test_duration(self, current_filter: Dict): + def _handle_test_duration(self, current_filter: ParsedFilter) -> None: value = current_filter["value"] operation = current_filter["comparison_op"] if operation == "lte": @@ -163,28 +170,28 @@ def _handle_test_duration(self, current_filter: Dict): else: self.filterTestDurationMin = toIntOrDefault(value, None) - def _handle_config_name(self, current_filter: Dict): + def _handle_config_name(self, current_filter: ParsedFilter) -> None: self.filterConfigs.add(current_filter["value"]) - def _handle_compiler(self, current_filter: Dict): + def _handle_compiler(self, current_filter: ParsedFilter) -> None: self.filterCompiler.add(current_filter["value"]) - def _handle_architecture(self, current_filter: Dict): + def _handle_architecture(self, current_filter: ParsedFilter) -> None: self.filterArchitecture.add(current_filter["value"]) - def _handle_hardware(self, current_filter: Dict): + def _handle_hardware(self, current_filter: ParsedFilter) -> None: self.filterHardware.add(current_filter["value"]) - def _handle_path(self, current_filter: Dict): + def _handle_path(self, current_filter: ParsedFilter) -> None: if current_filter["field"] == "boot.path": self.filterBootPath = current_filter["value"] else: self.filterTestPath = current_filter["value"] - def _handle_build_valid(self, current_filter: Dict): + def _handle_build_valid(self, current_filter: ParsedFilter) -> None: self.filterBuildValid.add(current_filter["value"]) - def _handle_build_duration(self, current_filter: Dict): + def _handle_build_duration(self, current_filter: ParsedFilter) -> None: value = current_filter["value"][0] operation = current_filter["comparison_op"] if operation == "lte": @@ -192,10 +199,14 @@ def _handle_build_duration(self, current_filter: Dict): else: self.filterBuildDurationMin = toIntOrDefault(value, None) - def _handle_issues(self, current_filter): - tab = current_filter["field"].split('.')[0] + def _handle_issues(self, current_filter: ParsedFilter) -> None: + tab = current_filter["field"].split(".")[0] self.filterIssues[tab].add(current_filter["value"]) + def _handle_platforms(self, current_filter: ParsedFilter) -> None: + tab = current_filter["field"].split(".")[0] + self.filterPlatforms[tab].add(current_filter["value"]) + def _processFilters(self): try: for current_filter in self.filters: @@ -270,7 +281,7 @@ def create_filters_from_req(self, request): self.add_filter(filter_term, request.GET.get(k), "exact") - def add_filter(self, field, value, comparison_op): + def add_filter(self, field: str, value: Any, comparison_op: str) -> None: self.validate_comparison_op(comparison_op) self.filters.append( {"field": field, "value": value, "comparison_op": comparison_op} @@ -291,50 +302,64 @@ def get_grouped_filters(self): for f in self.filters: field = f["field"] - value = f['value'] + value = f["value"] if field not in grouped_filters: grouped_filters[field] = f - elif type(grouped_filters[field]['value']) is str: - grouped_filters[field]['value'] = [grouped_filters[field]['value'], value] + elif type(grouped_filters[field]["value"]) is str: + grouped_filters[field]["value"] = [ + grouped_filters[field]["value"], + value, + ] else: - grouped_filters[field]['value'].append(value) + grouped_filters[field]["value"].append(value) return grouped_filters def is_build_filtered_out( - self, *, - duration: Optional[int], - valid: Optional[bool], - issue_id: Optional[str] + self, + *, + duration: Optional[int], + valid: Optional[bool], + issue_id: Optional[str], + platform: Optional[str] = None, ) -> bool: return ( - len(self.filterBuildValid) > 0 - and (str(valid).lower() not in self.filterBuildValid) - ) or ( - (self.filterBuildDurationMax is not None or self.filterBuildDurationMin is not None) - and duration is None - ) or ( - self.filterBuildDurationMax is not None and ( - toIntOrDefault(duration, 0) > self.filterBuildDurationMax + ( + len(self.filterBuildValid) > 0 + and (str(valid).lower() not in self.filterBuildValid) ) - ) or ( - self.filterBuildDurationMin is not None and ( - toIntOrDefault(duration, 0) < self.filterBuildDurationMin + or ( + ( + self.filterBuildDurationMax is not None + or self.filterBuildDurationMin is not None + ) + and duration is None + ) + or ( + self.filterBuildDurationMax is not None + and (toIntOrDefault(duration, 0) > self.filterBuildDurationMax) + ) + or ( + self.filterBuildDurationMin is not None + and (toIntOrDefault(duration, 0) < self.filterBuildDurationMin) + ) + or ( + len(self.filterIssues["build"]) > 0 + and (issue_id not in self.filterIssues["build"] or valid is True) ) - ) or ( - len(self.filterIssues["build"]) > 0 - and ( - issue_id not in self.filterIssues["build"] - or valid is True + or ( + len(self.filterPlatforms["build"]) > 0 + and (platform not in self.filterPlatforms["build"]) ) ) def is_record_filtered_out( - self, *, - hardwares: Optional[List[str]] = None, - architecture: Optional[str], - compiler: Optional[str], - config_name: Optional[str] + self, + *, + hardwares: Optional[List[str]] = None, + architecture: Optional[str], + compiler: Optional[str], + config_name: Optional[str], ) -> bool: hardware_compatibles = [UNKNOWN_STRING] record_architecture = UNKNOWN_STRING @@ -373,21 +398,19 @@ def is_record_filtered_out( return False def is_boot_filtered_out( - self, *, - path: Optional[str], - status: Optional[str], - duration: Optional[int], - issue_id: Optional[str] = None, - incident_test_id: Optional[str] = "incident_test_id", + self, + *, + path: Optional[str], + status: Optional[str], + duration: Optional[int], + issue_id: Optional[str] = None, + incident_test_id: Optional[str] = "incident_test_id", + platform: Optional[str] = None, ) -> bool: if ( - ( - self.filterBootPath != "" - and (self.filterBootPath not in path) - ) + (self.filterBootPath != "" and (self.filterBootPath not in path)) or ( - len(self.filterBootStatus) > 0 - and (status not in self.filterBootStatus) + len(self.filterBootStatus) > 0 and (status not in self.filterBootStatus) ) or ( ( @@ -398,20 +421,18 @@ def is_boot_filtered_out( ) or ( self.filterBootDurationMax is not None - and ( - toIntOrDefault(duration, 0) > self.filterBootDurationMax - ) + and (toIntOrDefault(duration, 0) > self.filterBootDurationMax) ) or ( self.filterBootDurationMin is not None - and ( - toIntOrDefault(duration, 0) < self.filterBootDurationMin - ) + and (toIntOrDefault(duration, 0) < self.filterBootDurationMin) ) or should_filter_test_issue( - self.filterIssues["boot"], - issue_id, - incident_test_id + self.filterIssues["boot"], issue_id, incident_test_id + ) + or ( + len(self.filterPlatforms["boot"]) > 0 + and (platform not in self.filterPlatforms["boot"]) ) ): return True @@ -419,21 +440,19 @@ def is_boot_filtered_out( return False def is_test_filtered_out( - self, *, - path: Optional[str], - status: Optional[str], - duration: Optional[int], - issue_id: Optional[str] = None, - incident_test_id: Optional[str] = "incident_test_id", + self, + *, + path: Optional[str], + status: Optional[str], + duration: Optional[int], + issue_id: Optional[str] = None, + incident_test_id: Optional[str] = "incident_test_id", + platform: Optional[str] = None, ) -> bool: if ( - ( - self.filterTestPath != "" - and (self.filterTestPath not in path) - ) + (self.filterTestPath != "" and (self.filterTestPath not in path)) or ( - len(self.filterTestStatus) > 0 - and (status not in self.filterTestStatus) + len(self.filterTestStatus) > 0 and (status not in self.filterTestStatus) ) or ( ( @@ -444,20 +463,18 @@ def is_test_filtered_out( ) or ( self.filterTestDurationMax is not None - and ( - toIntOrDefault(duration, 0) > self.filterTestDurationMax - ) + and (toIntOrDefault(duration, 0) > self.filterTestDurationMax) ) or ( self.filterTestDurationMin is not None - and ( - toIntOrDefault(duration, 0) < self.filterTestDurationMin - ) + and (toIntOrDefault(duration, 0) < self.filterTestDurationMin) ) or should_filter_test_issue( - self.filterIssues["test"], - issue_id, - incident_test_id + self.filterIssues["test"], issue_id, incident_test_id + ) + or ( + len(self.filterPlatforms["test"]) > 0 + and (platform not in self.filterPlatforms["test"]) ) ): return True diff --git a/backend/kernelCI_app/viewCommon.py b/backend/kernelCI_app/viewCommon.py index a7b969b0..e793b0c3 100644 --- a/backend/kernelCI_app/viewCommon.py +++ b/backend/kernelCI_app/viewCommon.py @@ -1,5 +1,6 @@ from typing import TypedDict from kernelCI_app.utils import create_issue +from kernelCI_app.helpers.build import build_status_map class BuildDict(TypedDict): @@ -14,14 +15,12 @@ def create_default_build_status(): def create_details_build_summary(builds: list[BuildDict]): - status_map = {True: "valid", False: "invalid", None: "null"} - build_summ = create_default_build_status() config_summ = {} arch_summ = {} for build in builds: - status_key = status_map[build["valid"]] + status_key = build_status_map[build["valid"]] build_summ[status_key] += 1 if config := build["config_name"]: diff --git a/backend/kernelCI_app/views/hardwareDetailsView.py b/backend/kernelCI_app/views/hardwareDetailsView.py index 7a3879d5..d11d81a5 100644 --- a/backend/kernelCI_app/views/hardwareDetailsView.py +++ b/backend/kernelCI_app/views/hardwareDetailsView.py @@ -17,10 +17,13 @@ from kernelCI_app.helpers.trees import get_tree_heads from kernelCI_app.helpers.filters import UNKNOWN_STRING, FilterParams from kernelCI_app.helpers.misc import ( + handle_build_misc, handle_environment_misc, - env_misc_value_or_default + build_misc_value_or_default, + env_misc_value_or_default, ) from kernelCI_app.typeModels.hardwareDetails import PostBody, DefaultRecordValues +from kernelCI_app.helpers.build import build_status_map from pydantic import ValidationError DEFAULT_DAYS_INTERVAL = 3 @@ -117,6 +120,7 @@ def generate_test_dict(): return { "history": [], "archSummary": {}, + "platforms": defaultdict(lambda: defaultdict(int)), "platformsFailing": set(), "statusSummary": defaultdict(int), "failReasons": defaultdict(int), @@ -221,9 +225,15 @@ def is_build_filtered_in( build: Dict, processed_builds: Set[str], ) -> bool: - is_build_not_processed = not build["id"] in processed_builds + platform = build_misc_value_or_default(handle_build_misc(build["misc"])).get( + "platform" + ) + is_build_not_processed = build["id"] not in processed_builds is_build_filtered_out = self.filterParams.is_build_filtered_out( - valid=build["valid"], duration=build["duration"], issue_id=build["issue_id"] + valid=build["valid"], + duration=build["duration"], + issue_id=build["issue_id"], + platform=platform, ) return is_build_not_processed and not is_build_filtered_out @@ -247,6 +257,9 @@ def test_in_filter(self, table_test: Literal["boot", "test"], record: Dict) -> b path = record["path"] issue_id = record["incidents__issue__id"] incidents_test_id = record["incidents__test_id"] + platform = env_misc_value_or_default( + handle_environment_misc(record["environment_misc"]) + ).get("platform") if table_test == "boot": test_filter_pass = not self.filterParams.is_boot_filtered_out( @@ -255,6 +268,7 @@ def test_in_filter(self, table_test: Literal["boot", "test"], record: Dict) -> b path=path, issue_id=issue_id, incident_test_id=incidents_test_id, + platform=platform, ) else: test_filter_pass = not self.filterParams.is_test_filtered_out( @@ -263,6 +277,7 @@ def test_in_filter(self, table_test: Literal["boot", "test"], record: Dict) -> b path=path, issue_id=issue_id, incident_test_id=incidents_test_id, + platform=platform, ) return test_filter_pass @@ -274,11 +289,12 @@ def handle_test(self, record, tests): tests["statusSummary"][status] += 1 tests["configs"][record["build__config_name"]][status] += 1 + environment_misc = handle_environment_misc(record["environment_misc"]) + test_platform = env_misc_value_or_default(environment_misc).get("platform") + tests["platforms"][test_platform][status] += 1 + if status == "ERROR" or status == "FAIL" or status == "MISS": - environment_misc = handle_environment_misc(record["environment_misc"]) - tests["platformsFailing"].add( - env_misc_value_or_default(environment_misc).get("platform") - ) + tests["platformsFailing"].add(test_platform) tests["failReasons"][extract_error_message(record["misc"])] += 1 archKey = f'{record["build__architecture"]}{record["build__compiler"]}' @@ -340,7 +356,12 @@ def sanitize_records(self, records, trees: List, is_all_selected: bool): boots = generate_test_dict() compatibles: Set[str] = set() tree_status_summary = defaultdict(generate_tree_status_summary_dict) - builds = {"items": [], "issues": {}, "failedWithUnknownIssues": 0} + builds = { + "items": [], + "issues": {}, + "platforms": defaultdict(lambda: defaultdict(int)), + "failedWithUnknownIssues": 0, + } for record in records: try: @@ -389,7 +410,7 @@ def sanitize_records(self, records, trees: List, is_all_selected: bool): ) if should_process_test: - processed_tests.add(record['id']) + processed_tests.add(record["id"]) self.handle_test(record, boots if is_record_boot else tests) should_process_build = self.is_build_filtered_in(build, processed_builds) @@ -407,13 +428,24 @@ def sanitize_records(self, records, trees: List, is_all_selected: bool): is_failed_task=record["build__valid"] is not True, issue_from="build", ) + build_platform = build_misc_value_or_default( + handle_build_misc(record["build__misc"]) + ).get("platform") + build_status = build_status_map[record["build__valid"]] + builds["platforms"][build_platform][build_status] += 1 builds["summary"] = create_details_build_summary(builds["items"]) mutate_properties_to_list(builds, ["issues"]) mutate_properties_to_list(tests, ["issues", "platformsFailing", "archSummary"]) mutate_properties_to_list(boots, ["issues", "platformsFailing", "archSummary"]) - return (builds, tests, boots, tree_status_summary, list(compatibles)) + return ( + builds, + tests, + boots, + tree_status_summary, + list(compatibles), + ) def _assign_default_record_values(self, record: Dict) -> None: if record["build__architecture"] is None: @@ -573,13 +605,11 @@ def post(self, request, hardware_id): origin = post_body.origin end_datetime = datetime.fromtimestamp( - int(post_body.endTimestampInSeconds), - timezone.utc + int(post_body.endTimestampInSeconds), timezone.utc ) start_datetime = datetime.fromtimestamp( - int(post_body.startTimestampInSeconds), - timezone.utc + int(post_body.startTimestampInSeconds), timezone.utc ) selected_commits = post_body.selectedCommits @@ -631,9 +661,7 @@ def post(self, request, hardware_id): setQueryCache(self.cache_key_get_full_data, params, records) builds, tests, boots, tree_status_summary, compatibles = self.sanitize_records( - records, - trees_with_selected_commits, - is_all_selected + records, trees_with_selected_commits, is_all_selected ) configs, archs, compilers = self.get_filter_options( @@ -657,5 +685,6 @@ def post(self, request, hardware_id): "compilers": compilers, "trees": trees_with_status_count, "compatibles": compatibles, - }, safe=False + }, + safe=False, ) diff --git a/dashboard/src/components/Cards/PlatformsCard.tsx b/dashboard/src/components/Cards/PlatformsCard.tsx new file mode 100644 index 00000000..78c13c00 --- /dev/null +++ b/dashboard/src/components/Cards/PlatformsCard.tsx @@ -0,0 +1,61 @@ +import { memo, useMemo } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import type { TFilter, TFilterObjectsKeys } from '@/types/general'; + +import { DumbListingContent } from '@/components/ListingContent/ListingContent'; +import ListingItem, { + type IListingItem, +} from '@/components/ListingItem/ListingItem'; +import FilterLink from '@/components/Tabs/FilterLink'; +import { BuildStatus } from '@/components/Status/Status'; + +import BaseCard from './BaseCard'; + +interface IPlatformsCard { + platforms: IListingItem[]; + issueFilterSection: TFilterObjectsKeys; + diffFilter: TFilter; +} + +const PlatformsCard = ({ + platforms, + issueFilterSection, + diffFilter, +}: IPlatformsCard): JSX.Element => { + const content = useMemo(() => { + return ( + + {platforms.map((item, i) => ( + + + } + /> + + ))} + + ); + }, [platforms, diffFilter, issueFilterSection]); + + return ( + } + content={content} + /> + ); +}; + +export const MemoizedPlatformsCard = memo(PlatformsCard); diff --git a/dashboard/src/locales/messages/index.ts b/dashboard/src/locales/messages/index.ts index 6950c250..4a84dc72 100644 --- a/dashboard/src/locales/messages/index.ts +++ b/dashboard/src/locales/messages/index.ts @@ -69,9 +69,11 @@ export const messages = { 'filter.architectureSubtitle': 'Please select one or more Architectures:', 'filter.bootDuration': 'Boot duration', 'filter.bootIssue': 'Boot issue', + 'filter.bootPlatform': 'Boot Platforms', 'filter.bootStatus': 'Boot Status', 'filter.buildDuration': 'Build duration', 'filter.buildIssue': 'Build Issue', + 'filter.buildPlatform': 'Build Platforms', 'filter.buildStatus': 'Build Status', 'filter.compilersSubtitle': 'Please select one or more compilers:', 'filter.configsSubtitle': 'Please select one or more configs:', @@ -87,9 +89,11 @@ export const messages = { 'filter.min': 'Min', 'filter.onlySpecificTab': 'Only affects a specific tab', 'filter.perTabFilter': 'Per tab filters', + 'filter.platformSubtitle': 'Please select one or more platforms:', 'filter.statusSubtitle': 'Please select one or more Status:', 'filter.testDuration': 'Test duration', 'filter.testIssue': 'Test issue', + 'filter.testPlatform': 'Test Platforms', 'filter.testStatus': 'Test Status', 'filter.treeSubtitle': 'Please select one or more Trees:', 'filter.treeURL': 'Tree URL', @@ -177,6 +181,7 @@ export const messages = { 'hardware.path': 'Hardware', 'hardware.searchPlaceholder': 'Search by hardware name', 'hardwareDetails.compatibles': 'Compatibles', + 'hardwareDetails.platforms': 'Platforms', 'hardwareDetails.timeFrame': 'Results from {startDate} and {startTime} to {endDate} {endTime}', 'hardwareDetails.treeBranch': 'Tree / Branch', diff --git a/dashboard/src/pages/hardwareDetails/HardwareDetailsFilter.tsx b/dashboard/src/pages/hardwareDetails/HardwareDetailsFilter.tsx index bea17498..3e2c9848 100644 --- a/dashboard/src/pages/hardwareDetails/HardwareDetailsFilter.tsx +++ b/dashboard/src/pages/hardwareDetails/HardwareDetailsFilter.tsx @@ -47,6 +47,9 @@ export const createFilter = ( const buildIssue: TFilterValues = {}; const bootIssue: TFilterValues = {}; const testIssue: TFilterValues = {}; + const buildPlatform: TFilterValues = {}; + const bootPlatform: TFilterValues = {}; + const testPlatform: TFilterValues = {}; const configs: TFilterValues = {}; const archs: TFilterValues = {}; @@ -81,6 +84,9 @@ export const createFilter = ( data.builds.issues.forEach(i => (buildIssue[i.id] = false)); data.boots.issues.forEach(i => (bootIssue[i.id] = false)); data.tests.issues.forEach(i => (testIssue[i.id] = false)); + Object.keys(data.builds.platforms).forEach(i => (buildPlatform[i] = false)); + Object.keys(data.boots.platforms).forEach(i => (bootPlatform[i] = false)); + Object.keys(data.tests.platforms).forEach(i => (testPlatform[i] = false)); } return { @@ -95,6 +101,9 @@ export const createFilter = ( buildIssue, bootIssue, testIssue, + buildPlatform, + bootPlatform, + testPlatform, }; }; @@ -129,6 +138,21 @@ const sectionHardware: ISectionItem[] = [ subtitle: 'filter.issueSubtitle', sectionKey: 'testIssue', }, + { + title: 'filter.buildPlatform', + subtitle: 'filter.platformSubtitle', + sectionKey: 'buildPlatform', + }, + { + title: 'filter.bootPlatform', + subtitle: 'filter.platformSubtitle', + sectionKey: 'bootPlatform', + }, + { + title: 'filter.testPlatform', + subtitle: 'filter.platformSubtitle', + sectionKey: 'testPlatform', + }, { title: 'global.configs', subtitle: 'filter.configsSubtitle', diff --git a/dashboard/src/pages/hardwareDetails/Tabs/Boots/BootsTab.tsx b/dashboard/src/pages/hardwareDetails/Tabs/Boots/BootsTab.tsx index b3bb0dd5..d3a6dc6b 100644 --- a/dashboard/src/pages/hardwareDetails/Tabs/Boots/BootsTab.tsx +++ b/dashboard/src/pages/hardwareDetails/Tabs/Boots/BootsTab.tsx @@ -1,6 +1,6 @@ import { FormattedMessage } from 'react-intl'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import type { LinkProps } from '@tanstack/react-router'; @@ -26,6 +26,9 @@ import { import MemoizedStatusCard from '@/components/Tabs/Tests/StatusCard'; import MemoizedConfigList from '@/components/Tabs/Tests/ConfigsList'; import MemoizedErrorsSummary from '@/components/Tabs/Tests/ErrorsSummary'; +import { MemoizedPlatformsCard } from '@/components/Cards/PlatformsCard'; + +import { sanitizePlatforms } from '@/utils/utils'; import HardwareCommitNavigationGraph from '@/pages/hardwareDetails/Tabs/HardwareCommitNavigationGraph'; @@ -89,6 +92,11 @@ const BootsTab = ({ boots, hardwareId, trees }: TBootsTab): JSX.Element => { [navigate], ); + const platformItems = useMemo( + () => sanitizePlatforms(boots.platforms), + [boots.platforms], + ); + return (
@@ -97,11 +105,6 @@ const BootsTab = ({ boots, hardwareId, trees }: TBootsTab): JSX.Element => { title={} statusCounts={boots.statusSummary} /> - } - configStatusCounts={boots.configs} - diffFilter={diffFilter} - /> } archCompilerErrors={boots.archSummary} @@ -115,7 +118,22 @@ const BootsTab = ({ boots, hardwareId, trees }: TBootsTab): JSX.Element => { issueFilterSection="bootIssue" />
- +
+ + } + configStatusCounts={boots.configs} + diffFilter={diffFilter} + /> + +
{ configStatusCounts={boots.configs} diffFilter={diffFilter} /> + } archCompilerErrors={boots.archSummary} diff --git a/dashboard/src/pages/hardwareDetails/Tabs/Build/BuildTab.tsx b/dashboard/src/pages/hardwareDetails/Tabs/Build/BuildTab.tsx index 256810c2..bf4c017a 100644 --- a/dashboard/src/pages/hardwareDetails/Tabs/Build/BuildTab.tsx +++ b/dashboard/src/pages/hardwareDetails/Tabs/Build/BuildTab.tsx @@ -5,7 +5,12 @@ import { useCallback, useMemo } from 'react'; import { useNavigate, useSearch } from '@tanstack/react-router'; import type { THardwareDetails } from '@/types/hardware/hardwareDetails'; -import { sanitizeArchs, sanitizeBuilds, sanitizeConfigs } from '@/utils/utils'; +import { + sanitizeArchs, + sanitizeBuilds, + sanitizeConfigs, + sanitizePlatforms, +} from '@/utils/utils'; import MemoizedIssuesList from '@/components/Cards/IssuesList'; @@ -19,6 +24,7 @@ import { import { MemoizedErrorsSummaryBuild } from '@/components/Tabs/Builds/BuildCards'; import { MemoizedConfigsCard } from '@/components/Tabs/Builds/ConfigsCard'; +import { MemoizedPlatformsCard } from '@/components/Cards/PlatformsCard'; import HardwareCommitNavigationGraph from '@/pages/hardwareDetails/Tabs/HardwareCommitNavigationGraph'; import type { TFilterObjectsKeys } from '@/types/general'; @@ -80,6 +86,11 @@ const BuildTab = ({ builds, hardwareId, trees }: TBuildTab): JSX.Element => { [builds.items], ); + const platformItems = useMemo( + () => sanitizePlatforms(builds.platforms), + [builds.platforms], + ); + return (
@@ -111,6 +122,11 @@ const BuildTab = ({ builds, hardwareId, trees }: TBuildTab): JSX.Element => { toggleFilterBySection={toggleFilterBySection} diffFilter={diffFilter} /> +
@@ -130,6 +146,11 @@ const BuildTab = ({ builds, hardwareId, trees }: TBuildTab): JSX.Element => { toggleFilterBySection={toggleFilterBySection} diffFilter={diffFilter} /> + } diff --git a/dashboard/src/pages/hardwareDetails/Tabs/Tests/TestsTab.tsx b/dashboard/src/pages/hardwareDetails/Tabs/Tests/TestsTab.tsx index 1645a03e..0cc8fdfb 100644 --- a/dashboard/src/pages/hardwareDetails/Tabs/Tests/TestsTab.tsx +++ b/dashboard/src/pages/hardwareDetails/Tabs/Tests/TestsTab.tsx @@ -1,6 +1,6 @@ import { FormattedMessage } from 'react-intl'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useNavigate, useSearch } from '@tanstack/react-router'; @@ -23,6 +23,9 @@ import MemoizedStatusCard from '@/components/Tabs/Tests/StatusCard'; import MemoizedConfigList from '@/components/Tabs/Tests/ConfigsList'; import MemoizedErrorsSummary from '@/components/Tabs/Tests/ErrorsSummary'; import HardwareCommitNavigationGraph from '@/pages/hardwareDetails/Tabs/HardwareCommitNavigationGraph'; +import { MemoizedPlatformsCard } from '@/components/Cards/PlatformsCard'; + +import { sanitizePlatforms } from '@/utils/utils'; import HardwareDetailsTestTable from './HardwareDetailsTestsTable'; @@ -74,6 +77,11 @@ const TestsTab = ({ tests, trees, hardwareId }: TTestsTab): JSX.Element => { [navigate], ); + const platformItems = useMemo( + () => sanitizePlatforms(tests.platforms), + [tests.platforms], + ); + return (
@@ -82,11 +90,6 @@ const TestsTab = ({ tests, trees, hardwareId }: TTestsTab): JSX.Element => { title={} statusCounts={tests.statusSummary} /> - } - configStatusCounts={tests.configs} - diffFilter={diffFilter} - /> } archCompilerErrors={tests.archSummary} @@ -100,7 +103,22 @@ const TestsTab = ({ tests, trees, hardwareId }: TTestsTab): JSX.Element => { issueFilterSection="testIssue" />
- +
+ + } + configStatusCounts={tests.configs} + diffFilter={diffFilter} + /> + +
{ configStatusCounts={tests.configs} diffFilter={diffFilter} /> + } archCompilerErrors={tests.archSummary} diff --git a/dashboard/src/types/general.ts b/dashboard/src/types/general.ts index 89330549..06cf0bae 100644 --- a/dashboard/src/types/general.ts +++ b/dashboard/src/types/general.ts @@ -138,6 +138,9 @@ export const zFilterObjectsKeys = z.enum([ 'testStatus', 'hardware', 'trees', + 'buildPlatform', + 'bootPlatform', + 'testPlatform', 'testPath', 'bootPath', 'buildIssue', @@ -178,6 +181,9 @@ export const zDiffFilter = z testDurationMax: zFilterNumberValue, hardware: zFilterBoolValue, trees: zFilterBoolValue, + buildPlatform: zFilterBoolValue, + bootPlatform: zFilterBoolValue, + testPlatform: zFilterBoolValue, buildIssue: zFilterBoolValue, bootIssue: zFilterBoolValue, testIssue: zFilterBoolValue, @@ -230,6 +236,9 @@ const requestFilters = { 'build.issue', 'test.issue', 'boot.issue', + 'build.platform', + 'boot.platform', + 'test.platform', ], } as const; @@ -263,6 +272,9 @@ export const filterFieldMap = { 'build.issue': 'buildIssue', 'boot.issue': 'bootIssue', 'test.issue': 'testIssue', + 'build.platform': 'buildPlatform', + 'boot.platform': 'bootPlatform', + 'test.platform': 'testPlatform', } as const satisfies Record; export const getTargetFilter = ( diff --git a/dashboard/src/types/hardware/hardwareDetails.ts b/dashboard/src/types/hardware/hardwareDetails.ts index 621254ca..7df4e4d5 100644 --- a/dashboard/src/types/hardware/hardwareDetails.ts +++ b/dashboard/src/types/hardware/hardwareDetails.ts @@ -28,12 +28,14 @@ type BuildsData = { items: BuildsTabBuild[]; issues: TIssue[]; summary: BuildSummary; + platforms: Record; failedWithUnknownIssues: number; }; type Tests = { archSummary: ArchCompilerStatus[]; history: TestHistory[]; + platforms: Record; platformsFailing: string[]; statusSummary: StatusCounts; failReasons: Record; diff --git a/dashboard/src/utils/utils.ts b/dashboard/src/utils/utils.ts index 0731eb01..9256be08 100644 --- a/dashboard/src/utils/utils.ts +++ b/dashboard/src/utils/utils.ts @@ -9,11 +9,13 @@ import type { Architecture, BuildsTabBuild, BuildStatus, + StatusCount, } from '@/types/general'; import { sanitizeTableValue } from '@/components/Table/tableUtils'; import type { ISummaryItem } from '@/components/Tabs/Summary'; import { UNKNOWN_STRING } from './constants/backend'; +import { groupStatus } from './status'; export function formatDate(date: Date | string, short?: boolean): string { const options: Intl.DateTimeFormatOptions = { @@ -64,6 +66,53 @@ export const sanitizeConfigs = ( })); }; +const isBuildPlatform = ( + platforms: Record | Record, +): platforms is Record => { + const platform = platforms[Object.keys(platforms)[0]]; + return ( + platform && + ('valid' in platform || 'invalid' in platform || 'null' in platform) + ); +}; + +export const sanitizePlatforms = ( + platforms: + | Record + | Record + | undefined, +): IListingItem[] => { + if (!platforms) return []; + + if (isBuildPlatform(platforms)) { + return Object.entries(platforms).map(([key, value]) => ({ + text: key, + errors: value.invalid, + success: value.valid, + unknown: value.null, + })); + } else { + return Object.entries(platforms).map(([key, value]) => { + const { successCount, failedCount, inconclusiveCount } = groupStatus({ + doneCount: value.DONE, + errorCount: value.ERROR, + failCount: value.FAIL, + missCount: value.MISS, + passCount: value.PASS, + skipCount: value.SKIP, + nullCount: value.NULL, + }); + + return { + text: key, + errors: failedCount, + success: successCount, + unknown: inconclusiveCount, + }; + }); + } +}; + const isBuildError = (build: BuildsTabBuild): number => { return build.valid || build.valid === null ? 0 : 1; };