@oneuptime/common 11.5.13 → 11.6.1

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 (156) hide show
  1. package/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.ts +41 -24
  2. package/Models/AnalyticsModels/MetricBaselineHourly.ts +2 -0
  3. package/Models/AnalyticsModels/MetricItemAggMV1m.ts +5 -2
  4. package/Models/AnalyticsModels/MetricItemAggMV1mByContainer.ts +5 -2
  5. package/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.ts +5 -2
  6. package/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +5 -2
  7. package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +5 -2
  8. package/Models/DatabaseModels/EnterpriseLicense.ts +21 -0
  9. package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +25 -0
  10. package/Models/DatabaseModels/GlobalConfig.ts +76 -0
  11. package/Server/API/EnterpriseLicenseAPI.ts +57 -0
  12. package/Server/API/GlobalConfigAPI.ts +62 -1
  13. package/Server/EnvironmentConfig.ts +22 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.ts +38 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.ts +25 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  17. package/Server/Services/AnalyticsDatabaseService.ts +61 -46
  18. package/Server/Services/DatabaseService.ts +66 -1
  19. package/Server/Services/WorkflowService.ts +100 -39
  20. package/Server/Types/Workflow/Components/Schedule.ts +57 -32
  21. package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +59 -0
  22. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +41 -4
  23. package/Server/Utils/Express.ts +13 -0
  24. package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +799 -0
  25. package/Server/Utils/Telemetry/TelemetryWriterClient.ts +263 -0
  26. package/Server/Utils/Telemetry/TelemetryWriterServer.ts +240 -0
  27. package/Server/Utils/Telemetry/TelemetryWriterShedMetrics.ts +93 -0
  28. package/Tests/Models/AnalyticsModels/AnalyticsBaseModel.test.ts +122 -0
  29. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +232 -0
  30. package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +107 -0
  31. package/Tests/Server/Services/WorkflowService.test.ts +308 -0
  32. package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +164 -3
  33. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +99 -0
  34. package/Tests/Server/Utils/Attribution.test.ts +203 -0
  35. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +183 -0
  36. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +340 -0
  37. package/Tests/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.test.ts +87 -0
  38. package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +165 -0
  39. package/Tests/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.test.ts +86 -0
  40. package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +1565 -0
  41. package/Tests/Server/Utils/Telemetry/TelemetryWriterClient.test.ts +313 -0
  42. package/Tests/Server/Utils/Telemetry/TelemetryWriterServer.test.ts +270 -0
  43. package/Tests/Server/Utils/Telemetry/TelemetryWriterShedMetrics.test.ts +127 -0
  44. package/Tests/Types/AI/CodeFixTaskType.test.ts +3 -0
  45. package/Tests/Types/HashCode.test.ts +41 -0
  46. package/Tests/Types/Log/LogQueryParser.test.ts +186 -0
  47. package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +228 -0
  48. package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +169 -0
  49. package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +289 -0
  50. package/Tests/Types/Monitor/MonitorStepLogMonitor.test.ts +231 -0
  51. package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +245 -0
  52. package/Tests/Types/Monitor/MonitorStepNetworkDeviceMonitor.test.ts +155 -0
  53. package/Tests/Types/Monitor/MonitorStepTelemetrySerialization.test.ts +141 -0
  54. package/Tests/Types/Monitor/MonitorStepTraceMonitor.test.ts +137 -0
  55. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +311 -0
  56. package/Tests/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.test.ts +461 -0
  57. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +2852 -0
  58. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +758 -0
  59. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +564 -0
  60. package/Tests/Utils/Array.test.ts +156 -0
  61. package/Tests/Utils/CronTab.test.ts +199 -0
  62. package/Tests/Utils/Dashboard/VariableInterpolation.test.ts +197 -0
  63. package/Tests/Utils/ModelImportExport.test.ts +101 -0
  64. package/Tests/Utils/Number.test.ts +179 -0
  65. package/Tests/Utils/ValueFormatter.test.ts +305 -0
  66. package/Tests/Utils/VersionUtil.test.ts +348 -0
  67. package/Tests/jest.setup.ts +1 -0
  68. package/Types/AnalyticsDatabase/TableColumn.ts +23 -0
  69. package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +6 -0
  70. package/Types/Monitor/MonitorStep.ts +24 -4
  71. package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +105 -0
  72. package/UI/Components/EditionLabel/EditionLabel.tsx +445 -12
  73. package/UI/Components/Workflow/ArgumentsForm.tsx +68 -30
  74. package/UI/Components/Workflow/CronScheduleField.tsx +411 -0
  75. package/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.ts +176 -0
  76. package/UI/Utils/NotificationMethodUtil.ts +310 -0
  77. package/Utils/CronTab.ts +823 -0
  78. package/Utils/ModelImportExport.ts +21 -6
  79. package/Utils/VersionUtil.ts +260 -0
  80. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js +25 -20
  81. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js.map +1 -1
  82. package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js +2 -0
  83. package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js.map +1 -1
  84. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js +5 -2
  85. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js.map +1 -1
  86. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js +5 -2
  87. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js.map +1 -1
  88. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js +5 -2
  89. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js.map +1 -1
  90. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +5 -2
  91. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js.map +1 -1
  92. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +5 -2
  93. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +22 -0
  95. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +26 -0
  97. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/GlobalConfig.js +81 -0
  99. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  100. package/build/dist/Server/API/EnterpriseLicenseAPI.js +47 -0
  101. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  102. package/build/dist/Server/API/GlobalConfigAPI.js +52 -1
  103. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  104. package/build/dist/Server/EnvironmentConfig.js +17 -0
  105. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js +18 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js +14 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  112. package/build/dist/Server/Services/AnalyticsDatabaseService.js +53 -17
  113. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  114. package/build/dist/Server/Services/DatabaseService.js +53 -1
  115. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  116. package/build/dist/Server/Services/WorkflowService.js +80 -30
  117. package/build/dist/Server/Services/WorkflowService.js.map +1 -1
  118. package/build/dist/Server/Types/Workflow/Components/Schedule.js +44 -19
  119. package/build/dist/Server/Types/Workflow/Components/Schedule.js.map +1 -1
  120. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js +24 -0
  121. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -0
  122. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +29 -4
  123. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  124. package/build/dist/Server/Utils/Express.js +12 -0
  125. package/build/dist/Server/Utils/Express.js.map +1 -1
  126. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +496 -0
  127. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -0
  128. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js +165 -0
  129. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js.map +1 -0
  130. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js +136 -0
  131. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js.map +1 -0
  132. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js +76 -0
  133. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js.map +1 -0
  134. package/build/dist/Types/AnalyticsDatabase/TableColumn.js +7 -0
  135. package/build/dist/Types/AnalyticsDatabase/TableColumn.js.map +1 -1
  136. package/build/dist/Types/Monitor/MonitorStep.js +20 -4
  137. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  138. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +73 -0
  139. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -0
  140. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +261 -13
  141. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  142. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +30 -6
  143. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  144. package/build/dist/UI/Components/Workflow/CronScheduleField.js +209 -0
  145. package/build/dist/UI/Components/Workflow/CronScheduleField.js.map +1 -0
  146. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js +129 -0
  147. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js.map +1 -0
  148. package/build/dist/UI/Utils/NotificationMethodUtil.js +189 -0
  149. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -0
  150. package/build/dist/Utils/CronTab.js +609 -0
  151. package/build/dist/Utils/CronTab.js.map +1 -0
  152. package/build/dist/Utils/ModelImportExport.js +20 -6
  153. package/build/dist/Utils/ModelImportExport.js.map +1 -1
  154. package/build/dist/Utils/VersionUtil.js +193 -0
  155. package/build/dist/Utils/VersionUtil.js.map +1 -0
  156. package/package.json +1 -1
