Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions apps/web/src/page/alerts/editor/derive-rows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { AlertDestinationDocument, AlertIncidentDocument } from "@maple/dom
import type { AlertRuleRow } from "@maple/web-data/collections/alerts"
import {
buildRuleStatesByRuleId,
rowToAlertDestinationDocument,
rowsToAlertDestinationDocuments,
rowToAlertIncidentDocument,
rowToAlertRuleDocument,
} from "@maple/web-data/collections/alerts"
Expand All @@ -18,7 +18,7 @@ import type { Model, TraceFacets } from "./model"
// The live rows as the documents the editor reads.

export const destinationsOf = (model: Pick<Model, "destinations">): ReadonlyArray<AlertDestinationDocument> =>
(model.destinations ?? []).map(rowToAlertDestinationDocument)
rowsToAlertDestinationDocuments(model.destinations ?? [])

const NO_STATES = buildRuleStatesByRuleId([])

Expand All @@ -28,7 +28,9 @@ export const tagSuggestionsOf = (
previous: ReadonlyArray<string>,
): ReadonlyArray<string> => {
const tags = [...new Set(rows.flatMap((row) => rowToAlertRuleDocument(row, NO_STATES).tags))].sort()
return tags.length === previous.length && tags.every((tag, index) => tag === previous[index]) ? previous : tags
return tags.length === previous.length && tags.every((tag, index) => tag === previous[index])
? previous
: tags
}

/** The edited rule's open incidents (`clearedIncidents` input). */
Expand All @@ -41,7 +43,13 @@ export const openIncidentsOf = (
.map(rowToAlertIncidentDocument)
.filter((incident) => incident.ruleId === model.editingRuleId && incident.status === "open")

const NO_FACETS: TraceFacets = { services: [], environments: [], spanNames: [], httpMethods: [], httpStatusCodes: [] }
const NO_FACETS: TraceFacets = {
services: [],
environments: [],
spanNames: [],
httpMethods: [],
httpStatusCodes: [],
}

/** The traces facets once answered (`autocompleteValues.traces`). */
export const traceFacetsOf = (model: Pick<Model, "traceFacets">): TraceFacets =>
Expand Down Expand Up @@ -81,7 +89,10 @@ const hasPayload = (model: Pick<Model, "previewWindow">, form: RuleForm): boolea
model.previewWindow !== null && previewPayload(form, model.previewWindow)._tag === "Some"

/** The timing of the draft's own preview: none while a new one loads, so nothing gates on an older query. */
export const draftTimingOf = (model: Pick<Model, "preview" | "previewWindow">, form: RuleForm): AlertQueryTiming | null =>
export const draftTimingOf = (
model: Pick<Model, "preview" | "previewWindow">,
form: RuleForm,
): AlertQueryTiming | null =>
hasPayload(model, form)
? AsyncData.match(model.preview.data, {
onIdle: () => null,
Expand All @@ -94,5 +105,7 @@ export const draftTimingOf = (model: Pick<Model, "preview" | "previewWindow">, f
: null

/** The draft's preview as `useAlertRulePreview` reads it. */
export const draftPreviewOf = (model: Pick<Model, "preview" | "previewWindow">, form: RuleForm): PreviewState =>
previewStateOf(model.preview, hasPayload(model, form), previewIssue(form))
export const draftPreviewOf = (
model: Pick<Model, "preview" | "previewWindow">,
form: RuleForm,
): PreviewState => previewStateOf(model.preview, hasPayload(model, form), previewIssue(form))
4 changes: 2 additions & 2 deletions apps/web/src/page/alerts/hub/derive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { AlertDestinationDocument } from "@maple/domain/http"
import { type AlertsOverviewReady, deriveOverview } from "@maple/web-data/alerts/overview"
import {
buildRuleStatesByRuleId,
rowToAlertDestinationDocument,
rowsToAlertDestinationDocuments,
rowToAlertIncidentDocument,
rowToAlertRuleDocument,
} from "@maple/web-data/collections/alerts"
Expand Down Expand Up @@ -32,7 +32,7 @@ export const overviewOf = (model: Model): Option.Option<AlertsOverviewReady> =>

/** `useAlertDestinationsList()`, `orElse(() => [])`. */
export const destinationsOf = (model: Model): ReadonlyArray<AlertDestinationDocument> =>
(model.destinations ?? []).map(rowToAlertDestinationDocument)
rowsToAlertDestinationDocuments(model.destinations ?? [])

export type AlertsTab = "inbox" | "rules" | "uptime" | "routing"

Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/page/alerts/hub/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ const withoutId = <A extends string>(ids: ReadonlyArray<A>, id: A): ReadonlyArra
ids.filter((existing) => existing !== id)

const destinationById = (model: Model, id: AlertDestinationId) =>
Option.map(
Option.flatMap(
Array.findFirst(model.destinations ?? [], (row) => row.id === id),
rowToAlertDestinationDocument,
)
Expand Down Expand Up @@ -367,7 +367,7 @@ const pickedFromMenu = (model: Model, menuId: string, value: string): Return =>
})
}
return Option.match(
Option.map(
Option.flatMap(
Array.findFirst(model.destinations ?? [], (row) => row.id === target),
rowToAlertDestinationDocument,
),
Expand Down
13 changes: 9 additions & 4 deletions apps/web/src/page/alerts/rule/derive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { V2AlertRulePreviewParams } from "@maple/domain/http/v2"
import { ruleToFormState } from "@maple/web-data/alerts/form-utils"
import {
buildRuleStatesByRuleId,
rowToAlertDestinationDocument,
rowsToAlertDestinationDocuments,
rowToAlertIncidentDocument,
rowToAlertRuleDocument,
} from "@maple/web-data/collections/alerts"
Expand Down Expand Up @@ -54,7 +54,7 @@ export const ruleIncidentsOf = (model: Model): ReadonlyArray<AlertIncidentDocume
})

export const destinationsOf = (model: Model): ReadonlyArray<AlertDestinationDocument> =>
(model.destinations ?? []).map(rowToAlertDestinationDocument)
rowsToAlertDestinationDocuments(model.destinations ?? [])

export const ruleDeliveriesOf = (model: Model): ReadonlyArray<AlertDeliveryEventDocument> =>
AsyncData.getOrElse(model.deliveries.data, () => []).filter((event) => event.ruleId === model.ruleId)
Expand Down Expand Up @@ -108,8 +108,13 @@ export const chartChecksOf = (
})
}

