fix(alerts): add AGENT_LIFECYCLE to condition_kind_enum + readable error toasts
Backend - V18 migration adds AGENT_LIFECYCLE to condition_kind_enum. Java ConditionKind enum shipped with this value but no Postgres migration extended the type, so any AGENT_LIFECYCLE rule insert failed with "invalid input value for enum condition_kind_enum". - ALTER TYPE ... ADD VALUE lives alone in its migration per Postgres constraint that the new value cannot be referenced in the same tx. - V18MigrationIT asserts the enum now contains all 7 kinds. Frontend - Add describeApiError(e) helper to unwrap openapi-fetch error bodies (Spring error JSON) into readable strings. String(e) on a plain object rendered "[object Object]" in toasts — the actual failure reason was hidden from the user. - Replace String(e) in all 13 toast descriptions across the alerting and outbound-connection mutation paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,7 @@ PostgreSQL (Flyway): `cameleer-server-app/src/main/resources/db/migration/`
|
|||||||
- V15 — Discriminate open-instance uniqueness by `context->'exchange'->>'id'` so EXCHANGE_MATCH/PER_EXCHANGE emits one alert_instance per matching exchange; scalar kinds resolve to `''` and keep one-open-per-rule.
|
- V15 — Discriminate open-instance uniqueness by `context->'exchange'->>'id'` so EXCHANGE_MATCH/PER_EXCHANGE emits one alert_instance per matching exchange; scalar kinds resolve to `''` and keep one-open-per-rule.
|
||||||
- V16 — Generalise the V15 discriminator to prefer `context->>'_subjectFingerprint'` (falls back to the V15 `exchange.id` expression for legacy rows). Enables AGENT_LIFECYCLE to emit one alert_instance per `(agent, eventType, timestamp)` via a canonical fingerprint in the evaluator firing's context.
|
- V16 — Generalise the V15 discriminator to prefer `context->>'_subjectFingerprint'` (falls back to the V15 `exchange.id` expression for legacy rows). Enables AGENT_LIFECYCLE to emit one alert_instance per `(agent, eventType, timestamp)` via a canonical fingerprint in the evaluator firing's context.
|
||||||
- V17 — Alerts inbox redesign: drop `ACKNOWLEDGED` from `alert_state_enum` (ack is now orthogonal via `acked_at`), add `read_at` + `deleted_at` timestamp columns (global, no per-user tracking), drop `alert_reads` table entirely, rework the V13/V15/V16 open-rule unique index predicate to `state IN ('PENDING','FIRING') AND deleted_at IS NULL` so ack doesn't close the slot and soft-delete frees it.
|
- V17 — Alerts inbox redesign: drop `ACKNOWLEDGED` from `alert_state_enum` (ack is now orthogonal via `acked_at`), add `read_at` + `deleted_at` timestamp columns (global, no per-user tracking), drop `alert_reads` table entirely, rework the V13/V15/V16 open-rule unique index predicate to `state IN ('PENDING','FIRING') AND deleted_at IS NULL` so ack doesn't close the slot and soft-delete frees it.
|
||||||
|
- V18 — Add `AGENT_LIFECYCLE` to `condition_kind_enum`. The Java `ConditionKind` enum had shipped with this value since the alerting branch, but no migration ever extended the Postgres type — any insert of a rule with `conditionKind=AGENT_LIFECYCLE` failed with `invalid input value for enum condition_kind_enum`. Single-statement migration because `ALTER TYPE ... ADD VALUE` can't coexist with usage of the new value in the same transaction.
|
||||||
|
|
||||||
ClickHouse: `cameleer-server-app/src/main/resources/clickhouse/init.sql` (run idempotently on startup)
|
ClickHouse: `cameleer-server-app/src/main/resources/clickhouse/init.sql` (run idempotently on startup)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
-- V18 — Add AGENT_LIFECYCLE to condition_kind_enum.
|
||||||
|
--
|
||||||
|
-- The Java ConditionKind enum shipped AGENT_LIFECYCLE with the alerting
|
||||||
|
-- branch, but no migration ever extended the Postgres type. Inserting a
|
||||||
|
-- rule with conditionKind=AGENT_LIFECYCLE failed with
|
||||||
|
-- ERROR: invalid input value for enum condition_kind_enum: "AGENT_LIFECYCLE"
|
||||||
|
-- ALTER TYPE ... ADD VALUE must live alone in its migration — Postgres won't
|
||||||
|
-- allow the new value to be referenced in the same transaction that adds it.
|
||||||
|
|
||||||
|
ALTER TYPE condition_kind_enum ADD VALUE IF NOT EXISTS 'AGENT_LIFECYCLE' AFTER 'AGENT_STATE';
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.cameleer.server.app.alerting.storage;
|
||||||
|
|
||||||
|
import com.cameleer.server.app.AbstractPostgresIT;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class V18MigrationIT extends AbstractPostgresIT {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void condition_kind_enum_includes_agent_lifecycle() {
|
||||||
|
var values = jdbcTemplate.queryForList("""
|
||||||
|
SELECT unnest(enum_range(NULL::condition_kind_enum))::text AS v
|
||||||
|
""", String.class);
|
||||||
|
assertThat(values).contains("AGENT_LIFECYCLE");
|
||||||
|
assertThat(values).containsExactlyInAnyOrder(
|
||||||
|
"ROUTE_METRIC", "EXCHANGE_MATCH", "AGENT_STATE", "AGENT_LIFECYCLE",
|
||||||
|
"DEPLOYMENT_STATE", "LOG_PATTERN", "JVM_METRIC");
|
||||||
|
}
|
||||||
|
}
|
||||||
24
ui/src/api/errors.ts
Normal file
24
ui/src/api/errors.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Turn whatever a fetch/mutation threw into a user-readable string.
|
||||||
|
*
|
||||||
|
* openapi-fetch rethrows the parsed response body as-is (a plain object like
|
||||||
|
* `{ error, message, path, status, timestamp }` for Spring errors), so
|
||||||
|
* `String(e)` renders "[object Object]" in a toast. This helper prefers an
|
||||||
|
* Error.message, falls back to a `message` field on plain objects, then
|
||||||
|
* Spring's `error` field, and finally JSON-stringifies the rest.
|
||||||
|
*/
|
||||||
|
export function describeApiError(e: unknown): string {
|
||||||
|
if (e instanceof Error) return e.message;
|
||||||
|
if (typeof e === 'string') return e;
|
||||||
|
if (typeof e === 'object' && e !== null) {
|
||||||
|
const obj = e as Record<string, unknown>;
|
||||||
|
if (typeof obj.message === 'string' && obj.message) return obj.message;
|
||||||
|
if (typeof obj.error === 'string' && obj.error) return obj.error;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(e);
|
||||||
|
} catch {
|
||||||
|
return String(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return String(e);
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
type TrustMode,
|
type TrustMode,
|
||||||
} from '../../api/queries/admin/outboundConnections';
|
} from '../../api/queries/admin/outboundConnections';
|
||||||
import { useEnvironments } from '../../api/queries/admin/environments';
|
import { useEnvironments } from '../../api/queries/admin/environments';
|
||||||
|
import { describeApiError } from '../../api/errors';
|
||||||
import sectionStyles from '../../styles/section-card.module.css';
|
import sectionStyles from '../../styles/section-card.module.css';
|
||||||
|
|
||||||
// ── Form state ──────────────────────────────────────────────────────────
|
// ── Form state ──────────────────────────────────────────────────────────
|
||||||
@@ -145,12 +146,12 @@ export default function OutboundConnectionEditor() {
|
|||||||
toast({ title: 'Created', description: form.name, variant: 'success' });
|
toast({ title: 'Created', description: form.name, variant: 'success' });
|
||||||
navigate('/admin/outbound-connections');
|
navigate('/admin/outbound-connections');
|
||||||
},
|
},
|
||||||
onError: (e) => toast({ title: 'Create failed', description: String(e), variant: 'error' }),
|
onError: (e) => toast({ title: 'Create failed', description: describeApiError(e), variant: 'error' }),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
updateMut.mutate(payload, {
|
updateMut.mutate(payload, {
|
||||||
onSuccess: () => toast({ title: 'Updated', description: form.name, variant: 'success' }),
|
onSuccess: () => toast({ title: 'Updated', description: form.name, variant: 'success' }),
|
||||||
onError: (e) => toast({ title: 'Update failed', description: String(e), variant: 'error' }),
|
onError: (e) => toast({ title: 'Update failed', description: describeApiError(e), variant: 'error' }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
type OutboundConnectionDto,
|
type OutboundConnectionDto,
|
||||||
type TrustMode,
|
type TrustMode,
|
||||||
} from '../../api/queries/admin/outboundConnections';
|
} from '../../api/queries/admin/outboundConnections';
|
||||||
|
import { describeApiError } from '../../api/errors';
|
||||||
import sectionStyles from '../../styles/section-card.module.css';
|
import sectionStyles from '../../styles/section-card.module.css';
|
||||||
|
|
||||||
export default function OutboundConnectionsPage() {
|
export default function OutboundConnectionsPage() {
|
||||||
@@ -23,7 +24,7 @@ export default function OutboundConnectionsPage() {
|
|||||||
if (!confirm(`Delete outbound connection "${c.name}"?`)) return;
|
if (!confirm(`Delete outbound connection "${c.name}"?`)) return;
|
||||||
deleteMut.mutate(c.id, {
|
deleteMut.mutate(c.id, {
|
||||||
onSuccess: () => toast({ title: 'Deleted', description: c.name, variant: 'success' }),
|
onSuccess: () => toast({ title: 'Deleted', description: c.name, variant: 'success' }),
|
||||||
onError: (e) => toast({ title: 'Delete failed', description: String(e), variant: 'error' }),
|
onError: (e) => toast({ title: 'Delete failed', description: describeApiError(e), variant: 'error' }),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
type AlertDto,
|
type AlertDto,
|
||||||
} from '../../api/queries/alerts';
|
} from '../../api/queries/alerts';
|
||||||
import { useCreateSilence } from '../../api/queries/alertSilences';
|
import { useCreateSilence } from '../../api/queries/alertSilences';
|
||||||
|
import { describeApiError } from '../../api/errors';
|
||||||
import { useAuthStore } from '../../auth/auth-store';
|
import { useAuthStore } from '../../auth/auth-store';
|
||||||
import { SilenceRuleMenu } from './SilenceRuleMenu';
|
import { SilenceRuleMenu } from './SilenceRuleMenu';
|
||||||
import { severityToAccent } from './severity-utils';
|
import { severityToAccent } from './severity-utils';
|
||||||
@@ -178,7 +179,7 @@ export default function InboxPage() {
|
|||||||
await ack.mutateAsync(id);
|
await ack.mutateAsync(id);
|
||||||
toast({ title: 'Acknowledged', description: title, variant: 'success' });
|
toast({ title: 'Acknowledged', description: title, variant: 'success' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: 'Ack failed', description: String(e), variant: 'error' });
|
toast({ title: 'Ack failed', description: describeApiError(e), variant: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -187,7 +188,7 @@ export default function InboxPage() {
|
|||||||
await markRead.mutateAsync(id);
|
await markRead.mutateAsync(id);
|
||||||
toast({ title: 'Marked as read', variant: 'success' });
|
toast({ title: 'Marked as read', variant: 'success' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: 'Mark read failed', description: String(e), variant: 'error' });
|
toast({ title: 'Mark read failed', description: describeApiError(e), variant: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -210,7 +211,7 @@ export default function InboxPage() {
|
|||||||
.mutateAsync(id)
|
.mutateAsync(id)
|
||||||
.then(
|
.then(
|
||||||
() => toast({ title: 'Restored', variant: 'success' }),
|
() => toast({ title: 'Restored', variant: 'success' }),
|
||||||
(e: unknown) => toast({ title: 'Undo failed', description: String(e), variant: 'error' }),
|
(e: unknown) => toast({ title: 'Undo failed', description: describeApiError(e), variant: 'error' }),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -219,7 +220,7 @@ export default function InboxPage() {
|
|||||||
) as unknown as string; // DS description accepts ReactNode at runtime
|
) as unknown as string; // DS description accepts ReactNode at runtime
|
||||||
toast({ title: 'Deleted', description: undoNode, variant: 'success', duration: 5000 });
|
toast({ title: 'Deleted', description: undoNode, variant: 'success', duration: 5000 });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: 'Delete failed', description: String(e), variant: 'error' });
|
toast({ title: 'Delete failed', description: describeApiError(e), variant: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -230,7 +231,7 @@ export default function InboxPage() {
|
|||||||
setSelected(new Set());
|
setSelected(new Set());
|
||||||
toast({ title: `Acknowledged ${ids.length} alert${ids.length === 1 ? '' : 's'}`, variant: 'success' });
|
toast({ title: `Acknowledged ${ids.length} alert${ids.length === 1 ? '' : 's'}`, variant: 'success' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: 'Bulk ack failed', description: String(e), variant: 'error' });
|
toast({ title: 'Bulk ack failed', description: describeApiError(e), variant: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -241,7 +242,7 @@ export default function InboxPage() {
|
|||||||
setSelected(new Set());
|
setSelected(new Set());
|
||||||
toast({ title: `Marked ${ids.length} as read`, variant: 'success' });
|
toast({ title: `Marked ${ids.length} as read`, variant: 'success' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: 'Bulk read failed', description: String(e), variant: 'error' });
|
toast({ title: 'Bulk read failed', description: describeApiError(e), variant: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useUsers, useGroups, useRoles } from '../../../api/queries/admin/rbac';
|
|||||||
import { useOutboundConnections } from '../../../api/queries/admin/outboundConnections';
|
import { useOutboundConnections } from '../../../api/queries/admin/outboundConnections';
|
||||||
import { useSelectedEnv } from '../../../api/queries/alertMeta';
|
import { useSelectedEnv } from '../../../api/queries/alertMeta';
|
||||||
import { useRenderPreview } from '../../../api/queries/alertRules';
|
import { useRenderPreview } from '../../../api/queries/alertRules';
|
||||||
|
import { describeApiError } from '../../../api/errors';
|
||||||
import type { FormState } from './form-state';
|
import type { FormState } from './form-state';
|
||||||
|
|
||||||
type TargetKind = FormState['targets'][number]['kind'];
|
type TargetKind = FormState['targets'][number]['kind'];
|
||||||
@@ -41,7 +42,7 @@ export function NotifyStep({
|
|||||||
const res = await preview.mutateAsync({ id: ruleId, req: {} });
|
const res = await preview.mutateAsync({ id: ruleId, req: {} });
|
||||||
setLastPreview(`TITLE:\n${res.title ?? ''}\n\nMESSAGE:\n${res.message ?? ''}`);
|
setLastPreview(`TITLE:\n${res.title ?? ''}\n\nMESSAGE:\n${res.message ?? ''}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: 'Preview failed', description: String(e), variant: 'error' });
|
toast({ title: 'Preview failed', description: describeApiError(e), variant: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { prefillFromPromotion, type PrefillWarning } from './promotion-prefill';
|
|||||||
import { useCatalog } from '../../../api/queries/catalog';
|
import { useCatalog } from '../../../api/queries/catalog';
|
||||||
import { useOutboundConnections } from '../../../api/queries/admin/outboundConnections';
|
import { useOutboundConnections } from '../../../api/queries/admin/outboundConnections';
|
||||||
import { useSelectedEnv } from '../../../api/queries/alertMeta';
|
import { useSelectedEnv } from '../../../api/queries/alertMeta';
|
||||||
|
import { describeApiError } from '../../../api/errors';
|
||||||
import sectionStyles from '../../../styles/section-card.module.css';
|
import sectionStyles from '../../../styles/section-card.module.css';
|
||||||
import css from './wizard.module.css';
|
import css from './wizard.module.css';
|
||||||
|
|
||||||
@@ -128,7 +129,7 @@ export default function RuleEditorWizard() {
|
|||||||
}
|
}
|
||||||
navigate('/alerts/rules');
|
navigate('/alerts/rules');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: 'Save failed', description: String(e), variant: 'error' });
|
toast({ title: 'Save failed', description: describeApiError(e), variant: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button, FormField, Input, useToast } from '@cameleer/design-system';
|
import { Button, FormField, Input, useToast } from '@cameleer/design-system';
|
||||||
import { useTestEvaluate } from '../../../api/queries/alertRules';
|
import { useTestEvaluate } from '../../../api/queries/alertRules';
|
||||||
|
import { describeApiError } from '../../../api/errors';
|
||||||
import type { FormState } from './form-state';
|
import type { FormState } from './form-state';
|
||||||
|
|
||||||
export function TriggerStep({
|
export function TriggerStep({
|
||||||
@@ -25,7 +26,7 @@ export function TriggerStep({
|
|||||||
const result = await testEvaluate.mutateAsync({ id: ruleId, req: {} });
|
const result = await testEvaluate.mutateAsync({ id: ruleId, req: {} });
|
||||||
setLastResult(JSON.stringify(result, null, 2));
|
setLastResult(JSON.stringify(result, null, 2));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: 'Test-evaluate failed', description: String(e), variant: 'error' });
|
toast({ title: 'Test-evaluate failed', description: describeApiError(e), variant: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from '../../api/queries/alertRules';
|
} from '../../api/queries/alertRules';
|
||||||
import { useEnvironments } from '../../api/queries/admin/environments';
|
import { useEnvironments } from '../../api/queries/admin/environments';
|
||||||
import { useSelectedEnv } from '../../api/queries/alertMeta';
|
import { useSelectedEnv } from '../../api/queries/alertMeta';
|
||||||
|
import { describeApiError } from '../../api/errors';
|
||||||
import tableStyles from '../../styles/table-section.module.css';
|
import tableStyles from '../../styles/table-section.module.css';
|
||||||
import css from './alerts-page.module.css';
|
import css from './alerts-page.module.css';
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ export default function RulesListPage() {
|
|||||||
await setEnabled.mutateAsync({ id: r.id!, enabled: !r.enabled });
|
await setEnabled.mutateAsync({ id: r.id!, enabled: !r.enabled });
|
||||||
toast({ title: r.enabled ? 'Disabled' : 'Enabled', description: r.name, variant: 'success' });
|
toast({ title: r.enabled ? 'Disabled' : 'Enabled', description: r.name, variant: 'success' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: 'Toggle failed', description: String(e), variant: 'error' });
|
toast({ title: 'Toggle failed', description: describeApiError(e), variant: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ export default function RulesListPage() {
|
|||||||
await deleteRule.mutateAsync(pendingDelete.id!);
|
await deleteRule.mutateAsync(pendingDelete.id!);
|
||||||
toast({ title: 'Deleted', description: pendingDelete.name, variant: 'success' });
|
toast({ title: 'Deleted', description: pendingDelete.name, variant: 'success' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: 'Delete failed', description: String(e), variant: 'error' });
|
toast({ title: 'Delete failed', description: describeApiError(e), variant: 'error' });
|
||||||
} finally {
|
} finally {
|
||||||
setPendingDelete(null);
|
setPendingDelete(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router';
|
|||||||
import { Button, Dropdown, useToast } from '@cameleer/design-system';
|
import { Button, Dropdown, useToast } from '@cameleer/design-system';
|
||||||
import type { DropdownItem } from '@cameleer/design-system';
|
import type { DropdownItem } from '@cameleer/design-system';
|
||||||
import { useCreateSilence } from '../../api/queries/alertSilences';
|
import { useCreateSilence } from '../../api/queries/alertSilences';
|
||||||
|
import { describeApiError } from '../../api/errors';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
ruleId: string;
|
ruleId: string;
|
||||||
@@ -37,7 +38,7 @@ export function SilenceRuleMenu({ ruleId, ruleTitle, onDone, variant = 'row' }:
|
|||||||
toast({ title: `Silenced for ${hours}h`, variant: 'success' });
|
toast({ title: `Silenced for ${hours}h`, variant: 'success' });
|
||||||
onDone?.();
|
onDone?.();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: 'Silence failed', description: String(e), variant: 'error' });
|
toast({ title: 'Silence failed', description: describeApiError(e), variant: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
useDeleteSilence,
|
useDeleteSilence,
|
||||||
type AlertSilenceResponse,
|
type AlertSilenceResponse,
|
||||||
} from '../../api/queries/alertSilences';
|
} from '../../api/queries/alertSilences';
|
||||||
|
import { describeApiError } from '../../api/errors';
|
||||||
import sectionStyles from '../../styles/section-card.module.css';
|
import sectionStyles from '../../styles/section-card.module.css';
|
||||||
import tableStyles from '../../styles/table-section.module.css';
|
import tableStyles from '../../styles/table-section.module.css';
|
||||||
import css from './alerts-page.module.css';
|
import css from './alerts-page.module.css';
|
||||||
@@ -63,7 +64,7 @@ export default function SilencesPage() {
|
|||||||
setHours(1);
|
setHours(1);
|
||||||
toast({ title: 'Silence created', variant: 'success' });
|
toast({ title: 'Silence created', variant: 'success' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: 'Create failed', description: String(e), variant: 'error' });
|
toast({ title: 'Create failed', description: describeApiError(e), variant: 'error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ export default function SilencesPage() {
|
|||||||
await remove.mutateAsync(pendingEnd.id!);
|
await remove.mutateAsync(pendingEnd.id!);
|
||||||
toast({ title: 'Silence removed', variant: 'success' });
|
toast({ title: 'Silence removed', variant: 'success' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({ title: 'Remove failed', description: String(e), variant: 'error' });
|
toast({ title: 'Remove failed', description: describeApiError(e), variant: 'error' });
|
||||||
} finally {
|
} finally {
|
||||||
setPendingEnd(null);
|
setPendingEnd(null);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user