@backstage-community/plugin-code-coverage 0.2.28

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.
@@ -0,0 +1,622 @@
1
+ import React, { useState, useEffect, Fragment } from 'react';
2
+ import { useEntity, humanizeEntityRef, MissingAnnotationEmptyState } from '@backstage/plugin-catalog-react';
3
+ import Box from '@material-ui/core/Box';
4
+ import Card from '@material-ui/core/Card';
5
+ import CardContent from '@material-ui/core/CardContent';
6
+ import CardHeader from '@material-ui/core/CardHeader';
7
+ import Typography from '@material-ui/core/Typography';
8
+ import { makeStyles } from '@material-ui/core/styles';
9
+ import TrendingDownIcon from '@material-ui/icons/TrendingDown';
10
+ import TrendingFlatIcon from '@material-ui/icons/TrendingFlat';
11
+ import TrendingUpIcon from '@material-ui/icons/TrendingUp';
12
+ import Alert from '@material-ui/lab/Alert';
13
+ import useAsync from 'react-use/esm/useAsync';
14
+ import { ResponsiveContainer, LineChart, CartesianGrid, XAxis, YAxis, Tooltip, Legend, Line } from 'recharts';
15
+ import { stringifyEntityRef } from '@backstage/catalog-model';
16
+ import { ResponseError } from '@backstage/errors';
17
+ import { createApiRef, useApi, createRouteRef, createPlugin, createApiFactory, discoveryApiRef, fetchApiRef, createRoutableExtension } from '@backstage/core-plugin-api';
18
+ import { Progress, ResponseErrorPanel, Table, Page, Content, ContentHeader } from '@backstage/core-components';
19
+ import { DateTime } from 'luxon';
20
+ import Modal from '@material-ui/core/Modal';
21
+ import FolderIcon from '@material-ui/icons/Folder';
22
+ import FileOutlinedIcon from '@material-ui/icons/InsertDriveFileOutlined';
23
+ import Paper from '@material-ui/core/Paper';
24
+ import 'highlight.js/styles/mono-blue.css';
25
+ import { highlight } from 'highlight.js';
26
+
27
+ var __defProp = Object.defineProperty;
28
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
29
+ var __publicField = (obj, key, value) => {
30
+ __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
31
+ return value;
32
+ };
33
+ const codeCoverageApiRef = createApiRef({
34
+ id: "plugin.code-coverage.service"
35
+ });
36
+ class CodeCoverageRestApi {
37
+ constructor(options) {
38
+ __publicField(this, "discoveryApi");
39
+ __publicField(this, "fetchApi");
40
+ this.discoveryApi = options.discoveryApi;
41
+ this.fetchApi = options.fetchApi;
42
+ }
43
+ async fetch(path) {
44
+ var _a;
45
+ const url = await this.discoveryApi.getBaseUrl("code-coverage");
46
+ const resp = await this.fetchApi.fetch(`${url}${path}`);
47
+ if (!resp.ok) {
48
+ throw await ResponseError.fromResponse(resp);
49
+ }
50
+ if ((_a = resp.headers.get("content-type")) == null ? void 0 : _a.includes("application/json")) {
51
+ return await resp.json();
52
+ }
53
+ return await resp.text();
54
+ }
55
+ async getCoverageForEntity(entityName) {
56
+ const entity = encodeURIComponent(stringifyEntityRef(entityName));
57
+ return await this.fetch(
58
+ `/report?entity=${entity}`
59
+ );
60
+ }
61
+ async getFileContentFromEntity(entityName, filePath) {
62
+ const entity = encodeURIComponent(stringifyEntityRef(entityName));
63
+ return await this.fetch(
64
+ `/file-content?entity=${entity}&path=${encodeURI(filePath)}`
65
+ );
66
+ }
67
+ async getCoverageHistoryForEntity(entityName, limit) {
68
+ const entity = encodeURIComponent(stringifyEntityRef(entityName));
69
+ const hasValidLimit = limit && limit > 0;
70
+ return await this.fetch(
71
+ `/history?entity=${entity}${hasValidLimit ? `&limit=${encodeURIComponent(String(limit))}` : ""}`
72
+ );
73
+ }
74
+ }
75
+
76
+ const useStyles$3 = makeStyles((theme) => ({
77
+ trendDown: {
78
+ color: theme.palette.status.warning
79
+ },
80
+ trendUp: {
81
+ color: theme.palette.status.ok
82
+ }
83
+ }));
84
+ const getTrendIcon = (trend, classes) => {
85
+ switch (true) {
86
+ case trend > 0:
87
+ return /* @__PURE__ */ React.createElement(TrendingUpIcon, { className: classes.trendUp });
88
+ case trend < 0:
89
+ return /* @__PURE__ */ React.createElement(TrendingDownIcon, { className: classes.trendDown });
90
+ case trend === 0:
91
+ default:
92
+ return /* @__PURE__ */ React.createElement(TrendingFlatIcon, null);
93
+ }
94
+ };
95
+ function formatDateToHuman(timeStamp) {
96
+ return DateTime.fromMillis(Number(timeStamp)).toLocaleString(
97
+ DateTime.DATETIME_MED
98
+ );
99
+ }
100
+ const CoverageHistoryChart = () => {
101
+ const { entity } = useEntity();
102
+ const codeCoverageApi = useApi(codeCoverageApiRef);
103
+ const {
104
+ loading: loadingHistory,
105
+ error: errorHistory,
106
+ value: valueHistory
107
+ } = useAsync(
108
+ async () => await codeCoverageApi.getCoverageHistoryForEntity({
109
+ kind: entity.kind,
110
+ namespace: entity.metadata.namespace || "default",
111
+ name: entity.metadata.name
112
+ })
113
+ );
114
+ const classes = useStyles$3();
115
+ if (loadingHistory) {
116
+ return /* @__PURE__ */ React.createElement(Progress, null);
117
+ }
118
+ if (errorHistory) {
119
+ return /* @__PURE__ */ React.createElement(ResponseErrorPanel, { error: errorHistory });
120
+ } else if (!valueHistory) {
121
+ return /* @__PURE__ */ React.createElement(Alert, { severity: "warning" }, "No history found.");
122
+ }
123
+ if (!valueHistory.history.length) {
124
+ return /* @__PURE__ */ React.createElement(Card, null, /* @__PURE__ */ React.createElement(CardHeader, { title: "History" }), /* @__PURE__ */ React.createElement(CardContent, null, "No coverage history found"));
125
+ }
126
+ const [oldestCoverage] = valueHistory.history.slice(-1);
127
+ const latestCoverage = valueHistory.history[0];
128
+ const getTrendForCoverage = (type) => {
129
+ if (!oldestCoverage[type].percentage) {
130
+ return 0;
131
+ }
132
+ return (latestCoverage[type].percentage - oldestCoverage[type].percentage) / oldestCoverage[type].percentage * 100;
133
+ };
134
+ const lineTrend = getTrendForCoverage("line");
135
+ const branchTrend = getTrendForCoverage("branch");
136
+ return /* @__PURE__ */ React.createElement(Card, null, /* @__PURE__ */ React.createElement(CardHeader, { title: "History" }), /* @__PURE__ */ React.createElement(CardContent, null, /* @__PURE__ */ React.createElement(Box, { px: 6, display: "flex" }, /* @__PURE__ */ React.createElement(Box, { display: "flex", mr: 4 }, getTrendIcon(lineTrend, classes), /* @__PURE__ */ React.createElement(Typography, null, "Current line: ", latestCoverage.line.percentage, "%", /* @__PURE__ */ React.createElement("br", null), "(", Math.floor(lineTrend), "% change over ", valueHistory.history.length, " ", "builds)")), /* @__PURE__ */ React.createElement(Box, { display: "flex" }, getTrendIcon(branchTrend, classes), /* @__PURE__ */ React.createElement(Typography, null, "Current branch: ", latestCoverage.branch.percentage, "%", /* @__PURE__ */ React.createElement("br", null), "(", Math.floor(branchTrend), "% change over", " ", valueHistory.history.length, " builds)"))), /* @__PURE__ */ React.createElement(ResponsiveContainer, { width: "100%", height: 300 }, /* @__PURE__ */ React.createElement(
137
+ LineChart,
138
+ {
139
+ data: valueHistory.history,
140
+ margin: { right: 48, top: 32 }
141
+ },
142
+ /* @__PURE__ */ React.createElement(CartesianGrid, { strokeDasharray: "3 3" }),
143
+ /* @__PURE__ */ React.createElement(
144
+ XAxis,
145
+ {
146
+ dataKey: "timestamp",
147
+ tickFormatter: formatDateToHuman,
148
+ reversed: true
149
+ }
150
+ ),
151
+ /* @__PURE__ */ React.createElement(YAxis, { dataKey: "line.percentage" }),
152
+ /* @__PURE__ */ React.createElement(YAxis, { dataKey: "branch.percentage" }),
153
+ /* @__PURE__ */ React.createElement(Tooltip, { labelFormatter: formatDateToHuman }),
154
+ /* @__PURE__ */ React.createElement(Legend, null),
155
+ /* @__PURE__ */ React.createElement(
156
+ Line,
157
+ {
158
+ type: "monotone",
159
+ dataKey: "branch.percentage",
160
+ stroke: "#8884d8"
161
+ }
162
+ ),
163
+ /* @__PURE__ */ React.createElement(Line, { type: "monotone", dataKey: "line.percentage", stroke: "#82ca9d" })
164
+ ))));
165
+ };
166
+
167
+ const useStyles$2 = makeStyles((theme) => ({
168
+ lineNumberCell: {
169
+ color: `${theme.palette.grey[500]}`,
170
+ fontSize: "90%",
171
+ borderRight: `1px solid ${theme.palette.grey[500]}`,
172
+ paddingRight: theme.spacing(1),
173
+ textAlign: "right"
174
+ },
175
+ hitCountCell: {
176
+ width: "50px",
177
+ borderRight: `1px solid ${theme.palette.grey[500]}`,
178
+ textAlign: "center",
179
+ color: theme.palette.common.white,
180
+ // need to enforce this color since it needs to stand out against colored background
181
+ paddingLeft: theme.spacing(1),
182
+ paddingRight: theme.spacing(1)
183
+ },
184
+ countRoundedRectangle: {
185
+ borderRadius: "45px",
186
+ fontSize: "90%",
187
+ padding: "1px 3px 1px 3px",
188
+ width: "50px"
189
+ },
190
+ hitCountRoundedRectangle: {
191
+ backgroundColor: `${theme.palette.success.main}`,
192
+ color: `${theme.palette.success.contrastText}`
193
+ },
194
+ notHitCountRoundedRectangle: {
195
+ backgroundColor: `${theme.palette.error.main}`,
196
+ color: `${theme.palette.error.contrastText}`
197
+ },
198
+ codeLine: {
199
+ paddingLeft: `${theme.spacing(1)}`,
200
+ whiteSpace: "pre",
201
+ fontSize: "90%"
202
+ },
203
+ hitCodeLine: {
204
+ backgroundColor: `${theme.palette.success.main}`,
205
+ color: `${theme.palette.success.contrastText}`
206
+ },
207
+ notHitCodeLine: {
208
+ backgroundColor: `${theme.palette.error.main}`,
209
+ color: `${theme.palette.error.contrastText}`
210
+ }
211
+ }));
212
+ const CodeRow = ({
213
+ lineNumber,
214
+ lineContent,
215
+ lineHits = null
216
+ }) => {
217
+ const classes = useStyles$2();
218
+ const hitCountRoundedRectangleClass = [classes.countRoundedRectangle];
219
+ const lineContentClass = [classes.codeLine];
220
+ let hitRoundedRectangle = null;
221
+ if (lineHits !== null) {
222
+ if (lineHits > 0) {
223
+ hitCountRoundedRectangleClass.push(classes.hitCountRoundedRectangle);
224
+ lineContentClass.push(classes.hitCodeLine);
225
+ } else {
226
+ hitCountRoundedRectangleClass.push(classes.notHitCountRoundedRectangle);
227
+ lineContentClass.push(classes.notHitCodeLine);
228
+ }
229
+ hitRoundedRectangle = /* @__PURE__ */ React.createElement("div", { className: hitCountRoundedRectangleClass.join(" ") }, lineHits);
230
+ }
231
+ return /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", { className: classes.lineNumberCell }, lineNumber), /* @__PURE__ */ React.createElement("td", { className: classes.hitCountCell }, hitRoundedRectangle), /* @__PURE__ */ React.createElement(
232
+ "td",
233
+ {
234
+ className: lineContentClass.join(" "),
235
+ dangerouslySetInnerHTML: { __html: lineContent }
236
+ }
237
+ ));
238
+ };
239
+
240
+ const highlightLines = (fileExtension, lines) => {
241
+ const formattedLines = [];
242
+ let fileformat = fileExtension;
243
+ if (fileExtension === "m") {
244
+ fileformat = "objectivec";
245
+ }
246
+ if (fileExtension === "tsx") {
247
+ fileformat = "typescript";
248
+ }
249
+ if (fileExtension === "jsx") {
250
+ fileformat = "javascript";
251
+ }
252
+ if (fileExtension === "kt") {
253
+ fileformat = "kotlin";
254
+ }
255
+ lines.forEach((line) => {
256
+ const result = highlight(line, {
257
+ language: fileformat,
258
+ ignoreIllegals: true
259
+ });
260
+ formattedLines.push(result.value);
261
+ });
262
+ return formattedLines;
263
+ };
264
+
265
+ const useStyles$1 = makeStyles((theme) => ({
266
+ paper: {
267
+ margin: "auto",
268
+ top: "2em",
269
+ width: "80%",
270
+ border: `2px solid ${theme.palette.common.black}`,
271
+ boxShadow: theme.shadows[5],
272
+ padding: theme.spacing(2, 4, 3),
273
+ overflow: "scroll"
274
+ },
275
+ coverageFileViewTable: {
276
+ borderSpacing: "0px",
277
+ width: "80%",
278
+ marginTop: theme.spacing(2)
279
+ }
280
+ }));
281
+ const FormattedLines = ({
282
+ highlightedLines,
283
+ lineHits
284
+ }) => {
285
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, highlightedLines.map((lineContent, idx) => {
286
+ const line = idx + 1;
287
+ return /* @__PURE__ */ React.createElement(
288
+ CodeRow,
289
+ {
290
+ key: line,
291
+ lineNumber: line,
292
+ lineContent,
293
+ lineHits: lineHits[line]
294
+ }
295
+ );
296
+ }));
297
+ };
298
+ const FileContent = ({ filename, coverage }) => {
299
+ const { entity } = useEntity();
300
+ const codeCoverageApi = useApi(codeCoverageApiRef);
301
+ const { loading, error, value } = useAsync(
302
+ async () => await codeCoverageApi.getFileContentFromEntity(
303
+ {
304
+ kind: entity.kind,
305
+ namespace: entity.metadata.namespace || "default",
306
+ name: entity.metadata.name
307
+ },
308
+ filename
309
+ ),
310
+ [entity]
311
+ );
312
+ const classes = useStyles$1();
313
+ if (loading) {
314
+ return /* @__PURE__ */ React.createElement(Progress, null);
315
+ }
316
+ if (error) {
317
+ return /* @__PURE__ */ React.createElement(ResponseErrorPanel, { error });
318
+ }
319
+ if (!value) {
320
+ return /* @__PURE__ */ React.createElement(Alert, { severity: "warning" }, "Unable to retrieve file content for ", filename);
321
+ }
322
+ const [language] = filename.split(".").slice(-1);
323
+ const highlightedLines = highlightLines(language, value.split("\n"));
324
+ const lineHits = Object.entries(coverage.lineHits).reduce(
325
+ (acc, next) => {
326
+ acc[next[0]] = next[1];
327
+ return acc;
328
+ },
329
+ {}
330
+ );
331
+ return /* @__PURE__ */ React.createElement(Paper, { variant: "outlined", className: classes.paper }, /* @__PURE__ */ React.createElement("table", { className: classes.coverageFileViewTable }, /* @__PURE__ */ React.createElement("tbody", null, /* @__PURE__ */ React.createElement(
332
+ FormattedLines,
333
+ {
334
+ highlightedLines,
335
+ lineHits
336
+ }
337
+ ))));
338
+ };
339
+
340
+ const useStyles = makeStyles((theme) => ({
341
+ container: {
342
+ marginTop: theme.spacing(2)
343
+ },
344
+ icon: {
345
+ marginRight: theme.spacing(1)
346
+ },
347
+ link: {
348
+ color: theme.palette.primary.main,
349
+ cursor: "pointer"
350
+ }
351
+ }));
352
+ const groupByPath = (files) => {
353
+ const acc = {};
354
+ files.forEach((file) => {
355
+ const filename = file.filename;
356
+ if (!file.filename)
357
+ return;
358
+ const pathArray = filename == null ? void 0 : filename.split("/").filter((el) => el !== "");
359
+ if (pathArray) {
360
+ if (!acc.hasOwnProperty(pathArray[0])) {
361
+ acc[pathArray[0]] = [];
362
+ }
363
+ acc[pathArray[0]].push(file);
364
+ }
365
+ });
366
+ return acc;
367
+ };
368
+ const removeVisitedPathGroup = (files, pathGroup) => {
369
+ return files.map((file) => {
370
+ var _a;
371
+ return {
372
+ ...file,
373
+ filename: file.filename ? file.filename.substring(
374
+ ((_a = file.filename) == null ? void 0 : _a.indexOf(pathGroup)) + pathGroup.length + 1
375
+ ) : file.filename
376
+ };
377
+ });
378
+ };
379
+ const buildFileStructure = (row) => {
380
+ const dataGroupedByPath = groupByPath(row.files);
381
+ row.files = Object.keys(dataGroupedByPath).map((pathGroup) => {
382
+ return buildFileStructure({
383
+ path: pathGroup,
384
+ files: dataGroupedByPath.hasOwnProperty("files") ? removeVisitedPathGroup(dataGroupedByPath.files, pathGroup) : removeVisitedPathGroup(dataGroupedByPath[pathGroup], pathGroup),
385
+ coverage: dataGroupedByPath[pathGroup].reduce(
386
+ (acc, cur) => acc + cur.coverage,
387
+ 0
388
+ ) / dataGroupedByPath[pathGroup].length,
389
+ missing: dataGroupedByPath[pathGroup].reduce(
390
+ (acc, cur) => acc + cur.missing,
391
+ 0
392
+ ),
393
+ tracked: dataGroupedByPath[pathGroup].reduce(
394
+ (acc, cur) => acc + cur.tracked,
395
+ 0
396
+ )
397
+ });
398
+ });
399
+ return row;
400
+ };
401
+ const formatInitialData = (value) => {
402
+ return buildFileStructure({
403
+ path: "",
404
+ coverage: value.aggregate.line.percentage,
405
+ missing: value.aggregate.line.missed,
406
+ tracked: value.aggregate.line.available,
407
+ files: value.files.map((fc) => {
408
+ return {
409
+ path: "",
410
+ filename: fc.filename,
411
+ coverage: Math.floor(
412
+ Object.values(fc.lineHits).filter((hits) => hits > 0).length / Object.values(fc.lineHits).length * 100
413
+ ),
414
+ missing: Object.values(fc.lineHits).filter((hits) => !hits).length,
415
+ tracked: Object.values(fc.lineHits).length
416
+ };
417
+ })
418
+ });
419
+ };
420
+ const getObjectsAtPath = (curData, path) => {
421
+ var _a;
422
+ let data = curData == null ? void 0 : curData.files;
423
+ for (const fragment of path) {
424
+ data = (_a = data == null ? void 0 : data.find((d) => d.path === fragment)) == null ? void 0 : _a.files;
425
+ }
426
+ return data;
427
+ };
428
+ const FileExplorer = () => {
429
+ const styles = useStyles();
430
+ const { entity } = useEntity();
431
+ const [curData, setCurData] = useState();
432
+ const [tableData, setTableData] = useState();
433
+ const [curPath, setCurPath] = useState("");
434
+ const [modalOpen, setModalOpen] = useState(false);
435
+ const [curFile, setCurFile] = useState("");
436
+ const codeCoverageApi = useApi(codeCoverageApiRef);
437
+ const { loading, error, value } = useAsync(
438
+ async () => await codeCoverageApi.getCoverageForEntity({
439
+ kind: entity.kind,
440
+ namespace: entity.metadata.namespace || "default",
441
+ name: entity.metadata.name
442
+ })
443
+ );
444
+ useEffect(() => {
445
+ if (!value)
446
+ return;
447
+ const data = formatInitialData(value);
448
+ setCurData(data);
449
+ if (data.files)
450
+ setTableData(data.files);
451
+ }, [value]);
452
+ if (loading) {
453
+ return /* @__PURE__ */ React.createElement(Progress, null);
454
+ } else if (error) {
455
+ return /* @__PURE__ */ React.createElement(ResponseErrorPanel, { error });
456
+ }
457
+ if (!value) {
458
+ return /* @__PURE__ */ React.createElement(Alert, { severity: "warning" }, "No code coverage found for ", humanizeEntityRef(entity));
459
+ }
460
+ const moveDownIntoPath = (path) => {
461
+ const nextPathData = tableData.find(
462
+ (d) => d.path === path
463
+ );
464
+ if (nextPathData && nextPathData.files) {
465
+ setTableData(nextPathData.files);
466
+ }
467
+ };
468
+ const moveUpIntoPath = (idx) => {
469
+ const path = curPath.split("/").slice(0, idx + 1);
470
+ setCurFile("");
471
+ setCurPath(path.join("/"));
472
+ setTableData(getObjectsAtPath(curData, path.slice(1)));
473
+ };
474
+ const columns = [
475
+ {
476
+ title: "Path",
477
+ type: "string",
478
+ field: "path",
479
+ render: (row) => {
480
+ var _a, _b;
481
+ return /* @__PURE__ */ React.createElement(
482
+ Box,
483
+ {
484
+ display: "flex",
485
+ alignItems: "center",
486
+ role: "button",
487
+ tabIndex: row.tableData.id,
488
+ className: styles.link,
489
+ onClick: () => {
490
+ var _a2;
491
+ if ((_a2 = row.files) == null ? void 0 : _a2.length) {
492
+ setCurPath(`${curPath}/${row.path}`);
493
+ moveDownIntoPath(row.path);
494
+ } else {
495
+ setCurFile(`${curPath.slice(1)}/${row.path}`);
496
+ setModalOpen(true);
497
+ }
498
+ }
499
+ },
500
+ ((_a = row.files) == null ? void 0 : _a.length) > 0 && /* @__PURE__ */ React.createElement(FolderIcon, { fontSize: "small", className: styles.icon }),
501
+ ((_b = row.files) == null ? void 0 : _b.length) === 0 && /* @__PURE__ */ React.createElement(FileOutlinedIcon, { fontSize: "small", className: styles.icon }),
502
+ row.path
503
+ );
504
+ }
505
+ },
506
+ {
507
+ title: "Coverage",
508
+ type: "numeric",
509
+ field: "coverage",
510
+ render: (row) => `${row.coverage.toFixed(2)}%`
511
+ },
512
+ {
513
+ title: "Missing lines",
514
+ type: "numeric",
515
+ field: "missing"
516
+ },
517
+ {
518
+ title: "Tracked lines",
519
+ type: "numeric",
520
+ field: "tracked"
521
+ }
522
+ ];
523
+ const pathArray = curPath.split("/");
524
+ const lastPathElementIndex = pathArray.length - 1;
525
+ const fileCoverage = value.files.find(
526
+ (f) => f.filename.endsWith(curFile)
527
+ );
528
+ if (!fileCoverage) {
529
+ return null;
530
+ }
531
+ return /* @__PURE__ */ React.createElement(Box, { className: styles.container }, /* @__PURE__ */ React.createElement(
532
+ Table,
533
+ {
534
+ emptyContent: /* @__PURE__ */ React.createElement(React.Fragment, null, "No files found"),
535
+ data: tableData || [],
536
+ columns,
537
+ title: /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Box, null, "Explore Files"), /* @__PURE__ */ React.createElement(
538
+ Box,
539
+ {
540
+ mt: 1,
541
+ style: {
542
+ fontSize: "0.875rem",
543
+ fontWeight: "normal",
544
+ display: "flex"
545
+ }
546
+ },
547
+ pathArray.map((pathElement, idx) => /* @__PURE__ */ React.createElement(Fragment, { key: pathElement || "root" }, /* @__PURE__ */ React.createElement(
548
+ "div",
549
+ {
550
+ role: "button",
551
+ tabIndex: idx,
552
+ className: idx !== lastPathElementIndex ? styles.link : void 0,
553
+ onKeyDown: () => moveUpIntoPath(idx),
554
+ onClick: () => moveUpIntoPath(idx)
555
+ },
556
+ pathElement || "root"
557
+ ), idx !== lastPathElementIndex && /* @__PURE__ */ React.createElement("div", null, "\xA0/\xA0")))
558
+ ))
559
+ }
560
+ ), /* @__PURE__ */ React.createElement(
561
+ Modal,
562
+ {
563
+ open: modalOpen,
564
+ onClick: (event) => event.stopPropagation(),
565
+ onClose: () => setModalOpen(false),
566
+ style: { overflow: "scroll" }
567
+ },
568
+ /* @__PURE__ */ React.createElement(FileContent, { filename: curFile, coverage: fileCoverage })
569
+ ));
570
+ };
571
+
572
+ const CodeCoveragePage = () => {
573
+ return /* @__PURE__ */ React.createElement(Page, { themeId: "tool" }, /* @__PURE__ */ React.createElement(Content, null, /* @__PURE__ */ React.createElement(ContentHeader, { title: "Code coverage" }), /* @__PURE__ */ React.createElement(CoverageHistoryChart, null), /* @__PURE__ */ React.createElement(FileExplorer, null)));
574
+ };
575
+
576
+ function isCodeCoverageAvailable(entity) {
577
+ var _a;
578
+ return Boolean((_a = entity.metadata.annotations) == null ? void 0 : _a["backstage.io/code-coverage"]);
579
+ }
580
+ const Router = () => {
581
+ const { entity } = useEntity();
582
+ if (!isCodeCoverageAvailable(entity)) {
583
+ return /* @__PURE__ */ React.createElement(MissingAnnotationEmptyState, { annotation: "backstage.io/code-coverage" });
584
+ }
585
+ return /* @__PURE__ */ React.createElement(CodeCoveragePage, null);
586
+ };
587
+
588
+ var Router$1 = /*#__PURE__*/Object.freeze({
589
+ __proto__: null,
590
+ Router: Router,
591
+ isCodeCoverageAvailable: isCodeCoverageAvailable
592
+ });
593
+
594
+ const rootRouteRef = createRouteRef({
595
+ id: "code-coverage"
596
+ });
597
+
598
+ const codeCoveragePlugin = createPlugin({
599
+ id: "code-coverage",
600
+ routes: {
601
+ root: rootRouteRef
602
+ },
603
+ apis: [
604
+ createApiFactory({
605
+ api: codeCoverageApiRef,
606
+ deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },
607
+ factory: ({ discoveryApi, fetchApi }) => new CodeCoverageRestApi({ discoveryApi, fetchApi })
608
+ })
609
+ ]
610
+ });
611
+ const EntityCodeCoverageContent = codeCoveragePlugin.provide(
612
+ createRoutableExtension({
613
+ name: "EntityCodeCoverageContent",
614
+ component: () => Promise.resolve().then(function () { return Router$1; }).then((m) => m.Router),
615
+ mountPoint: rootRouteRef
616
+ })
617
+ );
618
+
619
+ const isPluginApplicableToEntity = isCodeCoverageAvailable;
620
+
621
+ export { EntityCodeCoverageContent, codeCoveragePlugin, isCodeCoverageAvailable, isPluginApplicableToEntity };
622
+ //# sourceMappingURL=index.esm.js.map