export const payloadOf = (model: Model, rule: AlertRuleDocument | null): Option.Option<V2AlertRulePreviewParams> =>
rule === null || model.window === null ? Option.none() : previewPayload(ruleToFormState(rule), model.window)
export const payloadOf = (
model: Model,
rule: AlertRuleDocument | null,
): Option.Option<V2AlertRulePreviewParams> =>
rule === null || model.window === null
? Option.none()
: previewPayload(ruleToFormState(rule), model.window)

/** The picker's label: a preset's, else the range (not yet zone-aware). */
export const rangeLabelOf = (model: Model): string =>
Expand Down
24 changes: 16 additions & 8 deletions apps/web/src/page/integrations/subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,23 @@ const interimDocument = (label: string) => `<!doctype html>
@media (prefers-color-scheme: dark) { body { background:#1c1917; color:#d6d3d1; } }
</style></head><body>Connecting to ${label}…</body></html>`

/**
* A popup still open from an earlier click is reused by name; once it is on the provider's page it
* is cross-origin and its document throws, so it keeps that page until it is sent to the new URL.
*/
const openPopup = (name: string, features: string, label: string) =>
Effect.sync(() => {
const popup = window.open("", name, features)
if (popup !== null) {
popup.document.write(interimDocument(label))
popup.document.close()
}
return popup
})
Effect.sync(() => window.open("", name, features)).pipe(
Effect.tap((popup) =>
popup === null
? Effect.void
: Effect.ignore(
Effect.try(() => {
popup.document.write(interimDocument(label))
popup.document.close()
}),
),
),
)

/** Polls the popup until it closes: `PolledConnect` every 3 s (polling providers), then `Closed`. */

Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/page/settings/subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { isRunActive } from "./data-platform/update"
import { Message as AutomationMessage } from "./automation/model"
import { destinationRows } from "../alerts/shared/collections"
import { collectionLoadFailed } from "../alerts/shared/collection-load"
import { rowToAlertDestinationDocument } from "@maple/web-data/collections/alerts"
import { rowsToAlertDestinationDocuments } from "@maple/web-data/collections/alerts"
import type { Model } from "./model"
import { pollTicks } from "../../data/poll"

