@xenon-device-management/xenon 2.6.1 → 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.
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-BR2MSqZq.js → AnnotationOverlay-oISPBknJ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-B-kYDgwU.js → ApiKeyGate-BOm-SKAJ.js} +1 -1
- package/lib/public/assets/{BugReportButton-B5AX_Hq6.js → BugReportButton-C1WMf_3A.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-wL6hPElQ.js → DeviceMosaicView-BPJ5n5fY.js} +1 -1
- package/lib/public/assets/{EmptyState-DMkfqTdS.js → EmptyState-CZg80GnN.js} +1 -1
- package/lib/public/assets/{FieldGroup-C48fEQ2R.js → FieldGroup-D7zftz-q.js} +1 -1
- package/lib/public/assets/{FilterMenu-BEQWiqgS.js → FilterMenu-BKHKlX-8.js} +1 -1
- package/lib/public/assets/{Menu-ChzfRWa3.js → Menu-Ccznqijj.js} +1 -1
- package/lib/public/assets/{Modal-D4LAJuLk.js → Modal-FxdOVK12.js} +1 -1
- package/lib/public/assets/{RecordingPage-CErhVVhI.js → RecordingPage-BL8B1IKY.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CiwOgvEu.js → RecordingsPage-CCCL2xwa.js} +1 -1
- package/lib/public/assets/{SegmentedControl-CEGbL4Og.js → SegmentedControl-B3EMys8L.js} +1 -1
- package/lib/public/assets/{SettingCard-Ixm55gZS.js → SettingCard-BwvdX-C6.js} +1 -1
- package/lib/public/assets/{Table-C7osPLTJ.js → Table-sGNs22pM.js} +1 -1
- package/lib/public/assets/{activity-B6i2UUaG.js → activity-rQQOy6SD.js} +1 -1
- package/lib/public/assets/{ai-settings-D7gK7P2L.js → ai-settings-uIrII3el.js} +1 -1
- package/lib/public/assets/{api-keys-DrBdSosA.js → api-keys-B1-QmrZJ.js} +1 -1
- package/lib/public/assets/{apps-BWEUuoIK.js → apps-B8gYjYhN.js} +1 -1
- package/lib/public/assets/{arrow-left-DcJsqErm.js → arrow-left-CLT_rU6o.js} +1 -1
- package/lib/public/assets/{arrow-right-_g7OIFHi.js → arrow-right-DqK8eHkB.js} +1 -1
- package/lib/public/assets/{arrow-up-right-Bn6qC1nH.js → arrow-up-right-D96XxKQY.js} +1 -1
- package/lib/public/assets/{auth-shell-DslyrtGF.js → auth-shell-DpBFmfin.js} +2 -2
- package/lib/public/assets/builds-page-DqD7ySkz.js +11 -0
- package/lib/public/assets/{button-C8WXV74W.js → button-BrVVNvSB.js} +1 -1
- package/lib/public/assets/{calendar-D6zwX5yt.js → calendar-FX_no0De.js} +1 -1
- package/lib/public/assets/{check-DHCAmQzm.js → check-C6RAxVq9.js} +1 -1
- package/lib/public/assets/{chevron-left-DAjBpmZL.js → chevron-left-CcTJUfpS.js} +1 -1
- package/lib/public/assets/{chevron-right-CXeI6N1r.js → chevron-right-C39WUDQP.js} +1 -1
- package/lib/public/assets/{circle-check-CXNkbyeD.js → circle-check-Fo1eEzZe.js} +1 -1
- package/lib/public/assets/{circle-x-5wGBLvP4.js → circle-x-CXy5XzIu.js} +1 -1
- package/lib/public/assets/{clock-B6D_VfTn.js → clock-PBGPHMrI.js} +1 -1
- package/lib/public/assets/{copy-B01YbVjr.js → copy-ZGa-scbM.js} +1 -1
- package/lib/public/assets/{device-explorer-BJYJi3-I.js → device-explorer-BRYO7iUM.js} +1 -1
- package/lib/public/assets/{download-CYOnymUH.js → download-vaODl7Bn.js} +1 -1
- package/lib/public/assets/{forgot-password-ChGJEfSH.js → forgot-password-YfGbiIOM.js} +1 -1
- package/lib/public/assets/{funnel-Bsf-_RST.js → funnel-C05BIqke.js} +1 -1
- package/lib/public/assets/{index-DC1NFStQ.js → index-1ST_YG3o.js} +1 -1
- package/lib/public/assets/{index-rullcuA-.js → index-DdsuniRs.js} +2 -2
- package/lib/public/assets/{input-DAFtgyoC.js → input-C6Zp2Ibb.js} +1 -1
- package/lib/public/assets/{keycodes-0UtDeccx.js → keycodes-BlkuYXfQ.js} +1 -1
- package/lib/public/assets/{layers-C9QEszDA.js → layers-B-3IDp3k.js} +1 -1
- package/lib/public/assets/{list-checks-BDs_WJqC.js → list-checks-BUUYV0Vf.js} +1 -1
- package/lib/public/assets/{lock-Wfb-mfRt.js → lock-BUdNqIao.js} +1 -1
- package/lib/public/assets/{login-7ukAAXMQ.js → login-CD8bag9i.js} +1 -1
- package/lib/public/assets/{maintenance-settings-HJVTzD1e.js → maintenance-settings-BWQP_nfK.js} +1 -1
- package/lib/public/assets/{monitor-CdNmIwjG.js → monitor-qfb5f9oQ.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-qVSJPuaG.js → mouse-pointer-2-DAAlUY2l.js} +1 -1
- package/lib/public/assets/{network-BdrZxJP7.js → network-Qib4-4Yj.js} +1 -1
- package/lib/public/assets/{overview-C0JVQ10q.js → overview-BZQf20qe.js} +1 -1
- package/lib/public/assets/{page-header-BNgF7qLO.js → page-header-4jNxdU1M.js} +1 -1
- package/lib/public/assets/{plus-bjDKJLSM.js → plus-DVhPNSQy.js} +1 -1
- package/lib/public/assets/{profile-page-Dgqh9iFO.js → profile-page-DfITRDDC.js} +1 -1
- package/lib/public/assets/{reset-password-BKkG0r19.js → reset-password-s2xU05oa.js} +1 -1
- package/lib/public/assets/{runbook-page-CMhZZJVm.js → runbook-page-DdEiXQcm.js} +1 -1
- package/lib/public/assets/{select-C563Otbb.js → select-Bzs3ZUvb.js} +1 -1
- package/lib/public/assets/{selector-detail-page-DhnD3eaZ.js → selector-detail-page-QvRRVchI.js} +1 -1
- package/lib/public/assets/{selector-health-B7Oe-x9R.js → selector-health-DwUFxrZx.js} +1 -1
- package/lib/public/assets/{selector-health-page-BzP4CuGG.js → selector-health-page-N0tA9q9c.js} +1 -1
- package/lib/public/assets/{session-detail-page-DYccmo6I.js → session-detail-page-DcGGXXxL.js} +3 -3
- package/lib/public/assets/{settings-BtiYwsCN.js → settings-BUqAjzeY.js} +1 -1
- package/lib/public/assets/stat-tile-BwZr7q7_.js +8 -0
- package/lib/public/assets/{tablet-Dt9_zCsE.js → tablet-DiOf-kLM.js} +1 -1
- package/lib/public/assets/{teams-C-TH1VHJ.js → teams-DA6AfWGO.js} +1 -1
- package/lib/public/assets/{trash-2-Cy3hmCGn.js → trash-2-BZuuog-n.js} +1 -1
- package/lib/public/assets/{upload-BzeSfIf5.js → upload-DLHV2k4Z.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-CFPUu_Pu.js → use-password-reset-mode-DRY4lLdd.js} +1 -1
- package/lib/public/assets/{users-ByGYDd3w.js → users-Bd4LKBWY.js} +1 -1
- package/lib/public/assets/{users-CZwclIT8.js → users-BlhcXeTG.js} +1 -1
- package/lib/public/assets/{video-off-soegR0jK.js → video-off-DIaDZGNl.js} +1 -1
- package/lib/public/assets/{webhook-settings-DHkmj77L.js → webhook-settings-IWvqbRk2.js} +1 -1
- package/lib/public/assets/{zap-4ACghMsl.js → zap-4Qy4hVbF.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/routers/dashboard.js +21 -11
- package/lib/src/app/routers/sessionInsights.js +46 -9
- package/lib/test/unit/session-list-insights.spec.js +39 -0
- package/package.json +1 -1
- package/lib/public/assets/builds-page-CEZ9IZ9S.js +0 -11
- package/lib/public/assets/stat-tile-BlmNnQFY.js +0 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as w,b as k,r as a,j as e,$ as C}from"./index-
|
|
1
|
+
import{c as w,b as k,r as a,j as e,$ as C}from"./index-DdsuniRs.js";import{l as M,d as R,c as I,a as A,u as D}from"./users-BlhcXeTG.js";import{f as U}from"./time-B8FIgwPE.js";import{P}from"./page-header-4jNxdU1M.js";import{B as y}from"./button-BrVVNvSB.js";import{I as N}from"./input-C6Zp2Ibb.js";import{S as g}from"./select-Bzs3ZUvb.js";import{P as B}from"./plus-DVhPNSQy.js";import{T as L}from"./trash-2-BZuuog-n.js";import"./utils-CDN07tui.js";/**
|
|
2
2
|
* @license lucide-react v0.555.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as s}from"./index-
|
|
1
|
+
import{o as s}from"./index-DdsuniRs.js";const o="/xenon/api/users";async function c(){const t=await fetch(`${o}/`,{credentials:"include"});if(!t.ok)throw new Error(`listUsers failed (${t.status})`);return t.json()}async function i(t){const e=await fetch(`${o}/`,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",...s},body:JSON.stringify(t)});if(!e.ok){const n=await e.json().catch(()=>({}));throw new Error(n.error||`createUser failed (${e.status})`)}return e.json()}async function d(t,e){const n=await fetch(`${o}/${encodeURIComponent(t)}`,{method:"PATCH",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`updateUser failed (${n.status})`)}return n.json()}async function l(t){const e=await fetch(`${o}/${encodeURIComponent(t)}`,{method:"DELETE",credentials:"include"});if(!e.ok&&e.status!==204){const n=await e.json().catch(()=>({}));throw new Error(n.error||`deleteUser failed (${e.status})`)}}async function u(t){const e=await fetch(`${o}/${encodeURIComponent(t)}/reset-link`,{method:"POST",credentials:"include"}),n=await e.json().catch(()=>({}));if(!e.ok)throw new Error(n.error||`Reset link failed (${e.status})`);return n}export{i as a,u as c,l as d,c as l,d as u};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as P,r,X as c,j as e,J as u,A as b,C as U,K as D}from"./index-
|
|
1
|
+
import{t as P,r,X as c,j as e,J as u,A as b,C as U,K as D}from"./index-DdsuniRs.js";/* empty css */import{F as S}from"./FieldGroup-D7zftz-q.js";import{P as O}from"./page-header-4jNxdU1M.js";import{T as V}from"./trash-2-BZuuog-n.js";import{P as x}from"./plus-DVhPNSQy.js";import{C as B}from"./circle-x-CXy5XzIu.js";import{C as H}from"./circle-check-Fo1eEzZe.js";import{Z as I}from"./zap-4Qy4hVbF.js";import{A as J}from"./activity-rQQOy6SD.js";const C=[{id:"device_offline",label:"Device offline",icon:U,tone:"red"},{id:"device_new",label:"New device",icon:x,tone:"green"},{id:"session_failed",label:"Session failed",icon:B,tone:"amber"},{id:"selector_health_digest",label:"Selector health digest",icon:D,tone:"green"}],X=["udid","host","name","sessionId","failureReason","eventType","platform"],ae=()=>{const{toast:o}=P(),[g,T]=r.useState([]),[A,w]=r.useState(!0),[l,j]=r.useState(""),[d,k]=r.useState(["device_offline","session_failed"]),[h,f]=r.useState(!1),[v,N]=r.useState(!1),[n,_]=r.useState(!1),[y,m]=r.useState("");r.useEffect(()=>{p()},[]);const p=async()=>{w(!0);try{const s=await c.getWebhookConfigs();T(s||[])}catch(s){console.error("Failed to load webhook configs",s),o("Failed to load webhooks.","error")}finally{w(!1)}},z=async()=>{if(!l.trim()){o("Webhook URL is required.","error");return}if(d.length===0){o("Select at least one trigger event.","error");return}f(!0);try{await c.addWebhookConfig(l.trim(),d,"slack",y||void 0),j(""),m(""),_(!1),k(["device_offline","session_failed"]),await p(),o("Webhook saved.","success")}catch(s){console.error("Failed to add webhook",s),o(`Failed to add webhook: ${s.message||"unknown error"}`,"error")}finally{f(!1)}},E=async(s,t)=>{if(window.confirm(`Remove webhook for "${t}"?`))try{await c.deleteWebhookConfig(s),await p(),o("Webhook removed.","success")}catch(a){console.error("Failed to delete webhook",a),o(`Failed to delete: ${a.message||"unknown error"}`,"error")}},L=async()=>{if(l.trim()){N(!0);try{await c.testWebhook(l.trim(),"slack"),o("Test payload delivered.","success")}catch(s){o(`Test failed: ${s.message||"check the URL"}`,"error")}finally{N(!1)}}},$=s=>{k(t=>t.includes(s)?t.filter(a=>a!==s):[...t,s])},R=s=>{m(t=>t+`{{${s}}} `)};return e.jsxs("div",{className:"settings-container",children:[e.jsx(O,{icon:u,title:"Notifications",subtitle:"Configure Slack or generic webhooks to receive alerts for critical infrastructure events."}),e.jsxs("div",{className:"settings-content",children:[A?e.jsxs("div",{className:"settings-loading",style:{height:200},children:[e.jsx(b,{className:"animate-spin",size:28}),e.jsx("span",{children:"Loading webhooks…"})]}):g.length===0?e.jsxs("div",{className:"empty-state",children:[e.jsx("div",{className:"empty-state__icon",children:e.jsx(u,{size:28})}),e.jsx("h3",{className:"empty-state__title",children:"No webhooks configured"}),e.jsx("p",{className:"empty-state__copy",children:"Wire up a Slack incoming webhook or any HTTPS endpoint to receive alerts when devices go offline, new devices register, or sessions fail. Configure your first webhook below."})]}):e.jsx("div",{className:"webhook-list-grid",children:g.map(s=>{const t=(()=>{try{return JSON.parse(s.events)}catch{return[]}})();return e.jsxs("div",{className:"webhook-row-card",children:[e.jsxs("div",{className:"webhook-row-card__header",children:[e.jsx("span",{className:`pill-chip ${s.payloadTemplate?"pill-chip--admin":"pill-chip--scope"}`,children:s.payloadTemplate?"CUSTOM":"SLACK"}),e.jsx("span",{className:"webhook-row-card__url",title:s.url,children:s.url}),e.jsxs("button",{type:"button",className:"row-action row-action--danger",onClick:()=>E(s.id,s.url),"aria-label":`Remove webhook for ${s.url}`,children:[e.jsx(V,{size:13}),e.jsx("span",{children:"Remove"})]})]}),e.jsx("div",{className:"webhook-row-card__events",children:t.length===0?e.jsx("span",{className:"webhook-row-card__no-events",children:"No triggers"}):t.map(a=>{const i=C.find(F=>F.id===a),W=(i==null?void 0:i.icon)??u;return e.jsxs("span",{className:`event-pill event-pill--${(i==null?void 0:i.tone)||"gray"}`,children:[e.jsx(W,{size:11}),(i==null?void 0:i.label)||a]},a)})})]},s.id)})}),e.jsxs("div",{className:"add-webhook-card",children:[e.jsxs("div",{className:"add-webhook-card__header",children:[e.jsx(x,{size:14}),e.jsx("span",{children:"Add a new webhook"})]}),e.jsxs("div",{className:"add-webhook-card__body",children:[e.jsx(S,{label:"Webhook URL",description:"The endpoint we POST webhook events to.",htmlFor:"webhook-url",children:e.jsx("div",{className:"setting-input-wrapper",children:e.jsx("input",{id:"webhook-url",type:"url",placeholder:"https://hooks.slack.com/services/...",value:l,onChange:s=>j(s.target.value)})})}),e.jsx(S,{label:"Trigger events",children:e.jsx("div",{className:"event-toggle-grid",children:C.map(s=>{const t=d.includes(s.id),a=s.icon;return e.jsxs("button",{type:"button",role:"checkbox","aria-checked":t,className:`event-toggle event-toggle--${s.tone} ${t?"is-selected":""}`,onClick:()=>$(s.id),children:[e.jsx(a,{size:14,className:"event-toggle__lead"}),e.jsx("span",{children:s.label}),t&&e.jsx(H,{size:14,className:"event-toggle__check"})]},s.id)})})}),e.jsxs("div",{className:"template-section",children:[e.jsxs("button",{type:"button",className:"template-section__toggle",onClick:()=>_(!n),"aria-expanded":n,children:[e.jsx(I,{size:14,className:n?"tone-amber":"tone-dim"}),e.jsx("span",{children:"Use custom payload (optional)"}),e.jsx("span",{className:"template-section__indicator",children:n?"−":"+"})]}),n&&e.jsxs("div",{className:"template-editor",children:[e.jsx("p",{className:"template-editor__hint",children:"Define a JSON or text template. Click a variable below to insert it."}),e.jsx("div",{className:"template-editor__chips",children:X.map(s=>e.jsx("button",{type:"button",className:"template-editor__chip",onClick:()=>R(s),children:`{{${s}}}`},s))}),e.jsx("textarea",{className:"template-editor__textarea",placeholder:'Example: { "text": "Alert: Device {{udid}} is offline!" }',value:y,onChange:s=>m(s.target.value),rows:4})]})]})]}),e.jsxs("div",{className:"add-webhook-card__footer",children:[e.jsxs("button",{type:"button",className:"page-header-action page-header-action--ghost",onClick:L,disabled:!l.trim()||v,children:[v?e.jsx(b,{size:14,className:"animate-spin"}):e.jsx(J,{size:14}),e.jsx("span",{children:"Test payload"})]}),e.jsxs("button",{type:"button",className:"page-header-action",onClick:z,disabled:h||!l.trim(),children:[h?e.jsx(b,{size:14,className:"animate-spin"}):e.jsx(x,{size:14}),e.jsx("span",{children:h?"Saving…":"Save webhook"})]})]})]})]})]})};export{ae as WebhookSettings};
|
package/lib/public/index.html
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
Learn how to configure a non-root public URL by running `npm run build`.
|
|
37
37
|
-->
|
|
38
38
|
<title>Xenon</title>
|
|
39
|
-
<script type="module" crossorigin src="/xenon/assets/index-
|
|
39
|
+
<script type="module" crossorigin src="/xenon/assets/index-DdsuniRs.js"></script>
|
|
40
40
|
<link rel="stylesheet" crossorigin href="/xenon/assets/index-C8pvxG1f.css">
|
|
41
41
|
</head>
|
|
42
42
|
|
|
@@ -101,25 +101,34 @@ function isValidSession(request, response, next) {
|
|
|
101
101
|
}
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
|
-
// A
|
|
105
|
-
// of every session.
|
|
106
|
-
function
|
|
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 (
|
|
108
|
+
return read();
|
|
109
109
|
}
|
|
110
110
|
catch (err) {
|
|
111
|
-
if (!(err instanceof sessionInsights_1.
|
|
111
|
+
if (!(err instanceof sessionInsights_1.InvalidQueryError))
|
|
112
112
|
throw err;
|
|
113
|
-
response.status(400).json({ error:
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
},
|
|
152
|
-
where:
|
|
153
|
-
take:
|
|
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.
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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.
|
|
49
|
-
|
|
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
|
|
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,11 +0,0 @@
|
|
|
1
|
-
import{c as te,r as h,p as se,X as H,j as e,z as X,R as M,f as ae,T as re,q as ne,u as le,i as oe,t as ie,M as ce}from"./index-rullcuA-.js";import{s as de,b as I,P as xe,T as E,i as ue,f as q,a as V,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 D,k as Ne,l as W,S as w,m as ye,n as we,u as Se,o as Ce}from"./stat-tile-BlmNnQFY.js";import{I as J}from"./input-DAFtgyoC.js";import{L as $e}from"./layers-C9QEszDA.js";import{B as P}from"./button-C8WXV74W.js";import{D as ke}from"./download-CYOnymUH.js";import{s as Re}from"./labels-C21SA4JG.js";import{C as Te}from"./circle-x-5wGBLvP4.js";import{C as Ee}from"./circle-check-CXNkbyeD.js";import{C as Be}from"./chevron-right-CXeI6N1r.js";import{T as Me}from"./tablet-Dt9_zCsE.js";import{M as Ie}from"./monitor-CdNmIwjG.js";import{P as Le}from"./page-header-BNgF7qLO.js";import{S as _e}from"./select-C563Otbb.js";import{E as F}from"./EmptyState-DMkfqTdS.js";import{L as De}from"./list-checks-BDs_WJqC.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 Oe=[["path",{d:"M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"14sxne"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}],["path",{d:"M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16",key:"1hlbsb"}],["path",{d:"M16 16h5v5",key:"ccwih5"}]],Ae=te("refresh-ccw",Oe),Pe=15e3;function Fe(t,a){const[s,r]=h.useState(null),x=h.useRef(!0),c=h.useRef(0),{on:u}=se(),o=h.useCallback(async()=>{const d=++c.current;try{const m=await H.getSessionSummary(a?{buildId:a}:{since:de(t,Date.now())});x.current&&d===c.current&&m&&m.current&&r(m)}catch{}},[t,a]);return h.useEffect(()=>{x.current=!0,r(null),o();const d=setInterval(o,Pe);return()=>{x.current=!1,clearInterval(d)}},[o]),h.useEffect(()=>{const d=[u("session_started",o),u("session_stopped",o)];return()=>{for(const m of d)m()}},[u,o]),s}const U=(t,a)=>`${t} ${a}${t===1?"":"s"}`,Ue=({build:t})=>{const{passedCount:a,failedCount:s,runningCount:r,sessionCount:x}=t,c=Math.max(0,x-a-s-r);if(x===0)return null;const u=[a>0&&`${a} passed`,s>0&&`${s} failed`,r>0&&`${r} running`,c>0&&`${c} without a verdict`].filter(Boolean);return e.jsxs("div",{role:"img","aria-label":u.join(", "),title:u.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}}),c>0&&e.jsx("span",{style:{flexGrow:c}})]})},z=({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]}),ze=({builds:t,selectedBuildId:a,onSelect:s,timeFilter:r})=>{const[x,c]=h.useState(""),u=x.trim().toLowerCase(),o=Date.now(),d=t.filter(n=>{if(u&&!`${I(n)} ${n.name??""} ${n.id}`.toLowerCase().includes(u))return!1;if(r!=="all"){const l=Date.parse(String(n.createdAt));if(!Number.isFinite(l)||o-l>xe[r])return!1}return!0}),m=u?"No builds match.":r!=="all"?`No builds in the ${E[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(X,{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:n=>c(n.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(z,{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($e,{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:[E[r],", every build"]})]}),e.jsx("div",{className:"px-4 pt-3 pb-1 text-[11px] font-medium text-[var(--text-dim)]",children:"Builds"}),d.length===0&&e.jsx("div",{className:"px-4 py-6 text-xs text-[var(--text-dim)]",children:m}),d.map(n=>{const l=ue(n),j=l?U(n.sessionCount,"session"):`${q(n.createdAt)} · ${U(n.sessionCount,"session")}`;return e.jsxs(z,{active:n.id===a,onClick:()=>s(n.id),children:[e.jsx("div",{className:`text-[13px] font-medium truncate ${l?"text-[var(--text-muted)]":"text-[var(--text)]"}`,title:n.name||void 0,children:I(n)}),e.jsx("div",{className:"mt-0.5 text-[11px] text-[var(--text-muted)] tabular-nums",children:j}),e.jsx(Ue,{build:n})]},n.id)})]})]})},Ge={neutral:"bg-[var(--text-dim)]",green:"bg-[var(--color-success)]",red:"bg-[var(--red)]",amber:"bg-[var(--amber)]",blue:"bg-[var(--blue)]"},B=({label:t,count:a,active:s,onClick:r,tone:x="neutral",bullet:c=!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:[c&&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})]}),He=({sessions:t,active:a,onChange:s,search:r,onSearchChange:x,totalMatching:c,totalUnfiltered:u,capped:o=!1})=>{const d=V(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(B,{label:"All",count:d.all,active:a==="all",onClick:()=>s("all"),tone:"neutral",bullet:!1}),e.jsx(B,{label:"Passed",count:d.passed,active:a==="passed",onClick:()=>s("passed"),tone:"green"}),e.jsx(B,{label:"Failed",count:d.failed,active:a==="failed",onClick:()=>s("failed"),tone:"red"}),e.jsx(B,{label:"Running",count:d.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:[c," of ",u," sessions",o?" (the newest)":""]}),e.jsxs("div",{className:"relative w-64 shrink-0",children:[e.jsx(X,{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 Xe(t){return(t.split("-")[0]||t).slice(0,8).toUpperCase()}const qe=({build:t,failedCount:a,selectedCount:s,onRetryFailed:r,onExport:x})=>{const c=a>0||s>0,[u,o]=M.useState(!1),d=M.useRef(null);M.useEffect(()=>{const n=l=>{d.current&&!d.current.contains(l.target)&&o(!1)};return document.addEventListener("mousedown",n),()=>document.removeEventListener("mousedown",n)},[]);const m=c?`Retry ${s||a} session${(s||a)===1?"":"s"}`:"No failed sessions to retry";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:["#",Xe(t.id)]}),e.jsx("span",{"aria-hidden":"true",children:"·"}),e.jsxs("span",{className:"tabular-nums",children:["Started ",q(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(P,{type:"button",variant:"secondary",size:"sm",onClick:r,disabled:!c,title:m,className:"gap-1.5",children:[e.jsx(Ae,{className:"h-3.5 w-3.5"}),"Retry failed"]}),e.jsxs("div",{className:"relative",children:[e.jsxs(P,{type:"button",variant:"secondary",size:"sm",onClick:()=>o(n=>!n),className:"gap-1.5",children:[e.jsx(ke,{className:"h-3.5 w-3.5"}),"Export"]}),u&&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:()=>{o(!1),x("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:()=>{o(!1),x("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 Ve({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(re,{className:s}):a.includes("pad")||a.includes("tablet")?e.jsx(Me,{className:s}):a.includes("android")||a.includes("ios")?e.jsx(ne,{className:s}):e.jsx(Ie,{className:s})}const We={passed:{label:"Passed",Icon:Ee,cls:"text-[var(--color-success)]"},failed:{label:"Failed",Icon:Te,cls:"text-[var(--color-danger)]"},running:{label:"Running",Icon:ae,cls:"text-[var(--color-warning)]"},other:{Icon:be,cls:"text-[var(--text-dim)]"}},Je="Unnamed session. Set xe:options.name in its capabilities to name it.",Ke=({session:t,buildName:a,showBuild:s,showSelection:r,selected:x,onToggleSelect:c,onOpen:u})=>{var N,C,$;const o=me(t.status),d=We[o],m=o==="failed",n=pe(t),l=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:u,tabIndex:0,onKeyDown:f=>{f.target!==f.currentTarget||f.key!=="Enter"&&f.key!==" "||(f.preventDefault(),u())},"data-outcome":o,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:c,"aria-label":`Select session ${n.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 ${d.cls}`,children:[e.jsx(d.Icon,{className:`h-3.5 w-3.5 shrink-0 ${o==="running"?"animate-spin":""}`,"aria-hidden":"true"}),d.label??Re(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 ${n.source==="name"?"text-[var(--text)]":"text-[var(--text-muted)]"}`,title:n.source==="name"?n.text:`${n.text}
|
|
7
|
-
${Je}`,children:n.text}),l?e.jsx("div",{className:"mt-0.5 text-[11px] text-[var(--color-danger)] truncate",title:l,children:l}):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(Ve,{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:D(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"})})]})},T="px-3 py-2 text-[11px] font-medium text-[var(--text-dim)]",G=({sessions:t,statusFilter:a,searchQuery:s,buildNames:r,showBuild:x,showSelection:c,selectedIds:u,onToggleSelect:o,onToggleSelectAll:d,onOpenRow:m,empty:n})=>{const l=h.useMemo(()=>W(t,a,s),[t,a,s]);if(t.length===0)return e.jsx(e.Fragment,{children:n});if(l.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=l.every(p=>u.has(p.id)),S=l.some(p=>u.has(p.id)),v=l.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:[c&&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:()=>d(l.map(p=>p.id))})}),e.jsx("th",{className:`${T} pl-4 w-[112px]`,children:"Status"}),e.jsx("th",{className:T,children:"Test"}),e.jsx("th",{className:`${T} w-[240px]`,children:"Device"}),e.jsx("th",{className:`${T} w-[124px]`,children:"Started"}),e.jsx("th",{className:`${T} 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:l.map(p=>e.jsx(Ke,{session:p,buildName:p.build_id?r.get(p.build_id):null,showBuild:x,showSelection:c,selected:u.has(p.id),onToggleSelect:()=>o(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 Qe(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 Ye=({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,c=ye(s),u=s.passed+s.failed,o=a?we(s,r):null,d=o!==null?Qe(o,a):u>0?{text:`${s.passed} of ${u} 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:c===null?"—":`${Math.round(c)}%`,note:d.text,noteTone:d.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:D(s.medianMs),note:s.p90Ms===null?"No finished sessions":`p90 ${D(s.p90Ms)}`})]})},Ze={"24h":"24 hours","7d":"7 days","30d":"30 days"},et=`"xe:options": {
|
|
9
|
-
"name": "Checkout happy path",
|
|
10
|
-
"build": "Nightly smoke"
|
|
11
|
-
}`,tt=()=>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:et})]}),jt=()=>{const t=le(),{buildId:a}=oe(),{toast:s}=ie(),[r,x]=h.useState("7d"),[c,u]=h.useState("all"),[o,d]=h.useState(""),[m,n]=h.useState(new Set),l=Se({withSessions:!0,timeFilter:r}),j=Fe(r,l.selectedBuildId),{selectBuild:S,selectedBuildId:v}=l;M.useEffect(()=>{const i=a??null;i!==v&&(S(i),n(new Set))},[a,v,S]);const p=i=>t(i?`/builds/${i}`:"/builds"),N=i=>t(`/builds/${encodeURIComponent(i.build_id||"none")}/sessions/${i.id}`),C=h.useCallback(i=>{n(b=>{const g=new Set(b);return g.has(i)?g.delete(i):g.add(i),g})},[]),$=h.useCallback(i=>{n(b=>{const g=i.length>0&&i.every(y=>b.has(y)),k=new Set(b);return g?i.forEach(y=>k.delete(y)):i.forEach(y=>k.add(y)),k})},[]),f=h.useMemo(()=>new Map(l.builds.map(i=>[i.id,I(i)])),[l.builds]),L=l.builds.find(i=>i.id===v)||null,K=V(l.sessions),Q=()=>{s("Bulk retry lands in a future release.","info")},Y=async i=>{if(v)try{const b=m.size>0?Array.from(m):void 0,{blob:g,filename:k}=await H.exportBuild(v,i,b),y=URL.createObjectURL(g),R=document.createElement("a");R.href=y,R.download=k,document.body.appendChild(R),R.click(),document.body.removeChild(R),URL.revokeObjectURL(y);const A=b?b.length:"all";s(`Exported ${A} session${A===1?"":"s"} as ${i.toUpperCase()}.`,"success")}catch(b){const g=b instanceof Error?b.message:String(b);s(`Export failed: ${g}`,"error")}},_=v===null,Z=_&&r!=="all"?Ze[r]:null,ee=!_&&!L&&!l.loading,O=e.jsx(He,{sessions:l.sessions,active:c,onChange:u,search:o,onSearchChange:d,totalMatching:W(l.sessions,c,o).length,totalUnfiltered:l.sessions.length,capped:l.sessions.length>=Ce});return e.jsxs("div",{className:"flex flex-col h-full",children:[e.jsx(Le,{icon:ce,title:"Sessions",subtitle:"Every test session, newest first, with its build, device and outcome.",action:e.jsx(_e,{value:r,onChange:i=>x(i.target.value),selectSize:"sm","aria-label":"Period",className:"w-40 text-xs",children:Object.keys(E).map(i=>e.jsx("option",{value:i,children:E[i]},i))})}),e.jsx(Ye,{summary:j,periodLabel:Z}),e.jsxs("div",{className:"flex flex-1 min-h-0",children:[e.jsx(ze,{builds:l.builds,selectedBuildId:v,onSelect:p,timeFilter:r}),e.jsx("section",{className:"flex-1 flex flex-col min-w-0",children:ee?e.jsx(F,{title:"This build isn’t available",description:"It may have been removed, or belong to a team you’re not on."}):_?e.jsxs(e.Fragment,{children:[e.jsx("h2",{className:"sr-only",children:"All sessions"}),O,e.jsx(G,{sessions:l.sessions,statusFilter:c,searchQuery:o,buildNames:f,showBuild:!0,showSelection:!1,selectedIds:m,onToggleSelect:C,onToggleSelectAll:$,onOpenRow:N,empty:l.loading?null:e.jsx("div",{className:"px-6 py-6",children:e.jsx(F,{icon:e.jsx(De,{className:"h-5 w-5"}),title:r==="all"?"No sessions yet":`No sessions in the ${E[r].toLowerCase()}`,description:"Sessions appear here as your tests run.",action:e.jsx(tt,{})})})})]}):L?e.jsxs(e.Fragment,{children:[e.jsx(qe,{build:L,failedCount:K.failed,selectedCount:m.size,onRetryFailed:Q,onExport:Y}),O,e.jsx(G,{sessions:l.sessions,statusFilter:c,searchQuery:o,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{jt as BuildsPage,jt as default};
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import{c as k,r as i,p as I,X as b,j as p}from"./index-rullcuA-.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"}]],U=k("circle-dashed",E);function D(e){switch(e){case"success":case"passed":case"ended":return"passed";case"failed":case"error":case"timeout":return"failed";case"running":return"running";default:return"other"}}function Y(e){const t={all:e.length,passed:0,failed:0,running:0};for(const n of e){const r=D(n.status);r!=="other"&&(t[r]+=1)}return t}function F(e,t){return t==="all"?!0:D(e.status)===t}function X(e,t,n){const r=n.trim().toLowerCase();return e.filter(s=>{var o,d;return!(!F(s,t)||r&&![s.id,C(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,(d=s.owner)==null?void 0:d.email].filter(Boolean).join(" ").toLowerCase().includes(r))})}function z(e){var n;const t=(n=e.device_name)==null?void 0:n.trim();return t&&t.length>0?t:"Unknown Device"}function V(e){const t=e.device_platform??"";if(!t)return"—";const n=t.toLowerCase();return n==="ios"?"iOS":n==="tvos"?"tvOS":n.charAt(0).toUpperCase()+n.slice(1)}function G(e){return A(e)}function K(e){if(!e.startTime)return null;const t=Date.parse(e.startTime);if(!Number.isFinite(t))return null;const n=e.endTime?Date.parse(e.endTime):Date.now();return Number.isFinite(n)?Math.max(0,n-t):null}function Q(e){if(e==null||!Number.isFinite(e)||e<0)return"—";const t=e/1e3,n=Math.floor(t/3600),r=Math.floor(t%3600/60),o=(t-n*3600-r*60).toFixed(1);return n>0?`${n}h ${r}m ${o}s`:r>0?`${r}m ${o}s`:`${o}s`}const N=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],g=e=>String(e).padStart(2,"0");function h(e,t){const n=e[`appium:${t}`]??e[t];return typeof n=="string"&&n.trim()?n.trim():null}function B(e){if(!e)return null;let t;try{t=JSON.parse(e)}catch{return null}if(!t||typeof t!="object")return null;let n=t;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 x(e){const t=B(e);if(!t)return null;const n=h(t,"appPackage")??h(t,"bundleId");if(n)return n;const r=h(t,"app");if(r){const s=r.split(/[?#]/)[0].split(/[\\/]/).pop()??"";if(s&&s!=="download")return s}return h(t,"browserName")}function C(e){var r;const t=(r=e.name)==null?void 0:r.trim();if(t)return{text:t,source:"name"};const n=x(e.session_capabilities)??x(e.desired_capabilities);return n?{text:n,source:"app"}:{text:`Session ${e.id.slice(0,8)}`,source:"id"}}function S(e){return`${N[e.getMonth()]} ${e.getDate()}, ${g(e.getHours())}:${g(e.getMinutes())}`}function W(e){const t=e?new Date(e):null;return t&&!Number.isNaN(t.getTime())?S(t):"—"}function j(e){var n;const t=(n=e.name)==null?void 0:n.trim();return!t||t==="Default Build"}function Z(e){if(!j(e))return e.name.trim();const t=new Date(e.createdAt);return Number.isNaN(t.getTime())?"Build":`Build · ${S(t)}`}const ee={all:"All time","24h":"Last 24 hours","7d":"Last 7 days","30d":"Last 30 days"},O={"24h":864e5,"7d":7*864e5,"30d":30*864e5};function R(e,t){return e==="all"?null:new Date(t-O[e]).toISOString()}function te(e){if(e==null||!Number.isFinite(e)||e<0)return"—";if(e<1e3)return"<1s";const t=Math.floor(e/1e3),n=Math.floor(t/3600),r=Math.floor(t%3600/60),s=t%60;return n>0?`${n}h ${r}m`:r>0?`${r}m ${s}s`:`${s}s`}function ne(e,t=new Date){if(!e)return"—";const n=new Date(e);if(Number.isNaN(n.getTime()))return"—";const r=`${g(n.getHours())}:${g(n.getMinutes())}`;return n.getFullYear()!==t.getFullYear()?`${N[n.getMonth()]} ${n.getDate()}, ${n.getFullYear()} ${r}`:n.getMonth()===t.getMonth()&&n.getDate()===t.getDate()?r:S(n)}function y(e){const t=e.passed+e.failed;return t>0?e.passed/t*100:null}function re(e,t){const n=y(e),r=t?y(t):null;return n===null||r===null?null:n-r}function se(e){return e?e==="here"?"This server":e:null}const P=3e3,ae=500;function ie(e={}){const{withSessions:t=!1,timeFilter:n="all"}=e,[r,s]=i.useState([]),[o,d]=i.useState([]),[c,$]=i.useState(null),[w,_]=i.useState(!0),[T,v]=i.useState(null),l=i.useRef(!0),{on:f}=I(),M=i.useCallback(()=>{if(!t)return Promise.resolve([]);if(c)return b.getSessions({buildId:c});const a=R(n,Date.now())??void 0;return b.getSessions({since:a})},[t,c,n]),u=i.useCallback(async()=>{try{const[a,m]=await Promise.all([b.getBuilds(),M()]);if(!l.current)return;s(Array.isArray(a)?a:[]),d(Array.isArray(m)?m:[]),v(null)}catch(a){if(!l.current)return;v(a instanceof Error?a.message:String(a))}finally{l.current&&_(!1)}},[M]);return i.useEffect(()=>{l.current=!0,u();const a=setInterval(u,P);return()=>{l.current=!1,clearInterval(a)}},[u]),i.useEffect(()=>{const a=()=>u(),m=[f("session_started",a),f("session_stopped",a),f("device_added",a),f("device_removed",a)];return()=>{for(const L of m)L()}},[f,u]),{builds:r,selectedBuildId:c,sessions:o,loading:w,error:T,selectBuild:$,refresh:u}}const q={neutral:"text-[var(--text-muted)]",good:"text-[var(--color-success)]",bad:"text-[var(--color-danger)]"},oe=({label:e,value:t,valueCls:n="text-[var(--text)]",note:r,noteTone:s="neutral"})=>p.jsxs("div",{className:"min-w-0 rounded-lg border border-[var(--border)] bg-[var(--surface)] px-4 py-3",children:[p.jsx("div",{className:"text-xs text-[var(--text-muted)]",children:e}),p.jsx("div",{className:`mt-1 text-2xl font-semibold tabular-nums leading-tight truncate ${n}`,children:t}),p.jsx("div",{className:`mt-0.5 text-[11px] truncate ${q[s]}`,children:r??" "})]});export{U as C,O as P,oe as S,ee as T,Y as a,Z as b,D as c,C as d,z as e,W as f,ne as g,G as h,j as i,te as j,K as k,X as l,y as m,re as n,ae as o,V as p,Q as q,se as r,R as s,ie as u};
|