feat: introducing event type maintenance as asked in #224

This commit is contained in:
Raj Nandan Sharma
2025-02-02 23:03:58 +05:30
parent 7be9c62c7c
commit eccff16c5f
26 changed files with 501 additions and 173 deletions
@@ -0,0 +1,11 @@
export function up(knex) {
return knex.schema.alterTable("incidents", function (table) {
table.text("incident_type").defaultTo("INCIDENT");
});
}
export function down(knex) {
return knex.schema.alterTable("incidents", function (table) {
table.dropColumn("incident_type");
});
}
+15
View File
@@ -834,3 +834,18 @@ textarea::placeholder {
padding-right: 8px;
}
}
.bg-maintenance-in-progress {
background-color: #f9f3c2;
}
.text-maintenance-in-progress-text {
color: #ff6868;
}
.text-upcoming-maintenance {
color: #f0a04b;
}
.text-maintenance-completed {
color: #6499e9;
}
+99 -61
View File
@@ -14,7 +14,7 @@
if (incident.end_date_time) {
endTime = new Date(incident.end_date_time * 1000);
}
let incidentType = incident.incident_type;
const lastedFor = fd(startTime, endTime, selectedLang);
const startedAt = fdn(startTime, selectedLang);
@@ -23,6 +23,35 @@
if (nowTime < startTime) {
isFuture = true;
}
let incidentDateSummary = "";
let maintenanceBadge = "";
let maintenanceBadgeColor = "";
if (!isFuture && incident.state != "RESOLVED") {
incidentDateSummary = l(lang, "Started about %startedAt, still ongoing", {
startedAt
});
maintenanceBadge = "Maintenance in Progress";
maintenanceBadgeColor = "text-maintenance-in-progress";
} else if (!isFuture && incident.state == "RESOLVED") {
incidentDateSummary = l(lang, "Started %startedAt, lasted for %lastedFor", {
startedAt,
lastedFor
});
maintenanceBadge = "Maintenance Completed";
maintenanceBadgeColor = "text-maintenance-completed";
} else if (isFuture && incident.state != "RESOLVED") {
incidentDateSummary = l(lang, "Starts %startedAt", { startedAt });
maintenanceBadge = "Upcoming Maintenance";
maintenanceBadgeColor = "text-upcoming-maintenance";
} else if (isFuture && incident.state == "RESOLVED") {
incidentDateSummary = l(lang, "Starts %startedAt, will last for %lastedFor", {
startedAt,
lastedFor
});
maintenanceBadge = "Upcoming Maintenance";
maintenanceBadgeColor = "text-upcoming-maintenance";
}
</script>
<div class="newincident relative grid w-full grid-cols-12 gap-2 px-0 py-0 last:border-b-0">
@@ -30,47 +59,31 @@
<Accordion.Root bind:value={index} class="accor">
<Accordion.Item value="incident-0">
<Accordion.Trigger class="px-4 hover:bg-muted hover:no-underline">
<div class="justify-start text-left hover:no-underline">
<p
class="scroll-m-20 text-xs font-semibold leading-5 tracking-normal badge-{incident.state}"
>
{l(lang, incident.state)}
</p>
<div class="w-full text-left hover:no-underline">
{#if incidentType == "INCIDENT"}
<p
class="scroll-m-20 text-xs font-semibold leading-5 tracking-normal badge-{incident.state}"
>
{l(lang, incident.state)}
</p>
{:else if incidentType == "MAINTENANCE"}
<p
class="{maintenanceBadgeColor} scroll-m-20 text-xs font-semibold leading-5 tracking-normal"
>
{l(lang, maintenanceBadge)}
</p>
{/if}
<p class="scroll-m-20 text-lg font-medium tracking-tight">
{incident.title}
</p>
<p
class="scroll-m-20 text-sm font-medium tracking-wide text-muted-foreground"
>
{#if !isFuture && incident.state != "RESOLVED"}
<span>
{l(lang, "Started about %startedAt, still ongoing", {
startedAt
})}
</span>
{:else if !isFuture && incident.state == "RESOLVED"}
<span>
{l(
lang,
"Started %startedAt, lasted for %lastedFor",
{ startedAt, lastedFor }
)}
</span>
{:else if isFuture && incident.state != "RESOLVED"}
<span>
{l(lang, "Starts %startedAt", { startedAt })}
</span>
{:else if isFuture && incident.state == "RESOLVED"}
<span>
{l(
lang,
"Starts %startedAt, will last for %lastedFor",
{ startedAt, lastedFor }
)}
</span>
{/if}
</p>
{#if !!incidentDateSummary}
<p
class="scroll-m-20 text-sm font-medium tracking-wide text-muted-foreground"
>
{incidentDateSummary}
</p>
{/if}
</div>
</Accordion.Trigger>
<Accordion.Content>
@@ -100,30 +113,55 @@
{l(lang, "Updates")}
</p>
{#if incident.comments.length > 0}
<ol class="relative mt-2 pl-14">
{#each incident.comments as comment}
<li class="relative border-l pb-4 pl-[4.5rem] last:border-0">
<div
class="absolute top-0 w-28 -translate-x-32 rounded border bg-secondary px-1.5 py-1 text-center text-xs font-semibold"
{#if incidentType == "INCIDENT"}
<ol class="relative mt-2 pl-14">
{#each incident.comments as comment}
<li
class="relative border-l pb-4 pl-[4.5rem] last:border-0"
>
{l(lang, comment.state)}
</div>
<time
class=" mb-1 text-sm font-medium leading-none text-muted-foreground"
>
{f(
new Date(comment.commented_at * 1000),
"MMMM do yyyy, h:mm:ss a",
selectedLang
)}
</time>
<div
class="absolute top-0 w-28 -translate-x-32 rounded border bg-secondary px-1.5 py-1 text-center text-xs font-semibold"
>
{l(lang, comment.state)}
</div>
<p class="mb-4 text-sm font-normal">
{comment.comment}
</p>
</li>
{/each}
</ol>
<time
class=" mb-1 text-sm font-medium leading-none text-muted-foreground"
>
{f(
new Date(comment.commented_at * 1000),
"MMMM do yyyy, h:mm:ss a",
selectedLang
)}
</time>
<p class="mb-4 text-sm font-normal">
{comment.comment}
</p>
</li>
{/each}
</ol>
{:else if incidentType == "MAINTENANCE"}
<ol class="relative mt-2 pl-0">
{#each incident.comments as comment}
<li class="relative pb-2 last:border-0">
<time
class=" mb-1 text-sm font-medium leading-none text-muted-foreground"
>
{f(
new Date(comment.commented_at * 1000),
"MMMM do yyyy, h:mm:ss a",
selectedLang
)}
</time>
<p class="mb-2 text-sm font-normal">
{comment.comment}
</p>
</li>
{/each}
</ol>
{/if}
{:else}
<p class="text-sm font-medium">
{l(lang, "No Updates Yet")}
+28 -9
View File
@@ -3,7 +3,21 @@
import { format, formatDistance, formatDistanceToNow, formatDuration } from "date-fns";
import { ru, enUS, hi, de, zhCN, vi, ja, nl, da, fr, ko, ptBR, tr } from "date-fns/locale";
const locales = { ru, en: enUS, hi, de, "zh-CN": zhCN, vi, ja, nl, dk: da, fr, ko, "pt-BR": ptBR, tr };
const locales = {
ru,
en: enUS,
hi,
de,
"zh-CN": zhCN,
vi,
ja,
nl,
dk: da,
fr,
ko,
"pt-BR": ptBR,
tr
};
const f = function (date, formatStr, locale) {
return format(date, formatStr, {
@@ -27,16 +41,21 @@ const fdm = function (duration, locale) {
};
const l = function (sessionLangMap, key, args = {}) {
let obj = sessionLangMap[key];
try {
let obj = sessionLangMap[key];
// Replace placeholders in the string using the args object
if (obj && typeof obj === "string") {
obj = obj.replace(/%\w+/g, (placeholder) => {
const argKey = placeholder.slice(1); // Remove the `%` to get the key
return args[argKey] !== undefined ? args[argKey] : placeholder;
});
// Replace placeholders in the string using the args object
if (obj && typeof obj === "string") {
obj = obj.replace(/%\w+/g, (placeholder) => {
const argKey = placeholder.slice(1); // Remove the `%` to get the key
return args[argKey] !== undefined ? args[argKey] : placeholder;
});
}
return obj || key;
} catch (e) {
console.log(e);
return key;
}
return obj || key;
};
const summaryTime = function (summaryStatus) {
if (summaryStatus == "No Data") {
+7 -1
View File
@@ -26,6 +26,7 @@
"Light": "Hell",
"Link Copied": "Link kopiert",
"LIVE Status": "LIVE-Status",
"MAINTENANCE": "WARTUNG",
"Mode": "Modus",
"MONITORING": "ÜBERWACHUNG",
"No Data": "Keine Daten",
@@ -36,6 +37,7 @@
"Ongoing Incidents": "Laufende Vorfälle",
"Pinging": "Pingen",
"Recent Incidents": "Kürzliche Vorfälle",
"Recent Maintenances": "Kürzliche Wartungen",
"RESOLVED": "GELÖST",
"Share": "Teilen",
"Share this monitor using a link with others": "Teilen Sie diesen Monitor mit einem Link mit anderen",
@@ -51,5 +53,9 @@
"UP": "VERFÜGBAR",
"Updates": "Updates",
"Uptime": "Verfügbarkeit",
"%status for %duration": "%status für %duration"
"%status for %duration": "%status für %duration",
"Maintenance in Progress": "Wartung läuft",
"Maintenance Completed": "Wartung abgeschlossen",
"Upcoming Maintenance": "Bevorstehende Wartung",
"Browse Events": "Ereignisse durchsuchen"
}
+7 -1
View File
@@ -26,6 +26,7 @@
"Light": "Lys",
"Link Copied": "Link Kopieret",
"LIVE Status": "LIVE-status",
"MAINTENANCE": "VEDLIGEHOLDELSE",
"Mode": "Tilstand",
"MONITORING": "OVERVÅGNING",
"No Data": "Ingen Data",
@@ -36,6 +37,7 @@
"Ongoing Incidents": "Igangværende Hændelser",
"Pinging": "Pinger",
"Recent Incidents": "Seneste Hændelser",
"Recent Maintenances": "Seneste Vedligeholdelser",
"RESOLVED": "LØST",
"Share": "Del",
"Share this monitor using a link with others": "Del denne monitor ved hjælp af et link med andre",
@@ -51,5 +53,9 @@
"UP": "OPPE",
"Updates": "Opdateringer",
"Uptime": "Oppetid",
"%status for %duration": "%status i %duration"
"%status for %duration": "%status i %duration",
"Maintenance in Progress": "Vedligeholdelse i gang",
"Maintenance Completed": "Vedligeholdelse afsluttet",
"Upcoming Maintenance": "Kommende vedligeholdelse",
"Browse Events": "Gennemse begivenheder"
}
+8 -2
View File
@@ -26,6 +26,7 @@
"Light": "Light",
"Link Copied": "Link Copied",
"LIVE Status": "LIVE Status",
"MAINTENANCE": "MAINTENANCE",
"Mode": "Mode",
"MONITORING": "MONITORING",
"No Data": "No Data",
@@ -35,7 +36,9 @@
"No Updates Yet": "No Updates Yet",
"Ongoing Incidents": "Ongoing Incidents",
"Pinging": "Pinging",
"Browse Events": "Browse Events",
"Recent Incidents": "Recent Incidents",
"Recent Maintenances": "Recent Maintenances",
"RESOLVED": "RESOLVED",
"Share": "Share",
"Share this monitor using a link with others": "Share this monitor using a link with others",
@@ -51,5 +54,8 @@
"UP": "UP",
"Updates": "Updates",
"Uptime": "Uptime",
"%status for %duration": "%status for %duration"
}
"%status for %duration": "%status for %duration",
"Maintenance in Progress": "Maintenance in Progress",
"Maintenance Completed": "Maintenance Completed",
"Upcoming Maintenance": "Upcoming Maintenance"
}
+7 -1
View File
@@ -26,6 +26,7 @@
"Light": "Clair",
"Link Copied": "Lien copié",
"LIVE Status": "Statut LIVE",
"MAINTENANCE": "Maintenance",
"Mode": "Mode",
"MONITORING": "En surveillance",
"No Data": "Pas de données",
@@ -36,6 +37,7 @@
"Ongoing Incidents": "Incidents en cours",
"Pinging": "Ping en cours",
"Recent Incidents": "Incidents récents",
"Recent Maintenances": "Maintenances récentes",
"RESOLVED": "Résolu",
"Share": "Partager",
"Share this monitor using a link with others": "Partagez ce moniteur avec un lien à d'autres personnes",
@@ -51,5 +53,9 @@
"UP": "En ligne",
"Updates": "Mises à jour",
"Uptime": "Temps de fonctionnement",
"%status for %duration": "%status pendant %duration"
"%status for %duration": "%status pendant %duration",
"Maintenance in Progress": "Maintenance en cours",
"Maintenance Completed": "Maintenance terminée",
"Upcoming Maintenance": "Maintenance à venir",
"Browse Events": "Parcourir les événements"
}
+7 -1
View File
@@ -26,6 +26,7 @@
"Light": "लाइट",
"Link Copied": "लिंक कॉपी किया गया",
"LIVE Status": "लाइव स्टेटस",
"MAINTENANCE": "मेंटेनेंस",
"Mode": "मोड",
"MONITORING": "निगरानी",
"No Data": "कोई डेटा नहीं",
@@ -36,6 +37,7 @@
"Ongoing Incidents": "चल रही घटनाएँ",
"Pinging": "पिंगिंग",
"Recent Incidents": "हाल की घटनाएँ",
"Recent Maintenances": "हाल की मेंटेनेंस",
"RESOLVED": "सुलझाया गया",
"Share": "साझा करें",
"Share this monitor using a link with others": "इस मॉनिटर को दूसरों के साथ लिंक के माध्यम से साझा करें",
@@ -51,5 +53,9 @@
"UP": "अप",
"Updates": "अपडेट्स",
"Uptime": "अपटाइम",
"%status for %duration": "%duration के लिए %status"
"%status for %duration": "%duration के लिए %status",
"Maintenance in Progress": "मेंटेनेंस जारी है",
"Maintenance Completed": "मेंटेनेंस पूर्ण हुआ",
"Upcoming Maintenance": "आगामी मेंटेनेंस",
"Browse Events": "ईवेंट ब्राउज़ करें"
}
+7 -1
View File
@@ -26,6 +26,7 @@
"Light": "ライト",
"Link Copied": "リンクがコピーされました",
"LIVE Status": "ライブステータス",
"MAINTENANCE": "メンテナンス",
"Mode": "モード",
"MONITORING": "モニタリング中",
"No Data": "データなし",
@@ -36,6 +37,7 @@
"Ongoing Incidents": "進行中のインシデント",
"Pinging": "ピング中",
"Recent Incidents": "最近のインシデント",
"Recent Maintenances": "最近のメンテナンス",
"RESOLVED": "解決済み",
"Share": "共有",
"Share this monitor using a link with others": "このモニターをリンクで他の人と共有",
@@ -51,5 +53,9 @@
"UP": "稼働中",
"Updates": "更新",
"Uptime": "稼働時間",
"%status for %duration": "%duration の間 %status"
"%status for %duration": "%duration の間 %status",
"Maintenance in Progress": "メンテナンス中",
"Maintenance Completed": "メンテナンス完了",
"Upcoming Maintenance": "今後のメンテナンス",
"Browse Events": "イベントを閲覧"
}
+7 -1
View File
@@ -26,6 +26,7 @@
"Light": "라이트",
"Link Copied": "링크 복사됨",
"LIVE Status": "실시간 상태",
"MAINTENANCE": "유지보수",
"Mode": "모드",
"MONITORING": "감시 중",
"No Data": "데이터 없음",
@@ -36,6 +37,7 @@
"Ongoing Incidents": "진행중 사건",
"Pinging": "핑 요청",
"Recent Incidents": "최근 사건",
"Recent Maintenances": "최근 유지보수",
"RESOLVED": "해결됨",
"Share": "공유",
"Share this monitor using a link with others": "링크를 사용하여 다른 사람들과 이 모니터링 데이터를 공유해보세요",
@@ -51,5 +53,9 @@
"UP": "작동 중",
"Updates": "업데이트",
"Uptime": "업타임",
"%status for %duration": "%duration 동안 %status 상태였음"
"%status for %duration": "%duration 동안 %status 상태였음",
"Maintenance in Progress": "유지보수 진행 중",
"Maintenance Completed": "유지보수 완료",
"Upcoming Maintenance": "예정된 유지보수",
"Browse Events": "이벤트 찾아보기"
}
+8 -2
View File
@@ -26,6 +26,7 @@
"Light": "Licht",
"Link Copied": "Link gekopieerd",
"LIVE Status": "LIVE status",
"MAINTENANCE": "ONDERHOUD",
"Mode": "Modus",
"MONITORING": "MONITOREN",
"No Data": "Geen gegevens",
@@ -36,6 +37,7 @@
"Ongoing Incidents": "Lopende incidenten",
"Pinging": "Pingen",
"Recent Incidents": "Recente incidenten",
"Recent Maintenances": "Recente onderhoudsbeurten",
"RESOLVED": "OPGELOST",
"Share": "Delen",
"Share this monitor using a link with others": "Deel deze monitor met anderen via een link",
@@ -51,5 +53,9 @@
"UP": "BEREIKBAAR",
"Updates": "Updates",
"Uptime": "Uptime",
"%status for %duration": "%status voor %duration"
}
"%status for %duration": "%status voor %duration",
"Maintenance in Progress": "Onderhoud bezig",
"Maintenance Completed": "Onderhoud voltooid",
"Upcoming Maintenance": "Aankomend onderhoud",
"Browse Events": "Evenementen bekijken"
}
+7 -1
View File
@@ -26,6 +26,7 @@
"Light": "Claro",
"Link Copied": "Link Copiado",
"LIVE Status": "Status AO VIVO",
"MAINTENANCE": "MANUTENÇÃO",
"Mode": "Modo",
"MONITORING": "MONITORANDO",
"No Data": "Sem dados",
@@ -36,6 +37,7 @@
"Ongoing Incidents": "Incidentes em Andamento",
"Pinging": "Animado",
"Recent Incidents": "Incidentes Recentes",
"Recent Maintenances": "Manutenções Recentes",
"RESOLVED": "RESOLVIDO",
"Share": "Compartilhar",
"Share this monitor using a link with others": "Compartilhar este monitor através do link",
@@ -51,5 +53,9 @@
"UP": "OPERACIONAL",
"Updates": "Atualizações",
"Uptime": "Tempo de Atividade",
"%status for %duration": "%status por %duration"
"%status for %duration": "%status por %duration",
"Maintenance in Progress": "Manutenção em Progresso",
"Maintenance Completed": "Manutenção Concluída",
"Upcoming Maintenance": "Manutenção Agendada",
"Browse Events": "Navegar Eventos"
}
+7 -1
View File
@@ -26,6 +26,7 @@
"Light": "Светлая",
"Link Copied": "Ссылка скопирована",
"LIVE Status": "ЖИВОЙ статус",
"MAINTENANCE": "ОБСЛУЖИВАНИЕ",
"Mode": "Режим",
"MONITORING": "МОНИТОРИНГ",
"No Data": "Нет данных",
@@ -36,6 +37,7 @@
"Ongoing Incidents": "Текущие инциденты",
"Pinging": "Пингуется",
"Recent Incidents": "Недавние инциденты",
"Recent Maintenances": "Последние обслуживания",
"RESOLVED": "РЕШЕНО",
"Share": "Поделиться",
"Share this monitor using a link with others": "Поделитесь этим монитором с другими по ссылке",
@@ -51,5 +53,9 @@
"UP": "РАБОТАЕТ",
"Updates": "Обновления",
"Uptime": "Время работы",
"%status for %duration": "%status в течение %duration"
"%status for %duration": "%status в течение %duration",
"Maintenance in Progress": "Техническое обслуживание",
"Maintenance Completed": "Техническое обслуживание завершено",
"Upcoming Maintenance": "Предстоящее техническое обслуживание",
"Browse Events": "Просмотр событий"
}
+8 -2
View File
@@ -26,6 +26,7 @@
"Light": "Açık",
"Link Copied": "Bağlantı Kopyalandı",
"LIVE Status": "CANLI Durum",
"MAINTENANCE": "BAKIM",
"Mode": "Mod",
"MONITORING": "İZLEME",
"No Data": "Veri Yok",
@@ -36,6 +37,7 @@
"Ongoing Incidents": "Devam Eden Olaylar",
"Pinging": "Pinging",
"Recent Incidents": "Son Olaylar",
"Recent Maintenances": "Son Bakımlar",
"RESOLVED": "ÇÖZÜLDÜ",
"Share": "Paylaş",
"Share this monitor using a link with others": "Bu servisi bir bağlantı kullanarak başkalarıyla paylaşın",
@@ -51,5 +53,9 @@
"UP": "ÇALIŞIYOR",
"Updates": "Güncellemeler",
"Uptime": "Çalışma Süresi",
"%status for %duration": "%duration boyunca %status"
}
"%status for %duration": "%duration boyunca %status",
"Maintenance in Progress": "Bakım Devam Ediyor",
"Maintenance Completed": "Bakım Tamamlandı",
"Upcoming Maintenance": "Yaklaşan Bakım",
"Browse Events": "Etkinliklere Göz At"
}
+7 -1
View File
@@ -26,6 +26,7 @@
"Light": "Sáng",
"Link Copied": "Liên kết đã được sao chép",
"LIVE Status": "Trạng thái TRỰC TIẾP",
"MAINTENANCE": "Bảo trì",
"Mode": "Chế độ",
"MONITORING": "Đang giám sát",
"No Data": "Không có dữ liệu",
@@ -36,6 +37,7 @@
"Ongoing Incidents": "Sự cố đang diễn ra",
"Pinging": "Đang kiểm tra",
"Recent Incidents": "Sự cố gần đây",
"Recent Maintenances": "Bảo trì gần đây",
"RESOLVED": "Đã giải quyết",
"Share": "Chia sẻ",
"Share this monitor using a link with others": "Chia sẻ trình theo dõi này với người khác bằng liên kết",
@@ -51,5 +53,9 @@
"UP": "Hoạt động",
"Updates": "Cập nhật",
"Uptime": "Thời gian hoạt động",
"%status for %duration": "%status trong %duration"
"%status for %duration": "%status trong %duration",
"Maintenance in Progress": "Bảo trì đang diễn ra",
"Maintenance Completed": "Bảo trì đã hoàn thành",
"Upcoming Maintenance": "Bảo trì sắp tới",
"Browse Events": "Duyệt sự kiện"
}
+7 -1
View File
@@ -26,6 +26,7 @@
"Light": "浅色",
"Link Copied": "链接已复制",
"LIVE Status": "实时状态",
"MAINTENANCE": "维护",
"Mode": "模式",
"MONITORING": "正在监控",
"No Data": "无数据",
@@ -36,6 +37,7 @@
"Ongoing Incidents": "正在处理的事件",
"Pinging": "正在测试",
"Recent Incidents": "最近的事件",
"Recent Maintenances": "最近的维护",
"RESOLVED": "已解决",
"Share": "分享",
"Share this monitor using a link with others": "通过链接与他人分享此监控",
@@ -51,5 +53,9 @@
"UP": "可用",
"Updates": "更新",
"Uptime": "正常运行时间",
"%status for %duration": "%duration 内的 %status"
"%status for %duration": "%duration 内的 %status",
"Maintenance in Progress": "维护进行中",
"Maintenance Completed": "维护已完成",
"Upcoming Maintenance": "即将进行的维护",
"Browse Events": "浏览事件"
}
+10 -4
View File
@@ -490,11 +490,17 @@ export const CreateIncident = async (data) => {
start_date_time: data.start_date_time,
status: !!data.status ? data.status : "OPEN",
end_date_time: !!data.end_date_time ? data.end_date_time : null,
state: !!data.state ? data.state : "INVESTIGATING"
state: !!data.state ? data.state : "INVESTIGATING",
incident_type: !!data.incident_type ? data.incident_type : "INCIDENT"
};
//incident_type == INCIDENT delete endDateTime
if (incident.incident_type === "INCIDENT") {
incident.end_date_time = null;
}
//if endDateTime is provided and it is less than startDateTime, throw error
if (incident.end_date_time && incident.end_date_time < incident.start_date_time) {
if (!!incident.end_date_time && incident.end_date_time < incident.start_date_time) {
throw new Error("End date time cannot be less than start date time");
}
@@ -638,9 +644,9 @@ export const AddIncidentComment = async (incident_id, comment, state, commented_
}
let c = await db.insertIncidentComment(incident_id, comment, state, commented_at);
let incidentType = incidentExists.incident_type;
//update incident state
if (c) {
if (c && incidentType === "INCIDENT") {
let incidentUpdate = {
state: state
};
+4 -1
View File
@@ -51,7 +51,10 @@ const defaultEval = `(async function (statusCode, responseTime, responseData) {
async function manualIncident(monitor) {
let startTs = GetMinuteStartNowTimestampUTC();
let impactArr = await db.getIncidentsByMonitorTagRealtime(monitor.tag, startTs);
let incidentArr = await db.getIncidentsByMonitorTagRealtime(monitor.tag, startTs);
let maintenanceArr = await db.getMaintenanceByMonitorTagRealtime(monitor.tag, startTs);
let impactArr = incidentArr.concat(maintenanceArr);
let impact = "";
if (impactArr.length == 0) {
+21 -2
View File
@@ -394,7 +394,8 @@ class DbImpl {
status: data.status,
state: data.state,
created_at: this.knex.fn.now(),
updated_at: this.knex.fn.now()
updated_at: this.knex.fn.now(),
incident_type: data.incident_type
});
}
@@ -549,11 +550,29 @@ class DbImpl {
)
.innerJoin("incident_monitors as im", "i.id", "im.incident_id")
.where("im.monitor_tag", monitor_tag)
.andWhere("i.start_date_time", "<", timestamp)
.andWhere("i.start_date_time", "<=", timestamp)
.andWhere("i.status", "OPEN")
.andWhere("i.incident_type", "INCIDENT")
.andWhere("i.state", "!=", "RESOLVED");
}
async getMaintenanceByMonitorTagRealtime(monitor_tag, timestamp) {
return await this.knex("incidents as i")
.select(
"i.id as id",
"i.start_date_time as start_date_time",
"i.end_date_time as end_date_time",
"im.monitor_impact"
)
.innerJoin("incident_monitors as im", "i.id", "im.incident_id")
.where("im.monitor_tag", monitor_tag)
.andWhere("i.start_date_time", "<=", timestamp)
.andWhere("i.end_date_time", ">=", timestamp)
.andWhere("i.status", "OPEN")
.andWhere("i.incident_type", "MAINTENANCE")
.andWhere("i.state", "=", "RESOLVED");
}
//given array of ids get incidents
async getIncidentsByIds(ids) {
return await this.knex("incidents").whereIn("id", ids).andWhere("status", "OPEN");
-1
View File
@@ -137,7 +137,6 @@ const FetchData = async function (site, monitor, localTz, selectedLang, lang) {
_90Day[ts].timestamp = ts;
_90Day[ts].cssClass = cssClass;
_90Day[ts].summaryStatus = l(lang, summaryTime(summaryStatus), {
status: l(lang, summaryStatus),
duration: summaryDuration
+8
View File
@@ -0,0 +1,8 @@
// @ts-nocheck
import { redirect } from "@sveltejs/kit";
import { base } from "$app/paths";
export async function load({ parent, url }) {
throw redirect(302, base + "/docs/home");
}
+8 -2
View File
@@ -98,11 +98,17 @@ export async function load({ parent, url }) {
});
return incident;
});
let unresolvedIncidents = allOpenIncidents.filter((incident) => incident.state !== "RESOLVED");
let allRecentIncidents = allOpenIncidents.filter(
(incident) => incident.incident_type == "INCIDENT"
);
let allRecentMaintenances = allOpenIncidents.filter(
(incident) => incident.incident_type == "MAINTENANCE"
);
return {
monitors: monitorsActive,
unresolvedIncidents: allOpenIncidents,
allRecentIncidents,
allRecentMaintenances,
categoryName: requiredCategory,
isCategoryPage: isCategoryPage,
isMonitorPage: isMonitorPage,
+70 -15
View File
@@ -11,6 +11,7 @@
import { onMount } from "svelte";
import ShareMenu from "$lib/components/shareMenu.svelte";
import { scale } from "svelte/transition";
import { format } from "date-fns";
export let data;
let shareMenusToggle = false;
@@ -43,6 +44,14 @@
onMount(() => {
pageLoaded = true;
});
let kindFilter = "INCIDENT";
function kindOfIncidents(kind) {
kindFilter = kind;
}
if (data.allRecentIncidents.length == 0) {
kindOfIncidents("MAINTENANCE");
}
</script>
<svelte:head>
@@ -96,16 +105,47 @@
</Button>
</section>
{/if}
{#if data.unresolvedIncidents.length > 0}
{#if data.allRecentIncidents.length + data.allRecentMaintenances.length > 0}
<section
class="mx-auto mb-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center bg-transparent"
id=""
>
<div class="grid w-full grid-cols-2 gap-4">
<div class="col-span-2 text-center md:col-span-1 md:text-left">
<Badge variant="outline" class="border-0 pl-0">
{l(data.lang, "Ongoing Incidents")}
</Badge>
{#if kindFilter == "INCIDENT"}
{#if data.allRecentIncidents.length > 0}
<Button class="h-8 text-sm" on:click={() => kindOfIncidents("INCIDENT")}>
{l(data.lang, "Recent Incidents")}
</Button>
{/if}
{#if data.allRecentMaintenances.length > 0}
<Button
variant="secondary"
class="h-8 text-sm"
on:click={() => kindOfIncidents("MAINTENANCE")}
>
{l(data.lang, "Recent Maintenances")}
</Button>
{/if}
{:else}
{#if data.allRecentIncidents.length > 0}
<Button
variant="secondary"
class="h-8 text-sm"
on:click={() => kindOfIncidents("INCIDENT")}
>
{l(data.lang, "Recent Incidents")}
</Button>
{/if}
{#if data.allRecentMaintenances.length > 0}
<Button
class="h-8 text-sm"
on:click={() => kindOfIncidents("MAINTENANCE")}
>
{l(data.lang, "Recent Maintenances")}
</Button>
{/if}
{/if}
</div>
</div>
</section>
@@ -114,8 +154,19 @@
id=""
>
<Card.Root class="w-full">
<Card.Content class=" newincidents w-full overflow-hidden p-0">
{#each data.unresolvedIncidents as incident, index}
{#if kindFilter == "INCIDENT"}
<Card.Content class=" newincidents w-full overflow-hidden p-0">
{#each data.allRecentIncidents as incident, index}
<Incident
{incident}
lang={data.lang}
index="incident-{index}"
selectedLang={data.selectedLang}
/>
{/each}
</Card.Content>
{:else if kindFilter == "MAINTENANCE"}
{#each data.allRecentMaintenances as incident, index}
<Incident
{incident}
lang={data.lang}
@@ -123,7 +174,7 @@
selectedLang={data.selectedLang}
/>
{/each}
</Card.Content>
{/if}
</Card.Root>
</section>
{/if}
@@ -186,8 +237,10 @@
class="relative z-10 mx-auto mb-8 w-full max-w-[890px] flex-1 flex-col items-start backdrop-blur-[2px] md:w-[655px]"
>
{#each data.site.categories.filter((e) => e.name != "Home") as category}
<div
on:click={() => {
<a
href={`?category=${category.name}`}
on:click={(e) => {
e.preventDefault();
window.location.href = `?category=${category.name}`;
}}
>
@@ -210,7 +263,7 @@
</Card.Description>
</Card.Header>
</Card.Root>
</div>
</a>
{/each}
</section>
{/if}
@@ -218,21 +271,23 @@
class="mx-auto mb-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center bg-transparent"
id=""
>
<div
on:click={() => {
window.location.href = `${base}/incidents`;
<a
href="{base}/incidents/{format(new Date(), 'MMMM-yyyy')}"
on:click={(e) => {
e.preventDefault();
window.location.href = `${base}/incidents/${format(new Date(), "MMMM-yyyy")}`;
}}
class="bounce-right grid w-full cursor-pointer grid-cols-2 justify-between gap-4 rounded-md border bg-card px-4 py-2 text-sm font-medium hover:bg-secondary"
>
<div class="col-span-1 text-left">
{l(data.lang, "Recent Incidents")}
{l(data.lang, "Browse Events")}
</div>
<div class="text-right">
<span class="arrow float-right mt-0.5">
<ArrowRight class="h-4 w-4 text-muted-foreground hover:text-primary" />
</span>
</div>
</div>
</a>
</section>
{#if shareMenusToggle}
<div
@@ -45,7 +45,7 @@
id: "/(manage)/manage/(app)/app/alerts"
},
{
name: "Incidents",
name: "Events",
url: `${base}/manage/app/incidents`,
id: "/(manage)/manage/(app)/app/incidents"
},
@@ -5,7 +5,7 @@
import { Label } from "$lib/components/ui/label";
import { Button } from "$lib/components/ui/button";
import * as Alert from "$lib/components/ui/alert";
import * as RadioGroup from "$lib/components/ui/radio-group";
import moment from "moment";
import { DateInput } from "date-picker-svelte";
import { clickOutsideAction, slide } from "svelte-legos";
@@ -81,7 +81,6 @@
}
return i;
});
totalPages = Math.ceil(resp.total.count / limit);
} catch (error) {
alert("Error: " + error);
@@ -102,9 +101,12 @@
endDatetime: null,
startDatetime: null,
start_date_time: null,
endDatetime: null,
ent_date_time: null,
status: "OPEN",
state: "INVESTIGATING",
firstComment: ""
firstComment: "",
incident_type: "INCIDENT"
};
}
@@ -120,7 +122,8 @@
end_date_time: newIncident.endDatetime,
status: newIncident.status,
state: newIncident.state,
id: newIncident.id
id: newIncident.id,
incident_type: newIncident.incident_type
};
//convert data.start_date_time to timestamp
if (!!!toPost.start_date_time) {
@@ -128,12 +131,14 @@
return;
}
toPost.start_date_time = parseInt(new Date(toPost.start_date_time).getTime() / 1000);
if (!!!toPost.end_date_time) {
delete toPost.end_date_time;
} else {
if (!!toPost.end_date_time) {
toPost.end_date_time = parseInt(new Date(toPost.end_date_time).getTime() / 1000);
}
if (toPost.incident_type == "MAINTENANCE") {
toPost.state = "RESOLVED";
}
formStateCreate = "loading";
try {
let data = await fetch(base + "/manage/app/api/", {
@@ -155,7 +160,7 @@
if (!!!newIncident.id) {
newComment.comment = newIncident.firstComment;
newComment.id = 0;
newComment.state = newIncident.state;
newComment.state = toPost.state;
newComment.commented_at = newIncident.startDatetime;
await addNewComment(resp.incident_id);
@@ -403,8 +408,10 @@
</Select.Group>
</Select.Content>
</Select.Root>
</div>
<div class="mx-2">
{#if loadingData}
<Loader class="ml-2 mt-2 inline h-6 w-6 animate-spin" />
<Loader class="float-right ml-2 mt-2 inline h-6 w-6 animate-spin" />
{/if}
</div>
<Button
@@ -414,7 +421,7 @@
}}
>
<Plus class="mr-2 inline h-6 w-6" />
New Incident
New Event
</Button>
</div>
@@ -563,9 +570,17 @@
</td>
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold">
<span class="badge-{incident.state} rounded px-1.5 py-1"
>{incident.state}</span
>
{#if incident.incident_type == "MAINTENANCE"}
<span class="badge-MAINTENANCE rounded px-1.5 py-1">
MAINTENANCE
</span>
{:else}
<span
class="badge-{incident.state} rounded px-1.5 py-1"
>
{incident.state}
</span>
{/if}
</td>
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold">
<div class="flex gap-x-1.5">
@@ -639,21 +654,53 @@
<div class="rounded-md border p-4">
<div>
{#if newIncident.id}
<h2 class="text-lg font-medium">Edit Incident</h2>
<h2 class="text-lg font-medium">Edit Event</h2>
{:else}
<h2 class="text-lg font-medium">Add New Incident</h2>
<h2 class="text-lg font-medium">Add New Event</h2>
{/if}
</div>
<p class="mt-4 text-sm font-medium">Event Type</p>
<div class="mt-2 flex gap-4">
<RadioGroup.Root
class=" flex gap-x-2 {!!newIncident.id ? 'opacity-70' : ''}"
bind:value={newIncident.incident_type}
disabled={!!newIncident.id}
>
<Label
for="type-INCIDENT"
class="flex cursor-pointer items-center space-x-2 rounded-md border {newIncident.incident_type ==
'INCIDENT'
? 'bg-secondary shadow-md'
: ''} p-3"
>
<RadioGroup.Item value="INCIDENT" id="type-INCIDENT" />
<span>Incident</span>
</Label>
<Label
for="type-MAINTENANCE"
class="flex cursor-pointer items-center space-x-2 rounded-md border p-3 {newIncident.incident_type ==
'MAINTENANCE'
? 'bg-secondary shadow-md'
: ''}"
>
<RadioGroup.Item value="MAINTENANCE" id="type-MAINTENANCE" />
<span> Maintenance </span>
</Label>
</RadioGroup.Root>
</div>
<div class="mt-4 flex flex-row gap-4">
<div class="w-full">
<Label class="text-sm">
Incident Title
<span class="capitalize">{newIncident.incident_type}</span>
Title
<span class="text-red-500">*</span>
<span
class="float-right mt-2 text-xs font-semibold badge-{newIncident.state}"
>
{newIncident.state}
</span>
{#if newIncident.incident_type == "INCIDENT"}
<span
class="float-right mt-2 text-xs font-semibold badge-{newIncident.state}"
>
{newIncident.state}
</span>
{/if}
</Label>
<Input
class="mt-2"
@@ -667,7 +714,8 @@
<div class="mt-4 flex flex-row gap-4">
<div class="w-full">
<Label class="text-sm">
Incident Summary
<span class="capitalize">{newIncident.incident_type}</span>
Summary
<span class="text-red-500">*</span>
</Label>
<Input
@@ -678,20 +726,37 @@
</div>
</div>
{/if}
<div class="mt-4 flex gap-4">
<div class="col-span-1">
<Label class="mb-2 text-sm" for="start_date_time">
Incident Start Date Time
<span class="capitalize">{newIncident.incident_type}</span> Start
Date Time
<span class="text-red-500">*</span>
</Label>
<DateInput
bind:value={newIncident.startDatetime}
id="start_date_time"
timePrecision="minute"
disabled={!!newIncident.id}
class="mt-2 text-sm"
/>
</div>
{#if newIncident.incident_type == "MAINTENANCE"}
<div class="col-span-1">
<Label class="mb-2 text-sm" for="start_date_time">
<span class="capitalize">{newIncident.incident_type}</span> End
Date Time
<span class="text-red-500">*</span>
</Label>
<DateInput
bind:value={newIncident.endDatetime}
id="end_date_time"
timePrecision="minute"
class="mt-2 text-sm"
min={newIncident.startDatetime}
/>
</div>
{/if}
</div>
<div class="mt-4 grid h-16 w-full grid-cols-6 gap-2 border-t pt-4">
@@ -708,7 +773,9 @@
newIncident.title.trim().length == 0 ||
!!!newIncident.startDatetime ||
(!!!newIncident.id &&
newIncident.firstComment.trim().length == 0)}
newIncident.firstComment.trim().length == 0) ||
(!!!newIncident.endDatetime &&
newIncident.incident_type == "MAINTENANCE")}
>
Save
{#if formStateCreate === "loading"}
@@ -730,42 +797,44 @@
addNewComment();
}}
>
<div
class="bg-hover state-{newComment.state} mt-2 grid grid-cols-4 overflow-hidden rounded-md border text-xs font-medium"
>
{#if currentIncident.incident_type == "INCIDENT"}
<div
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("INVESTIGATING");
}}
class="bg-hover state-{newComment.state} mt-2 grid grid-cols-4 overflow-hidden rounded-md border text-xs font-medium"
>
INVESTIGATING
<div
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("INVESTIGATING");
}}
>
INVESTIGATING
</div>
<div
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("IDENTIFIED");
}}
>
IDENTIFIED
</div>
<div
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("MONITORING");
}}
>
MONITORING
</div>
<div
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("RESOLVED");
}}
>
RESOLVED
</div>
</div>
<div
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("IDENTIFIED");
}}
>
IDENTIFIED
</div>
<div
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("MONITORING");
}}
>
MONITORING
</div>
<div
class="col-span-1 cursor-pointer px-2 py-2 text-center hover:underline"
on:click={() => {
setCommentState("RESOLVED");
}}
>
RESOLVED
</div>
</div>
{/if}
<div class="mt-4 flex w-full gap-4">
<div class="text-sm font-medium leading-7">Time Stamp</div>
<DateInput
@@ -840,9 +909,11 @@
<div
class="text-xs font-semibold text-muted-foreground"
>
<span class="badge-{comment.state}">
{comment.state}
</span>
{#if currentIncident.incident_type == "INCIDENT"}
<span class="badge-{comment.state}">
{comment.state}
</span>
{/if}
{moment(comment.commented_at * 1000).format(
"YYYY-MM-DD HH:mm:ss"
)}