@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
@@ -0,0 +1,12 @@
1
+ .tree-item-severity {
2
+ .tree-section-title {
3
+ display: flex;
4
+ align-items: center;
5
+ }
6
+ }
7
+
8
+ .tree-item-severity-title {
9
+ display: inline-flex;
10
+ align-items: center;
11
+ gap: 8px;
12
+ }
@@ -0,0 +1,12 @@
1
+ type StickyOverrides = Partial<Record<`--${string}`, string>>;
2
+
3
+ export const createCategoriesStickyStyle = (depth: number, overrides: StickyOverrides = {}) =>
4
+ ({
5
+ "--categories-sticky-depth": `${depth}`,
6
+ "--tree-section-position": "sticky",
7
+ "--tree-section-top": `calc(${depth} * var(--categories-sticky-step))`,
8
+ "--tree-section-z": `${100 - depth}`,
9
+ "--tree-section-bg": "var(--bg-base-primary)",
10
+ "--tree-section-min-height": "var(--categories-sticky-step)",
11
+ ...overrides,
12
+ }) as Record<string, string>;
@@ -6,13 +6,13 @@ import {
6
6
  CurrentStatusChartWidget,
7
7
  DurationDynamicsChartWidget,
8
8
  DurationsChartWidget,
9
- FBSUAgePyramidChartWidget,
10
9
  Grid,
11
10
  GridItem,
12
11
  HeatMapWidget,
13
12
  Loadable,
14
13
  PageLoader,
15
14
  StabilityDistributionWidget,
15
+ StatusAgePyramidChartWidget,
16
16
  StatusDynamicsChartWidget,
17
17
  StatusTransitionsChartWidget,
18
18
  TestBaseGrowthDynamicsChartWidget,
@@ -113,15 +113,15 @@ const getChartWidgetByType = (
113
113
  />
114
114
  );
115
115
  }