Expand Down Expand Up @@ -99,7 +99,7 @@ export const subscriptions = Subscription.make<PageSubscriptionInput<Model>, Mes
? destinationRows(orgId, (rows) =>
Message.GotAutomationMessage({
message: AutomationMessage.UpdatedDestinations({
destinations: rows.map(rowToAlertDestinationDocument),
destinations: rowsToAlertDestinationDocuments(rows),
}),
}),
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
-- Retire the `hazel-oauth` alert destination (#2022 removed its transport and type), as
Comment thread
Makisuo marked this conversation as resolved.
Resolved
-- 20260924110710_drop_legacy_slack did for `slack-bot`. Rows of a type the code no longer
-- decodes crashed the dashboard's alert and automation pages. Every reference goes with them:
-- a rule left with no destination is disabled, since an enabled rule must name one; an
-- escalation severity left with none is dropped; the error policy may name none.
UPDATE "alert_rules" AS rules
Comment thread
Makisuo marked this conversation as resolved.
Resolved
SET
"destination_ids_json" = COALESCE(
(
SELECT jsonb_agg(destination_id)
FROM jsonb_array_elements_text(CASE WHEN jsonb_typeof(rules."destination_ids_json") = 'array' THEN rules."destination_ids_json" ELSE '[]'::jsonb END) AS ids(destination_id)
WHERE destination_id NOT IN (SELECT "id" FROM "alert_destinations" WHERE "type" = 'hazel-oauth')
),
'[]'::jsonb
),
"enabled" = rules."enabled" AND EXISTS (
SELECT 1
FROM jsonb_array_elements_text(CASE WHEN jsonb_typeof(rules."destination_ids_json") = 'array' THEN rules."destination_ids_json" ELSE '[]'::jsonb END) AS ids(destination_id)
WHERE destination_id NOT IN (SELECT "id" FROM "alert_destinations" WHERE "type" = 'hazel-oauth')
)
WHERE jsonb_typeof(rules."destination_ids_json") = 'array'
AND EXISTS (
SELECT 1
FROM jsonb_array_elements_text(CASE WHEN jsonb_typeof(rules."destination_ids_json") = 'array' THEN rules."destination_ids_json" ELSE '[]'::jsonb END) AS ids(destination_id)
JOIN "alert_destinations" AS destinations ON destinations."id" = destination_id
WHERE destinations."type" = 'hazel-oauth'
);--> statement-breakpoint
-- Each severity tier names its own destinations (the column above is their union); a tier
-- keeps its place even when it is left with none.
UPDATE "alert_rules" AS rules
SET "thresholds_json" = (
SELECT jsonb_agg(
CASE
WHEN jsonb_typeof(tier -> 'destinationIds') = 'array' THEN jsonb_set(
tier,
'{destinationIds}',
COALESCE(
(
SELECT jsonb_agg(destination_id)
FROM jsonb_array_elements_text(CASE WHEN jsonb_typeof(tier -> 'destinationIds') = 'array' THEN tier -> 'destinationIds' ELSE '[]'::jsonb END) AS ids(destination_id)
WHERE destination_id NOT IN (SELECT "id" FROM "alert_destinations" WHERE "type" = 'hazel-oauth')
),
'[]'::jsonb
)
)
ELSE tier
END
ORDER BY tiers.position
)
FROM jsonb_array_elements(CASE WHEN jsonb_typeof(rules."thresholds_json") = 'array' THEN rules."thresholds_json" ELSE '[]'::jsonb END) WITH ORDINALITY AS tiers(tier, position)
)
WHERE jsonb_typeof(rules."thresholds_json") = 'array'
AND jsonb_array_length(CASE WHEN jsonb_typeof(rules."thresholds_json") = 'array' THEN rules."thresholds_json" ELSE '[]'::jsonb END) > 0
AND EXISTS (
SELECT 1
FROM jsonb_array_elements(CASE WHEN jsonb_typeof(rules."thresholds_json") = 'array' THEN rules."thresholds_json" ELSE '[]'::jsonb END) AS tiers(tier)
CROSS JOIN LATERAL jsonb_array_elements_text(CASE WHEN jsonb_typeof(tier -> 'destinationIds') = 'array' THEN tier -> 'destinationIds' ELSE '[]'::jsonb END) AS ids(destination_id)
JOIN "alert_destinations" AS destinations ON destinations."id" = ids.destination_id
WHERE destinations."type" = 'hazel-oauth'
);--> statement-breakpoint
UPDATE "error_notification_policies" AS policies
SET "destination_ids_json" = COALESCE(
(
SELECT jsonb_agg(destination_id)
FROM jsonb_array_elements_text(CASE WHEN jsonb_typeof(policies."destination_ids_json") = 'array' THEN policies."destination_ids_json" ELSE '[]'::jsonb END) AS ids(destination_id)
WHERE destination_id NOT IN (SELECT "id" FROM "alert_destinations" WHERE "type" = 'hazel-oauth')
),
'[]'::jsonb
)
WHERE jsonb_typeof(policies."destination_ids_json") = 'array'
AND EXISTS (
SELECT 1
FROM jsonb_array_elements_text(CASE WHEN jsonb_typeof(policies."destination_ids_json") = 'array' THEN policies."destination_ids_json" ELSE '[]'::jsonb END) AS ids(destination_id)
JOIN "alert_destinations" AS destinations ON destinations."id" = destination_id
WHERE destinations."type" = 'hazel-oauth'
);--> statement-breakpoint
UPDATE "issue_escalation_policies" AS policies
SET "rules_json" = COALESCE(
(
SELECT jsonb_agg(jsonb_set(rule, '{destinationIds}', kept.ids) ORDER BY rules.position)
FROM jsonb_array_elements(CASE WHEN jsonb_typeof(policies."rules_json") = 'array' THEN policies."rules_json" ELSE '[]'::jsonb END) WITH ORDINALITY AS rules(rule, position)
CROSS JOIN LATERAL (
SELECT COALESCE(jsonb_agg(destination_id), '[]'::jsonb) AS ids
FROM jsonb_array_elements_text(CASE WHEN jsonb_typeof(rule -> 'destinationIds') = 'array' THEN rule -> 'destinationIds' ELSE '[]'::jsonb END) AS ids(destination_id)
WHERE destination_id NOT IN (SELECT "id" FROM "alert_destinations" WHERE "type" = 'hazel-oauth')
) AS kept
WHERE jsonb_array_length(kept.ids) > 0
),
'[]'::jsonb
)
WHERE jsonb_typeof(policies."rules_json") = 'array'
AND NOT EXISTS (
SELECT 1
FROM jsonb_array_elements(CASE WHEN jsonb_typeof(policies."rules_json") = 'array' THEN policies."rules_json" ELSE '[]'::jsonb END) AS rules(rule)
WHERE jsonb_typeof(rule -> 'destinationIds') IS DISTINCT FROM 'array'
)
AND EXISTS (
SELECT 1
FROM jsonb_array_elements(CASE WHEN jsonb_typeof(policies."rules_json") = 'array' THEN policies."rules_json" ELSE '[]'::jsonb END) AS rules(rule)
CROSS JOIN LATERAL jsonb_array_elements_text(CASE WHEN jsonb_typeof(rule -> 'destinationIds') = 'array' THEN rule -> 'destinationIds' ELSE '[]'::jsonb END) AS ids(destination_id)
JOIN "alert_destinations" AS destinations ON destinations."id" = ids.destination_id
WHERE destinations."type" = 'hazel-oauth'
);--> statement-breakpoint
DELETE FROM "error_notification_deliveries"
WHERE "destination_id" IN (SELECT "id" FROM "alert_destinations" WHERE "type" = 'hazel-oauth');--> statement-breakpoint
DELETE FROM "alert_delivery_events"
WHERE "destination_id" IN (SELECT "id" FROM "alert_destinations" WHERE "type" = 'hazel-oauth');--> statement-breakpoint
DELETE FROM "alert_destinations"
WHERE "type" = 'hazel-oauth';
Loading
Loading