feat: incidents from github to sqlite

This commit is contained in:
Raj Nandan Sharma
2025-01-08 23:14:59 +05:30
parent 0cc30bc67e
commit 70e9086646
29 changed files with 1282 additions and 1044 deletions
+4 -4
View File
@@ -23,7 +23,7 @@ Here is an example
```html
<script
async
src="https://kener.ing/embed-okbookmarks/js?theme=light&monitor=https://kener.ing/embed-okbookmarks"
src="https://kener.ing/embed-earth/js?theme=light&monitor=https://kener.ing/embed-earth"
></script>
```
@@ -40,7 +40,7 @@ Replace `[hostname]` with your kener hostname and `[tag]` with your monitor tag.
### Demo
<div class="border mx-auto rounded-sm w-585px">
<script async src="/embed-okbookmarks/js?theme=dark&monitor=/embed-okbookmarks"></script>
<script async src="/embed-earth/js?theme=dark&monitor=/embed-earth"></script>
</div>
## Iframe
@@ -62,7 +62,7 @@ Here is an example
```html
<iframe
src="https://kener.ing/embed-okbookmarks?theme=light"
src="https://kener.ing/embed-earth?theme=light"
width="100%"
height="200"
allowfullscreen="allowfullscreen"
@@ -83,5 +83,5 @@ You can pass the following parameters to the embed code
### Demo
<div class="border mx-auto rounded-sm w-585px">
<iframe src="/embed-okbookmarks?theme=dark" width="100%" height="200" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>
<iframe src="/embed-earth?theme=dark" width="100%" height="200" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe>
</div>
+333 -1
View File
@@ -236,8 +236,12 @@ section {
.oneline {
transition: transform 0.1s ease-in;
cursor: pointer;
position: relative;
}
.oneline:hover {
.oneline .oneline-in {
transition: transform 0.1s ease-in;
}
.oneline:hover .oneline-in {
transform: scaleY(1.2);
}
@@ -274,6 +278,30 @@ section {
z-index: 0;
}
.comein {
animation: comein 1s ease-in-out;
}
@keyframes comein {
0% {
bottom: -6px;
}
25% {
bottom: 0px;
}
50% {
bottom: -4px;
}
75% {
bottom: -2px;
}
100% {
bottom: -3px;
}
}
/* animate wiggle */
.wiggle:hover {
-webkit-animation-name: wiggle;
-ms-animation-name: wiggle;
@@ -395,3 +423,307 @@ textarea::placeholder {
.newincidents .newincident:last-child .accor .border-b {
border-bottom: none !important;
}
.bounce-right:hover .arrow {
animation: bounceRight 1s;
}
@keyframes bounceRight {
0%,
100% {
transform: translateX(0);
}
20% {
transform: translateX(-4px);
}
40% {
transform: translateX(4px);
}
60% {
transform: translateX(-2px);
}
80% {
transform: translateX(2px);
}
}
.bounce-left:hover .arrow {
animation: bounceLeft 1s;
}
@keyframes bounceLeft {
0%,
100% {
transform: translateX(0);
}
20% {
transform: translateX(4px);
}
40% {
transform: translateX(-4px);
}
60% {
transform: translateX(2px);
}
80% {
transform: translateX(-2px);
}
}
.mesh {
background-image: linear-gradient(
323deg,
rgba(255, 255, 255, 0.01) 0%,
rgba(255, 255, 255, 0.01) 16.667%,
rgba(46, 46, 46, 0.01) 16.667%,
rgba(46, 46, 46, 0.01) 33.334%,
rgba(226, 226, 226, 0.01) 33.334%,
rgba(226, 226, 226, 0.01) 50.001000000000005%,
rgba(159, 159, 159, 0.01) 50.001%,
rgba(159, 159, 159, 0.01) 66.668%,
rgba(149, 149, 149, 0.01) 66.668%,
rgba(149, 149, 149, 0.01) 83.33500000000001%,
rgba(43, 43, 43, 0.01) 83.335%,
rgba(43, 43, 43, 0.01) 100.002%
),
linear-gradient(
346deg,
rgba(166, 166, 166, 0.03) 0%,
rgba(166, 166, 166, 0.03) 25%,
rgba(240, 240, 240, 0.03) 25%,
rgba(240, 240, 240, 0.03) 50%,
rgba(121, 121, 121, 0.03) 50%,
rgba(121, 121, 121, 0.03) 75%,
rgba(40, 40, 40, 0.03) 75%,
rgba(40, 40, 40, 0.03) 100%
),
linear-gradient(
347deg,
rgba(209, 209, 209, 0.01) 0%,
rgba(209, 209, 209, 0.01) 25%,
rgba(22, 22, 22, 0.01) 25%,
rgba(22, 22, 22, 0.01) 50%,
rgba(125, 125, 125, 0.01) 50%,
rgba(125, 125, 125, 0.01) 75%,
rgba(205, 205, 205, 0.01) 75%,
rgba(205, 205, 205, 0.01) 100%
),
linear-gradient(
84deg,
rgba(195, 195, 195, 0.01) 0%,
rgba(195, 195, 195, 0.01) 14.286%,
rgba(64, 64, 64, 0.01) 14.286%,
rgba(64, 64, 64, 0.01) 28.572%,
rgba(67, 67, 67, 0.01) 28.572%,
rgba(67, 67, 67, 0.01) 42.858%,
rgba(214, 214, 214, 0.01) 42.858%,
rgba(214, 214, 214, 0.01) 57.144%,
rgba(45, 45, 45, 0.01) 57.144%,
rgba(45, 45, 45, 0.01) 71.42999999999999%,
rgba(47, 47, 47, 0.01) 71.43%,
rgba(47, 47, 47, 0.01) 85.71600000000001%,
rgba(172, 172, 172, 0.01) 85.716%,
rgba(172, 172, 172, 0.01) 100.002%
),
linear-gradient(
73deg,
rgba(111, 111, 111, 0.03) 0%,
rgba(111, 111, 111, 0.03) 16.667%,
rgba(202, 202, 202, 0.03) 16.667%,
rgba(202, 202, 202, 0.03) 33.334%,
rgba(57, 57, 57, 0.03) 33.334%,
rgba(57, 57, 57, 0.03) 50.001000000000005%,
rgba(197, 197, 197, 0.03) 50.001%,
rgba(197, 197, 197, 0.03) 66.668%,
rgba(97, 97, 97, 0.03) 66.668%,
rgba(97, 97, 97, 0.03) 83.33500000000001%,
rgba(56, 56, 56, 0.03) 83.335%,
rgba(56, 56, 56, 0.03) 100.002%
),
linear-gradient(
132deg,
rgba(88, 88, 88, 0.03) 0%,
rgba(88, 88, 88, 0.03) 20%,
rgba(249, 249, 249, 0.03) 20%,
rgba(249, 249, 249, 0.03) 40%,
rgba(2, 2, 2, 0.03) 40%,
rgba(2, 2, 2, 0.03) 60%,
rgba(185, 185, 185, 0.03) 60%,
rgba(185, 185, 185, 0.03) 80%,
rgba(196, 196, 196, 0.03) 80%,
rgba(196, 196, 196, 0.03) 100%
),
linear-gradient(
142deg,
rgba(160, 160, 160, 0.03) 0%,
rgba(160, 160, 160, 0.03) 12.5%,
rgba(204, 204, 204, 0.03) 12.5%,
rgba(204, 204, 204, 0.03) 25%,
rgba(108, 108, 108, 0.03) 25%,
rgba(108, 108, 108, 0.03) 37.5%,
rgba(191, 191, 191, 0.03) 37.5%,
rgba(191, 191, 191, 0.03) 50%,
rgba(231, 231, 231, 0.03) 50%,
rgba(231, 231, 231, 0.03) 62.5%,
rgba(70, 70, 70, 0.03) 62.5%,
rgba(70, 70, 70, 0.03) 75%,
rgba(166, 166, 166, 0.03) 75%,
rgba(166, 166, 166, 0.03) 87.5%,
rgba(199, 199, 199, 0.03) 87.5%,
rgba(199, 199, 199, 0.03) 100%
),
linear-gradient(
238deg,
rgba(116, 116, 116, 0.02) 0%,
rgba(116, 116, 116, 0.02) 20%,
rgba(141, 141, 141, 0.02) 20%,
rgba(141, 141, 141, 0.02) 40%,
rgba(152, 152, 152, 0.02) 40%,
rgba(152, 152, 152, 0.02) 60%,
rgba(61, 61, 61, 0.02) 60%,
rgba(61, 61, 61, 0.02) 80%,
rgba(139, 139, 139, 0.02) 80%,
rgba(139, 139, 139, 0.02) 100%
),
linear-gradient(
188deg,
rgba(227, 227, 227, 0.01) 0%,
rgba(227, 227, 227, 0.01) 20%,
rgba(105, 105, 105, 0.01) 20%,
rgba(105, 105, 105, 0.01) 40%,
rgba(72, 72, 72, 0.01) 40%,
rgba(72, 72, 72, 0.01) 60%,
rgba(33, 33, 33, 0.01) 60%,
rgba(33, 33, 33, 0.01) 80%,
rgba(57, 57, 57, 0.01) 80%,
rgba(57, 57, 57, 0.01) 100%
),
linear-gradient(90deg, hsl(237, 0%, 13%), hsl(237, 0%, 13%));
}
.dark .mesh {
background-image: linear-gradient(
158deg,
rgba(84, 84, 84, 0.03) 0%,
rgba(84, 84, 84, 0.03) 20%,
rgba(219, 219, 219, 0.03) 20%,
rgba(219, 219, 219, 0.03) 40%,
rgba(54, 54, 54, 0.03) 40%,
rgba(54, 54, 54, 0.03) 60%,
rgba(99, 99, 99, 0.03) 60%,
rgba(99, 99, 99, 0.03) 80%,
rgba(92, 92, 92, 0.03) 80%,
rgba(92, 92, 92, 0.03) 100%
),
linear-gradient(
45deg,
rgba(221, 221, 221, 0.02) 0%,
rgba(221, 221, 221, 0.02) 14.286%,
rgba(8, 8, 8, 0.02) 14.286%,
rgba(8, 8, 8, 0.02) 28.572%,
rgba(52, 52, 52, 0.02) 28.572%,
rgba(52, 52, 52, 0.02) 42.858%,
rgba(234, 234, 234, 0.02) 42.858%,
rgba(234, 234, 234, 0.02) 57.144%,
rgba(81, 81, 81, 0.02) 57.144%,
rgba(81, 81, 81, 0.02) 71.42999999999999%,
rgba(239, 239, 239, 0.02) 71.43%,
rgba(239, 239, 239, 0.02) 85.71600000000001%,
rgba(187, 187, 187, 0.02) 85.716%,
rgba(187, 187, 187, 0.02) 100.002%
),
linear-gradient(
109deg,
rgba(33, 33, 33, 0.03) 0%,
rgba(33, 33, 33, 0.03) 12.5%,
rgba(147, 147, 147, 0.03) 12.5%,
rgba(147, 147, 147, 0.03) 25%,
rgba(131, 131, 131, 0.03) 25%,
rgba(131, 131, 131, 0.03) 37.5%,
rgba(151, 151, 151, 0.03) 37.5%,
rgba(151, 151, 151, 0.03) 50%,
rgba(211, 211, 211, 0.03) 50%,
rgba(211, 211, 211, 0.03) 62.5%,
rgba(39, 39, 39, 0.03) 62.5%,
rgba(39, 39, 39, 0.03) 75%,
rgba(55, 55, 55, 0.03) 75%,
rgba(55, 55, 55, 0.03) 87.5%,
rgba(82, 82, 82, 0.03) 87.5%,
rgba(82, 82, 82, 0.03) 100%
),
linear-gradient(
348deg,
rgba(42, 42, 42, 0.02) 0%,
rgba(42, 42, 42, 0.02) 20%,
rgba(8, 8, 8, 0.02) 20%,
rgba(8, 8, 8, 0.02) 40%,
rgba(242, 242, 242, 0.02) 40%,
rgba(242, 242, 242, 0.02) 60%,
rgba(42, 42, 42, 0.02) 60%,
rgba(42, 42, 42, 0.02) 80%,
rgba(80, 80, 80, 0.02) 80%,
rgba(80, 80, 80, 0.02) 100%
),
linear-gradient(
120deg,
rgba(106, 106, 106, 0.03) 0%,
rgba(106, 106, 106, 0.03) 14.286%,
rgba(67, 67, 67, 0.03) 14.286%,
rgba(67, 67, 67, 0.03) 28.572%,
rgba(134, 134, 134, 0.03) 28.572%,
rgba(134, 134, 134, 0.03) 42.858%,
rgba(19, 19, 19, 0.03) 42.858%,
rgba(19, 19, 19, 0.03) 57.144%,
rgba(101, 101, 101, 0.03) 57.144%,
rgba(101, 101, 101, 0.03) 71.42999999999999%,
rgba(205, 205, 205, 0.03) 71.43%,
rgba(205, 205, 205, 0.03) 85.71600000000001%,
rgba(53, 53, 53, 0.03) 85.716%,
rgba(53, 53, 53, 0.03) 100.002%
),
linear-gradient(
45deg,
rgba(214, 214, 214, 0.03) 0%,
rgba(214, 214, 214, 0.03) 16.667%,
rgba(255, 255, 255, 0.03) 16.667%,
rgba(255, 255, 255, 0.03) 33.334%,
rgba(250, 250, 250, 0.03) 33.334%,
rgba(250, 250, 250, 0.03) 50.001000000000005%,
rgba(231, 231, 231, 0.03) 50.001%,
rgba(231, 231, 231, 0.03) 66.668%,
rgba(241, 241, 241, 0.03) 66.668%,
rgba(241, 241, 241, 0.03) 83.33500000000001%,
rgba(31, 31, 31, 0.03) 83.335%,
rgba(31, 31, 31, 0.03) 100.002%
),
linear-gradient(
59deg,
rgba(224, 224, 224, 0.03) 0%,
rgba(224, 224, 224, 0.03) 12.5%,
rgba(97, 97, 97, 0.03) 12.5%,
rgba(97, 97, 97, 0.03) 25%,
rgba(143, 143, 143, 0.03) 25%,
rgba(143, 143, 143, 0.03) 37.5%,
rgba(110, 110, 110, 0.03) 37.5%,
rgba(110, 110, 110, 0.03) 50%,
rgba(34, 34, 34, 0.03) 50%,
rgba(34, 34, 34, 0.03) 62.5%,
rgba(155, 155, 155, 0.03) 62.5%,
rgba(155, 155, 155, 0.03) 75%,
rgba(249, 249, 249, 0.03) 75%,
rgba(249, 249, 249, 0.03) 87.5%,
rgba(179, 179, 179, 0.03) 87.5%,
rgba(179, 179, 179, 0.03) 100%
),
linear-gradient(
241deg,
rgba(58, 58, 58, 0.02) 0%,
rgba(58, 58, 58, 0.02) 25%,
rgba(124, 124, 124, 0.02) 25%,
rgba(124, 124, 124, 0.02) 50%,
rgba(254, 254, 254, 0.02) 50%,
rgba(254, 254, 254, 0.02) 75%,
rgba(52, 52, 52, 0.02) 75%,
rgba(52, 52, 52, 0.02) 100%
),
linear-gradient(90deg, #ffffff, #ffffff);
}
+63 -25
View File
@@ -10,14 +10,16 @@
if (incident.end_date_time) {
endTime = incident.end_date_time;
}
let lastedFor = moment.duration(endTime - startTime, "seconds").humanize();
let startedAt = moment.duration(nowTime - startTime, "seconds").humanize();
let isFuture = false;
//is future incident
if (nowTime < startTime) {
isFuture = true;
}
</script>
<div
class="newincident relative grid w-full grid-cols-12 gap-2 px-0 py-0 last:border-b-0 md:w-[655px]"
>
<div class="newincident relative grid w-full grid-cols-12 gap-2 px-0 py-0 last:border-b-0">
<div class="col-span-12">
<Accordion.Root bind:value={index} class="accor">
<Accordion.Item value="incident-0">
@@ -35,42 +37,78 @@
<p
class="scroll-m-20 text-sm font-medium tracking-wide text-muted-foreground"
>
{#if incident.state != "RESOLVED"}
{#if !isFuture && incident.state != "RESOLVED"}
<span>
Started about {startedAt} ago, still ongoing
</span>
{:else}
{:else if !isFuture && incident.state == "RESOLVED"}
<span>
Started about {startedAt} ago, lasted for about {lastedFor}
</span>
{:else if isFuture && incident.state != "RESOLVED"}
<span>
Starts in {startedAt}
</span>
{:else if isFuture && incident.state == "RESOLVED"}
<span>
Starts in {startedAt}, will last for about {lastedFor}
</span>
{/if}
</p>
</div>
</Accordion.Trigger>
<Accordion.Content>
<div class="mt-2 px-4">
<ol class="relative pl-14">
{#each incident.comments as comment}
<li class="relative border-l pb-4 pl-[4.5rem] last:border-0">
<div class="px-4 pt-2">
{#if incident.monitors.length > 0}
<div class="flex gap-2">
{#each incident.monitors as monitor}
<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"
class="tag-affected-text flex gap-x-2 rounded-md bg-secondary px-1 py-1 pr-2"
>
{comment.state}
<div
class="bg-api-{monitor.impact_type.toLowerCase()} rounded px-1.5 py-1 text-xs font-semibold text-primary-foreground"
>
{monitor.impact_type}
</div>
{#if monitor.image}
<img src={monitor.image} class="mt-1 h-4 w-4" />
{/if}
<div class="mt-0.5 font-medium">
{monitor.name}
</div>
</div>
<time
class=" mb-1 text-sm font-normal leading-none text-muted-foreground"
>
{moment(comment.created_at).format(
"MMMM Do YYYY, h:mm:ss a"
)}
</time>
{/each}
</div>
{/if}
<p class="my-3 text-xs font-semibold uppercase text-muted-foreground">
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"
>
{comment.state}
</div>
<time
class=" mb-1 text-sm font-medium leading-none text-muted-foreground"
>
{moment(comment.created_at).format(
"MMMM Do YYYY, h:mm:ss a"
)}
</time>
<p class="mb-4 text-sm font-normal">
{comment.comment}
</p>
</li>
{/each}
</ol>
<p class="mb-4 text-sm font-normal">
{comment.comment}
</p>
</li>
{/each}
</ol>
{:else}
<p class="text-sm font-medium">No Updates Yet</p>
{/if}
</div>
</Accordion.Content>
</Accordion.Item>
+66 -29
View File
@@ -1,7 +1,7 @@
<script>
import { Badge } from "$lib/components/ui/badge";
import * as Popover from "$lib/components/ui/popover";
import { onMount } from "svelte";
import { Badge } from "$lib/components/ui/badge";
import { Button } from "$lib/components/ui/button";
import { base } from "$app/paths";
import { Share2, Link, CopyCheck, Code, TrendingUp, Percent, Loader } from "lucide-svelte";
@@ -15,6 +15,7 @@
import LoaderBoxes from "$lib/components/loaderbox.svelte";
import moment from "moment";
import NumberFlow from "@number-flow/svelte";
import Incident from "$lib/components/IncidentNew.svelte";
const dispatch = createEventDispatcher();
@@ -26,20 +27,40 @@
let _0Day = {};
let _90Day = monitor.pageData._90Day;
let uptime90Day = monitor.pageData.uptime90Day;
let incidents = {};
let dayIncidentsFull = [];
function getToday(startTs) {
//axios post using options application json
function loadIncidents() {
axios
.post(`${base}/api/today/incidents`, {
tag: monitor.tag,
startTs: monitor.pageData.midnight90DaysAgo,
endTs: monitor.pageData.midnightTomorrow,
localTz: localTz
})
.then((response) => {
if (response.data) {
incidents = response.data;
}
})
.catch((error) => {
console.log(error);
});
}
function getToday(startTs, incidentIDs) {
axios
.post(`${base}/api/today`, {
monitor: monitor,
localTz: localTz,
startTs: startTs
startTs: startTs,
incidentIDs: incidentIDs
})
.then((response) => {
if (response.data) {
_0Day = response.data._0Day;
dayUptime = response.data.uptime;
dayIncidentsFull = response.data.incidents;
}
loadingDayData = false;
})
@@ -117,8 +138,8 @@
}
onMount(async () => {
//for each div with class 90daygrid scroll to right most for mobile view needed
scrollToRight();
loadIncidents();
});
afterUpdate(() => {
dispatch("heightChange", {});
@@ -134,16 +155,18 @@
let dateFetchedFor = "";
let dayUptime = "NA";
let loadingDayData = false;
function dailyDataGetter(e, bar) {
function dailyDataGetter(e, bar, incidentObj) {
if (monitor.embed) {
return;
}
let incidentIDs = incidentObj?.ids || [];
dayUptime = "NA";
dateFetchedFor = moment(new Date(bar.timestamp * 1000)).format("dddd, MMMM Do, YYYY");
showDailyDataModal = true;
loadingDayData = true;
dayIncidentsFull = [];
setTimeout(() => {
getToday(bar.timestamp);
getToday(bar.timestamp, incidentIDs);
}, 750);
}
</script>
@@ -251,13 +274,15 @@
</div>
</div>
</div>
<div class="relative col-span-12 mt-1">
<div
class="relative col-span-12 mt-1"
use:clickOutsideAction
on:clickoutside={(e) => {
showDailyDataModal = false;
}}
>
<div
class="daygrid90 flex min-h-[60px] justify-start overflow-x-auto overflow-y-hidden py-1"
use:clickOutsideAction
on:clickoutside={(e) => {
showDailyDataModal = false;
}}
>
{#each Object.entries(_90Day) as [ts, bar]}
<a
@@ -267,13 +292,12 @@
show90Inline(e, bar);
}}
on:click={(e) => {
dailyDataGetter(e, bar);
dailyDataGetter(e, bar, incidents[ts]);
}}
style="transition: border-color {bar.ij * 2 + 100}ms ease-in;"
style="transition: opacity {bar.ij * 2 + 100}ms ease-in;"
href="#"
class="oneline h-[34px] w-[6px] border-b-2 {bar.border
? 'border-indigo-400'
: 'border-transparent'} pb-1"
class="oneline h-[34px] w-[6px]
{bar.border ? 'opacity-100' : 'opacity-20'} pb-1"
>
<div
class="oneline-in h-[30px] bg-{bar.cssClass} mx-auto w-[4px] rounded-{monitor.pageData.barRoundness.toUpperCase() ==
@@ -281,10 +305,20 @@
? 'none'
: 'sm'}"
></div>
{#if !!incidents[ts]}
<div
style="transition-delay: {Math.floor(
Math.random() * (1500 - 500 + 1)
) + 500}ms;"
class="bg-api-{incidents[
ts
].monitor_impact.toLowerCase()} comein absolute -bottom-[3px] left-[1px] h-[4px] w-[4px] rounded-full"
></div>
{/if}
</a>
{#if bar.showDetails}
<div class="show-hover absolute text-sm">
<div class="text-{bar.textClass} pt-1 text-xs font-semibold">
<div class="text-{bar.textClass} text-xs font-semibold">
{moment(new Date(bar.timestamp * 1000)).format(
"dddd, MMMM Do, YYYY"
)} -
@@ -308,6 +342,20 @@
>
{/if}
</div>
{#if dayIncidentsFull.length > 0}
<div class="-mx-2 mb-4 grid grid-cols-1">
<div class="col-span-1 px-2">
<Badge variant="outline" class="border-0 pl-0">
{l(lang, "root.incidents")}
</Badge>
</div>
{#each dayIncidentsFull as incident, index}
<div class="col-span-1">
<Incident {incident} {lang} index="incident-{index}" />
</div>
{/each}
</div>
{/if}
<div class="flex flex-wrap">
{#if loadingDayData}
<LoaderBoxes />
@@ -349,17 +397,6 @@
</div>
{/if}
</div>
{#if !!!monitor.embed}
<p class="z-4 absolute bottom-3 right-14 float-right text-right">
<a
href="{base}/incident/{monitor.tag}#past_incident"
class="text-xs font-medium text-muted-foreground hover:text-primary"
>
{l(lang, "root.recent_incidents")}
</a>
</p>
{/if}
</div>
</div>
</div>
+1 -1
View File
@@ -109,7 +109,7 @@
onMount(async () => {
protocol = window.location.protocol;
domain = window.location.host;
pathMonitorLink = `${protocol}//${domain}${base}/monitor-${monitor.tag}`;
pathMonitorLink = `${protocol}//${domain}${base}/?monitor=${monitor.tag}`;
pathMonitorBadgeUptime = `${protocol}//${domain}${base}/badge/${monitor.tag}/uptime`;
pathMonitorBadgeStatus = `${protocol}//${domain}${base}/badge/${monitor.tag}/status`;
pathMonitorBadgeDot = `${protocol}//${domain}${base}/badge/${monitor.tag}/dot`;
+41 -15
View File
@@ -397,21 +397,18 @@ export const GetDataGroupByDayAlternative = async (
}, {});
// Transform grouped data to final format
return Object.values(groupedData)
.map((group) => ({
timestamp: group.timestamp,
total: group.total,
UP: group.UP,
DOWN: group.DOWN,
DEGRADED: group.DEGRADED,
avgLatency:
group.total > 0 ? Number((group.latencySum / group.total).toFixed(3)) : null,
maxLatency:
group.latencies.length > 0 ? Number(Math.max(...group.latencies).toFixed(3)) : null,
minLatency:
group.latencies.length > 0 ? Number(Math.min(...group.latencies).toFixed(3)) : null
}))
.sort((a, b) => a.timestamp - b.timestamp);
return Object.values(groupedData).map((group) => ({
timestamp: group.timestamp,
total: group.total,
UP: group.UP,
DOWN: group.DOWN,
DEGRADED: group.DEGRADED,
avgLatency: group.total > 0 ? Number((group.latencySum / group.total).toFixed(3)) : null,
maxLatency:
group.latencies.length > 0 ? Number(Math.max(...group.latencies).toFixed(3)) : null,
minLatency:
group.latencies.length > 0 ? Number(Math.min(...group.latencies).toFixed(3)) : null
}));
};
export const CreateIncident = async (data) => {
@@ -639,3 +636,32 @@ export const GetIncidentsOpenHome = async (homeIncidentCount) => {
return incidents;
};
export const GetIncidentsPage = async (start, open) => {
let incidents = await db.getIncidentsBetween(start, open);
for (let i = 0; i < incidents.length; i++) {
incidents[i].monitors = await GetIncidentMonitors(incidents[i].id);
}
//get comments
for (let i = 0; i < incidents.length; i++) {
incidents[i].comments = await GetIncidentActiveComments(incidents[i].id);
}
return incidents;
};
export const GetIncidentsByIDS = async (ids) => {
if (ids.length == 0) {
return [];
}
let incidents = await db.getIncidentsByIds(ids);
for (let i = 0; i < incidents.length; i++) {
incidents[i].monitors = [];
}
//get comments
for (let i = 0; i < incidents.length; i++) {
incidents[i].comments = await GetIncidentActiveComments(incidents[i].id);
}
return incidents;
};
+19 -78
View File
@@ -3,20 +3,11 @@ import axios from "axios";
import ping from "ping";
import { UP, DOWN, DEGRADED } from "./constants.js";
import {
GetNowTimestampUTC,
GetMinuteStartNowTimestampUTC,
GetMinuteStartTimestampUTC,
ReplaceAllOccurrences,
GetRequiredSecrets,
ValidateMonitorAlerts
GetRequiredSecrets
} from "./tool.js";
import {
GetIncidentsManual,
GetEndTimeFromBody,
GetStartTimeFromBody,
CloseIssue
} from "./github.js";
import Randomstring from "randomstring";
import alerting from "./alerting.js";
import Queue from "queue";
import dotenv from "dotenv";
@@ -53,82 +44,32 @@ const defaultEval = `(function (statusCode, responseTime, responseData) {
})`;
async function manualIncident(monitor) {
let incidentsResp = await GetIncidentsManual(monitor.tag, "open");
let startTs = GetMinuteStartNowTimestampUTC();
let impactArr = await db.getIncidentsByMonitorTagRealtime(monitor.tag, startTs);
let manualData = {};
if (incidentsResp.length == 0) {
return manualData;
let impact = "";
if (impactArr.length == 0) {
return {};
}
let timeDownStart = +Infinity;
let timeDownEnd = 0;
let timeDegradedStart = +Infinity;
let timeDegradedEnd = 0;
for (let i = 0; i < incidentsResp.length; i++) {
const incident = incidentsResp[i];
const incident_number = incident.number;
let start_time = GetStartTimeFromBody(incident.body);
let allLabels = incident.labels.map((label) => label.name);
if (
allLabels.indexOf("incident-degraded") == -1 &&
allLabels.indexOf("incident-down") == -1
) {
continue;
for (let i = 0; i < impactArr.length; i++) {
const element = impactArr[i];
if (element.monitor_impact === "DOWN") {
impact = "DOWN";
break;
}
if (start_time === null) {
continue;
}
let newIncident = {
start_time: start_time
};
let end_time = GetEndTimeFromBody(incident.body);
if (end_time !== null) {
newIncident.end_time = end_time;
if (end_time <= GetNowTimestampUTC() && incident.state === "open") {
//close the issue after 30 secs
setTimeout(async () => {
await CloseIssue(incident_number);
}, 30000);
}
} else {
newIncident.end_time = GetNowTimestampUTC();
}
//check if labels has incident-degraded
if (allLabels.indexOf("incident-degraded") !== -1) {
timeDegradedStart = Math.min(timeDegradedStart, newIncident.start_time);
timeDegradedEnd = Math.max(timeDegradedEnd, newIncident.end_time);
}
if (allLabels.indexOf("incident-down") !== -1) {
timeDownStart = Math.min(timeDownStart, newIncident.start_time);
timeDownEnd = Math.max(timeDownEnd, newIncident.end_time);
if (element.monitor_impact === "DEGRADED") {
impact = "DEGRADED";
}
}
//start from start of minute if unix timeDownStart to timeDownEnd, step each minute
let start = GetMinuteStartTimestampUTC(timeDegradedStart);
let end = GetMinuteStartTimestampUTC(timeDegradedEnd);
for (let i = start; i <= end; i += 60) {
manualData[i] = {
status: DEGRADED,
let manualData = {
[startTs]: {
status: impact,
latency: 0,
type: MANUAL
};
}
start = GetMinuteStartTimestampUTC(timeDownStart);
end = GetMinuteStartTimestampUTC(timeDownEnd);
for (let i = start; i <= end; i += 60) {
manualData[i] = {
status: DOWN,
latency: 0,
type: MANUAL
};
}
}
};
return manualData;
}
+66
View File
@@ -603,6 +603,26 @@ class Sqlite {
});
}
//get id of first incident less than given start_date_time
async getPreviousIncidentId(start_date_time) {
let stmt = this.db.prepare(`
SELECT id FROM incidents
WHERE start_date_time < @start_date_time
ORDER BY start_date_time DESC
LIMIT 1;
`);
return stmt.get({ start_date_time });
}
//get incidents where ts between start and end
async getIncidentsBetween(start, end) {
let stmt = this.db.prepare(`
SELECT * FROM incidents
WHERE status = 'OPEN' AND start_date_time >= @start AND start_date_time <= @end order by start_date_time ASC;
`);
return stmt.all({ start, end });
}
//get total incident count
async getIncidentsCount(filter) {
let query = `
@@ -678,6 +698,52 @@ class Sqlite {
return stmt.all({ incident_id });
}
//given a monitor tag get incidents last 90 days status = OPEN
async getIncidentsByMonitorTag(monitor_tag, start, end) {
let stmt = this.db.prepare(`
SELECT
i.id AS id,
i.title AS title,
i.start_date_time AS start_date_time,
i.end_date_time AS end_date_time,
i.created_at AS created_at,
i.updated_at AS updated_at,
i.status AS status,
i.state AS state,
im.monitor_impact
FROM incidents i
INNER JOIN incident_monitors im
ON i.id = im.incident_id
WHERE im.monitor_tag = @monitor_tag AND i.start_date_time >= @start AND i.start_date_time <= @end AND i.status='OPEN';
`);
return stmt.all({ monitor_tag, start, end });
}
//given a timestamp get incidents that are open and start time is less than given timestamp
async getIncidentsByMonitorTagRealtime(monitor_tag, timestamp) {
let stmt = this.db.prepare(`
SELECT
i.id AS id,
i.start_date_time AS start_date_time,
i.end_date_time AS end_date_time,
im.monitor_impact
FROM incidents i
INNER JOIN incident_monitors im
ON i.id = im.incident_id
WHERE im.monitor_tag = @monitor_tag AND i.start_date_time < @timestamp AND i.status = 'OPEN' AND i.state != 'RESOLVED';
`);
return stmt.all({ monitor_tag, timestamp });
}
//given array of ids get incidents
async getIncidentsByIds(ids) {
let stmt = this.db.prepare(`
SELECT * FROM incidents
WHERE status='OPEN' AND id IN (${ids.map(() => "?").join(",")});
`);
return stmt.all(ids);
}
//remove monitor tag from incident given incident_id and monitor_tag
async removeIncidentMonitor(incident_id, monitor_tag) {
let stmt = this.db.prepare(`
+3 -1
View File
@@ -180,7 +180,9 @@ const FetchData = async function (site, monitor, localTz) {
uptime90Day: uptime90Day,
summaryText: summaryText,
summaryColorClass: summaryColorClass,
barRoundness: site.barRoundness
barRoundness: site.barRoundness,
midnight90DaysAgo: midnight90DaysAgo,
midnightTomorrow: midnightTomorrow
};
};
export { FetchData };
+3 -2
View File
@@ -44,10 +44,11 @@ input[type="file"] {
#end_date_time,
#newcomment_commented_at {
width: 100%;
padding: 6px 8px;
padding: 8px 12px;
border-radius: 6px;
font-size: 14px;
height: 2rem;
height: 40px;
line-height: 20px;
font-weight: 500;
width: 100%;
}
+47 -7
View File
@@ -2,15 +2,24 @@
import { FetchData } from "$lib/server/page";
import { GetMonitors, GetIncidentsOpenHome } from "$lib/server/controllers/controller.js";
export async function load({ parent }) {
export async function load({ parent, url }) {
let monitors = await GetMonitors({ status: "ACTIVE" });
const query = url.searchParams;
const requiredCategory = query.get("category") || "Home";
const parentData = await parent();
const siteData = parentData.site;
const github = siteData.github;
const monitorsActive = [];
for (let i = 0; i < monitors.length; i++) {
//only return monitors that have category as home or category is not present
if (!!monitors[i].category_name && monitors[i].category_name !== "Home") {
if (
!!!query.get("monitor") &&
!!monitors[i].category_name &&
monitors[i].category_name !== requiredCategory
) {
continue;
}
if (query.get("monitor") && query.get("monitor") !== monitors[i].tag) {
continue;
}
delete monitors[i].api;
@@ -24,12 +33,43 @@ export async function load({ parent }) {
monitorsActive.push(monitors[i]);
}
let allOpenIncidents = await GetIncidentsOpenHome(siteData.homeIncidentCount);
let resolvedIncidents = allOpenIncidents.filter((incident) => incident.state === "RESOLVED");
let unresolvedIncidents = allOpenIncidents.filter((incident) => incident.state !== "RESOLVED");
let eligibleTags = monitorsActive.map((monitor) => monitor.tag);
//filter incidents that have monitor_tag in monitors
allOpenIncidents = allOpenIncidents.filter((incident) => {
let incidentMonitors = incident.monitors;
let monitorTags = incidentMonitors.map((monitor) => monitor.monitor_tag);
let isPresent = false;
monitorTags.forEach((tag) => {
if (eligibleTags.includes(tag)) {
isPresent = true;
}
});
return isPresent;
});
allOpenIncidents = allOpenIncidents.map((incident) => {
let incidentMonitors = incident.monitors;
let monitorTags = incidentMonitors.map((monitor) => monitor.monitor_tag);
let xm = monitors.filter((monitor) => monitorTags.includes(monitor.tag));
incident.monitors = xm.map((monitor) => {
return {
tag: monitor.tag,
name: monitor.name,
description: monitor.description,
image: monitor.image,
impact_type: incidentMonitors.filter((m) => m.monitor_tag === monitor.tag)[0]
.monitor_impact
};
});
return incident;
});
let unresolvedIncidents = allOpenIncidents.filter((incident) => incident.state !== "RESOLVED");
return {
monitors: monitorsActive,
resolvedIncidents: resolvedIncidents,
unresolvedIncidents: unresolvedIncidents
unresolvedIncidents: allOpenIncidents,
categoryName: requiredCategory,
isCategoryPage: !!query.get("category") && query.get("category") !== "Home",
isMonitorPage: !!query.get("monitor")
};
}
+80 -48
View File
@@ -6,7 +6,7 @@
import { Badge } from "$lib/components/ui/badge";
import { l } from "$lib/i18n/client";
import { base } from "$app/paths";
import { ArrowRight, X } from "lucide-svelte";
import { ArrowRight, ChevronLeft, X } from "lucide-svelte";
import { hotKeyAction, clickOutsideAction } from "svelte-legos";
import { onMount } from "svelte";
import ShareMenu from "$lib/components/shareMenu.svelte";
@@ -29,14 +29,25 @@
}
}
}
let isHome = !data.isCategoryPage && !data.isMonitorPage;
if (data.isCategoryPage) {
let category = data.site.categories.find((e) => e.name == data.categoryName);
data.site.hero.title = category.name;
data.site.hero.subtitle = category.description;
}
onMount(() => {
pageLoaded = true;
});
</script>
<div class="mt-12"></div>
{#if data.site.hero}
<section class="mx-auto mb-8 flex w-full max-w-4xl flex-1 flex-col items-start justify-center">
{#if data.site.hero && !data.isMonitorPage}
<section
class="mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center"
>
<div class="mx-auto max-w-screen-xl px-4 lg:flex lg:items-center">
<div class="blurry-bg mx-auto max-w-3xl text-center">
{#if data.site.hero.image}
@@ -56,6 +67,26 @@
</div>
</section>
{/if}
{#if !isHome}
<section
class="mx-auto my-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center"
>
<Button
variant="secondary"
class="bounce-left h-8 justify-start pl-1.5"
on:click={() => {
if (data.isCategoryPage) {
return window.history.back();
}
if (data.isMonitorPage) {
return (window.location.href = `${base}/`);
}
}}
>
<ChevronLeft class="arrow mr-1 h-5 w-5" /> Back
</Button>
</section>
{/if}
{#if data.unresolvedIncidents.length > 0}
<section
class="mx-auto mb-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center bg-transparent"
@@ -73,8 +104,8 @@
class="mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center backdrop-blur-[2px]"
id=""
>
<Card.Root>
<Card.Content class=" newincidents p-0">
<Card.Root class="w-full">
<Card.Content class=" newincidents w-full overflow-hidden p-0">
{#each data.unresolvedIncidents as incident, index}
<Incident {incident} lang={data.lang} index="incident-{index}" />
{/each}
@@ -135,58 +166,59 @@
</Card.Root>
</section>
{/if}
{#if data.site.categories}
{#if data.site.categories && isHome}
<section
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}
<Card.Root class="mb-2 w-full">
<Card.Header class="relative pr-[100px]">
<Card.Title class="">{category.name}</Card.Title>
<Card.Description>
{#if category.description}
{@html category.description}
{/if}
<a
href="{base}/category-{category.name}"
class="{buttonVariants({
variant: 'ghost'
})} absolute right-2 top-1/2 -translate-y-1/2 transform"
>
<ArrowRight class="h-4 w-4" />
</a>
</Card.Description>
</Card.Header>
</Card.Root>
<div
on:click={() => {
window.location.href = `?category=${category.name}`;
}}
>
<Card.Root class="hover:bg-secondary">
<Card.Header class="bounce-right relative w-full cursor-pointer px-4 ">
<Card.Title class="w-full ">
{category.name}
<Button
variant="ghost"
class="arrow absolute right-4 top-9 h-5 w-5 p-0"
size="icon"
>
<ArrowRight class="h-4 w-4" />
</Button>
</Card.Title>
<Card.Description>
{#if category.description}
{@html category.description}
{/if}
</Card.Description>
</Card.Header>
</Card.Root>
</div>
{/each}
</section>
{/if}
{#if data.resolvedIncidents.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=""
<section
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`;
}}
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="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, "root.recent_incidents")}
</Badge>
</div>
<div class="col-span-1 text-left">
{l(data.lang, "root.recent_incidents")}
</div>
</section>
<section
class="mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center backdrop-blur-[2px]"
id=""
>
<Card.Root>
<Card.Content class="newincidents p-0">
{#each data.resolvedIncidents as incident, index}
<Incident {incident} lang={data.lang} index="incidentx-{index}" />
{/each}
</Card.Content>
</Card.Root>
</section>
{/if}
<div class="text-right">
<span class="arrow float-right mt-0.5">
<ArrowRight class="h-4 w-4" />
</span>
</div>
</div>
</section>
{#if shareMenusToggle}
<div
transition:scale={{ duration: 100 }}
@@ -2,12 +2,6 @@
// @ts-ignore
import { json } from "@sveltejs/kit";
import { auth, ParseIncidentPayload, GHIssueToKenerIncident } from "$lib/server/webhook";
import {
GetIncidentByNumber,
GetStartTimeFromBody,
GetEndTimeFromBody,
UpdateIssue
} from "$lib/server/github";
import {
CreateIncident,
@@ -2,7 +2,6 @@
// @ts-ignore
import { json } from "@sveltejs/kit";
import { auth, GHIssueToKenerIncident } from "$lib/server/webhook";
import { UpdateIssueLabels, GetIncidentByNumber } from "$lib/server/github";
export async function POST({ request, params }) {
const payload = await request.json();
@@ -41,7 +40,7 @@ export async function POST({ request, params }) {
);
}
let issue = await GetIncidentByNumber(incident_number);
let issue = null;
if (issue === null) {
return json(
{ error: "github error" },
@@ -73,7 +72,7 @@ export async function POST({ request, params }) {
body = body + " " + `[end_datetime:${endDatetime}]`;
}
let resp = await UpdateIssueLabels(incident_number, labels, body);
let resp = null;
if (resp === null) {
return json(
{ error: "github error" },
+3 -2
View File
@@ -8,12 +8,13 @@ import {
ParseUptime
} from "$lib/server/tool.js";
import db from "$lib/server/db/db.js";
import { GetAllSiteData } from "$lib/server/controllers/controller.js";
import { GetAllSiteData, GetIncidentsByIDS } from "$lib/server/controllers/controller.js";
export async function POST({ request }) {
const payload = await request.json();
const monitor = payload.monitor;
const localTz = payload.localTz;
const incidentIDs = payload.incidentIDs || [];
let _0Day = {};
const now = GetMinuteStartNowTimestampUTC() + 60;
@@ -68,7 +69,7 @@ export async function POST({ request }) {
uptime = ParseUptime(ups, total);
}
return json({ _0Day, uptime });
return json({ _0Day, uptime, incidents: await GetIncidentsByIDS(incidentIDs) });
}
export async function GET({ request }) {
@@ -0,0 +1,33 @@
// @ts-nocheck
import db from "$lib/server/db/db.js";
import { json } from "@sveltejs/kit";
import { BeginningOfDay } from "$lib/server/tool.js";
export async function POST({ request }) {
const payload = await request.json();
let start = payload.startTs;
let end = payload.endTs;
let tag = payload.tag;
let localTz = payload.localTz;
let incidents = await db.getIncidentsByMonitorTag(tag, start, end);
let resp = {};
for (let i = 0; i < incidents.length; i++) {
let incident = incidents[i];
let timestamp = incident.start_date_time;
let startOfThatDay = BeginningOfDay({
date: new Date(timestamp * 1000),
timeZone: localTz
});
if (!resp[startOfThatDay]) {
resp[startOfThatDay] = {
ids: [],
monitor_impact: incident.monitor_impact
};
}
resp[startOfThatDay].ids.push(incident.id);
if (resp[startOfThatDay].monitor_impact !== "DOWN") {
resp[startOfThatDay].monitor_impact = incident.monitor_impact;
}
}
return json(resp);
}
@@ -1,36 +0,0 @@
// @ts-nocheck
import { Mapper, GetOpenIncidents, FilterAndInsertMonitorInIncident } from "$lib/server/github.js";
import { FetchData } from "$lib/server/page";
import { GetMonitors } from "$lib/server/controllers/controller.js";
export async function load({ params, route, url, parent }) {
let monitors = await GetMonitors({ status: "ACTIVE" });
const parentData = await parent();
const siteData = parentData.site;
const github = siteData.github;
const monitorsActive = [];
for (let i = 0; i < monitors.length; i++) {
if (monitors[i].hidden !== undefined && monitors[i].hidden === true) {
continue;
}
if (
monitors[i].category_name === undefined ||
monitors[i].category_name !== params.category
) {
continue;
}
delete monitors[i].api;
delete monitors[i].default_status;
let data = await FetchData(siteData, monitors[i], parentData.localTz);
monitors[i].pageData = data;
monitors[i].embed = false;
monitorsActive.push(monitors[i]);
}
let openIncidents = await GetOpenIncidents();
let openIncidentsReduced = openIncidents.map(Mapper);
return {
monitors: monitorsActive,
openIncidents: FilterAndInsertMonitorInIncident(openIncidentsReduced, monitorsActive)
};
}
@@ -1,190 +0,0 @@
<script>
import Monitor from "$lib/components/monitor.svelte";
import * as Card from "$lib/components/ui/card";
import Incident from "$lib/components/incident.svelte";
import { Separator } from "$lib/components/ui/separator";
import { Button } from "$lib/components/ui/button";
import { ArrowRight, X } from "lucide-svelte";
import { Badge } from "$lib/components/ui/badge";
import { page } from "$app/stores";
import { l } from "$lib/i18n/client";
import ShareMenu from "$lib/components/shareMenu.svelte";
import { hotKeyAction, slide, clickOutsideAction } from "svelte-legos";
export let data;
let shareMenusToggle = false;
let activeMonitor = null;
function showShareMenu(e) {
shareMenusToggle = true;
activeMonitor = e.detail.monitor;
}
let category = data.site.categories.find((c) => c.name === $page.params.category);
let hasActiveIncidents = data.openIncidents.length > 0;
</script>
<svelte:head>
{#if category}
<title>{category.name} {l(data.lang, "root.category")}</title>
{#if category.description}
<meta name="description" content={category.description} />
{/if}
{/if}
</svelte:head>
<div class="mt-32"></div>
{#if category}
<section class="mx-auto mb-8 flex w-full max-w-4xl flex-1 flex-col items-start justify-center">
<div class="mx-auto max-w-screen-xl px-4 lg:flex lg:items-center">
<div class="blurry-bg mx-auto max-w-3xl text-center">
{#if category.name}
<h1
class="bg-gradient-to-r from-green-300 via-blue-500 to-purple-600 bg-clip-text text-5xl font-extrabold leading-snug text-transparent"
>
{category.name}
</h1>
{/if}
{#if category.description}
<p class="mx-auto mt-4 max-w-xl sm:text-xl">{category.description}</p>
{/if}
</div>
</div>
</section>
{/if}
{#if hasActiveIncidents}
<section
class="mx-auto mb-4 flex w-full flex-1 flex-col items-start justify-center bg-transparent backdrop-blur-[2px] md:w-[655px]"
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">
{l(data.lang, "root.ongoing_incidents")}
</Badge>
</div>
</div>
</section>
<section
class="mx-auto mb-8 flex w-full flex-1 flex-col items-start justify-center backdrop-blur-[2px] md:w-[655px]"
id=""
>
{#each data.openIncidents as incident, i}
<Incident
{incident}
state="close"
variant="title+body+comments+monitor"
monitor={incident.monitor}
lang={data.lang}
/>
{/each}
</section>
{/if}
{#if data.monitors.length > 0}
<section
class="mx-auto mb-2 flex w-full flex-1 flex-col items-start justify-center bg-transparent md:w-[655px]"
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 class="border-0 pl-0" variant="outline">
{l(data.lang, "root.availability_per_component")}
</Badge>
</div>
<div class="col-span-2 text-center md:col-span-1 md:text-right">
<Badge variant="outline" class="border-0 pr-0">
<span class="bg-api-up mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
></span>
<span class="mr-3">
{l(data.lang, "statuses.UP")}
</span>
<span
class="bg-api-degraded mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
></span>
<span class="mr-3">
{l(data.lang, "statuses.DEGRADED")}
</span>
<span
class="bg-api-down mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
></span>
<span class="mr-3">
{l(data.lang, "statuses.DOWN")}
</span>
</Badge>
</div>
</div>
</section>
<section
class="mx-auto mb-8 flex w-full flex-1 flex-col items-start justify-center backdrop-blur-[2px] md:w-[655px]"
>
<Card.Root class="w-full">
<Card.Content class="monitors-card p-0">
{#each data.monitors as monitor}
<Monitor
on:show_shareMenu={showShareMenu}
{monitor}
localTz={data.localTz}
lang={data.lang}
/>
{/each}
</Card.Content>
</Card.Root>
</section>
{:else}
<section
class="mx-auto mb-4 flex w-full max-w-[890px] flex-1 flex-col items-start justify-center bg-transparent"
id=""
>
<Card.Root class="mx-auto">
<Card.Content class="pt-4">
<h1
class="scroll-m-20 text-center text-2xl font-extrabold tracking-tight lg:text-2xl"
>
{l(data.lang, "root.no_monitors")}
</h1>
<p class="mt-3 text-center">
{l(data.lang, "root.read_doc_monitor")}
<a
href="https://kener.ing/docs#h1add-monitors"
target="_blank"
class="underline"
>
{l(data.lang, "root.here")}
</a>
</p>
</Card.Content>
</Card.Root>
</section>
{/if}
{#if shareMenusToggle}
<div
transition:slide={{ direction: "right" }}
use:hotKeyAction={{
code: "Escape",
cb: () => (shareMenusToggle = false)
}}
class="moldal-container fixed left-0 top-0 z-30 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm"
>
<div
use:clickOutsideAction
on:clickoutside={() => {
shareMenusToggle = false;
}}
class="absolute left-1/2 top-1/2 h-fit w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg md:w-[568px]"
>
<Button
variant="ghost"
on:click={() => {
shareMenusToggle = false;
}}
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
>
<X class="h-4 w-4 text-muted-foreground" />
</Button>
<div class="content">
<ShareMenu monitor={activeMonitor} lang={data.lang} />
</div>
</div>
</div>
{/if}
@@ -5,7 +5,7 @@ import { GetMonitors } from "$lib/server/controllers/controller.js";
export async function load({ params, route, url, parent }) {
let monitors = await GetMonitors({ status: "ACTIVE" });
const parentData = await parent();
const siteData = parentData.site;
const monitorsActive = [];
const query = url.searchParams;
const theme = query.get("theme");
@@ -17,7 +17,7 @@ export async function load({ params, route, url, parent }) {
delete monitors[i].api;
delete monitors[i].default_status;
monitors[i].embed = true;
let data = await FetchData(monitors[i], parentData.localTz);
let data = await FetchData(siteData, monitors[i], parentData.localTz);
monitors[i].pageData = data;
monitorsActive.push(monitors[i]);
}
@@ -1,26 +0,0 @@
// @ts-nocheck
import { GetIncidents, Mapper } from "$lib/server/github.js";
import { GetMonitors } from "$lib/server/controllers/controller.js";
// @ts-ignore
export async function load({ params, route, url, parent }) {
let monitors = await GetMonitors({ status: "ACTIVE" });
const siteData = await parent();
const github = siteData.site.github;
// @ts-ignore
const { description, name, tag, image } = monitors.find((monitor) => monitor.tag === params.id);
const allIncidents = await GetIncidents(tag, "all");
const gitHubActiveIssues = allIncidents.filter((issue) => {
return issue.state === "open";
});
const gitHubPastIssues = allIncidents.filter((issue) => {
return issue.state === "closed";
});
return {
issues: params.id,
githubConfig: github,
monitor: { description, name, image },
activeIncidents: await Promise.all(gitHubActiveIssues.map(Mapper, { github })),
pastIncidents: await Promise.all(gitHubPastIssues.map(Mapper, { github }))
};
}
@@ -1,101 +0,0 @@
<script>
import Incident from "$lib/components/incident.svelte";
import { Separator } from "$lib/components/ui/separator";
import { Badge } from "$lib/components/ui/badge";
import { l, summaryTime } from "$lib/i18n/client";
import moment from "moment";
export let data;
const formatDuration = (hours) => {
const duration = moment.duration(hours, "hours");
const days = Math.floor(duration.asDays());
const remainingHours = duration.hours();
if (days > 0) {
return `${days}d ${remainingHours}h`;
}
return `${hours}h`;
};
</script>
<svelte:head>
<title>
{data.monitor.name} - {l(data.lang, "root.incidents")}
</title>
</svelte:head>
<section class="mx-auto flex w-full max-w-4xl flex-1 flex-col items-start justify-center">
<div class="mx-auto max-w-screen-xl px-4 pb-16 pt-32 lg:flex lg:items-center">
<div class="blurry-bg mx-auto max-w-3xl text-center">
<h1
class="bg-gradient-to-r from-green-300 via-blue-500 to-purple-600 bg-clip-text text-5xl font-extrabold leading-snug text-transparent"
>
{data.monitor.name}
</h1>
<p class="mx-auto mt-4 max-w-xl sm:text-xl">
{@html data.monitor.description}
</p>
</div>
</div>
</section>
<section class="mx-auto mb-4 mt-8 flex w-full max-w-[840px] flex-1 flex-col">
<div class="container">
<h1 class="mb-4 text-2xl font-bold leading-none">
<Badge variant="outline" class="border-0 pl-0">
{l(data.lang, "root.active_incidents")}
</Badge>
</h1>
{#if data.activeIncidents.length > 0}
{#each data.activeIncidents as incident, i}
<Incident
{incident}
state={i == 0 ? "open" : "close"}
variant="title+body+comments"
monitor={data.monitor}
lang={data.lang}
/>
{/each}
{:else}
<div class="justify-left flex items-center">
<p class="text-base font-semibold">
{l(data.lang, "root.no_active_incident")}
</p>
</div>
{/if}
</div>
</section>
<Separator class="container mb-4 max-w-[780px]" />
<section class="mx-auto mb-4 mt-8 flex w-full max-w-[840px] flex-1 flex-col">
<div class="container">
<h1 class="mb-4 text-2xl font-bold leading-none">
<Badge variant="outline" class="border-0 pl-0">
{l(data.lang, "root.recent_incidents")} - {summaryTime(
data.lang,
`Last ${formatDuration(data.site.github.incidentSince)}`
)}
</Badge>
</h1>
{#if data.pastIncidents.length > 0}
{#each data.pastIncidents as incident}
<Incident
{incident}
state="close"
variant="title+body+comments"
monitor={data.monitor}
lang={data.lang}
/>
{/each}
{:else}
<div class="justify-left flex items-center">
<p class="text-base font-semibold">
{l(data.lang, "root.no_recent_incident")}
</p>
</div>
{/if}
</div>
</section>
@@ -1,24 +0,0 @@
// @ts-nocheck
// @ts-ignore
import { json } from "@sveltejs/kit";
import { GetCommentsForIssue } from "$lib/server/github.js";
import { marked } from "marked";
export async function GET({ params }) {
const incident_number = params.id;
let comments = await GetCommentsForIssue(incident_number);
comments = comments.map(
(
/** @type {{ body: string | import("markdown-it/lib/token")[]; created_at: any; updated_at: any; html_url: any; }} */ comment
) => {
const html = marked.parse(comment.body);
return {
body: html,
created_at: comment.created_at,
updated_at: comment.updated_at,
html_url: comment.html_url
};
}
);
return json(comments);
}
@@ -0,0 +1,9 @@
// @ts-nocheck
import { redirect } from "@sveltejs/kit";
import { base } from "$app/paths";
import moment from "moment";
export async function load({ parent, url }) {
throw redirect(302, base + "/incidents/" + moment().format("MMMM-YYYY"));
}
@@ -0,0 +1,54 @@
// @ts-nocheck
import {
GetMonitors,
GetIncidentsOpenHome,
GetIncidentsPage
} from "$lib/server/controllers/controller.js";
import { BeginningOfDay } from "$lib/server/tool.js";
import db from "$lib/server/db/db.js";
import moment from "moment";
export async function load({ parent, url, params }) {
const parentData = await parent();
let monitors = await GetMonitors({ status: "ACTIVE" });
const query = url.searchParams;
let todayStart = BeginningOfDay({ timeZone: parentData.localTz });
let tomorrowStart = todayStart + 86400;
let month = params.month;
if (!!!month) {
month = moment().format("MMMM-YYYY");
}
let monthStartTs = moment(month, "MMMM-YYYY").unix();
let monthEndTs = moment(month, "MMMM-YYYY").endOf("month").unix();
let incidents = await GetIncidentsPage(monthStartTs, monthEndTs);
incidents = incidents.map((incident) => {
let incidentMonitors = incident.monitors;
let monitorTags = incidentMonitors.map((monitor) => monitor.monitor_tag);
let xm = monitors.filter((monitor) => monitorTags.includes(monitor.tag));
incident.monitors = xm.map((monitor) => {
return {
tag: monitor.tag,
name: monitor.name,
description: monitor.description,
image: monitor.image,
impact_type: incidentMonitors.filter((m) => m.monitor_tag === monitor.tag)[0]
.monitor_impact
};
});
return incident;
});
return {
incidents: incidents,
nextMonthName: moment(month, "MMMM-YYYY").add(1, "months").format("MMMM-YYYY"),
prevMonthName: moment(month, "MMMM-YYYY").subtract(1, "months").format("MMMM-YYYY"),
thisMonthName: month
};
}
@@ -0,0 +1,147 @@
<script>
import moment from "moment";
import { Badge } from "$lib/components/ui/badge";
import Incident from "$lib/components/IncidentNew.svelte";
import { Button } from "$lib/components/ui/button";
import { ArrowRight, ArrowLeft, CalendarCheck2, ChevronLeft } from "lucide-svelte";
import { base } from "$app/paths";
import { goto } from "$app/navigation";
export let data;
let incidents = data.incidents;
let incidentSmartDates = {};
function tsToDate(mnt) {
return mnt.unix();
}
incidents.forEach((incident) => {
let startTime = incident.start_date_time;
let today = moment(startTime * 1000)
.startOf("day")
.unix();
let tomorrow = moment(startTime * 1000)
.add(1, "days")
.startOf("day")
.unix();
let dayAfterTomorrow = moment(startTime * 1000)
.add(2, "days")
.startOf("day")
.unix();
let yesterday = moment(startTime * 1000)
.subtract(1, "days")
.startOf("day")
.unix();
let dayBeforeYesterday = moment(startTime * 1000)
.subtract(2, "days")
.startOf("day")
.unix();
if (!incidentSmartDates[today]) {
incidentSmartDates[today] = [];
}
if (!incidentSmartDates[tomorrow]) {
incidentSmartDates[tomorrow] = [];
}
if (!incidentSmartDates[dayAfterTomorrow]) {
incidentSmartDates[dayAfterTomorrow] = [];
}
if (!incidentSmartDates[yesterday]) {
incidentSmartDates[yesterday] = [];
}
if (!incidentSmartDates[dayBeforeYesterday]) {
incidentSmartDates[dayBeforeYesterday] = [];
}
incidentSmartDates[today].push(incident);
});
//sort the incidentSmartDates ascending
let sortedIncidentSmartDates = Object.keys(incidentSmartDates).sort((a, b) => a - b);
</script>
<div class="mt-12"></div>
<section class="mx-auto my-2 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center">
<Button
variant="secondary"
class="bounce-left h-8 justify-start pl-1.5"
on:click={() => {
return window.history.back();
}}
>
<ChevronLeft class="arrow mr-1 h-5 w-5" /> Back
</Button>
</section>
<section class="mx-auto mb-8 flex w-full max-w-4xl flex-1 flex-col items-start justify-center">
<div
class="mesh mx-auto w-[655px] max-w-screen-xl rounded-md px-4 py-12 lg:flex lg:items-center"
>
<div class="blurry-bg mx-auto max-w-3xl text-center text-muted">
<h1 class=" text-5xl font-extrabold leading-tight">
{data.thisMonthName.replace("-", ", ")}
</h1>
<p class="mx-auto mt-4 max-w-xl font-medium sm:text-xl">Incidents</p>
</div>
</div>
<div
class="mx-auto mb-2 mt-4 flex w-full flex-1 flex-col items-start justify-center bg-transparent md:w-[655px]"
>
{#if sortedIncidentSmartDates.length == 0}
<div
class="mx-auto w-full rounded-md p-12 text-center"
style="background-image: linear-gradient( 135deg, #2AFADF 10%, #4C83FF 100%);"
>
<CalendarCheck2 class="mx-auto mb-4 h-12 w-12 text-green-700" />
<h1 class=" text-xl font-semibold leading-tight text-[rgba(0,0,0,0.44)]">
No Incidents on {data.thisMonthName.replace("-", ", ")}
</h1>
</div>
{/if}
{#each sortedIncidentSmartDates as date}
<div class="mb-4 grid w-full grid-cols-2 gap-x-4 rounded-md border bg-card">
<div class="text-md col-span-2 border-b p-2 px-4 font-medium">
{moment.unix(date).format("dddd, MMMM Do")}
</div>
{#if incidentSmartDates[date].length === 0}
<div class="col-span-2 p-2 px-4 text-sm font-medium text-muted-foreground">
No incidents
</div>
{/if}
{#each incidentSmartDates[date] as incident, index}
<div class="newincidents col-span-2">
<Incident {incident} lang={data.lang} index="incident-{index}" />
</div>
{/each}
</div>
{/each}
</div>
<div
class="mx-auto mb-2 mt-4 flex w-full flex-1 flex-col items-start justify-center bg-transparent md:w-[655px]"
>
<div class="flex w-full justify-between">
<Button
type="submit"
variant="secondary"
class="bounce-left"
on:click={() => {
window.location.href = `${base}/incidents/${data.prevMonthName}`;
}}
>
<ArrowLeft class="arrow mr-2 h-4 w-4" />
{data.prevMonthName.replace("-", ", ")}
</Button>
<Button
variant="secondary"
class="bounce-right"
on:click={() => {
window.location.href = `${base}/incidents/${data.nextMonthName}`;
}}
>
{data.nextMonthName.replace("-", ", ")}
<ArrowRight class="arrow ml-2 h-4 w-4" />
</Button>
</div>
</div>
</section>
@@ -1,31 +0,0 @@
// @ts-nocheck
import { Mapper, GetOpenIncidents, FilterAndInsertMonitorInIncident } from "$lib/server/github.js";
import { FetchData } from "$lib/server/page";
import { GetMonitors } from "$lib/server/controllers/controller.js";
export async function load({ params, route, url, parent }) {
let monitors = await GetMonitors({ status: "ACTIVE" });
const parentData = await parent();
const siteData = parentData.site;
const github = siteData.github;
const monitorsActive = [];
for (let i = 0; i < monitors.length; i++) {
//only return monitors that have category as home or category is not present
if (monitors[i].tag !== params.tag) {
continue;
}
delete monitors[i].api;
delete monitors[i].default_status;
let data = await FetchData(siteData, monitors[i], parentData.localTz);
monitors[i].pageData = data;
monitors[i].embed = false;
monitorsActive.push(monitors[i]);
}
let openIncidents = await GetOpenIncidents();
let openIncidentsReduced = openIncidents.map(Mapper);
return {
monitors: monitorsActive,
openIncidents: FilterAndInsertMonitorInIncident(openIncidentsReduced, monitorsActive)
};
}
@@ -1,163 +0,0 @@
<script>
import Monitor from "$lib/components/monitor.svelte";
import * as Card from "$lib/components/ui/card";
import Incident from "$lib/components/incident.svelte";
import { Separator } from "$lib/components/ui/separator";
import { Badge } from "$lib/components/ui/badge";
import { page } from "$app/stores";
import { l } from "$lib/i18n/client";
import ShareMenu from "$lib/components/shareMenu.svelte";
import { hotKeyAction, slide, clickOutsideAction } from "svelte-legos";
import { Button } from "$lib/components/ui/button";
import { ArrowRight, X } from "lucide-svelte";
export let data;
let shareMenusToggle = false;
let activeMonitor = null;
function showShareMenu(e) {
shareMenusToggle = true;
activeMonitor = e.detail.monitor;
}
let hasActiveIncidents = data.openIncidents.length > 0;
</script>
<svelte:head>
{#if data.monitors.length > 0}
<title>{data.monitors[0].name} Monitor Page</title>
{/if}
</svelte:head>
<div class="mt-32"></div>
{#if hasActiveIncidents}
<section
class="mx-auto mb-4 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">
{l(data.lang, "root.ongoing_incidents")}
</Badge>
</div>
</div>
</section>
<section
class="mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center"
id=""
>
{#each data.openIncidents as incident, i}
<Incident
{incident}
state="close"
variant="title+body+comments+monitor"
monitor={incident.monitor}
lang={data.lang}
/>
{/each}
</section>
{/if}
{#if data.monitors.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-1 justify-end gap-4">
<div class="col-span-1 text-center md:col-span-1 md:text-right">
<Badge variant="outline" class="border-0 pr-0">
<span class="bg-api-up mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
></span>
<span class="mr-3">
{l(data.lang, "statuses.UP")}
</span>
<span
class="bg-api-degraded mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
></span>
<span class="mr-3">
{l(data.lang, "statuses.DEGRADED")}
</span>
<span
class="bg-api-down mr-1 inline-flex h-[8px] w-[8px] rounded-full opacity-75"
></span>
<span class="mr-3">
{l(data.lang, "statuses.DOWN")}
</span>
</Badge>
</div>
</div>
</section>
<section
class="mx-auto mb-8 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center backdrop-blur-[2px]"
>
<Card.Root class="w-full">
<Card.Content class="monitors-card p-0">
{#each data.monitors as monitor}
<Monitor
on:show_shareMenu={showShareMenu}
{monitor}
localTz={data.localTz}
lang={data.lang}
/>
{/each}
</Card.Content>
</Card.Root>
</section>
{:else}
<section
class="mx-auto mb-4 flex w-full max-w-[655px] flex-1 flex-col items-start justify-center bg-transparent"
id=""
>
<Card.Root class="mx-auto">
<Card.Content class="pt-4">
<h1
class="scroll-m-20 text-center text-2xl font-extrabold tracking-tight lg:text-2xl"
>
{l(data.lang, "root.no_monitors")}
</h1>
<p class="mt-3 text-center">
{l(data.lang, "root.read_doc_monitor")}
<a
href="https://kener.ing/docs#h1add-monitors"
target="_blank"
class="underline"
>
{l(data.lang, "root.here")}
</a>
</p>
</Card.Content>
</Card.Root>
</section>
{/if}
{#if shareMenusToggle}
<div
transition:slide={{ direction: "right" }}
use:hotKeyAction={{
code: "Escape",
cb: () => (shareMenusToggle = false)
}}
class="moldal-container fixed left-0 top-0 z-30 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm"
>
<div
use:clickOutsideAction
on:clickoutside={() => {
shareMenusToggle = false;
}}
class="absolute left-1/2 top-1/2 h-fit w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg md:w-[568px]"
>
<Button
variant="ghost"
on:click={() => {
shareMenusToggle = false;
}}
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
>
<X class="h-4 w-4 text-muted-foreground" />
</Button>
<div class="content">
<ShareMenu monitor={activeMonitor} lang={data.lang} />
</div>
</div>
</div>
{/if}
+306 -249
View File
@@ -4,8 +4,11 @@
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { Button } from "$lib/components/ui/button";
import * as Alert from "$lib/components/ui/alert";
import moment from "moment";
import { DateInput } from "date-picker-svelte";
import { clickOutsideAction, slide } from "svelte-legos";
import { Tooltip } from "bits-ui";
import {
Plus,
@@ -14,7 +17,6 @@
Bell,
Loader,
ChevronLeft,
ChevronRight,
Info,
Hammer,
CalendarCheck,
@@ -22,6 +24,7 @@
Siren,
PenLine,
MessageSquarePlus,
ChevronRight,
Trash
} from "lucide-svelte";
import * as Select from "$lib/components/ui/select";
@@ -99,7 +102,8 @@
startDatetime: null,
start_date_time: null,
status: "OPEN",
state: "INVESTIGATING"
state: "INVESTIGATING",
firstComment: ""
};
}
@@ -145,8 +149,17 @@
if (resp.error) {
invalidFormMessage = resp.error;
} else {
await fetchData();
if (!!!newIncident.id) {
newComment.comment = newIncident.firstComment;
newComment.id = 0;
newComment.state = newIncident.state;
newComment.commented_at = newIncident.startDatetime;
await addNewComment(resp.incident_id);
}
showModal = false;
fetchData();
}
} catch (error) {
invalidFormMessage = "Error while saving data";
@@ -249,7 +262,6 @@
function showComments(i) {
currentIncident = i;
showCommentModal = true;
comments = [];
newComment.state = i.state;
newComment.id = 0;
@@ -258,7 +270,7 @@
fetchComments();
}
async function addNewComment() {
async function addNewComment(i) {
addCommentError = "";
if (newComment.comment.trim().length == 0 || loadingComments) {
return;
@@ -280,7 +292,7 @@
body: JSON.stringify({
action: newComment.id == 0 ? "addComment" : "updateComment",
data: {
incident_id: currentIncident.id,
incident_id: !!i ? i : currentIncident.id,
comment: newComment.comment,
comment_id: newComment.id,
state: newComment.state,
@@ -296,6 +308,7 @@
newComment.comment = "";
newComment.id = 0;
newComment.commented_at = null;
newIncident.state = newComment.state;
await fetchData();
}
} catch (error) {
@@ -338,6 +351,8 @@
newIncident.endDatetime = new Date(Number(i.end_date_time) * 1000);
}
showModal = true;
showComments(i);
}
function setCommentState(s) {
newComment.state = s;
@@ -579,24 +594,15 @@
<td class="whitespace-nowrap px-6 py-4 text-xs font-semibold">
<div class="flex gap-x-1.5">
<Button
variant="ghost"
size="icon"
class="h-6 w-6 p-1"
variant="secondary"
class="h-8 text-xs"
on:click={(e) => {
openIncidentSettings(incident);
}}
>
<Settings class="inline h-5 w-5" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-6 w-6 p-1"
on:click={(e) => {
showComments(incident);
}}
>
<MessageSquarePlus class="inline h-5 w-5" />
Update <MessageSquarePlus
class="ml-2 inline h-4 w-4"
/>
</Button>
</div>
</td>
@@ -611,252 +617,303 @@
</div>
{#if showModal}
<div class="fixed left-0 top-0 z-50 h-screen w-screen bg-card bg-opacity-20 backdrop-blur-sm">
<div class="absolute right-0 top-0 h-screen w-[800px] bg-background shadow-xl">
<div class="absolute top-0 flex h-12 w-full justify-between gap-2 border-b p-3">
{#if newIncident.id}
<h2 class="text-lg font-medium">Edit Incident</h2>
{:else}
<h2 class="text-lg font-medium">Add Incident</h2>
{/if}
</div>
<div class="mt-12 w-full overflow-y-auto p-3" style="height: calc(100vh - 7rem);">
<div class="flex flex-row gap-4">
<div class="w-full">
<Label class="text-sm">
Incident Title
<span class="text-red-500">*</span>
</Label>
<Input
class="mt-2"
bind:value={newIncident.title}
placeholder="Outage in all servers"
/>
</div>
</div>
<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="text-red-500">*</span>
</Label>
<DateInput
bind:value={newIncident.startDatetime}
id="start_date_time"
timePrecision="minute"
class=" mt-2 text-sm"
/>
</div>
</div>
<div class="mt-4 flex gap-4">
<p class="font-medium">
<label>
<input
on:change={(e) => {
newIncident.status =
e.target.checked === true ? "CLOSED" : "OPEN";
}}
type="checkbox"
checked={newIncident.status === "CLOSED"}
/>
Close/Delete this incident
</label>
</p>
</div>
</div>
<div class="absolute bottom-0 grid h-16 w-full grid-cols-6 gap-2 border-t p-3">
<div class="col-span-1">
<Button
variant="ghost"
class="col-span-1 w-full"
on:click={(e) => {
showModal = false;
}}>Cancel</Button
>
</div>
<div class="col-span-4 py-2.5">
<p class="text-right text-xs font-medium text-red-500">
{invalidFormMessage}
</p>
</div>
<div class="col-span-1">
<Button
class="col-span-1 w-full"
on:click={createIncident}
disabled={formStateCreate === "loading"}
>
Save
{#if formStateCreate === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</div>
</div>
</div>
{/if}
{#if showCommentModal}
<div
class="moldal-container fixed left-0 top-0 z-50 h-screen w-full bg-card bg-opacity-30 backdrop-blur-sm"
>
<div
class="absolute left-1/2 top-1/2 h-fit w-full max-w-xl -translate-x-1/2 -translate-y-1/2 rounded-md border bg-background shadow-lg backdrop-blur-lg"
transition:slide={{ direction: "right", duration: 200 }}
use:clickOutsideAction
on:clickoutside={(e) => {
showModal = false;
}}
class="absolute right-0 top-0 h-screen w-[800px] border-l bg-background p-4 shadow-xl"
>
<Button
variant="ghost"
on:click={() => {
showCommentModal = false;
}}
class="absolute right-2 top-2 z-40 h-6 w-6 rounded-full border bg-background p-1"
>
<X class="h-4 w-4 text-muted-foreground" />
</Button>
<div class="content px-4 py-4">
<h2 class="text-lg font-semibold">
Add Updates for <span class="underline">{currentIncident.title}</span>
</h2>
<form class="mt-4" on:submit|preventDefault={addNewComment}>
<div
class="bg-hover state-{newComment.state} mt-2 grid grid-cols-4 overflow-hidden rounded-md border text-xs font-medium"
>
<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 class="mt-0 w-full overflow-y-auto p-3" style="height: 100vh;">
<div class="rounded-md border p-4">
<div>
{#if newIncident.id}
<h2 class="text-lg font-medium">Edit Incident</h2>
{:else}
<h2 class="text-lg font-medium">Add New Incident</h2>
{/if}
</div>
<div class="mt-4 flex w-full gap-4">
<div class="text-sm font-medium leading-7">Time Stamp</div>
<DateInput
bind:value={newComment.commented_at}
id="newcomment_commented_at"
timePrecision="minute"
min={new Date(currentIncident.start_date_time * 1000)}
class="w-[200px] text-sm"
/>
</div>
<div class="mt-4">
<textarea
bind:value={newComment.comment}
class="h-24 w-full rounded-sm border p-2 text-sm"
placeholder="There is an outage in all server. Our best minds are on it. We will update you soon."
></textarea>
</div>
<div class="flex justify-between">
<div>
{#if loadingComments}
<Loader class="mt-2 inline h-6 w-6 animate-spin" />
{/if}
</div>
<div>
{#if addCommentError}
<p class="mt-4 text-xs text-red-500">{addCommentError}</p>
{/if}
</div>
<div class="flex gap-x-2">
{#if !!newComment.id}
<Button
type="reset"
variant="ghost"
on:click={() =>
(newComment = {
comment: "",
id: 0,
state: currentIncident.state
})}
class="mt-2 h-8 p-1 px-2 text-xs"
<div class="mt-4 flex flex-row gap-4">
<div class="w-full">
<Label class="text-sm">
Incident Title
<span class="text-red-500">*</span>
<span
class="float-right mt-2 text-xs font-semibold badge-{newIncident.state}"
>
Cancel
</Button>
{/if}
{newIncident.state}
</span>
</Label>
<Input
class="mt-2"
bind:value={newIncident.title}
placeholder="Outage in all servers"
/>
</div>
</div>
{#if !!!newIncident.id}
<div class="mt-4 flex flex-row gap-4">
<div class="w-full">
<Label class="text-sm">
Incident Summary
<span class="text-red-500">*</span>
</Label>
<Input
class="mt-2"
bind:value={newIncident.firstComment}
placeholder="We are facing degraded service in all servers"
/>
</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="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>
</div>
<div class="mt-4 grid h-16 w-full grid-cols-6 gap-2 border-t pt-4">
<div class="col-span-4 py-2.5">
<p class="text-right text-xs font-medium text-red-500">
{invalidFormMessage}
</p>
</div>
<div class="col-span-2 justify-end">
<Button
type="submit"
disabled={newComment.comment.trim().length == 0 || loadingComments}
class="mt-2 h-8 p-1 px-2.5 text-xs"
class="float-right"
on:click={createIncident}
disabled={formStateCreate === "loading" ||
newIncident.title.trim().length == 0 ||
!!!newIncident.startDatetime ||
(!!!newIncident.id &&
newIncident.firstComment.trim().length == 0)}
>
{#if !!newComment.id}
Update Comment
{:else}
Add Comment
Save
{#if formStateCreate === "loading"}
<Loader class="ml-2 inline h-4 w-4 animate-spin" />
{/if}
</Button>
</div>
</div>
</form>
<div class="mt-4 max-h-[400px] overflow-y-auto border-t">
{#each comments as comment}
<div class="flex items-center justify-between gap-2 border-b py-2">
</div>
{#if newIncident.id}
<div class="mt-4 rounded-md border px-4 py-4">
<h2 class="text-lg font-semibold">
Add Updates for <span class="underline">{currentIncident.title}</span>
</h2>
<form
class="mt-4"
on:submit|preventDefault={(e) => {
addNewComment();
}}
>
<div
class="w-full rounded px-2 py-2 {newComment.id == comment.id
? 'bg-input'
: ''}"
class="bg-hover state-{newComment.state} mt-2 grid grid-cols-4 overflow-hidden rounded-md border text-xs font-medium"
>
<p class="mb-2 text-xs font-medium">{comment.comment}</p>
<div class="flex w-full justify-between gap-x-2">
<div class="text-xs font-semibold text-muted-foreground">
<span class="badge-{comment.state}">
{comment.state}
</span>
{moment(comment.commented_at * 1000).format(
"YYYY-MM-DD HH:mm:ss"
)}
</div>
<div class="flex gap-x-2 pr-2">
<Button
variant="ghost"
size="icon"
class="h-5 w-5 p-1"
on:click={() => {
setCommentEdit(comment);
}}
>
<PenLine class="inline h-4 w-4 text-muted-foreground" />
</Button>
<Button
variant="ghost"
size="icon"
class="h-5 w-5 p-1"
on:click={() => deleteComment(comment)}
>
<Trash class="inline h-4 w-4 text-muted-foreground" />
</Button>
</div>
<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="mt-4 flex w-full gap-4">
<div class="text-sm font-medium leading-7">Time Stamp</div>
<DateInput
bind:value={newComment.commented_at}
id="newcomment_commented_at"
timePrecision="minute"
min={new Date(currentIncident.start_date_time * 1000)}
class="w-[200px] text-sm"
/>
</div>
<div class="mt-4">
<textarea
bind:value={newComment.comment}
class="h-24 w-full rounded-sm border p-2 text-sm"
placeholder="There is an outage in all server. Our best minds are on it. We will update you soon."
></textarea>
</div>
<div class="flex justify-between">
<div>
{#if loadingComments}
<Loader class="mt-2 inline h-6 w-6 animate-spin" />
{/if}
</div>
<div>
{#if addCommentError}
<p class="mt-4 text-xs text-red-500">{addCommentError}</p>
{/if}
</div>
<div class="flex gap-x-2">
{#if !!newComment.id}
<Button
type="reset"
variant="ghost"
on:click={() =>
(newComment = {
comment: "",
id: 0,
state: currentIncident.state
})}
class="mt-2"
>
Cancel Update
</Button>
{/if}
<Button
type="submit"
disabled={newComment.comment.trim().length == 0 ||
loadingComments}
class="mt-2"
>
{#if !!newComment.id}
Update Comment
{:else}
Add Comment
{/if}
</Button>
</div>
</div>
</form>
<div class="mt-4 border-t">
{#each comments as comment}
<div class="flex items-center justify-between gap-2 border-b py-2">
<div
class="w-full rounded px-2 py-2 {newComment.id == comment.id
? 'bg-input'
: ''}"
>
<p class="mb-2 text-xs font-medium">{comment.comment}</p>
<div class="flex w-full justify-between gap-x-2">
<div
class="text-xs font-semibold text-muted-foreground"
>
<span class="badge-{comment.state}">
{comment.state}
</span>
{moment(comment.commented_at * 1000).format(
"YYYY-MM-DD HH:mm:ss"
)}
</div>
<div class="flex gap-x-2 pr-2">
<Button
variant="ghost"
size="icon"
class="h-5 w-5 p-1"
on:click={() => {
setCommentEdit(comment);
}}
>
<PenLine
class="inline h-4 w-4 text-muted-foreground"
/>
</Button>
<Button
variant="ghost"
size="icon"
class="h-5 w-5 p-1"
on:click={() => deleteComment(comment)}
>
<Trash
class="inline h-4 w-4 text-muted-foreground"
/>
</Button>
</div>
</div>
</div>
</div>
{/each}
</div>
{/each}
</div>
</div>
{#if newIncident.status == "OPEN"}
<Alert.Root variant="destructive" class="relative mt-4">
<Alert.Title>Delete Incident</Alert.Title>
<Alert.Description>
You can delete the incident. This will stop showing up in the status
page.
</Alert.Description>
<Button
variant="destructive"
class="absolute right-4 top-4"
on:click={() => {
newIncident.status = "CLOSED";
createIncident();
}}
>
Delete
</Button>
</Alert.Root>
{:else}
<Alert.Root class="relative mt-4">
<Alert.Title>Restore Incident</Alert.Title>
<Alert.Description>
Restore the incident. This will start showing up in the status page.
</Alert.Description>
<Button
class="absolute right-4 top-4"
on:click={() => {
newIncident.status = "OPEN";
createIncident();
}}
>
Restore
</Button>
</Alert.Root>
{/if}
{/if}
</div>
<Button
variant="outline"
size="icon"
class="absolute right-[785px] top-7 h-8 w-8 rounded-md"
on:click={(e) => {
showModal = false;
}}
>
<ChevronRight class="h-6 w-6 " />
</Button>
</div>
</div>
{/if}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB