@backstage-community/plugin-xcmetrics 0.2.53 → 0.2.55

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 (57) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{esm/index-DfJWmS4I.esm.js → api/XcmetricsClient.esm.js} +7 -50
  3. package/dist/api/XcmetricsClient.esm.js.map +1 -0
  4. package/dist/api/types.esm.js +8 -0
  5. package/dist/api/types.esm.js.map +1 -0
  6. package/dist/components/Accordion/Accordion.esm.js +43 -0
  7. package/dist/components/Accordion/Accordion.esm.js.map +1 -0
  8. package/dist/components/BuildDetails/BuildDetails.esm.js +139 -0
  9. package/dist/components/BuildDetails/BuildDetails.esm.js.map +1 -0
  10. package/dist/components/BuildList/BuildList.esm.js +73 -0
  11. package/dist/components/BuildList/BuildList.esm.js.map +1 -0
  12. package/dist/components/BuildListFilter/BuildListFilter.esm.js +124 -0
  13. package/dist/components/BuildListFilter/BuildListFilter.esm.js.map +1 -0
  14. package/dist/components/BuildTableColumns.esm.js +61 -0
  15. package/dist/components/BuildTableColumns.esm.js.map +1 -0
  16. package/dist/components/BuildTimeline/BuildTimeline.esm.js +67 -0
  17. package/dist/components/BuildTimeline/BuildTimeline.esm.js.map +1 -0
  18. package/dist/components/DataValue/DataValue.esm.js +11 -0
  19. package/dist/components/DataValue/DataValue.esm.js.map +1 -0
  20. package/dist/components/DatePicker/DatePicker.esm.js +59 -0
  21. package/dist/components/DatePicker/DatePicker.esm.js.map +1 -0
  22. package/dist/components/Overview/Overview.esm.js +53 -0
  23. package/dist/components/Overview/Overview.esm.js.map +1 -0
  24. package/dist/components/OverviewTrends/OverviewTrends.esm.js +132 -0
  25. package/dist/components/OverviewTrends/OverviewTrends.esm.js.map +1 -0
  26. package/dist/components/PreformattedText/PreformattedText.esm.js +81 -0
  27. package/dist/components/PreformattedText/PreformattedText.esm.js.map +1 -0
  28. package/dist/components/StatusCell/StatusCell.esm.js +79 -0
  29. package/dist/components/StatusCell/StatusCell.esm.js.map +1 -0
  30. package/dist/components/StatusIcon/StatusIcon.esm.js +12 -0
  31. package/dist/components/StatusIcon/StatusIcon.esm.js.map +1 -0
  32. package/dist/components/StatusMatrix/StatusMatrix.esm.js +68 -0
  33. package/dist/components/StatusMatrix/StatusMatrix.esm.js.map +1 -0
  34. package/dist/components/Trend/Trend.esm.js +20 -0
  35. package/dist/components/Trend/Trend.esm.js.map +1 -0
  36. package/dist/components/XcmetricsLayout/XcmetricsLayout.esm.js +22 -0
  37. package/dist/components/XcmetricsLayout/XcmetricsLayout.esm.js.map +1 -0
  38. package/dist/components/XcmetricsLayout/index.esm.js +2 -0
  39. package/dist/components/XcmetricsLayout/index.esm.js.map +1 -0
  40. package/dist/index.esm.js +1 -4
  41. package/dist/index.esm.js.map +1 -1
  42. package/dist/plugin.esm.js +33 -0
  43. package/dist/plugin.esm.js.map +1 -0
  44. package/dist/routes.esm.js +13 -0
  45. package/dist/routes.esm.js.map +1 -0
  46. package/dist/utils/array.esm.js +12 -0
  47. package/dist/utils/array.esm.js.map +1 -0
  48. package/dist/utils/buildData.esm.js +21 -0
  49. package/dist/utils/buildData.esm.js.map +1 -0
  50. package/dist/utils/classnames.esm.js +5 -0
  51. package/dist/utils/classnames.esm.js.map +1 -0
  52. package/dist/utils/format.esm.js +30 -0
  53. package/dist/utils/format.esm.js.map +1 -0
  54. package/package.json +11 -7
  55. package/dist/esm/index-Dehp5p72.esm.js +0 -966
  56. package/dist/esm/index-Dehp5p72.esm.js.map +0 -1
  57. package/dist/esm/index-DfJWmS4I.esm.js.map +0 -1
