@liiift-studio/deploy-vercel-from-sanity 1.0.9 → 1.0.10
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/dist/index.js +27 -13
- package/dist/index.mjs +27 -13
- package/package.json +1 -1
- package/src/components/DeployItem.tsx +40 -14
- package/src/version.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -283,12 +283,13 @@ function DeployHistory({ target, token, onClose }) {
|
|
|
283
283
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
284
284
|
var POLL_INTERVAL_MS = 5e3;
|
|
285
285
|
var LABEL_WIDTH = 64;
|
|
286
|
+
var PENDING_TIMEOUT_MS = 6e4;
|
|
286
287
|
function DeployItem({ target, token, onDelete, onEdit }) {
|
|
287
288
|
const { projectId, hookId } = parseHookUrl(target.url);
|
|
288
289
|
const toast = (0, import_ui3.useToast)();
|
|
289
290
|
const [deployments, setDeployments] = (0, import_react2.useState)([]);
|
|
290
291
|
const [loadingInitial, setLoadingInitial] = (0, import_react2.useState)(true);
|
|
291
|
-
const [
|
|
292
|
+
const [pendingSince, setPendingSince] = (0, import_react2.useState)(null);
|
|
292
293
|
const [canceling, setCanceling] = (0, import_react2.useState)(false);
|
|
293
294
|
const [deployError, setDeployError] = (0, import_react2.useState)(null);
|
|
294
295
|
const [showHistory, setShowHistory] = (0, import_react2.useState)(false);
|
|
@@ -299,8 +300,10 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
299
300
|
const [errorLines, setErrorLines] = (0, import_react2.useState)([]);
|
|
300
301
|
const [loadingLogs, setLoadingLogs] = (0, import_react2.useState)(false);
|
|
301
302
|
const [logError, setLogError] = (0, import_react2.useState)(null);
|
|
303
|
+
const triggeredFromUidRef = (0, import_react2.useRef)(void 0);
|
|
302
304
|
const latest = deployments[0];
|
|
303
|
-
const
|
|
305
|
+
const isPending = pendingSince !== null;
|
|
306
|
+
const isActive = isPending || isActiveState(latest?.state);
|
|
304
307
|
const fetchDeployments = (0, import_react2.useCallback)(async () => {
|
|
305
308
|
if (!projectId || !hookId || !token) return;
|
|
306
309
|
try {
|
|
@@ -319,8 +322,14 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
319
322
|
return () => clearInterval(id);
|
|
320
323
|
}, [isActive, fetchDeployments]);
|
|
321
324
|
(0, import_react2.useEffect)(() => {
|
|
322
|
-
if (
|
|
323
|
-
|
|
325
|
+
if (!isPending) return;
|
|
326
|
+
if (latest?.uid && latest.uid !== triggeredFromUidRef.current) setPendingSince(null);
|
|
327
|
+
}, [isPending, latest?.uid]);
|
|
328
|
+
(0, import_react2.useEffect)(() => {
|
|
329
|
+
if (!isPending) return;
|
|
330
|
+
const id = setTimeout(() => setPendingSince(null), PENDING_TIMEOUT_MS);
|
|
331
|
+
return () => clearTimeout(id);
|
|
332
|
+
}, [isPending]);
|
|
324
333
|
const prevStateRef = (0, import_react2.useRef)(void 0);
|
|
325
334
|
(0, import_react2.useEffect)(() => {
|
|
326
335
|
const current = latest?.state;
|
|
@@ -344,7 +353,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
344
353
|
const timerRef = (0, import_react2.useRef)(null);
|
|
345
354
|
(0, import_react2.useEffect)(() => {
|
|
346
355
|
if (isActive) {
|
|
347
|
-
const start = latest?.created ?? Date.now();
|
|
356
|
+
const start = pendingSince ?? latest?.created ?? Date.now();
|
|
348
357
|
setElapsed(Math.floor((Date.now() - start) / 1e3));
|
|
349
358
|
timerRef.current = setInterval(() => {
|
|
350
359
|
setElapsed(Math.floor((Date.now() - start) / 1e3));
|
|
@@ -356,22 +365,23 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
356
365
|
return () => {
|
|
357
366
|
if (timerRef.current) clearInterval(timerRef.current);
|
|
358
367
|
};
|
|
359
|
-
}, [isActive, latest?.created]);
|
|
368
|
+
}, [isActive, pendingSince, latest?.created]);
|
|
360
369
|
const deploy = (0, import_react2.useCallback)(() => {
|
|
361
370
|
(0, import_react_dom.flushSync)(() => {
|
|
362
371
|
setDeployError(null);
|
|
363
|
-
|
|
372
|
+
triggeredFromUidRef.current = latest?.uid;
|
|
373
|
+
setPendingSince(Date.now());
|
|
364
374
|
});
|
|
365
375
|
void (async () => {
|
|
366
376
|
try {
|
|
367
377
|
await triggerDeploy(target.url);
|
|
368
378
|
setTimeout(fetchDeployments, 2e3);
|
|
369
379
|
} catch (err) {
|
|
370
|
-
|
|
380
|
+
setPendingSince(null);
|
|
371
381
|
setDeployError(err instanceof Error ? err.message : "Deploy failed");
|
|
372
382
|
}
|
|
373
383
|
})();
|
|
374
|
-
}, [target.url, fetchDeployments]);
|
|
384
|
+
}, [target.url, fetchDeployments, latest?.uid]);
|
|
375
385
|
const cancel = (0, import_react2.useCallback)(async () => {
|
|
376
386
|
if (!latest?.uid) return;
|
|
377
387
|
setCanceling(true);
|
|
@@ -437,7 +447,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
437
447
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true", style: { marginLeft: "-0.1em", opacity: 0.5 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M5 3.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm0 2.122a2.25 2.25 0 1 0-1.5 0v.878A2.25 2.25 0 0 0 5.75 8.5h1.5v2.128a2.251 2.251 0 1 0 1.5 0V8.5h1.5a2.25 2.25 0 0 0 2.25-2.25v-.878a2.25 2.25 0 1 0-1.5 0v.878a.75.75 0 0 1-.75.75h-4.5A.75.75 0 0 1 5 6.25v-.878zm3.75 7.378a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm3-8.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0z" }) })
|
|
438
448
|
] }) }),
|
|
439
449
|
token && !loadingInitial && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
440
|
-
|
|
450
|
+
isPending ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StatusBadge, { state: "QUEUED" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StatusBadge, { state: latest?.state }),
|
|
441
451
|
isActiveState(latest?.state) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
442
452
|
import_ui3.Button,
|
|
443
453
|
{
|
|
@@ -450,10 +460,13 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
450
460
|
style: { cursor: "pointer" }
|
|
451
461
|
}
|
|
452
462
|
),
|
|
453
|
-
|
|
463
|
+
isPending ? (
|
|
464
|
+
/* Optimistic — dimmed spinner only; no elapsed time until a real build exists */
|
|
465
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Spinner, { muted: true, style: { marginLeft: 4, opacity: 0.5 } })
|
|
466
|
+
) : isActive ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_ui3.Flex, { align: "center", gap: 1, children: [
|
|
454
467
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Spinner, { muted: true, style: { marginLeft: 4 } }),
|
|
455
468
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, muted: true, children: formatDuration(elapsed) })
|
|
456
|
-
] }) :
|
|
469
|
+
] }) : deployedAt ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_ui3.Text, { size: 1, muted: true, children: deployedAt }) : null
|
|
457
470
|
] })
|
|
458
471
|
] }),
|
|
459
472
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
@@ -696,6 +709,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
696
709
|
{
|
|
697
710
|
text: "Deploy",
|
|
698
711
|
tone: "primary",
|
|
712
|
+
loading: isPending,
|
|
699
713
|
disabled: isActive,
|
|
700
714
|
onClick: deploy,
|
|
701
715
|
style: {
|
|
@@ -936,7 +950,7 @@ function DeployTargetForm({ initial, onSaved, onClose }) {
|
|
|
936
950
|
}
|
|
937
951
|
|
|
938
952
|
// src/version.ts
|
|
939
|
-
var VERSION = "1.0.
|
|
953
|
+
var VERSION = "1.0.10";
|
|
940
954
|
|
|
941
955
|
// src/components/DeployTool.tsx
|
|
942
956
|
var import_jsx_runtime6 = require("react/jsx-runtime");
|
package/dist/index.mjs
CHANGED
|
@@ -305,12 +305,13 @@ function DeployHistory({ target, token, onClose }) {
|
|
|
305
305
|
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
306
306
|
var POLL_INTERVAL_MS = 5e3;
|
|
307
307
|
var LABEL_WIDTH = 64;
|
|
308
|
+
var PENDING_TIMEOUT_MS = 6e4;
|
|
308
309
|
function DeployItem({ target, token, onDelete, onEdit }) {
|
|
309
310
|
const { projectId, hookId } = parseHookUrl(target.url);
|
|
310
311
|
const toast = useToast();
|
|
311
312
|
const [deployments, setDeployments] = useState2([]);
|
|
312
313
|
const [loadingInitial, setLoadingInitial] = useState2(true);
|
|
313
|
-
const [
|
|
314
|
+
const [pendingSince, setPendingSince] = useState2(null);
|
|
314
315
|
const [canceling, setCanceling] = useState2(false);
|
|
315
316
|
const [deployError, setDeployError] = useState2(null);
|
|
316
317
|
const [showHistory, setShowHistory] = useState2(false);
|
|
@@ -321,8 +322,10 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
321
322
|
const [errorLines, setErrorLines] = useState2([]);
|
|
322
323
|
const [loadingLogs, setLoadingLogs] = useState2(false);
|
|
323
324
|
const [logError, setLogError] = useState2(null);
|
|
325
|
+
const triggeredFromUidRef = useRef(void 0);
|
|
324
326
|
const latest = deployments[0];
|
|
325
|
-
const
|
|
327
|
+
const isPending = pendingSince !== null;
|
|
328
|
+
const isActive = isPending || isActiveState(latest?.state);
|
|
326
329
|
const fetchDeployments = useCallback2(async () => {
|
|
327
330
|
if (!projectId || !hookId || !token) return;
|
|
328
331
|
try {
|
|
@@ -341,8 +344,14 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
341
344
|
return () => clearInterval(id);
|
|
342
345
|
}, [isActive, fetchDeployments]);
|
|
343
346
|
useEffect2(() => {
|
|
344
|
-
if (
|
|
345
|
-
|
|
347
|
+
if (!isPending) return;
|
|
348
|
+
if (latest?.uid && latest.uid !== triggeredFromUidRef.current) setPendingSince(null);
|
|
349
|
+
}, [isPending, latest?.uid]);
|
|
350
|
+
useEffect2(() => {
|
|
351
|
+
if (!isPending) return;
|
|
352
|
+
const id = setTimeout(() => setPendingSince(null), PENDING_TIMEOUT_MS);
|
|
353
|
+
return () => clearTimeout(id);
|
|
354
|
+
}, [isPending]);
|
|
346
355
|
const prevStateRef = useRef(void 0);
|
|
347
356
|
useEffect2(() => {
|
|
348
357
|
const current = latest?.state;
|
|
@@ -366,7 +375,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
366
375
|
const timerRef = useRef(null);
|
|
367
376
|
useEffect2(() => {
|
|
368
377
|
if (isActive) {
|
|
369
|
-
const start = latest?.created ?? Date.now();
|
|
378
|
+
const start = pendingSince ?? latest?.created ?? Date.now();
|
|
370
379
|
setElapsed(Math.floor((Date.now() - start) / 1e3));
|
|
371
380
|
timerRef.current = setInterval(() => {
|
|
372
381
|
setElapsed(Math.floor((Date.now() - start) / 1e3));
|
|
@@ -378,22 +387,23 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
378
387
|
return () => {
|
|
379
388
|
if (timerRef.current) clearInterval(timerRef.current);
|
|
380
389
|
};
|
|
381
|
-
}, [isActive, latest?.created]);
|
|
390
|
+
}, [isActive, pendingSince, latest?.created]);
|
|
382
391
|
const deploy = useCallback2(() => {
|
|
383
392
|
flushSync(() => {
|
|
384
393
|
setDeployError(null);
|
|
385
|
-
|
|
394
|
+
triggeredFromUidRef.current = latest?.uid;
|
|
395
|
+
setPendingSince(Date.now());
|
|
386
396
|
});
|
|
387
397
|
void (async () => {
|
|
388
398
|
try {
|
|
389
399
|
await triggerDeploy(target.url);
|
|
390
400
|
setTimeout(fetchDeployments, 2e3);
|
|
391
401
|
} catch (err) {
|
|
392
|
-
|
|
402
|
+
setPendingSince(null);
|
|
393
403
|
setDeployError(err instanceof Error ? err.message : "Deploy failed");
|
|
394
404
|
}
|
|
395
405
|
})();
|
|
396
|
-
}, [target.url, fetchDeployments]);
|
|
406
|
+
}, [target.url, fetchDeployments, latest?.uid]);
|
|
397
407
|
const cancel = useCallback2(async () => {
|
|
398
408
|
if (!latest?.uid) return;
|
|
399
409
|
setCanceling(true);
|
|
@@ -459,7 +469,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
459
469
|
/* @__PURE__ */ jsx3("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true", style: { marginLeft: "-0.1em", opacity: 0.5 }, children: /* @__PURE__ */ jsx3("path", { d: "M5 3.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm0 2.122a2.25 2.25 0 1 0-1.5 0v.878A2.25 2.25 0 0 0 5.75 8.5h1.5v2.128a2.251 2.251 0 1 0 1.5 0V8.5h1.5a2.25 2.25 0 0 0 2.25-2.25v-.878a2.25 2.25 0 1 0-1.5 0v.878a.75.75 0 0 1-.75.75h-4.5A.75.75 0 0 1 5 6.25v-.878zm3.75 7.378a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm3-8.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0z" }) })
|
|
460
470
|
] }) }),
|
|
461
471
|
token && !loadingInitial && /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
462
|
-
|
|
472
|
+
isPending ? /* @__PURE__ */ jsx3(StatusBadge, { state: "QUEUED" }) : /* @__PURE__ */ jsx3(StatusBadge, { state: latest?.state }),
|
|
463
473
|
isActiveState(latest?.state) && /* @__PURE__ */ jsx3(
|
|
464
474
|
Button2,
|
|
465
475
|
{
|
|
@@ -472,10 +482,13 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
472
482
|
style: { cursor: "pointer" }
|
|
473
483
|
}
|
|
474
484
|
),
|
|
475
|
-
|
|
485
|
+
isPending ? (
|
|
486
|
+
/* Optimistic — dimmed spinner only; no elapsed time until a real build exists */
|
|
487
|
+
/* @__PURE__ */ jsx3(Spinner3, { muted: true, style: { marginLeft: 4, opacity: 0.5 } })
|
|
488
|
+
) : isActive ? /* @__PURE__ */ jsxs3(Flex3, { align: "center", gap: 1, children: [
|
|
476
489
|
/* @__PURE__ */ jsx3(Spinner3, { muted: true, style: { marginLeft: 4 } }),
|
|
477
490
|
/* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, children: formatDuration(elapsed) })
|
|
478
|
-
] }) :
|
|
491
|
+
] }) : deployedAt ? /* @__PURE__ */ jsx3(Text2, { size: 1, muted: true, children: deployedAt }) : null
|
|
479
492
|
] })
|
|
480
493
|
] }),
|
|
481
494
|
/* @__PURE__ */ jsx3(
|
|
@@ -718,6 +731,7 @@ function DeployItem({ target, token, onDelete, onEdit }) {
|
|
|
718
731
|
{
|
|
719
732
|
text: "Deploy",
|
|
720
733
|
tone: "primary",
|
|
734
|
+
loading: isPending,
|
|
721
735
|
disabled: isActive,
|
|
722
736
|
onClick: deploy,
|
|
723
737
|
style: {
|
|
@@ -969,7 +983,7 @@ function DeployTargetForm({ initial, onSaved, onClose }) {
|
|
|
969
983
|
}
|
|
970
984
|
|
|
971
985
|
// src/version.ts
|
|
972
|
-
var VERSION = "1.0.
|
|
986
|
+
var VERSION = "1.0.10";
|
|
973
987
|
|
|
974
988
|
// src/components/DeployTool.tsx
|
|
975
989
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@liiift-studio/deploy-vercel-from-sanity",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "Sanity Studio plugin — trigger and monitor Vercel deployments with full status, history, and build logs. Supports v3, v4, and v5.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Liiift Studio",
|
|
@@ -17,6 +17,8 @@ import type { DeployTarget, VercelDeployment } from '../types'
|
|
|
17
17
|
|
|
18
18
|
const POLL_INTERVAL_MS = 5_000
|
|
19
19
|
const LABEL_WIDTH = 64
|
|
20
|
+
/** Max time (ms) to hold the optimistic pending state before giving up and deferring to real API status */
|
|
21
|
+
const PENDING_TIMEOUT_MS = 60_000
|
|
20
22
|
|
|
21
23
|
interface DeployItemProps {
|
|
22
24
|
target: DeployTarget
|
|
@@ -31,7 +33,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
31
33
|
|
|
32
34
|
const [deployments, setDeployments] = useState<VercelDeployment[]>([])
|
|
33
35
|
const [loadingInitial, setLoadingInitial] = useState(true)
|
|
34
|
-
const [
|
|
36
|
+
const [pendingSince, setPendingSince] = useState<number | null>(null)
|
|
35
37
|
const [canceling, setCanceling] = useState(false)
|
|
36
38
|
const [deployError, setDeployError] = useState<string | null>(null)
|
|
37
39
|
const [showHistory, setShowHistory] = useState(false)
|
|
@@ -43,8 +45,13 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
43
45
|
const [loadingLogs, setLoadingLogs] = useState(false)
|
|
44
46
|
const [logError, setLogError] = useState<string | null>(null)
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
+
/** uid of the deployment that was latest when Deploy was clicked — lets us tell the optimistic state apart from a genuinely new deployment */
|
|
49
|
+
const triggeredFromUidRef = useRef<string | undefined>(undefined)
|
|
50
|
+
|
|
51
|
+
const latest = deployments[0]
|
|
52
|
+
/** True between the click and the API returning a new deployment — drives the optimistic "Queued" state */
|
|
53
|
+
const isPending = pendingSince !== null
|
|
54
|
+
const isActive = isPending || isActiveState(latest?.state)
|
|
48
55
|
|
|
49
56
|
// ── Fetch deployments ──────────────────────────────────────────────────────
|
|
50
57
|
const fetchDeployments = useCallback(async () => {
|
|
@@ -67,9 +74,20 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
67
74
|
return () => clearInterval(id)
|
|
68
75
|
}, [isActive, fetchDeployments])
|
|
69
76
|
|
|
77
|
+
// Hand off from the optimistic state only once the API returns a deployment
|
|
78
|
+
// that is not the one which was already latest when Deploy was clicked.
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!isPending) return
|
|
81
|
+
if (latest?.uid && latest.uid !== triggeredFromUidRef.current) setPendingSince(null)
|
|
82
|
+
}, [isPending, latest?.uid])
|
|
83
|
+
|
|
84
|
+
// Safety net — never strand the card in the optimistic state if the new
|
|
85
|
+
// deployment never appears (hook accepted but nothing was queued).
|
|
70
86
|
useEffect(() => {
|
|
71
|
-
if (
|
|
72
|
-
|
|
87
|
+
if (!isPending) return
|
|
88
|
+
const id = setTimeout(() => setPendingSince(null), PENDING_TIMEOUT_MS)
|
|
89
|
+
return () => clearTimeout(id)
|
|
90
|
+
}, [isPending])
|
|
73
91
|
|
|
74
92
|
//── Deploy-complete toast ─────────────────────────────────────────────────
|
|
75
93
|
const prevStateRef = useRef<string | undefined>(undefined)
|
|
@@ -98,7 +116,8 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
98
116
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
99
117
|
useEffect(() => {
|
|
100
118
|
if (isActive) {
|
|
101
|
-
|
|
119
|
+
// While optimistic, count from the click; once real, count from the deployment's own timestamp
|
|
120
|
+
const start = pendingSince ?? latest?.created ?? Date.now()
|
|
102
121
|
setElapsed(Math.floor((Date.now() - start) / 1000))
|
|
103
122
|
timerRef.current = setInterval(() => {
|
|
104
123
|
setElapsed(Math.floor((Date.now() - start) / 1000))
|
|
@@ -108,24 +127,27 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
108
127
|
if (timerRef.current) clearInterval(timerRef.current)
|
|
109
128
|
}
|
|
110
129
|
return () => { if (timerRef.current) clearInterval(timerRef.current) }
|
|
111
|
-
}, [isActive, latest?.created])
|
|
130
|
+
}, [isActive, pendingSince, latest?.created])
|
|
112
131
|
|
|
113
132
|
// ── Actions ───────────────────────────────────────────────────────────────
|
|
114
133
|
const deploy = useCallback(() => {
|
|
134
|
+
// Paint the optimistic "Queued" state in the same frame as the click,
|
|
135
|
+
// before the hook request is even sent
|
|
115
136
|
flushSync(() => {
|
|
116
137
|
setDeployError(null)
|
|
117
|
-
|
|
138
|
+
triggeredFromUidRef.current = latest?.uid
|
|
139
|
+
setPendingSince(Date.now())
|
|
118
140
|
})
|
|
119
141
|
void (async () => {
|
|
120
142
|
try {
|
|
121
143
|
await triggerDeploy(target.url)
|
|
122
144
|
setTimeout(fetchDeployments, 2000)
|
|
123
145
|
} catch (err) {
|
|
124
|
-
|
|
146
|
+
setPendingSince(null)
|
|
125
147
|
setDeployError(err instanceof Error ? err.message : 'Deploy failed')
|
|
126
148
|
}
|
|
127
149
|
})()
|
|
128
|
-
}, [target.url, fetchDeployments])
|
|
150
|
+
}, [target.url, fetchDeployments, latest?.uid])
|
|
129
151
|
|
|
130
152
|
const cancel = useCallback(async () => {
|
|
131
153
|
if (!latest?.uid) return
|
|
@@ -218,8 +240,8 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
218
240
|
)}
|
|
219
241
|
{token && !loadingInitial && (
|
|
220
242
|
<>
|
|
221
|
-
{
|
|
222
|
-
<
|
|
243
|
+
{isPending ? (
|
|
244
|
+
<StatusBadge state="QUEUED" />
|
|
223
245
|
) : (
|
|
224
246
|
<StatusBadge state={latest?.state} />
|
|
225
247
|
)}
|
|
@@ -234,12 +256,15 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
234
256
|
style={{ cursor: 'pointer' }}
|
|
235
257
|
/>
|
|
236
258
|
)}
|
|
237
|
-
{
|
|
259
|
+
{isPending ? (
|
|
260
|
+
/* Optimistic — dimmed spinner only; no elapsed time until a real build exists */
|
|
261
|
+
<Spinner muted style={{ marginLeft: 4, opacity: 0.5 }} />
|
|
262
|
+
) : isActive ? (
|
|
238
263
|
<Flex align="center" gap={1}>
|
|
239
264
|
<Spinner muted style={{ marginLeft: 4 }} />
|
|
240
265
|
<Text size={1} muted>{formatDuration(elapsed)}</Text>
|
|
241
266
|
</Flex>
|
|
242
|
-
) :
|
|
267
|
+
) : deployedAt ? (
|
|
243
268
|
<Text size={1} muted>{deployedAt}</Text>
|
|
244
269
|
) : null}
|
|
245
270
|
</>
|
|
@@ -562,6 +587,7 @@ export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps)
|
|
|
562
587
|
<Button
|
|
563
588
|
text="Deploy"
|
|
564
589
|
tone="primary"
|
|
590
|
+
loading={isPending}
|
|
565
591
|
disabled={isActive}
|
|
566
592
|
onClick={deploy}
|
|
567
593
|
style={{
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Package version — keep in sync with package.json
|
|
2
|
-
export const VERSION = '1.0.
|
|
2
|
+
export const VERSION = '1.0.10'
|