@allurereport/web-awesome 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist/multi/173.app-8be6acc0a596a2197dbf.js +1 -0
  2. package/dist/multi/174.app-8be6acc0a596a2197dbf.js +1 -0
  3. package/dist/multi/252.app-8be6acc0a596a2197dbf.js +1 -0
  4. package/dist/multi/282.app-8be6acc0a596a2197dbf.js +1 -0
  5. package/dist/multi/29.app-8be6acc0a596a2197dbf.js +1 -0
  6. package/dist/multi/416.app-8be6acc0a596a2197dbf.js +1 -0
  7. package/dist/multi/527.app-8be6acc0a596a2197dbf.js +1 -0
  8. package/dist/multi/600.app-8be6acc0a596a2197dbf.js +1 -0
  9. package/dist/multi/605.app-8be6acc0a596a2197dbf.js +1 -0
  10. package/dist/multi/638.app-8be6acc0a596a2197dbf.js +1 -0
  11. package/dist/multi/672.app-8be6acc0a596a2197dbf.js +1 -0
  12. package/dist/multi/686.app-8be6acc0a596a2197dbf.js +1 -0
  13. package/dist/multi/725.app-8be6acc0a596a2197dbf.js +1 -0
  14. package/dist/multi/741.app-8be6acc0a596a2197dbf.js +1 -0
  15. package/dist/multi/749.app-8be6acc0a596a2197dbf.js +1 -0
  16. package/dist/multi/755.app-8be6acc0a596a2197dbf.js +1 -0
  17. package/dist/multi/894.app-8be6acc0a596a2197dbf.js +1 -0
  18. package/dist/multi/943.app-8be6acc0a596a2197dbf.js +1 -0
  19. package/dist/multi/980.app-8be6acc0a596a2197dbf.js +1 -0
  20. package/dist/multi/app-8be6acc0a596a2197dbf.js +2 -0
  21. package/dist/multi/manifest.json +21 -21
  22. package/dist/multi/{styles-9e390bad7ce54a807a8e.css → styles-0b84e1ef76554ad2db9a.css} +19 -10
  23. package/dist/single/app-8221eb856e47b4ef50d6.js +2 -0
  24. package/dist/single/manifest.json +1 -1
  25. package/package.json +8 -10
  26. package/src/components/BaseLayout/index.tsx +5 -4
  27. package/src/components/Categories/CategoriesTree/index.tsx +14 -0
  28. package/src/components/Categories/CategoriesTree/styles.scss +14 -0
  29. package/src/components/Categories/CategoryHeaderItem/index.tsx +50 -0
  30. package/src/components/Categories/CategoryHeaderItem/styles.scss +32 -0
  31. package/src/components/Categories/CategoryTreeItem/index.tsx +309 -0
  32. package/src/components/Categories/CategoryTreeItem/styles.scss +47 -0
  33. package/src/components/Categories/GroupTreeItem/index.tsx +76 -0
  34. package/src/components/Categories/GroupTreeItem/styles.scss +47 -0
  35. package/src/components/Categories/HistoryTreeItem/index.tsx +71 -0
  36. package/src/components/Categories/HistoryTreeItem/styles.scss +53 -0
  37. package/src/components/Categories/LabelTreeItem/index.tsx +150 -0
  38. package/src/components/Categories/LabelTreeItem/styles.scss +102 -0
  39. package/src/components/Categories/MessageTreeItem/index.tsx +107 -0
  40. package/src/components/Categories/MessageTreeItem/styles.scss +178 -0
  41. package/src/components/Categories/SeverityTreeItem/index.tsx +50 -0
  42. package/src/components/Categories/SeverityTreeItem/styles.scss +12 -0
  43. package/src/components/Categories/sticky.ts +12 -0
  44. package/src/components/Charts/index.tsx +5 -5
  45. package/src/components/Header/index.tsx +4 -2
  46. package/src/components/MainReport/index.tsx +148 -47
  47. package/src/components/Metadata/index.tsx +75 -1
  48. package/src/components/Metadata/styles.scss +10 -0
  49. package/src/components/ReportBody/styles.scss +1 -1
  50. package/src/components/ReportCategories/index.tsx +26 -0
  51. package/src/components/ReportCategories/styles.scss +55 -0
  52. package/src/components/ReportFilters/CategoriesFilter.tsx +41 -0
  53. package/src/components/ReportFilters/index.tsx +12 -1
  54. package/src/components/ReportQualityGateResults/index.tsx +77 -19
  55. package/src/components/ReportQualityGateResults/styles.scss +13 -0
  56. package/src/components/SplitLayout/index.tsx +4 -2
  57. package/src/components/SplitLayout/styles.scss +1 -0
  58. package/src/components/TestResult/TrDescription/index.tsx +60 -10
  59. package/src/components/TestResult/TrDescription/styles.scss +4 -0
  60. package/src/components/TestResult/TrError/TrDiff.tsx +2 -6
  61. package/src/components/TestResult/TrError/styles.scss +4 -0
  62. package/src/components/TestResult/TrInfo/index.tsx +8 -1
  63. package/src/components/TestResult/TrInfo/styles.scss +4 -0
  64. package/src/components/TestResult/TrLinks/index.tsx +4 -4
  65. package/src/components/TestResult/TrOverview.tsx +3 -2
  66. package/src/components/TestResult/TrSeverity/index.tsx +13 -4
  67. package/src/components/TestResult/TrSeverity/styles.scss +1 -0
  68. package/src/components/TestResult/TrTabs/index.tsx +5 -0
  69. package/src/components/Timeline/index.tsx +2 -5
  70. package/src/index.tsx +6 -2
  71. package/src/locales/az.json +108 -79
  72. package/src/locales/de.json +26 -5
  73. package/src/locales/en.json +26 -5
  74. package/src/locales/es.json +26 -5
  75. package/src/locales/fr.json +26 -5
  76. package/src/locales/he.json +26 -5
  77. package/src/locales/hy.json +26 -5
  78. package/src/locales/it.json +26 -5
  79. package/src/locales/ja.json +26 -5
  80. package/src/locales/ka.json +26 -5
  81. package/src/locales/kr.json +26 -5
  82. package/src/locales/nl.json +26 -5
  83. package/src/locales/pl.json +26 -5
  84. package/src/locales/pt.json +26 -5
  85. package/src/locales/ru.json +26 -5
  86. package/src/locales/sv.json +26 -5
  87. package/src/locales/tr.json +26 -5
  88. package/src/locales/{ua.json → uk.json} +26 -5
  89. package/src/locales/zh.json +26 -5
  90. package/src/stores/categories.ts +44 -0
  91. package/src/stores/locale.ts +69 -37
  92. package/src/stores/qualityGate.ts +2 -2
  93. package/src/stores/router.ts +55 -3
  94. package/src/stores/testResult.ts +14 -3
  95. package/src/stores/timeline.ts +5 -2
  96. package/src/stores/treeFilters/actions.ts +10 -1
  97. package/src/stores/treeFilters/constants.ts +1 -0
  98. package/src/stores/treeFilters/model.ts +2 -0
  99. package/src/stores/treeFilters/store.ts +45 -0
  100. package/src/stores/treeFilters/utils.ts +10 -0
  101. package/src/stores/treeSwitcher.ts +9 -0
  102. package/src/utils/ownerAddress.ts +92 -0
  103. package/src/utils/time.ts +16 -2
  104. package/src/utils/treeFilters.ts +16 -9
  105. package/test/utils/ownerAddress.test.ts +89 -0
  106. package/test/utils/treeFilters.test.ts +39 -0
  107. package/types.d.ts +2 -1
  108. package/dist/multi/173.app-79c65c7bff941abcbc51.js +0 -1
  109. package/dist/multi/174.app-79c65c7bff941abcbc51.js +0 -1
  110. package/dist/multi/252.app-79c65c7bff941abcbc51.js +0 -1
  111. package/dist/multi/282.app-79c65c7bff941abcbc51.js +0 -1
  112. package/dist/multi/29.app-79c65c7bff941abcbc51.js +0 -1
  113. package/dist/multi/416.app-79c65c7bff941abcbc51.js +0 -1
  114. package/dist/multi/527.app-79c65c7bff941abcbc51.js +0 -1
  115. package/dist/multi/600.app-79c65c7bff941abcbc51.js +0 -1
  116. package/dist/multi/605.app-79c65c7bff941abcbc51.js +0 -1
  117. package/dist/multi/638.app-79c65c7bff941abcbc51.js +0 -1
  118. package/dist/multi/672.app-79c65c7bff941abcbc51.js +0 -1
  119. package/dist/multi/686.app-79c65c7bff941abcbc51.js +0 -1
  120. package/dist/multi/725.app-79c65c7bff941abcbc51.js +0 -1
  121. package/dist/multi/741.app-79c65c7bff941abcbc51.js +0 -1
  122. package/dist/multi/755.app-79c65c7bff941abcbc51.js +0 -1
  123. package/dist/multi/894.app-79c65c7bff941abcbc51.js +0 -1
  124. package/dist/multi/91.app-79c65c7bff941abcbc51.js +0 -1
  125. package/dist/multi/943.app-79c65c7bff941abcbc51.js +0 -1
  126. package/dist/multi/980.app-79c65c7bff941abcbc51.js +0 -1
  127. package/dist/multi/app-79c65c7bff941abcbc51.js +0 -2
  128. package/dist/single/app-3ca67f29d0f1166c08ca.js +0 -2
  129. package/src/components/SectionTabs/index.tsx +0 -0
  130. /package/dist/multi/{app-79c65c7bff941abcbc51.js.LICENSE.txt → app-8be6acc0a596a2197dbf.js.LICENSE.txt} +0 -0
  131. /package/dist/single/{app-3ca67f29d0f1166c08ca.js.LICENSE.txt → app-8221eb856e47b4ef50d6.js.LICENSE.txt} +0 -0
