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 (
+