sanity-plugin-dashboard-widget-vercel 3.1.2 → 3.1.3
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/index.js +364 -328
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +365 -330
- package/lib/index.mjs.map +1 -1
- package/package.json +18 -19
- package/src/app.tsx +22 -30
- package/src/components/DeployButton/index.tsx +8 -7
- package/src/components/DialogForm/index.tsx +78 -96
- package/src/machines/deploy.ts +94 -91
- package/src/machines/deploymentTargetList.ts +113 -90
- package/src/machines/dialog.ts +44 -44
- package/src/machines/form.ts +136 -91
- package/src/machines/refresh.ts +22 -18
- package/src/utils/fetcher.ts +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {UploadIcon} from '@sanity/icons'
|
|
2
2
|
import {Box, Button, useToast} from '@sanity/ui'
|
|
3
3
|
import {useMachine} from '@xstate/react'
|
|
4
|
-
import React, {useEffect
|
|
4
|
+
import React, {useEffect} from 'react'
|
|
5
5
|
|
|
6
6
|
import {WIDGET_NAME} from '../../constants'
|
|
7
|
-
import deployMachine from '../../machines/deploy'
|
|
7
|
+
import {deployMachine} from '../../machines/deploy'
|
|
8
8
|
import StateDebug from '../StateDebug'
|
|
9
9
|
|
|
10
10
|
type Props = {
|
|
@@ -16,9 +16,9 @@ type Props = {
|
|
|
16
16
|
const DeployButton = (props: Props) => {
|
|
17
17
|
const {deployHook, onDeploySuccess, targetName} = props
|
|
18
18
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const [deployState, deployStateTransition, deployStateInterpreter] = useMachine(deployMachine, {
|
|
20
|
+
input: {deployHook},
|
|
21
|
+
})
|
|
22
22
|
|
|
23
23
|
const toast = useToast()
|
|
24
24
|
|
|
@@ -54,18 +54,19 @@ const DeployButton = (props: Props) => {
|
|
|
54
54
|
}, [isError, isSuccess, toast, targetName, deployState.context.error])
|
|
55
55
|
|
|
56
56
|
useEffect(() => {
|
|
57
|
-
deployStateInterpreter.
|
|
57
|
+
const subscription = deployStateInterpreter.subscribe((state) => {
|
|
58
58
|
if (state.value === 'success') {
|
|
59
59
|
if (onDeploySuccess) {
|
|
60
60
|
onDeploySuccess()
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
})
|
|
64
|
+
|
|
65
|
+
return () => subscription.unsubscribe()
|
|
64
66
|
}, [deployStateInterpreter, onDeploySuccess])
|
|
65
67
|
|
|
66
68
|
return (
|
|
67
69
|
<Box padding={3} style={{position: 'relative'}}>
|
|
68
|
-
{/* xstate debug */}
|
|
69
70
|
<StateDebug name="Deploy" state={deployState} />
|
|
70
71
|
|
|
71
72
|
<Button
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import {yupResolver} from '@hookform/resolvers/yup'
|
|
2
|
-
import {Box, Button, Dialog, Flex, Stack} from '@sanity/ui'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import React, {FC} from 'react'
|
|
2
|
+
import {Box, Button, Dialog, Flex, Stack, useToast} from '@sanity/ui'
|
|
3
|
+
import {useActor} from '@xstate/react'
|
|
4
|
+
import React, {FC, useEffect} from 'react'
|
|
6
5
|
import {useForm} from 'react-hook-form'
|
|
7
6
|
import * as yup from 'yup'
|
|
8
7
|
|
|
9
|
-
import {
|
|
10
|
-
import formMachine from '../../machines/form'
|
|
8
|
+
import {Z_INDEX_DIALOG} from '../../constants'
|
|
9
|
+
import {formMachine} from '../../machines/form'
|
|
11
10
|
import sanitizeFormData from '../../utils/sanitizeFormData'
|
|
12
11
|
import FormFieldInputText from '../FormFieldInputText'
|
|
13
12
|
import {Sanity} from '../../types'
|
|
14
13
|
import {useSanityClient} from '../../client'
|
|
14
|
+
import {toPromise} from 'xstate'
|
|
15
15
|
|
|
16
16
|
type Props = {
|
|
17
17
|
deploymentTarget?: Sanity.DeploymentTarget
|
|
@@ -42,66 +42,13 @@ const formSchema = yup.object().shape({
|
|
|
42
42
|
const DialogForm: FC<Props> = (props: Props) => {
|
|
43
43
|
const {deploymentTarget, onClose, onCreate, onDelete, onUpdate} = props
|
|
44
44
|
const client = useSanityClient()
|
|
45
|
+
const toast = useToast()
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
services: {
|
|
49
|
-
formSubmittedService: async () => {
|
|
50
|
-
onClose()
|
|
51
|
-
},
|
|
52
|
-
// TODO: refactor
|
|
53
|
-
createDocumentService: async (_context, event: any) => {
|
|
54
|
-
let document
|
|
55
|
-
try {
|
|
56
|
-
document = await client.create({
|
|
57
|
-
_id: `vercel.${uuid()}`,
|
|
58
|
-
_type: DEPLOYMENT_TARGET_DOCUMENT_TYPE,
|
|
59
|
-
...event.formData,
|
|
60
|
-
})
|
|
61
|
-
if (onCreate) {
|
|
62
|
-
onCreate(document as Sanity.DeploymentTarget)
|
|
63
|
-
}
|
|
64
|
-
return Promise.resolve()
|
|
65
|
-
} catch (e) {
|
|
66
|
-
return Promise.reject(e)
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
// TODO: refactor
|
|
70
|
-
deleteDocumentService: async () => {
|
|
71
|
-
if (deploymentTarget) {
|
|
72
|
-
try {
|
|
73
|
-
await client.delete(deploymentTarget._id)
|
|
74
|
-
if (onDelete) {
|
|
75
|
-
onDelete(deploymentTarget._id)
|
|
76
|
-
}
|
|
77
|
-
return Promise.resolve()
|
|
78
|
-
} catch (e) {
|
|
79
|
-
return Promise.reject(e)
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return Promise.resolve()
|
|
83
|
-
},
|
|
84
|
-
// TODO: refactor
|
|
85
|
-
updateDocumentService: async (_context, event: any) => {
|
|
86
|
-
let document
|
|
87
|
-
if (deploymentTarget) {
|
|
88
|
-
try {
|
|
89
|
-
document = await client.patch(deploymentTarget._id).set(event.formData).commit()
|
|
90
|
-
if (onUpdate) {
|
|
91
|
-
onUpdate(document as Sanity.DeploymentTarget)
|
|
92
|
-
}
|
|
93
|
-
return Promise.resolve()
|
|
94
|
-
} catch (e) {
|
|
95
|
-
return Promise.reject(e)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return Promise.resolve()
|
|
99
|
-
},
|
|
100
|
-
},
|
|
47
|
+
const [formState, formStateTransition, formStateActorRef] = useActor(formMachine, {
|
|
48
|
+
input: {client},
|
|
101
49
|
})
|
|
102
50
|
|
|
103
|
-
const formUpdating =
|
|
104
|
-
formState.matches('creating') || formState.matches('deleting') || formState.matches('updating')
|
|
51
|
+
const formUpdating = formState.hasTag('busy')
|
|
105
52
|
|
|
106
53
|
// react-hook-form v7
|
|
107
54
|
const {
|
|
@@ -122,49 +69,84 @@ const DialogForm: FC<Props> = (props: Props) => {
|
|
|
122
69
|
resolver: yupResolver(formSchema),
|
|
123
70
|
})
|
|
124
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Handle errors and reaching the done state
|
|
74
|
+
*/
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (formState.matches('error')) {
|
|
77
|
+
toast.push({
|
|
78
|
+
status: 'error',
|
|
79
|
+
title: formState.context.message || 'An error occurred',
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* If the machine is done it means it reached updated, created, deleted or error state.
|
|
84
|
+
* We don't care which one, we just want to close the dialog
|
|
85
|
+
*/
|
|
86
|
+
if (formState.status === 'done') {
|
|
87
|
+
onClose()
|
|
88
|
+
}
|
|
89
|
+
}, [formState, onClose, toast])
|
|
90
|
+
|
|
125
91
|
// Callbacks
|
|
126
92
|
// - submit react-hook-form
|
|
127
93
|
const onSubmit = async (formData: FormData) => {
|
|
128
94
|
const sanitizedFormData = sanitizeFormData(formData)
|
|
129
|
-
|
|
130
|
-
formData: sanitizedFormData
|
|
131
|
-
}
|
|
95
|
+
if (deploymentTarget) {
|
|
96
|
+
formStateTransition({type: 'UPDATE', id: deploymentTarget._id, formData: sanitizedFormData})
|
|
97
|
+
} else {
|
|
98
|
+
formStateTransition({type: 'CREATE', formData: sanitizedFormData})
|
|
99
|
+
}
|
|
100
|
+
await toPromise(formStateActorRef)
|
|
101
|
+
const snapshot = formStateActorRef.getSnapshot()
|
|
102
|
+
const {document} = snapshot.context
|
|
103
|
+
if (!document) return
|
|
104
|
+
if (snapshot.matches('created')) {
|
|
105
|
+
onCreate?.(document)
|
|
106
|
+
} else if (snapshot.matches('updated')) {
|
|
107
|
+
onUpdate?.(document)
|
|
108
|
+
}
|
|
132
109
|
}
|
|
133
110
|
|
|
134
|
-
const handleDelete = () => {
|
|
135
|
-
|
|
111
|
+
const handleDelete = async () => {
|
|
112
|
+
const id = deploymentTarget!._id
|
|
113
|
+
formStateTransition({type: 'DELETE', id})
|
|
114
|
+
await toPromise(formStateActorRef)
|
|
115
|
+
if (formStateActorRef.getSnapshot().matches('deleted')) {
|
|
116
|
+
onDelete?.(id)
|
|
117
|
+
}
|
|
136
118
|
}
|
|
137
119
|
|
|
138
|
-
const Footer = () => (
|
|
139
|
-
<Box padding={3}>
|
|
140
|
-
<Flex justify={deploymentTarget ? 'space-between' : 'flex-end'}>
|
|
141
|
-
{/* Delete button */}
|
|
142
|
-
{deploymentTarget && (
|
|
143
|
-
<Button
|
|
144
|
-
disabled={formUpdating}
|
|
145
|
-
fontSize={1}
|
|
146
|
-
mode="bleed"
|
|
147
|
-
onClick={handleDelete}
|
|
148
|
-
text="Delete"
|
|
149
|
-
tone="critical"
|
|
150
|
-
/>
|
|
151
|
-
)}
|
|
152
|
-
|
|
153
|
-
{/* Submit button */}
|
|
154
|
-
<Button
|
|
155
|
-
disabled={formUpdating || !isDirty || !isValid}
|
|
156
|
-
fontSize={1}
|
|
157
|
-
onClick={handleSubmit(onSubmit)}
|
|
158
|
-
text={deploymentTarget ? 'Update and close' : 'Create'}
|
|
159
|
-
tone="primary"
|
|
160
|
-
/>
|
|
161
|
-
</Flex>
|
|
162
|
-
</Box>
|
|
163
|
-
)
|
|
164
|
-
|
|
165
120
|
return (
|
|
166
121
|
<Dialog
|
|
167
|
-
footer={
|
|
122
|
+
footer={
|
|
123
|
+
<Box padding={3}>
|
|
124
|
+
<Flex justify={deploymentTarget ? 'space-between' : 'flex-end'}>
|
|
125
|
+
{/* Delete button */}
|
|
126
|
+
{deploymentTarget && (
|
|
127
|
+
<Button
|
|
128
|
+
loading={formState.matches('deleting')}
|
|
129
|
+
disabled={formUpdating}
|
|
130
|
+
fontSize={1}
|
|
131
|
+
mode="bleed"
|
|
132
|
+
onClick={handleDelete}
|
|
133
|
+
text="Delete"
|
|
134
|
+
tone="critical"
|
|
135
|
+
/>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
{/* Submit button */}
|
|
139
|
+
<Button
|
|
140
|
+
loading={formState.matches('creating') || formState.matches('updating')}
|
|
141
|
+
disabled={!isDirty || !isValid}
|
|
142
|
+
fontSize={1}
|
|
143
|
+
onClick={handleSubmit(onSubmit)}
|
|
144
|
+
text={deploymentTarget ? 'Update and close' : 'Create'}
|
|
145
|
+
tone="primary"
|
|
146
|
+
/>
|
|
147
|
+
</Flex>
|
|
148
|
+
</Box>
|
|
149
|
+
}
|
|
168
150
|
header={`${deploymentTarget ? 'Edit' : 'Create'} deployment target`}
|
|
169
151
|
id="create"
|
|
170
152
|
onClose={onClose}
|
package/src/machines/deploy.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {assign, Machine} from 'xstate'
|
|
1
|
+
import {assign, setup, fromPromise} from 'xstate'
|
|
3
2
|
import {Vercel} from '../types'
|
|
4
3
|
|
|
5
4
|
type Context = {
|
|
5
|
+
deployHook: string
|
|
6
6
|
disabled: boolean
|
|
7
7
|
feedback?: string
|
|
8
8
|
label?: string
|
|
@@ -11,104 +11,107 @@ type Context = {
|
|
|
11
11
|
|
|
12
12
|
type Event = {type: 'DEPLOY'}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
idle: {}
|
|
17
|
-
deploying: {}
|
|
18
|
-
success: {}
|
|
19
|
-
error: {}
|
|
20
|
-
}
|
|
14
|
+
interface DeployActorInput {
|
|
15
|
+
deployHook: string
|
|
21
16
|
}
|
|
22
17
|
|
|
23
|
-
const deployMachine = (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
18
|
+
export const deployMachine = setup({
|
|
19
|
+
types: {
|
|
20
|
+
context: {} as Context,
|
|
21
|
+
events: {} as Event,
|
|
22
|
+
input: {} as DeployActorInput,
|
|
23
|
+
},
|
|
24
|
+
actors: {
|
|
25
|
+
deploy: fromPromise(async ({input, signal}: {input: DeployActorInput; signal: AbortSignal}) => {
|
|
26
|
+
try {
|
|
27
|
+
if (!input.deployHook) {
|
|
28
|
+
throw new Error('No deployHook URL defined')
|
|
29
|
+
}
|
|
30
|
+
const res = await fetch(input.deployHook, {method: 'POST', signal})
|
|
31
|
+
const data = await res.json()
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
const errorMessage = (data?.error as Vercel.Error).message || res.statusText
|
|
34
|
+
throw errorMessage
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (typeof err === 'string') {
|
|
38
|
+
throw err
|
|
39
|
+
}
|
|
40
|
+
console.error('Unable to deploy with error:', err)
|
|
41
|
+
throw new Error('Please check the developer console for more information')
|
|
42
|
+
}
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
}).createMachine({
|
|
46
|
+
/** @xstate-layout N4IgpgJg5mDOIC5QTABwDYHsCeA6AlhOmAMQAiAogAoAyA8gJoDaADALqKiqaz4Au+TADtOIAB6IATCxa4AnADYAHAHYArArUslAZjksNAGhDZEARk3yVmhSxVyHC-QBYAvq+MoMOXF6zZ8ISgSCGEwAiEAN0wAa3C-HwSAoIRA6IBjAEMBYVY2PNFuXhyRJHFENTMzXElFJSUzNTVnWx0FY1MESXVcZzUdM2k1OTUVBoV3TzR-X2mcQOCwACclzCXcDGyAMzWAW1nvPCSF1KjMLJK8grKi-kFS0AkESura5QamlpY2jvMB+QcDkqLH0wxYEw8ICSuFgAFd0uk4LByNR6Mx2IUeHdhKIns1ZN9BtYQVpRnJfl1JM55P1BnY1Ep9CpGpMoXM8MtVksUbRGNcuFiSriKs4CQNurYRgZ7BSGr1ASNupIzEo+kp3JChJgUPAyklMcV7sKEABadomRAmtQAhW2wE6VnQwjEA3Yh7lBDOSQUsxeqyaPoWYbWFSO9kHfwLV1CspPHSSHQ1ZyA1UKJzJlQ+iz+5oKewKWrfNyQ6FwhFI6NG2OINOSXDiypKAxtMyZi1dEE1Qk6L0FpTSUMl8OctaVnHVhC1+uDRvNhStin6XDaHQ9liSXTJiUa1xAA */
|
|
47
|
+
id: 'deploy',
|
|
48
|
+
initial: 'idle',
|
|
49
|
+
context: ({input}) => ({
|
|
50
|
+
disabled: false,
|
|
51
|
+
feedback: undefined,
|
|
52
|
+
label: undefined,
|
|
53
|
+
error: undefined,
|
|
54
|
+
deployHook: input.deployHook,
|
|
55
|
+
}),
|
|
56
|
+
states: {
|
|
57
|
+
idle: {
|
|
58
|
+
entry: assign({
|
|
59
|
+
feedback: () => undefined,
|
|
60
|
+
label: () => 'Deploy',
|
|
61
|
+
}),
|
|
62
|
+
on: {
|
|
63
|
+
DEPLOY: {
|
|
64
|
+
target: 'deploying',
|
|
65
|
+
},
|
|
34
66
|
},
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
67
|
+
},
|
|
68
|
+
deploying: {
|
|
69
|
+
entry: assign({
|
|
70
|
+
disabled: () => true,
|
|
71
|
+
label: () => 'Deploying',
|
|
72
|
+
}),
|
|
73
|
+
exit: assign({
|
|
74
|
+
disabled: () => false,
|
|
75
|
+
label: () => 'Deploy',
|
|
76
|
+
}),
|
|
77
|
+
invoke: {
|
|
78
|
+
src: 'deploy',
|
|
79
|
+
input: ({context}) => ({deployHook: context.deployHook}),
|
|
80
|
+
onDone: {
|
|
81
|
+
target: 'success',
|
|
44
82
|
},
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}),
|
|
54
|
-
invoke: {
|
|
55
|
-
onDone: {
|
|
56
|
-
target: 'success',
|
|
57
|
-
},
|
|
58
|
-
onError: {
|
|
59
|
-
target: 'error',
|
|
60
|
-
actions: assign({
|
|
61
|
-
error: (_context, event) => {
|
|
62
|
-
return event.data
|
|
63
|
-
},
|
|
64
|
-
}),
|
|
83
|
+
onError: {
|
|
84
|
+
target: 'error',
|
|
85
|
+
actions: assign({
|
|
86
|
+
error: ({event}) => {
|
|
87
|
+
if ('error' in event) {
|
|
88
|
+
return event.error as unknown as string
|
|
89
|
+
}
|
|
90
|
+
return 'Unknown error'
|
|
65
91
|
},
|
|
66
|
-
src: 'deploy',
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
success: {
|
|
70
|
-
entry: [assign({feedback: () => 'Succesfully started!'})],
|
|
71
|
-
exit: assign({
|
|
72
|
-
feedback: () => undefined,
|
|
73
92
|
}),
|
|
74
|
-
on: {
|
|
75
|
-
DEPLOY: 'deploying',
|
|
76
|
-
},
|
|
77
93
|
},
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
success: {
|
|
97
|
+
entry: assign({
|
|
98
|
+
feedback: () => 'Successfully started!',
|
|
99
|
+
}),
|
|
100
|
+
exit: assign({
|
|
101
|
+
feedback: () => undefined,
|
|
102
|
+
}),
|
|
103
|
+
on: {
|
|
104
|
+
DEPLOY: {
|
|
105
|
+
target: 'deploying',
|
|
82
106
|
},
|
|
83
107
|
},
|
|
84
108
|
},
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return new Promise(async (resolve, reject) => {
|
|
90
|
-
try {
|
|
91
|
-
if (!deployHook) {
|
|
92
|
-
return reject(new Error('No deployHook URL defined'))
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const res = await fetch(deployHook, {method: 'POST'})
|
|
96
|
-
const data = await res.json()
|
|
97
|
-
|
|
98
|
-
if (!res.ok) {
|
|
99
|
-
const errorMessage = (data?.error as Vercel.Error).message || res.statusText
|
|
100
|
-
return reject(errorMessage)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return resolve()
|
|
104
|
-
} catch (err) {
|
|
105
|
-
console.error('Unable to deploy with error:', err)
|
|
106
|
-
return reject(new Error('Please check the developer console for more information'))
|
|
107
|
-
}
|
|
108
|
-
})
|
|
109
|
+
error: {
|
|
110
|
+
on: {
|
|
111
|
+
DEPLOY: {
|
|
112
|
+
target: 'deploying',
|
|
109
113
|
},
|
|
110
114
|
},
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
export default deployMachine
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
})
|