@@ -21,7 +21,8 @@
21
21
  "results": "Результати",
22
22
  "globalAttachments": "Глобальні Вкладення",
23
23
  "globalErrors": "Глобальні Помилки",
24
- "qualityGates": "Ворота Якості"
24
+ "qualityGates": "Ворота Якості",
25
+ "categories": "Категорії"
25
26
  },
26
27
  "search": {
27
28
  "search": "Пошук",
@@ -29,13 +30,19 @@
29
30
  },
30
31
  "filters": {
31
32
  "flaky": "Нестабільні",
33
+ "nonFlaky": "Стабільні",
32
34
  "retry": "Повторені",
33
35
  "new": "Нові",
34
36
  "fixed": "Виправлені",
35
37
  "regressed": "Регресовані",
36
38
  "malfunctioned": "Зламані",
37
39
  "transition": "Перехід",
40
+ "status": "Статус",
41
+ "severity": "Важливість",
42
+ "owner": "Власник",
43
+ "layer": "Шар",
38
44
  "tags": "Теги",
45
+ "categories": "Категорії",
39
46
  "goto_filter": "Перейти до фільтру",
40
47
  "errors": {
41
48
  "max_values_one": "Тільки перше {{count}} значення використовується для фільтрації.\nДодаткові значення ігноруються",
@@ -48,7 +55,8 @@
48
55
  "fixed": "Показати тести, які тепер успішні, але раніше були \"неуспішними\" або \"зламаними\" в попередньому звіті",
49
56
  "regressed": "Показати результати тестів, які змінили статус на \"неуспішний\" з \"успішний\" або \"зламаний\"",
50
57
  "malfunctioned": "Показати результати тестів, які змінили статус на \"зламаний\" з \"успішний\" або \"неуспішний\"",
51
- "tags": "Показати результати тестів, які мають вказані теги"
58
+ "tags": "Показати результати тестів, які мають вказані теги",
59
+ "categories": "Показати результати тестів, які мають зазначені категорії"
52
60
  }
53
61
  },