@@ -5,6 +5,7 @@ import FormFieldSchemaType from "../Forms/Types/FormFieldSchemaType";
5
5
  import { CustomElementProps } from "../Forms/Types/Field";
6
6
  import FormValues from "../Forms/Types/FormValues";
7
7
  import ComponentValuePickerModal from "./ComponentValuePickerModal";
8
+ import CronScheduleField from "./CronScheduleField";
8
9
  import ModelFieldPicker from "./ModelFieldPicker";
9
10
  import { componentInputTypeToFormFieldType } from "./Utils";
10
11
  import VariableModal from "./VariableModal";
@@ -198,40 +199,75 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
198
199
  arg.type === ComponentInputType.Select &&
199
200
  Boolean(component.metadata.tableName);
200
201
 
201
- const baseField: {
202
+ /*
203
+ * The Schedule trigger's CronTab argument gets a dedicated
204
+ * cron picker (presets, custom expression with live preview,
205
+ * or a variable reference) instead of a bare dropdown.
206
+ */
207
+ const useCronPicker: boolean =
208
+ arg.type === ComponentInputType.CronTab;
209
+
210
+ let baseField: {
202
211
  fieldType: import("../Forms/Types/FormFieldSchemaType").default;
203
212
  dropdownOptions?: Array<DropdownOption> | undefined;
204
213
  getCustomElement?: (
205
214
  values: FormValues<JSONObject>,
206
215
  customProps: CustomElementProps,
207
216
  ) => 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
- );
217
+ };
218
+
219
+ if (useFieldPicker) {
220
+ baseField = {
221
+ fieldType: FormFieldSchemaType.CustomComponent,
222
+ getCustomElement: (
223
+ _values: FormValues<JSONObject>,
224
+ customProps: CustomElementProps,
225
+ ): ReactElement => {
226
+ return (
227
+ <ModelFieldPicker
228
+ tableName={component.metadata.tableName as string}
229
+ initialValue={customProps.initialValue}
230
+ onChange={(value: string) => {
231
+ void customProps.onChange?.(value);
232
+ }}
233
+ placeholder={customProps.placeholder}
234
+ error={customProps.error}
235
+ tabIndex={customProps.tabIndex}
236
+ />
237
+ );
238
+ },
239
+ };
240
+ } else if (useCronPicker) {
241
+ baseField = {
242
+ fieldType: FormFieldSchemaType.CustomComponent,
243
+ getCustomElement: (
244
+ _values: FormValues<JSONObject>,
245
+ customProps: CustomElementProps,
246
+ ): ReactElement => {
247
+ return (
248
+ <CronScheduleField
249
+ workflowId={props.workflowId}
250
+ initialValue={
251
+ customProps.initialValue as string | null
252
+ }
253
+ onChange={(value: string) => {
254
+ void customProps.onChange?.(value);
255
+ }}
256
+ placeholder={customProps.placeholder}
257
+ error={customProps.error}
258
+ tabIndex={customProps.tabIndex}
259
+ />
260
+ );
261
+ },
262
+ };
263
+ } else {
264
+ baseField = componentInputTypeToFormFieldType(
265
+ arg.type,
266
+ component.arguments && component.arguments[arg.id]
267
+ ? component.arguments[arg.id]
268
+ : null,
269
+ );
270
+ }
235
271
 
