@xenon-device-management/xenon 2.6.2 → 2.7.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 (79) hide show
  1. package/lib/package.json +1 -1
  2. package/lib/public/assets/{AnnotationOverlay-U9aJANVi.js → AnnotationOverlay-oISPBknJ.js} +1 -1
  3. package/lib/public/assets/{ApiKeyGate-6265m84B.js → ApiKeyGate-BOm-SKAJ.js} +1 -1
  4. package/lib/public/assets/{BugReportButton-CGeVEuQ-.js → BugReportButton-C1WMf_3A.js} +1 -1
  5. package/lib/public/assets/{DeviceMosaicView-C_3HwhVZ.js → DeviceMosaicView-BPJ5n5fY.js} +1 -1
  6. package/lib/public/assets/{EmptyState-D5BTZSIa.js → EmptyState-CZg80GnN.js} +1 -1
  7. package/lib/public/assets/{FieldGroup-CDBhSDsT.js → FieldGroup-D7zftz-q.js} +1 -1
  8. package/lib/public/assets/{FilterMenu-7JEhwNEM.js → FilterMenu-BKHKlX-8.js} +1 -1
  9. package/lib/public/assets/{Menu-DWl5uJ9E.js → Menu-Ccznqijj.js} +1 -1
  10. package/lib/public/assets/{Modal-BZEfRpq8.js → Modal-FxdOVK12.js} +1 -1
  11. package/lib/public/assets/{RecordingPage-B_EK9yKX.js → RecordingPage-BL8B1IKY.js} +1 -1
  12. package/lib/public/assets/{RecordingsPage-CptMr6IM.js → RecordingsPage-CCCL2xwa.js} +1 -1
  13. package/lib/public/assets/{SegmentedControl-D7BBuUDP.js → SegmentedControl-B3EMys8L.js} +1 -1
  14. package/lib/public/assets/{SettingCard-W3QrpCyh.js → SettingCard-BwvdX-C6.js} +1 -1
  15. package/lib/public/assets/{Table-BthtE6KF.js → Table-sGNs22pM.js} +1 -1
  16. package/lib/public/assets/{activity-CdoCkXe1.js → activity-rQQOy6SD.js} +1 -1
  17. package/lib/public/assets/{ai-settings-CkGIJyKN.js → ai-settings-uIrII3el.js} +1 -1
  18. package/lib/public/assets/{api-keys-2lHmq44B.js → api-keys-B1-QmrZJ.js} +1 -1
  19. package/lib/public/assets/{apps-BCHpRvo_.js → apps-B8gYjYhN.js} +1 -1
  20. package/lib/public/assets/{arrow-left-CpaUvzcV.js → arrow-left-CLT_rU6o.js} +1 -1
  21. package/lib/public/assets/{arrow-right-TCqk2-9N.js → arrow-right-DqK8eHkB.js} +1 -1
  22. package/lib/public/assets/{arrow-up-right-B6JBqR-t.js → arrow-up-right-D96XxKQY.js} +1 -1
  23. package/lib/public/assets/{auth-shell-BSyFjCa8.js → auth-shell-DpBFmfin.js} +2 -2
  24. package/lib/public/assets/builds-page-DqD7ySkz.js +11 -0
  25. package/lib/public/assets/{button-CB1ehl9z.js → button-BrVVNvSB.js} +1 -1
  26. package/lib/public/assets/{calendar-DXZ0-sa6.js → calendar-FX_no0De.js} +1 -1
  27. package/lib/public/assets/{check-Bq0QWhtG.js → check-C6RAxVq9.js} +1 -1
  28. package/lib/public/assets/{chevron-left-BMI8J11V.js → chevron-left-CcTJUfpS.js} +1 -1
  29. package/lib/public/assets/{chevron-right-tbezHlhW.js → chevron-right-C39WUDQP.js} +1 -1
  30. package/lib/public/assets/{circle-check-BgkLodxU.js → circle-check-Fo1eEzZe.js} +1 -1
  31. package/lib/public/assets/{circle-x-DuFD0enL.js → circle-x-CXy5XzIu.js} +1 -1
  32. package/lib/public/assets/{clock-C7AhkXMm.js → clock-PBGPHMrI.js} +1 -1
  33. package/lib/public/assets/{copy-CiNhxJMO.js → copy-ZGa-scbM.js} +1 -1
  34. package/lib/public/assets/{device-explorer-BFWU04bT.js → device-explorer-BRYO7iUM.js} +1 -1
  35. package/lib/public/assets/{download-Di_q2qj5.js → download-vaODl7Bn.js} +1 -1
  36. package/lib/public/assets/{forgot-password-gtciaIez.js → forgot-password-YfGbiIOM.js} +1 -1
  37. package/lib/public/assets/{funnel-BY1R0ydM.js → funnel-C05BIqke.js} +1 -1
  38. package/lib/public/assets/{index-Bgx65Mb_.js → index-1ST_YG3o.js} +1 -1
  39. package/lib/public/assets/{index-B1iJx0w1.js → index-DdsuniRs.js} +2 -2
  40. package/lib/public/assets/{input-B83lhHJ_.js → input-C6Zp2Ibb.js} +1 -1
  41. package/lib/public/assets/{keycodes-BjiWgQik.js → keycodes-BlkuYXfQ.js} +1 -1
  42. package/lib/public/assets/{layers-CxMkF86z.js → layers-B-3IDp3k.js} +1 -1
  43. package/lib/public/assets/{list-checks-2GnUUcTR.js → list-checks-BUUYV0Vf.js} +1 -1
  44. package/lib/public/assets/{lock-BRKQ_6ix.js → lock-BUdNqIao.js} +1 -1
  45. package/lib/public/assets/{login-Chmwsz_d.js → login-CD8bag9i.js} +1 -1
  46. package/lib/public/assets/{maintenance-settings-DEb0LrBA.js → maintenance-settings-BWQP_nfK.js} +1 -1
  47. package/lib/public/assets/{monitor-ClL-76V7.js → monitor-qfb5f9oQ.js} +1 -1
  48. package/lib/public/assets/{mouse-pointer-2-C815rsZl.js → mouse-pointer-2-DAAlUY2l.js} +1 -1
  49. package/lib/public/assets/{network-CBd_S-GU.js → network-Qib4-4Yj.js} +1 -1
  50. package/lib/public/assets/{overview-B0paJ7DP.js → overview-BZQf20qe.js} +1 -1
  51. package/lib/public/assets/{page-header-D9FkEgQm.js → page-header-4jNxdU1M.js} +1 -1
  52. package/lib/public/assets/{plus-DF-cFokG.js → plus-DVhPNSQy.js} +1 -1
  53. package/lib/public/assets/{profile-page-BrdnJBRA.js → profile-page-DfITRDDC.js} +1 -1
  54. package/lib/public/assets/{reset-password-DJFBWJnT.js → reset-password-s2xU05oa.js} +1 -1
  55. package/lib/public/assets/{runbook-page-D5SIpqrf.js → runbook-page-DdEiXQcm.js} +1 -1
  56. package/lib/public/assets/{select-Wc_B9XUe.js → select-Bzs3ZUvb.js} +1 -1
  57. package/lib/public/assets/{selector-detail-page-q7-lRo_l.js → selector-detail-page-QvRRVchI.js} +1 -1
  58. package/lib/public/assets/{selector-health-Qe-NusTk.js → selector-health-DwUFxrZx.js} +1 -1
  59. package/lib/public/assets/{selector-health-page-Dw2WUlDJ.js → selector-health-page-N0tA9q9c.js} +1 -1
  60. package/lib/public/assets/{session-detail-page-bZpHxz1l.js → session-detail-page-DcGGXXxL.js} +1 -1
  61. package/lib/public/assets/{settings-DA_CIMVB.js → settings-BUqAjzeY.js} +1 -1
  62. package/lib/public/assets/stat-tile-BwZr7q7_.js +8 -0
  63. package/lib/public/assets/{tablet-_Uwg1m0t.js → tablet-DiOf-kLM.js} +1 -1
  64. package/lib/public/assets/{teams-ZTGk8Wky.js → teams-DA6AfWGO.js} +1 -1
  65. package/lib/public/assets/{trash-2-BcWlaBK8.js → trash-2-BZuuog-n.js} +1 -1
  66. package/lib/public/assets/{upload-47Lf5U7r.js → upload-DLHV2k4Z.js} +1 -1
  67. package/lib/public/assets/{use-password-reset-mode-B3cWCSu5.js → use-password-reset-mode-DRY4lLdd.js} +1 -1
  68. package/lib/public/assets/{users-IiOEsNnr.js → users-Bd4LKBWY.js} +1 -1
  69. package/lib/public/assets/{users-DGHET-4G.js → users-BlhcXeTG.js} +1 -1
  70. package/lib/public/assets/{video-off-D-jBA0at.js → video-off-DIaDZGNl.js} +1 -1
  71. package/lib/public/assets/{webhook-settings-HqV4V6_J.js → webhook-settings-IWvqbRk2.js} +1 -1
  72. package/lib/public/assets/{zap-iAfVTWG0.js → zap-4Qy4hVbF.js} +1 -1
  73. package/lib/public/index.html +1 -1
  74. package/lib/src/app/routers/dashboard.js +21 -11
  75. package/lib/src/app/routers/sessionInsights.js +46 -9
  76. package/lib/test/unit/session-list-insights.spec.js +39 -0
  77. package/package.json +1 -1
  78. package/lib/public/assets/builds-page-D0RnbdGV.js +0 -11
  79. package/lib/public/assets/stat-tile-CxsAGcDU.js +0 -8
