@liiift-studio/deploy-vercel-from-sanity 0.1.4 → 0.1.7
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/README.md +92 -70
- package/dist/index.js +333 -132
- package/dist/index.mjs +342 -136
- package/package.json +1 -1
- package/src/components/DeployItem.tsx +339 -155
- package/src/components/DeployTool.tsx +33 -26
- package/src/lib/api.ts +33 -2
- package/src/lib/helpers.ts +25 -8
- package/src/types.ts +11 -3
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
// Per-deploy-target card — shows status, build timer, history, cancel, and
|
|
1
|
+
// Per-deploy-target card — shows status, build timer, history, cancel, deploy, copy URL, and error logs
|
|
2
2
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
3
3
|
import {
|
|
4
|
-
Card, Box, Stack, Flex, Text, Button, Tooltip, Badge, Spinner,
|
|
4
|
+
Card, Box, Stack, Flex, Text, Button, Tooltip, Badge, Spinner,
|
|
5
|
+
MenuButton, Menu, MenuItem, Code,
|
|
5
6
|
} from '@sanity/ui'
|
|
6
7
|
import {
|
|
7
8
|
RocketIcon, ClockIcon, TrashIcon, EllipsisVerticalIcon, LaunchIcon,
|
|
9
|
+
CopyIcon, CheckmarkIcon, WarningOutlineIcon,
|
|
8
10
|
} from '@sanity/icons'
|
|
9
|
-
import { listDeployments, cancelDeployment, triggerDeploy } from '../lib/api'
|
|
10
|
-
import { parseHookUrl, isActiveState, formatDuration, timeAgo, shortSha, safeHref } from '../lib/helpers'
|
|
11
|
+
import { listDeployments, cancelDeployment, triggerDeploy, getDeploymentEvents } from '../lib/api'
|
|
12
|
+
import { parseHookUrl, isActiveState, formatDuration, timeAgo, shortSha, safeHref, projectHref } from '../lib/helpers'
|
|
11
13
|
import { StatusBadge } from './StatusBadge'
|
|
12
14
|
import { DeployHistory } from './DeployHistory'
|
|
13
15
|
import type { DeployTarget, VercelDeployment } from '../types'
|
|
@@ -23,15 +25,24 @@ interface DeployItemProps {
|
|
|
23
25
|
export function DeployItem({ target, token, onDelete }: DeployItemProps) {
|
|
24
26
|
const { projectId, hookId } = parseHookUrl(target.url)
|
|
25
27
|
|
|
26
|
-
const [deployments, setDeployments]
|
|
28
|
+
const [deployments, setDeployments] = useState<VercelDeployment[]>([])
|
|
27
29
|
const [loadingInitial, setLoadingInitial] = useState(true)
|
|
28
|
-
const [triggering, setTriggering]
|
|
29
|
-
const [canceling, setCanceling]
|
|
30
|
-
const [deployError, setDeployError]
|
|
31
|
-
const [showHistory, setShowHistory]
|
|
32
|
-
const [elapsed, setElapsed]
|
|
30
|
+
const [triggering, setTriggering] = useState(false)
|
|
31
|
+
const [canceling, setCanceling] = useState(false)
|
|
32
|
+
const [deployError, setDeployError] = useState<string | null>(null)
|
|
33
|
+
const [showHistory, setShowHistory] = useState(false)
|
|
34
|
+
const [elapsed, setElapsed] = useState(0)
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
// Copy URL state
|
|
37
|
+
const [copied, setCopied] = useState(false)
|
|
38
|
+
|
|
39
|
+
// Error log expansion state
|
|
40
|
+
const [showErrorLogs, setShowErrorLogs] = useState(false)
|
|
41
|
+
const [errorLines, setErrorLines] = useState<string[]>([])
|
|
42
|
+
const [loadingLogs, setLoadingLogs] = useState(false)
|
|
43
|
+
const [logError, setLogError] = useState<string | null>(null)
|
|
44
|
+
|
|
45
|
+
const latest = deployments[0]
|
|
35
46
|
const isActive = triggering || isActiveState(latest?.state)
|
|
36
47
|
|
|
37
48
|
// ── Fetch deployments ──────────────────────────────────────────────────────
|
|
@@ -45,25 +56,27 @@ export function DeployItem({ target, token, onDelete }: DeployItemProps) {
|
|
|
45
56
|
}
|
|
46
57
|
}, [projectId, hookId, token, target.teamId])
|
|
47
58
|
|
|
48
|
-
// Initial fetch
|
|
49
59
|
useEffect(() => {
|
|
50
60
|
fetchDeployments().finally(() => setLoadingInitial(false))
|
|
51
61
|
}, [fetchDeployments])
|
|
52
62
|
|
|
53
|
-
// Polling while active
|
|
54
63
|
useEffect(() => {
|
|
55
64
|
if (!isActive) return
|
|
56
65
|
const id = setInterval(fetchDeployments, POLL_INTERVAL_MS)
|
|
57
66
|
return () => clearInterval(id)
|
|
58
67
|
}, [isActive, fetchDeployments])
|
|
59
68
|
|
|
60
|
-
// Stop triggering once a deployment appears in an active state
|
|
61
69
|
useEffect(() => {
|
|
62
|
-
if (triggering && latest && latest.state !== undefined)
|
|
63
|
-
setTriggering(false)
|
|
64
|
-
}
|
|
70
|
+
if (triggering && latest && latest.state !== undefined) setTriggering(false)
|
|
65
71
|
}, [triggering, latest])
|
|
66
72
|
|
|
73
|
+
// Reset error logs when the deployment changes
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
setShowErrorLogs(false)
|
|
76
|
+
setErrorLines([])
|
|
77
|
+
setLogError(null)
|
|
78
|
+
}, [latest?.uid])
|
|
79
|
+
|
|
67
80
|
// ── Build timer ───────────────────────────────────────────────────────────
|
|
68
81
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
69
82
|
useEffect(() => {
|
|
@@ -77,9 +90,7 @@ export function DeployItem({ target, token, onDelete }: DeployItemProps) {
|
|
|
77
90
|
setElapsed(0)
|
|
78
91
|
if (timerRef.current) clearInterval(timerRef.current)
|
|
79
92
|
}
|
|
80
|
-
return () => {
|
|
81
|
-
if (timerRef.current) clearInterval(timerRef.current)
|
|
82
|
-
}
|
|
93
|
+
return () => { if (timerRef.current) clearInterval(timerRef.current) }
|
|
83
94
|
}, [isActive, latest?.created])
|
|
84
95
|
|
|
85
96
|
// ── Actions ───────────────────────────────────────────────────────────────
|
|
@@ -88,7 +99,6 @@ export function DeployItem({ target, token, onDelete }: DeployItemProps) {
|
|
|
88
99
|
setTriggering(true)
|
|
89
100
|
try {
|
|
90
101
|
await triggerDeploy(target.url)
|
|
91
|
-
// Poll immediately to pick up the new deployment faster
|
|
92
102
|
setTimeout(fetchDeployments, 2000)
|
|
93
103
|
} catch (err) {
|
|
94
104
|
setTriggering(false)
|
|
@@ -109,161 +119,333 @@ export function DeployItem({ target, token, onDelete }: DeployItemProps) {
|
|
|
109
119
|
}
|
|
110
120
|
}, [latest?.uid, token, target.teamId, fetchDeployments])
|
|
111
121
|
|
|
122
|
+
const copyUrl = useCallback(() => {
|
|
123
|
+
if (!latest?.url) return
|
|
124
|
+
navigator.clipboard.writeText(`https://${latest.url}`).then(() => {
|
|
125
|
+
setCopied(true)
|
|
126
|
+
setTimeout(() => setCopied(false), 2000)
|
|
127
|
+
}).catch(err => {
|
|
128
|
+
console.error('deploy-vercel-from-sanity: clipboard error', err)
|
|
129
|
+
})
|
|
130
|
+
}, [latest?.url])
|
|
131
|
+
|
|
132
|
+
const fetchErrorLogs = useCallback(async () => {
|
|
133
|
+
if (!latest?.uid) return
|
|
134
|
+
setLoadingLogs(true)
|
|
135
|
+
setLogError(null)
|
|
136
|
+
try {
|
|
137
|
+
const events = await getDeploymentEvents({
|
|
138
|
+
deploymentId: latest.uid,
|
|
139
|
+
token,
|
|
140
|
+
teamId: target.teamId,
|
|
141
|
+
})
|
|
142
|
+
// Keep stderr lines and stdout lines that look like errors, in forward order
|
|
143
|
+
const lines = events
|
|
144
|
+
.filter(e => e.type === 'stderr' || e.type === 'stdout')
|
|
145
|
+
.map(e => e.text ?? '')
|
|
146
|
+
.filter(Boolean)
|
|
147
|
+
.reverse() // API returns backward — flip to chronological
|
|
148
|
+
.slice(-30) // last 30 lines
|
|
149
|
+
setErrorLines(lines.length > 0 ? lines : ['No log output captured.'])
|
|
150
|
+
} catch (err) {
|
|
151
|
+
setLogError(err instanceof Error ? err.message : 'Failed to load build logs')
|
|
152
|
+
} finally {
|
|
153
|
+
setLoadingLogs(false)
|
|
154
|
+
}
|
|
155
|
+
}, [latest?.uid, token, target.teamId])
|
|
156
|
+
|
|
157
|
+
const toggleErrorLogs = useCallback(() => {
|
|
158
|
+
if (!showErrorLogs && errorLines.length === 0 && !logError) {
|
|
159
|
+
fetchErrorLogs()
|
|
160
|
+
}
|
|
161
|
+
setShowErrorLogs(v => !v)
|
|
162
|
+
}, [showErrorLogs, errorLines.length, logError, fetchErrorLogs])
|
|
163
|
+
|
|
112
164
|
// ── Derived display values ────────────────────────────────────────────────
|
|
113
|
-
const branch
|
|
165
|
+
const branch = latest?.meta?.githubCommitRef
|
|
114
166
|
const commitMsg = latest?.meta?.githubCommitMessage?.split('\n')[0]
|
|
115
|
-
const sha
|
|
116
|
-
const creator
|
|
167
|
+
const sha = shortSha(latest?.meta?.githubCommitSha)
|
|
168
|
+
const creator = latest?.creator?.username
|
|
117
169
|
const deployedAt = latest?.created ? timeAgo(latest.created) : null
|
|
170
|
+
const vercelProjectUrl = projectHref(latest?.inspectorUrl)
|
|
171
|
+
const isError = latest?.state === 'ERROR'
|
|
118
172
|
|
|
119
173
|
return (
|
|
120
174
|
<>
|
|
121
|
-
<Card padding={
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
175
|
+
<Card padding={5} radius={2} shadow={1} tone="default">
|
|
176
|
+
<Flex gap={4} align="stretch">
|
|
177
|
+
|
|
178
|
+
{/* ── Left: info column ──────────────────────────────────── */}
|
|
179
|
+
<Stack space={3} flex={1} style={{ minWidth: 0 }}>
|
|
180
|
+
|
|
181
|
+
{/* Name + menu */}
|
|
182
|
+
<Flex align="center" justify="space-between" gap={2}>
|
|
126
183
|
<Text size={2} weight="semibold">{target.name}</Text>
|
|
127
|
-
<
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
<Text size={0} muted>Hook: {hookId || '—'}</Text>
|
|
133
|
-
{target.teamId && (
|
|
134
|
-
<>
|
|
135
|
-
<Text size={0} muted>·</Text>
|
|
136
|
-
<Text size={0} muted>Team: {target.teamId}</Text>
|
|
137
|
-
</>
|
|
138
|
-
)}
|
|
139
|
-
</Flex>
|
|
140
|
-
</Stack>
|
|
141
|
-
|
|
142
|
-
{/* Action menu */}
|
|
143
|
-
<MenuButton
|
|
144
|
-
button={
|
|
145
|
-
<Button mode="ghost" icon={EllipsisVerticalIcon} />
|
|
146
|
-
}
|
|
147
|
-
id={`menu-${target._id}`}
|
|
148
|
-
menu={
|
|
149
|
-
<Menu>
|
|
150
|
-
<MenuItem
|
|
151
|
-
text="History"
|
|
152
|
-
icon={ClockIcon}
|
|
153
|
-
onClick={() => setShowHistory(true)}
|
|
154
|
-
/>
|
|
155
|
-
{safeHref(latest?.inspectorUrl) && (
|
|
156
|
-
<MenuItem
|
|
157
|
-
text="Build logs"
|
|
158
|
-
icon={LaunchIcon}
|
|
159
|
-
as="a"
|
|
160
|
-
href={safeHref(latest?.inspectorUrl)}
|
|
161
|
-
target="_blank"
|
|
162
|
-
rel="noreferrer"
|
|
163
|
-
/>
|
|
164
|
-
)}
|
|
165
|
-
{!target.disableDeleteAction && (
|
|
184
|
+
<MenuButton
|
|
185
|
+
button={<Button mode="ghost" icon={EllipsisVerticalIcon} padding={2} />}
|
|
186
|
+
id={`menu-${target._id}`}
|
|
187
|
+
menu={
|
|
188
|
+
<Menu>
|
|
166
189
|
<MenuItem
|
|
167
|
-
text="
|
|
168
|
-
icon={
|
|
169
|
-
|
|
170
|
-
onClick={() => onDelete(target)}
|
|
190
|
+
text="History"
|
|
191
|
+
icon={ClockIcon}
|
|
192
|
+
onClick={() => setShowHistory(true)}
|
|
171
193
|
/>
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
194
|
+
{safeHref(latest?.inspectorUrl) && (
|
|
195
|
+
<MenuItem
|
|
196
|
+
text="Build logs"
|
|
197
|
+
icon={LaunchIcon}
|
|
198
|
+
as="a"
|
|
199
|
+
href={safeHref(latest?.inspectorUrl)}
|
|
200
|
+
target="_blank"
|
|
201
|
+
rel="noreferrer"
|
|
202
|
+
/>
|
|
203
|
+
)}
|
|
204
|
+
{vercelProjectUrl && (
|
|
205
|
+
<MenuItem
|
|
206
|
+
text="Open in Vercel"
|
|
207
|
+
icon={LaunchIcon}
|
|
208
|
+
as="a"
|
|
209
|
+
href={vercelProjectUrl}
|
|
210
|
+
target="_blank"
|
|
211
|
+
rel="noreferrer"
|
|
212
|
+
/>
|
|
213
|
+
)}
|
|
214
|
+
{!target.disableDeleteAction && (
|
|
215
|
+
<MenuItem
|
|
216
|
+
text="Delete"
|
|
217
|
+
icon={TrashIcon}
|
|
218
|
+
tone="critical"
|
|
219
|
+
onClick={() => onDelete(target)}
|
|
220
|
+
/>
|
|
221
|
+
)}
|
|
222
|
+
</Menu>
|
|
223
|
+
}
|
|
224
|
+
popover={{ placement: 'bottom-end' }}
|
|
225
|
+
/>
|
|
226
|
+
</Flex>
|
|
178
227
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
228
|
+
{/* Subtitle: project + hook IDs */}
|
|
229
|
+
<Flex gap={2} wrap="wrap">
|
|
230
|
+
<Text size={0} muted>
|
|
231
|
+
{projectId ? `${projectId.slice(0, 18)}…` : '—'}
|
|
232
|
+
</Text>
|
|
233
|
+
<Text size={0} muted>·</Text>
|
|
234
|
+
<Text size={0} muted>Hook: {hookId || '—'}</Text>
|
|
235
|
+
{target.teamId && (
|
|
236
|
+
<>
|
|
237
|
+
<Text size={0} muted>·</Text>
|
|
238
|
+
<Text size={0} muted>Team: {target.teamId}</Text>
|
|
239
|
+
</>
|
|
240
|
+
)}
|
|
184
241
|
</Flex>
|
|
185
|
-
) : (
|
|
186
|
-
<Stack space={3}>
|
|
187
|
-
<Flex align="center" gap={3} wrap="wrap">
|
|
188
|
-
{/* Status badge */}
|
|
189
|
-
{triggering ? (
|
|
190
|
-
<Flex align="center" gap={2}>
|
|
191
|
-
<Spinner muted />
|
|
192
|
-
<Badge tone="caution" mode="outline">Triggering…</Badge>
|
|
193
|
-
</Flex>
|
|
194
|
-
) : (
|
|
195
|
-
<StatusBadge state={latest?.state} showSpinner />
|
|
196
|
-
)}
|
|
197
242
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
243
|
+
{/* Status + metadata */}
|
|
244
|
+
{loadingInitial ? (
|
|
245
|
+
<Flex align="center" gap={2}>
|
|
246
|
+
<Spinner muted />
|
|
247
|
+
<Text size={1} muted>Loading…</Text>
|
|
248
|
+
</Flex>
|
|
249
|
+
) : (
|
|
250
|
+
<Stack space={2}>
|
|
251
|
+
<Flex align="center" gap={2} wrap="wrap">
|
|
252
|
+
{/* Status */}
|
|
253
|
+
{triggering ? (
|
|
254
|
+
<Flex align="center" gap={2}>
|
|
255
|
+
<Spinner muted />
|
|
256
|
+
<Badge tone="caution" mode="outline">Triggering…</Badge>
|
|
257
|
+
</Flex>
|
|
258
|
+
) : (
|
|
259
|
+
<StatusBadge state={latest?.state} showSpinner />
|
|
260
|
+
)}
|
|
205
261
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
262
|
+
{/* Build timer (active) or time since deploy (idle) */}
|
|
263
|
+
{isActive && elapsed > 0 ? (
|
|
264
|
+
<Flex align="center" gap={1}>
|
|
265
|
+
<ClockIcon />
|
|
266
|
+
<Text size={1} muted>{formatDuration(elapsed)}</Text>
|
|
267
|
+
</Flex>
|
|
268
|
+
) : (!isActive && deployedAt) ? (
|
|
269
|
+
<Text size={1} muted>{deployedAt}</Text>
|
|
270
|
+
) : null}
|
|
210
271
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
272
|
+
{/* Branch */}
|
|
273
|
+
{branch && (
|
|
274
|
+
<>
|
|
275
|
+
<Text size={1} muted>·</Text>
|
|
276
|
+
<Badge tone="default" mode="outline">{branch}</Badge>
|
|
277
|
+
</>
|
|
278
|
+
)}
|
|
279
|
+
|
|
280
|
+
{/* Commit SHA — tooltip shows full message */}
|
|
281
|
+
{sha && (
|
|
282
|
+
<Tooltip
|
|
283
|
+
content={
|
|
284
|
+
<Box padding={2}>
|
|
285
|
+
<Text size={1}>{commitMsg ?? sha}</Text>
|
|
286
|
+
</Box>
|
|
287
|
+
}
|
|
288
|
+
portal
|
|
289
|
+
>
|
|
290
|
+
<Text
|
|
291
|
+
size={1}
|
|
292
|
+
muted
|
|
293
|
+
style={{ cursor: 'default', fontFamily: 'monospace' }}
|
|
294
|
+
>
|
|
295
|
+
{sha}
|
|
296
|
+
</Text>
|
|
297
|
+
</Tooltip>
|
|
298
|
+
)}
|
|
218
299
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
300
|
+
{/* Creator */}
|
|
301
|
+
{creator && <Text size={1} muted>by {creator}</Text>}
|
|
302
|
+
|
|
303
|
+
{/* Preview link + copy URL */}
|
|
304
|
+
{latest?.url && latest.state === 'READY' && (
|
|
305
|
+
<>
|
|
306
|
+
<a
|
|
307
|
+
href={`https://${latest.url}`}
|
|
308
|
+
target="_blank"
|
|
309
|
+
rel="noreferrer"
|
|
310
|
+
style={{ color: 'inherit' }}
|
|
311
|
+
>
|
|
312
|
+
<Flex align="center" gap={1}>
|
|
313
|
+
<LaunchIcon />
|
|
314
|
+
<Text size={1}>Preview</Text>
|
|
315
|
+
</Flex>
|
|
316
|
+
</a>
|
|
317
|
+
<Tooltip
|
|
318
|
+
content={
|
|
319
|
+
<Box padding={2}>
|
|
320
|
+
<Text size={1}>{copied ? 'Copied!' : 'Copy URL'}</Text>
|
|
321
|
+
</Box>
|
|
322
|
+
}
|
|
323
|
+
portal
|
|
324
|
+
>
|
|
325
|
+
<Button
|
|
326
|
+
mode="ghost"
|
|
327
|
+
icon={copied ? CheckmarkIcon : CopyIcon}
|
|
328
|
+
padding={1}
|
|
329
|
+
tone={copied ? 'positive' : 'default'}
|
|
330
|
+
onClick={copyUrl}
|
|
331
|
+
/>
|
|
332
|
+
</Tooltip>
|
|
333
|
+
</>
|
|
334
|
+
)}
|
|
335
|
+
</Flex>
|
|
336
|
+
|
|
337
|
+
{/* Commit message */}
|
|
338
|
+
{commitMsg && (
|
|
339
|
+
<Text
|
|
340
|
+
size={0}
|
|
341
|
+
muted
|
|
342
|
+
style={{
|
|
343
|
+
overflow: 'hidden',
|
|
344
|
+
textOverflow: 'ellipsis',
|
|
345
|
+
whiteSpace: 'nowrap',
|
|
346
|
+
}}
|
|
228
347
|
>
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
</Text>
|
|
232
|
-
</Tooltip>
|
|
348
|
+
{commitMsg}
|
|
349
|
+
</Text>
|
|
233
350
|
)}
|
|
234
351
|
|
|
235
|
-
{/*
|
|
236
|
-
{
|
|
237
|
-
<
|
|
238
|
-
|
|
352
|
+
{/* Error expansion */}
|
|
353
|
+
{isError && (
|
|
354
|
+
<Stack space={2}>
|
|
355
|
+
<Button
|
|
356
|
+
text={showErrorLogs ? 'Hide error details' : 'Show error details'}
|
|
357
|
+
mode="ghost"
|
|
358
|
+
tone="critical"
|
|
359
|
+
icon={WarningOutlineIcon}
|
|
360
|
+
fontSize={1}
|
|
361
|
+
padding={2}
|
|
362
|
+
onClick={toggleErrorLogs}
|
|
363
|
+
style={{ alignSelf: 'flex-start' }}
|
|
364
|
+
/>
|
|
239
365
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
366
|
+
{showErrorLogs && (
|
|
367
|
+
<Card tone="critical" radius={2} padding={3}>
|
|
368
|
+
{loadingLogs && (
|
|
369
|
+
<Flex align="center" gap={2}>
|
|
370
|
+
<Spinner muted />
|
|
371
|
+
<Text size={1} muted>Loading logs…</Text>
|
|
372
|
+
</Flex>
|
|
373
|
+
)}
|
|
374
|
+
{logError && (
|
|
375
|
+
<Stack space={2}>
|
|
376
|
+
<Text size={1}>{logError}</Text>
|
|
377
|
+
{safeHref(latest?.inspectorUrl) && (
|
|
378
|
+
<a
|
|
379
|
+
href={safeHref(latest?.inspectorUrl)}
|
|
380
|
+
target="_blank"
|
|
381
|
+
rel="noreferrer"
|
|
382
|
+
style={{ color: 'inherit' }}
|
|
383
|
+
>
|
|
384
|
+
<Flex align="center" gap={1}>
|
|
385
|
+
<LaunchIcon />
|
|
386
|
+
<Text size={1}>View full logs in Vercel</Text>
|
|
387
|
+
</Flex>
|
|
388
|
+
</a>
|
|
389
|
+
)}
|
|
390
|
+
</Stack>
|
|
391
|
+
)}
|
|
392
|
+
{!loadingLogs && !logError && errorLines.length > 0 && (
|
|
393
|
+
<Stack space={2}>
|
|
394
|
+
<Box
|
|
395
|
+
style={{
|
|
396
|
+
maxHeight: 240,
|
|
397
|
+
overflowY: 'auto',
|
|
398
|
+
fontFamily: 'monospace',
|
|
399
|
+
fontSize: 11,
|
|
400
|
+
lineHeight: 1.6,
|
|
401
|
+
}}
|
|
402
|
+
>
|
|
403
|
+
{errorLines.map((line, i) => (
|
|
404
|
+
<Code
|
|
405
|
+
key={i}
|
|
406
|
+
size={1}
|
|
407
|
+
style={{ display: 'block', whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}
|
|
408
|
+
>
|
|
409
|
+
{line}
|
|
410
|
+
</Code>
|
|
411
|
+
))}
|
|
412
|
+
</Box>
|
|
413
|
+
{safeHref(latest?.inspectorUrl) && (
|
|
414
|
+
<a
|
|
415
|
+
href={safeHref(latest?.inspectorUrl)}
|
|
416
|
+
target="_blank"
|
|
417
|
+
rel="noreferrer"
|
|
418
|
+
style={{ color: 'inherit' }}
|
|
419
|
+
>
|
|
420
|
+
<Flex align="center" gap={1}>
|
|
421
|
+
<LaunchIcon />
|
|
422
|
+
<Text size={1}>View full logs in Vercel</Text>
|
|
423
|
+
</Flex>
|
|
424
|
+
</a>
|
|
425
|
+
)}
|
|
426
|
+
</Stack>
|
|
427
|
+
)}
|
|
428
|
+
</Card>
|
|
429
|
+
)}
|
|
430
|
+
</Stack>
|
|
253
431
|
)}
|
|
254
|
-
</Flex>
|
|
255
432
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
433
|
+
{/* Trigger error */}
|
|
434
|
+
{deployError && (
|
|
435
|
+
<Card tone="critical" padding={2} radius={2}>
|
|
436
|
+
<Text size={1}>{deployError}</Text>
|
|
437
|
+
</Card>
|
|
438
|
+
)}
|
|
439
|
+
</Stack>
|
|
440
|
+
)}
|
|
441
|
+
</Stack>
|
|
264
442
|
|
|
265
|
-
{/* ──
|
|
266
|
-
<Flex
|
|
443
|
+
{/* ── Right: action buttons — stretch full card height ── */}
|
|
444
|
+
<Flex
|
|
445
|
+
direction="column"
|
|
446
|
+
gap={2}
|
|
447
|
+
style={{ flexShrink: 0, alignSelf: 'stretch' }}
|
|
448
|
+
>
|
|
267
449
|
{isActiveState(latest?.state) && (
|
|
268
450
|
<Button
|
|
269
451
|
text="Cancel"
|
|
@@ -281,9 +463,11 @@ export function DeployItem({ target, token, onDelete }: DeployItemProps) {
|
|
|
281
463
|
loading={triggering}
|
|
282
464
|
disabled={isActive}
|
|
283
465
|
onClick={deploy}
|
|
466
|
+
style={{ flex: 1 }}
|
|
284
467
|
/>
|
|
285
468
|
</Flex>
|
|
286
|
-
|
|
469
|
+
|
|
470
|
+
</Flex>
|
|
287
471
|
</Card>
|
|
288
472
|
|
|
289
473
|
{showHistory && (
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useState, useEffect, useCallback } from 'react'
|
|
3
3
|
import { useClient } from 'sanity'
|
|
4
4
|
import {
|
|
5
|
-
Card, Box, Stack, Flex, Text, Heading, Spinner, Button, Dialog, useToast,
|
|
5
|
+
Card, Box, Stack, Flex, Text, Heading, Spinner, Button, Badge, Dialog, useToast,
|
|
6
6
|
} from '@sanity/ui'
|
|
7
7
|
import { RocketIcon, TokenIcon, TrashIcon, WarningOutlineIcon } from '@sanity/icons'
|
|
8
8
|
import { DeployItem } from './DeployItem'
|
|
@@ -81,11 +81,7 @@ export function DeployTool() {
|
|
|
81
81
|
)
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
if (!token
|
|
85
|
-
return <TokenSetup onSaved={() => { setShowTokenSetup(false); load() }} />
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (showTokenSetup) {
|
|
84
|
+
if (!token || showTokenSetup) {
|
|
89
85
|
return (
|
|
90
86
|
<TokenSetup
|
|
91
87
|
onSaved={() => {
|
|
@@ -106,13 +102,16 @@ export function DeployTool() {
|
|
|
106
102
|
<RocketIcon />
|
|
107
103
|
<Heading size={2}>Deploy</Heading>
|
|
108
104
|
</Flex>
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
105
|
+
<Flex align="center" gap={3}>
|
|
106
|
+
<Badge tone="positive" mode="outline">Connected</Badge>
|
|
107
|
+
<Button
|
|
108
|
+
text="Change Token"
|
|
109
|
+
mode="ghost"
|
|
110
|
+
icon={TokenIcon}
|
|
111
|
+
fontSize={1}
|
|
112
|
+
onClick={() => setShowTokenSetup(true)}
|
|
113
|
+
/>
|
|
114
|
+
</Flex>
|
|
116
115
|
</Flex>
|
|
117
116
|
|
|
118
117
|
{/* ── No targets ──────────────────────────────────────────── */}
|
|
@@ -121,22 +120,30 @@ export function DeployTool() {
|
|
|
121
120
|
<Stack space={3} style={{ textAlign: 'center' }}>
|
|
122
121
|
<Text size={2} weight="semibold">No deploy targets configured</Text>
|
|
123
122
|
<Text size={1} muted>
|
|
124
|
-
Create a <code>vercel_deploy</code> document in the dataset with a Vercel
|
|
125
|
-
hook URL
|
|
123
|
+
Create a <code>vercel_deploy</code> document in the dataset with a Vercel
|
|
124
|
+
deploy hook URL.
|
|
126
125
|
</Text>
|
|
127
126
|
</Stack>
|
|
128
127
|
</Card>
|
|
129
128
|
)}
|
|
130
129
|
|
|
131
|
-
{/* ── Deploy targets
|
|
132
|
-
{
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
130
|
+
{/* ── Deploy targets — responsive 2-col grid ──────────────── */}
|
|
131
|
+
{targets.length > 0 && (
|
|
132
|
+
<div style={{
|
|
133
|
+
display: 'grid',
|
|
134
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(540px, 1fr))',
|
|
135
|
+
gap: '16px',
|
|
136
|
+
}}>
|
|
137
|
+
{targets.map(target => (
|
|
138
|
+
<DeployItem
|
|
139
|
+
key={target._id}
|
|
140
|
+
target={target}
|
|
141
|
+
token={token}
|
|
142
|
+
onDelete={setPendingDelete}
|
|
143
|
+
/>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
140
147
|
</Stack>
|
|
141
148
|
</Box>
|
|
142
149
|
|
|
@@ -172,8 +179,8 @@ export function DeployTool() {
|
|
|
172
179
|
<Text size={2} weight="semibold">{pendingDelete.name}</Text>
|
|
173
180
|
</Flex>
|
|
174
181
|
<Text size={1} muted>
|
|
175
|
-
This removes the deploy target from the dataset. The Vercel deploy hook
|
|
176
|
-
not affected.
|
|
182
|
+
This removes the deploy target from the dataset. The Vercel deploy hook
|
|
183
|
+
itself is not affected.
|
|
177
184
|
</Text>
|
|
178
185
|
</Stack>
|
|
179
186
|
</Box>
|