236
272
  /*
237
273
  * For WorkflowSelect, inject the dynamically fetched list
@@ -244,10 +280,12 @@ const ArgumentsForm: FunctionComponent<ComponentProps> = (
244
280
  /*
245
281
  * The "pick from component / variable" footer doesn't
246
282
  * apply to the field picker (it edits a structured object,
247
- * not a free-text expression) or to WorkflowSelect.
283
+ * not a free-text expression), to WorkflowSelect, or to the
284
+ * cron picker (which has its own built-in variable selector,
285
+ * and a schedule can't reference component return values).
248
286
  */
249
287
  const showVariableFooter: boolean =
250
- !isWorkflowSelect && !useFieldPicker;
288
+ !isWorkflowSelect && !useFieldPicker && !useCronPicker;
251
289
 
252
290
  return {
253
291
  title: `${arg.name}`,
@@ -0,0 +1,411 @@
1
+ import Dropdown, { DropdownOption, DropdownValue } from "../Dropdown/Dropdown";
2
+ import Icon from "../Icon/Icon";
3
+ import VariableModal from "./VariableModal";
4
+ import IconProp from "../../../Types/Icon/IconProp";
5
+ import ObjectID from "../../../Types/ObjectID";
6
+ import CronTab from "../../../Utils/CronTab";
7
+ import React, {
8
+ FunctionComponent,
9
+ ReactElement,
10
+ useMemo,
11
+ useState,
12
+ } from "react";
13
+
14
+ export interface ComponentProps {
15
+ workflowId: ObjectID;
16
+ initialValue?: string | null | undefined;
17
+ onChange: (value: string) => void;
18
+ error?: string | undefined;
19
+ placeholder?: string | undefined;
20
+ tabIndex?: number | undefined;
21
+ }
22
+
23
+ type ScheduleMode = "preset" | "custom" | "variable";
24
+
25
+ const PRESET_OPTIONS: Array<DropdownOption> = CronTab.PRESETS.map(
26
+ (preset: { label: string; value: string }) => {
27
+ return {
28
+ label: preset.label,
29
+ value: preset.value,
30
+ };
31
+ },
32
+ );
33
+
34
+ const PRESET_VALUES: Set<string> = new Set(
35
+ CronTab.PRESETS.map((preset: { value: string }) => {
36
+ return preset.value;
37
+ }),
38
+ );
39
+
40
+ const normalizeInitialValue: (value: string | null | undefined) => string = (
41
+ value: string | null | undefined,
42
+ ): string => {
43
+ if (value === null || value === undefined) {
44
+ return "";
45
+ }
46
+ return `${value}`;
47
+ };
48
+
49
+ const inferInitialMode: (value: string) => ScheduleMode = (
50
+ value: string,
51
+ ): ScheduleMode => {
52
+ const trimmed: string = value.trim();
53
+
54
+ if (CronTab.isVariableExpression(trimmed)) {
55
+ return "variable";
56
+ }
57
+
58
+ if (trimmed !== "" && !PRESET_VALUES.has(trimmed)) {
59
+ return "custom";
60
+ }
61
+
62
+ return "preset";
63
+ };
64
+
65
+ const formatUtc: (date: Date) => string = (date: Date): string => {
66
+ try {
67
+ return (
68
+ new Intl.DateTimeFormat("en-US", {
69
+ timeZone: "UTC",
70
+ weekday: "short",
71
+ year: "numeric",
72
+ month: "short",
73
+ day: "numeric",
74
+ hour: "2-digit",
75
+ minute: "2-digit",
76
+ hour12: false,
77
+ }).format(date) + " UTC"
78
+ );
79
+ } catch {
80
+ return date.toUTCString();
81
+ }
82
+ };
83
+
84
+ /**
85
+ * A rich picker for the workflow "Schedule" trigger. It lets the user choose a
86
+ * common schedule from a preset list, write any custom cron expression (with a
87
+ * live human-readable description, next-run preview and validation), or point
88
+ * the schedule at a workflow / global variable that resolves to a cron
89
+ * expression when the workflow is saved.
90
+ */
91
+ const CronScheduleField: FunctionComponent<ComponentProps> = (
92
+ props: ComponentProps,
93
+ ): ReactElement => {
94
+ const initialValue: string = useMemo(() => {
95
+ return normalizeInitialValue(props.initialValue);
96
+ }, []);
97
+
98
+ const [value, setValue] = useState<string>(initialValue);
99
+ const [mode, setMode] = useState<ScheduleMode>(
100
+ inferInitialMode(initialValue),
101
+ );
102
+ const [showVariableModal, setShowVariableModal] = useState<boolean>(false);
103
+
104
+ const emit: (next: string) => void = (next: string): void => {
105
+ setValue(next);
106
+ props.onChange(next);
107
+ };
108
+
109
+ const trimmedValue: string = value.trim();
110
+ const isVariable: boolean = CronTab.isVariableExpression(trimmedValue);
111
+
112
+ const validationError: string | null = useMemo(() => {
113
+ if (trimmedValue === "" || isVariable) {
114
+ return null;
115
+ }
116
+ return CronTab.getValidationError(trimmedValue);
117
+ }, [trimmedValue, isVariable]);
118
+
119
+ const description: string | null = useMemo(() => {
120
+ if (trimmedValue === "" || isVariable || validationError) {
121
+ return null;
122
+ }
123
+ return CronTab.getHumanReadableDescription(trimmedValue);
124
+ }, [trimmedValue, isVariable, validationError]);
125
+
126
+ const nextRuns: Array<Date> = useMemo(() => {
127
+ if (trimmedValue === "" || isVariable || validationError) {
128
+ return [];
129
+ }
130
+ try {
131
+ return CronTab.getNextExecutionTimes(trimmedValue, 3, new Date());
132
+ } catch {
133
+ return [];
134
+ }
135
+ }, [trimmedValue, isVariable, validationError]);
136
+
137
+ const selectedPresetOption: DropdownOption | undefined = PRESET_OPTIONS.find(
138
+ (option: DropdownOption) => {
139
+ return option.value === trimmedValue;
140
+ },
141
+ );
142
+
143
+ const renderModeTab: (args: {
144
+ tabMode: ScheduleMode;
145
+ label: string;
146
+ icon: IconProp;
147
+ }) => ReactElement = (args: {
148
+ tabMode: ScheduleMode;
149
+ label: string;
150
+ icon: IconProp;
151
+ }): ReactElement => {
152
+ const isActive: boolean = mode === args.tabMode;
153
+ return (
154
+ <button
155
+ type="button"
156
+ onClick={() => {
157
+ setMode(args.tabMode);
158
+ }}
159
+ className={`inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
160
+ isActive
161
+ ? "bg-white text-indigo-700 shadow-sm ring-1 ring-gray-200"
162
+ : "text-gray-500 hover:text-gray-700"
163
+ }`}
164
+ >
165
+ <Icon
166
+ icon={args.icon}
167
+ className={`h-3.5 w-3.5 ${
168
+ isActive ? "text-indigo-600" : "text-gray-400"
169
+ }`}
170
+ />
171
+ {args.label}
172
+ </button>
173
+ );
174
+ };
175
+
176
+ const renderPreview: () => ReactElement | null = (): ReactElement | null => {
177
+ if (isVariable) {
178
+ return null;
179
+ }
180
+
181
+ if (validationError) {
182
+ return (
183
+ <div className="flex items-start gap-2 rounded-md border border-red-200 bg-red-50/60 px-3 py-2 text-xs text-red-800">
184
+ <Icon
185
+ icon={IconProp.Error}
186
+ className="h-4 w-4 text-red-500 mt-0.5 flex-shrink-0"
187
+ />
188
+ <div>
189
+ <p className="font-semibold">Not a valid cron expression</p>
190
+ <p className="text-red-700">{validationError}</p>
191
+ </div>
192
+ </div>
193
+ );
194
+ }
195
+
196
+ if (!description) {
197
+ return null;
198
+ }
199
+
200
+ return (
201
+ <div className="rounded-md border border-gray-200 bg-gray-50/60 px-3 py-2.5">
202
+ <div className="flex items-start gap-2">
203
+ <Icon
204
+ icon={IconProp.Clock}
205
+ className="h-4 w-4 text-indigo-500 mt-0.5 flex-shrink-0"
206
+ />
207
+ <div className="min-w-0">
208
+ <p className="text-sm font-medium text-gray-800">{description}</p>
209
+ {nextRuns.length > 0 && (
210
+ <div className="mt-1.5">
211
+ <p className="text-[10px] font-semibold uppercase tracking-wider text-gray-400">
212
+ Next runs
213
+ </p>
214
+ <ul className="mt-0.5 space-y-0.5">
215
+ {nextRuns.map((run: Date, index: number) => {
216
+ return (
217
+ <li
218
+ key={index}
219
+ className="text-xs text-gray-600 font-mono"
220
+ >
221
+ {formatUtc(run)}
222
+ </li>
223
+ );
224
+ })}
225
+ </ul>
226
+ </div>
227
+ )}
228
+ </div>
229
+ </div>
230
+ </div>
231
+ );
232
+ };
233
+
234
+ return (
235
+ <div className="space-y-3">
236
+ {/* Mode selector */}
237
+ <div className="inline-flex items-center gap-1 rounded-lg bg-gray-100 p-1">
238
+ {renderModeTab({
239
+ tabMode: "preset",
240
+ label: "Common schedules",
241
+ icon: IconProp.Clock,
242
+ })}
243
+ {renderModeTab({
244
+ tabMode: "custom",
245
+ label: "Custom cron",
246
+ icon: IconProp.Code,
247
+ })}
248
+ {renderModeTab({
249
+ tabMode: "variable",
250
+ label: "Variable",
251
+ icon: IconProp.Variable,
252
+ })}
253
+ </div>
254
+
255
+ {mode === "preset" && (
256
+ <div className="space-y-3">
257
+ <Dropdown
258
+ options={PRESET_OPTIONS}
259
+ value={selectedPresetOption}
260
+ placeholder="Select a schedule"
261
+ tabIndex={props.tabIndex}
262
+ onChange={(
263
+ newValue: DropdownValue | Array<DropdownValue> | null,
264
+ ) => {
265
+ if (typeof newValue === "string") {
266
+ emit(newValue);
267
+ }
268
+ }}
269
+ />
270
+ {!selectedPresetOption && trimmedValue !== "" && !isVariable && (
271
+ <p className="text-xs text-gray-500">
272
+ Your current schedule{" "}
273
+ <code className="rounded bg-gray-100 px-1 py-0.5 font-mono text-gray-700">
274
+ {trimmedValue}
275
+ </code>{" "}
276
+ is not one of the presets. Switch to{" "}
277
+ <button
278
+ type="button"
279
+ className="text-indigo-600 underline hover:text-indigo-700"
280
+ onClick={() => {
281
+ setMode("custom");
282
+ }}
283
+ >
284
+ Custom cron
285
+ </button>{" "}
286
+ to edit it.
287
+ </p>
288
+ )}
289
+ {renderPreview()}
290
+ </div>
291
+ )}
292
+
293
+ {mode === "custom" && (
294
+ <div className="space-y-3">
295
+ <input
296
+ type="text"
297
+ value={value}
298
+ spellCheck={false}
299
+ autoComplete="off"
300
+ tabIndex={props.tabIndex}
301
+ placeholder={props.placeholder || "e.g. 0 */18 * * *"}
302
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
303
+ emit(event.target.value);
304
+ }}
305
+ className={`block w-full rounded-md border bg-white px-3 py-2 font-mono text-sm placeholder-gray-400 focus:outline-none focus:ring-1 ${
306
+ validationError
307
+ ? "border-red-300 focus:border-red-500 focus:ring-red-500"
308
+ : "border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
309
+ }`}
310
+ />
311
+ <p className="text-xs text-gray-400 font-mono">
312
+ minute hour day-of-month month day-of-week
313
+ </p>
314
+ {renderPreview()}
315
+ </div>
316
+ )}
317
+
318
+ {mode === "variable" && (
319
+ <div className="space-y-3">
320
+ {isVariable ? (
321
+ <div className="flex items-center justify-between gap-3 rounded-md border border-indigo-200 bg-indigo-50/50 px-3 py-2">
322
+ <div className="flex items-center gap-2 min-w-0">
323
+ <Icon
324
+ icon={IconProp.Variable}
325
+ className="h-4 w-4 text-indigo-500 flex-shrink-0"
326
+ />
327
+ <code className="truncate font-mono text-sm text-indigo-800">
328
+ {trimmedValue}
329
+ </code>
330
+ </div>
331
+ <div className="flex items-center gap-2 flex-shrink-0">
332
+ <button
333
+ type="button"
334
+ className="text-xs font-medium text-indigo-600 hover:text-indigo-700"
335
+ onClick={() => {
336
+ setShowVariableModal(true);
337
+ }}
338
+ >
339
+ Change
340
+ </button>
341
+ <span className="text-gray-300">·</span>
342
+ <button
343
+ type="button"
344
+ className="text-xs font-medium text-gray-500 hover:text-gray-700"
345
+ onClick={() => {
346
+ emit("");
347
+ setMode("preset");
348
+ }}
349
+ >
350
+ Clear
351
+ </button>
352
+ </div>
353
+ </div>
354
+ ) : (
355
+ <button
356
+ type="button"
357
+ onClick={() => {
358
+ setShowVariableModal(true);
359
+ }}
360
+ className="inline-flex items-center gap-1.5 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 hover:border-gray-400"
361
+ >
362
+ <Icon
363
+ icon={IconProp.Variable}
364
+ className="h-4 w-4 text-gray-500"
365
+ />
366
+ Select a variable
367
+ </button>
368
+ )}
369
+
370
+ <div className="flex items-start gap-2 rounded-md border border-gray-200 bg-gray-50/60 px-3 py-2 text-xs text-gray-600">
371
+ <Icon
372
+ icon={IconProp.Info}
373
+ className="h-4 w-4 text-gray-400 mt-0.5 flex-shrink-0"
374
+ />
375
+ <p>
376
+ The variable is resolved when the workflow is saved. It must
377
+ contain a valid cron expression (for example{" "}
378
+ <code className="rounded bg-gray-100 px-1 py-0.5 font-mono text-gray-700">
379
+ 0 */18 * * *
380
+ </code>
381
+ ). Component return values can&apos;t be used here because the
382
+ schedule is set up before the workflow runs.
383
+ </p>
384
+ </div>
385
+ </div>
386
+ )}
387
+
388
+ {props.error && (
389
+ <p className="text-xs text-red-600" data-testid="error-message">
390
+ {props.error}
391
+ </p>
392
+ )}
393
+
394
+ {showVariableModal && (
395
+ <VariableModal
396
+ workflowId={props.workflowId}
397
+ onClose={() => {
398
+ setShowVariableModal(false);
399
+ }}
400
+ onSave={(variableId: string) => {
401
+ setShowVariableModal(false);
402
+ setMode("variable");
403
+ emit(variableId);
404
+ }}
405
+ />
406
+ )}
407
+ </div>
408
+ );
409
+ };
410
+
411
+ export default CronScheduleField;