@oneuptime/common 11.5.12 → 11.6.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 (193) hide show
  1. package/Models/DatabaseModels/AIInsight.ts +30 -0
  2. package/Models/DatabaseModels/EnterpriseLicense.ts +21 -0
  3. package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +25 -0
  4. package/Models/DatabaseModels/GlobalConfig.ts +76 -0
  5. package/Models/DatabaseModels/Project.ts +30 -0
  6. package/Models/DatabaseModels/TelemetryException.ts +128 -0
  7. package/Server/API/AIAgentDataAPI.ts +129 -5
  8. package/Server/API/AIInvestigationAPI.ts +87 -0
  9. package/Server/API/EnterpriseLicenseAPI.ts +57 -0
  10. package/Server/API/GlobalConfigAPI.ts +62 -1
  11. package/Server/EnvironmentConfig.ts +22 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.ts +56 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.ts +38 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.ts +25 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  16. package/Server/Services/AIRunService.ts +8 -0
  17. package/Server/Services/AnalyticsDatabaseService.ts +48 -45
  18. package/Server/Services/DatabaseService.ts +66 -1
  19. package/Server/Services/TelemetryExceptionService.ts +320 -24
  20. package/Server/Services/WorkflowService.ts +100 -39
  21. package/Server/Types/Workflow/Components/Schedule.ts +57 -32
  22. package/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.ts +15 -4
  23. package/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.ts +15 -4
  24. package/Server/Utils/AI/SRE/Insights/InsightScanner.ts +222 -24
  25. package/Server/Utils/AI/SRE/Insights/InsightTriageRunner.ts +225 -9
  26. package/Server/Utils/AI/SRE/SubjectCodeFixRun.ts +39 -0
  27. package/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.ts +114 -0
  28. package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +59 -0
  29. package/Server/Utils/Express.ts +13 -0
  30. package/Server/Utils/Telemetry/ExceptionSanitizer.ts +222 -0
  31. package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +799 -0
  32. package/Server/Utils/Telemetry/TelemetryWriterClient.ts +263 -0
  33. package/Server/Utils/Telemetry/TelemetryWriterServer.ts +240 -0
  34. package/Server/Utils/Telemetry/TelemetryWriterShedMetrics.ts +93 -0
  35. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +232 -0
  36. package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +205 -1
  37. package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +107 -0
  38. package/Tests/Server/Services/WorkflowService.test.ts +308 -0
  39. package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +150 -141
  40. package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +277 -1
  41. package/Tests/Server/Utils/Attribution.test.ts +203 -0
  42. package/Tests/Server/Utils/ExceptionSanitizer.test.ts +70 -0
  43. package/Tests/Server/Utils/InsightTriageClassification.test.ts +57 -0
  44. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +183 -0
  45. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +340 -0
  46. package/Tests/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.test.ts +87 -0
  47. package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +165 -0
  48. package/Tests/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.test.ts +86 -0
  49. package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +1565 -0
  50. package/Tests/Server/Utils/Telemetry/TelemetryWriterClient.test.ts +313 -0
  51. package/Tests/Server/Utils/Telemetry/TelemetryWriterServer.test.ts +270 -0
  52. package/Tests/Server/Utils/Telemetry/TelemetryWriterShedMetrics.test.ts +127 -0
  53. package/Tests/Server/Utils/TelemetryImprovementTaskTrigger.test.ts +141 -0
  54. package/Tests/Types/AI/CodeFixTaskType.test.ts +3 -0
  55. package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +228 -0
  56. package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +169 -0
  57. package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +289 -0
  58. package/Tests/Types/Monitor/MonitorStepLogMonitor.test.ts +231 -0
  59. package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +245 -0
  60. package/Tests/Types/Monitor/MonitorStepNetworkDeviceMonitor.test.ts +155 -0
  61. package/Tests/Types/Monitor/MonitorStepTelemetrySerialization.test.ts +141 -0
  62. package/Tests/Types/Monitor/MonitorStepTraceMonitor.test.ts +137 -0
  63. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +311 -0
  64. package/Tests/UI/Components/ModelTableSelectFromColumns.test.ts +259 -0
  65. package/Tests/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.test.ts +461 -0
  66. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +2852 -0
  67. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +758 -0
  68. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +564 -0
  69. package/Tests/Utils/Array.test.ts +156 -0
  70. package/Tests/Utils/CronTab.test.ts +199 -0
  71. package/Tests/Utils/ModelImportExport.test.ts +101 -0
  72. package/Tests/Utils/Number.test.ts +179 -0
  73. package/Tests/Utils/VersionUtil.test.ts +348 -0
  74. package/Tests/jest.setup.ts +1 -0
  75. package/Types/AI/CodeFixTaskContext.ts +6 -0
  76. package/Types/AI/CodeFixTaskType.ts +38 -1
  77. package/Types/AI/ExceptionAIClassification.ts +27 -0
  78. package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +6 -0
  79. package/Types/Log/LogScrubPatternType.ts +7 -0
  80. package/Types/Monitor/MonitorStep.ts +24 -4
  81. package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +105 -0
  82. package/Types/Trace/TraceScrubPatternType.ts +7 -0
  83. package/UI/Components/EditionLabel/EditionLabel.tsx +445 -12
  84. package/UI/Components/ModelTable/BaseModelTable.tsx +15 -46
  85. package/UI/Components/ModelTable/SelectFromColumns.ts +129 -0
  86. package/UI/Components/Workflow/ArgumentsForm.tsx +68 -30
  87. package/UI/Components/Workflow/CronScheduleField.tsx +411 -0
  88. package/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.ts +176 -0
  89. package/UI/Utils/NotificationMethodUtil.ts +310 -0
  90. package/Utils/CronTab.ts +823 -0
  91. package/Utils/ModelImportExport.ts +21 -6
  92. package/Utils/VersionUtil.ts +260 -0
  93. package/build/dist/Models/DatabaseModels/AIInsight.js +31 -0
  94. package/build/dist/Models/DatabaseModels/AIInsight.js.map +1 -1
  95. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +22 -0
  96. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  97. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +26 -0
  98. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/GlobalConfig.js +81 -0
  100. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  101. package/build/dist/Models/DatabaseModels/Project.js +31 -0
  102. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  103. package/build/dist/Models/DatabaseModels/TelemetryException.js +134 -0
  104. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  105. package/build/dist/Server/API/AIAgentDataAPI.js +97 -8
  106. package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
  107. package/build/dist/Server/API/AIInvestigationAPI.js +55 -0
  108. package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
  109. package/build/dist/Server/API/EnterpriseLicenseAPI.js +47 -0
  110. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  111. package/build/dist/Server/API/GlobalConfigAPI.js +52 -1
  112. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  113. package/build/dist/Server/EnvironmentConfig.js +17 -0
  114. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js +33 -0
  116. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js.map +1 -0
  117. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js +18 -0
  118. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js.map +1 -0
  119. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js +14 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js.map +1 -0
  121. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  122. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  123. package/build/dist/Server/Services/AIRunService.js +9 -2
  124. package/build/dist/Server/Services/AIRunService.js.map +1 -1
  125. package/build/dist/Server/Services/AnalyticsDatabaseService.js +44 -16
  126. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  127. package/build/dist/Server/Services/DatabaseService.js +53 -1
  128. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  129. package/build/dist/Server/Services/TelemetryExceptionService.js +263 -22
  130. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  131. package/build/dist/Server/Services/WorkflowService.js +80 -30
  132. package/build/dist/Server/Services/WorkflowService.js.map +1 -1
  133. package/build/dist/Server/Types/Workflow/Components/Schedule.js +44 -19
  134. package/build/dist/Server/Types/Workflow/Components/Schedule.js.map +1 -1
  135. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js +12 -4
  136. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js.map +1 -1
  137. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js +12 -4
  138. package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js.map +1 -1
  139. package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js +193 -22
  140. package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js.map +1 -1
  141. package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js +178 -9
  142. package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js.map +1 -1
  143. package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js +37 -0
  144. package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js.map +1 -1
  145. package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js +98 -0
  146. package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js.map +1 -0
  147. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js +24 -0
  148. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -0
  149. package/build/dist/Server/Utils/Express.js +12 -0
  150. package/build/dist/Server/Utils/Express.js.map +1 -1
  151. package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js +149 -0
  152. package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js.map +1 -0
  153. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +496 -0
  154. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -0
  155. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js +165 -0
  156. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js.map +1 -0
  157. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js +136 -0
  158. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js.map +1 -0
  159. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js +76 -0
  160. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js.map +1 -0
  161. package/build/dist/Types/AI/CodeFixTaskType.js +38 -1
  162. package/build/dist/Types/AI/CodeFixTaskType.js.map +1 -1
  163. package/build/dist/Types/AI/ExceptionAIClassification.js +28 -0
  164. package/build/dist/Types/AI/ExceptionAIClassification.js.map +1 -0
  165. package/build/dist/Types/Log/LogScrubPatternType.js +7 -0
  166. package/build/dist/Types/Log/LogScrubPatternType.js.map +1 -1
  167. package/build/dist/Types/Monitor/MonitorStep.js +20 -4
  168. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  169. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +73 -0
  170. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -0
  171. package/build/dist/Types/Trace/TraceScrubPatternType.js +7 -0
  172. package/build/dist/Types/Trace/TraceScrubPatternType.js.map +1 -1
  173. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +261 -13
  174. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  175. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +12 -37
  176. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  177. package/build/dist/UI/Components/ModelTable/SelectFromColumns.js +82 -0
  178. package/build/dist/UI/Components/ModelTable/SelectFromColumns.js.map +1 -0
  179. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +30 -6
  180. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  181. package/build/dist/UI/Components/Workflow/CronScheduleField.js +209 -0
  182. package/build/dist/UI/Components/Workflow/CronScheduleField.js.map +1 -0
  183. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js +129 -0
  184. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js.map +1 -0
  185. package/build/dist/UI/Utils/NotificationMethodUtil.js +189 -0
  186. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -0
  187. package/build/dist/Utils/CronTab.js +609 -0
  188. package/build/dist/Utils/CronTab.js.map +1 -0
  189. package/build/dist/Utils/ModelImportExport.js +20 -6
  190. package/build/dist/Utils/ModelImportExport.js.map +1 -1
  191. package/build/dist/Utils/VersionUtil.js +193 -0
  192. package/build/dist/Utils/VersionUtil.js.map +1 -0
  193. package/package.json +1 -1
