diff --git a/backend/kernelCI_app/constants/general.py b/backend/kernelCI_app/constants/general.py index 6063d7067..aa471d650 100644 --- a/backend/kernelCI_app/constants/general.py +++ b/backend/kernelCI_app/constants/general.py @@ -1,3 +1,4 @@ DEFAULT_ORIGIN = 'maestro' UNKNOWN_STRING = 'Unknown' +UNCATEGORIZED_STRING = "Uncategorized" diff --git a/backend/kernelCI_app/helpers/commonDetails.py b/backend/kernelCI_app/helpers/commonDetails.py index 80f9b65af..a52bac421 100644 --- a/backend/kernelCI_app/helpers/commonDetails.py +++ b/backend/kernelCI_app/helpers/commonDetails.py @@ -1,5 +1,6 @@ -from kernelCI_app.constants.general import UNKNOWN_STRING -from typing import Set +from typing import Dict, Literal, Set + +type PossibleTabs = Literal["build", "boot", "test"] def add_unfiltered_issue( @@ -8,9 +9,11 @@ def add_unfiltered_issue( issue_version: int, should_increment: bool, issue_set: Set, - is_invalid: bool + is_invalid: bool, + unknown_issue_flag_dict: Dict[PossibleTabs, bool], + unknown_issue_flag_tab: PossibleTabs, ) -> None: if issue_id is not None and issue_version is not None and should_increment: issue_set.add((issue_id, issue_version)) elif is_invalid is True: - issue_set.add((UNKNOWN_STRING, None)) + unknown_issue_flag_dict[unknown_issue_flag_tab] = True diff --git a/backend/kernelCI_app/helpers/filters.py b/backend/kernelCI_app/helpers/filters.py index ac751eeb7..c0c5e58eb 100644 --- a/backend/kernelCI_app/helpers/filters.py +++ b/backend/kernelCI_app/helpers/filters.py @@ -1,6 +1,8 @@ -from typing import Optional, Dict, List, Tuple, TypedDict, Literal, Any, Union +from typing import Optional, Dict, List, Set, Tuple, TypedDict, Literal, Any, Union from django.http import HttpResponseBadRequest import re +from kernelCI_app.constants.general import UNCATEGORIZED_STRING +from kernelCI_app.helpers.commonDetails import PossibleTabs from kernelCI_app.typeModels.databases import PASS_STATUS, failure_status_list from kernelCI_app.utils import getErrorResponseBody from kernelCI_app.constants.general import UNKNOWN_STRING @@ -83,9 +85,8 @@ def verify_issue_in_filter( issue_version: Optional[int], ) -> bool: is_unknown_issue = False - if issue_filter_data == UNKNOWN_STRING: - filter_issue_id = UNKNOWN_STRING - filter_issue_version = None + if issue_filter_data == UNCATEGORIZED_STRING: + filter_issue_id = UNCATEGORIZED_STRING is_unknown_issue = True else: filter_issue_id, filter_issue_version = issue_filter_data @@ -123,7 +124,7 @@ def should_filter_test_issue( if test_status == PASS_STATUS: return True - has_unknown_filter = UNKNOWN_STRING in issue_filters + has_uncategorized_filter = UNCATEGORIZED_STRING in issue_filters is_exclusively_build_issue_result = is_exclusively_build_issue( issue_id=issue_id, @@ -131,10 +132,10 @@ def should_filter_test_issue( incident_test_id=incident_test_id, ) - if is_exclusively_build_issue_result and has_unknown_filter: + if is_exclusively_build_issue_result and has_uncategorized_filter: return False if is_exclusively_build_issue_result: - issue_id = UNKNOWN_STRING + issue_id = UNCATEGORIZED_STRING is_issue_filtered_out_result = is_issue_filtered_out( issue_id=issue_id, issue_version=issue_version, issue_filters=issue_filters @@ -158,7 +159,7 @@ def should_filter_build_issue( if not is_build_invalid(build_valid): return True - has_unknown_filter = UNKNOWN_STRING in issue_filters + has_uncategorized_filter = UNCATEGORIZED_STRING in issue_filters is_exclusively_test_issue_result = is_exclusively_test_issue( issue_id=issue_id, @@ -166,10 +167,10 @@ def should_filter_build_issue( incident_test_id=incident_test_id, ) - if is_exclusively_test_issue_result and has_unknown_filter: + if is_exclusively_test_issue_result and has_uncategorized_filter: return False if is_exclusively_test_issue_result: - issue_id = UNKNOWN_STRING + issue_id = UNCATEGORIZED_STRING is_issue_filtered_out_result = is_issue_filtered_out( issue_id=issue_id, issue_version=issue_version, issue_filters=issue_filters @@ -189,7 +190,7 @@ def should_increment_test_issue( incident_test_id=incident_test_id, ) if is_exclusively_build_issue_result: - return (UNKNOWN_STRING, None, False) + return (UNCATEGORIZED_STRING, None, False) is_issue_from_test_result = is_issue_from_test( issue_id=issue_id, @@ -213,7 +214,7 @@ def should_increment_build_issue( incident_test_id=incident_test_id, ) if is_exclusively_test_issue_result: - return (UNKNOWN_STRING, None, False) + return (UNCATEGORIZED_STRING, None, False) is_issue_from_build_result = is_issue_from_build( issue_id=issue_id, @@ -283,7 +284,11 @@ def __init__(self, data: Dict, process_body=False) -> None: self.filterTestPath = "" self.filterBootPath = "" self.filterBuildValid = set() - self.filterIssues = {"build": set(), "boot": set(), "test": set()} + self.filterIssues: Dict[PossibleTabs, Set[str, Optional[int]]] = { + "build": set(), + "boot": set(), + "test": set(), + } self.filterPlatforms = { "boot": set(), "test": set(), @@ -372,8 +377,8 @@ def _handle_issues(self, current_filter: ParsedFilter) -> None: tab = current_filter["field"].split(".")[0] filter_value = current_filter["value"] - if filter_value == UNKNOWN_STRING: - self.filterIssues[tab].add((UNKNOWN_STRING, None)) + if filter_value == UNCATEGORIZED_STRING: + self.filterIssues[tab].add((UNCATEGORIZED_STRING, None)) else: issue_id, issue_version = filter_value.rsplit(",", 1) issue_version = int(issue_version) if issue_version != "null" else None diff --git a/backend/kernelCI_app/helpers/hardwareDetails.py b/backend/kernelCI_app/helpers/hardwareDetails.py index 55ff43bc7..fed3c68c7 100644 --- a/backend/kernelCI_app/helpers/hardwareDetails.py +++ b/backend/kernelCI_app/helpers/hardwareDetails.py @@ -4,12 +4,13 @@ from typing import Dict, List, Literal, Optional, Set from kernelCI_app.cache import getQueryCache, setQueryCache +from kernelCI_app.constants.general import UNCATEGORIZED_STRING from kernelCI_app.constants.hardwareDetails import ( SELECTED_HEAD_TREE_VALUE, ) from kernelCI_app.constants.general import UNKNOWN_STRING from kernelCI_app.helpers.build import build_status_map -from kernelCI_app.helpers.commonDetails import add_unfiltered_issue +from kernelCI_app.helpers.commonDetails import PossibleTabs, add_unfiltered_issue from kernelCI_app.helpers.filters import ( FilterParams, is_test_failure, @@ -892,15 +893,19 @@ def process_filters(*, instance, record: Dict) -> None: should_increment=is_build_issue, issue_set=instance.unfiltered_build_issues, is_invalid=is_invalid, + unknown_issue_flag_dict=instance.unfiltered_uncategorized_issue_flags, + unknown_issue_flag_tab="build", ) if record["id"] is not None: if is_boot(record["path"]): issue_set = instance.unfiltered_boot_issues platform_set = instance.unfiltered_boot_platforms + flag_tab: PossibleTabs = "boot" else: issue_set = instance.unfiltered_test_issues platform_set = instance.unfiltered_test_platforms + flag_tab: PossibleTabs = "test" test_issue_id = record["incidents__issue__id"] test_issue_version = record["incidents__issue__version"] @@ -917,6 +922,8 @@ def process_filters(*, instance, record: Dict) -> None: should_increment=is_test_issue, issue_set=issue_set, is_invalid=is_invalid, + unknown_issue_flag_dict=instance.unfiltered_uncategorized_issue_flags, + unknown_issue_flag_tab=flag_tab, ) environment_misc = handle_environment_misc(record["environment_misc"]) @@ -954,9 +961,9 @@ def assign_default_record_values(record: Dict) -> None: record["build__incidents__issue__id"] is None and record["build__valid"] is not True ): - record["build__incidents__issue__id"] = UNKNOWN_STRING + record["build__incidents__issue__id"] = UNCATEGORIZED_STRING if record["incidents__issue__id"] is None and record["status"] == FAIL_STATUS: - record["incidents__issue__id"] = UNKNOWN_STRING + record["incidents__issue__id"] = UNCATEGORIZED_STRING def format_issue_summary_for_response( diff --git a/backend/kernelCI_app/helpers/treeDetails.py b/backend/kernelCI_app/helpers/treeDetails.py index 2a3179c74..12d1d1a90 100644 --- a/backend/kernelCI_app/helpers/treeDetails.py +++ b/backend/kernelCI_app/helpers/treeDetails.py @@ -1,6 +1,7 @@ from collections.abc import Callable from typing import Any, Optional, TypedDict -from kernelCI_app.helpers.commonDetails import add_unfiltered_issue +from kernelCI_app.constants.general import UNCATEGORIZED_STRING +from kernelCI_app.helpers.commonDetails import PossibleTabs, add_unfiltered_issue from kernelCI_app.helpers.filters import ( is_test_failure, should_increment_build_issue, @@ -218,7 +219,7 @@ def get_current_row_data(current_row: dict) -> dict: or current_row_data["build_valid"] is None or current_row_data["test_status"] == FAIL_STATUS ): - current_row_data["issue_id"] = UNKNOWN_STRING + current_row_data["issue_id"] = UNCATEGORIZED_STRING current_row_data["build_misc"] = handle_build_misc(current_row_data["build_misc"]) if current_row_data["test_path"] is None: current_row_data["test_path"] = UNKNOWN_STRING @@ -545,6 +546,8 @@ def process_filters(instance, row_data: dict) -> None: issue_version=build_issue_version, should_increment=is_build_issue, issue_set=instance.unfiltered_build_issues, + unknown_issue_flag_dict=instance.unfiltered_uncategorized_issue_flags, + unknown_issue_flag_tab="build", is_invalid=is_invalid, ) @@ -557,8 +560,10 @@ def process_filters(instance, row_data: dict) -> None: if is_boot(row_data["test_path"]): issue_set = instance.unfiltered_boot_issues + flag_tab: PossibleTabs = "boot" else: issue_set = instance.unfiltered_test_issues + flag_tab: PossibleTabs = "test" is_invalid = row_data["test_status"] == FAIL_STATUS add_unfiltered_issue( @@ -567,4 +572,6 @@ def process_filters(instance, row_data: dict) -> None: should_increment=is_test_issue, issue_set=issue_set, is_invalid=is_invalid, + unknown_issue_flag_dict=instance.unfiltered_uncategorized_issue_flags, + unknown_issue_flag_tab=flag_tab, ) diff --git a/backend/kernelCI_app/typeModels/commonDetails.py b/backend/kernelCI_app/typeModels/commonDetails.py index 409e2f40b..64dd8433c 100644 --- a/backend/kernelCI_app/typeModels/commonDetails.py +++ b/backend/kernelCI_app/typeModels/commonDetails.py @@ -106,6 +106,7 @@ class GlobalFilters(BaseModel): class LocalFilters(BaseModel): issues: List[Tuple[str, Optional[int]]] + has_unknown_issue: bool class DetailsFilters(BaseModel): diff --git a/backend/kernelCI_app/views/hardwareDetailsSummaryView.py b/backend/kernelCI_app/views/hardwareDetailsSummaryView.py index c6279494e..b66c4ee06 100644 --- a/backend/kernelCI_app/views/hardwareDetailsSummaryView.py +++ b/backend/kernelCI_app/views/hardwareDetailsSummaryView.py @@ -5,6 +5,7 @@ from drf_spectacular.utils import extend_schema from http import HTTPStatus import json +from kernelCI_app.helpers.commonDetails import PossibleTabs from kernelCI_app.helpers.hardwareDetails import ( assign_default_record_values, decide_if_is_build_in_filter, @@ -102,6 +103,11 @@ def __init__(self): self.unfiltered_build_issues = set() self.unfiltered_boot_issues = set() self.unfiltered_test_issues = set() + self.unfiltered_uncategorized_issue_flags: Dict[PossibleTabs, bool] = { + "build": False, + "boot": False, + "test": False, + } self.unfiltered_boot_platforms = set() self.unfiltered_test_platforms = set() @@ -135,7 +141,11 @@ def _process_test(self, record: Dict) -> None: processed_tests=self.processed_tests, ) - if should_process_test and not is_issue_processed_result and is_test_processed_result: + if ( + should_process_test + and not is_issue_processed_result + and is_test_processed_result + ): process_issue( record=record, task_issues_dict=self.issue_dicts[test_type_key], @@ -281,7 +291,9 @@ def post(self, request, hardware_id) -> Response: is_all_selected = len(self.selected_commits) == 0 try: - self._sanitize_records(records, trees_with_selected_commits, is_all_selected) + self._sanitize_records( + records, trees_with_selected_commits, is_all_selected + ) self._format_processing_for_response(hardware_id=hardware_id) @@ -310,14 +322,23 @@ def post(self, request, hardware_id) -> Response: ), builds=LocalFilters( issues=list(self.unfiltered_build_issues), + has_unknown_issue=self.unfiltered_uncategorized_issue_flags[ + "build" + ], ), boots=HardwareTestLocalFilters( issues=list(self.unfiltered_boot_issues), platforms=list(self.unfiltered_boot_platforms), + has_unknown_issue=self.unfiltered_uncategorized_issue_flags[ + "boot" + ], ), tests=HardwareTestLocalFilters( issues=list(self.unfiltered_test_issues), platforms=list(self.unfiltered_test_platforms), + has_unknown_issue=self.unfiltered_uncategorized_issue_flags[ + "test" + ], ), ), common=HardwareCommon( diff --git a/backend/kernelCI_app/views/hardwareDetailsView.py b/backend/kernelCI_app/views/hardwareDetailsView.py index 437c80b1a..e7b99eb31 100644 --- a/backend/kernelCI_app/views/hardwareDetailsView.py +++ b/backend/kernelCI_app/views/hardwareDetailsView.py @@ -5,6 +5,7 @@ from django.views.decorators.csrf import csrf_exempt from drf_spectacular.utils import extend_schema from http import HTTPStatus +from kernelCI_app.helpers.commonDetails import PossibleTabs from kernelCI_app.helpers.filters import FilterParams from kernelCI_app.helpers.hardwareDetails import ( assign_default_record_values, @@ -256,6 +257,12 @@ def post(self, request, hardware_id): self.builds["summary"] = create_details_build_summary(self.builds["items"]) self._format_processing_for_response() + self.unfiltered_uncategorized_issue_flags: Dict[PossibleTabs, bool] = { + "build": False, + "boot": False, + "test": False, + } + get_filter_options( instance=self, records=records, @@ -307,14 +314,25 @@ def post(self, request, hardware_id): architectures=self.global_architectures, compilers=self.global_compilers, ), - builds=LocalFilters(issues=list(self.unfiltered_build_issues)), + builds=LocalFilters( + issues=list(self.unfiltered_build_issues), + has_unknown_issue=self.unfiltered_uncategorized_issue_flags[ + "build" + ], + ), boots=HardwareTestLocalFilters( issues=list(self.unfiltered_boot_issues), platforms=list(self.unfiltered_boot_platforms), + has_unknown_issue=self.unfiltered_uncategorized_issue_flags[ + "boot" + ], ), tests=HardwareTestLocalFilters( issues=list(self.unfiltered_test_issues), platforms=list(self.unfiltered_test_platforms), + has_unknown_issue=self.unfiltered_uncategorized_issue_flags[ + "test" + ], ), ), common=HardwareCommon( diff --git a/backend/kernelCI_app/views/treeDetailsSummaryView.py b/backend/kernelCI_app/views/treeDetailsSummaryView.py index 2f61035ea..ae7a1c470 100644 --- a/backend/kernelCI_app/views/treeDetailsSummaryView.py +++ b/backend/kernelCI_app/views/treeDetailsSummaryView.py @@ -1,5 +1,7 @@ +from typing import Dict from pydantic import ValidationError from rest_framework.response import Response +from kernelCI_app.helpers.commonDetails import PossibleTabs from kernelCI_app.helpers.filters import FilterParams from kernelCI_app.helpers.treeDetails import ( call_based_on_compatible_and_misc_platform, @@ -19,7 +21,19 @@ process_tests_issue, process_filters, ) -from kernelCI_app.typeModels.treeDetails import SummaryResponse, TreeQueryParameters +from kernelCI_app.typeModels.commonDetails import ( + BuildSummary, + DetailsFilters, + GlobalFilters, + LocalFilters, + Summary, + TestSummary, +) +from kernelCI_app.typeModels.treeDetails import ( + SummaryResponse, + TreeCommon, + TreeQueryParameters, +) from kernelCI_app.utils import ( convert_issues_dict_to_list, ) @@ -74,6 +88,11 @@ def __init__(self): self.unfiltered_test_issues = set() self.unfiltered_boot_issues = set() self.unfiltered_build_issues = set() + self.unfiltered_uncategorized_issue_flags: Dict[PossibleTabs, bool] = { + "build": False, + "boot": False, + "test": False, + } def _process_boots_test(self, row_data): test_id = row_data["test_id"] @@ -168,64 +187,71 @@ 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"], - "architectures": self.build_summary["architectures"], - "configs": self.build_summary["configs"], - "issues": self.build_issues, - "unknown_issues": self.failed_builds_with_unknown_issues - }, - "boots": { - "status": self.bootStatusSummary, - "architectures": list(self.bootArchSummary.values()), - "configs": self.bootConfigs, - "issues": self.bootIssues, - "unknown_issues": self.failedBootsWithUnknownIssues, - "environment_compatible": self.bootEnvironmentCompatible, - "environment_misc": self.bootEnvironmentMisc, - "fail_reasons": self.bootFailReasons, - "failed_platforms": list(self.bootPlatformsFailing), - }, - "tests": { - "status": self.testStatusSummary, - "architectures": list(self.test_arch_summary.values()), - "configs": self.test_configs, - "issues": self.testIssues, - "unknown_issues": self.failedTestsWithUnknownIssues, - "environment_compatible": self.testEnvironmentCompatible, - "environment_misc": self.testEnvironmentMisc, - "fail_reasons": self.testFailReasons, - "failed_platforms": list(self.testPlatformsWithErrors), - }, - }, - "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: - SummaryResponse(**response) + valid_response = SummaryResponse( + common=TreeCommon( + tree_url=self.tree_url, + hardware=list(self.hardwareUsed), + git_commit_tags=self.git_commit_tags, + ), + summary=Summary( + builds=BuildSummary( + status=self.build_summary["builds"], + architectures=self.build_summary["architectures"], + configs=self.build_summary["configs"], + issues=self.build_issues, + unknown_issues=self.failed_builds_with_unknown_issues, + ), + boots=TestSummary( + status=self.bootStatusSummary, + architectures=list(self.bootArchSummary.values()), + configs=self.bootConfigs, + issues=self.bootIssues, + unknown_issues=self.failedBootsWithUnknownIssues, + environment_compatible=self.bootEnvironmentCompatible, + environment_misc=self.bootEnvironmentMisc, + fail_reasons=self.bootFailReasons, + failed_platforms=list(self.bootPlatformsFailing), + ), + tests=TestSummary( + status=self.testStatusSummary, + architectures=list(self.test_arch_summary.values()), + configs=self.test_configs, + issues=self.testIssues, + unknown_issues=self.failedTestsWithUnknownIssues, + environment_compatible=self.testEnvironmentCompatible, + environment_misc=self.testEnvironmentMisc, + fail_reasons=self.testFailReasons, + failed_platforms=list(self.testPlatformsWithErrors), + ), + ), + filters=DetailsFilters( + all=GlobalFilters( + configs=list(self.global_configs), + architectures=list(self.global_architectures), + compilers=list(self.global_compilers), + ), + builds=LocalFilters( + issues=list(self.unfiltered_build_issues), + has_unknown_issue=self.unfiltered_uncategorized_issue_flags[ + "build" + ], + ), + boots=LocalFilters( + issues=list(self.unfiltered_boot_issues), + has_unknown_issue=self.unfiltered_uncategorized_issue_flags[ + "boot" + ], + ), + tests=LocalFilters( + issues=list(self.unfiltered_test_issues), + has_unknown_issue=self.unfiltered_uncategorized_issue_flags[ + "test" + ], + ), + ), + ) except ValidationError as e: - return Response(data=e.errors()) + return Response(data=e.errors(), status=HTTPStatus.INTERNAL_SERVER_ERROR) - return Response(response) + return Response(valid_response.model_dump(), status=HTTPStatus.OK) diff --git a/backend/kernelCI_app/views/treeDetailsView.py b/backend/kernelCI_app/views/treeDetailsView.py index 587c3b15d..9a8e951a2 100644 --- a/backend/kernelCI_app/views/treeDetailsView.py +++ b/backend/kernelCI_app/views/treeDetailsView.py @@ -1,7 +1,8 @@ -from typing import List +from typing import Dict, List from http import HTTPStatus from rest_framework.views import APIView from rest_framework.response import Response +from kernelCI_app.helpers.commonDetails import PossibleTabs from kernelCI_app.helpers.filters import ( FilterParams, ) @@ -26,6 +27,14 @@ process_tests_issue, process_filters, ) +from kernelCI_app.typeModels.commonDetails import ( + BuildSummary, + DetailsFilters, + GlobalFilters, + LocalFilters, + Summary, + TestSummary, +) from kernelCI_app.utils import ( convert_issues_dict_to_list, ) @@ -36,8 +45,9 @@ from kernelCI_app.typeModels.issues import Issue, IssueDict from kernelCI_app.typeModels.treeDetails import ( + TreeCommon, TreeDetailsFullResponse, - TreeQueryParameters + TreeQueryParameters, ) @@ -84,6 +94,11 @@ def __init__(self): self.unfiltered_test_issues = set() self.unfiltered_boot_issues = set() self.unfiltered_build_issues = set() + self.unfiltered_uncategorized_issue_flags: Dict[PossibleTabs, bool] = { + "build": False, + "boot": False, + "test": False, + } def _process_boots_test(self, row_data): test_id = row_data["test_id"] @@ -168,7 +183,7 @@ def _sanitize_rows(self, rows): @extend_schema( responses=TreeDetailsFullResponse, parameters=[TreeQueryParameters], - methods=["GET"] + methods=["GET"], ) def get(self, request, commit_hash: str | None): rows = get_tree_details_data(request, commit_hash) @@ -187,58 +202,67 @@ def get(self, request, commit_hash: str | None): builds=self.builds, boots=self.bootHistory, tests=self.testHistory, - summary={ - "builds": { - "status": self.build_summary["builds"], - "architectures": self.build_summary["architectures"], - "configs": self.build_summary["configs"], - "issues": self.build_issues, - "unknown_issues": self.failed_builds_with_unknown_issues, - }, - "boots": { - "status": self.bootStatusSummary, - "architectures": list(self.bootArchSummary.values()), - "configs": self.bootConfigs, - "issues": self.bootIssues, - "unknown_issues": self.failedBootsWithUnknownIssues, - "environment_compatible": self.bootEnvironmentCompatible, - "environment_misc": self.bootEnvironmentMisc, - "fail_reasons": self.bootFailReasons, - "failed_platforms": list(self.bootPlatformsFailing), - }, - "tests": { - "status": self.testStatusSummary, - "architectures": list(self.test_arch_summary.values()), - "configs": self.test_configs, - "issues": self.testIssues, - "unknown_issues": self.failedTestsWithUnknownIssues, - "environment_compatible": self.testEnvironmentCompatible, - "environment_misc": self.testEnvironmentMisc, - "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), - }, - } + summary=Summary( + builds=BuildSummary( + status=self.build_summary["builds"], + architectures=self.build_summary["architectures"], + configs=self.build_summary["configs"], + issues=self.build_issues, + unknown_issues=self.failed_builds_with_unknown_issues, + ), + boots=TestSummary( + status=self.bootStatusSummary, + architectures=list(self.bootArchSummary.values()), + configs=self.bootConfigs, + issues=self.bootIssues, + unknown_issues=self.failedBootsWithUnknownIssues, + environment_compatible=self.bootEnvironmentCompatible, + environment_misc=self.bootEnvironmentMisc, + fail_reasons=self.bootFailReasons, + failed_platforms=list(self.bootPlatformsFailing), + ), + tests=TestSummary( + status=self.testStatusSummary, + architectures=list(self.test_arch_summary.values()), + configs=self.test_configs, + issues=self.testIssues, + unknown_issues=self.failedTestsWithUnknownIssues, + environment_compatible=self.testEnvironmentCompatible, + environment_misc=self.testEnvironmentMisc, + fail_reasons=self.testFailReasons, + failed_platforms=list(self.testPlatformsWithErrors), + ), + ), + common=TreeCommon( + hardware=list(self.hardwareUsed), + tree_url=self.tree_url, + git_commit_tags=self.git_commit_tags, + ), + filters=DetailsFilters( + all=GlobalFilters( + configs=list(self.global_configs), + architectures=list(self.global_architectures), + compilers=list(self.global_compilers), + ), + builds=LocalFilters( + issues=list(self.unfiltered_build_issues), + has_unknown_issue=self.unfiltered_uncategorized_issue_flags[ + "build" + ], + ), + boots=LocalFilters( + issues=list(self.unfiltered_boot_issues), + has_unknown_issue=self.unfiltered_uncategorized_issue_flags[ + "boot" + ], + ), + tests=LocalFilters( + issues=list(self.unfiltered_test_issues), + has_unknown_issue=self.unfiltered_uncategorized_issue_flags[ + "test" + ], + ), + ), ) except ValidationError as e: return create_api_error_response( diff --git a/backend/schema.yml b/backend/schema.yml index 21b5379d0..06a7f15be 100644 --- a/backend/schema.yml +++ b/backend/schema.yml @@ -2029,6 +2029,9 @@ components: type: array title: Issues type: array + has_unknown_issue: + title: Has Unknown Issue + type: boolean platforms: items: type: string @@ -2036,6 +2039,7 @@ components: type: array required: - issues + - has_unknown_issue - platforms title: HardwareTestLocalFilters type: object @@ -2306,8 +2310,12 @@ components: type: array title: Issues type: array + has_unknown_issue: + title: Has Unknown Issue + type: boolean required: - issues + - has_unknown_issue title: LocalFilters type: object LogData: @@ -2765,6 +2773,8 @@ components: $ref: '#/components/schemas/Checkout__GitCommitHash' git_commit_name: $ref: '#/components/schemas/Checkout__GitCommitName' + git_commit_tags: + $ref: '#/components/schemas/Checkout__GitCommitTags' earliest_start_time: format: date-time title: Earliest Start Time @@ -2778,6 +2788,7 @@ components: required: - git_commit_hash - git_commit_name + - git_commit_tags - earliest_start_time - builds - boots diff --git a/dashboard/src/components/Cards/IssuesList.tsx b/dashboard/src/components/Cards/IssuesList.tsx index 24fb80e59..7c28fedc7 100644 --- a/dashboard/src/components/Cards/IssuesList.tsx +++ b/dashboard/src/components/Cards/IssuesList.tsx @@ -21,7 +21,7 @@ import FilterLink from '@/components/Tabs/FilterLink'; import LinkWithIcon from '@/components/LinkWithIcon/LinkWithIcon'; -import { UNKNOWN_STRING } from '@/utils/constants/backend'; +import { UNCATEGORIZED_STRING } from '@/utils/constants/backend'; import { MemoizedMoreDetailsIconLink } from '@/components/Button/MoreDetailsButton'; import { IssueTooltip } from '@/components/Issue/IssueTooltip'; @@ -144,7 +144,8 @@ const IssuesList = ({ unknown={issue.incidents_info.incidentsCount} hasBottomBorder text={ - issue.comment ?? formatMessage({ id: 'global.unknown' }) + issue.comment ?? + formatMessage({ id: 'issue.uncategorized' }) } tooltip={issue.comment} /> @@ -176,7 +177,7 @@ const IssuesList = ({ icon={} /> )} - {issue.id !== UNKNOWN_STRING && ( + {issue.id !== UNCATEGORIZED_STRING && ( @@ -188,12 +189,12 @@ const IssuesList = ({ {failedWithUnknownIssues && ( )} diff --git a/dashboard/src/components/Tabs/Filters.tsx b/dashboard/src/components/Tabs/Filters.tsx index 04575f690..1300f144f 100644 --- a/dashboard/src/components/Tabs/Filters.tsx +++ b/dashboard/src/components/Tabs/Filters.tsx @@ -19,7 +19,7 @@ import type { TFilterNumberKeys, } from '@/types/general'; import { filterFieldMap, zFilterObjectsKeys } from '@/types/general'; -import { UNKNOWN_STRING } from '@/utils/constants/backend'; +import { UNCATEGORIZED_STRING } from '@/utils/constants/backend'; import { version_prefix } from '@/utils/utils'; export const NO_VALID_INDEX = -1; @@ -55,10 +55,10 @@ export const mapFilterToReq = (filter: TFilter): TFilter => { } if (reqField.includes('issue')) { - let issue_id = UNKNOWN_STRING; + let issue_id = UNCATEGORIZED_STRING; let issue_version = null; - if (value !== UNKNOWN_STRING) { + if (value !== UNCATEGORIZED_STRING) { const split_issue_data = value.split(` ${version_prefix}`); issue_version = split_issue_data.pop(); issue_id = split_issue_data.join(` ${version_prefix}`); diff --git a/dashboard/src/locales/messages/index.ts b/dashboard/src/locales/messages/index.ts index d7541fa62..3675296f3 100644 --- a/dashboard/src/locales/messages/index.ts +++ b/dashboard/src/locales/messages/index.ts @@ -199,6 +199,7 @@ export const messages = { 'issue.noIssueFound': 'No issue found.', 'issue.tooltip': 'Issues groups several builds or tests by matching result status and logs.{br}They may also be linked to an external issue tracker or mailing list discussion.', + 'issue.uncategorized': 'Uncategorized', 'issueDetails.buildValid': 'Build Valid', 'issueDetails.comment': 'Comment', 'issueDetails.culpritCode': 'Code', diff --git a/dashboard/src/pages/TreeDetails/TreeDetailsFilter.tsx b/dashboard/src/pages/TreeDetails/TreeDetailsFilter.tsx index bb37d86b9..9a3b06799 100644 --- a/dashboard/src/pages/TreeDetails/TreeDetailsFilter.tsx +++ b/dashboard/src/pages/TreeDetails/TreeDetailsFilter.tsx @@ -16,6 +16,7 @@ import { import { isTFilterObjectKeys, type TFilter } from '@/types/general'; import { cleanFalseFilters } from '@/components/Tabs/tabsUtils'; import { getIssueFilterLabel } from '@/utils/utils'; +import { UNCATEGORIZED_STRING } from '@/utils/constants/backend'; type TFilterValues = Record; @@ -54,18 +55,32 @@ export const createFilter = (data: TreeDetailsSummary | undefined): TFilter => { data.common.hardware.forEach(h => (hardware[h] = false)); - data.filters.builds.issues.forEach( + const buildFilters = data.filters.builds; + buildFilters.issues.forEach( i => (buildIssue[getIssueFilterLabel({ id: i[0], version: i[1] })] = false), ); - data.filters.boots.issues.forEach( + if (buildFilters.has_unknown_issue) { + buildIssue[UNCATEGORIZED_STRING] = false; + } + + const bootFilters = data.filters.boots; + bootFilters.issues.forEach( i => (bootIssue[getIssueFilterLabel({ id: i[0], version: i[1] })] = false), ); - data.filters.tests.issues.forEach( + if (bootFilters.has_unknown_issue) { + bootIssue[UNCATEGORIZED_STRING] = false; + } + + const testFilters = data.filters.tests; + testFilters.issues.forEach( i => (testIssue[getIssueFilterLabel({ id: i[0], version: i[1] })] = false), ); + if (testFilters.has_unknown_issue) { + testIssue[UNCATEGORIZED_STRING] = false; + } } return { diff --git a/dashboard/src/pages/hardwareDetails/HardwareDetailsFilter.tsx b/dashboard/src/pages/hardwareDetails/HardwareDetailsFilter.tsx index d931c5511..60d67fe67 100644 --- a/dashboard/src/pages/hardwareDetails/HardwareDetailsFilter.tsx +++ b/dashboard/src/pages/hardwareDetails/HardwareDetailsFilter.tsx @@ -19,6 +19,7 @@ import { isTFilterObjectKeys, type TFilter } from '@/types/general'; import { cleanFalseFilters } from '@/components/Tabs/tabsUtils'; import type { HardwareDetailsSummary } from '@/types/hardware/hardwareDetails'; import { getIssueFilterLabel } from '@/utils/utils'; +import { UNCATEGORIZED_STRING } from '@/utils/constants/backend'; type TFilterValues = Record; @@ -83,18 +84,34 @@ export const createFilter = ( data.filters.all.configs.forEach(config => { configs[config ?? 'Unknown'] = false; }); - data.filters.builds.issues.forEach( + + const buildFilters = data.filters.builds; + buildFilters.issues.forEach( i => (buildIssue[getIssueFilterLabel({ id: i[0], version: i[1] })] = false), ); - data.filters.boots.issues.forEach( + if (buildFilters.has_unknown_issue) { + buildIssue[UNCATEGORIZED_STRING] = false; + } + + const bootFilters = data.filters.boots; + bootFilters.issues.forEach( i => (bootIssue[getIssueFilterLabel({ id: i[0], version: i[1] })] = false), ); - data.filters.tests.issues.forEach( + if (bootFilters.has_unknown_issue) { + bootIssue[UNCATEGORIZED_STRING] = false; + } + + const testFilters = data.filters.tests; + testFilters.issues.forEach( i => (testIssue[getIssueFilterLabel({ id: i[0], version: i[1] })] = false), ); + if (testFilters.has_unknown_issue) { + testIssue[UNCATEGORIZED_STRING] = false; + } + (data.filters.boots.platforms ?? []).forEach( p => (bootPlatform[p] = false), ); diff --git a/dashboard/src/types/commonDetails.ts b/dashboard/src/types/commonDetails.ts index e39da39d2..aadceefe1 100644 --- a/dashboard/src/types/commonDetails.ts +++ b/dashboard/src/types/commonDetails.ts @@ -44,6 +44,7 @@ export type IssueFilterItem = [string, number?]; export type LocalFilters = { issues: IssueFilterItem[]; + has_unknown_issue: boolean; }; export type DetailsFilters = { diff --git a/dashboard/src/utils/constants/backend.ts b/dashboard/src/utils/constants/backend.ts index 58608bef2..0fc22405a 100644 --- a/dashboard/src/utils/constants/backend.ts +++ b/dashboard/src/utils/constants/backend.ts @@ -1 +1,2 @@ export const UNKNOWN_STRING = 'Unknown'; +export const UNCATEGORIZED_STRING = 'Uncategorized';