fix(incidents): viewer token access, reopen, multi-field update and status confirms (#15994)

This commit is contained in:
uddhavdave
2026-10-29 10:16:49 +00:00
committed by GitHub
parent e833044c29
commit a558edd763
28 changed files with 321 additions and 70 deletions
@@ -84,10 +84,9 @@ impl IncidentStatus {
}
}
/// Update payload enum for incident field updates
/// Ensures only one field can be updated per request
/// One field per request; `deny_unknown_fields` rejects a payload that sends a second one.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(untagged)]
#[serde(untagged, deny_unknown_fields)]
pub enum UpdatePayload {
/// Update incident title
Title { title: String },
@@ -2305,6 +2304,21 @@ mod tests {
}
}
#[test]
fn test_update_payload_rejects_a_second_field() {
for json in [
r#"{"title":"x","status":"resolved"}"#,
r#"{"severity":"P4","status":"resolved"}"#,
r#"{"title":"x","severity":"P1"}"#,
r#"{"status":"resolved","note":"x"}"#,
] {
assert!(
serde_json::from_str::<UpdatePayload>(json).is_err(),
"{json} must not deserialize to one field"
);
}
}
#[test]
fn test_update_status_serde_roundtrip() {
for status in [
+4
View File
@@ -3112,6 +3112,10 @@ pub async fn update_status_as(
} else {
infra::table::alert_incidents::update_status(org_id, incident_id, status).await?
};
// The ack guard left a resolved row alone: record and replicate nothing.
if status == "acknowledged" && updated.status != "acknowledged" {
return model_to_incident(updated).await;
}
if status == "acknowledged" && updated.acknowledged_by.as_deref() == Some(user_id) {
ack_incident_alerts(org_id, incident_id, user_id).await;
}
+21
View File
@@ -79,6 +79,7 @@ struct PendingMigrations {
ai_chat_shares: bool,
fleet: bool,
public_dashboards: bool,
incident_integrations: bool,
missing_ownership: bool,
profiles: bool,
}
@@ -575,6 +576,9 @@ fn all_org_ownership_keys(pending: &PendingMigrations) -> Vec<&'static str> {
if pending.profiles {
keys.push("profiles");
}
if pending.incident_integrations {
keys.push("incident_integrations");
}
keys
}
@@ -766,6 +770,11 @@ fn pending_migrations(latest: &str, existing: &str) -> PendingMigrations {
log::info!("[OFGA:Local] profiles stream permissions migration needed");
pending.profiles = true;
}
let v0_0_62 = version_compare::Version::from("0.0.62").unwrap();
if meta_version >= v0_0_62 && existing_model_version < v0_0_62 {
log::info!("[OFGA:Local] incident integrations permissions migration needed");
pending.incident_integrations = true;
}
pending
}
@@ -884,4 +893,16 @@ mod tests {
);
assert!(OFGA_MODELS.contains_key("profiles"));
}
#[test]
fn incident_integrations_back_fill_runs_once_across_0_0_62() {
let pending = pending_migrations("0.0.62", "0.0.61");
assert!(pending.incident_integrations && !pending.profiles);
assert_eq!(
all_org_ownership_keys(&pending),
vec!["incident_integrations"]
);
assert!(!pending_migrations("0.0.62", "0.0.62").incident_integrations);
assert!(!pending_migrations("0.0.61", "0.0.60").incident_integrations);
}
}
+35 -3
View File
@@ -75,6 +75,13 @@ impl RemovalTarget {
}
}
/// True when `requested` is not the same set of custom roles as `current`.
#[cfg(feature = "enterprise")]
fn custom_roles_differ(requested: &[String], current: &[String]) -> bool {
let requested: std::collections::HashSet<&String> = requested.iter().collect();
requested != current.iter().collect()
}
fn redact_token(token: &str) -> String {
if token.is_empty() {
return String::new();
@@ -501,9 +508,20 @@ pub async fn update_user(
is_org_updated |= local_user.role.ne(&new_user.role);
#[cfg(feature = "enterprise")]
if let Some(cr) = new_org_role.custom_role {
custom_roles_need_change = true;
custom_roles.extend(cr);
is_org_updated = true;
// Only an admin grants custom roles; a self-update may resend its own unchanged.
if update_mode.is_self_update()
&& !matches!(local_user.role, UserRole::Admin | UserRole::Root)
&& custom_roles_differ(
&cr,
&o2_openfga::authorizer::roles::get_roles_for_org_user(org_id, email).await,
)
{
message = "Self custom roles cannot be changed";
} else {
custom_roles_need_change = true;
custom_roles.extend(cr);
is_org_updated = true;
}
}
}
}
@@ -1577,6 +1595,20 @@ mod tests {
// Mutex to ensure test setup is serialized to prevent race conditions
static TEST_SETUP_LOCK: tokio::sync::OnceCell<Mutex<()>> = tokio::sync::OnceCell::const_new();
#[cfg(feature = "enterprise")]
#[test]
fn a_self_update_may_resend_its_custom_roles_but_not_change_them() {
let current = ["a".to_string(), "b".to_string()];
assert!(!custom_roles_differ(&["b".into(), "a".into()], &current));
assert!(custom_roles_differ(
&["a".into(), "b".into(), "c".into()],
&current
));
assert!(custom_roles_differ(&["a".into()], &current));
assert!(custom_roles_differ(&["c".into()], &[]));
assert!(!custom_roles_differ(&[], &[]));
}
#[test]
fn test_redact_token_exactly_four_chars() {
// 4 visible chars + 8 stars = 12 total
+48 -1
View File
@@ -327,7 +327,7 @@ pub async fn update_status(
update_status_with(get_orm_client_rw().await, org_id, id, status).await
}
/// [`update_status`] on a given connection; a resolve ends the downtime mute.
/// [`update_status`] on a given connection; resolve ends the mute, reopen clears resolve and ack.
pub async fn update_status_with<C: ConnectionTrait>(
conn: &C,
org_id: &str,
@@ -343,6 +343,10 @@ pub async fn update_status_with<C: ConnectionTrait>(
.map_err(|e| Error::DbError(DbError::SeaORMError(e.to_string())))?
.ok_or_else(|| Error::DbError(DbError::SeaORMError("Incident not found".to_string())))?;
// Like `acknowledge()`, an ack only takes an open incident, so a replica cannot undo a resolve.
if status == "acknowledged" && incident.status != "open" {
return Ok(incident);
}
let mut active: alert_incidents::ActiveModel = incident.into();
active.status = Set(status.to_string());
active.updated_at = Set(now);
@@ -351,6 +355,10 @@ pub async fn update_status_with<C: ConnectionTrait>(
active.resolved_at = Set(Some(now));
// A manual reopen must run as a live incident, not stay muted forever.
active.muted_by_downtime_id = Set(None);
} else if status == "open" {
active.resolved_at = Set(None);
active.acknowledged_by = Set(None);
active.acknowledged_at = Set(None);
}
active
@@ -1459,6 +1467,45 @@ mod tests {
);
}
#[tokio::test]
async fn a_reopen_clears_the_resolve_and_the_acknowledgement() {
let db = incidents_db().await;
let mut incident = new_incident(
"acme",
"P2",
serde_json::json!({"service": "payments"}),
"service",
1,
None,
None,
);
incident.status = Set("acknowledged".to_string());
incident.acknowledged_by = Set(Some("ana@example.com".to_string()));
incident.acknowledged_at = Set(Some(2));
let incident = incident.insert(&db).await.unwrap();
let resolved = update_status_with(&db, "acme", &incident.id, "resolved")
.await
.unwrap();
assert!(resolved.resolved_at.is_some());
assert_eq!(resolved.acknowledged_by.as_deref(), Some("ana@example.com"));
// A replicated ack never clears the resolve; only a reopen does.
let acked = update_status_with(&db, "acme", &incident.id, "acknowledged")
.await
.unwrap();
assert_eq!(acked.status, "resolved");
assert_eq!(acked.resolved_at, resolved.resolved_at);
let reopened = update_status_with(&db, "acme", &incident.id, "open")
.await
.unwrap();
assert_eq!(reopened.status, "open");
assert_eq!(reopened.resolved_at, None);
assert_eq!(reopened.acknowledged_by, None);
assert_eq!(reopened.acknowledged_at, None);
}
#[tokio::test]
async fn the_list_carries_the_muting_downtime_for_the_muted_chip() {
let db = incidents_db().await;
@@ -207,6 +207,7 @@ export class AlertsPage {
alertCloneButton: '[data-test="alert-list-{alertName}-clone-alert"]',
alertMoreOptions: '[data-test="alert-list-{alertName}-more-options"]',
confirmButton: '[data-test="confirm-dialog"] [data-test="o-dialog-primary-btn"]',
confirmDialogProviderPrimary: '[data-test="confirm-dialog-provider"] [data-test="o-dialog-primary-btn"]',
// Alert movement locators
selectAllCheckboxRowName: '# Name Owner Period Frequency',
@@ -2388,6 +2389,7 @@ export class AlertsPage {
testLogger.info(`Target lifecycle row index: ${targetIndex}`);
await allRows.nth(targetIndex).locator(this.locators.incidentAckButton).click();
await this.page.locator(this.locators.confirmDialogProviderPrimary).click();
await this.page.waitForTimeout(1000);
testLogger.info('Clicked acknowledge button');
}
@@ -2409,6 +2411,7 @@ export class AlertsPage {
const resolveButton = row.locator(this.locators.incidentResolveButton);
await resolveButton.waitFor({ state: 'visible', timeout: 10000 });
await resolveButton.click();
await this.page.locator(this.locators.confirmDialogProviderPrimary).click();
await this.page.waitForTimeout(1000);
testLogger.info('Clicked resolve button');
}
@@ -2422,6 +2425,7 @@ export class AlertsPage {
const reopenButton = row.locator(this.locators.incidentReopenButton);
await reopenButton.waitFor({ state: 'visible', timeout: 10000 });
await reopenButton.click();
await this.page.locator(this.locators.confirmDialogProviderPrimary).click();
await this.page.waitForTimeout(1000);
testLogger.info('Clicked reopen button');
}
@@ -487,6 +487,49 @@ describe("IncidentDetailDrawer.vue", () => {
expect(incidentsService.updateStatus).toHaveBeenCalledWith("default", "1", "open");
});
it.each([
["resolveIncident", "Resolve incident?"],
["reopenIncident", "Reopen incident?"],
])("%s asks first and changes nothing when cancelled", async (action, title) => {
mockConfirm.mockResolvedValueOnce(false);
await wrapper.vm[action]();
expect(mockConfirm).toHaveBeenLastCalledWith(expect.objectContaining({ title }));
expect(incidentsService.updateStatus).not.toHaveBeenCalled();
});
it.each([
["resolved", "Resolve incident?"],
["open", "Reopen incident?"],
])(
"the Manage chip for %s asks first and changes nothing when cancelled",
async (status, title) => {
mockConfirm.mockResolvedValueOnce(false);
await wrapper.vm.handleStatusChange(status);
expect(mockConfirm).toHaveBeenLastCalledWith(expect.objectContaining({ title }));
expect(incidentsService.updateStatus).not.toHaveBeenCalled();
},
);
it("a reopen from the Manage chip drops the cleared resolve and acknowledgement", async () => {
wrapper.vm.incidentDetails.status = "resolved";
wrapper.vm.incidentDetails.resolved_at = 1700000000000000;
wrapper.vm.incidentDetails.acknowledged_by = "ana@example.com";
vi.mocked(incidentsService.updateStatus).mockResolvedValueOnce({
data: { status: "open", resolved_at: null, acknowledged_by: null, acknowledged_at: null },
} as never);
await wrapper.vm.handleStatusChange("open");
expect(incidentsService.updateStatus).toHaveBeenCalledWith("default", "1", "open");
expect(wrapper.vm.incidentDetails.status).toBe("open");
expect(wrapper.vm.incidentDetails.resolved_at).toBeUndefined();
expect(wrapper.vm.incidentDetails.acknowledged_by).toBeNull();
});
it("should set updating state during status update", async () => {
// acknowledgeIncident awaits the confirm dialog before it ever touches
// `updating`, so a fixed number of microtask hops can't be assumed here —
@@ -2692,6 +2692,7 @@ export default defineComponent({
incidentDetails.value.status = response.data.status;
incidentDetails.value.acknowledged_by = response.data.acknowledged_by;
incidentDetails.value.acknowledged_at = response.data.acknowledged_at;
incidentDetails.value.resolved_at = response.data.resolved_at ?? undefined;
incidentDetails.value.updated_at = response.data.updated_at || Date.now() * 1000;
editableStatus.value = response.data.status;
toast({
@@ -2734,8 +2735,29 @@ export default defineComponent({
// The caller's promise must cover the update, not just the confirm dialog.
return updateStatus("acknowledged");
};
const resolveIncident = () => updateStatus("resolved");
const reopenIncident = () => updateStatus("open");
// Resolving closes the incident, so it confirms like Acknowledge; Reopen too.
const resolveIncident = async () => {
const ok = await confirm({
title: t("alerts.incidents.resolveConfirmTitle"),
message: t("alerts.incidents.resolveConfirmMessage"),
confirmLabel: t("alerts.incidents.resolve"),
cancelLabel: t("common.cancel"),
persistent: false,
});
if (!ok) return;
return updateStatus("resolved");
};
const reopenIncident = async () => {
const ok = await confirm({
title: t("alerts.incidents.reopenConfirmTitle"),
message: t("alerts.incidents.reopenConfirmMessage"),
confirmLabel: t("alerts.incidents.reopen"),
cancelLabel: t("common.cancel"),
persistent: false,
});
if (!ok) return;
return updateStatus("open");
};
// Title editing. OInlineEdit owns the open/close and the Escape-restores
// behaviour; this only has to persist a committed value. `isEditingTitle`
@@ -2883,45 +2905,15 @@ export default defineComponent({
return typeof conditionData === "string" ? conditionData : "--";
};
// Handle status change from dropdown
const handleStatusChange = async (newStatus: "open" | "acknowledged" | "resolved") => {
if (!incidentDetails.value || updating.value) return;
updating.value = true;
try {
const response = await updateIncidentStatus.mutateAsync({
id: incidentDetails.value.id,
status: newStatus,
});
// Update local state with the actual status from the API response
incidentDetails.value.status = response.data.status;
editableStatus.value = response.data.status;
toast({
variant: "success",
message: t("toastMessages.alerts.incidentStatusUpdatedTo", {
status: response.data.status,
}),
});
// Mark data as stale so incident list will refresh
store.dispatch("incidents/setShouldRefresh", true);
// Refresh events in timeline if Activity tab is active
if (activeTab.value === "activity") {
timelineRefreshTrigger.value++;
}
} catch (error: any) {
console.error("Failed to update status:", error);
toast({
variant: "error",
message: error?.response?.data?.message || t("alerts.incidents.statusUpdateFailed"),
});
// Revert on error
editableStatus.value = incidentDetails.value.status;
} finally {
updating.value = false;
}
// The Manage panel chips take the same confirmed path as the header buttons.
const handleStatusChange = (newStatus: "open" | "acknowledged" | "resolved") => {
if (updating.value) return;
const actions = {
open: reopenIncident,
acknowledged: acknowledgeIncident,
resolved: resolveIncident,
};
return actions[newStatus]();
};
// Handle trigger row click - find the alert index by name
@@ -460,6 +460,22 @@ describe("IncidentList.vue", () => {
expect(incidentsService.updateStatus).toHaveBeenCalledWith("default", incident.id, "open");
});
it.each([
["resolveIncident", "Resolve incident?"],
["reopenIncident", "Reopen incident?"],
] as const)("%s asks first and changes nothing when cancelled", async (action, title) => {
// The instance exposes setup() bindings that the mounted type does not declare.
const vm = wrapper.vm as unknown as Record<
typeof action,
(incident: Incident) => Promise<void>
> & { allIncidents: Incident[] };
mockConfirm.mockResolvedValueOnce(false);
await vm[action](vm.allIncidents[0]);
await flushPromises();
expect(mockConfirm).toHaveBeenLastCalledWith(expect.objectContaining({ title }));
expect(incidentsService.updateStatus).not.toHaveBeenCalled();
});
it("reloads incidents after successful status update", async () => {
const incident = (wrapper.vm as any).allIncidents[0];
vi.clearAllMocks();
+18 -2
View File
@@ -914,11 +914,27 @@ export default defineComponent({
updateStatus(incident, "acknowledged");
};
const resolveIncident = (incident: Incident) => {
const resolveIncident = async (incident: Incident) => {
const ok = await confirm({
title: t("alerts.incidents.resolveConfirmTitle"),
message: t("alerts.incidents.resolveConfirmMessage"),
confirmLabel: t("alerts.incidents.resolve"),
cancelLabel: t("common.cancel"),
persistent: false,
});
if (!ok) return;
updateStatus(incident, "resolved");
};
const reopenIncident = (incident: Incident) => {
const reopenIncident = async (incident: Incident) => {
const ok = await confirm({
title: t("alerts.incidents.reopenConfirmTitle"),
message: t("alerts.incidents.reopenConfirmMessage"),
confirmLabel: t("alerts.incidents.reopen"),
cancelLabel: t("common.cancel"),
persistent: false,
});
if (!ok) return;
updateStatus(incident, "open");
};
@@ -60,6 +60,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:columns="columns"
row-key="email"
:loading="props.loading"
:forbidden="forbidden"
:global-filter="userSearchKey"
pagination="client"
:page-size="100"
@@ -158,6 +159,9 @@ const userSearchKey = ref("");
const hasFetchedOrgServiceAccounts = ref(false);
// A viewer cannot list service accounts; show the no-access state instead of failing.
const forbidden = ref(false);
const groupUsersMap = ref(new Set());
const { serviceAccountsState } = usePermissions();
@@ -220,10 +224,18 @@ const updateUserTable = async (value: string) => {
const fetchOrgServiceAccounts = async () => {
// fetch group users
hasFetchedOrgServiceAccounts.value = true;
const data: any = await serviceAccountsState.getServiceAccounts(
store.state.selectedOrganization.identifier,
);
forbidden.value = false;
let data: Array<{ email: string; is_system?: boolean }>;
try {
data = (await serviceAccountsState.getServiceAccounts(
store.state.selectedOrganization.identifier,
)) as typeof data;
} catch (e: unknown) {
const status = (e as { response?: { status?: number } } | undefined)?.response?.status;
if (status !== 403) throw e;
forbidden.value = true;
data = [];
}
serviceAccountsState.service_accounts_users = cloneDeep(
data
.filter((user: any) => user.is_system !== true) // Filter out system accounts
@@ -112,6 +112,7 @@ const GROUP_OF: Record<string, ModuleGroupId> = {
template: "alerting",
destination: "alerting",
incidents: "alerting",
incident_integrations: "alerting",
oncall: "alerting",
oncall_response: "alerting",
pipeline: "pipelines",
@@ -181,6 +182,7 @@ const ICON_OF: Record<string, IconName> = {
template: "article",
destination: "webhook",
incidents: "shield-alert-outline",
incident_integrations: "webhook",
oncall: "person-pin-circle",
oncall_response: "forum",
pipeline: "lan",
+4 -1
View File
@@ -10270,6 +10270,10 @@
"acknowledgeConfirmMessage": "يؤدي هذا إلى وضع الحادث كقيد العمل. يمكنك إعادة فتحه لاحقًا إذا لزم الأمر.",
"acknowledgeConfirmLabel": "إقرار",
"acknowledgeConfirmCancelLabel": "إلغاء",
"resolveConfirmTitle": "حل الحادث؟",
"resolveConfirmMessage": "يؤدي هذا إلى إغلاق الحادث وإزالته من القائمة النشطة. يمكنك إعادة فتحه لاحقًا إذا لزم الأمر.",
"reopenConfirmTitle": "إعادة فتح الحادث؟",
"reopenConfirmMessage": "يؤدي هذا إلى إعادة الحادث إلى حالة مفتوح ومسح حله والإقرار به.",
"acknowledgeAriaLabel": "الإقرار بالحادث",
"acknowledgedBy": "تم الإقرار بواسطة",
"resolve": "العزم",
@@ -20997,7 +21001,6 @@
"successfullyImportedTemplates": "تم استيراد {count} قالب بنجاح | تم استيراد {count} من القوالب بنجاح",
"aiAnalysisIsAlreadyRunningFor": "تحليل الذكاء الاصطناعي جار بالفعل لهذا الحادث",
"incidentSeverityUpdatedTo": "تم تحديث خطورة الحادث إلى {severity}",
"incidentStatusUpdatedTo": "تم تحديث حالة الحادث إلى {status}",
"failedToLoadIncidentDetails": "فشل في تحميل تفاصيل الحادث",
"incidentsRefreshed": "الحوادث التي تم تحديثها",
"failedToPostComment": "فشل في نشر التعليق",
+4 -1
View File
@@ -10270,6 +10270,10 @@
"acknowledgeConfirmMessage": "Damit wird der Vorfall als in Bearbeitung markiert. Sie können ihn später bei Bedarf wieder öffnen.",
"acknowledgeConfirmLabel": "Bestätigen",
"acknowledgeConfirmCancelLabel": "Abbrechen",
"resolveConfirmTitle": "Vorfall auflösen?",
"resolveConfirmMessage": "Dadurch wird der Vorfall geschlossen und aus der aktiven Liste entfernt. Sie können ihn bei Bedarf später wieder öffnen.",
"reopenConfirmTitle": "Vorfall wieder öffnen?",
"reopenConfirmMessage": "Dadurch wird der Vorfall wieder auf offen gesetzt und seine Auflösung und Bestätigung werden entfernt.",
"acknowledgeAriaLabel": "Vorfall bestätigen",
"acknowledgedBy": "Bestätigt von",
"resolve": "Auflösen",
@@ -20997,7 +21001,6 @@
"successfullyImportedTemplates": "Erfolgreich {count} Vorlage importiert | Erfolgreich {count} Vorlagen importiert",
"aiAnalysisIsAlreadyRunningFor": "Für diesen Vorfall läuft bereits eine KI-Analyse",
"incidentSeverityUpdatedTo": "Schweregrad des Vorfalls auf {severity} aktualisiert",
"incidentStatusUpdatedTo": "Status des Vorfalls auf {status} aktualisiert",
"failedToLoadIncidentDetails": "Fehler beim Laden der Vorfall-Details",
"incidentsRefreshed": "Vorfälle aktualisiert",
"failedToPostComment": "Fehler beim Posten des Kommentars",
+4 -1
View File
@@ -10268,6 +10268,10 @@
"acknowledgeConfirmMessage": "This marks the incident as being worked on. You can reopen it later if needed.",
"acknowledgeConfirmLabel": "Acknowledge",
"acknowledgeConfirmCancelLabel": "Cancel",
"resolveConfirmTitle": "Resolve incident?",
"resolveConfirmMessage": "This closes the incident and removes it from the active list. You can reopen it later if needed.",
"reopenConfirmTitle": "Reopen incident?",
"reopenConfirmMessage": "This sets the incident back to open and clears its resolution and acknowledgement.",
"acknowledgeAriaLabel": "Acknowledge incident",
"acknowledgedBy": "Acknowledged by",
"resolve": "Resolve",
@@ -20995,7 +20999,6 @@
"successfullyImportedTemplates": "Successfully imported {count} template | Successfully imported {count} templates",
"aiAnalysisIsAlreadyRunningFor": "AI analysis is already running for this incident",
"incidentSeverityUpdatedTo": "Incident severity updated to {severity}",
"incidentStatusUpdatedTo": "Incident status updated to {status}",
"failedToLoadIncidentDetails": "Failed to load incident details",
"incidentsRefreshed": "Incidents refreshed",
"failedToPostComment": "Failed to post comment",
+4 -1
View File
@@ -10270,6 +10270,10 @@
"acknowledgeConfirmMessage": "Esto marca el incidente como en curso. Puedes reabrirlo más tarde si es necesario.",
"acknowledgeConfirmLabel": "Reconocer",
"acknowledgeConfirmCancelLabel": "Cancelar",
"resolveConfirmTitle": "¿Resolver incidente?",
"resolveConfirmMessage": "Esto cierra el incidente y lo quita de la lista activa. Puedes reabrirlo más tarde si es necesario.",
"reopenConfirmTitle": "¿Reabrir incidente?",
"reopenConfirmMessage": "Esto devuelve el incidente al estado abierto y borra su resolución y reconocimiento.",
"acknowledgeAriaLabel": "Reconocer incidente",
"acknowledgedBy": "Reconocido por",
"resolve": "Resolver",
@@ -20997,7 +21001,6 @@
"successfullyImportedTemplates": "Se importó correctamente {count} plantilla | Se importaron correctamente {count} plantillas",
"aiAnalysisIsAlreadyRunningFor": "El análisis de IA ya se está ejecutando para este incidente",
"incidentSeverityUpdatedTo": "Gravedad del incidente actualizada a {severity}",
"incidentStatusUpdatedTo": "Estado del incidente actualizado a {status}",
"failedToLoadIncidentDetails": "No se pudieron cargar los detalles del incidente",
"incidentsRefreshed": "Incidentes actualizados",
"failedToPostComment": "No se pudo publicar el comentario",
+4 -1
View File
@@ -10270,6 +10270,10 @@
"acknowledgeConfirmMessage": "Cela marque l'incident comme étant en cours de traitement. Vous pourrez le rouvrir plus tard si nécessaire.",
"acknowledgeConfirmLabel": "Acquitter",
"acknowledgeConfirmCancelLabel": "Annuler",
"resolveConfirmTitle": "Résoudre l'incident ?",
"resolveConfirmMessage": "Cela ferme l'incident et le retire de la liste active. Vous pourrez le rouvrir plus tard si nécessaire.",
"reopenConfirmTitle": "Rouvrir l'incident ?",
"reopenConfirmMessage": "Cela remet l'incident à l'état ouvert et efface sa résolution et son acquittement.",
"acknowledgeAriaLabel": "Acquitter l'incident",
"acknowledgedBy": "Acquitté par",
"resolve": "Résoudre",
@@ -20997,7 +21001,6 @@
"successfullyImportedTemplates": "{count} modèle importé avec succès | {count} modèles importés avec succès",
"aiAnalysisIsAlreadyRunningFor": "Une analyse IA est déjà en cours pour cet incident",
"incidentSeverityUpdatedTo": "La gravité de l'incident a été mise à jour vers {severity}",
"incidentStatusUpdatedTo": "Le statut de l'incident a été mis à jour vers {status}",
"failedToLoadIncidentDetails": "Échec du chargement des détails de l'incident",
"incidentsRefreshed": "Incidents actualisés",
"failedToPostComment": "Échec de l'envoi du commentaire",
+4 -1
View File
@@ -10270,6 +10270,10 @@
"acknowledgeConfirmMessage": "Questa azione contrassegna l'incidente come preso in carico. Puoi riaprirlo in seguito, se necessario.",
"acknowledgeConfirmLabel": "Prendi in carico",
"acknowledgeConfirmCancelLabel": "Annulla",
"resolveConfirmTitle": "Risolvere l'incidente?",
"resolveConfirmMessage": "Questo chiude l'incidente e lo rimuove dall'elenco attivo. Puoi riaprirlo più tardi se necessario.",
"reopenConfirmTitle": "Riaprire l'incidente?",
"reopenConfirmMessage": "Questo riporta l'incidente allo stato aperto e cancella la risoluzione e la presa in carico.",
"acknowledgeAriaLabel": "Prendi in carico l'incidente",
"acknowledgedBy": "Preso in carico da",
"resolve": "Risolvi",
@@ -20997,7 +21001,6 @@
"successfullyImportedTemplates": "{count} template importato con successo | {count} template importati con successo",
"aiAnalysisIsAlreadyRunningFor": "L'analisi AI è già in esecuzione per questo incidente",
"incidentSeverityUpdatedTo": "Gravità dell'incidente aggiornata a {severity}",
"incidentStatusUpdatedTo": "Stato dell'incidente aggiornato a {status}",
"failedToLoadIncidentDetails": "Impossibile caricare i dettagli dell'incidente",
"incidentsRefreshed": "Incidenti aggiornati",
"failedToPostComment": "Impossibile pubblicare il commento",
+4 -1
View File
@@ -10270,6 +10270,10 @@
"acknowledgeConfirmMessage": "これによりインシデントは対応中としてマークされます。必要に応じて後で再度オープンできます。",
"acknowledgeConfirmLabel": "確認済みにする",
"acknowledgeConfirmCancelLabel": "キャンセル",
"resolveConfirmTitle": "インシデントを解決しますか?",
"resolveConfirmMessage": "インシデントをクローズし、アクティブな一覧から削除します。必要に応じて後で再オープンできます。",
"reopenConfirmTitle": "インシデントを再オープンしますか?",
"reopenConfirmMessage": "インシデントをオープン状態に戻し、解決と確認済みの情報をクリアします。",
"acknowledgeAriaLabel": "インシデントを確認済みにする",
"acknowledgedBy": "確認者",
"resolve": "解決",
@@ -20997,7 +21001,6 @@
"successfullyImportedTemplates": "テンプレートを{count}件正常にインポートしました | テンプレートを{count}件正常にインポートしました",
"aiAnalysisIsAlreadyRunningFor": "このインシデントに対してAI分析が既に実行されています",
"incidentSeverityUpdatedTo": "インシデントの重要度が{severity}に更新されました",
"incidentStatusUpdatedTo": "インシデントのステータスが{status}に更新されました",
"failedToLoadIncidentDetails": "インシデント詳細の読み込みに失敗しました",
"incidentsRefreshed": "インシデントを更新しました",
"failedToPostComment": "コメントの投稿に失敗しました",
+4 -1
View File
@@ -10270,6 +10270,10 @@
"acknowledgeConfirmMessage": "이 작업은 인시던트가 작업 중임을 표시합니다. 필요한 경우 나중에 다시 열 수 있습니다.",
"acknowledgeConfirmLabel": "확인",
"acknowledgeConfirmCancelLabel": "취소",
"resolveConfirmTitle": "인시던트를 해결하시겠습니까?",
"resolveConfirmMessage": "인시던트를 종료하고 활성 목록에서 제거합니다. 필요하면 나중에 다시 열 수 있습니다.",
"reopenConfirmTitle": "인시던트를 다시 여시겠습니까?",
"reopenConfirmMessage": "인시던트를 열린 상태로 되돌리고 해결 및 확인 정보를 지웁니다.",
"acknowledgeAriaLabel": "인시던트 확인",
"acknowledgedBy": "확인자",
"resolve": "해결",
@@ -20997,7 +21001,6 @@
"successfullyImportedTemplates": "{count}개 템플릿을 성공적으로 가져왔습니다. | {count}개 템플릿을 성공적으로 가져왔습니다.",
"aiAnalysisIsAlreadyRunningFor": "이 인시던트에 대해 AI 분석이 이미 실행 중입니다.",
"incidentSeverityUpdatedTo": "인시던트 심각도가 {severity}(으)로 업데이트되었습니다.",
"incidentStatusUpdatedTo": "인시던트 상태가 {status}(으)로 업데이트되었습니다.",
"failedToLoadIncidentDetails": "인시던트 세부 정보를 불러오지 못했습니다.",
"incidentsRefreshed": "인시던트가 새로 고쳐졌습니다.",
"failedToPostComment": "댓글을 게시하지 못했습니다.",
+4 -1
View File
@@ -10270,6 +10270,10 @@
"acknowledgeConfirmMessage": "Hiermee wordt het incident gemarkeerd als in behandeling. Je kunt het later opnieuw openen als dat nodig is.",
"acknowledgeConfirmLabel": "Bevestigen",
"acknowledgeConfirmCancelLabel": "Annuleren",
"resolveConfirmTitle": "Incident oplossen?",
"resolveConfirmMessage": "Hiermee wordt het incident gesloten en uit de actieve lijst verwijderd. U kunt het later zo nodig heropenen.",
"reopenConfirmTitle": "Incident heropenen?",
"reopenConfirmMessage": "Hiermee wordt het incident weer op open gezet en worden de oplossing en bevestiging gewist.",
"acknowledgeAriaLabel": "Incident bevestigen",
"acknowledgedBy": "Bevestigd door",
"resolve": "Oplossen",
@@ -20997,7 +21001,6 @@
"successfullyImportedTemplates": "{count} sjabloon succesvol geïmporteerd | {count} sjablonen succesvol geïmporteerd",
"aiAnalysisIsAlreadyRunningFor": "AI-analyse draait al voor dit incident",
"incidentSeverityUpdatedTo": "Ernst van incident bijgewerkt naar {severity}",
"incidentStatusUpdatedTo": "Incidentstatus bijgewerkt naar {status}",
"failedToLoadIncidentDetails": "Kan incidentdetails niet laden",
"incidentsRefreshed": "Incidenten vernieuwd",
"failedToPostComment": "Kan reactie niet plaatsen",
+4 -1
View File
@@ -10270,6 +10270,10 @@
"acknowledgeConfirmMessage": "To oznacza incydent jako będący w trakcie obsługi. W razie potrzeby możesz go później ponownie otworzyć.",
"acknowledgeConfirmLabel": "Potwierdź",
"acknowledgeConfirmCancelLabel": "Anuluj",
"resolveConfirmTitle": "Rozwiązać incydent?",
"resolveConfirmMessage": "Spowoduje to zamknięcie incydentu i usunięcie go z listy aktywnych. W razie potrzeby możesz otworzyć go ponownie później.",
"reopenConfirmTitle": "Ponownie otworzyć incydent?",
"reopenConfirmMessage": "Spowoduje to przywrócenie incydentu do stanu otwartego i wyczyszczenie jego rozwiązania i potwierdzenia.",
"acknowledgeAriaLabel": "Potwierdź incydent",
"acknowledgedBy": "Potwierdzone przez",
"resolve": "Rozwiąż",
@@ -20997,7 +21001,6 @@
"successfullyImportedTemplates": "Pomyślnie zaimportowano {count} szablon | Pomyślnie zaimportowano szablony: {count}",
"aiAnalysisIsAlreadyRunningFor": "Analiza AI już trwa dla tego incydentu",
"incidentSeverityUpdatedTo": "Poziom ważności incydentu zaktualizowano na {severity}",
"incidentStatusUpdatedTo": "Status incydentu zaktualizowano na {status}",
"failedToLoadIncidentDetails": "Nie udało się wczytać szczegółów incydentu",
"incidentsRefreshed": "Odświeżono incydenty",
"failedToPostComment": "Nie udało się dodać komentarza",
+4 -1
View File
@@ -10270,6 +10270,10 @@
"acknowledgeConfirmMessage": "Isto marca o incidente como em tratamento. Pode reabri-lo depois, se necessário.",
"acknowledgeConfirmLabel": "Reconhecer",
"acknowledgeConfirmCancelLabel": "Cancelar",
"resolveConfirmTitle": "Resolver incidente?",
"resolveConfirmMessage": "Isto fecha o incidente e remove-o da lista ativa. Pode reabri-lo mais tarde, se necessário.",
"reopenConfirmTitle": "Reabrir incidente?",
"reopenConfirmMessage": "Isto volta a colocar o incidente como aberto e limpa a resolução e o reconhecimento.",
"acknowledgeAriaLabel": "Reconhecer incidente",
"acknowledgedBy": "Reconhecido por",
"resolve": "Resolver",
@@ -20997,7 +21001,6 @@
"successfullyImportedTemplates": "Importado com sucesso {count} modelo | Importados com sucesso {count} modelos",
"aiAnalysisIsAlreadyRunningFor": "A análise de IA já está em curso para este incidente",
"incidentSeverityUpdatedTo": "Severidade do incidente atualizada para {severity}",
"incidentStatusUpdatedTo": "Estado do incidente atualizado para {status}",
"failedToLoadIncidentDetails": "Falha ao carregar detalhes do incidente",
"incidentsRefreshed": "Incidentes atualizados",
"failedToPostComment": "Falha ao publicar comentário",
+4 -1
View File
@@ -10270,6 +10270,10 @@
"acknowledgeConfirmMessage": "Инцидент будет помечен как взятый в работу. При необходимости его можно будет переоткрыть позже.",
"acknowledgeConfirmLabel": "Подтвердить",
"acknowledgeConfirmCancelLabel": "Отмена",
"resolveConfirmTitle": "Разрешить инцидент?",
"resolveConfirmMessage": "Инцидент будет закрыт и удалён из списка активных. При необходимости его можно открыть повторно позже.",
"reopenConfirmTitle": "Открыть инцидент повторно?",
"reopenConfirmMessage": "Инцидент вернётся в состояние «открыт», а его разрешение и подтверждение будут сброшены.",
"acknowledgeAriaLabel": "Подтвердить инцидент",
"acknowledgedBy": "Кем подтверждено",
"resolve": "Разрешить",
@@ -20997,7 +21001,6 @@
"successfullyImportedTemplates": "Успешно импортирован {count} шаблон | Успешно импортировано шаблонов: {count}",
"aiAnalysisIsAlreadyRunningFor": "Анализ ИИ уже выполняется для этого инцидента",
"incidentSeverityUpdatedTo": "Критичность инцидента обновлена до {severity}",
"incidentStatusUpdatedTo": "Статус инцидента обновлен до {status}",
"failedToLoadIncidentDetails": "Не удалось загрузить сведения об инциденте",
"incidentsRefreshed": "Инциденты обновлены",
"failedToPostComment": "Не удалось опубликовать комментарий",
+4 -1
View File
@@ -10270,6 +10270,10 @@
"acknowledgeConfirmMessage": "Bu, vakanın üzerinde çalışıldığını gösterir. Gerekirse daha sonra yeniden açabilirsiniz.",
"acknowledgeConfirmLabel": "Onayla",
"acknowledgeConfirmCancelLabel": "İptal",
"resolveConfirmTitle": "Vaka çözülsün mü?",
"resolveConfirmMessage": "Bu işlem vakayı kapatır ve etkin listeden kaldırır. Gerekirse daha sonra yeniden açabilirsiniz.",
"reopenConfirmTitle": "Vaka yeniden açılsın mı?",
"reopenConfirmMessage": "Bu işlem vakayı yeniden açık durumuna getirir ve çözüm ile onay bilgilerini temizler.",
"acknowledgeAriaLabel": "Vakayı onayla",
"acknowledgedBy": "Onaylayan",
"resolve": "Çöz",
@@ -20997,7 +21001,6 @@
"successfullyImportedTemplates": "{count} şablon başarıyla içe aktarıldı | {count} şablon başarıyla içe aktarıldı",
"aiAnalysisIsAlreadyRunningFor": "Bu vaka için AI analizi zaten çalışıyor",
"incidentSeverityUpdatedTo": "Vaka önem derecesi {severity} olarak güncellendi",
"incidentStatusUpdatedTo": "Vaka durumu {status} olarak güncellendi",
"failedToLoadIncidentDetails": "Vaka ayrıntıları yüklenemedi",
"incidentsRefreshed": "Vakalar yenilendi",
"failedToPostComment": "Yorum gönderilemedi",
+4 -1
View File
@@ -10270,6 +10270,10 @@
"acknowledgeConfirmMessage": "Thao tác này đánh dấu sự cố là đang được xử lý. Bạn có thể mở lại sau nếu cần.",
"acknowledgeConfirmLabel": "Xác nhận",
"acknowledgeConfirmCancelLabel": "Hủy",
"resolveConfirmTitle": "Giải quyết sự cố?",
"resolveConfirmMessage": "Thao tác này đóng sự cố và xóa nó khỏi danh sách đang hoạt động. Bạn có thể mở lại sau nếu cần.",
"reopenConfirmTitle": "Mở lại sự cố?",
"reopenConfirmMessage": "Thao tác này đưa sự cố về trạng thái mở và xóa thông tin giải quyết và xác nhận.",
"acknowledgeAriaLabel": "Xác nhận sự cố",
"acknowledgedBy": "Được xác nhận bởi",
"resolve": "Giải quyết",
@@ -20997,7 +21001,6 @@
"successfullyImportedTemplates": "Đã nhập thành công {count} mẫu | Đã nhập thành công {count} mẫu",
"aiAnalysisIsAlreadyRunningFor": "Đã có phân tích AI đang chạy cho sự cố này",
"incidentSeverityUpdatedTo": "Mức độ nghiêm trọng của sự cố đã được cập nhật thành {severity}",
"incidentStatusUpdatedTo": "Trạng thái sự cố đã được cập nhật thành {status}",
"failedToLoadIncidentDetails": "Không thể tải chi tiết sự cố",
"incidentsRefreshed": "Đã làm mới sự cố",
"failedToPostComment": "Không thể đăng bình luận",
+4 -1
View File
@@ -10270,6 +10270,10 @@
"acknowledgeConfirmMessage": "此操作会将事件标记为正在处理。如有需要,之后可重新打开。",
"acknowledgeConfirmLabel": "确认",
"acknowledgeConfirmCancelLabel": "取消",
"resolveConfirmTitle": "解决事件?",
"resolveConfirmMessage": "这将关闭事件并将其从活动列表中移除。如有需要,您可以稍后重新打开。",
"reopenConfirmTitle": "重新打开事件?",
"reopenConfirmMessage": "这会将事件恢复为打开状态,并清除其解决和确认信息。",
"acknowledgeAriaLabel": "确认事件",
"acknowledgedBy": "确认人",
"resolve": "解决",
@@ -20997,7 +21001,6 @@
"successfullyImportedTemplates": "成功导入 {count} 个模板 | 成功导入 {count} 个模板",
"aiAnalysisIsAlreadyRunningFor": "此事件已在进行 AI 分析",
"incidentSeverityUpdatedTo": "事件严重级别已更新为 {severity}",
"incidentStatusUpdatedTo": "事件状态已更新为 {status}",
"failedToLoadIncidentDetails": "加载事件详情失败",
"incidentsRefreshed": "事件已刷新",
"failedToPostComment": "发布评论失败",
+4 -1
View File
@@ -10265,6 +10265,10 @@
"acknowledgeConfirmMessage": "此操作會將事件標記為處理中。之後如有需要,可以重新開啟。",
"acknowledgeConfirmLabel": "確認",
"acknowledgeConfirmCancelLabel": "取消",
"resolveConfirmTitle": "解決事件?",
"resolveConfirmMessage": "這將關閉事件並將其從作用中清單移除。如有需要,您可以稍後重新開啟。",
"reopenConfirmTitle": "重新開啟事件?",
"reopenConfirmMessage": "這會將事件恢復為開啟狀態,並清除其解決與確認資訊。",
"acknowledgeAriaLabel": "確認事件",
"acknowledgedBy": "確認者",
"resolve": "解決",
@@ -20992,7 +20996,6 @@
"successfullyImportedTemplates": "已成功匯入 {count} 個範本 | 已成功匯入 {count} 個範本",
"aiAnalysisIsAlreadyRunningFor": "此事件的 AI 分析已在執行中",
"incidentSeverityUpdatedTo": "事件嚴重性已更新為 {severity}",
"incidentStatusUpdatedTo": "事件狀態已更新為 {status}",
"failedToLoadIncidentDetails": "無法載入事件詳細資訊",
"incidentsRefreshed": "事件已重新整理",
"failedToPostComment": "張貼留言失敗",