@shipfox/client-secrets 0.2.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.
- package/.storybook/main.ts +1 -0
- package/.storybook/preview.css +4 -0
- package/.storybook/preview.tsx +56 -0
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +102 -0
- package/LICENSE +21 -0
- package/dist/components/copy-key.d.ts +8 -0
- package/dist/components/copy-key.d.ts.map +1 -0
- package/dist/components/copy-key.js +21 -0
- package/dist/components/copy-key.js.map +1 -0
- package/dist/components/delete-entry-dialog.d.ts +9 -0
- package/dist/components/delete-entry-dialog.d.ts.map +1 -0
- package/dist/components/delete-entry-dialog.js +66 -0
- package/dist/components/delete-entry-dialog.js.map +1 -0
- package/dist/components/form-errors.d.ts +17 -0
- package/dist/components/form-errors.d.ts.map +1 -0
- package/dist/components/form-errors.js +81 -0
- package/dist/components/form-errors.js.map +1 -0
- package/dist/components/form-shell.d.ts +14 -0
- package/dist/components/form-shell.d.ts.map +1 -0
- package/dist/components/form-shell.js +28 -0
- package/dist/components/form-shell.js.map +1 -0
- package/dist/components/secret-form.d.ts +10 -0
- package/dist/components/secret-form.d.ts.map +1 -0
- package/dist/components/secret-form.js +176 -0
- package/dist/components/secret-form.js.map +1 -0
- package/dist/components/secret-form.stories.d.ts +9 -0
- package/dist/components/secret-form.stories.d.ts.map +1 -0
- package/dist/components/secret-form.stories.js +60 -0
- package/dist/components/secret-form.stories.js.map +1 -0
- package/dist/components/store-key.d.ts +20 -0
- package/dist/components/store-key.d.ts.map +1 -0
- package/dist/components/store-key.js +25 -0
- package/dist/components/store-key.js.map +1 -0
- package/dist/components/store-section-shell.d.ts +10 -0
- package/dist/components/store-section-shell.d.ts.map +1 -0
- package/dist/components/store-section-shell.js +40 -0
- package/dist/components/store-section-shell.js.map +1 -0
- package/dist/components/variable-form.d.ts +12 -0
- package/dist/components/variable-form.d.ts.map +1 -0
- package/dist/components/variable-form.js +186 -0
- package/dist/components/variable-form.js.map +1 -0
- package/dist/components/variable-form.stories.d.ts +9 -0
- package/dist/components/variable-form.stories.d.ts.map +1 -0
- package/dist/components/variable-form.stories.js +61 -0
- package/dist/components/variable-form.stories.js.map +1 -0
- package/dist/components/workspace-secrets-section.d.ts +4 -0
- package/dist/components/workspace-secrets-section.d.ts.map +1 -0
- package/dist/components/workspace-secrets-section.js +251 -0
- package/dist/components/workspace-secrets-section.js.map +1 -0
- package/dist/components/workspace-secrets-section.stories.d.ts +13 -0
- package/dist/components/workspace-secrets-section.stories.d.ts.map +1 -0
- package/dist/components/workspace-secrets-section.stories.js +140 -0
- package/dist/components/workspace-secrets-section.stories.js.map +1 -0
- package/dist/components/workspace-variables-section.d.ts +4 -0
- package/dist/components/workspace-variables-section.d.ts.map +1 -0
- package/dist/components/workspace-variables-section.js +255 -0
- package/dist/components/workspace-variables-section.js.map +1 -0
- package/dist/components/workspace-variables-section.stories.d.ts +11 -0
- package/dist/components/workspace-variables-section.stories.d.ts.map +1 -0
- package/dist/components/workspace-variables-section.stories.js +127 -0
- package/dist/components/workspace-variables-section.stories.js.map +1 -0
- package/dist/hooks/api/create-store-api.d.ts +56 -0
- package/dist/hooks/api/create-store-api.d.ts.map +1 -0
- package/dist/hooks/api/create-store-api.js +95 -0
- package/dist/hooks/api/create-store-api.js.map +1 -0
- package/dist/hooks/api/secrets.d.ts +61 -0
- package/dist/hooks/api/secrets.d.ts.map +1 -0
- package/dist/hooks/api/secrets.js +15 -0
- package/dist/hooks/api/secrets.js.map +1 -0
- package/dist/hooks/api/variables.d.ts +88 -0
- package/dist/hooks/api/variables.d.ts.map +1 -0
- package/dist/hooks/api/variables.js +52 -0
- package/dist/hooks/api/variables.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/package.json +89 -0
- package/src/components/copy-key.test.ts +44 -0
- package/src/components/copy-key.ts +21 -0
- package/src/components/delete-entry-dialog.tsx +57 -0
- package/src/components/form-errors.test.ts +67 -0
- package/src/components/form-errors.ts +59 -0
- package/src/components/form-shell.tsx +31 -0
- package/src/components/secret-form.stories.tsx +58 -0
- package/src/components/secret-form.test.tsx +37 -0
- package/src/components/secret-form.tsx +170 -0
- package/src/components/store-key.test.ts +53 -0
- package/src/components/store-key.ts +36 -0
- package/src/components/store-section-shell.tsx +32 -0
- package/src/components/variable-form.stories.tsx +58 -0
- package/src/components/variable-form.test.tsx +60 -0
- package/src/components/variable-form.tsx +182 -0
- package/src/components/workspace-secrets-section.stories.tsx +128 -0
- package/src/components/workspace-secrets-section.tsx +231 -0
- package/src/components/workspace-variables-section.stories.tsx +121 -0
- package/src/components/workspace-variables-section.tsx +238 -0
- package/src/hooks/api/create-store-api.ts +114 -0
- package/src/hooks/api/secrets.test.ts +157 -0
- package/src/hooks/api/secrets.ts +26 -0
- package/src/hooks/api/variables.ts +61 -0
- package/src/index.ts +5 -0
- package/test/fixtures/secrets.ts +59 -0
- package/test/setup.ts +3 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- package/vercel.json +8 -0
- package/vitest.config.ts +75 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { isShortSecretValue } from '@shipfox/api-secrets-dto';
|
|
3
|
+
import { Button, IconButton } from '@shipfox/react-ui/button';
|
|
4
|
+
import { Callout } from '@shipfox/react-ui/callout';
|
|
5
|
+
import { FormField, FormFieldInput, FormFieldTextarea, fieldError } from '@shipfox/react-ui/form-field';
|
|
6
|
+
import { Text } from '@shipfox/react-ui/typography';
|
|
7
|
+
import { useForm } from '@tanstack/react-form';
|
|
8
|
+
import { useState } from 'react';
|
|
9
|
+
import { usePutSecretMutation } from '#hooks/api/secrets.js';
|
|
10
|
+
import { secretsErrorToFormError } from './form-errors.js';
|
|
11
|
+
import { FormBody, FormFooter } from './form-shell.js';
|
|
12
|
+
import { STORE_KEY_HELP, validateNewStoreKey } from './store-key.js';
|
|
13
|
+
export const SECRET_FORM_ID = 'secret-form';
|
|
14
|
+
// Client-side mirror of the server default SECRETS_SHORT_VALUE_WARN_LENGTH. The
|
|
15
|
+
// advisory is best-effort UX, so drift from a self-hosted override is cosmetic.
|
|
16
|
+
const SHORT_VALUE_THRESHOLD = 12;
|
|
17
|
+
export function SecretForm({ workspaceId, mode, existingKey, reservedKeys = [], onSaved, onCancel }) {
|
|
18
|
+
const putSecret = usePutSecretMutation();
|
|
19
|
+
const [formError, setFormError] = useState();
|
|
20
|
+
const [showValue, setShowValue] = useState(false);
|
|
21
|
+
const form = useForm({
|
|
22
|
+
defaultValues: {
|
|
23
|
+
key: existingKey ?? '',
|
|
24
|
+
value: ''
|
|
25
|
+
},
|
|
26
|
+
onSubmit: async ({ value })=>{
|
|
27
|
+
setFormError(undefined);
|
|
28
|
+
try {
|
|
29
|
+
await putSecret.mutateAsync({
|
|
30
|
+
workspaceId,
|
|
31
|
+
key: value.key,
|
|
32
|
+
body: {
|
|
33
|
+
value: value.value
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
onSaved();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
const mapped = secretsErrorToFormError(error);
|
|
39
|
+
if (mapped.kind === 'field') {
|
|
40
|
+
form.setFieldMeta(mapped.field, (prev)=>({
|
|
41
|
+
...prev,
|
|
42
|
+
errorMap: {
|
|
43
|
+
...prev.errorMap,
|
|
44
|
+
onServer: mapped.message
|
|
45
|
+
}
|
|
46
|
+
}));
|
|
47
|
+
} else {
|
|
48
|
+
setFormError(mapped.message);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
54
|
+
children: [
|
|
55
|
+
/*#__PURE__*/ _jsxs(FormBody, {
|
|
56
|
+
children: [
|
|
57
|
+
/*#__PURE__*/ _jsxs("form", {
|
|
58
|
+
id: SECRET_FORM_ID,
|
|
59
|
+
className: "flex w-full flex-col gap-16",
|
|
60
|
+
noValidate: true,
|
|
61
|
+
onSubmit: (event)=>{
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
event.stopPropagation();
|
|
64
|
+
void form.handleSubmit();
|
|
65
|
+
},
|
|
66
|
+
children: [
|
|
67
|
+
/*#__PURE__*/ _jsx(form.Field, {
|
|
68
|
+
name: "key",
|
|
69
|
+
validators: {
|
|
70
|
+
onBlur: ({ value })=>validateNewStoreKey(value, {
|
|
71
|
+
mode,
|
|
72
|
+
reservedKeys,
|
|
73
|
+
kind: 'secret'
|
|
74
|
+
}),
|
|
75
|
+
onSubmit: ({ value })=>validateNewStoreKey(value, {
|
|
76
|
+
mode,
|
|
77
|
+
reservedKeys,
|
|
78
|
+
kind: 'secret'
|
|
79
|
+
})
|
|
80
|
+
},
|
|
81
|
+
children: (field)=>/*#__PURE__*/ _jsx(FormField, {
|
|
82
|
+
label: "Name",
|
|
83
|
+
id: "secret-key",
|
|
84
|
+
description: STORE_KEY_HELP,
|
|
85
|
+
error: fieldError(field),
|
|
86
|
+
children: /*#__PURE__*/ _jsx(FormFieldInput, {
|
|
87
|
+
className: "font-code",
|
|
88
|
+
autoComplete: "off",
|
|
89
|
+
spellCheck: false,
|
|
90
|
+
disabled: mode === 'edit',
|
|
91
|
+
placeholder: "MY_TOKEN",
|
|
92
|
+
value: field.state.value,
|
|
93
|
+
onChange: (event)=>field.handleChange(event.target.value.toUpperCase()),
|
|
94
|
+
onBlur: field.handleBlur
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
}),
|
|
98
|
+
/*#__PURE__*/ _jsx(form.Field, {
|
|
99
|
+
name: "value",
|
|
100
|
+
validators: {
|
|
101
|
+
onBlur: ({ value })=>value.length > 0 ? undefined : 'A value is required.',
|
|
102
|
+
onSubmit: ({ value })=>value.length > 0 ? undefined : 'A value is required.'
|
|
103
|
+
},
|
|
104
|
+
children: (field)=>/*#__PURE__*/ _jsxs(FormField, {
|
|
105
|
+
label: "Value",
|
|
106
|
+
id: "secret-value",
|
|
107
|
+
error: fieldError(field),
|
|
108
|
+
children: [
|
|
109
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
110
|
+
className: "relative",
|
|
111
|
+
children: [
|
|
112
|
+
/*#__PURE__*/ _jsx(FormFieldTextarea, {
|
|
113
|
+
className: showValue ? 'pr-32 font-code' : 'pr-32 font-code [-webkit-text-security:disc]',
|
|
114
|
+
autoComplete: "off",
|
|
115
|
+
spellCheck: false,
|
|
116
|
+
rows: 3,
|
|
117
|
+
placeholder: mode === 'edit' ? 'Enter a new value to replace the current one' : 'Paste the secret value',
|
|
118
|
+
value: field.state.value,
|
|
119
|
+
onChange: (event)=>field.handleChange(event.target.value),
|
|
120
|
+
onBlur: field.handleBlur
|
|
121
|
+
}),
|
|
122
|
+
/*#__PURE__*/ _jsx(IconButton, {
|
|
123
|
+
type: "button",
|
|
124
|
+
variant: "transparent",
|
|
125
|
+
size: "xs",
|
|
126
|
+
className: "absolute top-6 right-6",
|
|
127
|
+
icon: showValue ? 'eyeOffLine' : 'eyeLine',
|
|
128
|
+
"aria-label": showValue ? 'Hide value' : 'Show value',
|
|
129
|
+
onClick: ()=>setShowValue((prev)=>!prev)
|
|
130
|
+
})
|
|
131
|
+
]
|
|
132
|
+
}),
|
|
133
|
+
isShortSecretValue(field.state.value, SHORT_VALUE_THRESHOLD) ? /*#__PURE__*/ _jsx(Callout, {
|
|
134
|
+
role: "alert",
|
|
135
|
+
type: "warning",
|
|
136
|
+
children: /*#__PURE__*/ _jsx(Text, {
|
|
137
|
+
size: "sm",
|
|
138
|
+
children: "Very short secrets can match ordinary log text and redact unrelated output. If this value is not sensitive, store it as a Variable instead."
|
|
139
|
+
})
|
|
140
|
+
}) : null
|
|
141
|
+
]
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
]
|
|
145
|
+
}),
|
|
146
|
+
formError ? /*#__PURE__*/ _jsx(Callout, {
|
|
147
|
+
role: "alert",
|
|
148
|
+
type: "error",
|
|
149
|
+
children: /*#__PURE__*/ _jsx(Text, {
|
|
150
|
+
size: "sm",
|
|
151
|
+
children: formError
|
|
152
|
+
})
|
|
153
|
+
}) : null
|
|
154
|
+
]
|
|
155
|
+
}),
|
|
156
|
+
/*#__PURE__*/ _jsxs(FormFooter, {
|
|
157
|
+
children: [
|
|
158
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
159
|
+
variant: "secondary",
|
|
160
|
+
type: "button",
|
|
161
|
+
onClick: onCancel,
|
|
162
|
+
children: "Cancel"
|
|
163
|
+
}),
|
|
164
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
165
|
+
type: "submit",
|
|
166
|
+
form: SECRET_FORM_ID,
|
|
167
|
+
isLoading: putSecret.isPending,
|
|
168
|
+
children: mode === 'edit' ? 'Update secret' : 'Create secret'
|
|
169
|
+
})
|
|
170
|
+
]
|
|
171
|
+
})
|
|
172
|
+
]
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
//# sourceMappingURL=secret-form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/secret-form.tsx"],"sourcesContent":["import {isShortSecretValue} from '@shipfox/api-secrets-dto';\nimport {Button, IconButton} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {\n FormField,\n FormFieldInput,\n FormFieldTextarea,\n fieldError,\n} from '@shipfox/react-ui/form-field';\nimport {Text} from '@shipfox/react-ui/typography';\nimport {useForm} from '@tanstack/react-form';\nimport {useState} from 'react';\nimport {usePutSecretMutation} from '#hooks/api/secrets.js';\nimport {secretsErrorToFormError} from './form-errors.js';\nimport {FormBody, FormFooter} from './form-shell.js';\nimport {STORE_KEY_HELP, validateNewStoreKey} from './store-key.js';\n\nexport const SECRET_FORM_ID = 'secret-form';\n\n// Client-side mirror of the server default SECRETS_SHORT_VALUE_WARN_LENGTH. The\n// advisory is best-effort UX, so drift from a self-hosted override is cosmetic.\nconst SHORT_VALUE_THRESHOLD = 12;\n\nexport function SecretForm({\n workspaceId,\n mode,\n existingKey,\n reservedKeys = [],\n onSaved,\n onCancel,\n}: {\n workspaceId: string;\n mode: 'create' | 'edit';\n existingKey?: string | undefined;\n reservedKeys?: readonly string[] | undefined;\n onSaved: () => void;\n onCancel: () => void;\n}) {\n const putSecret = usePutSecretMutation();\n const [formError, setFormError] = useState<string | undefined>();\n const [showValue, setShowValue] = useState(false);\n\n const form = useForm({\n defaultValues: {key: existingKey ?? '', value: ''},\n onSubmit: async ({value}) => {\n setFormError(undefined);\n try {\n await putSecret.mutateAsync({workspaceId, key: value.key, body: {value: value.value}});\n onSaved();\n } catch (error) {\n const mapped = secretsErrorToFormError(error);\n if (mapped.kind === 'field') {\n form.setFieldMeta(mapped.field, (prev) => ({\n ...prev,\n errorMap: {...prev.errorMap, onServer: mapped.message},\n }));\n } else {\n setFormError(mapped.message);\n }\n }\n },\n });\n\n return (\n <>\n <FormBody>\n <form\n id={SECRET_FORM_ID}\n className=\"flex w-full flex-col gap-16\"\n noValidate\n onSubmit={(event) => {\n event.preventDefault();\n event.stopPropagation();\n void form.handleSubmit();\n }}\n >\n <form.Field\n name=\"key\"\n validators={{\n onBlur: ({value}) => validateNewStoreKey(value, {mode, reservedKeys, kind: 'secret'}),\n onSubmit: ({value}) =>\n validateNewStoreKey(value, {mode, reservedKeys, kind: 'secret'}),\n }}\n >\n {(field) => (\n <FormField\n label=\"Name\"\n id=\"secret-key\"\n description={STORE_KEY_HELP}\n error={fieldError(field)}\n >\n <FormFieldInput\n className=\"font-code\"\n autoComplete=\"off\"\n spellCheck={false}\n disabled={mode === 'edit'}\n placeholder=\"MY_TOKEN\"\n value={field.state.value}\n onChange={(event) => field.handleChange(event.target.value.toUpperCase())}\n onBlur={field.handleBlur}\n />\n </FormField>\n )}\n </form.Field>\n\n <form.Field\n name=\"value\"\n validators={{\n onBlur: ({value}) => (value.length > 0 ? undefined : 'A value is required.'),\n onSubmit: ({value}) => (value.length > 0 ? undefined : 'A value is required.'),\n }}\n >\n {(field) => (\n <FormField label=\"Value\" id=\"secret-value\" error={fieldError(field)}>\n <div className=\"relative\">\n <FormFieldTextarea\n className={\n showValue ? 'pr-32 font-code' : 'pr-32 font-code [-webkit-text-security:disc]'\n }\n autoComplete=\"off\"\n spellCheck={false}\n rows={3}\n placeholder={\n mode === 'edit'\n ? 'Enter a new value to replace the current one'\n : 'Paste the secret value'\n }\n value={field.state.value}\n onChange={(event) => field.handleChange(event.target.value)}\n onBlur={field.handleBlur}\n />\n <IconButton\n type=\"button\"\n variant=\"transparent\"\n size=\"xs\"\n className=\"absolute top-6 right-6\"\n icon={showValue ? 'eyeOffLine' : 'eyeLine'}\n aria-label={showValue ? 'Hide value' : 'Show value'}\n onClick={() => setShowValue((prev) => !prev)}\n />\n </div>\n {isShortSecretValue(field.state.value, SHORT_VALUE_THRESHOLD) ? (\n <Callout role=\"alert\" type=\"warning\">\n <Text size=\"sm\">\n Very short secrets can match ordinary log text and redact unrelated output. If\n this value is not sensitive, store it as a Variable instead.\n </Text>\n </Callout>\n ) : null}\n </FormField>\n )}\n </form.Field>\n </form>\n {formError ? (\n <Callout role=\"alert\" type=\"error\">\n <Text size=\"sm\">{formError}</Text>\n </Callout>\n ) : null}\n </FormBody>\n <FormFooter>\n <Button variant=\"secondary\" type=\"button\" onClick={onCancel}>\n Cancel\n </Button>\n <Button type=\"submit\" form={SECRET_FORM_ID} isLoading={putSecret.isPending}>\n {mode === 'edit' ? 'Update secret' : 'Create secret'}\n </Button>\n </FormFooter>\n </>\n );\n}\n"],"names":["isShortSecretValue","Button","IconButton","Callout","FormField","FormFieldInput","FormFieldTextarea","fieldError","Text","useForm","useState","usePutSecretMutation","secretsErrorToFormError","FormBody","FormFooter","STORE_KEY_HELP","validateNewStoreKey","SECRET_FORM_ID","SHORT_VALUE_THRESHOLD","SecretForm","workspaceId","mode","existingKey","reservedKeys","onSaved","onCancel","putSecret","formError","setFormError","showValue","setShowValue","form","defaultValues","key","value","onSubmit","undefined","mutateAsync","body","error","mapped","kind","setFieldMeta","field","prev","errorMap","onServer","message","id","className","noValidate","event","preventDefault","stopPropagation","handleSubmit","Field","name","validators","onBlur","label","description","autoComplete","spellCheck","disabled","placeholder","state","onChange","handleChange","target","toUpperCase","handleBlur","length","div","rows","type","variant","size","icon","aria-label","onClick","role","isLoading","isPending"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { SecretForm } from './secret-form.js';
|
|
3
|
+
declare const meta: Meta<typeof SecretForm>;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<typeof SecretForm>;
|
|
6
|
+
export declare const Playground: Story;
|
|
7
|
+
export declare const Edit: Story;
|
|
8
|
+
export declare const ShortValueWarning: Story;
|
|
9
|
+
//# sourceMappingURL=secret-form.stories.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-form.stories.d.ts","sourceRoot":"","sources":["../../src/components/secret-form.stories.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,IAAI,EAAE,QAAQ,EAAC,MAAM,kBAAkB,CAAC;AAKrD,OAAO,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAmB5C,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,UAAU,CAgBjC,CAAC;AACF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,UAAU,CAAC,CAAC;AAEzC,eAAO,MAAM,UAAU,EAAE,KAAU,CAAC;AAEpC,eAAO,MAAM,IAAI,EAAE,KAElB,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,KAK/B,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Toaster } from '@shipfox/react-ui/toast';
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
4
|
+
import { useMemo } from 'react';
|
|
5
|
+
import { userEvent, within } from 'storybook/test';
|
|
6
|
+
import { SecretForm } from './secret-form.js';
|
|
7
|
+
const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
|
|
8
|
+
function Wrapper({ children }) {
|
|
9
|
+
const queryClient = useMemo(()=>new QueryClient({
|
|
10
|
+
defaultOptions: {
|
|
11
|
+
queries: {
|
|
12
|
+
retry: false
|
|
13
|
+
},
|
|
14
|
+
mutations: {
|
|
15
|
+
retry: false
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}), []);
|
|
19
|
+
return /*#__PURE__*/ _jsxs(QueryClientProvider, {
|
|
20
|
+
client: queryClient,
|
|
21
|
+
children: [
|
|
22
|
+
/*#__PURE__*/ _jsx("div", {
|
|
23
|
+
className: "w-[520px] overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base",
|
|
24
|
+
children: children
|
|
25
|
+
}),
|
|
26
|
+
/*#__PURE__*/ _jsx(Toaster, {})
|
|
27
|
+
]
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const meta = {
|
|
31
|
+
title: 'Secrets/SecretForm',
|
|
32
|
+
component: SecretForm,
|
|
33
|
+
args: {
|
|
34
|
+
workspaceId: WORKSPACE_ID,
|
|
35
|
+
mode: 'create',
|
|
36
|
+
onSaved: ()=>undefined,
|
|
37
|
+
onCancel: ()=>undefined
|
|
38
|
+
},
|
|
39
|
+
decorators: [
|
|
40
|
+
(Story)=>/*#__PURE__*/ _jsx(Wrapper, {
|
|
41
|
+
children: /*#__PURE__*/ _jsx(Story, {})
|
|
42
|
+
})
|
|
43
|
+
]
|
|
44
|
+
};
|
|
45
|
+
export default meta;
|
|
46
|
+
export const Playground = {};
|
|
47
|
+
export const Edit = {
|
|
48
|
+
args: {
|
|
49
|
+
mode: 'edit',
|
|
50
|
+
existingKey: 'API_TOKEN'
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
export const ShortValueWarning = {
|
|
54
|
+
play: async ({ canvasElement })=>{
|
|
55
|
+
const canvas = within(canvasElement);
|
|
56
|
+
await userEvent.type(canvas.getByLabelText('Value'), 'abc');
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
//# sourceMappingURL=secret-form.stories.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/secret-form.stories.tsx"],"sourcesContent":["import {Toaster} from '@shipfox/react-ui/toast';\nimport type {Meta, StoryObj} from '@storybook/react';\nimport {QueryClient, QueryClientProvider} from '@tanstack/react-query';\nimport type {ReactNode} from 'react';\nimport {useMemo} from 'react';\nimport {userEvent, within} from 'storybook/test';\nimport {SecretForm} from './secret-form.js';\n\nconst WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';\n\nfunction Wrapper({children}: {children: ReactNode}) {\n const queryClient = useMemo(\n () => new QueryClient({defaultOptions: {queries: {retry: false}, mutations: {retry: false}}}),\n [],\n );\n return (\n <QueryClientProvider client={queryClient}>\n <div className=\"w-[520px] overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base\">\n {children}\n </div>\n <Toaster />\n </QueryClientProvider>\n );\n}\n\nconst meta: Meta<typeof SecretForm> = {\n title: 'Secrets/SecretForm',\n component: SecretForm,\n args: {\n workspaceId: WORKSPACE_ID,\n mode: 'create',\n onSaved: () => undefined,\n onCancel: () => undefined,\n },\n decorators: [\n (Story) => (\n <Wrapper>\n <Story />\n </Wrapper>\n ),\n ],\n};\nexport default meta;\n\ntype Story = StoryObj<typeof SecretForm>;\n\nexport const Playground: Story = {};\n\nexport const Edit: Story = {\n args: {mode: 'edit', existingKey: 'API_TOKEN'},\n};\n\nexport const ShortValueWarning: Story = {\n play: async ({canvasElement}) => {\n const canvas = within(canvasElement);\n await userEvent.type(canvas.getByLabelText('Value'), 'abc');\n },\n};\n"],"names":["Toaster","QueryClient","QueryClientProvider","useMemo","userEvent","within","SecretForm","WORKSPACE_ID","Wrapper","children","queryClient","defaultOptions","queries","retry","mutations","client","div","className","meta","title","component","args","workspaceId","mode","onSaved","undefined","onCancel","decorators","Story","Playground","Edit","existingKey","ShortValueWarning","play","canvasElement","canvas","type","getByLabelText"],"mappings":";AAAA,SAAQA,OAAO,QAAO,0BAA0B;AAEhD,SAAQC,WAAW,EAAEC,mBAAmB,QAAO,wBAAwB;AAEvE,SAAQC,OAAO,QAAO,QAAQ;AAC9B,SAAQC,SAAS,EAAEC,MAAM,QAAO,iBAAiB;AACjD,SAAQC,UAAU,QAAO,mBAAmB;AAE5C,MAAMC,eAAe;AAErB,SAASC,QAAQ,EAACC,QAAQ,EAAwB;IAChD,MAAMC,cAAcP,QAClB,IAAM,IAAIF,YAAY;YAACU,gBAAgB;gBAACC,SAAS;oBAACC,OAAO;gBAAK;gBAAGC,WAAW;oBAACD,OAAO;gBAAK;YAAC;QAAC,IAC3F,EAAE;IAEJ,qBACE,MAACX;QAAoBa,QAAQL;;0BAC3B,KAACM;gBAAIC,WAAU;0BACZR;;0BAEH,KAACT;;;AAGP;AAEA,MAAMkB,OAAgC;IACpCC,OAAO;IACPC,WAAWd;IACXe,MAAM;QACJC,aAAaf;QACbgB,MAAM;QACNC,SAAS,IAAMC;QACfC,UAAU,IAAMD;IAClB;IACAE,YAAY;QACV,CAACC,sBACC,KAACpB;0BACC,cAAA,KAACoB;;KAGN;AACH;AACA,eAAeV,KAAK;AAIpB,OAAO,MAAMW,aAAoB,CAAC,EAAE;AAEpC,OAAO,MAAMC,OAAc;IACzBT,MAAM;QAACE,MAAM;QAAQQ,aAAa;IAAW;AAC/C,EAAE;AAEF,OAAO,MAAMC,oBAA2B;IACtCC,MAAM,OAAO,EAACC,aAAa,EAAC;QAC1B,MAAMC,SAAS9B,OAAO6B;QACtB,MAAM9B,UAAUgC,IAAI,CAACD,OAAOE,cAAc,CAAC,UAAU;IACvD;AACF,EAAE"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const STORE_KEY_HELP = "Uppercase letters, digits and underscores; must start with a letter or underscore.";
|
|
2
|
+
/**
|
|
3
|
+
* Validates a store key against the shared `secretKeySchema` and returns a
|
|
4
|
+
* human-readable message (not Zod's default) when it fails. The name input is
|
|
5
|
+
* also auto-uppercased at the call site, so the common case never trips this.
|
|
6
|
+
*/
|
|
7
|
+
export declare function validateStoreKey(value: string): string | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Format validation plus a create-mode collision guard. `PUT /:key` is an
|
|
10
|
+
* upsert and single writes never return a duplicate-key error, so without this
|
|
11
|
+
* a create with an existing name would silently overwrite the current value —
|
|
12
|
+
* unrecoverable for write-only secrets. Edit mode locks the key, so it skips
|
|
13
|
+
* the collision check.
|
|
14
|
+
*/
|
|
15
|
+
export declare function validateNewStoreKey(value: string, { mode, reservedKeys, kind, }: {
|
|
16
|
+
mode: 'create' | 'edit';
|
|
17
|
+
reservedKeys: readonly string[];
|
|
18
|
+
kind: 'secret' | 'variable';
|
|
19
|
+
}): string | undefined;
|
|
20
|
+
//# sourceMappingURL=store-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-key.d.ts","sourceRoot":"","sources":["../../src/components/store-key.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,uFAC2D,CAAC;AAEvF;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAElE;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,EACE,IAAI,EACJ,YAAY,EACZ,IAAI,GACL,EAAE;IAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,CAAA;CAAC,GACzF,MAAM,GAAG,SAAS,CAOpB"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { secretKeySchema } from '@shipfox/api-secrets-dto';
|
|
2
|
+
export const STORE_KEY_HELP = 'Uppercase letters, digits and underscores; must start with a letter or underscore.';
|
|
3
|
+
/**
|
|
4
|
+
* Validates a store key against the shared `secretKeySchema` and returns a
|
|
5
|
+
* human-readable message (not Zod's default) when it fails. The name input is
|
|
6
|
+
* also auto-uppercased at the call site, so the common case never trips this.
|
|
7
|
+
*/ export function validateStoreKey(value) {
|
|
8
|
+
return secretKeySchema.safeParse(value).success ? undefined : STORE_KEY_HELP;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Format validation plus a create-mode collision guard. `PUT /:key` is an
|
|
12
|
+
* upsert and single writes never return a duplicate-key error, so without this
|
|
13
|
+
* a create with an existing name would silently overwrite the current value —
|
|
14
|
+
* unrecoverable for write-only secrets. Edit mode locks the key, so it skips
|
|
15
|
+
* the collision check.
|
|
16
|
+
*/ export function validateNewStoreKey(value, { mode, reservedKeys, kind }) {
|
|
17
|
+
const formatError = validateStoreKey(value);
|
|
18
|
+
if (formatError) return formatError;
|
|
19
|
+
if (mode === 'create' && reservedKeys.includes(value)) {
|
|
20
|
+
return `A ${kind} with this name already exists. Edit it instead.`;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=store-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/store-key.ts"],"sourcesContent":["import {secretKeySchema} from '@shipfox/api-secrets-dto';\n\nexport const STORE_KEY_HELP =\n 'Uppercase letters, digits and underscores; must start with a letter or underscore.';\n\n/**\n * Validates a store key against the shared `secretKeySchema` and returns a\n * human-readable message (not Zod's default) when it fails. The name input is\n * also auto-uppercased at the call site, so the common case never trips this.\n */\nexport function validateStoreKey(value: string): string | undefined {\n return secretKeySchema.safeParse(value).success ? undefined : STORE_KEY_HELP;\n}\n\n/**\n * Format validation plus a create-mode collision guard. `PUT /:key` is an\n * upsert and single writes never return a duplicate-key error, so without this\n * a create with an existing name would silently overwrite the current value —\n * unrecoverable for write-only secrets. Edit mode locks the key, so it skips\n * the collision check.\n */\nexport function validateNewStoreKey(\n value: string,\n {\n mode,\n reservedKeys,\n kind,\n }: {mode: 'create' | 'edit'; reservedKeys: readonly string[]; kind: 'secret' | 'variable'},\n): string | undefined {\n const formatError = validateStoreKey(value);\n if (formatError) return formatError;\n if (mode === 'create' && reservedKeys.includes(value)) {\n return `A ${kind} with this name already exists. Edit it instead.`;\n }\n return undefined;\n}\n"],"names":["secretKeySchema","STORE_KEY_HELP","validateStoreKey","value","safeParse","success","undefined","validateNewStoreKey","mode","reservedKeys","kind","formatError","includes"],"mappings":"AAAA,SAAQA,eAAe,QAAO,2BAA2B;AAEzD,OAAO,MAAMC,iBACX,qFAAqF;AAEvF;;;;CAIC,GACD,OAAO,SAASC,iBAAiBC,KAAa;IAC5C,OAAOH,gBAAgBI,SAAS,CAACD,OAAOE,OAAO,GAAGC,YAAYL;AAChE;AAEA;;;;;;CAMC,GACD,OAAO,SAASM,oBACdJ,KAAa,EACb,EACEK,IAAI,EACJC,YAAY,EACZC,IAAI,EACoF;IAE1F,MAAMC,cAAcT,iBAAiBC;IACrC,IAAIQ,aAAa,OAAOA;IACxB,IAAIH,SAAS,YAAYC,aAAaG,QAAQ,CAACT,QAAQ;QACrD,OAAO,CAAC,EAAE,EAAEO,KAAK,gDAAgD,CAAC;IACpE;IACA,OAAOJ;AACT"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const STORE_SURFACE_CLASS = "overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base";
|
|
2
|
+
/** Placeholder rows shown while the store list loads. */
|
|
3
|
+
export declare function StoreRowsSkeleton({ label }: {
|
|
4
|
+
label: string;
|
|
5
|
+
}): import("react").JSX.Element;
|
|
6
|
+
export declare function StoreSurface({ className, children, }: {
|
|
7
|
+
className?: string;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=store-section-shell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-section-shell.d.ts","sourceRoot":"","sources":["../../src/components/store-section-shell.tsx"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,2FAC0D,CAAC;AAE3F,yDAAyD;AACzD,wBAAgB,iBAAiB,CAAC,EAAC,KAAK,EAAC,EAAE;IAAC,KAAK,EAAE,MAAM,CAAA;CAAC,+BAczD;AAED,wBAAgB,YAAY,CAAC,EAC3B,SAAS,EACT,QAAQ,GACT,EAAE;IACD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,+BAEA"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Skeleton } from '@shipfox/react-ui/skeleton';
|
|
3
|
+
import { cn } from '@shipfox/react-ui/utils';
|
|
4
|
+
export const STORE_SURFACE_CLASS = 'overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base';
|
|
5
|
+
/** Placeholder rows shown while the store list loads. */ export function StoreRowsSkeleton({ label }) {
|
|
6
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
7
|
+
role: "status",
|
|
8
|
+
"aria-label": label,
|
|
9
|
+
className: STORE_SURFACE_CLASS,
|
|
10
|
+
children: /*#__PURE__*/ _jsx("ul", {
|
|
11
|
+
className: "divide-y divide-border-neutral-base",
|
|
12
|
+
children: [
|
|
13
|
+
0,
|
|
14
|
+
1,
|
|
15
|
+
2
|
|
16
|
+
].map((row)=>/*#__PURE__*/ _jsxs("li", {
|
|
17
|
+
className: "flex items-center gap-12 px-16 py-12",
|
|
18
|
+
children: [
|
|
19
|
+
/*#__PURE__*/ _jsx(Skeleton, {
|
|
20
|
+
className: "h-16 w-140"
|
|
21
|
+
}),
|
|
22
|
+
/*#__PURE__*/ _jsx(Skeleton, {
|
|
23
|
+
className: "h-16 w-96"
|
|
24
|
+
}),
|
|
25
|
+
/*#__PURE__*/ _jsx(Skeleton, {
|
|
26
|
+
className: "ml-auto h-14 w-80 shrink-0"
|
|
27
|
+
})
|
|
28
|
+
]
|
|
29
|
+
}, row))
|
|
30
|
+
})
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function StoreSurface({ className, children }) {
|
|
34
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
35
|
+
className: cn(STORE_SURFACE_CLASS, className),
|
|
36
|
+
children: children
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=store-section-shell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/store-section-shell.tsx"],"sourcesContent":["import {Skeleton} from '@shipfox/react-ui/skeleton';\nimport {cn} from '@shipfox/react-ui/utils';\n\nexport const STORE_SURFACE_CLASS =\n 'overflow-hidden rounded-8 border border-border-neutral-base bg-background-neutral-base';\n\n/** Placeholder rows shown while the store list loads. */\nexport function StoreRowsSkeleton({label}: {label: string}) {\n return (\n <div role=\"status\" aria-label={label} className={STORE_SURFACE_CLASS}>\n <ul className=\"divide-y divide-border-neutral-base\">\n {[0, 1, 2].map((row) => (\n <li key={row} className=\"flex items-center gap-12 px-16 py-12\">\n <Skeleton className=\"h-16 w-140\" />\n <Skeleton className=\"h-16 w-96\" />\n <Skeleton className=\"ml-auto h-14 w-80 shrink-0\" />\n </li>\n ))}\n </ul>\n </div>\n );\n}\n\nexport function StoreSurface({\n className,\n children,\n}: {\n className?: string;\n children: React.ReactNode;\n}) {\n return <div className={cn(STORE_SURFACE_CLASS, className)}>{children}</div>;\n}\n"],"names":["Skeleton","cn","STORE_SURFACE_CLASS","StoreRowsSkeleton","label","div","role","aria-label","className","ul","map","row","li","StoreSurface","children"],"mappings":";AAAA,SAAQA,QAAQ,QAAO,6BAA6B;AACpD,SAAQC,EAAE,QAAO,0BAA0B;AAE3C,OAAO,MAAMC,sBACX,yFAAyF;AAE3F,uDAAuD,GACvD,OAAO,SAASC,kBAAkB,EAACC,KAAK,EAAkB;IACxD,qBACE,KAACC;QAAIC,MAAK;QAASC,cAAYH;QAAOI,WAAWN;kBAC/C,cAAA,KAACO;YAAGD,WAAU;sBACX;gBAAC;gBAAG;gBAAG;aAAE,CAACE,GAAG,CAAC,CAACC,oBACd,MAACC;oBAAaJ,WAAU;;sCACtB,KAACR;4BAASQ,WAAU;;sCACpB,KAACR;4BAASQ,WAAU;;sCACpB,KAACR;4BAASQ,WAAU;;;mBAHbG;;;AASnB;AAEA,OAAO,SAASE,aAAa,EAC3BL,SAAS,EACTM,QAAQ,EAIT;IACC,qBAAO,KAACT;QAAIG,WAAWP,GAAGC,qBAAqBM;kBAAaM;;AAC9D"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const VARIABLE_FORM_ID = "variable-form";
|
|
2
|
+
export declare function VariableForm({ workspaceId, mode, existingKey, existingValue, existingValueTruncated, reservedKeys, onSaved, onCancel, }: {
|
|
3
|
+
workspaceId: string;
|
|
4
|
+
mode: 'create' | 'edit';
|
|
5
|
+
existingKey?: string | undefined;
|
|
6
|
+
existingValue?: string | undefined;
|
|
7
|
+
existingValueTruncated?: boolean | undefined;
|
|
8
|
+
reservedKeys?: readonly string[] | undefined;
|
|
9
|
+
onSaved: () => void;
|
|
10
|
+
onCancel: () => void;
|
|
11
|
+
}): import("react").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=variable-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"variable-form.d.ts","sourceRoot":"","sources":["../../src/components/variable-form.tsx"],"names":[],"mappings":"AAiBA,eAAO,MAAM,gBAAgB,kBAAkB,CAAC;AAEhD,wBAAgB,YAAY,CAAC,EAC3B,WAAW,EACX,IAAI,EACJ,WAAW,EACX,aAAa,EACb,sBAA8B,EAC9B,YAAiB,EACjB,OAAO,EACP,QAAQ,GACT,EAAE;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,sBAAsB,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7C,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;IAC7C,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB,+BAgJA"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { isSensitiveSecretName } from '@shipfox/api-secrets-dto';
|
|
3
|
+
import { Button } from '@shipfox/react-ui/button';
|
|
4
|
+
import { Callout } from '@shipfox/react-ui/callout';
|
|
5
|
+
import { FormField, FormFieldInput, FormFieldTextarea, fieldError } from '@shipfox/react-ui/form-field';
|
|
6
|
+
import { Text } from '@shipfox/react-ui/typography';
|
|
7
|
+
import { useForm } from '@tanstack/react-form';
|
|
8
|
+
import { useEffect, useRef, useState } from 'react';
|
|
9
|
+
import { usePutVariableMutation, useVariableQuery } from '#hooks/api/variables.js';
|
|
10
|
+
import { secretsErrorToFormError } from './form-errors.js';
|
|
11
|
+
import { FormBody, FormFooter } from './form-shell.js';
|
|
12
|
+
import { STORE_KEY_HELP, validateNewStoreKey } from './store-key.js';
|
|
13
|
+
export const VARIABLE_FORM_ID = 'variable-form';
|
|
14
|
+
export function VariableForm({ workspaceId, mode, existingKey, existingValue, existingValueTruncated = false, reservedKeys = [], onSaved, onCancel }) {
|
|
15
|
+
const putVariable = usePutVariableMutation();
|
|
16
|
+
const [formError, setFormError] = useState();
|
|
17
|
+
// The list value is only a preview, so a truncated variable must load its full
|
|
18
|
+
// value before editing to avoid saving the truncated preview back.
|
|
19
|
+
const needsFullValue = mode === 'edit' && existingValueTruncated;
|
|
20
|
+
const fullValueQuery = useVariableQuery(workspaceId, needsFullValue ? existingKey : undefined);
|
|
21
|
+
const form = useForm({
|
|
22
|
+
defaultValues: {
|
|
23
|
+
key: existingKey ?? '',
|
|
24
|
+
value: existingValue ?? ''
|
|
25
|
+
},
|
|
26
|
+
onSubmit: async ({ value })=>{
|
|
27
|
+
setFormError(undefined);
|
|
28
|
+
try {
|
|
29
|
+
await putVariable.mutateAsync({
|
|
30
|
+
workspaceId,
|
|
31
|
+
key: value.key,
|
|
32
|
+
body: {
|
|
33
|
+
value: value.value
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
onSaved();
|
|
37
|
+
} catch (error) {
|
|
38
|
+
const mapped = secretsErrorToFormError(error);
|
|
39
|
+
if (mapped.kind === 'field') {
|
|
40
|
+
form.setFieldMeta(mapped.field, (prev)=>({
|
|
41
|
+
...prev,
|
|
42
|
+
errorMap: {
|
|
43
|
+
...prev.errorMap,
|
|
44
|
+
onServer: mapped.message
|
|
45
|
+
}
|
|
46
|
+
}));
|
|
47
|
+
} else {
|
|
48
|
+
setFormError(mapped.message);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const populatedRef = useRef(false);
|
|
54
|
+
useEffect(()=>{
|
|
55
|
+
if (needsFullValue && fullValueQuery.data && !populatedRef.current) {
|
|
56
|
+
populatedRef.current = true;
|
|
57
|
+
form.setFieldValue('value', fullValueQuery.data.value);
|
|
58
|
+
}
|
|
59
|
+
}, [
|
|
60
|
+
needsFullValue,
|
|
61
|
+
fullValueQuery.data,
|
|
62
|
+
form
|
|
63
|
+
]);
|
|
64
|
+
// The full value must be loaded before a save is allowed, otherwise submitting would
|
|
65
|
+
// overwrite the stored value with the truncated preview. Block until it arrives —
|
|
66
|
+
// whether the fetch is still pending or has failed.
|
|
67
|
+
const awaitingFullValue = needsFullValue && fullValueQuery.data === undefined;
|
|
68
|
+
const loadingFullValue = needsFullValue && fullValueQuery.isPending;
|
|
69
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
70
|
+
children: [
|
|
71
|
+
/*#__PURE__*/ _jsxs(FormBody, {
|
|
72
|
+
children: [
|
|
73
|
+
/*#__PURE__*/ _jsxs("form", {
|
|
74
|
+
id: VARIABLE_FORM_ID,
|
|
75
|
+
className: "flex w-full flex-col gap-16",
|
|
76
|
+
noValidate: true,
|
|
77
|
+
onSubmit: (event)=>{
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
event.stopPropagation();
|
|
80
|
+
void form.handleSubmit();
|
|
81
|
+
},
|
|
82
|
+
children: [
|
|
83
|
+
/*#__PURE__*/ _jsx(form.Field, {
|
|
84
|
+
name: "key",
|
|
85
|
+
validators: {
|
|
86
|
+
onBlur: ({ value })=>validateNewStoreKey(value, {
|
|
87
|
+
mode,
|
|
88
|
+
reservedKeys,
|
|
89
|
+
kind: 'variable'
|
|
90
|
+
}),
|
|
91
|
+
onSubmit: ({ value })=>validateNewStoreKey(value, {
|
|
92
|
+
mode,
|
|
93
|
+
reservedKeys,
|
|
94
|
+
kind: 'variable'
|
|
95
|
+
})
|
|
96
|
+
},
|
|
97
|
+
children: (field)=>/*#__PURE__*/ _jsxs(FormField, {
|
|
98
|
+
label: "Name",
|
|
99
|
+
id: "variable-key",
|
|
100
|
+
description: STORE_KEY_HELP,
|
|
101
|
+
error: fieldError(field),
|
|
102
|
+
children: [
|
|
103
|
+
/*#__PURE__*/ _jsx(FormFieldInput, {
|
|
104
|
+
className: "font-code",
|
|
105
|
+
autoComplete: "off",
|
|
106
|
+
spellCheck: false,
|
|
107
|
+
disabled: mode === 'edit',
|
|
108
|
+
placeholder: "LOG_LEVEL",
|
|
109
|
+
value: field.state.value,
|
|
110
|
+
onChange: (event)=>field.handleChange(event.target.value.toUpperCase()),
|
|
111
|
+
onBlur: field.handleBlur
|
|
112
|
+
}),
|
|
113
|
+
isSensitiveSecretName(field.state.value) ? /*#__PURE__*/ _jsx(Callout, {
|
|
114
|
+
role: "alert",
|
|
115
|
+
type: "warning",
|
|
116
|
+
children: /*#__PURE__*/ _jsx(Text, {
|
|
117
|
+
size: "sm",
|
|
118
|
+
"aria-live": "polite",
|
|
119
|
+
children: "This looks like it may be sensitive. Variables are stored in plaintext and are not redacted from logs. Use a Secret if this value contains private data."
|
|
120
|
+
})
|
|
121
|
+
}) : null
|
|
122
|
+
]
|
|
123
|
+
})
|
|
124
|
+
}),
|
|
125
|
+
/*#__PURE__*/ _jsx(form.Field, {
|
|
126
|
+
name: "value",
|
|
127
|
+
children: (field)=>/*#__PURE__*/ _jsx(FormField, {
|
|
128
|
+
label: "Value",
|
|
129
|
+
id: "variable-value",
|
|
130
|
+
error: fieldError(field),
|
|
131
|
+
description: loadingFullValue ? 'Loading the current value…' : undefined,
|
|
132
|
+
children: /*#__PURE__*/ _jsx(FormFieldTextarea, {
|
|
133
|
+
className: "font-code",
|
|
134
|
+
autoComplete: "off",
|
|
135
|
+
spellCheck: false,
|
|
136
|
+
rows: 3,
|
|
137
|
+
disabled: awaitingFullValue,
|
|
138
|
+
placeholder: "debug",
|
|
139
|
+
value: field.state.value,
|
|
140
|
+
onChange: (event)=>field.handleChange(event.target.value),
|
|
141
|
+
onBlur: field.handleBlur
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
]
|
|
146
|
+
}),
|
|
147
|
+
awaitingFullValue && fullValueQuery.isError ? /*#__PURE__*/ _jsx(Callout, {
|
|
148
|
+
role: "alert",
|
|
149
|
+
type: "error",
|
|
150
|
+
children: /*#__PURE__*/ _jsx(Text, {
|
|
151
|
+
size: "sm",
|
|
152
|
+
children: "Could not load the current value. Close and try again."
|
|
153
|
+
})
|
|
154
|
+
}) : null,
|
|
155
|
+
formError ? /*#__PURE__*/ _jsx(Callout, {
|
|
156
|
+
role: "alert",
|
|
157
|
+
type: "error",
|
|
158
|
+
children: /*#__PURE__*/ _jsx(Text, {
|
|
159
|
+
size: "sm",
|
|
160
|
+
children: formError
|
|
161
|
+
})
|
|
162
|
+
}) : null
|
|
163
|
+
]
|
|
164
|
+
}),
|
|
165
|
+
/*#__PURE__*/ _jsxs(FormFooter, {
|
|
166
|
+
children: [
|
|
167
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
168
|
+
variant: "secondary",
|
|
169
|
+
type: "button",
|
|
170
|
+
onClick: onCancel,
|
|
171
|
+
children: "Cancel"
|
|
172
|
+
}),
|
|
173
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
174
|
+
type: "submit",
|
|
175
|
+
form: VARIABLE_FORM_ID,
|
|
176
|
+
isLoading: putVariable.isPending,
|
|
177
|
+
disabled: awaitingFullValue,
|
|
178
|
+
children: mode === 'edit' ? 'Update variable' : 'Create variable'
|
|
179
|
+
})
|
|
180
|
+
]
|
|
181
|
+
})
|
|
182
|
+
]
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
//# sourceMappingURL=variable-form.js.map
|