@@ -101,25 +101,34 @@ function isValidSession(request, response, next) {
101
101
  }
102
102
  });
103
103
  }
104
- // A `since` that isn't a date is refused, rather than ignored into a list
105
- // of every session.
106
- function sinceOf(request, response) {
104
+ // A query parameter that can't be read is refused, rather than ignored into
105
+ // a list of every session. Undefined when it was refused.
106
+ function readQuery(response, read) {
107
107
  try {
108
- return (0, sessionInsights_1.parseSince)(request.query.since);
108
+ return read();
109
109
  }
110
110
  catch (err) {
111
- if (!(err instanceof sessionInsights_1.InvalidSinceError))
111
+ if (!(err instanceof sessionInsights_1.InvalidQueryError))
112
112
  throw err;
113
- response.status(400).json({ error: 'invalid_since', message: err.message });
113
+ response.status(400).json({ error: err.code, message: err.message });
114
114
  return undefined;
115
115
  }
116
116
  }
117
+ function sinceOf(request, response) {
118
+ return readQuery(response, () => (0, sessionInsights_1.parseSince)(request.query.since));
119
+ }
117
120
  function getSessions(request, response) {
118
121
  return __awaiter(this, void 0, void 0, function* () {
119
122
  const { buildId, query, status, platform } = request.query;
120
123
  const since = sinceOf(request, response);
121
124
  if (since === undefined)
122
125
  return;
126
+ const limit = readQuery(response, () => (0, sessionInsights_1.parseLimit)(request.query.limit));
127
+ if (limit === undefined)
128
+ return;
129
+ const cursor = readQuery(response, () => (0, sessionInsights_1.cursorWhere)(request.query.before, request.query.beforeId));
130
+ if (cursor === undefined)
131
+ return;
123
132
  const where = {};
124
133
  if (since) {
125
134
  where.createdAt = { gte: since };
@@ -145,12 +154,13 @@ function getSessions(request, response) {
145
154
  }
146
155
  // The team rule is part of the query, so `take` counts the caller's sessions.
147
156
  const visible = yield (0, sessionVisibility_1.visibleSessionWhere)(authOf(request));
157
+ const and = [where, ...(cursor ? [cursor] : []), ...(visible ? [visible] : [])];
148
158
  const sessions = yield prisma_1.prisma.session.findMany({
149
- orderBy: {
150
- createdAt: 'desc',
151
- },
152
- where: visible ? { AND: [where, visible] } : where,
153
- take: 500,
159
+ // Newest first; the id orders rows created in the same millisecond, so a
160
+ // page's cursor (its last row) continues exactly where it stopped.
161
+ orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
162
+ where: and.length === 1 ? where : { AND: and },
163
+ take: limit,
154
164
  });
155
165
  return response.status(200).json(yield (0, sessionInsights_1.describeSessions)(sessions));
156
166
  });
@@ -9,9 +9,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.InvalidSinceError = void 0;
12
+ exports.SESSION_PAGE_MAX = exports.SESSION_PAGE_DEFAULT = exports.InvalidQueryError = void 0;
13
13
  exports.sessionOutcome = sessionOutcome;
14
14
  exports.parseSince = parseSince;
15
+ exports.parseLimit = parseLimit;
16
+ exports.cursorWhere = cursorWhere;
15
17
  exports.describeSessions = describeSessions;
16
18
  exports.percentile = percentile;
17
19
  exports.summarizeSessions = summarizeSessions;
@@ -40,21 +42,56 @@ function sessionOutcome(status) {
40
42
  return 'other';
41
43
  }
42
44
  }
43
- class InvalidSinceError extends Error {
44
- constructor() {
45
- super('since must be an ISO 8601 date');
45
+ /** A query parameter that can't be read: answered 400 with `code`. */
46
+ class InvalidQueryError extends Error {
47
+ constructor(code, message) {
48
+ super(message);
49
+ this.code = code;
46
50
  }
47
51
  }
48
- exports.InvalidSinceError = InvalidSinceError;
49
- /** The `since` query parameter: null when absent; a date, or InvalidSinceError. */
50
- function parseSince(raw) {
52
+ exports.InvalidQueryError = InvalidQueryError;
53
+ function parseDate(raw, name) {
51
54
  if (raw === undefined || raw === '')
52
55
  return null;
53
56
  const at = typeof raw === 'string' ? Date.parse(raw) : NaN;
54
- if (!Number.isFinite(at))
55
- throw new InvalidSinceError();
57
+ if (!Number.isFinite(at)) {
58
+ throw new InvalidQueryError(`invalid_${name}`, `${name} must be an ISO 8601 date`);
59
+ }
56
60
  return new Date(at);
57
61
  }
62
+ /** The `since` query parameter: null when absent; a date, or InvalidQueryError. */
63
+ function parseSince(raw) {
64
+ return parseDate(raw, 'since');
65
+ }
66
+ /** GET /session's page size when none is asked for, as before paging. */
67
+ exports.SESSION_PAGE_DEFAULT = 500;
68
+ /** The most rows one GET /session answers; a larger limit gets this many. */
69
+ exports.SESSION_PAGE_MAX = 2000;
70
+ /** The `limit` query parameter: a whole number from 1, at most SESSION_PAGE_MAX. */
71
+ function parseLimit(raw) {
72
+ if (raw === undefined || raw === '')
73
+ return exports.SESSION_PAGE_DEFAULT;
74
+ const n = typeof raw === 'string' && /^\d+$/.test(raw) ? Number(raw) : NaN;
75
+ if (!Number.isInteger(n) || n < 1) {
76
+ throw new InvalidQueryError('invalid_limit', 'limit must be a whole number from 1');
77
+ }
78
+ return Math.min(n, exports.SESSION_PAGE_MAX);
79
+ }
80
+ /**
81
+ * Where the next page starts: after the last row of the page before, given
82
+ * as its `createdAt` (`before`) and `id` (`beforeId`). Pages go newest first
83
+ * by createdAt then id, so rows created in the same millisecond are neither
84
+ * skipped nor repeated. Without `beforeId`, everything older than `before`.
85
+ * Null when there is no cursor.
86
+ */
87
+ function cursorWhere(before, beforeId) {
88
+ const at = parseDate(before, 'before');
89
+ if (!at)
90
+ return null;
91
+ if (typeof beforeId !== 'string' || !beforeId)
92
+ return { createdAt: { lt: at } };
93
+ return { OR: [{ createdAt: { lt: at } }, { createdAt: at, id: { lt: beforeId } }] };
94
+ }
58
95
  /**
59
96
  * Each listed session with who ran it (`owner`) and where (`ranOn`): 'here'
60
97
  * for this server, the node's host for another server's phone, null when
@@ -72,6 +72,45 @@ describe('Sessions page data', () => {
72
72
  data: { id, name, email, passwordHash: 'x', accessKey: `ak-${id}` },
73
73
  });
74
74
  describe('GET /session', () => {
75
+ // Pages, newest first: limit, and a cursor at the last row of the page
76
+ // before (before = its createdAt, beforeId = its id). The id breaks ties,
77
+ // so rows created in the same millisecond are neither skipped nor repeated.
78
+ describe('paging', () => {
79
+ const t = (min) => new Date(now - min * MIN);
80
+ const ids = (res) => res.body.map((r) => r.id);
81
+ beforeEach(() => __awaiter(void 0, void 0, void 0, function* () {
82
+ yield session({ id: 'a', createdAt: t(1) });
83
+ yield session({ id: 'b', createdAt: t(2) });
84
+ yield session({ id: 'c', createdAt: t(2) });
85
+ yield session({ id: 'd', createdAt: t(3) });
86
+ }));
87
+ it('keeps to the limit, newest first, the id breaking ties', () => __awaiter(void 0, void 0, void 0, function* () {
88
+ (0, chai_1.expect)(ids(yield (0, loopbackRequest_1.default)(app()).get('/session?limit=2'))).to.deep.equal(['a', 'c']);
89
+ }));
90
+ it('continues after the cursor, neither skipping nor repeating a row', () => __awaiter(void 0, void 0, void 0, function* () {
91
+ const res = yield (0, loopbackRequest_1.default)(app()).get(`/session?limit=2&before=${t(2).toISOString()}&beforeId=c`);
92
+ (0, chai_1.expect)(ids(res)).to.deep.equal(['b', 'd']);
93
+ }));
94
+ it('takes a cursor with no id as everything older than its time', () => __awaiter(void 0, void 0, void 0, function* () {
95
+ const res = yield (0, loopbackRequest_1.default)(app()).get(`/session?before=${t(2).toISOString()}`);
96
+ (0, chai_1.expect)(ids(res)).to.deep.equal(['d']);
97
+ }));
98
+ it('refuses a limit or a cursor it cannot read', () => __awaiter(void 0, void 0, void 0, function* () {
99
+ for (const q of ['limit=abc', 'limit=0', 'limit=-5', 'limit=2.5']) {
100
+ const res = yield (0, loopbackRequest_1.default)(app()).get(`/session?${q}`);
101
+ (0, chai_1.expect)(res.status, q).to.equal(400);
102
+ (0, chai_1.expect)(res.body.error, q).to.equal('invalid_limit');
103
+ }
104
+ const res = yield (0, loopbackRequest_1.default)(app()).get('/session?before=soon');
105
+ (0, chai_1.expect)(res.status).to.equal(400);
106
+ (0, chai_1.expect)(res.body.error).to.equal('invalid_before');
107
+ }));
108
+ it('answers a limit over the most with the most, not an error', () => __awaiter(void 0, void 0, void 0, function* () {
109
+ const res = yield (0, loopbackRequest_1.default)(app()).get('/session?limit=999999');
110
+ (0, chai_1.expect)(res.status).to.equal(200);
111
+ (0, chai_1.expect)(ids(res)).to.deep.equal(['a', 'c', 'b', 'd']);
112
+ }));
113
+ });
75
114
  it('keeps the sessions created since the given time, newest first', () => __awaiter(void 0, void 0, void 0, function* () {
76
115
  yield session({ id: 'old', createdAt: ago(3 * HOUR) });
77
116
  yield session({ id: 'recent', createdAt: ago(30 * MIN) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenon-device-management/xenon",
3
- "version": "2.6.2",
3
+ "version": "2.7.0",
4
4
  "description": "Xenon - Intelligent Mobile Infrastructure. A self-healing device orchestration platform for Appium.",
5
5
  "main": "./lib/src/index.js",
6
6
  "exports": {
@@ -1,11 +0,0 @@
1
- import{c as ee,r as h,p as te,X,j as e,z as q,R as _,f as se,T as ae,q as re,u as ne,i as le,t as oe,M as ie}from"./index-B1iJx0w1.js";import{s as ce,b as I,P as de,T as B,i as ue,f as V,a as xe,c as me,d as pe,p as he,e as fe,r as ve,C as be,g as ge,h as je,j as O,k as Ne,l as W,S as w,m as ye,n as we,u as Se,o as Ce,q as $e}from"./stat-tile-CxsAGcDU.js";import{I as J}from"./input-B83lhHJ_.js";import{L as ke}from"./layers-CxMkF86z.js";import{B as F}from"./button-CB1ehl9z.js";import{D as Te}from"./download-Di_q2qj5.js";import{s as Ee}from"./labels-C21SA4JG.js";import{C as Re}from"./circle-x-DuFD0enL.js";import{C as Me}from"./circle-check-BgkLodxU.js";import{C as Be}from"./chevron-right-tbezHlhW.js";import{T as Ie}from"./tablet-_Uwg1m0t.js";import{M as Le}from"./monitor-ClL-76V7.js";import{P as _e}from"./page-header-D9FkEgQm.js";import{S as De}from"./select-Wc_B9XUe.js";import{E as U}from"./EmptyState-D5BTZSIa.js";import{L as Oe}from"./list-checks-2GnUUcTR.js";import"./time-B8FIgwPE.js";import"./utils-CDN07tui.js";/**
2
- * @license lucide-react v0.555.0 - ISC
3
- *
4
- * This source code is licensed under the ISC license.
5
- * See the LICENSE file in the root directory of this source tree.
6
- */const Ae=[["rect",{width:"8",height:"4",x:"8",y:"2",rx:"1",ry:"1",key:"tgr4d6"}],["path",{d:"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2",key:"116196"}],["path",{d:"M12 11h4",key:"1jrz19"}],["path",{d:"M12 16h4",key:"n85exb"}],["path",{d:"M8 11h.01",key:"1dfujw"}],["path",{d:"M8 16h.01",key:"18s6g9"}]],Pe=ee("clipboard-list",Ae),Fe=15e3;function Ue(t,a){const[s,r]=h.useState(null),x=h.useRef(!0),l=h.useRef(0),{on:d}=te(),u=h.useCallback(async()=>{const i=++l.current;try{const m=await X.getSessionSummary(a?{buildId:a}:{since:ce(t,Date.now())});x.current&&i===l.current&&m&&m.current&&r(m)}catch{}},[t,a]);return h.useEffect(()=>{x.current=!0,r(null),u();const i=setInterval(u,Fe);return()=>{x.current=!1,clearInterval(i)}},[u]),h.useEffect(()=>{const i=[d("session_started",u),d("session_stopped",u)];return()=>{for(const m of i)m()}},[d,u]),s}const z=(t,a)=>`${t} ${a}${t===1?"":"s"}`,ze=({build:t})=>{const{passedCount:a,failedCount:s,runningCount:r,sessionCount:x}=t,l=Math.max(0,x-a-s-r);if(x===0)return null;const d=[a>0&&`${a} passed`,s>0&&`${s} failed`,r>0&&`${r} running`,l>0&&`${l} without a verdict`].filter(Boolean);return e.jsxs("div",{role:"img","aria-label":d.join(", "),title:d.join(", "),className:"mt-2 flex h-1 w-full gap-px overflow-hidden rounded-full bg-[rgb(var(--rgb-fg)/0.08)]",children:[a>0&&e.jsx("span",{className:"bg-[var(--color-success)]",style:{flexGrow:a}}),s>0&&e.jsx("span",{className:"bg-[var(--color-danger)]",style:{flexGrow:s}}),r>0&&e.jsx("span",{className:"bg-[var(--color-warning)]",style:{flexGrow:r}}),l>0&&e.jsx("span",{style:{flexGrow:l}})]})},H=({active:t,onClick:a,children:s})=>e.jsxs("button",{type:"button",onClick:a,"aria-current":t?"true":void 0,className:`w-full text-left relative px-4 py-2.5 transition-colors ${t?"bg-[var(--surface-2)]":"hover:bg-[rgb(var(--rgb-fg)/0.04)]"}`,children:[t&&e.jsx("span",{className:"absolute left-0 top-2 bottom-2 w-[3px] rounded-r bg-[var(--color-accent)]"}),s]}),He=({builds:t,selectedBuildId:a,onSelect:s,timeFilter:r})=>{const[x,l]=h.useState(""),d=x.trim().toLowerCase(),u=Date.now(),i=t.filter(c=>{if(d&&!`${I(c)} ${c.name??""} ${c.id}`.toLowerCase().includes(d))return!1;if(r!=="all"){const n=Date.parse(String(c.createdAt));if(!Number.isFinite(n)||u-n>de[r])return!1}return!0}),m=d?"No builds match.":r!=="all"?`No builds in the ${B[r].toLowerCase()}.`:"No builds yet. They appear here after your first test run.";return e.jsxs("aside",{className:"w-[280px] shrink-0 border-r border-[var(--border)] bg-[var(--surface)] flex flex-col min-h-0",children:[e.jsx("div",{className:"p-3 border-b border-[var(--border)]",children:e.jsxs("div",{className:"relative",children:[e.jsx(q,{className:"absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-[var(--text-dim)]"}),e.jsx(J,{type:"text",value:x,onChange:c=>l(c.target.value),placeholder:"Find builds…","aria-label":"Find builds",className:"w-full h-8 pl-8 pr-2 text-xs"})]})}),e.jsxs("div",{role:"group","aria-label":"Builds",className:"flex-1 overflow-y-auto py-1",children:[e.jsxs(H,{active:a===null,onClick:()=>s(null),children:[e.jsxs("div",{className:"flex items-center gap-2 text-[13px] font-medium text-[var(--text)]",children:[e.jsx(ke,{className:"h-3.5 w-3.5 text-[var(--text-dim)]","aria-hidden":"true"}),"All sessions"]}),e.jsxs("div",{className:"mt-0.5 text-[11px] text-[var(--text-muted)]",children:[B[r],", every build"]})]}),e.jsx("div",{className:"px-4 pt-3 pb-1 text-[11px] font-medium text-[var(--text-dim)]",children:"Builds"}),i.length===0&&e.jsx("div",{className:"px-4 py-6 text-xs text-[var(--text-dim)]",children:m}),i.map(c=>{const n=ue(c),j=n?z(c.sessionCount,"session"):`${V(c.createdAt)} · ${z(c.sessionCount,"session")}`;return e.jsxs(H,{active:c.id===a,onClick:()=>s(c.id),children:[e.jsx("div",{className:`text-[13px] font-medium truncate ${n?"text-[var(--text-muted)]":"text-[var(--text)]"}`,title:c.name||void 0,children:I(c)}),e.jsx("div",{className:"mt-0.5 text-[11px] text-[var(--text-muted)] tabular-nums",children:j}),e.jsx(ze,{build:c})]},c.id)})]})]})},Ge={neutral:"bg-[var(--text-dim)]",green:"bg-[var(--color-success)]",red:"bg-[var(--red)]",amber:"bg-[var(--amber)]",blue:"bg-[var(--blue)]"},L=({label:t,count:a,active:s,onClick:r,tone:x="neutral",bullet:l=!0})=>e.jsxs("button",{type:"button",onClick:r,"aria-pressed":s,className:`inline-flex items-center gap-2 h-8 px-3 rounded-md border text-[11px] font-semibold transition-colors cursor-pointer ${s?"bg-[var(--surface-2)] border-[var(--border-strong)] text-[var(--text)]":"bg-transparent border-transparent text-[var(--text-muted)] hover:text-[var(--text)] hover:bg-[var(--surface-2)]/50"}`,children:[l&&x!=="neutral"&&e.jsx("span",{className:`h-1.5 w-1.5 rounded-full ${Ge[x]}`}),e.jsx("span",{children:t}),typeof a=="number"&&e.jsx("span",{className:"font-mono text-[10px] text-[var(--text-dim)]",children:a})]}),Xe=({sessions:t,active:a,onChange:s,search:r,onSearchChange:x,totalMatching:l,totalUnfiltered:d,capped:u=!1})=>{const i=xe(t);return e.jsxs("div",{className:"flex items-center gap-3 px-4 py-2.5 border-b border-[var(--border)] bg-[var(--surface)]",children:[e.jsxs("div",{role:"group","aria-label":"Status",className:"flex items-center gap-1.5",children:[e.jsx(L,{label:"All",count:i.all,active:a==="all",onClick:()=>s("all"),tone:"neutral",bullet:!1}),e.jsx(L,{label:"Passed",count:i.passed,active:a==="passed",onClick:()=>s("passed"),tone:"green"}),e.jsx(L,{label:"Failed",count:i.failed,active:a==="failed",onClick:()=>s("failed"),tone:"red"}),e.jsx(L,{label:"Running",count:i.running,active:a==="running",onClick:()=>s("running"),tone:"amber"})]}),e.jsxs("span",{className:"ml-auto text-[11px] text-[var(--text-dim)] tabular-nums whitespace-nowrap",children:[l," of ",d," sessions",u?" (the newest)":""]}),e.jsxs("div",{className:"relative w-64 shrink-0",children:[e.jsx(q,{className:"absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-[var(--text-dim)]"}),e.jsx(J,{type:"text",value:r,onChange:m=>x(m.target.value),placeholder:"Search tests, devices, people…","aria-label":"Search sessions",className:"w-full h-8 pl-8 pr-2 text-xs"})]})]})};function qe(t){return(t.split("-")[0]||t).slice(0,8).toUpperCase()}const Ve=({build:t,failedToCopy:a,onCopyFailed:s,onExport:r})=>{const[x,l]=_.useState(!1),d=_.useRef(null);_.useEffect(()=>{const i=m=>{d.current&&!d.current.contains(m.target)&&l(!1)};return document.addEventListener("mousedown",i),()=>document.removeEventListener("mousedown",i)},[]);const u=a>0?`Copy the ${a} failed test${a===1?"":"s"}, with why each failed, for a re-run or a ticket`:"No failed tests to copy";return e.jsxs("header",{className:"flex items-start justify-between gap-4 px-4 py-3 border-b border-[var(--border)] bg-[var(--surface)]",children:[e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center gap-2 text-[11px] text-[var(--text-dim)]",children:[e.jsx("span",{children:"Build"}),e.jsxs("span",{className:"font-mono text-[var(--color-accent)]",title:t.id,children:["#",qe(t.id)]}),e.jsx("span",{"aria-hidden":"true",children:"·"}),e.jsxs("span",{className:"tabular-nums",children:["Started ",V(t.createdAt)]})]}),e.jsx("h2",{className:"mt-0.5 text-sm font-semibold text-[var(--text)]",children:I(t)})]}),e.jsxs("div",{className:"flex items-center gap-2",ref:d,children:[e.jsxs(F,{type:"button",variant:"secondary",size:"sm",onClick:s,disabled:a===0,title:u,className:"gap-1.5",children:[e.jsx(Pe,{className:"h-3.5 w-3.5"}),"Copy failed tests"]}),e.jsxs("div",{className:"relative",children:[e.jsxs(F,{type:"button",variant:"secondary",size:"sm",onClick:()=>l(i=>!i),className:"gap-1.5",children:[e.jsx(Te,{className:"h-3.5 w-3.5"}),"Export"]}),x&&e.jsxs("div",{className:"absolute top-full right-0 mt-1 w-40 rounded-md border border-[var(--border)] bg-[var(--surface)] shadow-lg overflow-hidden z-20",children:[e.jsx("button",{type:"button",onClick:()=>{l(!1),r("json")},className:"w-full text-left px-3 py-2 text-xs text-[var(--text)] hover:bg-[var(--surface-2)]",children:"Export as JSON"}),e.jsx("button",{type:"button",onClick:()=>{l(!1),r("csv")},className:"w-full text-left px-3 py-2 text-xs text-[var(--text)] hover:bg-[var(--surface-2)]",children:"Export as CSV"})]})]})]})]})};function We({platform:t}){const a=(t||"").toLowerCase(),s="h-3.5 w-3.5 shrink-0 text-[var(--text-dim)]";return a.includes("tv")?e.jsx(ae,{className:s}):a.includes("pad")||a.includes("tablet")?e.jsx(Ie,{className:s}):a.includes("android")||a.includes("ios")?e.jsx(re,{className:s}):e.jsx(Le,{className:s})}const Je={passed:{label:"Passed",Icon:Me,cls:"text-[var(--color-success)]"},failed:{label:"Failed",Icon:Re,cls:"text-[var(--color-danger)]"},running:{label:"Running",Icon:se,cls:"text-[var(--color-warning)]"},other:{Icon:be,cls:"text-[var(--text-dim)]"}},Ke="Unnamed session. Set xe:options.name in its capabilities to name it.",Qe=({session:t,buildName:a,showBuild:s,showSelection:r,selected:x,onToggleSelect:l,onOpen:d})=>{var N,C,$;const u=me(t.status),i=Je[u],m=u==="failed",c=pe(t),n=m?(N=t.failure_reason)==null?void 0:N.trim():"",j=[he(t),t.device_version].filter(f=>f&&f!=="—").join(" "),S=[fe(t),j].filter(Boolean).join(" · "),v=[ve(t.ranOn),(C=t.owner)==null?void 0:C.name].filter(Boolean).join(" · "),p=m?"shadow-[inset_3px_0_0_var(--color-danger)]":"";return e.jsxs("tr",{onClick:d,tabIndex:0,onKeyDown:f=>{f.target!==f.currentTarget||f.key!=="Enter"&&f.key!==" "||(f.preventDefault(),d())},"data-outcome":u,className:`group border-b border-[var(--border)] cursor-pointer transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--color-focus-ring)] ${m?"bg-[rgb(var(--rgb-red)/0.04)] hover:bg-[rgb(var(--rgb-red)/0.08)]":"hover:bg-[var(--surface-2)]"}`,children:[r&&e.jsx("td",{className:`pl-4 pr-2 py-3 align-top ${p}`,onClick:f=>f.stopPropagation(),children:e.jsx("input",{type:"checkbox",checked:x,onChange:l,"aria-label":`Select session ${c.text}`})}),e.jsx("td",{className:`px-4 py-3 align-top ${r?"":p}`,children:e.jsxs("span",{className:`inline-flex items-center gap-1.5 text-xs font-medium ${i.cls}`,children:[e.jsx(i.Icon,{className:`h-3.5 w-3.5 shrink-0 ${u==="running"?"animate-spin":""}`,"aria-hidden":"true"}),i.label??Ee(t.status||"unknown")]})}),e.jsxs("td",{className:"px-3 py-3 align-top min-w-0",children:[e.jsx("div",{className:`text-[13px] font-medium truncate ${c.source==="name"?"text-[var(--text)]":"text-[var(--text-muted)]"}`,title:c.source==="name"?c.text:`${c.text}
7
- ${Ke}`,children:c.text}),n?e.jsx("div",{className:"mt-0.5 text-[11px] text-[var(--color-danger)] truncate",title:n,children:n}):s&&a&&e.jsx("div",{className:"mt-0.5 text-[11px] text-[var(--text-muted)] truncate",title:a,children:a})]}),e.jsxs("td",{className:"px-3 py-3 align-top min-w-0",children:[e.jsxs("div",{className:"flex items-center gap-1.5 text-[13px] text-[var(--text)] min-w-0",children:[e.jsx(We,{platform:t.device_platform}),e.jsx("span",{className:"truncate",title:t.device_udid,children:S})]}),v&&e.jsx("div",{className:"mt-0.5 text-[11px] text-[var(--text-muted)] truncate",title:[t.ranOn==="here"?null:`Node ${t.node_id}`,($=t.owner)==null?void 0:$.email].filter(Boolean).join(`
8
- `),children:v})]}),e.jsx("td",{className:"px-3 py-3 align-top text-xs text-[var(--text-muted)] tabular-nums whitespace-nowrap",title:je(t.startTime),children:ge(t.startTime)}),e.jsx("td",{className:"px-3 py-3 align-top text-xs text-[var(--text)] tabular-nums whitespace-nowrap text-right",children:O(Ne(t))}),e.jsx("td",{className:"pr-4 pl-1 py-3 align-top text-right",children:e.jsx(Be,{className:"inline h-4 w-4 text-[var(--text-dim)] group-hover:text-[var(--text)]","aria-hidden":"true"})})]})},M="px-3 py-2 text-[11px] font-medium text-[var(--text-dim)]",G=({sessions:t,statusFilter:a,searchQuery:s,buildNames:r,showBuild:x,showSelection:l,selectedIds:d,onToggleSelect:u,onToggleSelectAll:i,onOpenRow:m,empty:c})=>{const n=h.useMemo(()=>W(t,a,s),[t,a,s]);if(t.length===0)return e.jsx(e.Fragment,{children:c});if(n.length===0)return e.jsx("div",{className:"px-6 py-12 text-center text-xs text-[var(--text-dim)]",children:"No sessions match the current filters."});const j=n.every(p=>d.has(p.id)),S=n.some(p=>d.has(p.id)),v=n.some(p=>{var N;return!((N=p.name)!=null&&N.trim())});return e.jsxs("div",{className:"flex-1 overflow-y-auto",children:[e.jsxs("table",{className:"w-full text-left table-fixed",children:[e.jsx("thead",{className:"sticky top-0 bg-[var(--surface)] border-b border-[var(--border)] z-10",children:e.jsxs("tr",{children:[l&&e.jsx("th",{className:"pl-4 pr-2 py-2 w-[44px]",children:e.jsx("input",{type:"checkbox","aria-label":"Select all shown",checked:j,ref:p=>{p&&(p.indeterminate=S&&!j)},onChange:()=>i(n.map(p=>p.id))})}),e.jsx("th",{className:`${M} pl-4 w-[112px]`,children:"Status"}),e.jsx("th",{className:M,children:"Test"}),e.jsx("th",{className:`${M} w-[240px]`,children:"Device"}),e.jsx("th",{className:`${M} w-[124px]`,children:"Started"}),e.jsx("th",{className:`${M} w-[84px] text-right`,children:"Duration"}),e.jsx("th",{className:"w-[40px]",children:e.jsx("span",{className:"sr-only",children:"Open"})})]})}),e.jsx("tbody",{children:n.map(p=>e.jsx(Qe,{session:p,buildName:p.build_id?r.get(p.build_id):null,showBuild:x,showSelection:l,selected:d.has(p.id),onToggleSelect:()=>u(p.id),onOpen:()=>m(p)},p.id))})]}),v&&e.jsxs("p",{className:"px-4 py-3 text-[11px] text-[var(--text-dim)]",children:["Unnamed sessions show their app. Name a test with"," ",e.jsx("code",{className:"font-mono text-[var(--text-muted)]",children:"xe:options.name"})," and a run with"," ",e.jsx("code",{className:"font-mono text-[var(--text-muted)]",children:"xe:options.build"})," in its capabilities."]})]})};function Ye(t,a){const s=Math.round(t);return s===0?{text:`No change vs prior ${a}`,tone:"neutral"}:s>0?{text:`+${s} pts vs prior ${a}`,tone:"good"}:{text:`−${-s} pts vs prior ${a}`,tone:"bad"}}const Ze=({summary:t,periodLabel:a})=>{if(!t)return e.jsxs("div",{className:"grid grid-cols-4 gap-3 px-6 py-3 border-b border-[var(--border)]",children:[e.jsx(w,{label:"Pass rate",value:"—"}),e.jsx(w,{label:"Failed",value:"—"}),e.jsx(w,{label:"Running now",value:"—"}),e.jsx(w,{label:"Median duration",value:"—"})]});const{current:s,previous:r,runningNow:x}=t,l=ye(s),d=s.passed+s.failed,u=a?we(s,r):null,i=u!==null?Ye(u,a):d>0?{text:`${s.passed} of ${d} passed`,tone:"neutral"}:{text:"No finished sessions",tone:"neutral"};return e.jsxs("div",{className:"grid grid-cols-4 gap-3 px-6 py-3 border-b border-[var(--border)]",children:[e.jsx(w,{label:"Pass rate",value:l===null?"—":`${Math.round(l)}%`,note:i.text,noteTone:i.tone}),e.jsx(w,{label:"Failed",value:s.failed,valueCls:s.failed>0?"text-[var(--color-danger)]":"text-[var(--text)]",note:`of ${s.total} session${s.total===1?"":"s"}`}),e.jsx(w,{label:"Running now",value:x.sessions,note:x.sessions>0?`on ${x.devices} device${x.devices===1?"":"s"}`:"Nothing running"}),e.jsx(w,{label:"Median duration",value:O(s.medianMs),note:s.p90Ms===null?"No finished sessions":`p90 ${O(s.p90Ms)}`})]})},et={"24h":"24 hours","7d":"7 days","30d":"30 days"},tt=`"xe:options": {
9
- "name": "Checkout happy path",
10
- "build": "Nightly smoke"
11
- }`,st=()=>e.jsxs("div",{className:"text-xs text-[var(--text-muted)]",children:[e.jsx("p",{children:"Name each test, and group a run, in its capabilities:"}),e.jsx("pre",{className:"mt-2 inline-block rounded-md border border-[var(--border)] bg-[var(--surface-2)] px-3 py-2 text-left font-mono text-[11px] text-[var(--text)]",children:tt})]}),Nt=()=>{const t=ne(),{buildId:a}=le(),{toast:s}=oe(),[r,x]=h.useState("7d"),[l,d]=h.useState("all"),[u,i]=h.useState(""),[m,c]=h.useState(new Set),n=Se({withSessions:!0,timeFilter:r}),j=Ue(r,n.selectedBuildId),{selectBuild:S,selectedBuildId:v}=n;_.useEffect(()=>{const o=a??null;o!==v&&(S(o),c(new Set))},[a,v,S]);const p=o=>t(o?`/builds/${o}`:"/builds"),N=o=>t(`/builds/${encodeURIComponent(o.build_id||"none")}/sessions/${o.id}`),C=h.useCallback(o=>{c(b=>{const g=new Set(b);return g.has(o)?g.delete(o):g.add(o),g})},[]),$=h.useCallback(o=>{c(b=>{const g=o.length>0&&o.every(y=>b.has(y)),E=new Set(b);return g?o.forEach(y=>E.delete(y)):o.forEach(y=>E.add(y)),E})},[]),f=h.useMemo(()=>new Map(n.builds.map(o=>[o.id,I(o)])),[n.builds]),k=n.builds.find(o=>o.id===v)||null,T=Ce(k?I(k):"this build",n.sessions,m),K=async()=>{if(T.count!==0)try{await navigator.clipboard.writeText(T.text),s(`Copied ${T.count} failed test${T.count===1?"":"s"}`,"success")}catch{s("Clipboard unavailable","error")}},Q=async o=>{if(v)try{const b=m.size>0?Array.from(m):void 0,{blob:g,filename:E}=await X.exportBuild(v,o,b),y=URL.createObjectURL(g),R=document.createElement("a");R.href=y,R.download=E,document.body.appendChild(R),R.click(),document.body.removeChild(R),URL.revokeObjectURL(y);const P=b?b.length:"all";s(`Exported ${P} session${P===1?"":"s"} as ${o.toUpperCase()}.`,"success")}catch(b){const g=b instanceof Error?b.message:String(b);s(`Export failed: ${g}`,"error")}},D=v===null,Y=D&&r!=="all"?et[r]:null,Z=!D&&!k&&!n.loading,A=e.jsx(Xe,{sessions:n.sessions,active:l,onChange:d,search:u,onSearchChange:i,totalMatching:W(n.sessions,l,u).length,totalUnfiltered:n.sessions.length,capped:n.sessions.length>=$e});return e.jsxs("div",{className:"flex flex-col h-full",children:[e.jsx(_e,{icon:ie,title:"Sessions",subtitle:"Every test session, newest first, with its build, device and outcome.",action:e.jsx(De,{value:r,onChange:o=>x(o.target.value),selectSize:"sm","aria-label":"Period",className:"w-40 text-xs",children:Object.keys(B).map(o=>e.jsx("option",{value:o,children:B[o]},o))})}),e.jsx(Ze,{summary:j,periodLabel:Y}),e.jsxs("div",{className:"flex flex-1 min-h-0",children:[e.jsx(He,{builds:n.builds,selectedBuildId:v,onSelect:p,timeFilter:r}),e.jsx("section",{className:"flex-1 flex flex-col min-w-0",children:Z?e.jsx(U,{title:"This build isn’t available",description:"It may have been removed, or belong to a team you’re not on."}):D?e.jsxs(e.Fragment,{children:[e.jsx("h2",{className:"sr-only",children:"All sessions"}),A,e.jsx(G,{sessions:n.sessions,statusFilter:l,searchQuery:u,buildNames:f,showBuild:!0,showSelection:!1,selectedIds:m,onToggleSelect:C,onToggleSelectAll:$,onOpenRow:N,empty:n.loading?null:e.jsx("div",{className:"px-6 py-6",children:e.jsx(U,{icon:e.jsx(Oe,{className:"h-5 w-5"}),title:r==="all"?"No sessions yet":`No sessions in the ${B[r].toLowerCase()}`,description:"Sessions appear here as your tests run.",action:e.jsx(st,{})})})})]}):k?e.jsxs(e.Fragment,{children:[e.jsx(Ve,{build:k,failedToCopy:T.count,onCopyFailed:K,onExport:Q}),A,e.jsx(G,{sessions:n.sessions,statusFilter:l,searchQuery:u,buildNames:f,showBuild:!1,showSelection:!0,selectedIds:m,onToggleSelect:C,onToggleSelectAll:$,onOpenRow:N,empty:e.jsx("div",{className:"px-6 py-12 text-center text-xs text-[var(--text-dim)]",children:"No sessions in this build yet. Trigger one from your test runner."})})]}):null})]})]})};export{Nt as BuildsPage,Nt as default};
@@ -1,8 +0,0 @@
1
- import{c as F,r as u,p as I,X as M,j as b}from"./index-B1iJx0w1.js";import{f as A}from"./time-B8FIgwPE.js";/**
2
- * @license lucide-react v0.555.0 - ISC
3
- *
4
- * This source code is licensed under the ISC license.
5
- * See the LICENSE file in the root directory of this source tree.
6
- */const E=[["path",{d:"M10.1 2.182a10 10 0 0 1 3.8 0",key:"5ilxe3"}],["path",{d:"M13.9 21.818a10 10 0 0 1-3.8 0",key:"11zvb9"}],["path",{d:"M17.609 3.721a10 10 0 0 1 2.69 2.7",key:"1iw5b2"}],["path",{d:"M2.182 13.9a10 10 0 0 1 0-3.8",key:"c0bmvh"}],["path",{d:"M20.279 17.609a10 10 0 0 1-2.7 2.69",key:"1ruxm7"}],["path",{d:"M21.818 10.1a10 10 0 0 1 0 3.8",key:"qkgqxc"}],["path",{d:"M3.721 6.391a10 10 0 0 1 2.7-2.69",key:"1mcia2"}],["path",{d:"M6.391 20.279a10 10 0 0 1-2.69-2.7",key:"1fvljs"}]],Y=F("circle-dashed",E);function y(t){switch(t){case"success":case"passed":case"ended":return"passed";case"failed":case"error":case"timeout":return"failed";case"running":return"running";default:return"other"}}function X(t){const e={all:t.length,passed:0,failed:0,running:0};for(const n of t){const r=y(n.status);r!=="other"&&(e[r]+=1)}return e}function B(t,e){return e==="all"?!0:y(t.status)===e}function V(t,e,n){const r=n.trim().toLowerCase();return t.filter(s=>{var o,l;return!(!B(s,e)||r&&![s.id,L(s).text,s.name,s.failure_reason,s.device_name,s.device_platform,s.device_version,s.node_id,s.ranOn,(o=s.owner)==null?void 0:o.name,(l=s.owner)==null?void 0:l.email].filter(Boolean).join(" ").toLowerCase().includes(r))})}function j(t){var n;const e=(n=t.device_name)==null?void 0:n.trim();return e&&e.length>0?e:"Unknown Device"}function C(t){const e=t.device_platform??"";if(!e)return"—";const n=e.toLowerCase();return n==="ios"?"iOS":n==="tvos"?"tvOS":n.charAt(0).toUpperCase()+n.slice(1)}function G(t){return A(t)}function K(t){if(!t.startTime)return null;const e=Date.parse(t.startTime);if(!Number.isFinite(e))return null;const n=t.endTime?Date.parse(t.endTime):Date.now();return Number.isFinite(n)?Math.max(0,n-e):null}function Q(t){if(t==null||!Number.isFinite(t)||t<0)return"—";const e=t/1e3,n=Math.floor(e/3600),r=Math.floor(e%3600/60),o=(e-n*3600-r*60).toFixed(1);return n>0?`${n}h ${r}m ${o}s`:r>0?`${r}m ${o}s`:`${o}s`}const T=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],S=t=>String(t).padStart(2,"0");function v(t,e){const n=t[`appium:${e}`]??t[e];return typeof n=="string"&&n.trim()?n.trim():null}function O(t){if(!t)return null;let e;try{e=JSON.parse(t)}catch{return null}if(!e||typeof e!="object")return null;let n=e;if(n.capabilities&&typeof n.capabilities=="object"&&(n=n.capabilities),n.alwaysMatch||n.firstMatch){const r=Array.isArray(n.firstMatch)?n.firstMatch[0]:void 0;return{...n.alwaysMatch||{},...r||{}}}return n}function _(t){const e=O(t);if(!e)return null;const n=v(e,"appPackage")??v(e,"bundleId");if(n)return n;const r=v(e,"app");if(r){const s=r.split(/[?#]/)[0].split(/[\\/]/).pop()??"";if(s&&s!=="download")return s}return v(e,"browserName")}function L(t){var r;const e=(r=t.name)==null?void 0:r.trim();if(e)return{text:e,source:"name"};const n=_(t.session_capabilities)??_(t.desired_capabilities);return n?{text:n,source:"app"}:{text:`Session ${t.id.slice(0,8)}`,source:"id"}}function D(t){return`${T[t.getMonth()]} ${t.getDate()}, ${S(t.getHours())}:${S(t.getMinutes())}`}function W(t){const e=t?new Date(t):null;return e&&!Number.isNaN(e.getTime())?D(e):"—"}function R(t){var n;const e=(n=t.name)==null?void 0:n.trim();return!e||e==="Default Build"}function Z(t){if(!R(t))return t.name.trim();const e=new Date(t.createdAt);return Number.isNaN(e.getTime())?"Build":`Build · ${D(e)}`}const tt={all:"All time","24h":"Last 24 hours","7d":"Last 7 days","30d":"Last 30 days"},P={"24h":864e5,"7d":7*864e5,"30d":30*864e5};function q(t,e){return t==="all"?null:new Date(e-P[t]).toISOString()}function et(t){if(t==null||!Number.isFinite(t)||t<0)return"—";if(t<1e3)return"<1s";const e=Math.floor(t/1e3),n=Math.floor(e/3600),r=Math.floor(e%3600/60),s=e%60;return n>0?`${n}h ${r}m`:r>0?`${r}m ${s}s`:`${s}s`}function nt(t,e=new Date){if(!t)return"—";const n=new Date(t);if(Number.isNaN(n.getTime()))return"—";const r=`${S(n.getHours())}:${S(n.getMinutes())}`;return n.getFullYear()!==e.getFullYear()?`${T[n.getMonth()]} ${n.getDate()}, ${n.getFullYear()} ${r}`:n.getMonth()===e.getMonth()&&n.getDate()===e.getDate()?r:D(n)}function w(t){const e=t.passed+t.failed;return e>0?t.passed/e*100:null}function rt(t,e){const n=w(t),r=e?w(e):null;return n===null||r===null?null:n-r}function st(t){return t?t==="here"?"This server":t:null}function at(t,e,n){var c,h;const r=n&&n.size>0?e.filter(i=>n.has(i.id)):e,s=r.filter(i=>y(i.status)==="failed").reverse();if(s.length===0)return{count:0,text:""};const o=n&&n.size>0?`${r.length} selected session${r.length===1?"":"s"}`:`${r.length} session${r.length===1?"":"s"}`,l=[`Failed tests in ${t}: ${s.length} of ${o}`,""];for(const i of s){const $=((h=(c=i.failure_reason)==null?void 0:c.split(`
7
- `)[0])==null?void 0:h.trim())||"No reason recorded",x=[C(i),i.device_version].filter(d=>d&&d!=="—").join(" ");l.push(`- ${L(i).text}`,` ${$}`,` ${[j(i),x,`session ${i.id}`].filter(Boolean).join(" · ")}`)}return{count:s.length,text:l.join(`
8
- `)}}const H=3e3,it=500;function ot(t={}){const{withSessions:e=!1,timeFilter:n="all"}=t,[r,s]=u.useState([]),[o,l]=u.useState([]),[c,h]=u.useState(null),[i,$]=u.useState(!0),[x,d]=u.useState(null),m=u.useRef(!0),{on:p}=I(),N=u.useCallback(()=>{if(!e)return Promise.resolve([]);if(c)return M.getSessions({buildId:c});const a=q(n,Date.now())??void 0;return M.getSessions({since:a})},[e,c,n]),f=u.useCallback(async()=>{try{const[a,g]=await Promise.all([M.getBuilds(),N()]);if(!m.current)return;s(Array.isArray(a)?a:[]),l(Array.isArray(g)?g:[]),d(null)}catch(a){if(!m.current)return;d(a instanceof Error?a.message:String(a))}finally{m.current&&$(!1)}},[N]);return u.useEffect(()=>{m.current=!0,f();const a=setInterval(f,H);return()=>{m.current=!1,clearInterval(a)}},[f]),u.useEffect(()=>{const a=()=>f(),g=[p("session_started",a),p("session_stopped",a),p("device_added",a),p("device_removed",a)];return()=>{for(const k of g)k()}},[p,f]),{builds:r,selectedBuildId:c,sessions:o,loading:i,error:x,selectBuild:h,refresh:f}}const J={neutral:"text-[var(--text-muted)]",good:"text-[var(--color-success)]",bad:"text-[var(--color-danger)]"},ut=({label:t,value:e,valueCls:n="text-[var(--text)]",note:r,noteTone:s="neutral"})=>b.jsxs("div",{className:"min-w-0 rounded-lg border border-[var(--border)] bg-[var(--surface)] px-4 py-3",children:[b.jsx("div",{className:"text-xs text-[var(--text-muted)]",children:t}),b.jsx("div",{className:`mt-1 text-2xl font-semibold tabular-nums leading-tight truncate ${n}`,children:e}),b.jsx("div",{className:`mt-0.5 text-[11px] truncate ${J[s]}`,children:r??" "})]});export{Y as C,P,ut as S,tt as T,X as a,Z as b,y as c,L as d,j as e,W as f,nt as g,G as h,R as i,et as j,K as k,V as l,w as m,rt as n,at as o,C as p,it as q,st as r,q as s,Q as t,ot as u};