@liiift-studio/deploy-vercel-from-sanity 1.1.1 → 1.2.1

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.
@@ -1,571 +0,0 @@
1
- // Per-deploy-target card — shows status, build timer, history, cancel, deploy, copy URL, and error logs
2
- import { useState, useEffect, useCallback, useRef } from 'react'
3
- import { flushSync } from 'react-dom'
4
- import {
5
- Card, Box, Flex, Text, Button, Badge, Spinner,
6
- } from '@sanity/ui'
7
- import { Stack, useToast, Tooltip, ActionMenu, Code } from '../ui'
8
- import {
9
- ClockIcon, TrashIcon, EllipsisVerticalIcon, LaunchIcon,
10
- CopyIcon, CheckmarkIcon, WarningOutlineIcon, ChevronDownIcon, ChevronUpIcon, EditIcon, SchemaIcon
11
- } from '../icons'
12
- import { listDeployments, cancelDeployment, triggerDeploy, getDeploymentEvents } from '../lib/api'
13
- import { parseHookUrl, isActiveState, formatDuration, timeAgo, shortSha, safeHref, projectHref, githubCommitHref } from '../lib/helpers'
14
- import { StatusBadge } from './StatusBadge'
15
- import { DeployHistory } from './DeployHistory'
16
- import type { DeployTarget, VercelDeployment } from '../types'
17
-
18
- const POLL_INTERVAL_MS = 5_000
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
22
-
23
- interface DeployItemProps {
24
- target: DeployTarget
25
- token: string
26
- onDelete: (target: DeployTarget) => void
27
- onEdit: (target: DeployTarget) => void
28
- }
29
-
30
- export function DeployItem({ target, token, onDelete, onEdit }: DeployItemProps) {
31
- const { projectId, hookId } = parseHookUrl(target.url)
32
- const toast = useToast()
33
-
34
- const [deployments, setDeployments] = useState<VercelDeployment[]>([])
35
- const [loadingInitial, setLoadingInitial] = useState(true)
36
- const [pendingSince, setPendingSince] = useState<number | null>(null)
37
- const [canceling, setCanceling] = useState(false)
38
- const [deployError, setDeployError] = useState<string | null>(null)
39
- const [showHistory, setShowHistory] = useState(false)
40
- const [elapsed, setElapsed] = useState(0)
41
- const [copied, setCopied] = useState(false)
42
- const [showDetails, setShowDetails] = useState(false)
43
- const [showErrorLogs, setShowErrorLogs] = useState(false)
44
- const [errorLines, setErrorLines] = useState<string[]>([])
45
- const [loadingLogs, setLoadingLogs] = useState(false)
46
- const [logError, setLogError] = useState<string | null>(null)
47
-
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)
55
-
56
- // ── Fetch deployments ──────────────────────────────────────────────────────
57
- const fetchDeployments = useCallback(async () => {
58
- if (!projectId || !hookId || !token) return
59
- try {
60
- const data = await listDeployments({ projectId, hookId, token, teamId: target.teamId })
61
- setDeployments(data)
62
- } catch (err) {
63
- console.error('deploy-vercel-from-sanity: fetch error', err)
64
- }
65
- }, [projectId, hookId, token, target.teamId])
66
-
67
- useEffect(() => {
68
- fetchDeployments().finally(() => setLoadingInitial(false))
69
- }, [fetchDeployments])
70
-
71
- useEffect(() => {
72
- if (!isActive) return
73
- const id = setInterval(fetchDeployments, POLL_INTERVAL_MS)
74
- return () => clearInterval(id)
75
- }, [isActive, fetchDeployments])
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).
86
- useEffect(() => {
87
- if (!isPending) return
88
- const id = setTimeout(() => setPendingSince(null), PENDING_TIMEOUT_MS)
89
- return () => clearTimeout(id)
90
- }, [isPending])
91
-
92
- //── Deploy-complete toast ─────────────────────────────────────────────────
93
- const prevStateRef = useRef<string | undefined>(undefined)
94
- useEffect(() => {
95
- const current = latest?.state
96
- const prev = prevStateRef.current
97
- if (prev && isActiveState(prev as never) && current && !isActiveState(current as never)) {
98
- if (current === 'READY') {
99
- toast.push({ status: 'success', title: `${target.name} deployed`, description: 'Build completed successfully' })
100
- } else if (current === 'ERROR') {
101
- toast.push({ status: 'error', title: `${target.name} failed`, description: 'Build encountered an error — check error details' })
102
- } else if (current === 'CANCELED') {
103
- toast.push({ status: 'warning', title: `${target.name} canceled`, description: 'Deployment was canceled' })
104
- }
105
- }
106
- prevStateRef.current = current
107
- }, [latest?.state, target.name, toast])
108
-
109
- useEffect(() => {
110
- setShowErrorLogs(false)
111
- setErrorLines([])
112
- setLogError(null)
113
- }, [latest?.uid])
114
-
115
- // ── Build timer ───────────────────────────────────────────────────────────
116
- const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
117
- useEffect(() => {
118
- if (isActive) {
119
- // While optimistic, count from the click; once real, count from the deployment's own timestamp
120
- const start = pendingSince ?? latest?.created ?? Date.now()
121
- setElapsed(Math.floor((Date.now() - start) / 1000))
122
- timerRef.current = setInterval(() => {
123
- setElapsed(Math.floor((Date.now() - start) / 1000))
124
- }, 1000)
125
- } else {
126
- setElapsed(0)
127
- if (timerRef.current) clearInterval(timerRef.current)
128
- }
129
- return () => { if (timerRef.current) clearInterval(timerRef.current) }
130
- }, [isActive, pendingSince, latest?.created])
131
-
132
- // ── Actions ───────────────────────────────────────────────────────────────
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
136
- flushSync(() => {
137
- setDeployError(null)
138
- triggeredFromUidRef.current = latest?.uid
139
- setPendingSince(Date.now())
140
- })
141
- void (async () => {
142
- try {
143
- await triggerDeploy(target.url)
144
- setTimeout(fetchDeployments, 2000)
145
- } catch (err) {
146
- setPendingSince(null)
147
- setDeployError(err instanceof Error ? err.message : 'Deploy failed')
148
- }
149
- })()
150
- }, [target.url, fetchDeployments, latest?.uid])
151
-
152
- const cancel = useCallback(async () => {
153
- if (!latest?.uid) return
154
- setCanceling(true)
155
- try {
156
- await cancelDeployment({ deploymentId: latest.uid, token, teamId: target.teamId })
157
- await fetchDeployments()
158
- } catch (err) {
159
- console.error('deploy-vercel-from-sanity: cancel error', err)
160
- } finally {
161
- setCanceling(false)
162
- }
163
- }, [latest?.uid, token, target.teamId, fetchDeployments])
164
-
165
- const copyUrl = useCallback(() => {
166
- if (!latest?.url) return
167
- const fullUrl = `https://${latest.url}`
168
- navigator.clipboard.writeText(fullUrl).then(() => {
169
- setCopied(true)
170
- setTimeout(() => setCopied(false), 2000)
171
- }).catch(() => {
172
- // Clipboard API unavailable — surface the URL for manual copy
173
- window.prompt('Copy deployment URL:', fullUrl)
174
- })
175
- }, [latest?.url])
176
-
177
- const fetchErrorLogs = useCallback(async () => {
178
- if (!latest?.uid) return
179
- setLoadingLogs(true)
180
- setLogError(null)
181
- try {
182
- const events = await getDeploymentEvents({
183
- deploymentId: latest.uid,
184
- token,
185
- teamId: target.teamId,
186
- })
187
- const lines = events
188
- .filter(e => e.type === 'stderr' || e.type === 'stdout')
189
- .map(e => e.text ?? '')
190
- .filter(Boolean)
191
- .reverse()
192
- .slice(-30)
193
- setErrorLines(lines.length > 0 ? lines : ['No stderr or stdout was captured for this build. Open the full build log in Vercel for details.'])
194
- } catch (err) {
195
- setLogError(err instanceof Error ? err.message : 'Failed to load build logs')
196
- } finally {
197
- setLoadingLogs(false)
198
- }
199
- }, [latest?.uid, token, target.teamId])
200
-
201
- const toggleErrorLogs = useCallback(() => {
202
- if (!showErrorLogs && errorLines.length === 0 && !logError) fetchErrorLogs()
203
- setShowErrorLogs(v => !v)
204
- }, [showErrorLogs, errorLines.length, logError, fetchErrorLogs])
205
-
206
- // ── Derived display values ────────────────────────────────────────────────
207
- const branch = latest?.meta?.githubCommitRef
208
- const commitMsg = latest?.meta?.githubCommitMessage?.split('\n')[0]
209
- const sha = shortSha(latest?.meta?.githubCommitSha)
210
- const fullSha = latest?.meta?.githubCommitSha
211
- const commitHref = githubCommitHref(latest?.meta)
212
- const creator = latest?.creator?.username
213
- const deployedAt = latest?.created ? timeAgo(latest.created) : null
214
- const vercelProjectUrl = projectHref(latest?.inspectorUrl)
215
- const isError = latest?.state === 'ERROR'
216
-
217
- return (
218
- <>
219
- <Card radius={2} shadow={1} tone="default">
220
- <Flex align="stretch" className="dvfs-card-flex">
221
-
222
- {/* ── Left: info column ──────────────────────────────────── */}
223
- <Flex direction="column" flex={1} style={{ minWidth: 0 }}>
224
-
225
- <Stack space={3} padding={3} style={{ flex: 1 }}>
226
-
227
- {/* ── Title row: name + branch + status + menu ──────── */}
228
- <Flex align="center" justify="space-between" gap={2}>
229
- <Flex align="center" gap={2} style={{ minWidth: 0, flexWrap: 'wrap' }}>
230
- <Text size={2} weight="semibold" style={{ flexShrink: 0 }}>{target.name}</Text>
231
- {branch && (
232
- <Badge tone="default" padding={2}>
233
- <Flex align="center" gap={1}>
234
- {branch}
235
- <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" style={{ marginLeft: "-0.1em", opacity: 0.5 }}>
236
- <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" />
237
- </svg>
238
- </Flex>
239
- </Badge>
240
- )}
241
- {token && !loadingInitial && (
242
- <>
243
- {isPending ? (
244
- <StatusBadge state="QUEUED" />
245
- ) : (
246
- <StatusBadge state={latest?.state} />
247
- )}
248
- {isActiveState(latest?.state) && (
249
- <Button
250
- text="Cancel"
251
- mode="ghost"
252
- tone="critical"
253
- loading={canceling}
254
- disabled={canceling}
255
- onClick={cancel}
256
- style={{ cursor: 'pointer' }}
257
- />
258
- )}
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 ? (
263
- <Flex align="center" gap={1}>
264
- <Spinner muted style={{ marginLeft: 4 }} />
265
- <Text size={1} muted>{formatDuration(elapsed)}</Text>
266
- </Flex>
267
- ) : deployedAt ? (
268
- <Text size={1} muted>{deployedAt}</Text>
269
- ) : null}
270
- </>
271
- )}
272
- </Flex>
273
- <ActionMenu
274
- id={`menu-${target._id}`}
275
- buttonIcon={EllipsisVerticalIcon}
276
- items={[
277
- { text: 'Edit target', icon: EditIcon, onClick: () => onEdit(target) },
278
- { text: 'History', icon: ClockIcon, onClick: () => setShowHistory(true) },
279
- ...(safeHref(latest?.inspectorUrl)
280
- ? [{ text: 'Build logs', icon: LaunchIcon, href: safeHref(latest?.inspectorUrl)! }]
281
- : []),
282
- ...(vercelProjectUrl
283
- ? [{ text: 'Open in Vercel', icon: LaunchIcon, href: vercelProjectUrl }]
284
- : []),
285
- ...(!target.disableDeleteAction
286
- ? [{ text: 'Delete', icon: TrashIcon, tone: 'critical' as const, onClick: () => onDelete(target) }]
287
- : []),
288
- ]}
289
- />
290
- </Flex>
291
-
292
- {/* ── Divider below title ────────────────────────────── */}
293
- <hr style={{ border: 'none', borderTop: '1px solid currentColor', opacity: 0.1, margin: 0 }} />
294
-
295
- {/* ── Status + metadata ──────────────────────────────── */}
296
- {!token ? (
297
- <Text size={1} muted>Connect a Vercel API token to see deployment status.</Text>
298
- ) : loadingInitial ? (
299
- <Flex align="center" gap={2}>
300
- <Spinner muted />
301
- <Text size={1} muted>Loading…</Text>
302
- </Flex>
303
- ) : (
304
- <Stack space={2}>
305
-
306
- {/* Metadata row */}
307
- <Flex align="center" gap={2} wrap="wrap">
308
-
309
- {/* Visit link + copy URL */}
310
- {latest?.url && latest.state === 'READY' && (
311
- <>
312
- <a
313
- href={`https://${latest.url}`}
314
- target="_blank"
315
- rel="noreferrer"
316
- style={{ color: 'inherit' }}
317
- >
318
- <Flex align="center" gap={1}>
319
- <Text size={1}>{latest.url}</Text>
320
- </Flex>
321
- </a>
322
- </>
323
- )}
324
-
325
- {/* Commit SHA — links to GitHub if repo info available, tooltip shows full message */}
326
- {sha && (
327
- <Tooltip text={commitMsg ?? sha}>
328
- {commitHref ? (
329
- <a
330
- href={commitHref}
331
- target="_blank"
332
- rel="noreferrer"
333
- style={{ color: 'inherit', textDecoration: 'none' }}
334
- >
335
- <Text size={1} muted style={{ cursor: 'pointer', fontFamily: 'monospace' }}>
336
- {sha}
337
- </Text>
338
- </a>
339
- ) : (
340
- <Text size={1} muted style={{ cursor: 'default', fontFamily: 'monospace' }}>
341
- {sha}
342
- </Text>
343
- )}
344
- </Tooltip>
345
- )}
346
-
347
- {/* Creator */}
348
- {creator && <Text size={1} muted>by {creator}</Text>}
349
-
350
- {/* Build duration — only shown when READY and ready timestamp is available */}
351
- {latest?.state === 'READY' && latest.ready && latest.created && (
352
- <Text size={1} muted>Took {formatDuration(Math.floor((latest.ready - latest.created) / 1000))} to build</Text>
353
- )}
354
-
355
- {/* Visit link + copy URL */}
356
- {latest?.url && latest.state === 'READY' && (
357
- <>
358
- <Tooltip text={copied ? 'Copied!' : 'Copy URL'}>
359
- <Button
360
- mode="ghost"
361
- icon={copied ? CheckmarkIcon : CopyIcon}
362
- padding={1}
363
- tone={copied ? 'positive' : 'default'}
364
- onClick={copyUrl}
365
- style={{ cursor: 'pointer' }}
366
- />
367
- </Tooltip>
368
- </>
369
- )}
370
- </Flex>
371
-
372
- <Flex align="center" gap={2} wrap="wrap">
373
- {/* Commit message */}
374
- {commitMsg && (
375
- <Text
376
- size={0}
377
- muted
378
- style={{ fontStyle: 'italic'}}
379
- >
380
- {commitMsg}
381
- </Text>
382
- )}
383
-
384
- {/* Error expansion */}
385
- {isError && (
386
- <Stack space={2}>
387
- <Button
388
- text={showErrorLogs ? 'Hide error details' : 'Show error details'}
389
- mode="ghost"
390
- tone="critical"
391
- icon={WarningOutlineIcon}
392
- fontSize={1}
393
- padding={2}
394
- onClick={toggleErrorLogs}
395
- style={{ alignSelf: 'flex-start', cursor: 'pointer' }}
396
- />
397
- {showErrorLogs && (
398
- <Card tone="critical" radius={2} padding={3}>
399
- {loadingLogs && (
400
- <Flex align="center" gap={2}>
401
- <Spinner muted />
402
- <Text size={1} muted>Loading logs…</Text>
403
- </Flex>
404
- )}
405
- {logError && (
406
- <Stack space={2}>
407
- <Text size={1}>{logError}</Text>
408
- {safeHref(latest?.inspectorUrl) && (
409
- <a href={safeHref(latest?.inspectorUrl)} target="_blank" rel="noreferrer" style={{ color: 'inherit' }}>
410
- <Flex align="center" gap={1}>
411
- <LaunchIcon />
412
- <Text size={1}>View full logs in Vercel</Text>
413
- </Flex>
414
- </a>
415
- )}
416
- </Stack>
417
- )}
418
- {!loadingLogs && !logError && errorLines.length > 0 && (
419
- <Stack space={2}>
420
- <Box style={{ maxHeight: 240, overflowY: 'auto', fontFamily: 'monospace', fontSize: 13, lineHeight: 1.6 }}>
421
- {errorLines.map((line, i) => (
422
- <Code key={i} style={{ display: 'block', whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
423
- {line}
424
- </Code>
425
- ))}
426
- </Box>
427
- {safeHref(latest?.inspectorUrl) && (
428
- <a href={safeHref(latest?.inspectorUrl)} target="_blank" rel="noreferrer" style={{ color: 'inherit' }}>
429
- <Flex align="center" gap={1}>
430
- <LaunchIcon />
431
- <Text size={1}>View full logs in Vercel</Text>
432
- </Flex>
433
- </a>
434
- )}
435
- </Stack>
436
- )}
437
- </Card>
438
- )}
439
- </Stack>
440
- )}
441
-
442
- {/* Trigger error */}
443
- {deployError && (
444
- <Card tone="critical" padding={2} radius={2}>
445
- <Text size={1}>{deployError}</Text>
446
- </Card>
447
- )}
448
- </Flex>
449
- </Stack>
450
- )}
451
-
452
- </Stack>
453
-
454
- {/* ── Details accordion — flush to left/bottom/right ─────── */}
455
- <Box>
456
- <Button
457
- mode="ghost"
458
- iconRight={showDetails ? ChevronUpIcon : ChevronDownIcon}
459
- text="Details"
460
- fontSize={0}
461
- padding={3}
462
- onClick={() => setShowDetails(v => !v)}
463
- style={{ width: '100%', justifyContent: 'flex-start', borderRadius: 0, cursor: 'pointer' }}
464
- />
465
- {showDetails && (
466
- <Card tone="primary" padding={3} className="dvfs-accordion-content" style={{ borderRadius: 0, borderTop: '1px solid rgba(128,128,128,0.15)' }}>
467
- <Stack space={2}>
468
- <Flex gap={2} align="center">
469
- <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Project</Text>
470
- <Text size={0} muted style={{ fontFamily: 'monospace' }}>{projectId || '—'}</Text>
471
- </Flex>
472
- <Flex gap={2} align="center">
473
- <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Hook</Text>
474
- <Text size={0} muted style={{ fontFamily: 'monospace' }}>{hookId || '—'}</Text>
475
- </Flex>
476
- {target.teamId && (
477
- <Flex gap={2} align="center">
478
- <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Team</Text>
479
- <Text size={0} muted style={{ fontFamily: 'monospace' }}>{target.teamId}</Text>
480
- </Flex>
481
- )}
482
- {fullSha && (
483
- <Flex gap={2} align="flex-start">
484
- <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Commit</Text>
485
- <Text size={0} muted style={{ fontFamily: 'monospace', wordBreak: 'break-all' }}>{fullSha}</Text>
486
- </Flex>
487
- )}
488
- {latest?.meta?.githubCommitAuthorName && (
489
- <Flex gap={2} align="center">
490
- <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Author</Text>
491
- <Text size={0} muted>{latest.meta.githubCommitAuthorName}</Text>
492
- </Flex>
493
- )}
494
- {branch && (
495
- <Flex gap={2} align="center">
496
- <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Branch</Text>
497
- <Text size={0} muted style={{ fontFamily: 'monospace' }}>{branch}</Text>
498
- </Flex>
499
- )}
500
- {latest?.uid && (
501
- <Flex gap={2} align="center">
502
- <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Deploy ID</Text>
503
- <Text size={0} muted style={{ fontFamily: 'monospace' }}>{latest.uid}</Text>
504
- </Flex>
505
- )}
506
- {latest?.url && (
507
- <Flex gap={2} align="flex-start">
508
- <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>URL</Text>
509
- <Text size={0} muted style={{ fontFamily: 'monospace', wordBreak: 'break-all' }}>{latest.url}</Text>
510
- </Flex>
511
- )}
512
- {safeHref(latest?.inspectorUrl) && (
513
- <Flex gap={2} align="center">
514
- <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Inspector</Text>
515
- <a href={safeHref(latest?.inspectorUrl)!} target="_blank" rel="noreferrer" style={{ color: 'inherit' }}>
516
- <Flex align="center" gap={1}>
517
- <Text size={0} muted style={{ fontFamily: 'monospace' }}>Open in Vercel</Text>
518
- <LaunchIcon style={{ width: 10, height: 10 }} />
519
- </Flex>
520
- </a>
521
- </Flex>
522
- )}
523
- {latest?.created && (
524
- <Flex gap={2} align="center">
525
- <Text size={0} muted weight="semibold" style={{ minWidth: LABEL_WIDTH }}>Created</Text>
526
- <Text size={0} muted>{new Date(latest.created).toLocaleString()}</Text>
527
- </Flex>
528
- )}
529
- </Stack>
530
- </Card>
531
- )}
532
- </Box>
533
-
534
- </Flex>
535
-
536
- {/* ── Right: action buttons — stretch full card height ── */}
537
- <Flex
538
- direction="column"
539
- gap={2}
540
- className="dvfs-deploy-col"
541
- style={{ flexShrink: 0, alignSelf: 'stretch' }}
542
- >
543
- <Button
544
- text="Deploy"
545
- tone="primary"
546
- loading={isPending}
547
- disabled={isActive}
548
- onClick={deploy}
549
- style={{
550
- flex: 1,
551
- borderRadius: 0,
552
- borderTopRightRadius: 3,
553
- borderBottomRightRadius: 3,
554
- cursor: 'pointer',
555
- }}
556
- />
557
- </Flex>
558
-
559
- </Flex>
560
- </Card>
561
-
562
- {showHistory && (
563
- <DeployHistory
564
- target={target}
565
- token={token}
566
- onClose={() => setShowHistory(false)}
567
- />
568
- )}
569
- </>
570
- )
571
- }