@@ -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;
@@ -0,0 +1,176 @@
1
+ /*
2
+ * Pure resolution of breadcrumb destinations.
3
+ *
4
+ * A breadcrumb trail mirrors the URL segments of the current page: crumb `i`
5
+ * conceptually corresponds to the URL truncated to `i + 3` segments (the
6
+ * leading "", "dashboard" and the project id account for the first three).
7
+ *
8
+ * Historically each crumb linked to that truncated URL directly. That is wrong
9
+ * for the many intermediate segments that are only section *prefixes* with no
10
+ * page of their own — `/user-settings`, `/on-call-duty`, `/<product>/settings`,
11
+ * `/ai`, `/llm`. Linking to such a prefix navigates to a route that matches
12
+ * nothing and renders a blank page. Leaf detail pages (`/traces/view/:id`) have
13
+ * the mirror problem: the last crumb's truncated URL drops the id.
14
+ *
15
+ * This module contains the pure, framework-agnostic decision of *what* each
16
+ * crumb should point at, expressed as a `BreadcrumbTarget` descriptor. It has no
17
+ * dependency on the router or the app's route table so it can be unit-tested in
18
+ * isolation. Callers translate the descriptor into a concrete route.
19
+ */
20
+
21
+ export enum BreadcrumbTargetKind {
22
+ // Link to the project home (URL truncated to its first 3 segments).
23
+ Home = "home",
24
+ // The current page — rendered as plain text, never a link.
25
+ CurrentPage = "current",
26
+ // Link to an ancestor page that lies on the current path, at `depth` segments.
27
+ Ancestor = "ancestor",
28
+ // Link to a real page on a different branch (the section's landing page).
29
+ SectionLanding = "landing",
30
+ }
31
+
32
+ export interface BreadcrumbTarget {
33
+ kind: BreadcrumbTargetKind;
34
+ /*
35
+ * Number of URL segments to keep (including the leading empty segment) when
36
+ * building the link. Present for Home and Ancestor.
37
+ */
38
+ depth?: number | undefined;
39
+ /*
40
+ * Fully-qualified route pattern (may still contain `:params`) of the section
41
+ * landing page. Present only for SectionLanding.
42
+ */
43
+ routePattern?: string | undefined;
44
+ }
45
+
46
+ export interface ResolveBreadcrumbTrailOptions {
47
+ // Route pattern of the page the trail is for, e.g. "/dashboard/:projectId/x".
48
+ pagePattern: string;
49
+ // Number of crumbs in the trail.
50
+ crumbCount: number;
51
+ // Every real, navigable route pattern, in declaration order.
52
+ realRoutePatterns: ReadonlyArray<string>;
53
+ }
54
+
55
+ /*
56
+ * Strip a trailing slash so "/a/b/" and "/a/b" compare equal. The root "/" is
57
+ * left intact.
58
+ */
59
+ export function normalizeRoutePath(path: string): string {
60
+ if (path.length > 1 && path.endsWith("/")) {
61
+ return path.replace(/\/+$/, "");
62
+ }
63
+ return path;
64
+ }
65
+
66
+ /*
67
+ * Resolve the destination of a single crumb.
68
+ *
69
+ * - First crumb ("Project") -> Home.
70
+ * - Last crumb -> CurrentPage (plain text).
71
+ * - Intermediate crumb -> the nearest real ancestor route on the
72
+ * current path; else the section's
73
+ * landing page; else CurrentPage. It
74
+ * never points at a non-existent route.
75
+ */
76
+ export function resolveBreadcrumbTarget(options: {
77
+ index: number;
78
+ crumbCount: number;
79
+ pagePattern: string;
80
+ realRoutePatterns: ReadonlyArray<string>;
81
+ }): BreadcrumbTarget {
82
+ const { index, crumbCount, pagePattern, realRoutePatterns } = options;
83
+
84
+ if (index <= 0) {
85
+ return { kind: BreadcrumbTargetKind.Home, depth: 3 };
86
+ }
87
+
88
+ if (index >= crumbCount - 1) {
89
+ return { kind: BreadcrumbTargetKind.CurrentPage };
90
+ }
91
+
92
+ const normalizedPage: string = normalizeRoutePath(pagePattern);
93
+ const pageSegments: Array<string> = normalizedPage.split("/");
94
+ const realRouteSet: Set<string> = new Set(
95
+ realRoutePatterns.map(normalizeRoutePath),
96
+ );
97
+
98
+ // Depth (segment count) of this crumb's own URL prefix.
99
+ const crumbDepth: number = index + 3;
100
+
101
+ /*
102
+ * Walk from this crumb's prefix depth toward — but not including — the current
103
+ * page and link to the nearest ancestor that is a real page. This links
104
+ * "Insights" to /ai/insights on an /ai/insights/settings page, and keeps
105
+ * section list pages (e.g. "Traces" -> /traces) linkable as before.
106
+ */
107
+ for (let depth: number = crumbDepth; depth < pageSegments.length; depth++) {
108
+ const ancestorPattern: string = pageSegments.slice(0, depth).join("/");
109
+ if (realRouteSet.has(ancestorPattern)) {
110
+ return { kind: BreadcrumbTargetKind.Ancestor, depth };
111
+ }
112
+ }
113
+
114
+ /*
115
+ * The crumb sits at or beyond the current page's own depth — either a "packed"
116
+ * trail with more crumbs than URL segments (e.g. Project > Profiler > Overview
117
+ * on /profiles, where "Profiler" and "Overview" share the single /profiles
118
+ * segment), or exactly the page depth. There is no distinct ancestor page to
119
+ * point at, so render it as plain text like the current page's own crumb
120
+ * rather than inventing a link to a sibling.
121
+ */
122
+ if (crumbDepth >= pageSegments.length) {
123
+ return { kind: BreadcrumbTargetKind.CurrentPage };
124
+ }
125
+
126
+ /*
127
+ * The prefix is a bare section root with no page above the current page (e.g.
128
+ * /user-settings). Fall back to the section's landing page — the first
129
+ * declared real route under the prefix — so the crumb still navigates
130
+ * somewhere useful.
131
+ */
132
+ const prefixWithSlash: string =
133
+ pageSegments.slice(0, crumbDepth).join("/") + "/";
134
+ const landingPattern: string | undefined = realRoutePatterns
135
+ .map(normalizeRoutePath)
136
+ .find((pattern: string): boolean => {
137
+ return pattern.startsWith(prefixWithSlash);
138
+ });
139
+
140
+ // No page under the prefix, or the landing is the current page -> plain text.
141
+ if (!landingPattern || landingPattern === normalizedPage) {
142
+ return { kind: BreadcrumbTargetKind.CurrentPage };
143
+ }
144
+
145
+ /*
146
+ * The landing is a real page on a different branch of this section (e.g.
147
+ * "On-Call Duty" -> /policies while viewing /schedules). A landing that lay on
148
+ * the current path would already have been returned as an Ancestor by the loop
149
+ * above, so at this point it is always off-path; the caller fills in the
150
+ * project id.
151
+ */
152
+ return {
153
+ kind: BreadcrumbTargetKind.SectionLanding,
154
+ routePattern: landingPattern,
155
+ };
156
+ }
157
+
158
+ // Resolve the destination of every crumb in a trail.
159
+ export function resolveBreadcrumbTrail(
160
+ options: ResolveBreadcrumbTrailOptions,
161
+ ): Array<BreadcrumbTarget> {
162
+ const { pagePattern, crumbCount, realRoutePatterns } = options;
163
+
164
+ const targets: Array<BreadcrumbTarget> = [];
165
+ for (let index: number = 0; index < crumbCount; index++) {
166
+ targets.push(
167
+ resolveBreadcrumbTarget({
168
+ index,
169
+ crumbCount,
170
+ pagePattern,
171
+ realRoutePatterns,
172
+ }),
173
+ );
174
+ }
175
+ return targets;
176
+ }