@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.
Files changed (114) hide show
  1. package/.storybook/main.ts +1 -0
  2. package/.storybook/preview.css +4 -0
  3. package/.storybook/preview.tsx +56 -0
  4. package/.turbo/turbo-build.log +2 -0
  5. package/.turbo/turbo-type$colon$emit.log +1 -0
  6. package/.turbo/turbo-type.log +1 -0
  7. package/CHANGELOG.md +102 -0
  8. package/LICENSE +21 -0
  9. package/dist/components/copy-key.d.ts +8 -0
  10. package/dist/components/copy-key.d.ts.map +1 -0
  11. package/dist/components/copy-key.js +21 -0
  12. package/dist/components/copy-key.js.map +1 -0
  13. package/dist/components/delete-entry-dialog.d.ts +9 -0
  14. package/dist/components/delete-entry-dialog.d.ts.map +1 -0
  15. package/dist/components/delete-entry-dialog.js +66 -0
  16. package/dist/components/delete-entry-dialog.js.map +1 -0
  17. package/dist/components/form-errors.d.ts +17 -0
  18. package/dist/components/form-errors.d.ts.map +1 -0
  19. package/dist/components/form-errors.js +81 -0
  20. package/dist/components/form-errors.js.map +1 -0
  21. package/dist/components/form-shell.d.ts +14 -0
  22. package/dist/components/form-shell.d.ts.map +1 -0
  23. package/dist/components/form-shell.js +28 -0
  24. package/dist/components/form-shell.js.map +1 -0
  25. package/dist/components/secret-form.d.ts +10 -0
  26. package/dist/components/secret-form.d.ts.map +1 -0
  27. package/dist/components/secret-form.js +176 -0
  28. package/dist/components/secret-form.js.map +1 -0
  29. package/dist/components/secret-form.stories.d.ts +9 -0
  30. package/dist/components/secret-form.stories.d.ts.map +1 -0
  31. package/dist/components/secret-form.stories.js +60 -0
  32. package/dist/components/secret-form.stories.js.map +1 -0
  33. package/dist/components/store-key.d.ts +20 -0
  34. package/dist/components/store-key.d.ts.map +1 -0
  35. package/dist/components/store-key.js +25 -0
  36. package/dist/components/store-key.js.map +1 -0
  37. package/dist/components/store-section-shell.d.ts +10 -0
  38. package/dist/components/store-section-shell.d.ts.map +1 -0
  39. package/dist/components/store-section-shell.js +40 -0
  40. package/dist/components/store-section-shell.js.map +1 -0
  41. package/dist/components/variable-form.d.ts +12 -0
  42. package/dist/components/variable-form.d.ts.map +1 -0
  43. package/dist/components/variable-form.js +186 -0
  44. package/dist/components/variable-form.js.map +1 -0
  45. package/dist/components/variable-form.stories.d.ts +9 -0
  46. package/dist/components/variable-form.stories.d.ts.map +1 -0
  47. package/dist/components/variable-form.stories.js +61 -0
  48. package/dist/components/variable-form.stories.js.map +1 -0
  49. package/dist/components/workspace-secrets-section.d.ts +4 -0
  50. package/dist/components/workspace-secrets-section.d.ts.map +1 -0
  51. package/dist/components/workspace-secrets-section.js +251 -0
  52. package/dist/components/workspace-secrets-section.js.map +1 -0
  53. package/dist/components/workspace-secrets-section.stories.d.ts +13 -0
  54. package/dist/components/workspace-secrets-section.stories.d.ts.map +1 -0
  55. package/dist/components/workspace-secrets-section.stories.js +140 -0
  56. package/dist/components/workspace-secrets-section.stories.js.map +1 -0
  57. package/dist/components/workspace-variables-section.d.ts +4 -0
  58. package/dist/components/workspace-variables-section.d.ts.map +1 -0
  59. package/dist/components/workspace-variables-section.js +255 -0
  60. package/dist/components/workspace-variables-section.js.map +1 -0
  61. package/dist/components/workspace-variables-section.stories.d.ts +11 -0
  62. package/dist/components/workspace-variables-section.stories.d.ts.map +1 -0
  63. package/dist/components/workspace-variables-section.stories.js +127 -0
  64. package/dist/components/workspace-variables-section.stories.js.map +1 -0
  65. package/dist/hooks/api/create-store-api.d.ts +56 -0
  66. package/dist/hooks/api/create-store-api.d.ts.map +1 -0
  67. package/dist/hooks/api/create-store-api.js +95 -0
  68. package/dist/hooks/api/create-store-api.js.map +1 -0
  69. package/dist/hooks/api/secrets.d.ts +61 -0
  70. package/dist/hooks/api/secrets.d.ts.map +1 -0
  71. package/dist/hooks/api/secrets.js +15 -0
  72. package/dist/hooks/api/secrets.js.map +1 -0
  73. package/dist/hooks/api/variables.d.ts +88 -0
  74. package/dist/hooks/api/variables.d.ts.map +1 -0
  75. package/dist/hooks/api/variables.js +52 -0
  76. package/dist/hooks/api/variables.js.map +1 -0
  77. package/dist/index.d.ts +6 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +7 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/tsconfig.test.tsbuildinfo +1 -0
  82. package/package.json +89 -0
  83. package/src/components/copy-key.test.ts +44 -0
  84. package/src/components/copy-key.ts +21 -0
  85. package/src/components/delete-entry-dialog.tsx +57 -0
  86. package/src/components/form-errors.test.ts +67 -0
  87. package/src/components/form-errors.ts +59 -0
  88. package/src/components/form-shell.tsx +31 -0
  89. package/src/components/secret-form.stories.tsx +58 -0
  90. package/src/components/secret-form.test.tsx +37 -0
  91. package/src/components/secret-form.tsx +170 -0
  92. package/src/components/store-key.test.ts +53 -0
  93. package/src/components/store-key.ts +36 -0
  94. package/src/components/store-section-shell.tsx +32 -0
  95. package/src/components/variable-form.stories.tsx +58 -0
  96. package/src/components/variable-form.test.tsx +60 -0
  97. package/src/components/variable-form.tsx +182 -0
  98. package/src/components/workspace-secrets-section.stories.tsx +128 -0
  99. package/src/components/workspace-secrets-section.tsx +231 -0
  100. package/src/components/workspace-variables-section.stories.tsx +121 -0
  101. package/src/components/workspace-variables-section.tsx +238 -0
  102. package/src/hooks/api/create-store-api.ts +114 -0
  103. package/src/hooks/api/secrets.test.ts +157 -0
  104. package/src/hooks/api/secrets.ts +26 -0
  105. package/src/hooks/api/variables.ts +61 -0
  106. package/src/index.ts +5 -0
  107. package/test/fixtures/secrets.ts +59 -0
  108. package/test/setup.ts +3 -0
  109. package/tsconfig.build.json +9 -0
  110. package/tsconfig.build.tsbuildinfo +1 -0
  111. package/tsconfig.json +3 -0
  112. package/tsconfig.test.json +8 -0
  113. package/vercel.json +8 -0
  114. 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":";AAAA,SAAQA,kBAAkB,QAAO,2BAA2B;AAC5D,SAAQC,MAAM,EAAEC,UAAU,QAAO,2BAA2B;AAC5D,SAAQC,OAAO,QAAO,4BAA4B;AAClD,SACEC,SAAS,EACTC,cAAc,EACdC,iBAAiB,EACjBC,UAAU,QACL,+BAA+B;AACtC,SAAQC,IAAI,QAAO,+BAA+B;AAClD,SAAQC,OAAO,QAAO,uBAAuB;AAC7C,SAAQC,QAAQ,QAAO,QAAQ;AAC/B,SAAQC,oBAAoB,QAAO,wBAAwB;AAC3D,SAAQC,uBAAuB,QAAO,mBAAmB;AACzD,SAAQC,QAAQ,EAAEC,UAAU,QAAO,kBAAkB;AACrD,SAAQC,cAAc,EAAEC,mBAAmB,QAAO,iBAAiB;AAEnE,OAAO,MAAMC,iBAAiB,cAAc;AAE5C,gFAAgF;AAChF,gFAAgF;AAChF,MAAMC,wBAAwB;AAE9B,OAAO,SAASC,WAAW,EACzBC,WAAW,EACXC,IAAI,EACJC,WAAW,EACXC,eAAe,EAAE,EACjBC,OAAO,EACPC,QAAQ,EAQT;IACC,MAAMC,YAAYf;IAClB,MAAM,CAACgB,WAAWC,aAAa,GAAGlB;IAClC,MAAM,CAACmB,WAAWC,aAAa,GAAGpB,SAAS;IAE3C,MAAMqB,OAAOtB,QAAQ;QACnBuB,eAAe;YAACC,KAAKX,eAAe;YAAIY,OAAO;QAAE;QACjDC,UAAU,OAAO,EAACD,KAAK,EAAC;YACtBN,aAAaQ;YACb,IAAI;gBACF,MAAMV,UAAUW,WAAW,CAAC;oBAACjB;oBAAaa,KAAKC,MAAMD,GAAG;oBAAEK,MAAM;wBAACJ,OAAOA,MAAMA,KAAK;oBAAA;gBAAC;gBACpFV;YACF,EAAE,OAAOe,OAAO;gBACd,MAAMC,SAAS5B,wBAAwB2B;gBACvC,IAAIC,OAAOC,IAAI,KAAK,SAAS;oBAC3BV,KAAKW,YAAY,CAACF,OAAOG,KAAK,EAAE,CAACC,OAAU,CAAA;4BACzC,GAAGA,IAAI;4BACPC,UAAU;gCAAC,GAAGD,KAAKC,QAAQ;gCAAEC,UAAUN,OAAOO,OAAO;4BAAA;wBACvD,CAAA;gBACF,OAAO;oBACLnB,aAAaY,OAAOO,OAAO;gBAC7B;YACF;QACF;IACF;IAEA,qBACE;;0BACE,MAAClC;;kCACC,MAACkB;wBACCiB,IAAI/B;wBACJgC,WAAU;wBACVC,UAAU;wBACVf,UAAU,CAACgB;4BACTA,MAAMC,cAAc;4BACpBD,MAAME,eAAe;4BACrB,KAAKtB,KAAKuB,YAAY;wBACxB;;0CAEA,KAACvB,KAAKwB,KAAK;gCACTC,MAAK;gCACLC,YAAY;oCACVC,QAAQ,CAAC,EAACxB,KAAK,EAAC,GAAKlB,oBAAoBkB,OAAO;4CAACb;4CAAME;4CAAckB,MAAM;wCAAQ;oCACnFN,UAAU,CAAC,EAACD,KAAK,EAAC,GAChBlB,oBAAoBkB,OAAO;4CAACb;4CAAME;4CAAckB,MAAM;wCAAQ;gCAClE;0CAEC,CAACE,sBACA,KAACvC;wCACCuD,OAAM;wCACNX,IAAG;wCACHY,aAAa7C;wCACbwB,OAAOhC,WAAWoC;kDAElB,cAAA,KAACtC;4CACC4C,WAAU;4CACVY,cAAa;4CACbC,YAAY;4CACZC,UAAU1C,SAAS;4CACnB2C,aAAY;4CACZ9B,OAAOS,MAAMsB,KAAK,CAAC/B,KAAK;4CACxBgC,UAAU,CAACf,QAAUR,MAAMwB,YAAY,CAAChB,MAAMiB,MAAM,CAAClC,KAAK,CAACmC,WAAW;4CACtEX,QAAQf,MAAM2B,UAAU;;;;0CAMhC,KAACvC,KAAKwB,KAAK;gCACTC,MAAK;gCACLC,YAAY;oCACVC,QAAQ,CAAC,EAACxB,KAAK,EAAC,GAAMA,MAAMqC,MAAM,GAAG,IAAInC,YAAY;oCACrDD,UAAU,CAAC,EAACD,KAAK,EAAC,GAAMA,MAAMqC,MAAM,GAAG,IAAInC,YAAY;gCACzD;0CAEC,CAACO,sBACA,MAACvC;wCAAUuD,OAAM;wCAAQX,IAAG;wCAAeT,OAAOhC,WAAWoC;;0DAC3D,MAAC6B;gDAAIvB,WAAU;;kEACb,KAAC3C;wDACC2C,WACEpB,YAAY,oBAAoB;wDAElCgC,cAAa;wDACbC,YAAY;wDACZW,MAAM;wDACNT,aACE3C,SAAS,SACL,iDACA;wDAENa,OAAOS,MAAMsB,KAAK,CAAC/B,KAAK;wDACxBgC,UAAU,CAACf,QAAUR,MAAMwB,YAAY,CAAChB,MAAMiB,MAAM,CAAClC,KAAK;wDAC1DwB,QAAQf,MAAM2B,UAAU;;kEAE1B,KAACpE;wDACCwE,MAAK;wDACLC,SAAQ;wDACRC,MAAK;wDACL3B,WAAU;wDACV4B,MAAMhD,YAAY,eAAe;wDACjCiD,cAAYjD,YAAY,eAAe;wDACvCkD,SAAS,IAAMjD,aAAa,CAACc,OAAS,CAACA;;;;4CAG1C5C,mBAAmB2C,MAAMsB,KAAK,CAAC/B,KAAK,EAAEhB,uCACrC,KAACf;gDAAQ6E,MAAK;gDAAQN,MAAK;0DACzB,cAAA,KAAClE;oDAAKoE,MAAK;8DAAK;;iDAKhB;;;;;;oBAKXjD,0BACC,KAACxB;wBAAQ6E,MAAK;wBAAQN,MAAK;kCACzB,cAAA,KAAClE;4BAAKoE,MAAK;sCAAMjD;;yBAEjB;;;0BAEN,MAACb;;kCACC,KAACb;wBAAO0E,SAAQ;wBAAYD,MAAK;wBAASK,SAAStD;kCAAU;;kCAG7D,KAACxB;wBAAOyE,MAAK;wBAAS3C,MAAMd;wBAAgBgE,WAAWvD,UAAUwD,SAAS;kCACvE7D,SAAS,SAAS,kBAAkB;;;;;;AAK/C"}
@@ -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