@postgres.ai/shared 3.5.1-pr-1027.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.
Files changed (202) hide show
  1. package/.gitlab-ci.yml +60 -0
  2. package/components/AlertSnackbar/index.tsx +23 -0
  3. package/components/AlertSnackbar/useAlertSnackbar.tsx +65 -0
  4. package/components/Button/index.tsx +79 -0
  5. package/components/Button2/index.tsx +43 -0
  6. package/components/Button2/styles.module.scss +82 -0
  7. package/components/DestroyCloneModal/index.tsx +56 -0
  8. package/components/DestroyCloneRestrictionModal/index.tsx +50 -0
  9. package/components/ErrorStub/index.tsx +83 -0
  10. package/components/FormattedText/index.tsx +44 -0
  11. package/components/FormattedText/styles.module.scss +34 -0
  12. package/components/GatewayLink/index.tsx +33 -0
  13. package/components/HorizontalScrollContainer/index.tsx +131 -0
  14. package/components/HorizontalScrollContainer/types.ts +12 -0
  15. package/components/HorizontalScrollContainer/utils.ts +16 -0
  16. package/components/ImportantText/index.tsx +29 -0
  17. package/components/Link2/index.tsx +31 -0
  18. package/components/Link2/styles.module.scss +12 -0
  19. package/components/MenuButton/index.tsx +80 -0
  20. package/components/MenuButton/styles.module.scss +42 -0
  21. package/components/Modal/index.tsx +93 -0
  22. package/components/PageSpinner/index.tsx +18 -0
  23. package/components/PageSpinner/styles.module.scss +13 -0
  24. package/components/ResetCloneModal/index.tsx +154 -0
  25. package/components/SectionTitle/index.tsx +74 -0
  26. package/components/Select/index.tsx +42 -0
  27. package/components/SimpleModalControls/index.tsx +56 -0
  28. package/components/Spinner/icon.tsx +29 -0
  29. package/components/Spinner/index.tsx +16 -0
  30. package/components/Spinner/styles.module.scss +33 -0
  31. package/components/Status/index.tsx +61 -0
  32. package/components/Status/styles.module.scss +45 -0
  33. package/components/StubContainer/index.tsx +41 -0
  34. package/components/StubSpinner/index.tsx +49 -0
  35. package/components/StubSpinnerFlex/index.tsx +20 -0
  36. package/components/StubSpinnerFlex/styles.module.scss +20 -0
  37. package/components/SyntaxHighlight/index.tsx +107 -0
  38. package/components/Table/RowMenu/index.tsx +111 -0
  39. package/components/Table/index.tsx +140 -0
  40. package/components/Text/index.tsx +28 -0
  41. package/components/TextField/index.tsx +117 -0
  42. package/components/Tooltip/index.tsx +52 -0
  43. package/config/index.ts +32 -0
  44. package/config/links.ts +6 -0
  45. package/craco.config.js +80 -0
  46. package/helpers/getEntropy.ts +232 -0
  47. package/helpers/localStorage.ts +15 -0
  48. package/helpers/request.ts +47 -0
  49. package/hooks/useWindowDimensions.ts +16 -0
  50. package/icons/ArrowDropDown/index.tsx +29 -0
  51. package/icons/Circle/index.tsx +27 -0
  52. package/icons/External/index.tsx +14 -0
  53. package/icons/Info/index.tsx +12 -0
  54. package/icons/Renewable/index.tsx +65 -0
  55. package/icons/Shield/index.tsx +33 -0
  56. package/icons/Warning/index.tsx +29 -0
  57. package/meta.json +1 -0
  58. package/package.json +55 -0
  59. package/pages/Clone/Status/index.tsx +73 -0
  60. package/pages/Clone/context.ts +22 -0
  61. package/pages/Clone/index.tsx +634 -0
  62. package/pages/Clone/stores/Main.ts +206 -0
  63. package/pages/Clone/useCreatedStores.ts +11 -0
  64. package/pages/Configuration/Header/index.tsx +84 -0
  65. package/pages/Configuration/InputWithTooltip/index.tsx +240 -0
  66. package/pages/Configuration/ResponseMessage/index.tsx +71 -0
  67. package/pages/Configuration/configOptions.ts +60 -0
  68. package/pages/Configuration/index.tsx +1184 -0
  69. package/pages/Configuration/styles.module.scss +122 -0
  70. package/pages/Configuration/tooltipText.tsx +157 -0
  71. package/pages/Configuration/useForm.ts +108 -0
  72. package/pages/Configuration/utils/index.ts +153 -0
  73. package/pages/CreateClone/index.tsx +311 -0
  74. package/pages/CreateClone/stores/Main.ts +107 -0
  75. package/pages/CreateClone/styles.module.scss +71 -0
  76. package/pages/CreateClone/useCreatedStores.ts +11 -0
  77. package/pages/CreateClone/useForm.ts +36 -0
  78. package/pages/Instance/Clones/Header/Item/index.tsx +15 -0
  79. package/pages/Instance/Clones/Header/Item/styles.module.scss +17 -0
  80. package/pages/Instance/Clones/Header/index.tsx +74 -0
  81. package/pages/Instance/Clones/Header/styles.module.scss +11 -0
  82. package/pages/Instance/Clones/index.tsx +135 -0
  83. package/pages/Instance/ClonesModal/index.tsx +71 -0
  84. package/pages/Instance/ClonesModal/utils.ts +21 -0
  85. package/pages/Instance/InactiveInstance/index.tsx +165 -0
  86. package/pages/Instance/InactiveInstance/utils.ts +9 -0
  87. package/pages/Instance/Info/Connection/ConnectModal/Content/index.tsx +176 -0
  88. package/pages/Instance/Info/Connection/ConnectModal/Content/utils.ts +24 -0
  89. package/pages/Instance/Info/Connection/ConnectModal/index.tsx +36 -0
  90. package/pages/Instance/Info/Connection/index.tsx +81 -0
  91. package/pages/Instance/Info/Details/index.tsx +20 -0
  92. package/pages/Instance/Info/Disks/Disk/ActionsMenu/index.tsx +100 -0
  93. package/pages/Instance/Info/Disks/Disk/Marker/index.tsx +26 -0
  94. package/pages/Instance/Info/Disks/Disk/ProgressBar/PointerIcon.tsx +20 -0
  95. package/pages/Instance/Info/Disks/Disk/ProgressBar/index.tsx +73 -0
  96. package/pages/Instance/Info/Disks/Disk/Status/index.tsx +75 -0
  97. package/pages/Instance/Info/Disks/Disk/index.tsx +168 -0
  98. package/pages/Instance/Info/Disks/index.tsx +65 -0
  99. package/pages/Instance/Info/Icons/index.tsx +39 -0
  100. package/pages/Instance/Info/Retrieval/RefreshFailedAlert/index.tsx +32 -0
  101. package/pages/Instance/Info/Retrieval/RefreshFailedAlert/styles.module.scss +33 -0
  102. package/pages/Instance/Info/Retrieval/RetrievalModal/index.tsx +49 -0
  103. package/pages/Instance/Info/Retrieval/RetrievalModal/styles.module.scss +6 -0
  104. package/pages/Instance/Info/Retrieval/RetrievalTable/index.tsx +53 -0
  105. package/pages/Instance/Info/Retrieval/RetrievalTable/styles.module.scss +29 -0
  106. package/pages/Instance/Info/Retrieval/index.tsx +95 -0
  107. package/pages/Instance/Info/Retrieval/utils.ts +10 -0
  108. package/pages/Instance/Info/Snapshots/Calendar/Day/index.tsx +125 -0
  109. package/pages/Instance/Info/Snapshots/Calendar/index.tsx +133 -0
  110. package/pages/Instance/Info/Snapshots/Calendar/utils.ts +74 -0
  111. package/pages/Instance/Info/Snapshots/TimeLine/Day/index.tsx +79 -0
  112. package/pages/Instance/Info/Snapshots/TimeLine/index.tsx +57 -0
  113. package/pages/Instance/Info/Snapshots/index.tsx +97 -0
  114. package/pages/Instance/Info/Snapshots/utils.ts +18 -0
  115. package/pages/Instance/Info/Status/InstanceResponseModal/index.tsx +32 -0
  116. package/pages/Instance/Info/Status/InstanceResponseModal/styles.module.scss +3 -0
  117. package/pages/Instance/Info/Status/index.tsx +85 -0
  118. package/pages/Instance/Info/Status/styles.module.scss +12 -0
  119. package/pages/Instance/Info/Status/utils.ts +24 -0
  120. package/pages/Instance/Info/components/Property/index.tsx +32 -0
  121. package/pages/Instance/Info/components/Property/styles.module.scss +21 -0
  122. package/pages/Instance/Info/components/Section/index.tsx +50 -0
  123. package/pages/Instance/Info/components/ValueStatus/index.tsx +51 -0
  124. package/pages/Instance/Info/index.tsx +129 -0
  125. package/pages/Instance/SnapshotsModal/index.tsx +169 -0
  126. package/pages/Instance/SnapshotsModal/utils.ts +17 -0
  127. package/pages/Instance/Tabs/index.tsx +98 -0
  128. package/pages/Instance/components/ClonesList/ConnectionModal/index.tsx +196 -0
  129. package/pages/Instance/components/ClonesList/MenuCell/index.tsx +98 -0
  130. package/pages/Instance/components/ClonesList/MenuCell/utils.ts +21 -0
  131. package/pages/Instance/components/ClonesList/index.tsx +189 -0
  132. package/pages/Instance/components/ClonesList/styles.module.scss +32 -0
  133. package/pages/Instance/components/ErrorStub/index.tsx +77 -0
  134. package/pages/Instance/components/ModalReloadButton/index.tsx +43 -0
  135. package/pages/Instance/components/Tags/Tag/index.tsx +60 -0
  136. package/pages/Instance/components/Tags/index.tsx +42 -0
  137. package/pages/Instance/context.ts +39 -0
  138. package/pages/Instance/index.tsx +235 -0
  139. package/pages/Instance/stores/ClonesModal.ts +35 -0
  140. package/pages/Instance/stores/Main.ts +335 -0
  141. package/pages/Instance/stores/SnapshotsModal.ts +35 -0
  142. package/pages/Instance/styles.scss +40 -0
  143. package/pages/Instance/useCreatedStores.ts +14 -0
  144. package/pages/Logs/Icons/PlusIcon.tsx +8 -0
  145. package/pages/Logs/constants/index.ts +7 -0
  146. package/pages/Logs/hooks/useWsScroll.tsx +44 -0
  147. package/pages/Logs/index.tsx +267 -0
  148. package/pages/Logs/utils/index.ts +20 -0
  149. package/pages/Logs/wsLogs.ts +110 -0
  150. package/pages/Logs/wsSnackbar.ts +27 -0
  151. package/postgres.ai-shared-3.5.0.tgz +0 -0
  152. package/react-app-env.d.ts +71 -0
  153. package/scripts/copy-assets.js +30 -0
  154. package/scripts/pack.js +70 -0
  155. package/stores/Snapshots.ts +54 -0
  156. package/styles/colors.ts +67 -0
  157. package/styles/global.scss +29 -0
  158. package/styles/icons.tsx +1917 -0
  159. package/styles/mixins.scss +30 -0
  160. package/styles/styles.ts +87 -0
  161. package/styles/theme.ts +53 -0
  162. package/styles/vars.scss +43 -0
  163. package/styles/vars.ts +40 -0
  164. package/tsconfig.build.json +37 -0
  165. package/tsconfig.json +30 -0
  166. package/types/api/endpoints/createClone.ts +10 -0
  167. package/types/api/endpoints/destroyClone.ts +7 -0
  168. package/types/api/endpoints/getClone.ts +6 -0
  169. package/types/api/endpoints/getConfig.ts +6 -0
  170. package/types/api/endpoints/getEngine.ts +13 -0
  171. package/types/api/endpoints/getFullConfig.ts +4 -0
  172. package/types/api/endpoints/getInstance.ts +6 -0
  173. package/types/api/endpoints/getInstanceRetrieval.ts +6 -0
  174. package/types/api/endpoints/getSeImages.ts +22 -0
  175. package/types/api/endpoints/getSnapshots.ts +6 -0
  176. package/types/api/endpoints/getWSToken.ts +6 -0
  177. package/types/api/endpoints/initWS.ts +1 -0
  178. package/types/api/endpoints/refreshInstance.ts +4 -0
  179. package/types/api/endpoints/resetClone.ts +8 -0
  180. package/types/api/endpoints/testDbSource.ts +48 -0
  181. package/types/api/endpoints/updateClone.ts +10 -0
  182. package/types/api/endpoints/updateConfig.ts +6 -0
  183. package/types/api/entities/clone.ts +42 -0
  184. package/types/api/entities/config.ts +114 -0
  185. package/types/api/entities/dbSource.ts +13 -0
  186. package/types/api/entities/instance.ts +67 -0
  187. package/types/api/entities/instanceRetrieval.ts +46 -0
  188. package/types/api/entities/instanceState.ts +102 -0
  189. package/types/api/entities/pool.ts +27 -0
  190. package/types/api/entities/snapshot.ts +18 -0
  191. package/types/api/entities/wsToken.ts +7 -0
  192. package/types/byte-size/index.d.ts +22 -0
  193. package/utils/api.ts +30 -0
  194. package/utils/clone.ts +31 -0
  195. package/utils/connection.ts +38 -0
  196. package/utils/date.ts +87 -0
  197. package/utils/instance.ts +10 -0
  198. package/utils/numbers.ts +11 -0
  199. package/utils/react.ts +10 -0
  200. package/utils/snapshot.ts +4 -0
  201. package/utils/strings.ts +11 -0
  202. package/utils/units.ts +23 -0
