mirror of
https://github.com/BluemediaDev/fancy-gatus.git
synced 2025-07-30 20:52:32 +02:00
Use SvelteKit 5 and daisyUI 5
This commit is contained in:
parent
be983a0b4e
commit
3b15ec5ea3
45 changed files with 5215 additions and 9859 deletions
12
src/lib/components/Footer.svelte
Normal file
12
src/lib/components/Footer.svelte
Normal file
|
@ -0,0 +1,12 @@
|
|||
<footer class="footer sm:footer-horizontal footer-center text-base-content p-4 text-base">
|
||||
<aside>
|
||||
<p>
|
||||
Powered by
|
||||
<a class="underline" target="_blank" href="https://github.com/TwiN/gatus">Gatus</a>
|
||||
and
|
||||
<a class="underline" target="_blank" href="https://github.com/BluemediaDev/fancy-gatus"
|
||||
>Fancy Gatus</a
|
||||
>
|
||||
</p>
|
||||
</aside>
|
||||
</footer>
|
21
src/lib/components/Header.svelte
Normal file
21
src/lib/components/Header.svelte
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { persistentSettings } from '$lib/persistent_store'
|
||||
|
||||
let { title = 'Infrastructure Status' } = $props()
|
||||
</script>
|
||||
|
||||
<div class="mb-10 flex items-center justify-center md:justify-start">
|
||||
<img src="/img/logo.png" alt="Logo" class="w-14 md:w-16 h-auto" />
|
||||
<h2 class="ml-4 text-xl md:text-2xl font-semibold">{title}</h2>
|
||||
<span class="hidden md:block grow"></span>
|
||||
<label class="invisible md:visible md:text-3xl swap swap-rotate">
|
||||
<!-- this hidden checkbox controls the state -->
|
||||
<input type="checkbox" bind:checked={$persistentSettings.darkmode} />
|
||||
|
||||
<!-- sun icon -->
|
||||
<i class="bi bi-sun swap-off"></i>
|
||||
|
||||
<!-- moon icon -->
|
||||
<i class="bi bi-moon swap-on"></i>
|
||||
</label>
|
||||
</div>
|
228
src/lib/components/Loader.svelte
Normal file
228
src/lib/components/Loader.svelte
Normal file
|
@ -0,0 +1,228 @@
|
|||
<div class="loader-wrapper w-full flex flex-column justify-center items-center">
|
||||
<div class="loader">
|
||||
<svg viewBox="0 0 80 80">
|
||||
<circle id="test" cx="40" cy="40" r="32"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="loader triangle">
|
||||
<svg viewBox="0 0 86 80">
|
||||
<polygon points="43 8 79 72 7 72"></polygon>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="loader">
|
||||
<svg viewBox="0 0 80 80">
|
||||
<rect x="8" y="8" width="64" height="64"></rect>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.loader-wrapper {
|
||||
height: calc(100vh - (var(--spacing) * 5 * 2));
|
||||
}
|
||||
.loader {
|
||||
--duration: 3s;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
position: relative;
|
||||
}
|
||||
.loader:before {
|
||||
content: '';
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
display: block;
|
||||
background: var(--color-primary);
|
||||
top: 37px;
|
||||
left: 19px;
|
||||
transform: translate(-18px, -18px);
|
||||
-webkit-animation: dotRect var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
|
||||
animation: dotRect var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
|
||||
}
|
||||
.loader svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.loader svg rect,
|
||||
.loader svg polygon,
|
||||
.loader svg circle {
|
||||
fill: none;
|
||||
stroke: var(--color-base-content);
|
||||
stroke-width: 10px;
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
.loader svg polygon {
|
||||
stroke-dasharray: 145 76 145 76;
|
||||
stroke-dashoffset: 0;
|
||||
-webkit-animation: pathTriangle var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
|
||||
animation: pathTriangle var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
|
||||
}
|
||||
.loader svg rect {
|
||||
stroke-dasharray: 192 64 192 64;
|
||||
stroke-dashoffset: 0;
|
||||
-webkit-animation: pathRect 3s cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
|
||||
animation: pathRect 3s cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
|
||||
}
|
||||
.loader svg circle {
|
||||
stroke-dasharray: 150 50 150 50;
|
||||
stroke-dashoffset: 75;
|
||||
-webkit-animation: pathCircle var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
|
||||
animation: pathCircle var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
|
||||
}
|
||||
.loader.triangle {
|
||||
width: 48px;
|
||||
}
|
||||
.loader.triangle:before {
|
||||
left: 21px;
|
||||
transform: translate(-10px, -18px);
|
||||
-webkit-animation: dotTriangle var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
|
||||
animation: dotTriangle var(--duration) cubic-bezier(0.785, 0.135, 0.15, 0.86) infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes pathTriangle {
|
||||
33% {
|
||||
stroke-dashoffset: 74;
|
||||
}
|
||||
66% {
|
||||
stroke-dashoffset: 147;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 221;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pathTriangle {
|
||||
33% {
|
||||
stroke-dashoffset: 74;
|
||||
}
|
||||
66% {
|
||||
stroke-dashoffset: 147;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 221;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes dotTriangle {
|
||||
33% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
66% {
|
||||
transform: translate(10px, -18px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-10px, -18px);
|
||||
}
|
||||
}
|
||||
@keyframes dotTriangle {
|
||||
33% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
66% {
|
||||
transform: translate(10px, -18px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-10px, -18px);
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes pathRect {
|
||||
25% {
|
||||
stroke-dashoffset: 64;
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: 128;
|
||||
}
|
||||
75% {
|
||||
stroke-dashoffset: 192;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 256;
|
||||
}
|
||||
}
|
||||
@keyframes pathRect {
|
||||
25% {
|
||||
stroke-dashoffset: 64;
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: 128;
|
||||
}
|
||||
75% {
|
||||
stroke-dashoffset: 192;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 256;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes dotRect {
|
||||
25% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
50% {
|
||||
transform: translate(18px, -18px);
|
||||
}
|
||||
75% {
|
||||
transform: translate(0, -36px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-18px, -18px);
|
||||
}
|
||||
}
|
||||
@keyframes dotRect {
|
||||
25% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
50% {
|
||||
transform: translate(18px, -18px);
|
||||
}
|
||||
75% {
|
||||
transform: translate(0, -36px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-18px, -18px);
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes pathCircle {
|
||||
25% {
|
||||
stroke-dashoffset: 125;
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: 175;
|
||||
}
|
||||
75% {
|
||||
stroke-dashoffset: 225;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 275;
|
||||
}
|
||||
}
|
||||
@keyframes pathCircle {
|
||||
25% {
|
||||
stroke-dashoffset: 125;
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: 175;
|
||||
}
|
||||
75% {
|
||||
stroke-dashoffset: 225;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 275;
|
||||
}
|
||||
}
|
||||
.loader {
|
||||
display: inline-block;
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
.loader {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
32
src/lib/components/Notice.svelte
Normal file
32
src/lib/components/Notice.svelte
Normal file
|
@ -0,0 +1,32 @@
|
|||
<script lang="ts">
|
||||
import type { Notice } from '$lib/types/config'
|
||||
|
||||
let props: {
|
||||
notice: Notice
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={[
|
||||
'alert',
|
||||
'w-full',
|
||||
'mb-10',
|
||||
'p-0',
|
||||
'shadow-sm',
|
||||
props.notice.type === undefined ? 'bg-base-100' : '',
|
||||
props.notice.type === 'success' ? 'alert-success' : '',
|
||||
props.notice.type === 'warning' ? 'alert-warning' : '',
|
||||
props.notice.type === 'error' ? 'alert-error' : '',
|
||||
]}
|
||||
>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{props.notice.title}</h2>
|
||||
<p>{props.notice.content}</p>
|
||||
<div class="mt-2">
|
||||
<p><span class="font-bold">Created at:</span> {props.notice.createdAt}</p>
|
||||
{#if props.notice.updatedAt}
|
||||
<p><span class="font-bold">Last updated:</span> {props.notice.updatedAt}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
44
src/lib/components/OverallStatus.svelte
Normal file
44
src/lib/components/OverallStatus.svelte
Normal file
|
@ -0,0 +1,44 @@
|
|||
<script lang="ts">
|
||||
import type { Status } from '$lib/types/api'
|
||||
|
||||
let props: {
|
||||
failedStatuses: Status[]
|
||||
} = $props()
|
||||
|
||||
let overallStatusText = $derived.by(() => {
|
||||
if (props.failedStatuses.length === 0) {
|
||||
return 'All services healthy.'
|
||||
} else if (props.failedStatuses.length === 2) {
|
||||
return 'Two services are experiencing issues.'
|
||||
} else if (props.failedStatuses.length === 3) {
|
||||
return 'Three services are experiencing issues.'
|
||||
} else if (props.failedStatuses.length > 3) {
|
||||
return 'Multiple services are experiencing issues.'
|
||||
} else {
|
||||
return 'One service is experiencing issues.'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="card w-full mt-5 mb-10 bg-base-100 shadow-sm">
|
||||
<div class="card-body flex flex-row items-center">
|
||||
<div class="inline-grid *:[grid-area:1/1]">
|
||||
<div
|
||||
class={[
|
||||
'status',
|
||||
'status-lg',
|
||||
'animate-ping',
|
||||
props.failedStatuses.length === 0 ? 'status-success' : 'status-warning',
|
||||
]}
|
||||
></div>
|
||||
<div
|
||||
class={[
|
||||
'status',
|
||||
'status-lg',
|
||||
props.failedStatuses.length === 0 ? 'status-success' : 'status-warning',
|
||||
]}
|
||||
></div>
|
||||
</div>
|
||||
<p class="ml-2 text-lg">{overallStatusText}</p>
|
||||
</div>
|
||||
</div>
|
47
src/lib/components/RefreshSettings.svelte
Normal file
47
src/lib/components/RefreshSettings.svelte
Normal file
|
@ -0,0 +1,47 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
|
||||
let {
|
||||
defaultRefreshInterval = 60,
|
||||
onRefresh,
|
||||
}: { defaultRefreshInterval?: number; onRefresh: () => void } = $props()
|
||||
|
||||
let selectedInterval: number = $state(defaultRefreshInterval)
|
||||
let refreshHandler: ReturnType<typeof setInterval> | undefined = $state(undefined)
|
||||
|
||||
function setRefreshInterval() {
|
||||
if (refreshHandler) clearInterval(refreshHandler)
|
||||
refreshHandler = setInterval(() => onRefresh(), selectedInterval * 1000)
|
||||
}
|
||||
|
||||
function handleRefreshIntervalChange() {
|
||||
onRefresh()
|
||||
setRefreshInterval()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
setRefreshInterval()
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (refreshHandler) clearInterval(refreshHandler)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="hidden xl:block fixed left-4 bottom-5 card bg-base-100 shadow-sm">
|
||||
<div class="card-body p-2 flex flex-row items-center">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
<select
|
||||
class="select select-xs"
|
||||
bind:value={selectedInterval}
|
||||
onchange={handleRefreshIntervalChange}
|
||||
>
|
||||
<option value={10}>10s</option>
|
||||
<option value={30}>30s</option>
|
||||
<option value={60}>1m</option>
|
||||
<option value={120}>2m</option>
|
||||
<option value={300}>5m</option>
|
||||
<option value={600}>10m</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
56
src/lib/components/Status.svelte
Normal file
56
src/lib/components/Status.svelte
Normal file
|
@ -0,0 +1,56 @@
|
|||
<script lang="ts">
|
||||
import type { Status } from '$lib/types/api'
|
||||
|
||||
let props: {
|
||||
status: Status
|
||||
} = $props()
|
||||
|
||||
let isFailed = $derived(!props.status.results[props.status.results.length - 1].success)
|
||||
</script>
|
||||
|
||||
<li
|
||||
class="list-row w-full flex flex-col lg:flex-row lg:items-center justify-center lg:justify-start"
|
||||
>
|
||||
<p>
|
||||
<span class="font-semibold">{props.status.name}</span>
|
||||
{#if props.status.results[props.status.results.length - 1].hostname}
|
||||
| {props.status.results[props.status.results.length - 1].hostname}
|
||||
{/if}
|
||||
</p>
|
||||
<span class="hidden lg:flex grow"></span>
|
||||
<div class="h-full w-fit mr-8 flex flex-row justify-between items-center gap-1">
|
||||
{#each props.status.results as result (result.timestamp)}
|
||||
<div class="h-fit w-fit lg:tooltip">
|
||||
<div class="tooltip-content hidden lg:flex flex-col items-center">
|
||||
<p>{new Date(result.timestamp).toLocaleString()}</p>
|
||||
{#if result.success}
|
||||
<p class="text-success">Operational</p>
|
||||
{:else}
|
||||
<p class="text-warning">Service Problem</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class={['w-2', 'h-5', 'rounded-sm', result.success ? 'bg-success' : 'bg-warning']}
|
||||
></div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="w-35 flex flex-row items-center">
|
||||
<div class="inline-grid *:[grid-area:1/1]">
|
||||
<div
|
||||
class={[
|
||||
'status',
|
||||
'status-md',
|
||||
'animate-ping',
|
||||
isFailed ? 'status-warning' : 'status-success',
|
||||
]}
|
||||
></div>
|
||||
<div class={['status', 'status-md', isFailed ? 'status-warning' : 'status-success']}></div>
|
||||
</div>
|
||||
{#if isFailed}
|
||||
<p class="ml-2 text-md text-warning">Service Problem</p>
|
||||
{:else}
|
||||
<p class="ml-2 text-md text-success">Operational</p>
|
||||
{/if}
|
||||
</div>
|
||||
</li>
|
35
src/lib/components/StatusGroup.svelte
Normal file
35
src/lib/components/StatusGroup.svelte
Normal file
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
import type { Status as StatusType } from '$lib/types/api'
|
||||
import Status from '$lib/components/Status.svelte'
|
||||
|
||||
let props: {
|
||||
title: string
|
||||
statuses: StatusType[]
|
||||
expandByDefault?: boolean
|
||||
} = $props()
|
||||
|
||||
let hasFailedStatuses = $derived.by(() => {
|
||||
return (
|
||||
props.statuses.filter((status) => {
|
||||
return !status.results[status.results.length - 1].success
|
||||
}).length > 0
|
||||
)
|
||||
})
|
||||
|
||||
let expanded = $derived(hasFailedStatuses || props.expandByDefault)
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<div tabindex="0" class="collapse collapse-plus mb-5 bg-base-100 border-base-300 border">
|
||||
<input type="checkbox" checked={expanded} />
|
||||
<div class="collapse-title font-semibold">
|
||||
{props.title}
|
||||
</div>
|
||||
<div class="collapse-content flex flex-col">
|
||||
<ul class="list w-full">
|
||||
{#each props.statuses as status (status.key)}
|
||||
<Status {status} />
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
Loading…
Add table
Add a link
Reference in a new issue