@oneuptime/common 10.2.20 → 10.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,11 @@
1
1
  import ComponentLoader from "../ComponentLoader/ComponentLoader";
2
2
  import ErrorMessage from "../ErrorMessage/ErrorMessage";
3
3
  import BasicForm, { FormProps } from "../Forms/BasicForm";
4
+ import FormFieldSchemaType from "../Forms/Types/FormFieldSchemaType";
5
+ import { CustomElementProps } from "../Forms/Types/Field";
4
6
  import FormValues from "../Forms/Types/FormValues";
5
7
  import ComponentValuePickerModal from "./ComponentValuePickerModal";
8
+ import ModelFieldPicker from "./ModelFieldPicker";
6
9
  import { componentInputTypeToFormFieldType } from "./Utils";
7
10
  import VariableModal from "./VariableModal";
8
11
  import Dictionary from "../../../Types/Dictionary";
@@ -140,17 +143,11 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
140
143
  }, [component]);
141
144
 
142
145
  return (
143
- <div className="mb-3 mt-3">
144
- <div className="mt-5 mb-5">
145
- <h2 className="text-base font-medium text-gray-500">Arguments</h2>
146
- <p className="text-sm font-medium text-gray-400 mb-5">
147
- Arguments for this component
148
- </p>
146
+ <div>
147
+ <div>
149
148
  {component.metadata.arguments &&
150
149
  component.metadata.arguments.length === 0 && (
151
- <ErrorMessage
152
- message={"This component does not take any arguments."}
153
- />
150
+ <ErrorMessage message={"This step does not need any settings."} />
154
151
  )}
155
152
  {/*
156
153
  If any argument is a WorkflowSelect and we're still fetching the
@@ -190,15 +187,51 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
190
187
  const isWorkflowSelect: boolean =
191
188
  arg.type === ComponentInputType.WorkflowSelect;
192
189
 
190
+ /*
191
+ * Database Select args (the "Select Fields" / "Listen on"
192
+ * trigger inputs) get a tree-style field picker backed by
193
+ * the model's schema, instead of a raw JSON textarea. We
194
+ * need a tableName on the component metadata to fetch the
195
+ * column list; without it, fall back to the JSON editor.
196
+ */
197
+ const useFieldPicker: boolean =
198
+ arg.type === ComponentInputType.Select &&
199
+ Boolean(component.metadata.tableName);
200
+
193
201
  const baseField: {
194
202
  fieldType: import("../Forms/Types/FormFieldSchemaType").default;
195
203
  dropdownOptions?: Array<DropdownOption> | undefined;
196
- } = componentInputTypeToFormFieldType(
197
- arg.type,
198
- component.arguments && component.arguments[arg.id]
199
- ? component.arguments[arg.id]
200
- : null,
201
- );
204
+ getCustomElement?: (
205
+ values: FormValues<JSONObject>,
206
+ customProps: CustomElementProps,
207
+ ) => ReactElement | undefined;
208
+ } = useFieldPicker
209
+ ? {
210
+ fieldType: FormFieldSchemaType.CustomComponent,
211
+ getCustomElement: (
212
+ _values: FormValues<JSONObject>,
213
+ customProps: CustomElementProps,
214
+ ): ReactElement => {
215
+ return (
216
+ <ModelFieldPicker
217
+ tableName={component.metadata.tableName as string}
218
+ initialValue={customProps.initialValue}
219
+ onChange={(value: string) => {
220
+ void customProps.onChange?.(value);
221
+ }}
222
+ placeholder={customProps.placeholder}
223
+ error={customProps.error}
224
+ tabIndex={customProps.tabIndex}
225
+ />
226
+ );
227
+ },
228
+ }
229
+ : componentInputTypeToFormFieldType(
230
+ arg.type,
231
+ component.arguments && component.arguments[arg.id]
232
+ ? component.arguments[arg.id]
233
+ : null,
234
+ );
202
235
 
203
236
  /*
204
237
  * For WorkflowSelect, inject the dynamically fetched list
@@ -208,13 +241,17 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
208
241
  baseField.dropdownOptions = workflowDropdownOptions;
209
242
  }
210
243
 
244
+ /*
245
+ * The "pick from component / variable" footer doesn't
246
+ * apply to the field picker (it edits a structured object,
247
+ * not a free-text expression) or to WorkflowSelect.
248
+ */
249
+ const showVariableFooter: boolean =
250
+ !isWorkflowSelect && !useFieldPicker;
251
+
211
252
  return {
212
253
  title: `${arg.name}`,
213
- /*
214
- * WorkflowSelect has no "pick from component/variable"
215
- * footer — it's a bound dropdown, not a free-text field.
216
- */
217
- footerElement: isWorkflowSelect ? undefined : (
254
+ footerElement: showVariableFooter ? (
218
255
  <div className="text-gray-500">
219
256
  <p className="text-sm">
220
257
  Pick this value from other{" "}
@@ -239,7 +276,7 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
239
276
  </button>
240
277
  </p>
241
278
  </div>
242
- ),
279
+ ) : undefined,
243
280
  description: `${
244
281
  arg.required ? "Required" : "Optional"
245
282
  }. ${arg.description}`,
@@ -11,11 +11,15 @@ const ComponentPortViewer: FunctionComponent<ComponentProps> = (
11
11
  props: ComponentProps,
12
12
  ): ReactElement => {
13
13
  return (
14
- <div className="mt-3 mb-3">
15
- <h2 className="text-sm font-semibold text-gray-600">{props.name}</h2>
16
- <p className="text-xs text-gray-400 mb-2">{props.description}</p>
14
+ <div>
15
+ {props.name && (
16
+ <h2 className="text-sm font-semibold text-gray-600">{props.name}</h2>
17
+ )}
18
+ {props.description && (
19
+ <p className="text-xs text-gray-400 mb-2">{props.description}</p>
20
+ )}
17
21
  {props.ports && props.ports.length === 0 && (
18
- <p className="text-xs text-gray-400 italic">No ports configured.</p>
22
+ <p className="text-xs text-gray-400 italic">No connections.</p>
19
23
  )}
20
24
  <div>
21
25
  {props.ports &&
@@ -11,12 +11,16 @@ const ComponentReturnValueViewer: FunctionComponent<ComponentProps> = (
11
11
  props: ComponentProps,
12
12
  ): ReactElement => {
13
13
  return (
14
- <div className="mt-3 mb-3">
15
- <h2 className="text-sm font-semibold text-gray-600">{props.name}</h2>
16
- <p className="text-xs text-gray-400 mb-2">{props.description}</p>
14
+ <div>
15
+ {props.name && (
16
+ <h2 className="text-sm font-semibold text-gray-600">{props.name}</h2>
17
+ )}
18
+ {props.description && (
19
+ <p className="text-xs text-gray-400 mb-2">{props.description}</p>
20
+ )}
17
21
  {props.returnValues && props.returnValues.length === 0 && (
18
22
  <p className="text-xs text-gray-400 italic">
19
- This component does not return any values.
23
+ This step does not return any data.
20
24
  </p>
21
25
  )}
22
26
  <div>
@@ -3,7 +3,7 @@ import BasicForm from "../Forms/BasicForm";
3
3
  import FormFieldSchemaType from "../Forms/Types/FormFieldSchemaType";
4
4
  import FormValues from "../Forms/Types/FormValues";
5
5
  import ConfirmModal from "../Modal/ConfirmModal";
6
- import SideOver from "../SideOver/SideOver";
6
+ import Modal, { ModalWidth } from "../Modal/Modal";
7
7
  import ArgumentsForm from "./ArgumentsForm";
8
8
  import ComponentPortViewer from "./ComponentPortViewer";
9
9
  import ComponentReturnValueViewer from "./ComponentReturnValueViewer";
@@ -28,6 +28,36 @@ export interface ComponentProps {
28
28
  webhookSecretKey?: string | undefined;
29
29
  }
30
30
 
31
+ interface SectionCardProps {
32
+ icon: IconProp;
33
+ title: string;
34
+ children: ReactElement | Array<ReactElement>;
35
+ tone?: "default" | "info" | undefined;
36
+ }
37
+
38
+ const SectionCard: FunctionComponent<SectionCardProps> = (
39
+ props: SectionCardProps,
40
+ ): ReactElement => {
41
+ const isInfo: boolean = props.tone === "info";
42
+ const containerClass: string = isInfo
43
+ ? "rounded-lg border border-blue-100 bg-blue-50/40 p-4"
44
+ : "rounded-lg border border-gray-200 bg-white p-4";
45
+ const iconClass: string = isInfo ? "text-blue-500" : "text-gray-400";
46
+ const titleClass: string = isInfo
47
+ ? "text-[11px] font-semibold uppercase tracking-wider text-blue-700"
48
+ : "text-[11px] font-semibold uppercase tracking-wider text-gray-500";
49
+
50
+ return (
51
+ <div className={containerClass}>
52
+ <div className="flex items-center gap-1.5 mb-3">
53
+ <Icon icon={props.icon} className={`h-3.5 w-3.5 ${iconClass}`} />
54
+ <span className={titleClass}>{props.title}</span>
55
+ </div>
56
+ {props.children}
57
+ </div>
58
+ );
59
+ };
60
+
31
61
  const ComponentSettingsModal: FunctionComponent<ComponentProps> = (
32
62
  props: ComponentProps,
33
63
  ): ReactElement => {
@@ -38,17 +68,128 @@ const ComponentSettingsModal: FunctionComponent<ComponentProps> = (
38
68
  const [showDeleteConfirmation, setShowDeleteConfirmation] =
39
69
  useState<boolean>(false);
40
70
 
71
+ const settingsSection: ReactElement = (
72
+ <SectionCard icon={IconProp.Settings} title="Settings">
73
+ <ArgumentsForm
74
+ graphComponents={props.graphComponents}
75
+ workflowId={props.workflowId}
76
+ component={component}
77
+ onFormChange={(c: NodeDataProp) => {
78
+ setComponent({ ...c });
79
+ }}
80
+ onHasFormValidationErrors={(value: Dictionary<boolean>) => {
81
+ setHasFormValidationErrors({
82
+ ...hasFormValidationErrors,
83
+ ...value,
84
+ });
85
+ }}
86
+ />
87
+ </SectionCard>
88
+ );
89
+
90
+ const idSection: ReactElement = (
91
+ <SectionCard icon={IconProp.Label} title="ID">
92
+ <BasicForm
93
+ hideSubmitButton={true}
94
+ initialValues={{ id: component?.id }}
95
+ onChange={(values: FormValues<JSONObject>) => {
96
+ setComponent({ ...component, ...values });
97
+ }}
98
+ onFormValidationErrorChanged={(hasError: boolean) => {
99
+ setHasFormValidationErrors({
100
+ ...hasFormValidationErrors,
101
+ id: hasError,
102
+ });
103
+ }}
104
+ fields={[
105
+ {
106
+ title: "Identifier",
107
+ description: `Used to reference this ${component.metadata.componentType.toLowerCase()} from other steps.`,
108
+ field: { id: true },
109
+ required: true,
110
+ fieldType: FormFieldSchemaType.Text,
111
+ },
112
+ ]}
113
+ />
114
+ </SectionCard>
115
+ );
116
+
117
+ const documentationSection: ReactElement | null = component.metadata
118
+ .documentationLink ? (
119
+ <SectionCard icon={IconProp.Book} title="Documentation" tone="info">
120
+ <DocumentationViewer
121
+ documentationLink={component.metadata.documentationLink}
122
+ workflowId={props.workflowId}
123
+ webhookSecretKey={props.webhookSecretKey}
124
+ />
125
+ </SectionCard>
126
+ ) : null;
127
+
128
+ /*
129
+ * Each connection/output card is only rendered if there's something to
130
+ * show — keeps the sidebar lean for triggers (no inputs) and components
131
+ * that don't return any data.
132
+ */
133
+ const hasInputs: boolean =
134
+ Array.isArray(component.metadata.inPorts) &&
135
+ component.metadata.inPorts.length > 0;
136
+ const hasOutputs: boolean =
137
+ Array.isArray(component.metadata.outPorts) &&
138
+ component.metadata.outPorts.length > 0;
139
+ const hasReturns: boolean =
140
+ Array.isArray(component.metadata.returnValues) &&
141
+ component.metadata.returnValues.length > 0;
142
+
143
+ const inputsSection: ReactElement | null = hasInputs ? (
144
+ <SectionCard icon={IconProp.ArrowCircleDown} title="Inputs">
145
+ <ComponentPortViewer
146
+ name=""
147
+ description="Where this step is reached from."
148
+ ports={component.metadata.inPorts}
149
+ />
150
+ </SectionCard>
151
+ ) : null;
152
+
153
+ const outputsSection: ReactElement | null = hasOutputs ? (
154
+ <SectionCard icon={IconProp.ArrowCircleRight} title="Outputs">
155
+ <ComponentPortViewer
156
+ name=""
157
+ description="What runs after this step."
158
+ ports={component.metadata.outPorts}
159
+ />
160
+ </SectionCard>
161
+ ) : null;
162
+
163
+ const returnsSection: ReactElement | null = hasReturns ? (
164
+ <SectionCard icon={IconProp.Database} title="Returns">
165
+ <ComponentReturnValueViewer
166
+ name=""
167
+ description="Data this step makes available downstream."
168
+ returnValues={component.metadata.returnValues}
169
+ />
170
+ </SectionCard>
171
+ ) : null;
172
+
173
+ const hasErrors: boolean = Object.values(hasFormValidationErrors).some(
174
+ (v: boolean) => {
175
+ return v;
176
+ },
177
+ );
178
+
41
179
  return (
42
- <SideOver
180
+ <Modal
43
181
  title={props.title}
44
182
  description={props.description}
45
183
  onClose={props.onClose}
46
184
  onSubmit={() => {
47
185
  return component && props.onSave(component);
48
186
  }}
187
+ submitButtonText="Save"
188
+ modalWidth={ModalWidth.Large}
189
+ disableSubmitButton={hasErrors}
49
190
  leftFooterElement={
50
191
  <Button
51
- title={`Delete`}
192
+ title="Delete"
52
193
  icon={IconProp.Trash}
53
194
  buttonStyle={ButtonStyleType.DANGER_OUTLINE}
54
195
  onClick={() => {
@@ -65,7 +206,7 @@ const ComponentSettingsModal: FunctionComponent<ComponentProps> = (
65
206
  onClose={() => {
66
207
  setShowDeleteConfirmation(false);
67
208
  }}
68
- submitButtonText={"Delete"}
209
+ submitButtonText="Delete"
69
210
  onSubmit={() => {
70
211
  props.onDelete(component);
71
212
  setShowDeleteConfirmation(false);
@@ -75,260 +216,23 @@ const ComponentSettingsModal: FunctionComponent<ComponentProps> = (
75
216
  />
76
217
  )}
77
218
 
78
- {/* Component ID Section */}
79
- <div
80
- style={{
81
- backgroundColor: "#f8fafc",
82
- borderRadius: "10px",
83
- border: "1px solid #e2e8f0",
84
- padding: "1rem",
85
- marginTop: "0.75rem",
86
- marginBottom: "1rem",
87
- }}
88
- >
89
- <div
90
- style={{
91
- display: "flex",
92
- alignItems: "center",
93
- gap: "0.5rem",
94
- marginBottom: "0.5rem",
95
- }}
96
- >
97
- <Icon
98
- icon={IconProp.Label}
99
- style={{
100
- color: "#64748b",
101
- width: "0.875rem",
102
- height: "0.875rem",
103
- }}
104
- />
105
- <span
106
- style={{
107
- fontSize: "0.8125rem",
108
- fontWeight: 600,
109
- color: "#334155",
110
- }}
111
- >
112
- Identity
113
- </span>
219
+ {/*
220
+ * Two-column layout: arguments take the main column (2/3 width on
221
+ * md+), metadata sits in a narrower sidebar. Collapses to one
222
+ * column below md.
223
+ */}
224
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
225
+ <div className="md:col-span-2 space-y-4">{settingsSection}</div>
226
+ <div className="md:col-span-1 space-y-4">
227
+ {idSection}
228
+ {documentationSection}
229
+ {inputsSection}
230
+ {outputsSection}
231
+ {returnsSection}
114
232
  </div>
115
- <BasicForm
116
- hideSubmitButton={true}
117
- initialValues={{
118
- id: component?.id,
119
- }}
120
- onChange={(values: FormValues<JSONObject>) => {
121
- setComponent({ ...component, ...values });
122
- }}
123
- onFormValidationErrorChanged={(hasError: boolean) => {
124
- setHasFormValidationErrors({
125
- ...hasFormValidationErrors,
126
- id: hasError,
127
- });
128
- }}
129
- fields={[
130
- {
131
- title: `${component.metadata.componentType} ID`,
132
- description: `Unique identifier used to reference this ${component.metadata.componentType.toLowerCase()} from other components.`,
133
- field: {
134
- id: true,
135
- },
136
- required: true,
137
- fieldType: FormFieldSchemaType.Text,
138
- },
139
- ]}
140
- />
141
- </div>
142
-
143
- {/* Documentation Section */}
144
- {component.metadata.documentationLink && (
145
- <div
146
- style={{
147
- backgroundColor: "#eff6ff",
148
- borderRadius: "10px",
149
- border: "1px solid #bfdbfe",
150
- padding: "1rem",
151
- marginBottom: "1rem",
152
- }}
153
- >
154
- <div
155
- style={{
156
- display: "flex",
157
- alignItems: "center",
158
- gap: "0.5rem",
159
- marginBottom: "0.5rem",
160
- }}
161
- >
162
- <Icon
163
- icon={IconProp.Book}
164
- style={{
165
- color: "#3b82f6",
166
- width: "0.875rem",
167
- height: "0.875rem",
168
- }}
169
- />
170
- <span
171
- style={{
172
- fontSize: "0.8125rem",
173
- fontWeight: 600,
174
- color: "#1e40af",
175
- }}
176
- >
177
- Documentation
178
- </span>
179
- </div>
180
- <DocumentationViewer
181
- documentationLink={component.metadata.documentationLink}
182
- workflowId={props.workflowId}
183
- webhookSecretKey={props.webhookSecretKey}
184
- />
185
- </div>
186
- )}
187
-
188
- {/* Arguments Section */}
189
- <div
190
- style={{
191
- backgroundColor: "#ffffff",
192
- borderRadius: "10px",
193
- border: "1px solid #e2e8f0",
194
- padding: "1rem",
195
- marginBottom: "1rem",
196
- }}
197
- >
198
- <div
199
- style={{
200
- display: "flex",
201
- alignItems: "center",
202
- gap: "0.5rem",
203
- marginBottom: "0.75rem",
204
- }}
205
- >
206
- <Icon
207
- icon={IconProp.Settings}
208
- style={{
209
- color: "#64748b",
210
- width: "0.875rem",
211
- height: "0.875rem",
212
- }}
213
- />
214
- <span
215
- style={{
216
- fontSize: "0.8125rem",
217
- fontWeight: 600,
218
- color: "#334155",
219
- }}
220
- >
221
- Configuration
222
- </span>
223
- </div>
224
- <ArgumentsForm
225
- graphComponents={props.graphComponents}
226
- workflowId={props.workflowId}
227
- component={component}
228
- onFormChange={(component: NodeDataProp) => {
229
- setComponent({ ...component });
230
- }}
231
- onHasFormValidationErrors={(value: Dictionary<boolean>) => {
232
- setHasFormValidationErrors({
233
- ...hasFormValidationErrors,
234
- ...value,
235
- });
236
- }}
237
- />
238
- </div>
239
-
240
- {/* Ports Section */}
241
- <div
242
- style={{
243
- backgroundColor: "#ffffff",
244
- borderRadius: "10px",
245
- border: "1px solid #e2e8f0",
246
- padding: "1rem",
247
- marginBottom: "1rem",
248
- }}
249
- >
250
- <div
251
- style={{
252
- display: "flex",
253
- alignItems: "center",
254
- gap: "0.5rem",
255
- marginBottom: "0.25rem",
256
- }}
257
- >
258
- <Icon
259
- icon={IconProp.Link}
260
- style={{
261
- color: "#64748b",
262
- width: "0.875rem",
263
- height: "0.875rem",
264
- }}
265
- />
266
- <span
267
- style={{
268
- fontSize: "0.8125rem",
269
- fontWeight: 600,
270
- color: "#334155",
271
- }}
272
- >
273
- Connections
274
- </span>
275
- </div>
276
- <ComponentPortViewer
277
- name="In Ports"
278
- description="Input connections for this component"
279
- ports={component.metadata.inPorts}
280
- />
281
- <ComponentPortViewer
282
- name="Out Ports"
283
- description="Output connections from this component"
284
- ports={component.metadata.outPorts}
285
- />
286
- </div>
287
-
288
- {/* Return Values Section */}
289
- <div
290
- style={{
291
- backgroundColor: "#ffffff",
292
- borderRadius: "10px",
293
- border: "1px solid #e2e8f0",
294
- padding: "1rem",
295
- marginBottom: "1rem",
296
- }}
297
- >
298
- <div
299
- style={{
300
- display: "flex",
301
- alignItems: "center",
302
- gap: "0.5rem",
303
- marginBottom: "0.25rem",
304
- }}
305
- >
306
- <Icon
307
- icon={IconProp.ArrowCircleRight}
308
- style={{
309
- color: "#64748b",
310
- width: "0.875rem",
311
- height: "0.875rem",
312
- }}
313
- />
314
- <span
315
- style={{
316
- fontSize: "0.8125rem",
317
- fontWeight: 600,
318
- color: "#334155",
319
- }}
320
- >
321
- Output
322
- </span>
323
- </div>
324
- <ComponentReturnValueViewer
325
- name="Return Values"
326
- description="Values this component produces for downstream use"
327
- returnValues={component.metadata.returnValues}
328
- />
329
233
  </div>
330
234
  </>
331
- </SideOver>
235
+ </Modal>
332
236
  );
333
237
  };
334
238