@oneuptime/common 12.0.9 → 12.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/NetworkDevice.ts +44 -0
- package/Models/DatabaseModels/NetworkDeviceLink.ts +116 -0
- package/Models/DatabaseModels/RunnerJob.ts +17 -2
- package/Server/API/LlmProviderAPI.ts +8 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +240 -0
- package/Server/Services/NetworkDeviceLinkService.ts +338 -2
- package/Server/Services/NetworkDeviceService.ts +265 -12
- package/Server/Services/RunnerJobService.ts +24 -1
- package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
- package/Server/Types/Database/JSONColumnQuery.ts +42 -4
- package/Server/Utils/LLM/LLMService.ts +108 -19
- package/Server/Utils/Monitor/MonitorAlert.ts +22 -115
- package/Server/Utils/Monitor/MonitorIncident.ts +7 -337
- package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +27 -2
- package/Server/Utils/Monitor/SeriesResourceLabels.ts +32 -0
- package/Server/Utils/Monitor/SeriesResourceLinker.ts +469 -0
- package/Server/Utils/SSRFProtection.ts +5 -4
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
- package/Tests/App/AdminDashboard/AdminUsersBulkAddToProject.test.tsx +955 -0
- package/Tests/App/AdminDashboard/BulkAddUsersToProject.test.ts +778 -0
- package/Tests/App/AdminDashboard/BulkAddUsersToProjectModal.test.tsx +1051 -0
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
- package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +81 -6
- package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +543 -0
- package/Tests/App/Dashboard/PayAsYouGoFreePlan.test.ts +125 -0
- package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +612 -0
- package/Tests/App/Dashboard/SloWidgetChartRendering.test.tsx +536 -0
- package/Tests/App/Dashboard/UserElementEmail.test.tsx +447 -0
- package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +284 -0
- package/Tests/Models/AnalyticsModels/CommonModel.test.ts +312 -0
- package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +250 -0
- package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +46 -18
- package/Tests/Server/Services/NetworkDeviceLabelRuleRun.test.ts +618 -0
- package/Tests/Server/Services/NetworkDeviceLinkParentValidation.test.ts +1043 -0
- package/Tests/Server/Services/NetworkSiteAssignmentRuleRun.test.ts +706 -0
- package/Tests/Server/Services/RunnerJobEnqueue.test.ts +412 -0
- package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
- package/Tests/Server/Utils/AI/LLMServiceRequestPolicy.test.ts +2 -2
- package/Tests/Server/Utils/AI/LLMServiceRetryPolicy.test.ts +450 -0
- package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +556 -0
- package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +547 -0
- package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +30 -0
- package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +699 -0
- package/Tests/Server/Utils/SeriesResourceLabels.test.ts +55 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
- package/Tests/Types/API/URLMalformedTolerance.test.ts +301 -0
- package/Tests/Types/API/URLTelScheme.test.ts +261 -0
- package/Tests/Types/API/URLUnsupportedScheme.test.ts +148 -0
- package/Tests/Types/Billing/PayAsYouGoPricing.test.ts +64 -0
- package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +107 -0
- package/Tests/Types/Monitor/MonitorType.test.ts +62 -0
- package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +184 -0
- package/Tests/Types/Runbook/RunbookStepType.test.ts +124 -0
- package/Tests/Types/Workflow/BaseModelComponents.test.ts +17 -0
- package/Tests/UI/Components/Button.test.tsx +279 -0
- package/Tests/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
- package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
- package/Tests/UI/Components/DictionaryValuePlaceholderAndSuggestions.test.tsx +595 -0
- package/Tests/UI/Components/Icon.test.tsx +57 -0
- package/Tests/UI/Components/List.test.tsx +20 -3
- package/Tests/UI/Components/LogsSavedViewsDropdown.test.tsx +304 -0
- package/Tests/UI/Components/LogsViewerClearSavedView.test.tsx +179 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
- package/Tests/UI/Components/Pagination.test.tsx +721 -146
- package/Tests/UI/Components/PaginationIntegration.test.tsx +386 -0
- package/Tests/UI/Components/PaginationUtil.test.ts +417 -0
- package/Tests/UI/Components/TelemetrySavedViewsDropdown.test.tsx +343 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnControl.test.ts +289 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnOperators.test.ts +271 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRow.test.ts +119 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.test.ts +968 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditor.render.test.tsx +766 -0
- package/Tests/UI/Utils/RunNetworkRule.test.ts +320 -0
- package/Tests/Utils/APIRetryPolicy.test.ts +823 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
- package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
- package/Tests/Utils/NetworkAutomation/RuleRunSummary.test.ts +286 -0
- package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
- package/Types/API/Protocol.ts +9 -0
- package/Types/API/URL.ts +286 -20
- package/Types/Billing/PayAsYouGoPricing.ts +47 -0
- package/Types/CustomField/CustomFieldType.ts +28 -0
- package/Types/Monitor/MonitorType.ts +11 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
- package/Types/NetworkAutomation/RuleRunResult.ts +121 -0
- package/Types/Runbook/RunbookStepType.ts +23 -0
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +7 -0
- package/UI/Components/Button/Button.tsx +14 -22
- package/UI/Components/Charts/Area/AreaChart.tsx +4 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +4 -2
- package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +13 -0
- package/UI/Components/Charts/Line/LineChart.tsx +4 -2
- package/UI/Components/Charts/Types/XAxis/XAxis.ts +6 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +15 -12
- package/UI/Components/Dictionary/Dictionary.tsx +4 -39
- package/UI/Components/Icon/Icon.tsx +1 -1
- package/UI/Components/LogsViewer/LogsViewer.tsx +5 -1
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +8 -0
- package/UI/Components/LogsViewer/components/LogsPagination.tsx +29 -91
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +2 -0
- package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +66 -1
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +55 -0
- package/UI/Components/Pagination/Pagination.tsx +373 -328
- package/UI/Components/Pagination/PaginationUtil.ts +263 -0
- package/UI/Components/Pagination/Screenshots/README.md +15 -0
- package/UI/Components/Pagination/Screenshots/pagination-before-after.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-go-to-page-modal.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-long-list.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-mobile.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-telemetry.png +0 -0
- package/UI/Components/TelemetryViewer/components/SavedViewsDropdown.tsx +66 -1
- package/UI/Components/TelemetryViewer/components/TelemetryPagination.tsx +38 -92
- package/UI/Components/TextArea/TextArea.tsx +3 -0
- package/UI/Components/Workflow/ArgumentsForm.tsx +18 -1
- package/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.tsx +177 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.tsx +243 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnControl.ts +255 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.tsx +178 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnOperators.ts +206 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnRow.ts +157 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.ts +517 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnValueInput.tsx +482 -0
- package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +131 -0
- package/UI/Components/Workflow/ColumnEditor/ModelRecordForm.tsx +163 -0
- package/UI/Components/Workflow/ModelColumnEditor.tsx +284 -100
- package/UI/Utils/NetworkAutomation/RunNetworkRule.ts +122 -0
- package/Utils/API.ts +243 -36
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
- package/Utils/NetworkAutomation/RuleRunSummary.ts +147 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +115 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RunnerJob.js +17 -2
- package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
- package/build/dist/Server/API/LlmProviderAPI.js +9 -1
- package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js +33 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js +202 -0
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.js +253 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +223 -12
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/RunnerJobService.js +20 -2
- package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +12 -5
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +40 -4
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
- package/build/dist/Server/Utils/LLM/LLMService.js +78 -22
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -98
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +7 -254
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +14 -2
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +27 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +285 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -0
- package/build/dist/Server/Utils/SSRFProtection.js +5 -4
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +128 -35
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
- package/build/dist/Types/API/Protocol.js +9 -0
- package/build/dist/Types/API/Protocol.js.map +1 -1
- package/build/dist/Types/API/URL.js +241 -18
- package/build/dist/Types/API/URL.js.map +1 -1
- package/build/dist/Types/Billing/PayAsYouGoPricing.js +41 -0
- package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldType.js +26 -0
- package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +10 -0
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js +39 -0
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -0
- package/build/dist/Types/Runbook/RunbookStepType.js +21 -0
- package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/Button/Button.js +10 -16
- package/build/dist/UI/Components/Button/Button.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +12 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +17 -10
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js +4 -18
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +1 -1
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +3 -3
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +5 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +14 -44
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +20 -2
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +42 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/Pagination/Pagination.js +174 -199
- package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
- package/build/dist/UI/Components/Pagination/PaginationUtil.js +160 -0
- package/build/dist/UI/Components/Pagination/PaginationUtil.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js +20 -2
- package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js +23 -47
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js.map +1 -1
- package/build/dist/UI/Components/TextArea/TextArea.js +1 -1
- package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +14 -2
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js +86 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js +119 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js +206 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js +57 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js +150 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js +67 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js +309 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js +204 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +66 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js +83 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +187 -68
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -1
- package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js +75 -0
- package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js.map +1 -0
- package/build/dist/Utils/API.js +175 -34
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +37 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +25 -3
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js +78 -0
- package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import RunbookStepType, {
|
|
2
|
+
PAYLOAD_CARRYING_STEP_TYPES,
|
|
2
3
|
RUNNER_EXECUTED_STEP_TYPES,
|
|
4
|
+
isPayloadCarryingStepType,
|
|
3
5
|
isRunnerExecutedStepType,
|
|
4
6
|
} from "../../../Types/Runbook/RunbookStepType";
|
|
5
7
|
import { describe, expect, test } from "@jest/globals";
|
|
@@ -240,3 +242,125 @@ describe("isRunnerExecutedStepType", () => {
|
|
|
240
242
|
);
|
|
241
243
|
});
|
|
242
244
|
});
|
|
245
|
+
|
|
246
|
+
/*
|
|
247
|
+
* The second partition, inside the Runner lane: how a job carries its
|
|
248
|
+
* instruction.
|
|
249
|
+
*
|
|
250
|
+
* A Bash or JavaScript job carries a script. An SSH or Kubernetes job carries
|
|
251
|
+
* structured instructions plus a credential resolved at claim time, and an
|
|
252
|
+
* EMPTY script — which is the whole reason this split has to be named
|
|
253
|
+
* somewhere. Issue #3209 was RunnerJob.script being declared a required
|
|
254
|
+
* column: an empty string is falsy, so the required-column check rejected
|
|
255
|
+
* every SSH and Kubernetes job at create() with "script is required". The rule
|
|
256
|
+
* is per-type and cannot be written as column metadata, so it lives in
|
|
257
|
+
* RunnerJobService.enqueue and reads this list.
|
|
258
|
+
*/
|
|
259
|
+
describe("PAYLOAD_CARRYING_STEP_TYPES", () => {
|
|
260
|
+
test("contains exactly SSH and Kubernetes", () => {
|
|
261
|
+
expect(PAYLOAD_CARRYING_STEP_TYPES).toEqual([
|
|
262
|
+
RunbookStepType.SSH,
|
|
263
|
+
RunbookStepType.Kubernetes,
|
|
264
|
+
]);
|
|
265
|
+
expect(new Set(PAYLOAD_CARRYING_STEP_TYPES).size).toBe(
|
|
266
|
+
PAYLOAD_CARRYING_STEP_TYPES.length,
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("every entry is also a Runner-executed type", () => {
|
|
271
|
+
/*
|
|
272
|
+
* A payload-carrying type that the Worker executes would be a
|
|
273
|
+
* contradiction: the payload exists to be handed to a Runner.
|
|
274
|
+
*/
|
|
275
|
+
for (const type of PAYLOAD_CARRYING_STEP_TYPES) {
|
|
276
|
+
expect(RUNNER_EXECUTED_STEP_TYPES).toContain(type);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("isPayloadCarryingStepType", () => {
|
|
282
|
+
test.each([RunbookStepType.SSH, RunbookStepType.Kubernetes])(
|
|
283
|
+
"%s carries a payload rather than a script",
|
|
284
|
+
(type: RunbookStepType) => {
|
|
285
|
+
expect(isPayloadCarryingStepType(type)).toBe(true);
|
|
286
|
+
},
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
test.each([RunbookStepType.Bash, RunbookStepType.JavaScript])(
|
|
290
|
+
"%s carries a script rather than a payload",
|
|
291
|
+
(type: RunbookStepType) => {
|
|
292
|
+
/*
|
|
293
|
+
* Answering true for a script type would let a Bash job with no script
|
|
294
|
+
* through enqueue, and the Runner would spawn an empty shell and report
|
|
295
|
+
* success — indistinguishable from a step that actually ran.
|
|
296
|
+
*/
|
|
297
|
+
expect(isPayloadCarryingStepType(type)).toBe(false);
|
|
298
|
+
},
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
test("the Runner lane splits cleanly into script-carrying and payload-carrying", () => {
|
|
302
|
+
/*
|
|
303
|
+
* Total and disjoint within the Runner lane. enqueue treats the two as
|
|
304
|
+
* complementary — anything not payload-carrying is required to bring a
|
|
305
|
+
* script — so a new Runner type that belongs in neither set does not
|
|
306
|
+
* exist, and a new one added to the enum must be placed here deliberately.
|
|
307
|
+
*/
|
|
308
|
+
const payload: Array<RunbookStepType> = RUNNER_EXECUTED_STEP_TYPES.filter(
|
|
309
|
+
(type: RunbookStepType) => {
|
|
310
|
+
return isPayloadCarryingStepType(type);
|
|
311
|
+
},
|
|
312
|
+
);
|
|
313
|
+
const script: Array<RunbookStepType> = RUNNER_EXECUTED_STEP_TYPES.filter(
|
|
314
|
+
(type: RunbookStepType) => {
|
|
315
|
+
return !isPayloadCarryingStepType(type);
|
|
316
|
+
},
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
expect(payload.slice().sort()).toEqual(
|
|
320
|
+
PAYLOAD_CARRYING_STEP_TYPES.slice().sort(),
|
|
321
|
+
);
|
|
322
|
+
expect(script.slice().sort()).toEqual(
|
|
323
|
+
[RunbookStepType.JavaScript, RunbookStepType.Bash].slice().sort(),
|
|
324
|
+
);
|
|
325
|
+
expect(payload.length + script.length).toBe(
|
|
326
|
+
RUNNER_EXECUTED_STEP_TYPES.length,
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("agrees with the exported list for every member of the enum", () => {
|
|
331
|
+
for (const type of Object.values(RunbookStepType)) {
|
|
332
|
+
const answer: boolean = isPayloadCarryingStepType(type);
|
|
333
|
+
|
|
334
|
+
expect(typeof answer).toBe("boolean");
|
|
335
|
+
expect(answer).toBe(PAYLOAD_CARRYING_STEP_TYPES.includes(type));
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("no Worker-executed type carries a payload", () => {
|
|
340
|
+
for (const type of WORKER_EXECUTED_STEP_TYPES) {
|
|
341
|
+
expect(isPayloadCarryingStepType(type)).toBe(false);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("near misses, null and undefined do not carry a payload", () => {
|
|
346
|
+
/*
|
|
347
|
+
* Falling back to false is the safe direction: an unrecognised type is
|
|
348
|
+
* then required to bring a script, and enqueue rejects it rather than
|
|
349
|
+
* creating a job with nothing in it.
|
|
350
|
+
*/
|
|
351
|
+
const notPayloadTypes: Array<unknown> = [
|
|
352
|
+
"",
|
|
353
|
+
" ",
|
|
354
|
+
"ssh",
|
|
355
|
+
"SSH ",
|
|
356
|
+
"kubernetes",
|
|
357
|
+
"Docker",
|
|
358
|
+
null,
|
|
359
|
+
undefined,
|
|
360
|
+
];
|
|
361
|
+
|
|
362
|
+
for (const value of notPayloadTypes) {
|
|
363
|
+
expect(isPayloadCarryingStepType(value as RunbookStepType)).toBe(false);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
});
|
|
@@ -203,6 +203,23 @@ describe("the write payload is keyed on columns", () => {
|
|
|
203
203
|
);
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
+
/*
|
|
207
|
+
* ArgumentsForm reads the argument id to decide whether the record editor
|
|
208
|
+
* opens with a blank row for every column a create must supply: "json" is a
|
|
209
|
+
* create, anything else is an update. An update that opened on six seeded
|
|
210
|
+
* fields when the builder meant to set one would be a regression nothing else
|
|
211
|
+
* here would catch, so the two spellings are pinned as a pair.
|
|
212
|
+
*/
|
|
213
|
+
test('a create\'s payload is "json" and an update\'s is "data", never the reverse', () => {
|
|
214
|
+
expect(argumentOf(find("-create-one"), "json")).toBeDefined();
|
|
215
|
+
expect(argumentOf(find("-create-one"), "data")).toBeUndefined();
|
|
216
|
+
|
|
217
|
+
for (const idSuffix of ["-update-one", "-update-many"]) {
|
|
218
|
+
expect(argumentOf(find(idSuffix), "data")).toBeDefined();
|
|
219
|
+
expect(argumentOf(find(idSuffix), "json")).toBeUndefined();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
206
223
|
test("no component declares a BaseModel argument", () => {
|
|
207
224
|
/*
|
|
208
225
|
* The bug this whole change came from: the row editor was selected only for
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Button, {
|
|
2
2
|
ButtonSize,
|
|
3
3
|
ButtonStyleType,
|
|
4
|
+
ComponentProps,
|
|
4
5
|
} from "../../../UI/Components/Button/Button";
|
|
5
6
|
import ButtonType from "../../../UI/Components/Button/ButtonTypes";
|
|
6
7
|
import ShortcutKey from "../../../UI/Components/ShortcutKey/ShortcutKey";
|
|
@@ -200,3 +201,281 @@ describe("Button", () => {
|
|
|
200
201
|
expect(handleClick).toBeCalled();
|
|
201
202
|
});
|
|
202
203
|
});
|
|
204
|
+
|
|
205
|
+
type RenderButtonFunction = (props: Partial<ComponentProps>) => HTMLElement;
|
|
206
|
+
|
|
207
|
+
const renderButton: RenderButtonFunction = (
|
|
208
|
+
props: Partial<ComponentProps>,
|
|
209
|
+
): HTMLElement => {
|
|
210
|
+
const { unmount } = render(<Button dataTestId="test-id" {...props} />);
|
|
211
|
+
const button: HTMLElement = screen.getByTestId("test-id");
|
|
212
|
+
// Clone so assertions survive the unmount and each case starts from a clean DOM.
|
|
213
|
+
const clone: HTMLElement = button.cloneNode(true) as HTMLElement;
|
|
214
|
+
unmount();
|
|
215
|
+
return clone;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
type ClassesOfFunction = (element: HTMLElement) => Array<string>;
|
|
219
|
+
|
|
220
|
+
const classesOf: ClassesOfFunction = (element: HTMLElement): Array<string> => {
|
|
221
|
+
return element.className.split(/\s+/).filter((cssClass: string) => {
|
|
222
|
+
return cssClass.length > 0;
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/*
|
|
227
|
+
* Every ButtonStyleType, so new styles are covered by the invariants below
|
|
228
|
+
* without anyone having to remember to add them here.
|
|
229
|
+
*/
|
|
230
|
+
const ALL_BUTTON_STYLES: Array<[string, ButtonStyleType]> = Object.keys(
|
|
231
|
+
ButtonStyleType,
|
|
232
|
+
)
|
|
233
|
+
.filter((key: string) => {
|
|
234
|
+
return isNaN(Number(key));
|
|
235
|
+
})
|
|
236
|
+
.map((key: string) => {
|
|
237
|
+
return [key, ButtonStyleType[key as keyof typeof ButtonStyleType]] as [
|
|
238
|
+
string,
|
|
239
|
+
ButtonStyleType,
|
|
240
|
+
];
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("Button disabled styling", () => {
|
|
244
|
+
/*
|
|
245
|
+
* The regression this suite exists for: the disabled background used to be
|
|
246
|
+
* appended alongside the enabled one, and Tailwind emits .bg-indigo-300
|
|
247
|
+
* before .bg-indigo-600, so the enabled colour won the cascade and every
|
|
248
|
+
* disabled primary button rendered at full saturation.
|
|
249
|
+
*/
|
|
250
|
+
test("disabled PRIMARY does not carry the enabled background class", () => {
|
|
251
|
+
const button: HTMLElement = renderButton({
|
|
252
|
+
buttonStyle: ButtonStyleType.PRIMARY,
|
|
253
|
+
disabled: true,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(button).not.toHaveClass("bg-indigo-600");
|
|
257
|
+
expect(button).toHaveClass("bg-indigo-300");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("disabled SECONDARY does not carry the enabled background class", () => {
|
|
261
|
+
const button: HTMLElement = renderButton({
|
|
262
|
+
buttonStyle: ButtonStyleType.SECONDARY,
|
|
263
|
+
disabled: true,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(button).not.toHaveClass("bg-indigo-100");
|
|
267
|
+
expect(button).toHaveClass("bg-indigo-300");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("enabled PRIMARY keeps the enabled background and no disabled background", () => {
|
|
271
|
+
const button: HTMLElement = renderButton({
|
|
272
|
+
buttonStyle: ButtonStyleType.PRIMARY,
|
|
273
|
+
disabled: false,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(button).toHaveClass("bg-indigo-600");
|
|
277
|
+
expect(button).not.toHaveClass("bg-indigo-300");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("enabled SECONDARY keeps the enabled background and no disabled background", () => {
|
|
281
|
+
const button: HTMLElement = renderButton({
|
|
282
|
+
buttonStyle: ButtonStyleType.SECONDARY,
|
|
283
|
+
disabled: false,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(button).toHaveClass("bg-indigo-100");
|
|
287
|
+
expect(button).not.toHaveClass("bg-indigo-300");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("an omitted disabled prop styles PRIMARY as enabled", () => {
|
|
291
|
+
const button: HTMLElement = renderButton({
|
|
292
|
+
buttonStyle: ButtonStyleType.PRIMARY,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
expect(button).toHaveClass("bg-indigo-600");
|
|
296
|
+
expect(button).not.toHaveClass("bg-indigo-300");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("an omitted disabled prop styles SECONDARY as enabled", () => {
|
|
300
|
+
const button: HTMLElement = renderButton({
|
|
301
|
+
buttonStyle: ButtonStyleType.SECONDARY,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(button).toHaveClass("bg-indigo-100");
|
|
305
|
+
expect(button).not.toHaveClass("bg-indigo-300");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test.each(ALL_BUTTON_STYLES)(
|
|
309
|
+
"%s emits at most one background utility when disabled",
|
|
310
|
+
(_name: string, buttonStyle: ButtonStyleType) => {
|
|
311
|
+
const button: HTMLElement = renderButton({ buttonStyle, disabled: true });
|
|
312
|
+
|
|
313
|
+
const backgrounds: Array<string> = classesOf(button).filter(
|
|
314
|
+
(cssClass: string) => {
|
|
315
|
+
return cssClass.startsWith("bg-");
|
|
316
|
+
},
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
expect(backgrounds.length).toBeLessThanOrEqual(1);
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
test.each(ALL_BUTTON_STYLES)(
|
|
324
|
+
"%s emits at most one background utility when enabled",
|
|
325
|
+
(_name: string, buttonStyle: ButtonStyleType) => {
|
|
326
|
+
const button: HTMLElement = renderButton({
|
|
327
|
+
buttonStyle,
|
|
328
|
+
disabled: false,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const backgrounds: Array<string> = classesOf(button).filter(
|
|
332
|
+
(cssClass: string) => {
|
|
333
|
+
return cssClass.startsWith("bg-");
|
|
334
|
+
},
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
expect(backgrounds.length).toBeLessThanOrEqual(1);
|
|
338
|
+
},
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const DIMMING_STYLES: Array<[string, ButtonStyleType]> = [
|
|
342
|
+
["PRIMARY", ButtonStyleType.PRIMARY],
|
|
343
|
+
["SECONDARY", ButtonStyleType.SECONDARY],
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
test.each(DIMMING_STYLES)(
|
|
347
|
+
"%s resolves to a different background when disabled",
|
|
348
|
+
(_name: string, buttonStyle: ButtonStyleType) => {
|
|
349
|
+
type BackgroundOfFunction = (disabled: boolean) => string | undefined;
|
|
350
|
+
|
|
351
|
+
const backgroundOf: BackgroundOfFunction = (
|
|
352
|
+
disabled: boolean,
|
|
353
|
+
): string | undefined => {
|
|
354
|
+
return classesOf(renderButton({ buttonStyle, disabled })).find(
|
|
355
|
+
(cssClass: string) => {
|
|
356
|
+
return cssClass.startsWith("bg-");
|
|
357
|
+
},
|
|
358
|
+
);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const enabled: string | undefined = backgroundOf(false);
|
|
362
|
+
const disabled: string | undefined = backgroundOf(true);
|
|
363
|
+
|
|
364
|
+
expect(enabled).toBeDefined();
|
|
365
|
+
expect(disabled).toBeDefined();
|
|
366
|
+
expect(disabled).not.toBe(enabled);
|
|
367
|
+
},
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
describe("Button hover styling", () => {
|
|
372
|
+
/*
|
|
373
|
+
* Hover affordances are gated on `disabled`; the ternary used to be inverted,
|
|
374
|
+
* so hover styles applied to exactly the buttons that could not be clicked.
|
|
375
|
+
*/
|
|
376
|
+
const HOVER_GATED_STYLES: Array<[string, ButtonStyleType, string]> = [
|
|
377
|
+
["PRIMARY", ButtonStyleType.PRIMARY, "hover:bg-indigo-700"],
|
|
378
|
+
["SECONDARY", ButtonStyleType.SECONDARY, "hover:bg-indigo-200"],
|
|
379
|
+
["DANGER", ButtonStyleType.DANGER, "hover:bg-red-700"],
|
|
380
|
+
["DANGER_OUTLINE", ButtonStyleType.DANGER_OUTLINE, "hover:bg-red-50"],
|
|
381
|
+
["SUCCESS", ButtonStyleType.SUCCESS, "hover:bg-green-700"],
|
|
382
|
+
["SUCCESS_OUTLINE", ButtonStyleType.SUCCESS_OUTLINE, "hover:bg-green-50"],
|
|
383
|
+
["WARNING", ButtonStyleType.WARNING, "hover:bg-yellow-700"],
|
|
384
|
+
["WARNING_OUTLINE", ButtonStyleType.WARNING_OUTLINE, "hover:bg-yellow-50"],
|
|
385
|
+
["ICON", ButtonStyleType.ICON, "hover:text-gray-900"],
|
|
386
|
+
["ICON_LIGHT", ButtonStyleType.ICON_LIGHT, "hover:text-gray-500"],
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
test.each(HOVER_GATED_STYLES)(
|
|
390
|
+
"%s applies its hover class only when enabled",
|
|
391
|
+
(_name: string, buttonStyle: ButtonStyleType, hoverClass: string) => {
|
|
392
|
+
expect(renderButton({ buttonStyle, disabled: false })).toHaveClass(
|
|
393
|
+
hoverClass,
|
|
394
|
+
);
|
|
395
|
+
expect(renderButton({ buttonStyle, disabled: true })).not.toHaveClass(
|
|
396
|
+
hoverClass,
|
|
397
|
+
);
|
|
398
|
+
},
|
|
399
|
+
);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe("Button class list composition", () => {
|
|
403
|
+
test("the size class is appended for every style", () => {
|
|
404
|
+
ALL_BUTTON_STYLES.forEach(([, buttonStyle]: [string, ButtonStyleType]) => {
|
|
405
|
+
expect(
|
|
406
|
+
renderButton({ buttonStyle, buttonSize: ButtonSize.Small }),
|
|
407
|
+
).toHaveClass("px-2", "py-1");
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("a caller-supplied className is appended alongside the style classes", () => {
|
|
412
|
+
const button: HTMLElement = renderButton({
|
|
413
|
+
buttonStyle: ButtonStyleType.PRIMARY,
|
|
414
|
+
className: "my-custom-class",
|
|
415
|
+
disabled: true,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
expect(button).toHaveClass("my-custom-class");
|
|
419
|
+
expect(button).toHaveClass("bg-indigo-300");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("every style resolves to a non-empty class list in both states", () => {
|
|
423
|
+
ALL_BUTTON_STYLES.forEach(([, buttonStyle]: [string, ButtonStyleType]) => {
|
|
424
|
+
[true, false].forEach((disabled: boolean) => {
|
|
425
|
+
expect(
|
|
426
|
+
classesOf(renderButton({ buttonStyle, disabled })).length,
|
|
427
|
+
).toBeGreaterThan(0);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("no style emits a class twice", () => {
|
|
433
|
+
ALL_BUTTON_STYLES.forEach(([, buttonStyle]: [string, ButtonStyleType]) => {
|
|
434
|
+
[true, false].forEach((disabled: boolean) => {
|
|
435
|
+
const classes: Array<string> = classesOf(
|
|
436
|
+
renderButton({ buttonStyle, disabled }),
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
expect(classes).toHaveLength(new Set(classes).size);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe("Button disabled behaviour", () => {
|
|
446
|
+
test("a disabled button is marked disabled to assistive technology", () => {
|
|
447
|
+
const button: HTMLElement = renderButton({
|
|
448
|
+
buttonStyle: ButtonStyleType.PRIMARY,
|
|
449
|
+
disabled: true,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
expect(button).toHaveAttribute("disabled");
|
|
453
|
+
expect(button).toHaveAttribute("aria-disabled", "true");
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("a loading button is disabled", () => {
|
|
457
|
+
const button: HTMLElement = renderButton({
|
|
458
|
+
buttonStyle: ButtonStyleType.PRIMARY,
|
|
459
|
+
isLoading: true,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
expect(button).toHaveAttribute("disabled");
|
|
463
|
+
expect(button).toHaveAttribute("aria-disabled", "true");
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("a disabled button does not fire onClick", () => {
|
|
467
|
+
const handleClick: () => void = jest.fn();
|
|
468
|
+
render(
|
|
469
|
+
<Button
|
|
470
|
+
dataTestId="test-id"
|
|
471
|
+
buttonStyle={ButtonStyleType.PRIMARY}
|
|
472
|
+
disabled={true}
|
|
473
|
+
onClick={handleClick}
|
|
474
|
+
/>,
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
fireEvent.click(screen.getByTestId("test-id"));
|
|
478
|
+
|
|
479
|
+
expect(handleClick).not.toBeCalled();
|
|
480
|
+
});
|
|
481
|
+
});
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { cleanup, render } from "@testing-library/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { afterEach, describe, expect, test } from "@jest/globals";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* ResponsiveContainer measures its parent, which is always 0x0 in jsdom, so
|
|
8
|
+
* the chart renders nothing without a fixed size. Same shim as
|
|
9
|
+
* AxisTickColor.test.tsx — anything that asserts on rendered GEOMETRY needs
|
|
10
|
+
* it, because a 0x0 chart draws no marks either and would make every one of
|
|
11
|
+
* these assertions pass for the wrong reason.
|
|
12
|
+
*/
|
|
13
|
+
jest.mock("recharts", () => {
|
|
14
|
+
const actual: Record<string, any> = jest.requireActual("recharts");
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
ResponsiveContainer: ({ children }: { children: React.ReactElement }) => {
|
|
18
|
+
return React.cloneElement(children, { width: 600, height: 300 });
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
import AreaChartElement from "../../../../UI/Components/Charts/Area/AreaChart";
|
|
24
|
+
import BarChartElement from "../../../../UI/Components/Charts/Bar/BarChart";
|
|
25
|
+
import LineChartElement from "../../../../UI/Components/Charts/Line/LineChart";
|
|
26
|
+
import { CHART_DATA_POINT_X_AXIS_KEY } from "../../../../UI/Components/Charts/ChartLibrary/Types/ChartDataPoint";
|
|
27
|
+
import ChartCurve from "../../../../UI/Components/Charts/Types/ChartCurve";
|
|
28
|
+
import DataPoint from "../../../../UI/Components/Charts/Types/DataPoint";
|
|
29
|
+
import SeriesPoint from "../../../../UI/Components/Charts/Types/SeriesPoints";
|
|
30
|
+
import {
|
|
31
|
+
XAxis as ChartXAxis,
|
|
32
|
+
XAxisAggregateType,
|
|
33
|
+
} from "../../../../UI/Components/Charts/Types/XAxis/XAxis";
|
|
34
|
+
import XAxisType from "../../../../UI/Components/Charts/Types/XAxis/XAxisType";
|
|
35
|
+
import YAxis, {
|
|
36
|
+
YAxisPrecision,
|
|
37
|
+
} from "../../../../UI/Components/Charts/Types/YAxis/YAxis";
|
|
38
|
+
import YAxisType from "../../../../UI/Components/Charts/Types/YAxis/YAxisType";
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
* The three chart wrappers hand recharts a FIXED x-axis dataKey, while the
|
|
42
|
+
* rows they hand it are built by DataPointUtil. Those two used to agree only
|
|
43
|
+
* by convention: the util keyed each row by the caller's `xAxis.legend`, and
|
|
44
|
+
* every caller but one happened to pass the literal "Time".
|
|
45
|
+
*
|
|
46
|
+
* The one that did not — the SLO dashboard widget, which passed "" — got a
|
|
47
|
+
* chart with axes, grid and tooltip but no line: recharts looked up "Time" on
|
|
48
|
+
* every row, found nothing, and drew no path. Nothing failed, nothing logged;
|
|
49
|
+
* the widget just rendered an empty frame on public and private dashboards
|
|
50
|
+
* alike.
|
|
51
|
+
*
|
|
52
|
+
* These tests hold the axis key on both sides of that seam: whatever legend a
|
|
53
|
+
* caller passes, the marks get drawn.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
const START: Date = new Date("2026-08-10T00:00:00.000Z");
|
|
57
|
+
const END: Date = new Date("2026-08-10T01:00:00.000Z");
|
|
58
|
+
|
|
59
|
+
/* An hour window buckets at one minute, so these land on distinct buckets. */
|
|
60
|
+
type BuildPointsFunction = () => Array<DataPoint>;
|
|
61
|
+
|
|
62
|
+
const buildPoints: BuildPointsFunction = (): Array<DataPoint> => {
|
|
63
|
+
const points: Array<DataPoint> = [];
|
|
64
|
+
|
|
65
|
+
for (let index: number = 0; index < 10; index++) {
|
|
66
|
+
points.push({
|
|
67
|
+
x: new Date(START.getTime() + index * 60 * 1000),
|
|
68
|
+
y: 90 + index,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return points;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type BuildXAxisFunction = (legend: string) => ChartXAxis;
|
|
76
|
+
|
|
77
|
+
const buildXAxis: BuildXAxisFunction = (legend: string): ChartXAxis => {
|
|
78
|
+
return {
|
|
79
|
+
legend: legend,
|
|
80
|
+
options: {
|
|
81
|
+
type: XAxisType.Time,
|
|
82
|
+
min: START,
|
|
83
|
+
max: END,
|
|
84
|
+
aggregateType: XAxisAggregateType.Average,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const Y_AXIS: YAxis = {
|
|
90
|
+
legend: "%",
|
|
91
|
+
options: {
|
|
92
|
+
type: YAxisType.Number,
|
|
93
|
+
min: "auto",
|
|
94
|
+
max: 100,
|
|
95
|
+
precision: YAxisPrecision.TwoDecimals,
|
|
96
|
+
formatter: (value: number): string => {
|
|
97
|
+
return `${value}%`;
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const SERIES_NAME: string = "SLI";
|
|
103
|
+
|
|
104
|
+
type BuildSeriesFunction = () => Array<SeriesPoint>;
|
|
105
|
+
|
|
106
|
+
const buildSeries: BuildSeriesFunction = (): Array<SeriesPoint> => {
|
|
107
|
+
return [{ seriesName: SERIES_NAME, data: buildPoints() }];
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
* Legends worth covering: the canonical one every other caller passes, the
|
|
112
|
+
* empty string the SLO widget passed (the reported bug), and an arbitrary
|
|
113
|
+
* label, which is what a future caller is most likely to reach for.
|
|
114
|
+
*/
|
|
115
|
+
const LEGENDS: Array<[string, string]> = [
|
|
116
|
+
['the canonical "Time"', "Time"],
|
|
117
|
+
["an empty legend", ""],
|
|
118
|
+
["an arbitrary legend", "Timestamp"],
|
|
119
|
+
["a legend with markup-ish characters", "Time (UTC) <>"],
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
interface ChartUnderTest {
|
|
123
|
+
name: string;
|
|
124
|
+
/** CSS selector for the SVG geometry this chart type draws per series. */
|
|
125
|
+
markSelector: string;
|
|
126
|
+
render: (xAxis: ChartXAxis) => React.ReactElement;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const CHARTS: Array<ChartUnderTest> = [
|
|
130
|
+
{
|
|
131
|
+
name: "LineChart",
|
|
132
|
+
markSelector: "path.recharts-line-curve",
|
|
133
|
+
render: (xAxis: ChartXAxis): React.ReactElement => {
|
|
134
|
+
return (
|
|
135
|
+
<LineChartElement
|
|
136
|
+
data={buildSeries()}
|
|
137
|
+
xAxis={xAxis}
|
|
138
|
+
yAxis={Y_AXIS}
|
|
139
|
+
curve={ChartCurve.MONOTONE}
|
|
140
|
+
heightInPx={300}
|
|
141
|
+
showLegend={false}
|
|
142
|
+
sync={false}
|
|
143
|
+
syncid="x-axis-key-test"
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "AreaChart",
|
|
150
|
+
markSelector: "path.recharts-area-area",
|
|
151
|
+
render: (xAxis: ChartXAxis): React.ReactElement => {
|
|
152
|
+
return (
|
|
153
|
+
<AreaChartElement
|
|
154
|
+
data={buildSeries()}
|
|
155
|
+
xAxis={xAxis}
|
|
156
|
+
yAxis={Y_AXIS}
|
|
157
|
+
curve={ChartCurve.MONOTONE}
|
|
158
|
+
heightInPx={300}
|
|
159
|
+
showLegend={false}
|
|
160
|
+
sync={false}
|
|
161
|
+
syncid="x-axis-key-test"
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
/*
|
|
168
|
+
* Bars are drawn through a custom `shape` renderer, so the mark is a bare
|
|
169
|
+
* <path> under recharts' own rectangle group rather than a classed one.
|
|
170
|
+
*/
|
|
171
|
+
name: "BarChart",
|
|
172
|
+
markSelector: "g.recharts-bar-rectangle path",
|
|
173
|
+
render: (xAxis: ChartXAxis): React.ReactElement => {
|
|
174
|
+
return (
|
|
175
|
+
<BarChartElement
|
|
176
|
+
data={buildSeries()}
|
|
177
|
+
xAxis={xAxis}
|
|
178
|
+
yAxis={Y_AXIS}
|
|
179
|
+
heightInPx={300}
|
|
180
|
+
showLegend={false}
|
|
181
|
+
sync={false}
|
|
182
|
+
syncid="x-axis-key-test"
|
|
183
|
+
/>
|
|
184
|
+
);
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
afterEach((): void => {
|
|
190
|
+
cleanup();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("Chart x-axis data key", () => {
|
|
194
|
+
test("the canonical key is the literal recharts reads", (): void => {
|
|
195
|
+
/*
|
|
196
|
+
* Pinned, not derived. The wrappers and DataPointUtil both import this
|
|
197
|
+
* constant, so a rename would keep them agreeing with each other while
|
|
198
|
+
* silently changing the shape handed to recharts.
|
|
199
|
+
*/
|
|
200
|
+
expect(CHART_DATA_POINT_X_AXIS_KEY).toBe("Time");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
for (const chart of CHARTS) {
|
|
204
|
+
describe(chart.name, () => {
|
|
205
|
+
test.each(LEGENDS)(
|
|
206
|
+
`draws its series with %s`,
|
|
207
|
+
(_label: string, legend: string): void => {
|
|
208
|
+
const { container } = render(chart.render(buildXAxis(legend)));
|
|
209
|
+
|
|
210
|
+
const marks: Array<Element> = Array.from(
|
|
211
|
+
container.querySelectorAll(chart.markSelector),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(marks.length).toBeGreaterThan(0);
|
|
215
|
+
|
|
216
|
+
/*
|
|
217
|
+
* Present-but-empty geometry is the exact failure mode here: a
|
|
218
|
+
* <path> with no `d` renders nothing at all, so the element count
|
|
219
|
+
* alone would not have caught the bug.
|
|
220
|
+
*/
|
|
221
|
+
for (const mark of marks) {
|
|
222
|
+
expect(mark.getAttribute("d")).toBeTruthy();
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
test("draws the same geometry whatever the legend says", (): void => {
|
|
228
|
+
const canonical: RenderResultContainer = render(
|
|
229
|
+
chart.render(buildXAxis("Time")),
|
|
230
|
+
);
|
|
231
|
+
const canonicalPaths: Array<string | null> = Array.from(
|
|
232
|
+
canonical.container.querySelectorAll(chart.markSelector),
|
|
233
|
+
).map((element: Element) => {
|
|
234
|
+
return element.getAttribute("d");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
cleanup();
|
|
238
|
+
|
|
239
|
+
const other: RenderResultContainer = render(
|
|
240
|
+
chart.render(buildXAxis("")),
|
|
241
|
+
);
|
|
242
|
+
const otherPaths: Array<string | null> = Array.from(
|
|
243
|
+
other.container.querySelectorAll(chart.markSelector),
|
|
244
|
+
).map((element: Element) => {
|
|
245
|
+
return element.getAttribute("d");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(otherPaths).toEqual(canonicalPaths);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
interface RenderResultContainer {
|
|
255
|
+
container: HTMLElement;
|
|
256
|
+
}
|