@@ -0,0 +1,311 @@
1
+ import cn from 'classnames'
2
+ import { useEffect } from 'react'
3
+ import { useHistory } from 'react-router-dom'
4
+ import { observer } from 'mobx-react-lite'
5
+ import { useTimer } from 'use-timer'
6
+ import { Paper, FormControlLabel, Checkbox } from '@material-ui/core'
7
+ import { Info as InfoIcon } from '@material-ui/icons'
8
+
9
+ import { StubSpinner } from '@postgres.ai/shared/components/StubSpinnerFlex'
10
+ import { TextField } from '@postgres.ai/shared/components/TextField'
11
+ import { Select } from '@postgres.ai/shared/components/Select'
12
+ import { Button } from '@postgres.ai/shared/components/Button'
13
+ import { Spinner } from '@postgres.ai/shared/components/Spinner'
14
+ import { ErrorStub } from '@postgres.ai/shared/components/ErrorStub'
15
+ import { compareSnapshotsDesc } from '@postgres.ai/shared/utils/snapshot'
16
+ import { round } from '@postgres.ai/shared/utils/numbers'
17
+ import { formatBytesIEC } from '@postgres.ai/shared/utils/units'
18
+ import { SectionTitle } from '@postgres.ai/shared/components/SectionTitle'
19
+ import {
20
+ MIN_ENTROPY,
21
+ getEntropy,
22
+ validatePassword,
23
+ } from '@postgres.ai/shared/helpers/getEntropy'
24
+
25
+ import { useCreatedStores, MainStoreApi } from './useCreatedStores'
26
+ import { useForm, FormValues } from './useForm'
27
+
28
+ import styles from './styles.module.scss'
29
+
30
+ type Host = {
31
+ instanceId: string
32
+ routes: {
33
+ clone: (cloneId: string) => string
34
+ }
35
+ api: MainStoreApi
36
+ elements: {
37
+ breadcrumbs: React.ReactNode
38
+ }
39
+ }
40
+
41
+ type Props = Host
42
+
43
+ export const CreateClone = observer((props: Props) => {
44
+ const history = useHistory()
45
+ const stores = useCreatedStores(props.api)
46
+ const cloneError = stores.main.cloneError
47
+ const timer = useTimer()
48
+
49
+ // Form.
50
+ const onSubmit = async (values: FormValues) => {
51
+ if (!values.dbPassword || getEntropy(values.dbPassword) < MIN_ENTROPY) {
52
+ formik.setFieldError(
53
+ 'dbPassword',
54
+ validatePassword(values.dbPassword, MIN_ENTROPY),
55
+ )
56
+ return
57
+ }
58
+
59
+ timer.start()
60
+
61
+ const isSuccess = await stores.main.createClone(values)
62
+
63
+ formik.setFieldError('dbPassword', '')
64
+
65
+ if (!isSuccess || cloneError) {
66
+ timer.pause()
67
+ timer.reset()
68
+ }
69
+ }
70
+
71
+ const formik = useForm(onSubmit)
72
+
73
+ // Initial loading data.
74
+ useEffect(() => {
75
+ stores.main.load(props.instanceId)
76
+ }, [])
77
+
78
+ // Redirect when clone is created and stable.
79
+ useEffect(() => {
80
+ if (!stores.main.clone) return
81
+ if (!stores.main.isCloneStable) return
82
+
83
+ history.push(props.routes.clone(stores.main.clone.id))
84
+ }, [stores.main.clone, stores.main.isCloneStable])
85
+
86
+ // Snapshots.
87
+ const sortedSnapshots = stores.main.snapshots.data
88
+ ?.slice()
89
+ .sort(compareSnapshotsDesc)
90
+
91
+ useEffect(() => {
92
+ const [firstSnapshot] = sortedSnapshots ?? []
93
+ if (!firstSnapshot) return
94
+
95
+ formik.setFieldValue('snapshotId', firstSnapshot.id)
96
+ }, [Boolean(sortedSnapshots)])
97
+
98
+ const headRendered = (
99
+ <>
100
+ {/* //TODO: make global reset styles. */}
101
+ <style>{'p { margin: 0; }'}</style>
102
+
103
+ {props.elements.breadcrumbs}
104
+ <SectionTitle
105
+ className={styles.title}
106
+ tag="h1"
107
+ level={1}
108
+ text="Create clone"
109
+ />
110
+ </>
111
+ )
112
+
113
+ // Initial loading spinner.
114
+ if (!stores.main.instance || !stores.main.snapshots.data)
115
+ return (
116
+ <>
117
+ {headRendered}
118
+
119
+ <StubSpinner />
120
+ </>
121
+ )
122
+
123
+ // Instance getting error.
124
+ if (stores.main.instanceError)
125
+ return (
126
+ <>
127
+ {headRendered}
128
+
129
+ <ErrorStub message={stores.main.instanceError} />
130
+ </>
131
+ )
132
+
133
+ // Snapshots getting error.
134
+ if (stores.main.snapshots.error)
135
+ return <ErrorStub message={stores.main.snapshots.error} />
136
+
137
+ const isCloneUnstable = Boolean(
138
+ stores.main.clone && !stores.main.isCloneStable,
139
+ )
140
+ const isCreatingClone = formik.isSubmitting || isCloneUnstable
141
+
142
+ return (
143
+ <>
144
+ {headRendered}
145
+
146
+ <div className={styles.form}>
147
+ {stores.main.cloneError && (
148
+ <div className={styles.section}>
149
+ <ErrorStub message={stores.main.cloneError} />
150
+ </div>
151
+ )}
152
+
153
+ <div className={styles.section}>
154
+ <TextField
155
+ fullWidth
156
+ label="Clone ID"
157
+ value={formik.values.cloneId}
158
+ onChange={(e) => formik.setFieldValue('cloneId', e.target.value)}
159
+ error={Boolean(formik.errors.cloneId)}
160
+ disabled={isCreatingClone}
161
+ />
162
+
163
+ <Select
164
+ fullWidth
165
+ label="Data state time *"
166
+ value={formik.values.snapshotId}
167
+ disabled={!sortedSnapshots || isCreatingClone}
168
+ onChange={(e) => formik.setFieldValue('snapshotId', e.target.value)}
169
+ error={Boolean(formik.errors.snapshotId)}
170
+ items={
171
+ sortedSnapshots?.map((snapshot, i) => {
172
+ const isLatest = i === 0
173
+ return {
174
+ value: snapshot.id,
175
+ children: (
176
+ <>
177
+ {snapshot.dataStateAt}
178
+ {isLatest && (
179
+ <span className={styles.snapshotTag}>Latest</span>
180
+ )}
181
+ </>
182
+ ),
183
+ }
184
+ }) ?? []
185
+ }
186
+ />
187
+
188
+ <p className={styles.remark}>
189
+ By default latest snapshot of database is used. You can select&nbsp;
190
+ different snapshots if earlier database state is needed
191
+ </p>
192
+ </div>
193
+
194
+ <div className={styles.section}>
195
+ <h2 className={styles.title}>Database credentials *</h2>
196
+
197
+ <p className={styles.text}>
198
+ Set custom credentials for the new clone. Save the password in
199
+ reliable place, it can’t be read later.
200
+ </p>
201
+
202
+ <TextField
203
+ fullWidth
204
+ label="Database username *"
205
+ value={formik.values.dbUser}
206
+ onChange={(e) => formik.setFieldValue('dbUser', e.target.value)}
207
+ error={Boolean(formik.errors.dbUser)}
208
+ disabled={isCreatingClone}
209
+ />
210
+
211
+ <TextField
212
+ fullWidth
213
+ label="Database password *"
214
+ type="password"
215
+ value={formik.values.dbPassword}
216
+ onChange={(e) => {
217
+ formik.setFieldValue('dbPassword', e.target.value)
218
+ if (formik.errors.dbPassword) {
219
+ formik.setFieldError('dbPassword', '')
220
+ }
221
+ }}
222
+ error={Boolean(formik.errors.dbPassword)}
223
+ disabled={isCreatingClone}
224
+ />
225
+ <p
226
+ className={cn(
227
+ formik.errors.dbPassword && styles.error,
228
+ styles.remark,
229
+ )}
230
+ >
231
+ {formik.errors.dbPassword}
232
+ </p>
233
+ </div>
234
+
235
+ <div className={styles.section}>
236
+ <Paper className={styles.summary}>
237
+ <InfoIcon className={styles.summaryIcon} />
238
+
239
+ <div className={styles.params}>
240
+ <p className={styles.param}>
241
+ <span>Data size:</span>
242
+ <strong>
243
+ {stores.main.instance.state?.dataSize
244
+ ? formatBytesIEC(stores.main.instance.state.dataSize)
245
+ : '-'}
246
+ </strong>
247
+ </p>
248
+
249
+ <p className={styles.param}>
250
+ <span>Expected cloning time:</span>
251
+ <strong>
252
+ {round(
253
+ stores.main.instance.state?.cloning
254
+ .expectedCloningTime as number,
255
+ 2,
256
+ )}{' '}
257
+ s
258
+ </strong>
259
+ </p>
260
+ </div>
261
+ </Paper>
262
+ </div>
263
+
264
+ <div className={styles.section}>
265
+ <FormControlLabel
266
+ label="Enable deletion protection"
267
+ control={
268
+ <Checkbox
269
+ checked={formik.values.isProtected}
270
+ onChange={(e) =>
271
+ formik.setFieldValue('isProtected', e.target.checked)
272
+ }
273
+ name="protected"
274
+ disabled={isCreatingClone}
275
+ />
276
+ }
277
+ />
278
+
279
+ <p className={styles.remark}>
280
+ When enabled no one can delete this clone and automated deletion is
281
+ also disabled.
282
+ <br />
283
+ Please be careful: abandoned clones with this checkbox enabled may
284
+ cause out-of-disk-space events. Check disk space on daily basis and
285
+ delete this clone once the work is done.
286
+ </p>
287
+ </div>
288
+
289
+ <div className={styles.section}>
290
+ <div className={styles.controls}>
291
+ <Button
292
+ onClick={formik.submitForm}
293
+ variant="primary"
294
+ size="medium"
295
+ isDisabled={isCreatingClone}
296
+ >
297
+ Create clone
298
+ {isCreatingClone && (
299
+ <Spinner size="sm" className={styles.spinner} />
300
+ )}
301
+ </Button>
302
+
303
+ {isCreatingClone && (
304
+ <p className={styles.elapsedTime}>Elapsed time: {timer.time} s</p>
305
+ )}
306
+ </div>
307
+ </div>
308
+ </div>
309
+ </>
310
+ )
311
+ })
@@ -0,0 +1,107 @@
1
+ import { makeAutoObservable } from 'mobx'
2
+
3
+ import { Instance } from '@postgres.ai/shared/types/api/entities/instance'
4
+ import { Clone } from '@postgres.ai/shared/types/api/entities/clone'
5
+ import { GetInstance } from '@postgres.ai/shared/types/api/endpoints/getInstance'
6
+ import { CreateClone } from '@postgres.ai/shared/types/api/endpoints/createClone'
7
+ import { GetClone } from '@postgres.ai/shared/types/api/endpoints/getClone'
8
+ import {
9
+ SnapshotsStore,
10
+ SnapshotsApi,
11
+ } from '@postgres.ai/shared/stores/Snapshots'
12
+ import { getTextFromUnknownApiError } from '@postgres.ai/shared/utils/api'
13
+ import { checkIsCloneStable } from '@postgres.ai/shared/utils/clone'
14
+
15
+ import { FormValues } from '../useForm'
16
+
17
+ const UNSTABLE_CLONE_UPDATE_TIMEOUT = 1000
18
+
19
+ export type MainStoreApi = SnapshotsApi & {
20
+ getInstance: GetInstance
21
+ createClone: CreateClone
22
+ getClone: GetClone
23
+ }
24
+
25
+ export class MainStore {
26
+ instance: Instance | null = null
27
+ instanceError: string | null = null
28
+
29
+ clone: Clone | null = null
30
+ cloneError: string | null = null
31
+
32
+ private cloneUpdateTimeout?: number
33
+
34
+ private readonly api: MainStoreApi
35
+
36
+ readonly snapshots: SnapshotsStore
37
+
38
+ constructor(api: MainStoreApi) {
39
+ makeAutoObservable(this)
40
+
41
+ this.api = api
42
+ this.snapshots = new SnapshotsStore(api)
43
+ }
44
+
45
+ get isCloneStable() {
46
+ if (!this.clone) return
47
+ return checkIsCloneStable(this.clone)
48
+ }
49
+
50
+ load = async (instanceId: string) => {
51
+ const [instance, isLoadedSnapshots] = await Promise.all([
52
+ this.api.getInstance({ instanceId }),
53
+ this.snapshots.load(instanceId),
54
+ ])
55
+
56
+ if (instance.response) this.instance = instance.response
57
+ if (instance.error)
58
+ this.instanceError = await getTextFromUnknownApiError(instance.error)
59
+
60
+ return Boolean(instance.response) && isLoadedSnapshots
61
+ }
62
+
63
+ createClone = async (data: FormValues) => {
64
+ if (!this.instance) return false
65
+
66
+ const { response, error } = await this.api.createClone({
67
+ ...data,
68
+ instanceId: this.instance.id,
69
+ })
70
+
71
+ if (response) {
72
+ this.clone = response
73
+
74
+ this.updateCloneUntilStable({
75
+ instanceId: this.instance.id,
76
+ cloneId: this.clone.id,
77
+ })
78
+ }
79
+
80
+ if (error)
81
+ this.cloneError = await error.json().then((err) => err?.message || err)
82
+
83
+ return Boolean(response)
84
+ }
85
+
86
+ private updateCloneUntilStable = async (args: {
87
+ instanceId: string
88
+ cloneId: string
89
+ }) => {
90
+ window.clearTimeout(this.cloneUpdateTimeout)
91
+
92
+ const { response, error } = await this.api.getClone(args)
93
+
94
+ if (response) {
95
+ this.clone = response
96
+
97
+ if (!this.isCloneStable)
98
+ this.cloneUpdateTimeout = window.setTimeout(
99
+ () => this.updateCloneUntilStable(args),
100
+ UNSTABLE_CLONE_UPDATE_TIMEOUT,
101
+ )
102
+ }
103
+
104
+ if (error)
105
+ this.cloneError = await error.json().then((err) => err?.message || err)
106
+ }
107
+ }
@@ -0,0 +1,71 @@
1
+ .title {
2
+ margin-top: 16px;
3
+ }
4
+
5
+ .form {
6
+ max-width: 425px;
7
+ margin-top: 8px;
8
+ }
9
+
10
+ .section {
11
+ & + .section {
12
+ margin-top: 32px;
13
+ }
14
+ }
15
+
16
+ .title {
17
+ font-size: inherit;
18
+ font-weight: 700;
19
+ }
20
+
21
+ .text {
22
+ margin-top: 8px;
23
+ }
24
+
25
+ .remark {
26
+ font-size: 12px;
27
+ }
28
+
29
+ .error {
30
+ color: red;
31
+ }
32
+
33
+ .snapshotTag {
34
+ font-weight: 700;
35
+ margin-left: 4px;
36
+ }
37
+
38
+ .summary {
39
+ display: flex;
40
+ align-items: center;
41
+ background-color: rgba(#808080, 0.15) !important;
42
+ padding: 12px;
43
+ }
44
+
45
+ .summaryIcon {
46
+ width: 30px;
47
+ color: silver;
48
+ margin-right: 12px;
49
+ }
50
+
51
+ .params {
52
+ flex: 1 1 100%;
53
+ }
54
+
55
+ .param {
56
+ display: flex;
57
+ justify-content: space-between;
58
+ }
59
+
60
+ .controls {
61
+ display: flex;
62
+ align-items: center;
63
+ }
64
+
65
+ .spinner {
66
+ margin-left: 8px;
67
+ }
68
+
69
+ .elapsedTime {
70
+ margin-left: 24px;
71
+ }
@@ -0,0 +1,11 @@
1
+ import { useMemo } from 'react'
2
+
3
+ import { MainStore, MainStoreApi } from './stores/Main'
4
+
5
+ export const useCreatedStores = (api: MainStoreApi) => ({
6
+ main: useMemo(() => new MainStore(api), []),
7
+ })
8
+
9
+ export type Stores = ReturnType<typeof useCreatedStores>
10
+
11
+ export type { MainStoreApi }
@@ -0,0 +1,36 @@
1
+ import { useFormik } from 'formik'
2
+ import * as Yup from 'yup'
3
+
4
+ export type FormValues = {
5
+ cloneId: string
6
+ snapshotId: string
7
+ dbUser: string
8
+ dbPassword: string
9
+ isProtected: boolean
10
+ }
11
+
12
+ const Schema = Yup.object().shape({
13
+ cloneId: Yup.string(),
14
+ snapshotId: Yup.string().required('Date state time is required'),
15
+ dbUser: Yup.string().required('Database username is required'),
16
+ dbPassword: Yup.string().required('Database password is required'),
17
+ isProtected: Yup.boolean(),
18
+ })
19
+
20
+ export const useForm = (onSubmit: (values: FormValues) => void) => {
21
+ const formik = useFormik<FormValues>({
22
+ initialValues: {
23
+ cloneId: '',
24
+ snapshotId: '',
25
+ dbUser: '',
26
+ dbPassword: '',
27
+ isProtected: false,
28
+ },
29
+ validationSchema: Schema,
30
+ onSubmit,
31
+ validateOnBlur: false,
32
+ validateOnChange: false,
33
+ })
34
+
35
+ return formik
36
+ }
@@ -0,0 +1,15 @@
1
+ import styles from './styles.module.scss'
2
+
3
+ type Props = {
4
+ value: React.ReactNode
5
+ children: React.ReactNode
6
+ }
7
+
8
+ export const Item = (props: Props) => {
9
+ return (
10
+ <div className={styles.root}>
11
+ <div className={styles.value}>{props.value}</div>
12
+ <div className={styles.description}>{props.children}</div>
13
+ </div>
14
+ )
15
+ }
@@ -0,0 +1,17 @@
1
+ @import '@postgres.ai/shared/styles/vars';
2
+
3
+ .root {
4
+ font-size: 12px;
5
+ text-align: center;
6
+ color: $color-gray-semi-dark;
7
+ }
8
+
9
+ .value {
10
+ font-weight: bold;
11
+ font-size: 14px;
12
+ color: $color-black;
13
+ }
14
+
15
+ .description {
16
+ margin-top: 4px;
17
+ }
@@ -0,0 +1,74 @@
1
+ /*--------------------------------------------------------------------------
2
+ * Copyright (c) 2019-2021, Postgres.ai, Nikolay Samokhvalov nik@postgres.ai
3
+ * All Rights Reserved. Proprietary and confidential.
4
+ * Unauthorized copying of this file, via any medium is strictly prohibited
5
+ *--------------------------------------------------------------------------
6
+ */
7
+
8
+ import { formatBytesIEC } from '@postgres.ai/shared/utils/units'
9
+
10
+ import { Item } from './Item'
11
+
12
+ import styles from './styles.module.scss'
13
+
14
+ type Props = {
15
+ expectedCloningTimeS: number
16
+ logicalSize: number | null
17
+ clonesCount: number
18
+ clonesCountLastMonth?: number
19
+ }
20
+
21
+ export const Header = (props: Props) => {
22
+ const {
23
+ expectedCloningTimeS,
24
+ logicalSize,
25
+ clonesCount,
26
+ clonesCountLastMonth,
27
+ } = props
28
+
29
+ return (
30
+ <div className={styles.root}>
31
+ <Item value={expectedCloningTimeS ? `${expectedCloningTimeS} s` : '-'}>
32
+ average
33
+ <br />
34
+ cloning time
35
+ </Item>
36
+
37
+ <Item
38
+ value={
39
+ logicalSize ? formatBytesIEC(logicalSize, { precision: 2 }) : '-'
40
+ }
41
+ >
42
+ logical
43
+ <br />
44
+ data size
45
+ </Item>
46
+
47
+ <Item value={clonesCount}>
48
+ clones
49
+ <br />
50
+ now
51
+ </Item>
52
+
53
+ <Item
54
+ value={
55
+ logicalSize
56
+ ? formatBytesIEC(logicalSize * clonesCount, { precision: 2 })
57
+ : '-'
58
+ }
59
+ >
60
+ total
61
+ <br />
62
+ size of clones
63
+ </Item>
64
+
65
+ {clonesCountLastMonth && (
66
+ <Item value={clonesCountLastMonth}>
67
+ clones
68
+ <br />
69
+ in last month
70
+ </Item>
71
+ )}
72
+ </div>
73
+ )
74
+ }
@@ -0,0 +1,11 @@
1
+ @import '@postgres.ai/shared/styles/mixins';
2
+
3
+ .root {
4
+ display: flex;
5
+ justify-content: space-between;
6
+ padding: 20px;
7
+
8
+ @include sm {
9
+ padding: 20px 0;
10
+ }
11
+ }