@@ -1,966 +0,0 @@
1
- import React, { useState, useEffect, useRef } from 'react';
2
- import { Progress, TrendLine, Select, StatusAborted, StatusOK, StatusError, StatusWarning, EmptyState, ContentHeader, SupportButton, Table, InfoCard, StructuredMetadataTable, Page, Header, HeaderLabel, TabbedLayout, Content } from '@backstage/core-components';
3
- import { useApi } from '@backstage/core-plugin-api';
4
- import { x as xcmetricsApiRef, b as buildsRouteRef } from './index-DfJWmS4I.esm.js';
5
- import '@backstage/errors';
6
- import { Duration, DateTime } from 'luxon';
7
- import useAsync from 'react-use/esm/useAsync';
8
- import Alert from '@material-ui/lab/Alert';
9
- import { makeStyles, useTheme, createStyles } from '@material-ui/core/styles';
10
- import useMeasure from 'react-use/esm/useMeasure';
11
- import upperFirst from 'lodash/upperFirst';
12
- import Tooltip from '@material-ui/core/Tooltip';
13
- import Grid from '@material-ui/core/Grid';
14
- import Typography from '@material-ui/core/Typography';
15
- import AlertTitle from '@material-ui/lab/AlertTitle';
16
- import Chip from '@material-ui/core/Chip';
17
- import IconButton from '@material-ui/core/IconButton';
18
- import Button from '@material-ui/core/Button';
19
- import FilterList from '@material-ui/icons/FilterList';
20
- import withStyles from '@material-ui/core/styles/withStyles';
21
- import InputBase from '@material-ui/core/InputBase/InputBase';
22
- import makeStyles$1 from '@material-ui/core/styles/makeStyles';
23
- import Typography$1 from '@material-ui/core/Typography/Typography';
24
- import Divider from '@material-ui/core/Divider';
25
- import MuiAccordion from '@material-ui/core/Accordion';
26
- import MuiAccordionSummary from '@material-ui/core/AccordionSummary';
27
- import AccordionDetails from '@material-ui/core/AccordionDetails';
28
- import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
29
- import { ResponsiveContainer, BarChart, CartesianGrid, XAxis, YAxis, Tooltip as Tooltip$1, Legend, Bar } from 'recharts';
30
- import Dialog from '@material-ui/core/Dialog';
31
- import DialogActions from '@material-ui/core/DialogActions';
32
- import DialogContent from '@material-ui/core/DialogContent';
33
- import DialogTitle from '@material-ui/core/DialogTitle';
34
- import CloseIcon from '@material-ui/icons/Close';
35
-
36
- const sumField = (field, arr) => {
37
- return arr == null ? void 0 : arr.reduce((sum, current) => sum + field(current), 0);
38
- };
39
- const getValues = (field, arr) => {
40
- if (!(arr == null ? void 0 : arr.length)) {
41
- return void 0;
42
- }
43
- return arr.map((element) => field(element));
44
- };
45
-
46
- const formatDuration = (seconds) => {
47
- var _a;
48
- const duration = Duration.fromObject({
49
- seconds
50
- }).shiftTo("hours", "minutes", "seconds", "milliseconds");
51
- if (duration.hours + duration.minutes + duration.seconds === 0) {
52
- return `${Math.round(duration.milliseconds)} ms`;
53
- }
54
- const h = duration.hours ? `${duration.hours} h` : "";
55
- const m = duration.minutes ? `${duration.minutes} m` : "";
56
- const s = duration.hours < 12 ? `${(_a = duration.seconds) != null ? _a : 0} s` : "";
57
- return `${h} ${m} ${s}`;
58
- };
59
- const formatSecondsInterval = ([start, end]) => {
60
- return `${Math.round(start * 100) / 100} s - ${Math.round(end * 100) / 100} s`;
61
- };
62
- const formatTime = (timestamp) => {
63
- return DateTime.fromISO(timestamp).toLocaleString(
64
- DateTime.DATETIME_SHORT_WITH_SECONDS
65
- );
66
- };
67
- const formatPercentage = (number) => {
68
- return `${Math.round(number * 100)} %`;
69
- };
70
- const formatStatus = (status) => upperFirst(status);
71
-
72
- const getErrorRatios = (buildCounts) => {
73
- if (!(buildCounts == null ? void 0 : buildCounts.length)) {
74
- return void 0;
75
- }
76
- return buildCounts.map(
77
- (counts) => counts.builds === 0 ? 0 : counts.errors / counts.builds
78
- );
79
- };
80
- const getAverageDuration = (buildTimes, field) => {
81
- if (!(buildTimes == null ? void 0 : buildTimes.length)) {
82
- return void 0;
83
- }
84
- return formatDuration(
85
- buildTimes.reduce((sum, current) => sum + field(current), 0) / buildTimes.length
86
- );
87
- };
88
-
89
- const classNames = (...args) => args.filter((c) => !!c).join(" ");
90
- const cn = classNames;
91
-
92
- const TooltipContent = ({ buildId }) => {
93
- const client = useApi(xcmetricsApiRef);
94
- const { value, loading, error } = useAsync(
95
- async () => client.getBuild(buildId),
96
- []
97
- );
98
- if (error) {
99
- return /* @__PURE__ */ React.createElement("div", null, error.message);
100
- }
101
- if (loading || !(value == null ? void 0 : value.build)) {
102
- return /* @__PURE__ */ React.createElement(Progress, { style: { width: 100 } });
103
- }
104
- return /* @__PURE__ */ React.createElement("table", null, /* @__PURE__ */ React.createElement("tbody", null, /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", null, "Started"), /* @__PURE__ */ React.createElement("td", null, new Date(value.build.startTimestamp).toLocaleString())), /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", null, "Duration"), /* @__PURE__ */ React.createElement("td", null, formatDuration(value.build.duration))), /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", null, "Status"), /* @__PURE__ */ React.createElement("td", null, formatStatus(value.build.buildStatus)))));
105
- };
106
- const useStyles$9 = makeStyles((theme) => ({
107
- root: {
108
- width: ({ size }) => size,
109
- height: ({ size }) => size,
110
- marginRight: ({ spacing }) => spacing,
111
- marginBottom: ({ spacing }) => spacing,
112
- backgroundColor: theme.palette.grey[600],
113
- "&:hover": {
114
- transform: "scale(1.2)"
115
- }
116
- },
117
- ...{
118
- succeeded: {
119
- backgroundColor: theme.palette.type === "light" ? theme.palette.success.light : theme.palette.success.main
120
- }
121
- },
122
- // Make sure that key matches a status
123
- ...{
124
- failed: {
125
- backgroundColor: theme.palette.error[theme.palette.type]
126
- }
127
- },
128
- ...{
129
- stopped: {
130
- backgroundColor: theme.palette.warning[theme.palette.type]
131
- }
132
- }
133
- }));
134
- const StatusCell = (props) => {
135
- const classes = useStyles$9(props);
136
- const { buildStatus } = props;
137
- if (!buildStatus) {
138
- return /* @__PURE__ */ React.createElement("div", { className: classes.root });
139
- }
140
- return /* @__PURE__ */ React.createElement(
141
- Tooltip,
142
- {
143
- title: /* @__PURE__ */ React.createElement(TooltipContent, { buildId: buildStatus.id }),
144
- enterNextDelay: 500,
145
- arrow: true
146
- },
147
- /* @__PURE__ */ React.createElement(
148
- "div",
149
- {
150
- "data-testid": buildStatus.id,
151
- className: cn(classes.root, classes[buildStatus.buildStatus])
152
- }
153
- )
154
- );
155
- };
156
-
157
- const CELL_SIZE = 12;
158
- const CELL_MARGIN = 4;
159
- const MAX_ROWS = 4;
160
- const useStyles$8 = makeStyles((theme) => ({
161
- root: {
162
- marginTop: 8,
163
- display: "flex",
164
- flexWrap: "wrap",
165
- width: "100%"
166
- },
167
- loading: {
168
- animation: `$loadingOpacity 900ms ${theme.transitions.easing.easeInOut}`,
169
- animationIterationCount: "infinite"
170
- },
171
- "@keyframes loadingOpacity": {
172
- "0%": { opacity: 0.3 },
173
- "100%": { opacity: 0.8 }
174
- }
175
- }));
176
- const StatusMatrix = () => {
177
- const classes = useStyles$8();
178
- const [measureRef, { width: rootWidth }] = useMeasure();
179
- const client = useApi(xcmetricsApiRef);
180
- const {
181
- value: builds,
182
- loading,
183
- error
184
- } = useAsync(async () => client.getBuildStatuses(300), []);
185
- if (error) {
186
- return /* @__PURE__ */ React.createElement(Alert, { severity: "error" }, error.message);
187
- }
188
- const cols = Math.trunc(rootWidth / (CELL_SIZE + CELL_MARGIN)) || 1;
189
- return /* @__PURE__ */ React.createElement(
190
- "div",
191
- {
192
- className: cn(classes.root, loading && classes.loading),
193
- ref: measureRef
194
- },
195
- loading && [...new Array(cols * MAX_ROWS)].map((_, index) => {
196
- return /* @__PURE__ */ React.createElement(StatusCell, { key: index, size: CELL_SIZE, spacing: CELL_MARGIN });
197
- }),
198
- builds && builds.slice(0, cols * MAX_ROWS).map((buildStatus, index) => /* @__PURE__ */ React.createElement(
199
- StatusCell,
200
- {
201
- key: index,
202
- buildStatus,
203
- size: CELL_SIZE,
204
- spacing: CELL_MARGIN
205
- }
206
- ))
207
- );
208
- };
209
-
210
- const Trend = ({ data, title, color }) => {
211
- const emptyData = [0, 0];
212
- const max = Math.max(...data != null ? data : emptyData);
213
- return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Typography, { variant: "overline" }, title), /* @__PURE__ */ React.createElement(
214
- TrendLine,
215
- {
216
- data: data != null ? data : emptyData,
217
- title,
218
- max,
219
- color: data && color
220
- }
221
- ));
222
- };
223
-
224
- const DataValue = ({ field, value }) => {
225
- return /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(Typography, { variant: "caption" }, field), /* @__PURE__ */ React.createElement(Typography, { variant: "subtitle1" }, value != null ? value : "--"));
226
- };
227
- const DataValueGridItem = (props) => {
228
- var _a, _b, _c;
229
- return /* @__PURE__ */ React.createElement(Grid, { item: true, xs: (_a = props.xs) != null ? _a : 6, md: (_b = props.md) != null ? _b : 6, lg: (_c = props.lg) != null ? _c : 4 }, /* @__PURE__ */ React.createElement(DataValue, { ...props }));
230
- };
231
-
232
- const useStyles$7 = makeStyles({
233
- spacingTop: {
234
- marginTop: 8
235
- },
236
- spacingVertical: {
237
- marginTop: 8,
238
- marginBottom: 8
239
- }
240
- });
241
- const DAYS_SELECT_ITEMS = [
242
- { label: "7 days", value: 7 },
243
- { label: "14 days", value: 14 },
244
- { label: "30 days", value: 30 },
245
- { label: "60 days", value: 60 }
246
- ];
247
- const OverviewTrends = () => {
248
- var _a, _b;
249
- const [days, setDays] = useState(14);
250
- const theme = useTheme();
251
- const classes = useStyles$7();
252
- const client = useApi(xcmetricsApiRef);
253
- const buildCountsResult = useAsync(
254
- async () => client.getBuildCounts(days),
255
- [days]
256
- );
257
- const buildTimesResult = useAsync(
258
- async () => client.getBuildTimes(days),
259
- [days]
260
- );
261
- if (buildCountsResult.loading && buildTimesResult.loading) {
262
- return /* @__PURE__ */ React.createElement(Progress, null);
263
- }
264
- const sumBuilds = sumField((b) => b.builds, buildCountsResult.value);
265
- const sumErrors = sumField((b) => b.errors, buildCountsResult.value);
266
- const errorRate = sumBuilds && sumErrors ? sumErrors / sumBuilds : void 0;
267
- const averageBuildDurationP50 = getAverageDuration(
268
- buildTimesResult.value,
269
- (b) => b.durationP50
270
- );
271
- const averageBuildDurationP95 = getAverageDuration(
272
- buildTimesResult.value,
273
- (b) => b.durationP95
274
- );
275
- const totalBuildTime = sumField((t) => t.totalDuration, buildTimesResult.value);
276
- return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
277
- Select,
278
- {
279
- selected: days,
280
- items: DAYS_SELECT_ITEMS,
281
- label: "Trends for",
282
- onChange: (selection) => setDays(selection)
283
- }
284
- ), buildCountsResult.error && /* @__PURE__ */ React.createElement(Alert, { severity: "error", className: classes.spacingVertical }, /* @__PURE__ */ React.createElement(AlertTitle, null, "Failed to fetch build counts"), (_a = buildCountsResult == null ? void 0 : buildCountsResult.error) == null ? void 0 : _a.message), buildTimesResult.error && /* @__PURE__ */ React.createElement(Alert, { severity: "error", className: classes.spacingVertical }, /* @__PURE__ */ React.createElement(AlertTitle, null, "Failed to fetch build times"), (_b = buildTimesResult == null ? void 0 : buildTimesResult.error) == null ? void 0 : _b.message), (!buildCountsResult.error || !buildTimesResult.error) && /* @__PURE__ */ React.createElement("div", { className: classes.spacingVertical }, /* @__PURE__ */ React.createElement(
285
- Trend,
286
- {
287
- title: "Build Time",
288
- color: theme.palette.secondary.main,
289
- data: getValues((e) => e.durationP50, buildTimesResult.value)
290
- }
291
- ), /* @__PURE__ */ React.createElement(
292
- Trend,
293
- {
294
- title: "Error Rate",
295
- color: theme.palette.status.warning,
296
- data: getErrorRatios(buildCountsResult.value)
297
- }
298
- ), /* @__PURE__ */ React.createElement(
299
- Trend,
300
- {
301
- title: "Build Count",
302
- color: theme.palette.primary.main,
303
- data: getValues((e) => e.builds, buildCountsResult.value)
304
- }
305
- ), /* @__PURE__ */ React.createElement(
306
- Grid,
307
- {
308
- container: true,
309
- spacing: 3,
310
- direction: "row",
311
- className: classes.spacingTop
312
- },
313
- /* @__PURE__ */ React.createElement(DataValueGridItem, { field: "Build Count", value: sumBuilds }),
314
- /* @__PURE__ */ React.createElement(DataValueGridItem, { field: "Error Count", value: sumErrors }),
315
- /* @__PURE__ */ React.createElement(
316
- DataValueGridItem,
317
- {
318
- field: "Error Rate",
319
- value: errorRate && formatPercentage(errorRate)
320
- }
321
- ),
322
- /* @__PURE__ */ React.createElement(
323
- DataValueGridItem,
324
- {
325
- field: "Avg. Build Time (P50)",
326
- value: averageBuildDurationP50
327
- }
328
- ),
329
- /* @__PURE__ */ React.createElement(
330
- DataValueGridItem,
331
- {
332
- field: "Avg. Build Time (P95)",
333
- value: averageBuildDurationP95
334
- }
335
- ),
336
- /* @__PURE__ */ React.createElement(
337
- DataValueGridItem,
338
- {
339
- field: "Total Build Time",
340
- value: totalBuildTime && formatDuration(totalBuildTime)
341
- }
342
- )
343
- )));
344
- };
345
-
346
- const STATUS_ICONS = {
347
- succeeded: /* @__PURE__ */ React.createElement(StatusOK, null),
348
- failed: /* @__PURE__ */ React.createElement(StatusError, null),
349
- stopped: /* @__PURE__ */ React.createElement(StatusWarning, null)
350
- };
351
- const StatusIcon = ({ buildStatus }) => {
352
- var _a;
353
- return (_a = STATUS_ICONS[buildStatus]) != null ? _a : /* @__PURE__ */ React.createElement(StatusAborted, null);
354
- };
355
-
356
- const baseColumns = [
357
- {
358
- field: "buildStatus",
359
- render: (data) => /* @__PURE__ */ React.createElement(StatusIcon, { buildStatus: data.buildStatus })
360
- },
361
- {
362
- title: "Project",
363
- field: "projectName"
364
- },
365
- {
366
- title: "Schema",
367
- field: "schema"
368
- },
369
- {
370
- title: "Started",
371
- field: "startedAt",
372
- render: (data) => formatTime(data.startTimestamp),
373
- cellStyle: { whiteSpace: "nowrap" }
374
- },
375
- {
376
- title: "Duration",
377
- field: "duration",
378
- render: (data) => formatDuration(data.duration)
379
- },
380
- {
381
- title: "User",
382
- field: "userid"
383
- }
384
- ];
385
- const isCi = {
386
- field: "isCI",
387
- render: (data) => data.isCi && /* @__PURE__ */ React.createElement(Chip, { label: "CI", size: "small" }),
388
- width: "10",
389
- sorting: false
390
- };
391
- const overviewColumns = [...baseColumns, isCi];
392
- const buildPageColumns = [
393
- ...baseColumns,
394
- {
395
- title: "Host",
396
- field: "machineName"
397
- },
398
- {
399
- title: "Warnings",
400
- field: "warningCount"
401
- },
402
- {
403
- title: "Category",
404
- field: "category",
405
- render: (data) => /* @__PURE__ */ React.createElement(Chip, { label: data.category, size: "small" })
406
- },
407
- isCi
408
- ];
409
-
410
- const Overview = () => {
411
- const client = useApi(xcmetricsApiRef);
412
- const {
413
- value: builds,
414
- loading,
415
- error
416
- } = useAsync(async () => client.getBuilds(), []);
417
- if (loading) {
418
- return /* @__PURE__ */ React.createElement(Progress, null);
419
- } else if (error) {
420
- return /* @__PURE__ */ React.createElement(Alert, { severity: "error" }, error.message);
421
- }
422
- if (!builds || !builds.length) {
423
- return /* @__PURE__ */ React.createElement(
424
- EmptyState,
425
- {
426
- missing: "data",
427
- title: "No builds to show",
428
- description: "There are no builds in XCMetrics yet"
429
- }
430
- );
431
- }
432
- return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(ContentHeader, { title: "XCMetrics Dashboard" }, /* @__PURE__ */ React.createElement(SupportButton, null, "Dashboard for XCMetrics")), /* @__PURE__ */ React.createElement(Grid, { container: true, spacing: 3, direction: "row" }, /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12, md: 8, lg: 8, xl: 9 }, /* @__PURE__ */ React.createElement(
433
- Table,
434
- {
435
- options: {
436
- paging: false,
437
- search: false,
438
- sorting: false,
439
- draggable: false
440
- },
441
- data: builds,
442
- columns: overviewColumns,
443
- title: /* @__PURE__ */ React.createElement(React.Fragment, null, "Latest Builds", /* @__PURE__ */ React.createElement(StatusMatrix, null))
444
- }
445
- )), /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 12, md: 4, lg: 4, xl: 3 }, /* @__PURE__ */ React.createElement(InfoCard, null, /* @__PURE__ */ React.createElement(OverviewTrends, null)))));
446
- };
447
-
448
- const BootstrapInput = withStyles(
449
- (theme) => createStyles({
450
- root: {
451
- margin: theme.spacing(1, 0),
452
- maxWidth: 300,
453
- "label + &": {
454
- marginTop: theme.spacing(3)
455
- }
456
- },
457
- input: {
458
- borderRadius: 4,
459
- position: "relative",
460
- backgroundColor: theme.palette.background.paper,
461
- border: "1px solid #ced4da",
462
- fontSize: 16,
463
- padding: "10px 26px 10px 12px",
464
- transition: theme.transitions.create(["border-color", "box-shadow"]),
465
- fontFamily: "Helvetica Neue",
466
- height: 25,
467
- "&:focus": {
468
- background: theme.palette.background.paper,
469
- borderRadius: 4
470
- }
471
- }
472
- })
473
- )(InputBase);
474
- const useStyles$6 = makeStyles$1({
475
- root: {
476
- display: "flex",
477
- flexDirection: "column"
478
- }
479
- });
480
- const DatePicker = ({
481
- label,
482
- onDateChange,
483
- ...inputProps
484
- }) => {
485
- const classes = useStyles$6();
486
- return /* @__PURE__ */ React.createElement("div", { className: classes.root }, /* @__PURE__ */ React.createElement(Typography$1, { variant: "button" }, label), /* @__PURE__ */ React.createElement(
487
- BootstrapInput,
488
- {
489
- inputProps: { "aria-label": label },
490
- type: "date",
491
- fullWidth: true,
492
- onChange: (event) => onDateChange == null ? void 0 : onDateChange(event.target.value),
493
- ...inputProps
494
- }
495
- ));
496
- };
497
-
498
- const toSelectItems = (strings) => {
499
- return strings.map((str) => ({ label: str, value: str }));
500
- };
501
- const useStyles$5 = makeStyles((theme) => ({
502
- filtersContent: {
503
- padding: theme.spacing(2, 2, 2, 2.5)
504
- }
505
- }));
506
- const BuildListFilter = ({
507
- onFilterChange,
508
- initialValues
509
- }) => {
510
- const client = useApi(xcmetricsApiRef);
511
- const classes = useStyles$5();
512
- const [open, setOpen] = useState(false);
513
- const [values, setValues] = useState(initialValues);
514
- useEffect(() => onFilterChange(values), [onFilterChange, values]);
515
- const numFilters = Object.keys(values).reduce((sum, key) => {
516
- const filtersKey = key;
517
- return sum + Number(values[filtersKey] !== initialValues[filtersKey]);
518
- }, 0);
519
- const title = /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
520
- IconButton,
521
- {
522
- onClick: () => setOpen(!open),
523
- "aria-label": `${open ? "hide" : "show"} filters`
524
- },
525
- /* @__PURE__ */ React.createElement(FilterList, null)
526
- ), "Filters (", numFilters, ")", !!numFilters && /* @__PURE__ */ React.createElement(Button, { color: "primary", onClick: () => setValues(initialValues) }, "Clear all"));
527
- const statusItems = [
528
- { label: "All", value: "all" },
529
- { label: "Succeeded", value: "succeeded" },
530
- { label: "Failed", value: "failed" },
531
- { label: "Stopped", value: "stopped" }
532
- ];
533
- const { value: projects, loading } = useAsync(async () => {
534
- return client.getProjects();
535
- }, []);
536
- const content = /* @__PURE__ */ React.createElement(
537
- Grid,
538
- {
539
- container: true,
540
- spacing: 3,
541
- direction: "row",
542
- className: classes.filtersContent
543
- },
544
- /* @__PURE__ */ React.createElement(Grid, { item: true, sm: 6, md: 4, lg: 2 }, /* @__PURE__ */ React.createElement(
545
- DatePicker,
546
- {
547
- label: "From",
548
- value: values.from,
549
- onDateChange: (date) => setValues({ ...values, from: date })
550
- }
551
- )),
552
- /* @__PURE__ */ React.createElement(Grid, { item: true, sm: 6, md: 4, lg: 2 }, /* @__PURE__ */ React.createElement(
553
- DatePicker,
554
- {
555
- label: "To",
556
- value: values.to,
557
- onDateChange: (date) => setValues({ ...values, to: date })
558
- }
559
- )),
560
- /* @__PURE__ */ React.createElement(Grid, { item: true, sm: 6, md: 4, lg: 2 }, /* @__PURE__ */ React.createElement(
561
- Select,
562
- {
563
- label: "Status",
564
- items: statusItems,
565
- selected: !values.buildStatus ? "all" : values.buildStatus,
566
- onChange: (selection) => {
567
- const buildStatus = selection === "all" ? void 0 : selection;
568
- setValues({ ...values, buildStatus });
569
- }
570
- }
571
- )),
572
- /* @__PURE__ */ React.createElement(Grid, { item: true, sm: 6, md: 4, lg: 2 }, loading ? /* @__PURE__ */ React.createElement(
573
- Select,
574
- {
575
- label: "Project",
576
- placeholder: "Loading..",
577
- items: [],
578
- onChange: () => void 0
579
- }
580
- ) : /* @__PURE__ */ React.createElement(
581
- Select,
582
- {
583
- label: "Project",
584
- items: toSelectItems(["All"].concat(projects != null ? projects : [])),
585
- selected: values.project ? values.project : "All",
586
- onChange: (selection) => setValues({
587
- ...values,
588
- project: selection === "All" ? void 0 : selection
589
- })
590
- }
591
- ))
592
- );
593
- return /* @__PURE__ */ React.createElement(
594
- InfoCard,
595
- {
596
- title,
597
- titleTypographyProps: { variant: "h6" },
598
- divider: open,
599
- noPadding: true,
600
- variant: "gridItem"
601
- },
602
- open && content
603
- );
604
- };
605
-
606
- const useStyles$4 = makeStyles(
607
- (theme) => createStyles({
608
- heading: {
609
- flexBasis: "33.33%",
610
- flexShrink: 0
611
- },
612
- secondaryHeading: {
613
- color: theme.palette.text.secondary
614
- }
615
- })
616
- );
617
- const Accordion = (props) => {
618
- var _a;
619
- const classes = useStyles$4();
620
- return /* @__PURE__ */ React.createElement(
621
- MuiAccordion,
622
- {
623
- disabled: props.disabled,
624
- TransitionProps: { unmountOnExit: (_a = props.unmountOnExit) != null ? _a : false }
625
- },
626
- /* @__PURE__ */ React.createElement(
627
- MuiAccordionSummary,
628
- {
629
- expandIcon: /* @__PURE__ */ React.createElement(ExpandMoreIcon, null),
630
- "aria-controls": `${props.id}-content`,
631
- id: `${props.id}-header`
632
- },
633
- /* @__PURE__ */ React.createElement(Typography, { className: classes.heading }, props.heading),
634
- /* @__PURE__ */ React.createElement(Typography, { className: classes.secondaryHeading }, props.secondaryHeading)
635
- ),
636
- /* @__PURE__ */ React.createElement(AccordionDetails, null, props.children)
637
- );
638
- };
639
-
640
- const EMPTY_HEIGHT = 100;
641
- const useStyles$3 = makeStyles(
642
- (theme) => createStyles({
643
- toolTip: {
644
- backgroundColor: theme.palette.background.paper,
645
- opacity: 0.8,
646
- padding: 8
647
- }
648
- })
649
- );
650
- const TargetToolTip = ({ active, payload, label }) => {
651
- const classes = useStyles$3();
652
- if (active && payload && payload.length === 2) {
653
- const buildTime = payload[0].value[1] - payload[0].value[0];
654
- const compileTime = payload[1].value[1] - payload[1].value[0];
655
- return /* @__PURE__ */ React.createElement("div", { className: classes.toolTip }, `${label}: ${formatSecondsInterval(payload[0].value)}`, /* @__PURE__ */ React.createElement("br", null), buildTime > 0 && `Compile time: ${formatPercentage(compileTime / buildTime)}`);
656
- }
657
- return null;
658
- };
659
- const getTimelineData = (targets) => {
660
- const min = targets[0].startTimestampMicroseconds;
661
- return targets.filter((target) => target.fetchedFromCache === false).map((target) => ({
662
- name: target.name,
663
- buildTime: [
664
- target.startTimestampMicroseconds - min,
665
- target.endTimestampMicroseconds - min
666
- ],
667
- compileTime: [
668
- target.startTimestampMicroseconds - min,
669
- target.compilationEndTimestampMicroseconds - min
670
- ]
671
- }));
672
- };
673
- const BuildTimeline = ({
674
- targets,
675
- height,
676
- width
677
- }) => {
678
- const theme = useTheme();
679
- if (!targets.length)
680
- return /* @__PURE__ */ React.createElement(Typography, { paragraph: true }, "No Targets");
681
- const data = getTimelineData(targets);
682
- return /* @__PURE__ */ React.createElement(
683
- ResponsiveContainer,
684
- {
685
- height,
686
- width,
687
- minHeight: EMPTY_HEIGHT + targets.length * 5
688
- },
689
- /* @__PURE__ */ React.createElement(BarChart, { layout: "vertical", data, maxBarSize: 10, barGap: 0 }, /* @__PURE__ */ React.createElement(CartesianGrid, { strokeDasharray: "2 2" }), /* @__PURE__ */ React.createElement(XAxis, { type: "number", domain: [0, "dataMax"] }), /* @__PURE__ */ React.createElement(YAxis, { type: "category", dataKey: "name", padding: { top: 0, bottom: 0 } }), /* @__PURE__ */ React.createElement(Tooltip$1, { content: /* @__PURE__ */ React.createElement(TargetToolTip, null) }), /* @__PURE__ */ React.createElement(Legend, null), /* @__PURE__ */ React.createElement(
690
- Bar,
691
- {
692
- dataKey: "buildTime",
693
- fill: theme.palette.grey[400],
694
- minPointSize: 1
695
- }
696
- ), /* @__PURE__ */ React.createElement(Bar, { dataKey: "compileTime", fill: theme.palette.primary.main }))
697
- );
698
- };
699
-
700
- const useStyles$2 = makeStyles(
701
- (theme) => createStyles({
702
- pre: {
703
- whiteSpace: "pre-line",
704
- wordBreak: "break-all"
705
- },
706
- expandable: {
707
- cursor: "pointer"
708
- },
709
- fullPre: {
710
- whiteSpace: "pre-wrap"
711
- },
712
- closeButton: {
713
- position: "absolute",
714
- right: theme.spacing(1),
715
- top: theme.spacing(1),
716
- color: theme.palette.grey[500]
717
- }
718
- })
719
- );
720
- const PreformattedText = ({
721
- text,
722
- maxChars,
723
- expandable,
724
- title
725
- }) => {
726
- const [open, setOpen] = useState(false);
727
- const classes = useStyles$2();
728
- const handleKeyUp = (event) => {
729
- if (expandable && event.key === "Enter") {
730
- setOpen(true);
731
- }
732
- };
733
- return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
734
- "div",
735
- {
736
- role: expandable ? "button" : void 0,
737
- onClick: () => expandable && setOpen(true),
738
- onKeyUp: handleKeyUp,
739
- tabIndex: expandable ? 0 : void 0
740
- },
741
- /* @__PURE__ */ React.createElement("pre", { className: cn(classes.pre, expandable && classes.expandable) }, text.slice(0, maxChars - 1).trim(), text.length > maxChars - 1 && "\u2026")
742
- ), expandable && /* @__PURE__ */ React.createElement(
743
- Dialog,
744
- {
745
- open,
746
- onClose: () => setOpen(false),
747
- "aria-labelledby": "dialog-title",
748
- "aria-describedby": "dialog-description",
749
- maxWidth: "xl",
750
- fullWidth: true
751
- },
752
- /* @__PURE__ */ React.createElement(DialogTitle, { id: "dialog-title" }, title, /* @__PURE__ */ React.createElement(
753
- IconButton,
754
- {
755
- "aria-label": "close",
756
- className: classes.closeButton,
757
- onClick: () => setOpen(false)
758
- },
759
- /* @__PURE__ */ React.createElement(CloseIcon, null)
760
- )),
761
- /* @__PURE__ */ React.createElement(DialogContent, null, /* @__PURE__ */ React.createElement("pre", { className: classes.fullPre }, text)),
762
- /* @__PURE__ */ React.createElement(DialogActions, null, /* @__PURE__ */ React.createElement(Button, { color: "primary", onClick: () => setOpen(false) }, "Close"))
763
- ));
764
- };
765
-
766
- const useStyles$1 = makeStyles(
767
- (theme) => createStyles({
768
- divider: {
769
- marginTop: theme.spacing(2),
770
- marginBottom: theme.spacing(2)
771
- }
772
- })
773
- );
774
- const BuildDetails = ({
775
- buildData: { build, targets, xcode },
776
- showId
777
- }) => {
778
- var _a, _b;
779
- const classes = useStyles$1();
780
- const client = useApi(xcmetricsApiRef);
781
- const hostResult = useAsync(
782
- async () => client.getBuildHost(build.id),
783
- [build.id]
784
- );
785
- const errorsResult = useAsync(
786
- async () => client.getBuildErrors(build.id),
787
- [build.id]
788
- );
789
- const warningsResult = useAsync(
790
- async () => client.getBuildWarnings(build.id),
791
- [build.id]
792
- );
793
- const metadataResult = useAsync(
794
- async () => client.getBuildMetadata(build.id),
795
- [build.id]
796
- );
797
- const buildDetails = {
798
- project: build.projectName,
799
- schema: build.schema,
800
- category: build.category,
801
- userId: build.userid,
802
- "started at": formatTime(build.startTimestamp),
803
- "ended at": formatTime(build.endTimestamp),
804
- duration: formatDuration(build.duration),
805
- status: /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(StatusIcon, { buildStatus: build.buildStatus }), formatStatus(build.buildStatus)),
806
- xcode: xcode ? `${xcode.version} (${xcode.buildNumber})` : "Unknown",
807
- CI: build.isCi
808
- };
809
- return /* @__PURE__ */ React.createElement(Grid, { container: true, item: true, direction: "row" }, /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 4 }, /* @__PURE__ */ React.createElement(
810
- StructuredMetadataTable,
811
- {
812
- metadata: showId === false ? buildDetails : { id: build.id, ...buildDetails }
813
- }
814
- )), /* @__PURE__ */ React.createElement(Grid, { item: true, xs: 8 }, /* @__PURE__ */ React.createElement(
815
- Accordion,
816
- {
817
- id: "buildHost",
818
- heading: "Host",
819
- secondaryHeading: build.machineName
820
- },
821
- hostResult.loading && /* @__PURE__ */ React.createElement(Progress, null),
822
- !hostResult.loading && hostResult.value && /* @__PURE__ */ React.createElement(StructuredMetadataTable, { metadata: hostResult.value })
823
- ), /* @__PURE__ */ React.createElement(
824
- Accordion,
825
- {
826
- id: "buildErrors",
827
- heading: "Errors",
828
- secondaryHeading: build.errorCount,
829
- disabled: build.errorCount === 0
830
- },
831
- /* @__PURE__ */ React.createElement("div", null, errorsResult.loading && /* @__PURE__ */ React.createElement(Progress, null), !errorsResult.loading && ((_a = errorsResult.value) == null ? void 0 : _a.map((error, idx) => /* @__PURE__ */ React.createElement("div", { key: error.id }, /* @__PURE__ */ React.createElement(
832
- PreformattedText,
833
- {
834
- title: "Error Details",
835
- text: error.detail,
836
- maxChars: 190,
837
- expandable: true
838
- }
839
- ), idx !== errorsResult.value.length - 1 && /* @__PURE__ */ React.createElement(Divider, { className: classes.divider })))))
840
- ), /* @__PURE__ */ React.createElement(
841
- Accordion,
842
- {
843
- id: "buildWarnings",
844
- heading: "Warnings",
845
- secondaryHeading: build.warningCount,
846
- disabled: build.warningCount === 0
847
- },
848
- /* @__PURE__ */ React.createElement("div", null, warningsResult.loading && /* @__PURE__ */ React.createElement(Progress, null), !warningsResult.loading && ((_b = warningsResult.value) == null ? void 0 : _b.map((warning, idx) => {
849
- var _a2;
850
- return /* @__PURE__ */ React.createElement("div", { key: warning.id }, /* @__PURE__ */ React.createElement(
851
- PreformattedText,
852
- {
853
- title: "Warning Details",
854
- text: (_a2 = warning.detail) != null ? _a2 : warning.title,
855
- maxChars: 190,
856
- expandable: true
857
- }
858
- ), idx !== warningsResult.value.length - 1 && /* @__PURE__ */ React.createElement(Divider, { className: classes.divider }));
859
- })))
860
- ), /* @__PURE__ */ React.createElement(
861
- Accordion,
862
- {
863
- id: "buildMetadata",
864
- heading: "Metadata",
865
- disabled: !metadataResult.loading && !metadataResult.value
866
- },
867
- metadataResult.loading && /* @__PURE__ */ React.createElement(Progress, null),
868
- !metadataResult.loading && metadataResult.value && /* @__PURE__ */ React.createElement(StructuredMetadataTable, { metadata: metadataResult.value })
869
- ), /* @__PURE__ */ React.createElement(Accordion, { id: "buildTimeline", heading: "Timeline", unmountOnExit: true }, /* @__PURE__ */ React.createElement(BuildTimeline, { targets }))));
870
- };
871
- const withRequest = (Component) => ({ buildId, ...props }) => {
872
- const client = useApi(xcmetricsApiRef);
873
- const {
874
- value: buildResponse,
875
- loading,
876
- error
877
- } = useAsync(async () => client.getBuild(buildId), []);
878
- if (loading) {
879
- return /* @__PURE__ */ React.createElement(Progress, null);
880
- }
881
- if (error) {
882
- return /* @__PURE__ */ React.createElement(Alert, { severity: "error" }, error.message);
883
- }
884
- if (!buildResponse) {
885
- return /* @__PURE__ */ React.createElement(Alert, { severity: "error" }, "Could not load build ", buildId);
886
- }
887
- return /* @__PURE__ */ React.createElement(Component, { ...props, buildData: buildResponse });
888
- };
889
-
890
- const useStyles = makeStyles(
891
- (theme) => createStyles({
892
- detailPanel: {
893
- padding: theme.spacing(2),
894
- backgroundColor: theme.palette.background.paper
895
- }
896
- })
897
- );
898
- const BuildList = () => {
899
- const classes = useStyles();
900
- const client = useApi(xcmetricsApiRef);
901
- const tableRef = useRef();
902
- const initialFilters = {
903
- from: DateTime.now().minus({ years: 1 }).toISODate(),
904
- to: DateTime.now().toISODate()
905
- };
906
- const [filters, setFilters] = useState(initialFilters);
907
- const handleFilterChange = (values) => {
908
- var _a;
909
- setFilters(values);
910
- (_a = tableRef.current) == null ? void 0 : _a.onQueryChange();
911
- };
912
- return /* @__PURE__ */ React.createElement(Grid, { container: true, spacing: 3, direction: "column" }, /* @__PURE__ */ React.createElement(
913
- BuildListFilter,
914
- {
915
- onFilterChange: handleFilterChange,
916
- initialValues: initialFilters
917
- }
918
- ), /* @__PURE__ */ React.createElement(
919
- Table,
920
- {
921
- title: "Builds",
922
- columns: buildPageColumns,
923
- options: { paging: true, sorting: false, search: false, pageSize: 10 },
924
- tableRef,
925
- data: (query) => {
926
- return new Promise((resolve, reject) => {
927
- if (!query)
928
- return;
929
- client.getFilteredBuilds(
930
- filters,
931
- query.page + 1,
932
- // Page is 0-indexed in Table
933
- query.pageSize
934
- ).then((result) => {
935
- resolve({
936
- data: result.items,
937
- page: result.metadata.page - 1,
938
- totalCount: result.metadata.total
939
- });
940
- }).catch((reason) => reject(reason));
941
- });
942
- },
943
- detailPanel: (rowData) => {
944
- const BuildDetailsWithRequest = withRequest(BuildDetails);
945
- return /* @__PURE__ */ React.createElement("div", { className: classes.detailPanel }, /* @__PURE__ */ React.createElement(BuildDetailsWithRequest, { buildId: rowData.rowData.id }));
946
- }
947
- }
948
- ));
949
- };
950
-
951
- const TABS = [
952
- {
953
- path: "/",
954
- title: "Overview",
955
- component: /* @__PURE__ */ React.createElement(Overview, null)
956
- },
957
- {
958
- path: buildsRouteRef.path,
959
- title: "Builds",
960
- component: /* @__PURE__ */ React.createElement(BuildList, null)
961
- }
962
- ];
963
- const XcmetricsLayout = () => /* @__PURE__ */ React.createElement(Page, { themeId: "tool" }, /* @__PURE__ */ React.createElement(Header, { title: "XCMetrics", subtitle: "Dashboard" }, /* @__PURE__ */ React.createElement(HeaderLabel, { label: "Owner", value: "Spotify" }), /* @__PURE__ */ React.createElement(HeaderLabel, { label: "Lifecycle", value: "Alpha" })), /* @__PURE__ */ React.createElement(TabbedLayout, null, TABS.map((tab) => /* @__PURE__ */ React.createElement(TabbedLayout.Route, { key: tab.path, path: tab.path, title: tab.title }, /* @__PURE__ */ React.createElement(Content, null, tab.component)))));
964
-
965
- export { XcmetricsLayout };
966
- //# sourceMappingURL=index-Dehp5p72.esm.js.map