mirror of
https://github.com/rajnandan1/kener.git
synced 2026-04-30 23:41:33 -05:00
feat: incidents from github to sqlite
This commit is contained in:
+4
-4
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(`
|
||||
|
||||
@@ -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
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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}
|
||||
@@ -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 |
Reference in New Issue
Block a user