54
62
  "sort-by": {
@@ -82,6 +90,16 @@
82
90
  },
83
91
  "empty": {
84
92
  "no-results": "Немає результатів",
93
+ "no-value": "Немає значення",
94
+ "no-value-for": "Немає значення: {{entity}}",
95
+ "no-transition": "Немає переходу",
96
+ "no-layer": "Немає шару",
97
+ "no-owner": "Немає власника",
98
+ "no-severity": "Немає важливості",
99
+ "no-status": "Немає статусу",
100
+ "no-environment": "Немає середовища",
101
+ "no-flaky": "Немає нестабільності",
102
+ "empty-value": "Порожньо",
85
103
  "no-tests-found": "Результати не знайдено",
86
104
  "no-message-provided": "Повідомлення не надано",
87
105
  "clear-filters": "Очистити фільтри",
@@ -92,7 +110,8 @@
92
110
  "no-retries-results": "Інформація про перезапуски відсутня",
93
111
  "no-test-steps-results": "Немає інформації про кроки тестування",
94
112
  "no-test-case-results": "Немає результатів тест-кейсів",
95
- "no-environments-results": "Інформація про оточення відсутня"
113
+ "no-environments-results": "Інформація про оточення відсутня",
114
+ "no-categories-results": "Немає доступних результатів за категоріями"
96
115
  },
97
116
  "severity": {
98
117
  "blocker": "блокер",
@@ -116,6 +135,7 @@
116
135
  "labels": "Мітки",
117
136
  "metadata": "Метадані",
118
137
  "parameters": "Параметри",
138
+ "category": "Категорія",
119
139
  "description": "Опис",
120
140
  "links": "Посилання",
121
141
  "overview": "Огляд",
@@ -128,6 +148,7 @@
128
148
  "showLess": "Показати менше",
129
149
  "showMore": "Показати більше",
130
150
  "copy": "Скопіювати",
151
+ "copy-email": "Скопіювати email",
131
152
  "attempt": "Спроба {{attempt}} з {{total}}",
132
153
  "at": "в",
133
154
  "variables": "Змінні",
@@ -257,8 +278,8 @@
257
278
  "history": "{{timestamp, timestamp_date}}"
258
279
  }
259
280
  },