116
- case ChartType.FBSUAgePyramid: {
117
- const title = chartData.title ?? t("fbsuAgePyramid.title");
116
+ case ChartType.StatusAgePyramid: {
117
+ const title = chartData.title ?? t("statusAgePyramid.title");
118
118
 
119
119
  return (
120
- <FBSUAgePyramidChartWidget
120
+ <StatusAgePyramidChartWidget
121
121
  title={title}
122
122
  data={chartData.data}
123
123
  statuses={chartData.statuses}
124
- i18n={(key, props = {}) => t(`fbsuAgePyramid.${key}`, props)}
124
+ i18n={(key, props = {}) => t(`statusAgePyramid.${key}`, props)}
125
125
  />
126
126
  );
127
127
  }
@@ -4,7 +4,7 @@ import clsx from "clsx";
4
4
  import { HeaderControls } from "@/components/HeaderControls";
5
5
  import { SectionPicker } from "@/components/SectionPicker";
6
6
  import { TrBreadcrumbs } from "@/components/TestResult/TrHeader/TrBreadcrumbs";
7
- import { testResultRoute } from "@/stores/router";
7
+ import { rootTabRoute, testResultRoute } from "@/stores/router";
8
8
  import { currentTrId } from "@/stores/testResult";
9
9
  import { testResultStore } from "@/stores/testResults";
10
10
  import { CiInfo } from "./CiInfo";
@@ -14,7 +14,9 @@ interface HeaderProps {
14
14
  className?: ClassValue;
15
15
  }
16
16
 
17
- const isTestResultRoute = computed(() => testResultRoute.value.matches);
17
+ const isTestResultRoute = computed(
18
+ () => testResultRoute.value.matches || Boolean(rootTabRoute.value.params.testResultId),
19
+ );
18
20
  const testResult = computed(() => testResultStore.value?.data?.[currentTrId.value]);
19
21
 
20
22
  export const Header = ({ className }: HeaderProps) => {
@@ -1,24 +1,36 @@
1
1
  import { Counter, Loadable } from "@allurereport/web-components";
2
2
  import clsx from "clsx";
3
+ import { useEffect } from "preact/hooks";
3
4
  import { NavTab, NavTabs, NavTabsList, useNavTabsContext } from "@/components/NavTabs";
4
5
  import { ReportBody } from "@/components/ReportBody";
6
+ import { ReportCategories } from "@/components/ReportCategories";
5
7
  import { ReportGlobalAttachments } from "@/components/ReportGlobalAttachments";
6
8
  import { ReportGlobalErrors } from "@/components/ReportGlobalErrors";
7
9
  import { ReportHeader } from "@/components/ReportHeader";
8
10
  import { ReportMetadata } from "@/components/ReportMetadata";
9
- import { reportStatsStore } from "@/stores";
10
- import { useI18n } from "@/stores";
11
+ import { reportStatsStore, useI18n } from "@/stores";
12
+ import { categoriesStore } from "@/stores/categories";
13
+ import { currentEnvironment } from "@/stores/env";
11
14
  import { globalsStore } from "@/stores/globals";
12
15
  import { isSplitMode } from "@/stores/layout";
13
16
  import { qualityGateStore } from "@/stores/qualityGate";
17
+ import {
18
+ navigateToPlainTestResult,
19
+ navigateToRoot,
20
+ navigateToRootTabRoot,
21
+ navigateToRootTabTestResult,
22
+ rootTabRoute,
23
+ } from "@/stores/router";
24
+ import { currentTrId, trCurrentTab } from "@/stores/testResult";
14
25
  import { ReportQualityGateResults } from "../ReportQualityGateResults";
15
26
  import * as styles from "./styles.scss";
16
27
 
17
- enum ReportRootTab {
28
+ export enum ReportRootTab {
18
29
  Results = "results",
19
30
  QualityGate = "qualityGate",
20
31
  GlobalAttachments = "globalAttachments",
21
32
  GlobalErrors = "globalErrors",
33
+ Categories = "categories",
22
34
  }
23
35
 
24
36
  const viewsByTab = {
@@ -31,6 +43,7 @@ const viewsByTab = {
31
43
  [ReportRootTab.GlobalAttachments]: () => <ReportGlobalAttachments />,
32
44
  [ReportRootTab.GlobalErrors]: () => <ReportGlobalErrors />,
33
45
  [ReportRootTab.QualityGate]: () => <ReportQualityGateResults />,
46
+ [ReportRootTab.Categories]: () => <ReportCategories />,
34
47
  };
35
48
 
36
49
  const MainReportContent = () => {
@@ -43,55 +56,143 @@ const MainReportContent = () => {
43
56
 
44
57
  const MainReport = () => {
45
58
  const { t } = useI18n("tabs");
59
+ const rootTabToReportTab: Record<string, ReportRootTab> = {
60
+ categories: ReportRootTab.Categories,
61
+ qualityGate: ReportRootTab.QualityGate,
62
+ globalAttachments: ReportRootTab.GlobalAttachments,
63
+ globalErrors: ReportRootTab.GlobalErrors,
64
+ };
65
+ const reportTabToRootTab: Partial<Record<ReportRootTab, string>> = {
66
+ [ReportRootTab.Categories]: "categories",
67
+ [ReportRootTab.QualityGate]: "qualityGate",
68
+ [ReportRootTab.GlobalAttachments]: "globalAttachments",
69
+ [ReportRootTab.GlobalErrors]: "globalErrors",
70
+ };
71
+ const initialTab = rootTabRoute.value.matches
72
+ ? (rootTabToReportTab[rootTabRoute.value.params.rootTab] ?? ReportRootTab.Results)
73
+ : ReportRootTab.Results;
74
+
75
+ const RootTab = (props: { id: ReportRootTab; children: any }) => {
76
+ const { id, children } = props;
77
+ const { currentTab, setCurrentTab } = useNavTabsContext();
78
+ const isCurrentTab = currentTab === id;
79
+
80
+ const handleClick = () => {
81
+ if (isCurrentTab) {
82
+ return;
83
+ }
84
+ setCurrentTab(id);
85
+ if (id === ReportRootTab.Results) {
86
+ if (currentTrId.value) {
87
+ navigateToPlainTestResult({ testResultId: currentTrId.value, tab: trCurrentTab.value });
88
+ } else {
89
+ navigateToRoot();
90
+ }
91
+ return;
92
+ }
93
+ const rootTab = reportTabToRootTab[id];
94
+ if (rootTab) {
95
+ if (currentTrId.value) {
96
+ navigateToRootTabTestResult({ rootTab, testResultId: currentTrId.value, tab: trCurrentTab.value });
97
+ } else {
98
+ navigateToRootTabRoot({ rootTab });
99
+ }
100
+ } else {
101
+ navigateToRoot();
102
+ }
103
+ };
104
+
105
+ return (
106
+ <NavTab id={id} onClick={handleClick} isCurrentTab={isCurrentTab}>
107
+ {children}
108
+ </NavTab>
109
+ );
110
+ };
111
+
112
+ // @ts-ignore
113
+ const RootTabRouteSync = () => {
114
+ const { currentTab, setCurrentTab } = useNavTabsContext();
115
+ const routeKey = rootTabRoute.value.matches ? (rootTabRoute.value.params.rootTab ?? "") : "";
116
+ useEffect(() => {
117
+ const routeMatches = rootTabRoute.value.matches;
118
+ const routeTab = routeMatches ? rootTabRoute.value.params.rootTab : undefined;
119
+ const mapped = routeMatches ? (rootTabToReportTab[routeTab] ?? ReportRootTab.Results) : ReportRootTab.Results;
120
+ if (currentTab !== mapped) {
121
+ setCurrentTab(mapped);
122
+ }
123
+ }, [currentTab, setCurrentTab, routeKey]);
124
+ return null;
125
+ };
46
126
 
47
127
  return (
48
- <>
49
- <div className={clsx(styles.content, isSplitMode.value ? styles["scroll-inside"] : "")}>
50
- <ReportHeader />
51
- <div className={styles["main-report-tabs"]}>
52
- <NavTabs initialTab={ReportRootTab.Results}>
53
- <NavTabsList>
54
- <Loadable
55
- source={reportStatsStore}
56
- renderData={(stats) => (
57
- <NavTab id={ReportRootTab.Results}>
58
- {t("results")} <Counter count={stats?.total ?? 0} />
59
- </NavTab>
60
- )}
61
- />
62
- <Loadable
63
- source={qualityGateStore}
64
- renderData={(results) => (
128
+ <div className={clsx(styles.content, isSplitMode.value ? styles["scroll-inside"] : "")}>
129
+ <ReportHeader />
130
+ <div className={styles["main-report-tabs"]}>
131
+ <NavTabs initialTab={initialTab}>
132
+ <RootTabRouteSync />
133
+ <NavTabsList>
134
+ <Loadable
135
+ source={reportStatsStore}
136
+ renderData={(stats) => (
137
+ <RootTab id={ReportRootTab.Results}>
138
+ {t("results")} <Counter count={stats?.total ?? 0} />
139
+ </RootTab>
140
+ )}
141
+ />
142
+ <Loadable
143
+ source={categoriesStore}
144
+ renderData={(categories) => {
145
+ if (!categories || !categories.roots?.length) {
146
+ return null;
147
+ }
148
+ return (
65
149
  <>
66
- <NavTab id={ReportRootTab.QualityGate}>
67
- {t("qualityGates")}{" "}
68
- <Counter status={results.length > 0 ? "failed" : undefined} count={results.length} />
69
- </NavTab>
150
+ <RootTab id={ReportRootTab.Categories}>
151
+ {t("categories")} <Counter count={categories.roots?.length} />
152
+ </RootTab>
70
153
  </>
71
- )}
72
- />
73
- <Loadable
74
- source={globalsStore}
75
- renderData={({ attachments = [], errors = [] }) => (
76
- <>
77
- <NavTab id={ReportRootTab.GlobalAttachments}>
78
- {t("globalAttachments")} <Counter count={attachments.length} />
79
- </NavTab>
80
- <NavTab id={ReportRootTab.GlobalErrors}>
81
- {t("globalErrors")}{" "}
82
- <Counter status={errors.length > 0 ? "failed" : undefined} count={errors.length} />
83
- </NavTab>
84
- </>
85
- )}
86
- />
87
- </NavTabsList>
88
- <div className={styles["main-report-tabs-content"]}>
89
- <MainReportContent />
90
- </div>
91
- </NavTabs>
92
- </div>
154
+ );
155
+ }}
156
+ />
157
+ <Loadable
158
+ source={qualityGateStore}
159
+ renderData={(results) => {
160
+ const currentEnvResults = currentEnvironment.value
161
+ ? (results[currentEnvironment.value] ?? [])
162
+ : Object.values(results).flatMap((envResults) => envResults);
163
+
164
+ return (
165
+ <RootTab id={ReportRootTab.QualityGate}>
166
+ {t("qualityGates")}{" "}
167
+ <Counter
168
+ status={currentEnvResults.length > 0 ? "failed" : undefined}
169
+ count={currentEnvResults.length}
170
+ />
171
+ </RootTab>
172
+ );
173
+ }}
174
+ />
175
+ <Loadable
176
+ source={globalsStore}
177
+ renderData={({ attachments = [], errors = [] }) => (
178
+ <>
179
+ <RootTab id={ReportRootTab.GlobalAttachments}>
180
+ {t("globalAttachments")} <Counter count={attachments.length} />
181
+ </RootTab>
182
+ <RootTab id={ReportRootTab.GlobalErrors}>
183
+ {t("globalErrors")}{" "}
184
+ <Counter status={errors.length > 0 ? "failed" : undefined} count={errors.length} />
185
+ </RootTab>
186
+ </>
187
+ )}
188
+ />
189
+ </NavTabsList>
190
+ <div className={styles["main-report-tabs-content"]}>
191
+ <MainReportContent />
192
+ </div>
193
+ </NavTabs>
93
194
  </div>
94
- </>
195
+ </div>
95
196
  );
96
197
  };
97
198
  export default MainReport;
@@ -7,6 +7,7 @@ import type { MetadataProps } from "@/components/ReportMetadata";
7
7
  import { useI18n } from "@/stores/locale";
8
8
  import { getTagsFilterUrl } from "@/stores/treeFilters/utils";
9
9
  import { copyToClipboard } from "@/utils/copyToClipboard";
10
+ import { parseOwnerAddress } from "@/utils/ownerAddress";
10
11
  import * as styles from "./styles.scss";
11
12
 
12
13
  export const MetadataList: FunctionalComponent<MetadataProps & { columns?: number }> = ({
@@ -65,6 +66,49 @@ const OpenFilterUrlButton: FunctionalComponent<{ url: string }> = ({ url }) => {
65
66
  );
66
67
  };
67
68
 
69
+ const MAX_URL_LENGTH = 25;
70
+
71
+ const OwnerAction = (props: { ownerValue: string }) => {
72
+ const { t } = useI18n("ui");
73
+ const { ownerValue } = props;
74
+ const ownerAddress = parseOwnerAddress(ownerValue);
75
+
76
+ if (ownerAddress.type === "none") {
77
+ return null;
78
+ }
79
+
80
+ // Don't need to show copy email button here because
81
+ // we already have a button to copy the whole owner value
82
+ if (ownerAddress.type === "email" && ownerAddress.email === ownerValue) {
83
+ return null;
84
+ }
85
+
86
+ if (ownerAddress.type === "email") {
87
+ return (
88
+ <Button
89
+ icon={allureIcons.lineGeneralCopy3}
90
+ style="outline"
91
+ text={t("copy-email")}
92
+ onClick={() => copyToClipboard(ownerAddress.email)}
93
+ />
94
+ );
95
+ }
96
+
97
+ if (ownerAddress.type === "url") {
98
+ const truncatedUrl =
99
+ ownerAddress.url.length > MAX_URL_LENGTH ? `${ownerAddress.url.slice(0, MAX_URL_LENGTH)}...` : ownerAddress.url;
100
+ return (
101
+ <ButtonLink
102
+ href={ownerAddress.url}
103
+ target="_blank"
104
+ style="ghost"
105
+ icon={allureIcons.lineGeneralLinkExternal}
106
+ text={truncatedUrl}
107
+ />
108
+ );
109
+ }
110
+ };
111
+
68
112
  const MetadataTooltip = (props: { value: string; name: string }) => {
69
113
  const { value, name } = props;
70
114
  const { t } = useI18n("ui");
@@ -75,6 +119,7 @@ const MetadataTooltip = (props: { value: string; name: string }) => {
75
119
  <Text>{value}</Text>
76
120
  </div>
77
121
  {name === "tag" && <OpenFilterUrlButton url={getTagsFilterUrl([value])} />}
122
+ {name === "owner" && <OwnerAction ownerValue={value} />}
78
123
  <Button
79
124
  style={"outline"}
80
125
  icon={allureIcons.lineGeneralCopy3}
@@ -85,11 +130,40 @@ const MetadataTooltip = (props: { value: string; name: string }) => {
85
130
  );
86
131
  };
87
132
 
133
+ const MetaDataOwnerLabel: FunctionalComponent<{
134
+ value: string;
135
+ size?: "s" | "m";
136
+ }> = ({ value, size = "s" }) => {
137
+ const ownerAddress = parseOwnerAddress(value);
138
+ const displayName = ownerAddress.displayName ?? value;
139
+
140
+ return (
141
+ <Menu
142
+ size="xl"
143
+ menuTrigger={({ onClick }) => (
144
+ <div className={styles["report-metadata-keyvalue-wrapper"]}>
145
+ <Text type={"ui"} size={size} onClick={onClick} bold className={styles["report-metadata-keyvalue-value"]}>
146
+ {displayName}
147
+ </Text>
148
+ </div>
149
+ )}
150
+ >
151
+ <Menu.Section>
152
+ <MetadataTooltip value={value} name={"owner"} />
153
+ </Menu.Section>
154
+ </Menu>
155
+ );
156
+ };
157
+
88
158
  const MetaDataKeyLabel: FunctionalComponent<{
89
159
  name: string;
90
160
  size?: "s" | "m";
91
161
  value: string;
92
162
  }> = ({ name, size = "s", value }) => {
163
+ if (name === "owner") {
164
+ return <MetaDataOwnerLabel value={value} size={size} />;
165
+ }
166
+
93
167
  return (
94
168
  <Menu
95
169
  size="xl"
@@ -132,7 +206,7 @@ const MetadataKeyValue: FunctionalComponent<{
132
206
  </div>
133
207
  ) : (
134
208
  <div className={styles["report-metadata-values"]} data-testid={"metadata-item-value"}>
135
- <MetaDataKeyLabel value={value} name={title} />
209
+ <MetaDataKeyLabel value={value ?? ""} name={title} />
136
210
  </div>
137
211
  )}
138
212
  </div>
@@ -92,6 +92,16 @@
92
92
  white-space: nowrap;
93
93
  text-overflow: ellipsis;
94
94
  cursor: pointer;
95
+
96
+ &:is(a) {
97
+ color: inherit;
98
+ font-weight: bold;
99
+ text-decoration: none;
100
+
101
+ &:hover {
102
+ text-decoration: underline;
103
+ }
104
+ }
95
105
  }
96
106
 
97
107
  .report-metadata-list {
@@ -8,7 +8,7 @@
8
8
  top: 0;
9
9
  left: 0;
10
10
  background: var(--bg-base-primary);
11
- z-index: 1;
11
+ z-index: 10;
12
12
  }
13
13
 
14
14
  .headerRow {
@@ -0,0 +1,26 @@
1
+ import { Loadable, PageLoader } from "@allurereport/web-components";
2
+ import { CategoriesTree } from "@/components/Categories/CategoriesTree";
3
+ import { useI18n } from "@/stores";
4
+ import { categoriesStore } from "@/stores/categories";
5
+ import * as styles from "./styles.scss";
6
+
7
+ export const ReportCategories = () => {
8
+ const { t } = useI18n("empty");
9
+
10
+ return (
11
+ <Loadable
12
+ source={categoriesStore}
13
+ renderLoader={() => (
14
+ <div className={styles["report-categories-loader"]}>
15
+ <PageLoader />
16
+ </div>
17
+ )}
18
+ renderData={(store) => {
19
+ if (!categoriesStore.value.data?.roots?.length) {
20
+ return <div className={styles["report-categories-results-empty"]}>{t("no-categories-results")}</div>;
21
+ }
22
+ return <CategoriesTree store={store} />;
23
+ }}
24
+ />
25
+ );
26
+ };
@@ -0,0 +1,55 @@
1
+ @import "~@allurereport/web-components/mixins.scss";
2
+
3
+ .report-categories-results {
4
+ padding: 20px 0;
5
+
6
+ & > li + li {
7
+ margin-top: 12px;
8
+ }
9
+ }
10
+
11
+ .report-categories-results-empty {
12
+ display: flex;
13
+ padding: 48px 0;
14
+ width: 100%;
15
+ justify-content: center;
16
+ }
17
+
18
+ .report-categories-loader {
19
+ display: flex;
20
+ padding: 48px 0;
21
+ width: 100%;
22
+ justify-content: center;
23
+ }
24
+
25
+ .report-categories-result {
26
+ display: flex;
27
+ align-items: flex-start;
28
+ gap: 0 8px;
29
+
30
+ b {
31
+ font-weight: 700;
32
+ }
33
+ }
34
+
35
+ .report-categories-result-icon {
36
+ flex: 0 0 auto;
37
+ margin-top: 3px;
38
+ color: var(--bg-support-capella);
39
+ }
40
+
41
+ .report-categories-result-content {
42
+ flex: 1 1 auto;
43
+
44
+ & > * + * {
45
+ margin-top: 4px;
46
+ }
47
+ }
48
+
49
+ .report-categories-result-error {
50
+ margin-top: 8px;
51
+ }
52
+ .tree-item-category {
53
+ font-size: 24px;
54
+ margin-bottom: 16px;
55
+ }
@@ -0,0 +1,41 @@
1
+ import { computed } from "@preact/signals";
2
+ import { useI18n } from "@/stores";
3
+ import { treeCategories } from "@/stores/treeFilters/store";
4
+ import type { AwesomeArrayFieldFilter } from "../../stores/treeFilters/model";
5
+ import { ArrayFieldFilter } from "./BaseFilters";
6
+
7
+ const options = computed(() => {
8
+ return treeCategories.value.map((category) => ({ key: category, label: category }));
9
+ });
10
+
11
+ export const CategoriesFilter = (props: {
12
+ filter: AwesomeArrayFieldFilter;
13
+ onChange: (filter: AwesomeArrayFieldFilter) => void;
14
+ }) => {
15
+ const { filter, onChange } = props;
16
+ const { t } = useI18n("filters");
17
+
18
+ if (options.value.length === 0) {
19
+ return null;
20
+ }
21
+
22
+ return (
23
+ <ArrayFieldFilter
24
+ filter={filter}
25
+ onChange={onChange}
26
+ options={options.value}
27
+ label={t("categories")}
28
+ description={t("description.categories")}
29
+ counter
30
+ onClear={() =>
31
+ onChange({
32
+ ...filter,
33
+ value: {
34
+ ...filter.value,
35
+ value: [],
36
+ },
37
+ })
38
+ }
39
+ />
40
+ );
41
+ };
@@ -1,8 +1,15 @@
1
1
  import { For } from "@preact/signals/utils";
2
2
  import type { AwesomeFilter } from "@/stores/treeFilters/model";
3
3
  import { setTreeFilter, treeQuickFilters } from "@/stores/treeFilters/store";
4
- import { isFlakyFilter, isRetryFilter, isTagFilter, isTransitionFilter } from "@/stores/treeFilters/utils";
4
+ import {
5
+ isCategoryFilter,
6
+ isFlakyFilter,
7
+ isRetryFilter,
8
+ isTagFilter,
9
+ isTransitionFilter,
10
+ } from "@/stores/treeFilters/utils";
5
11
  import { BooleanFieldFilter } from "./BaseFilters";
12
+ import { CategoriesFilter } from "./CategoriesFilter";
6
13
  import { RetryFlakyFilter } from "./RetryFlaky";
7
14
  import { TagsFilter } from "./TagsFilter";
8
15
  import { TransitionFilter } from "./TransitionFilter";
@@ -28,6 +35,10 @@ const Filter = (props: { filter: AwesomeFilter; onChange: (filter: AwesomeFilter
28
35
  return <TagsFilter filter={filter} onChange={onChange} />;
29
36
  }
30
37
 
38
+ if (isCategoryFilter(filter)) {
39
+ return <CategoriesFilter filter={filter} onChange={onChange} />;
40
+ }
41
+
31
42
  return null;
32
43
  };
33
44