260
- "fbsuAgePyramid": {
261
- "title": "FBSU вікова піраміда",
281
+ "statusAgePyramid": {
282
+ "title": "Вікова піраміда за статусом",
262
283
  "status": {
263
284
  "passed": "$t(statuses:passed, capitalize)",
264
285
  "failed": "$t(statuses:failed, capitalize)",
@@ -21,7 +21,8 @@
21
21
  "results": "结果",
22
22
  "globalAttachments": "全局附件",
23
23
  "globalErrors": "全局错误",
24
- "qualityGates": "质量门"
24
+ "qualityGates": "质量门",
25
+ "categories": "分类"
25
26
  },
26
27
  "search": {
27
28
  "search": "搜索",
@@ -29,13 +30,19 @@
29
30
  },
30
31
  "filters": {
31
32
  "flaky": "不稳定",
33
+ "nonFlaky": "稳定",
32
34
  "retry": "重试",
33
35
  "new": "新的",
34
36
  "fixed": "已修复",
35
37
  "regressed": "已回归",
36
38
  "malfunctioned": "已损坏",
37
39
  "transition": "过渡",
40
+ "status": "状态",
41
+ "severity": "严重程度",
42
+ "owner": "负责人",
43
+ "layer": "层",
38
44
  "tags": "标签",
45
+ "categories": "分类",
39
46
  "goto_filter": "转到过滤器",
40
47
  "errors": {
41
48
  "max_values_one": "仅使用第一个 {{count}} 值进行过滤。\n忽略其他值",
@@ -48,7 +55,8 @@
48
55
  "fixed": "显示现在通过但之前\"失败\"或\"损坏\"的测试",
49
56
  "regressed": "显示从\"通过\"或\"损坏\"状态变为\"失败\"状态的测试结果",
50
57
  "malfunctioned": "显示从\"通过\"或\"失败\"状态变为\"损坏\"状态的测试结果",
51
- "tags": "显示具有指定标签的测试结果"
58
+ "tags": "显示具有指定标签的测试结果",
59
+ "categories": "显示具有指定分类的测试结果"
52
60
  }
53
61
  },
54
62
  "sort-by": {
@@ -82,6 +90,16 @@
82
90
  },
83
91
  "empty": {
84
92
  "no-results": "没有结果",
93
+ "no-value": "无值",
94
+ "no-value-for": "无值:{{entity}}",
95
+ "no-transition": "无过渡",
96
+ "no-layer": "无层",
97
+ "no-owner": "无负责人",
98
+ "no-severity": "无严重程度",
99
+ "no-status": "无状态",
100
+ "no-environment": "无环境",
101
+ "no-flaky": "无不稳定",
102
+ "empty-value": "空",
85
103
  "no-tests-found": "未找到结果",
86
104
  "no-message-provided": "未提供消息",
87
105
  "clear-filters": "清除过滤器",
@@ -92,7 +110,8 @@
92
110
  "no-retries-results": "没有重试信息",
93
111
  "no-test-steps-results": "没有可用的测试步骤信息",
94
112
  "no-test-case-results": "没有测试用例结果",
95
- "no-environments-results": "没有环境信息"
113
+ "no-environments-results": "没有环境信息",
114
+ "no-categories-results": "暂无可用的分类结果"
96
115
  },
97
116
  "severity": {
98
117
  "blocker": "阻断",
@@ -116,6 +135,7 @@
116
135
  "labels": "标签",
117
136
  "metadata": "元数据",
118
137
  "parameters": "参数",
138
+ "category": "分类",
119
139
  "description": "描述",
120
140
  "links": "链接",
121
141
  "overview": "概览",
@@ -128,6 +148,7 @@
128
148
  "showLess": "显示更少",
129
149
  "showMore": "显示更多",
130
150
  "copy": "复制",
151
+ "copy-email": "复制邮箱",
131
152
  "attempt": "第 {{attempt}} 次尝试,共 {{total}} 次",
132
153
  "at": "在",
133
154
  "variables": "变量",
@@ -306,8 +327,8 @@
306
327
  "history": "{{timestamp, timestamp_date}}"
307
328
  }
308
329
  },
309
- "fbsuAgePyramid": {
310
- "title": "FBSU年龄金字塔",
330
+ "statusAgePyramid": {
331
+ "title": "状态年龄金字塔",
311
332
  "status": {
312
333
  "passed": "$t(statuses:passed, capitalize)",
313
334
  "failed": "$t(statuses:failed, capitalize)",
@@ -0,0 +1,44 @@
1
+ import type { TestCategories } from "@allurereport/core-api";
2
+ import { fetchReportJsonData } from "@allurereport/web-commons";
3
+ import { computed, signal } from "@preact/signals";
4
+ import type { StoreSignalState } from "@/stores/types";
5
+
6
+ export const categoriesStore = signal<StoreSignalState<TestCategories>>({
7
+ loading: true,
8
+ error: undefined,
9
+ data: undefined,
10
+ });
11
+
12
+ export const noCategories = computed(() => categoriesStore?.value?.data.roots.length);
13
+
14
+ let lastCategoriesEnv: string | undefined;
15
+
16
+ const resolveCategoriesPath = (env?: string) => (env ? `widgets/${env}/categories.json` : "widgets/categories.json");
17
+
18
+ export const fetchCategoriesData = async (env?: string) => {
19
+ if (lastCategoriesEnv === env && categoriesStore.peek().data) {
20
+ return;
21
+ }
22
+ lastCategoriesEnv = env;
23
+ categoriesStore.value = {
24
+ ...categoriesStore.value,
25
+ loading: true,
26
+ error: undefined,
27
+ };
28
+
29
+ try {
30
+ const res = await fetchReportJsonData<TestCategories>(resolveCategoriesPath(env));
31
+
32
+ categoriesStore.value = {
33
+ data: res,
34
+ error: undefined,
35
+ loading: false,
36
+ };
37
+ } catch (e) {
38
+ categoriesStore.value = {
39
+ ...categoriesStore.value,
40
+ error: undefined,
41
+ loading: false,
42
+ };
43
+ }
44
+ };
@@ -1,5 +1,11 @@
1
1
  import { formatDuration } from "@allurereport/core-api";
2
- import { DEFAULT_LOCALE, LANG_LOCALE, type LangLocale, getReportOptions } from "@allurereport/web-commons";
2
+ import {
3
+ DEFAULT_LOCALE,
4
+ LANG_LOCALE,
5
+ type LangLocale,
6
+ getLocaleDateTimeOverride,
7
+ getReportOptions,
8
+ } from "@allurereport/web-commons";
3
9
  import { computed, signal } from "@preact/signals";
4
10
  import i18next, { type TOptions } from "i18next";
5
11
  import type { AwesomeReportOptions } from "types";
@@ -50,7 +56,8 @@ export const waitForI18next = i18next
50
56
  namespace: string,
51
57
  callback: (errorValue: unknown, translations: null) => void,
52
58
  ) => {
53
- await import(`@/locales/${language}.json`)
59
+ const loadLocale = language === "en-iso" ? "en" : language;
60
+ await import(`@/locales/${loadLocale}.json`)
54
61
  .then((resources: Record<string, null>) => {
55
62
  callback(null, resources[namespace]);
56
63
  })
@@ -66,43 +73,68 @@ export const waitForI18next = i18next
66
73
  interpolation: { escapeValue: false },
67
74
  })
68
75
  .then(() => {
69
- i18next.services.formatter.add("capitalize", (value) => {
76
+ i18next.services.formatter.add("capitalize", (value: string) => {
70
77
  return value.charAt(0).toLocaleUpperCase() + value.slice(1);
71
78
  });
72
- i18next.services.formatter.add("timestamp_date", (value: number, lng, options) => {
73
- const formatter = new Intl.DateTimeFormat(lng, {
74
- ...options,
75
- month: "numeric",
76
- day: "numeric",
77
- year: "numeric",
78
- });
79
- return formatter.format(value);
80
- });
81
- i18next.services.formatter.add("timestamp_long", (value: number, lng, options) => {
82
- const formatter = new Intl.DateTimeFormat(lng, {
83
- ...options,
84
- month: "numeric",
85
- day: "numeric",
86
- year: "numeric",
87
- hour: "numeric",
88
- minute: "numeric",
89
- second: "numeric",
90
- hour12: false,
91
- });
92
- return formatter.format(value).replace(",", ` ${i18next.t("ui:at")}`);
93
- });
94
- i18next.services.formatter.add("timestamp_long_no_seconds", (value: number, lng, options) => {
95
- const formatter = new Intl.DateTimeFormat(lng, {
96
- ...options,
97
- month: "numeric",
98
- day: "numeric",
99
- year: "numeric",
100
- hour: "numeric",
101
- minute: "numeric",
102
- hour12: false,
103
- });
104
- return formatter.format(value).replace(",", ` ${i18next.t("ui:at")}`);
105
- });
79
+ i18next.services.formatter.add(
80
+ "timestamp_date",
81
+ (value: number, lng: string, options?: Intl.DateTimeFormatOptions) => {
82
+ const override = getLocaleDateTimeOverride(lng, "date");
83
+ const formatter = new Intl.DateTimeFormat(override?.locale ?? lng, {
84
+ ...options,
85
+ month: "numeric",
86
+ day: "numeric",
87
+ year: "numeric",
88
+ ...(override?.options ?? {}),
89
+ });
90
+ const formatted = formatter.format(value);
91
+ return override?.stripComma ? formatted.replace(",", "") : formatted;
92
+ },
93
+ );
94
+
95
+ i18next.services.formatter.add(
96
+ "timestamp_long",
97
+ (value: number, lng: string, options?: Intl.DateTimeFormatOptions) => {
98
+ const override = getLocaleDateTimeOverride(lng, "dateTime");
99
+ const formatter = new Intl.DateTimeFormat(override?.locale ?? lng, {
100
+ ...options,
101
+ month: "numeric",
102
+ day: "numeric",
103
+ year: "numeric",
104
+ hour: "numeric",
105
+ minute: "numeric",
106
+ second: "numeric",
107
+ hour12: false,
108
+ ...(override?.options ?? {}),
109
+ });
110
+ const formatted = formatter.format(value);
111
+ if (override?.includeAtSeparator === false || override?.stripComma) {
112
+ return formatted.replace(",", "");
113
+ }
114
+ return formatted.replace(",", ` ${i18next.t("ui:at")}`);
115
+ },
116
+ );
117
+ i18next.services.formatter.add(
118
+ "timestamp_long_no_seconds",
119
+ (value: number, lng: string, options?: Intl.DateTimeFormatOptions) => {
120
+ const override = getLocaleDateTimeOverride(lng, "dateTimeNoSeconds");
121
+ const formatter = new Intl.DateTimeFormat(override?.locale ?? lng, {
122
+ ...options,
123
+ month: "numeric",
124
+ day: "numeric",
125
+ year: "numeric",
126
+ hour: "numeric",
127
+ minute: "numeric",
128
+ hour12: false,
129
+ ...(override?.options ?? {}),
130
+ });
131
+ const formatted = formatter.format(value);
132
+ if (override?.includeAtSeparator === false || override?.stripComma) {
133
+ return formatted.replace(",", "");
134
+ }
135
+ return formatted.replace(",", ` ${i18next.t("ui:at")}`);
136
+ },
137
+ );
106
138
  i18next.services.formatter.add("format_duration", (value: number) => {
107
139
  return formatDuration(value);
108
140
  });
@@ -3,7 +3,7 @@ import { fetchReportJsonData } from "@allurereport/web-commons";
3
3
  import { signal } from "@preact/signals";
4
4
  import { type StoreSignalState } from "./types";
5
5
 
6
- export const qualityGateStore = signal<StoreSignalState<QualityGateValidationResult[]>>({
6
+ export const qualityGateStore = signal<StoreSignalState<Record<string, QualityGateValidationResult[]>>>({
7
7
  loading: true,
8
8
  error: undefined,
9
9
  data: undefined,
@@ -11,7 +11,7 @@ export const qualityGateStore = signal<StoreSignalState<QualityGateValidationRes
11
11
 
12
12
  export const fetchQualityGateResults = async () => {
13
13
  try {
14
- const data = await fetchReportJsonData<QualityGateValidationResult[]>("widgets/quality-gate.json");
14
+ const data = await fetchReportJsonData<Record<string, QualityGateValidationResult[]>>("widgets/quality-gate.json");
15
15
 
16
16
  qualityGateStore.value = {
17
17
  data,
@@ -1,27 +1,79 @@
1
1
  import { createRoute, navigateTo as routerNavigateTo } from "@allurereport/web-commons";
2
2
  import { computed } from "@preact/signals";
3
3
 
4
+ const normalizeTab = (tab?: string) => (tab && tab !== "overview" ? tab : undefined);
5
+
4
6
  export const navigateToTestResult = (params: { testResultId: string; tab?: string }) => {
5
- routerNavigateTo({ path: "/:testResultId/:tab?", params, keepSearchParams: true });
7
+ const normalized = { ...params, tab: normalizeTab(params.tab) };
8
+ const path = rootTabRoute.value.matches ? "/:rootTab/:testResultId/:tab?" : "/:testResultId/:tab?";
9
+ const routeParams = rootTabRoute.value.matches
10
+ ? { ...normalized, rootTab: rootTabRoute.value.params.rootTab }
11
+ : normalized;
12
+ routerNavigateTo({ path, params: routeParams, keepSearchParams: true });
13
+ };
14
+
15
+ export const navigateToPlainTestResult = (params: { testResultId: string; tab?: string }) => {
16
+ const normalized = { ...params, tab: normalizeTab(params.tab) };
17
+ routerNavigateTo({ path: "/:testResultId/:tab?", params: normalized, keepSearchParams: true });
6
18
  };
7
19
 
8
20
  export const navigateToTestResultTab = (params: { testResultId: string; tab: string }) => {
9
- routerNavigateTo({ path: "/:testResultId/:tab?", params, keepSearchParams: true, replace: true });
21
+ const normalized = { ...params, tab: normalizeTab(params.tab) };
22
+ const path = rootTabRoute.value.matches ? "/:rootTab/:testResultId/:tab?" : "/:testResultId/:tab?";
23
+ const routeParams = rootTabRoute.value.matches
24
+ ? { ...normalized, rootTab: rootTabRoute.value.params.rootTab }
25
+ : normalized;
26
+ routerNavigateTo({ path, params: routeParams, keepSearchParams: true, replace: true });
10
27
  };
11
28
 
12
29
  export const navigateToRoot = () => {
13
30
  routerNavigateTo({ path: "/", keepSearchParams: true });
14
31
  };
15
32
 
33
+ export const navigateToCategoriesRoot = () => {
34
+ routerNavigateTo({ path: "/categories", keepSearchParams: true });
35
+ };
36
+
37
+ export const navigateToCategoriesTestResult = (params: { testResultId: string; tab?: string }) => {
38
+ const normalized = { ...params, tab: normalizeTab(params.tab) };
39
+ routerNavigateTo({ path: "/categories/:testResultId/:tab?", params: normalized, keepSearchParams: true });
40
+ };
41
+
42
+ export const navigateToRootTabRoot = (params: { rootTab: string }) => {
43
+ routerNavigateTo({ path: "/:rootTab", params, keepSearchParams: true });
44
+ };
45
+
46
+ export const navigateToRootTabTestResult = (params: { rootTab: string; testResultId: string; tab?: string }) => {
47
+ const normalized = { ...params, tab: normalizeTab(params.tab) };
48
+ routerNavigateTo({ path: "/:rootTab/:testResultId/:tab?", params: normalized, keepSearchParams: true });
49
+ };
50
+
16
51
  export const navigateToSection = (params: { section: "timeline" | "charts" }) => {
17
52
  routerNavigateTo({ path: "/:section", params, keepSearchParams: true, replace: false });
18
53
  };
19
54
 
20
55
  const sections = ["charts", "timeline"];
56
+ const rootTabs = ["categories", "qualityGate", "globalAttachments", "globalErrors"];
57
+
58
+ export const rootTabRoute = computed(() =>
59
+ createRoute<{ rootTab: string; testResultId?: string; tab?: string }>(
60
+ "/:rootTab/:testResultId?/:tab?",
61
+ ({ params }) => rootTabs.includes(params.rootTab) && params.rootTab !== params.testResultId,
62
+ ),
63
+ );
64
+
65
+ export const categoriesRoute = computed(() =>
66
+ createRoute<{ testResultId?: string; tab?: string }>("/categories/:testResultId?/:tab?"),
67
+ );
21
68
 
22
69
  export const testResultRoute = computed(() =>
23
70
  createRoute<{ testResultId: string; tab?: string }>("/:testResultId/:tab?", ({ params }) => {
24
- return params.testResultId && !sections.includes(params.testResultId);
71
+ return (
72
+ params.testResultId &&
73
+ params.testResultId !== "categories" &&
74
+ !sections.includes(params.testResultId) &&
75
+ !rootTabs.includes(params.testResultId)
76
+ );
25
77
  }),
26
78
  );
27
79
 
@@ -1,5 +1,16 @@
1
1
  import { computed } from "@preact/signals";
2
- import { testResultRoute } from "./router";
2
+ import { rootTabRoute, testResultRoute } from "./router";
3
3
 
4
- export const trCurrentTab = computed(() => testResultRoute.value.params.tab ?? "overview");
5
- export const currentTrId = computed(() => testResultRoute.value.params.testResultId);
4
+ const emptyRoute = { matches: false, params: {} as { testResultId?: string; tab?: string } };
5
+ const activeTestResultRoute = computed(() => {
6
+ if (rootTabRoute.value.matches && rootTabRoute.value.params.testResultId) {
7
+ return rootTabRoute.value;
8
+ }
9
+ if (testResultRoute.value.matches) {
10
+ return testResultRoute.value;
11
+ }
12
+ return emptyRoute;
13
+ });
14
+
15
+ export const trCurrentTab = computed(() => activeTestResultRoute.value.params.tab ?? "overview");
16
+ export const currentTrId = computed(() => activeTestResultRoute.value.params.testResultId);
@@ -5,8 +5,11 @@ import type { StoreSignalState } from "@/stores/types";
5
5
 
6
6
  export type TimlineTr = Pick<
7
7
  TestResult,
8
- "id" | "name" | "status" | "flaky" | "hidden" | "labels" | "environment" | "start" | "stop" | "duration"
9
- >;
8
+ "id" | "name" | "status" | "flaky" | "hidden" | "environment" | "start" | "duration"
9
+ > & {
10
+ host: string;
11
+ thread: string;
12
+ };
10
13
 
11
14
  export const timelineStore = signal<StoreSignalState<TimlineTr[]>>({
12
15
  loading: true,
@@ -2,7 +2,7 @@ import type { TestStatus, TestStatusTransition } from "@allurereport/core-api";
2
2
  import { ReportFetchError, fetchReportJsonData, setParams } from "@allurereport/web-commons";
3
3
  import { PARAMS } from "./constants";
4
4
  import type { TreeFiltersData } from "./model";
5
- import { treeTags } from "./store";
5
+ import { treeCategories, treeTags } from "./store";
6
6
 
7
7
  export const setQueryFilter = (query?: string) => {
8
8
  setParams({
@@ -46,14 +46,23 @@ export const setTagsFilter = (tags: string[]) => {
46
46
  });
47
47
  };
48
48
 
49
+ export const setCategoriesFilter = (categories: string[]) => {
50
+ setParams({
51
+ key: PARAMS.CATEGORIES,
52
+ value: categories,
53
+ });
54
+ };
55
+
49
56
  export const fetchTreeFiltersData = async () => {
50
57
  try {
51
58
  const response = await fetchReportJsonData<TreeFiltersData>("widgets/tree-filters.json", { bustCache: true });
52
59
 
53
60
  treeTags.value = response.tags;
61
+ treeCategories.value = response.categories ?? [];
54
62
  } catch (error) {
55
63
  if (error instanceof ReportFetchError && error.response.status === 404) {
56
64
  treeTags.value = [];
65
+ treeCategories.value = [];
57
66
  return;
58
67
  }
59
68
 
@@ -10,4 +10,5 @@ export const PARAMS = {
10
10
  RETRY: "retry",
11
11
  TRANSITION: "transition",
12
12
  TAGS: "tags",
13
+ CATEGORIES: "categories",
13
14
  } as const;
@@ -16,6 +16,7 @@ export type Filters = {
16
16
  retry?: boolean;
17
17
  transition?: TestStatusTransition[];
18
18
  tags?: string[];
19
+ categories?: string[];
19
20
  };
20
21
 
21
22
  export type AwesomeFieldFilter = FieldFilter<keyof AwesomeTreeLeaf>;
@@ -48,4 +49,5 @@ export type AwesomeBooleanFieldFilter = AwesomeFieldFilter & {
48
49
 
49
50
  export type TreeFiltersData = {
50
51
  tags: string[];
52
+ categories: string[];
51
53
  };
@@ -3,6 +3,7 @@ import { getParamValue, getParamValues } from "@allurereport/web-commons";
3
3
  import { computed, signal } from "@preact/signals";
4
4
  import type { AwesomeStatus } from "types";
5
5
  import {
6
+ setCategoriesFilter,
6
7
  setFlakyFilter,
7
8
  setQueryFilter,
8
9
  setRetryFilter,
@@ -19,6 +20,7 @@ import type {
19
20
  AwesomeStringFieldFilter,
20
21
  } from "./model";
21
22
  import {
23
+ isCategoryFilter,
22
24
  isFlakyFilter,
23
25
  isRetryFilter,
24
26
  isTagFilter,
@@ -28,8 +30,10 @@ import {
28
30
  } from "./utils";
29
31
 
30
32
  export const treeTags = signal<string[]>([]);
33
+ export const treeCategories = signal<string[]>([]);
31
34
 
32
35
  const hasTreeTags = computed(() => treeTags.value.length > 0);
36
+ const hasTreeCategories = computed(() => treeCategories.value.length > 0);
33
37
 
34
38
  const urlQueryFilter = computed<string | undefined>(() => {
35
39
  const queryValue = getParamValue(PARAMS.QUERY) ?? "";
@@ -82,6 +86,22 @@ const urlTagsFilter = computed<string[]>(() => {
82
86
  return tags.filter((tag) => treeTags.value.includes(tag));
83
87
  });
84
88
 
89
+ const EMPTY_CATEGORIES: string[] = [];
90
+
91
+ const urlCategoriesFilter = computed<string[]>(() => {
92
+ const categories = getParamValues(PARAMS.CATEGORIES) ?? EMPTY_CATEGORIES;
93
+
94
+ if (categories.length === 0) {
95
+ return EMPTY_CATEGORIES;
96
+ }
97
+
98
+ if (treeCategories.value.length === 0) {
99
+ return categories;
100
+ }
101
+
102
+ return categories.filter((category) => treeCategories.value.includes(category));
103
+ });
104
+
85
105
  const treeStatusFilter = computed<AwesomeStringFieldFilter>(() => ({
86
106
  type: "field",
87
107
  logicalOperator: "AND",
@@ -177,11 +197,23 @@ const treeTagsFilter = computed<AwesomeArrayFieldFilter>(() => ({
177
197
  },
178
198
  }));
179
199
 
200
+ const treeCategoriesFilter = computed<AwesomeArrayFieldFilter>(() => ({
201
+ type: "field",
202
+ logicalOperator: "AND",
203
+ value: {
204
+ key: "categories",
205
+ value: urlCategoriesFilter.value,
206
+ type: "array",
207
+ strict: false,
208
+ },
209
+ }));
210
+
180
211
  export const treeQuickFilters = computed<AwesomeFilter[]>(() => [
181
212
  treeRetryFilter.value,
182
213
  treeFlakyFilter.value,
183
214
  treeTransitionFilter.value,
184
215
  treeTagsFilter.value,
216
+ treeCategoriesFilter.value,
185
217
  ]);
186
218
 
187
219
  export const treeFilters = computed(() => {
@@ -220,6 +252,10 @@ export const treeFilters = computed(() => {
220
252
  filters.push(treeTagsFilter.value);
221
253
  }
222
254
 
255
+ if (urlCategoriesFilter.value.length > 0) {
256
+ filters.push(treeCategoriesFilter.value);
257
+ }
258
+
223
259
  if (urlStatusFilter.value) {
224
260
  filters.push(treeStatusFilter.value);
225
261
  }
@@ -255,6 +291,14 @@ export const setTreeFilter = (filter: AwesomeFilter) => {
255
291
  ) {
256
292
  setTagsFilter(filter.value.value);
257
293
  }
294
+
295
+ if (
296
+ isCategoryFilter(filter) &&
297
+ // Apply categories filter only if there are categories to filter by
298
+ hasTreeCategories.peek()
299
+ ) {
300
+ setCategoriesFilter(filter.value.value);
301
+ }
258
302
  };
259
303
 
260
304
  export const treeStatus = computed<AwesomeStatus>(() => urlStatusFilter.value ?? "total");
@@ -269,5 +313,6 @@ export const clearTreeFilters = () => {
269
313
  setFlakyFilter(false);
270
314
  setTransitionFilter([]);
271
315
  setTagsFilter([]);
316
+ setCategoriesFilter([]);
272
317
  setStatusFilter();
273
318
  };