mirror of
https://github.com/rajnandan1/kener.git
synced 2026-06-23 04:10:22 +00:00
implement event display settings and notifications list component
implements #665
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import { resolve } from "$app/paths";
|
||||
import clientResolver from "$lib/client/resolver.js";
|
||||
import type { NotificationEvent } from "$lib/server/controllers/dashboardController.js";
|
||||
|
||||
interface NotificationsResponse {
|
||||
notifications?: NotificationEvent[];
|
||||
}
|
||||
|
||||
export async function requestNotifications(monitorTags: string[] = []): Promise<NotificationEvent[]> {
|
||||
const query = monitorTags.length > 0 ? `?tags=${encodeURIComponent(monitorTags.join(","))}` : "";
|
||||
const response = await fetch(clientResolver(resolve, "/dashboard-apis/notifications") + query);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch notifications");
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as NotificationsResponse;
|
||||
return payload.notifications || [];
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/state";
|
||||
import { requestNotifications } from "$lib/client/notifications-client.js";
|
||||
import clientResolver from "$lib/client/resolver.js";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import { formatDate, formatDuration } from "$lib/stores/datetime";
|
||||
import { t } from "$lib/stores/i18n";
|
||||
import type { NotificationEvent } from "$lib/server/controllers/dashboardController.js";
|
||||
import Calendar from "@lucide/svelte/icons/calendar-1";
|
||||
import { format } from "date-fns";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
interface Props {
|
||||
monitorTags?: string[];
|
||||
eventsPath?: string;
|
||||
notifications?: NotificationEvent[];
|
||||
loading?: boolean;
|
||||
fetchOnMount?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
monitorTags = [],
|
||||
eventsPath = "",
|
||||
notifications = $bindable<NotificationEvent[]>([]),
|
||||
loading = $bindable(false),
|
||||
fetchOnMount = true
|
||||
}: Props = $props();
|
||||
|
||||
const defaultEventsPath = $derived(`/events/${format(new Date(), "MMMM-yyyy")}`);
|
||||
|
||||
const resolvedEventsPath = $derived.by(() => {
|
||||
const finalEventsPath = eventsPath || defaultEventsPath;
|
||||
if (page.data?.globalPageVisibilitySettings?.forceExclusivity) {
|
||||
const currentPagePath = page.params?.page_path?.trim();
|
||||
return currentPagePath ? `/${currentPagePath}${finalEventsPath}` : finalEventsPath;
|
||||
}
|
||||
return finalEventsPath;
|
||||
});
|
||||
|
||||
async function fetchNotifications() {
|
||||
loading = true;
|
||||
try {
|
||||
notifications = await requestNotifications(monitorTags);
|
||||
} catch {
|
||||
// silently fail
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (fetchOnMount) {
|
||||
void fetchNotifications();
|
||||
}
|
||||
});
|
||||
|
||||
function getEventId(eventURL: string) {
|
||||
return eventURL.split("/").filter(Boolean).at(-1) || "";
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet notificationItem(item: NotificationEvent)}
|
||||
<div class="my-0.5 flex items-center justify-between gap-2 text-xs">
|
||||
<span class="text-muted-foreground text-[11px] uppercase">{$t(item.eventType)}</span>
|
||||
<span class="text-{item.eventStatus.toLowerCase()}">{$t(item.eventStatus)}</span>
|
||||
</div>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<p class="line-clamp-2 text-sm">{item.eventTitle}</p>
|
||||
</div>
|
||||
<div class="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-xs">
|
||||
<span>{$formatDate(item.eventDate, page.data.dateAndTimeFormat.datePlusTime)}</span>
|
||||
|
||||
<span>•</span>
|
||||
<span>{$formatDuration(item.eventStartDateTime, item.eventEndDateTime, $t("Ongoing"))}</span>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div class="flex items-center justify-between border-b px-4 py-3">
|
||||
<h4 class="text-sm font-semibold">{$t("Events")}</h4>
|
||||
<Button variant="outline" href={clientResolver(resolve, resolvedEventsPath)} size="icon-sm" class="rounded-btn">
|
||||
<Calendar class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if notifications.length === 0}
|
||||
<div class="text-muted-foreground px-4 py-6 text-center text-sm">
|
||||
{$t("No events to show")}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="scrollbar-hidden max-h-96 overflow-y-auto">
|
||||
{#each notifications as item, i (`${item.eventType}-${item.eventURL}-${item.eventDate}-${i}`)}
|
||||
{@const eventId = getEventId(item.eventURL)}
|
||||
{#if item.eventURL.startsWith("/incidents/")}
|
||||
<a
|
||||
href={resolve("/(kener)/incidents/[incident_id]", { incident_id: eventId })}
|
||||
class="hover:bg-muted/60 block border-b px-4 py-3 last:border-b-0"
|
||||
>
|
||||
{@render notificationItem(item)}
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
href={resolve("/(kener)/maintenances/[maintenance_id]", { maintenance_id: eventId })}
|
||||
class="hover:bg-muted/60 block border-b px-4 py-3 last:border-b-0"
|
||||
>
|
||||
{@render notificationItem(item)}
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,22 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from "$app/paths";
|
||||
import { page } from "$app/state";
|
||||
import NotificationsList from "$lib/components/NotificationsList.svelte";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import * as Popover from "$lib/components/ui/popover/index.js";
|
||||
import { Spinner } from "$lib/components/ui/spinner/index.js";
|
||||
import { requestNotifications } from "$lib/client/notifications-client.js";
|
||||
import ICONS from "$lib/icons";
|
||||
import clientResolver from "$lib/client/resolver.js";
|
||||
import { formatDate, formatDuration } from "$lib/stores/datetime";
|
||||
import { t } from "$lib/stores/i18n";
|
||||
import type { NotificationEvent } from "$lib/server/controllers/dashboardController.js";
|
||||
import Calendar from "@lucide/svelte/icons/calendar-1";
|
||||
import { format } from "date-fns";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
interface Props {
|
||||
monitorTags?: string[];
|
||||
compact?: boolean;
|
||||
eventsPath: string;
|
||||
eventsPath?: string;
|
||||
}
|
||||
|
||||
let { monitorTags = [], compact = true, eventsPath = "" }: Props = $props();
|
||||
@@ -24,26 +20,10 @@
|
||||
let notifications = $state<NotificationEvent[]>([]);
|
||||
let loading = $state(false);
|
||||
|
||||
const defaultEventsPath = $derived(`/events/${format(new Date(), "MMMM-yyyy")}`);
|
||||
|
||||
const resolvedEventsPath = $derived.by(() => {
|
||||
const finalEventsPath = eventsPath || defaultEventsPath;
|
||||
if (page.data?.globalPageVisibilitySettings?.forceExclusivity) {
|
||||
const currentPagePath = page.params?.page_path?.trim();
|
||||
return currentPagePath ? `/${currentPagePath}${finalEventsPath}` : finalEventsPath;
|
||||
}
|
||||
return finalEventsPath;
|
||||
});
|
||||
|
||||
async function fetchNotifications() {
|
||||
loading = true;
|
||||
try {
|
||||
const query = monitorTags.length > 0 ? `?tags=${encodeURIComponent(monitorTags.join(","))}` : "";
|
||||
const response = await fetch(clientResolver(resolve, "/dashboard-apis/notifications") + query);
|
||||
if (response.ok) {
|
||||
const payload = await response.json();
|
||||
notifications = payload.notifications || [];
|
||||
}
|
||||
notifications = await requestNotifications(monitorTags);
|
||||
} catch {
|
||||
// silently fail
|
||||
} finally {
|
||||
@@ -52,7 +32,7 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
fetchNotifications();
|
||||
void fetchNotifications();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -87,40 +67,6 @@
|
||||
class="bg-background/30 supports-backdrop-filter:bg-background/20 w-96 rounded-3xl border p-0 shadow-2xl backdrop-blur-2xl"
|
||||
sideOffset={8}
|
||||
>
|
||||
<div class="flex items-center justify-between border-b px-4 py-3">
|
||||
<h4 class="text-sm font-semibold">{$t("Events")}</h4>
|
||||
<Button variant="outline" href={clientResolver(resolve, resolvedEventsPath)} size="icon-sm" class="rounded-btn">
|
||||
<Calendar class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if notifications.length === 0}
|
||||
<div class="text-muted-foreground px-4 py-6 text-sm">
|
||||
{$t("No events to show")}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="scrollbar-hidden max-h-96 overflow-y-auto">
|
||||
{#each notifications as item, i (`${item.eventType}-${item.eventURL}-${item.eventDate}-${i}`)}
|
||||
<a
|
||||
href={clientResolver(resolve, item.eventURL)}
|
||||
class="hover:bg-muted/60 block border-b px-4 py-3 last:border-b-0"
|
||||
>
|
||||
<div class="my-0.5 flex items-center justify-between gap-2 text-xs">
|
||||
<span class="text-muted-foreground text-[11px] uppercase">{$t(item.eventType)}</span>
|
||||
<span class="text-{item.eventStatus.toLowerCase()}">{$t(item.eventStatus)}</span>
|
||||
</div>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<p class="line-clamp-2 text-sm">{item.eventTitle}</p>
|
||||
</div>
|
||||
<div class="text-muted-foreground mt-1 flex flex-wrap items-center gap-2 text-xs">
|
||||
<span>{$formatDate(item.eventDate, page.data.dateAndTimeFormat.datePlusTime)}</span>
|
||||
|
||||
<span>•</span>
|
||||
<span>{$formatDuration(item.eventStartDateTime, item.eventEndDateTime, $t("Ongoing"))}</span>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<NotificationsList {monitorTags} {eventsPath} fetchOnMount={false} bind:notifications bind:loading />
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
|
||||
@@ -27,14 +27,16 @@
|
||||
interface Props {
|
||||
monitor_tags?: string[];
|
||||
embedMonitorTag?: string;
|
||||
hideNotificationsPopover?: boolean;
|
||||
}
|
||||
|
||||
let { monitor_tags = [], embedMonitorTag = "" }: Props = $props();
|
||||
let { monitor_tags = [], embedMonitorTag = "", hideNotificationsPopover = false }: Props = $props();
|
||||
|
||||
let protocol = $state("");
|
||||
let domain = $state("");
|
||||
let shareLink = $state("");
|
||||
const eventsPath = $derived(`/events/${format(new Date(), "MMMM-yyyy")}`);
|
||||
const showNotificationsPopover = $derived(!hideNotificationsPopover);
|
||||
|
||||
const rssHref = $derived.by(() => {
|
||||
const params = page.params;
|
||||
@@ -183,7 +185,9 @@
|
||||
<TimezoneSelector />
|
||||
{/if}
|
||||
</ButtonGroup.Root>
|
||||
<NotificationsPopover {eventsPath} monitorTags={monitor_tags} compact={true} />
|
||||
{#if showNotificationsPopover}
|
||||
<NotificationsPopover {eventsPath} monitorTags={monitor_tags} compact={true} />
|
||||
{/if}
|
||||
{#if loginDetails}
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -377,17 +377,18 @@ export const GetPageDashboardData = async (
|
||||
};
|
||||
}
|
||||
const eventSettings = layoutData.eventDisplaySettings;
|
||||
const showInlineEvents = eventSettings.showInlineEvents === true;
|
||||
// Fetch all dashboard data in parallel (respecting feature toggles)
|
||||
const [latestData, parsedMonitors, ongoingIncidents, ongoingMaintenances, upcomingMaintenances] = await Promise.all([
|
||||
GetLatestMonitoringDataAllActive(monitorTags),
|
||||
GetMonitorsParsed({ tags: monitorTags, status: "ACTIVE", is_hidden: "NO" }),
|
||||
eventSettings.incidents.enabled && eventSettings.incidents.ongoing.show
|
||||
showInlineEvents && eventSettings.incidents.enabled && eventSettings.incidents.ongoing.show
|
||||
? GetOngoingIncidentsForMonitorList(monitorTags)
|
||||
: Promise.resolve([] as IncidentForMonitorListWithComments[]),
|
||||
eventSettings.maintenances.enabled && eventSettings.maintenances.ongoing.show
|
||||
showInlineEvents && eventSettings.maintenances.enabled && eventSettings.maintenances.ongoing.show
|
||||
? GetOngoingMaintenances(monitorTags, nowTs)
|
||||
: Promise.resolve([] as MaintenanceEventsMonitorList[]),
|
||||
eventSettings.maintenances.enabled && eventSettings.maintenances.upcoming.show
|
||||
showInlineEvents && eventSettings.maintenances.enabled && eventSettings.maintenances.upcoming.show
|
||||
? GetUpcomingMaintenanceEventsForMonitorList(
|
||||
monitorTags,
|
||||
eventSettings.maintenances.upcoming.maxCount,
|
||||
|
||||
@@ -76,6 +76,42 @@ export interface LayoutServerData {
|
||||
metaSiteDescription?: string;
|
||||
}
|
||||
|
||||
function NormalizeEventDisplaySettings(settings?: Partial<EventDisplaySettings>): EventDisplaySettings {
|
||||
const defaults = structuredClone(seedSiteData.eventDisplaySettings);
|
||||
|
||||
return {
|
||||
showInlineEvents: Boolean(settings?.showInlineEvents ?? defaults.showInlineEvents),
|
||||
incidents: {
|
||||
...defaults.incidents,
|
||||
...settings?.incidents,
|
||||
ongoing: {
|
||||
...defaults.incidents.ongoing,
|
||||
...settings?.incidents?.ongoing,
|
||||
},
|
||||
resolved: {
|
||||
...defaults.incidents.resolved,
|
||||
...settings?.incidents?.resolved,
|
||||
},
|
||||
},
|
||||
maintenances: {
|
||||
...defaults.maintenances,
|
||||
...settings?.maintenances,
|
||||
ongoing: {
|
||||
...defaults.maintenances.ongoing,
|
||||
...settings?.maintenances?.ongoing,
|
||||
},
|
||||
past: {
|
||||
...defaults.maintenances.past,
|
||||
...settings?.maintenances?.past,
|
||||
},
|
||||
upcoming: {
|
||||
...defaults.maintenances.upcoming,
|
||||
...settings?.maintenances?.upcoming,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function GetLayoutServerData(cookies: Cookies, request: Request): Promise<LayoutServerData> {
|
||||
const userAgent = request.headers.get("user-agent") ?? "";
|
||||
const md = new MobileDetect(userAgent);
|
||||
@@ -139,7 +175,7 @@ export async function GetLayoutServerData(cookies: Cookies, request: Request): P
|
||||
font,
|
||||
canSendEmail,
|
||||
announcement: siteData.announcement,
|
||||
eventDisplaySettings: siteData.eventDisplaySettings || seedSiteData.eventDisplaySettings,
|
||||
eventDisplaySettings: NormalizeEventDisplaySettings(siteData.eventDisplaySettings),
|
||||
socialPreviewImage: siteData.socialPreviewImage,
|
||||
customCSS: siteData.customCSS,
|
||||
globalPageVisibilitySettings: siteData.globalPageVisibilitySettings || seedSiteData.globalPageVisibilitySettings,
|
||||
|
||||
@@ -142,6 +142,7 @@ const seedSiteData = {
|
||||
retentionDays: 90,
|
||||
},
|
||||
eventDisplaySettings: {
|
||||
showInlineEvents: false,
|
||||
incidents: {
|
||||
enabled: true,
|
||||
ongoing: { show: true },
|
||||
|
||||
@@ -98,6 +98,7 @@ export interface DataRetentionPolicy {
|
||||
}
|
||||
|
||||
export interface EventDisplaySettings {
|
||||
showInlineEvents: boolean;
|
||||
incidents: {
|
||||
enabled: boolean;
|
||||
ongoing: {
|
||||
|
||||
@@ -26,6 +26,7 @@ Use **Manage → Site Configurations** to control identity, navigation, monitor
|
||||
| Monitor sub menu options | `subMenuOptions` | Gates monitor share actions (badges/embed) on public monitor pages |
|
||||
| Global page visibility | `globalPageVisibilitySettings` | Controls page switcher visibility and page-scoped navigation/events |
|
||||
| Data retention policy | `dataRetentionPolicy` | Controls daily cleanup of old `monitoring_data` |
|
||||
| Event display settings | `eventDisplaySettings` | Controls event visibility and whether events render inline or in the notification surface |
|
||||
| Maintenance notifications | `globalMaintenanceNotificationSettings` | Controls which maintenance lifecycle events notify subscribers and reminder buffer timing |
|
||||
| Social preview & SEO | `metaSiteTitle`, `metaSiteDescription`, `socialPreviewImage` | Default `<title>`, `og:title`, `<meta description>`, `og:description`, `og:image` for all pages |
|
||||
|
||||
@@ -76,13 +77,14 @@ When enabled, cleanup runs daily at midnight UTC.
|
||||
|
||||
`eventDisplaySettings` controls which events are visible:
|
||||
|
||||
- `showInlineEvents`: when `true`, ongoing/upcoming events render inline on status pages; when `false` (default), they are shown through the notification UI.
|
||||
- incidents: ongoing/resolved + resolved limits
|
||||
- maintenances: ongoing/past/upcoming + limits
|
||||
|
||||
This affects:
|
||||
|
||||
- event sections on status pages
|
||||
- notifications payload API used by the UI
|
||||
- notification UI visibility and payloads used by the UI
|
||||
|
||||
## Maintenance notification settings {#maintenance-notification-settings}
|
||||
|
||||
@@ -126,7 +128,7 @@ These values are used as defaults for every page. Individual pages can override
|
||||
- Enable `forceExclusivity` and verify:
|
||||
- brand link stays within current page path,
|
||||
- notifications calendar opens page-scoped events for the current month.
|
||||
- Change event display settings and verify incident/maintenance visibility.
|
||||
- Change event display settings and verify inline events vs notification UI behavior.
|
||||
- Set retention policy and confirm scheduler logs in server output.
|
||||
- Toggle maintenance notification event types and verify subscribers receive (or don't receive) emails at each lifecycle stage.
|
||||
- Set meta title/description and social preview image, then check `<meta>` tags in page source.
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
import ThemePlus from "$lib/components/ThemePlus.svelte";
|
||||
import IncidentItem from "$lib/components/IncidentItem.svelte";
|
||||
import MaintenanceItem from "$lib/components/MaintenanceItem.svelte";
|
||||
import NotificationsList from "$lib/components/NotificationsList.svelte";
|
||||
import mdToHTML from "$lib/marked.js";
|
||||
import clientResolver, { absoluteResolve } from "$lib/client/resolver.js";
|
||||
import { resolve } from "$app/paths";
|
||||
|
||||
import { selectedTimezone } from "$lib/stores/timezone";
|
||||
import { getEndOfDayAtTz } from "$lib/client/datetime";
|
||||
import { requestMonitorBar } from "$lib/client/monitor-bar-client";
|
||||
@@ -16,7 +18,6 @@
|
||||
import { SveltePurify } from "@humanspeak/svelte-purify";
|
||||
import type { PageMonitorLayoutStyle } from "$lib/types/api";
|
||||
import GC from "$lib/global-constants.js";
|
||||
|
||||
let { data } = $props();
|
||||
let pageSettings = $derived(data.pageDetails.page_settings);
|
||||
let barCount = $derived.by(() =>
|
||||
@@ -31,6 +32,7 @@
|
||||
let requestVersion = 0;
|
||||
let viewType = $derived<PageMonitorLayoutStyle | undefined>(pageSettings?.monitor_layout_style);
|
||||
let isCompact = $derived(viewType === "compact-list" || viewType === "compact-grid");
|
||||
let showInlineEvents = $derived(data.eventDisplaySettings?.showInlineEvents === true);
|
||||
|
||||
function getGridItemSpanClass(index: number, total: number, type: typeof viewType): string {
|
||||
if (type === "default-grid") {
|
||||
@@ -143,7 +145,7 @@
|
||||
|
||||
<!-- page title -->
|
||||
<div class="flex flex-col gap-3 sm:gap-4">
|
||||
<ThemePlus monitor_tags={data.monitorTags} />
|
||||
<ThemePlus monitor_tags={data.monitorTags} hideNotificationsPopover={!!showInlineEvents} />
|
||||
<div class="flex flex-col gap-2 px-3 py-2 sm:px-4">
|
||||
{#if data.pageDetails?.page_logo}
|
||||
<img
|
||||
@@ -169,58 +171,68 @@
|
||||
</Item.Content>
|
||||
</Item.Root>
|
||||
</div>
|
||||
{#if !!data.monitorTags.length}
|
||||
<EventsCard statusClass={data.pageStatus.statusClass} statusText={data.pageStatus.statusSummary} />
|
||||
{#if data.ongoingIncidents && data.ongoingIncidents.length > 0}
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each data.ongoingIncidents as incident, i (incident.id ?? i)}
|
||||
<div class=" rounded-3xl border p-3 sm:p-4">
|
||||
<IncidentItem {incident} />
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-16">
|
||||
<div class="col-span-1 flex flex-col gap-3 sm:gap-4 {!!!showInlineEvents ? 'sm:col-span-16' : 'sm:col-span-11'}">
|
||||
{#if !!data.monitorTags.length}
|
||||
<EventsCard statusClass={data.pageStatus.statusClass} statusText={data.pageStatus.statusSummary} />
|
||||
{#if showInlineEvents && data.ongoingIncidents && data.ongoingIncidents.length > 0}
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each data.ongoingIncidents as incident, i (incident.id ?? i)}
|
||||
<div class=" rounded-3xl border p-3 sm:p-4">
|
||||
<IncidentItem {incident} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.ongoingMaintenances && data.ongoingMaintenances.length > 0}
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each data.ongoingMaintenances as maintenance, i (maintenance.id ?? i)}
|
||||
<div class="rounded-3xl border p-3 sm:p-4">
|
||||
<MaintenanceItem {maintenance} />
|
||||
{/if}
|
||||
{#if showInlineEvents && data.ongoingMaintenances && data.ongoingMaintenances.length > 0}
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each data.ongoingMaintenances as maintenance, i (maintenance.id ?? i)}
|
||||
<div class="rounded-3xl border p-3 sm:p-4">
|
||||
<MaintenanceItem {maintenance} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.upcomingMaintenances && data.upcomingMaintenances.length > 0}
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each data.upcomingMaintenances as maintenance, i (maintenance.id ?? i)}
|
||||
<div class="rounded-3xl border p-3 sm:p-4">
|
||||
<MaintenanceItem {maintenance} />
|
||||
{/if}
|
||||
{#if showInlineEvents && data.upcomingMaintenances && data.upcomingMaintenances.length > 0}
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each data.upcomingMaintenances as maintenance, i (maintenance.id ?? i)}
|
||||
<div class="rounded-3xl border p-3 sm:p-4">
|
||||
<MaintenanceItem {maintenance} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="overflow-hidden rounded-3xl border">
|
||||
<div class={`grid grid-cols-1 ${getGridContainerClass(viewType)}`}>
|
||||
{#each data.monitorTags as tag, i (tag)}
|
||||
<div
|
||||
class="{viewType === 'compact-grid' || viewType === 'default-grid'
|
||||
? `${getGridItemSpanClass(i, data.monitorTags.length, viewType)} bg-background`
|
||||
: i < data.monitorTags.length - 1
|
||||
? 'border-b'
|
||||
: ''} px-2 py-2 sm:px-0"
|
||||
>
|
||||
<MonitorBar
|
||||
{tag}
|
||||
prefetchedData={monitorBarDataByTag[tag]}
|
||||
prefetchedError={monitorBarErrorByTag[tag]}
|
||||
days={barCount}
|
||||
{endOfDayTodayAtTz}
|
||||
groupChildTags={data.monitorGroupMembersByTag?.[tag] || []}
|
||||
compact={isCompact}
|
||||
grid={viewType === "compact-grid" || viewType === "default-grid"}
|
||||
/>
|
||||
{/if}
|
||||
<div class="overflow-hidden rounded-3xl border">
|
||||
<div class={`grid grid-cols-1 ${getGridContainerClass(viewType)}`}>
|
||||
{#each data.monitorTags as tag, i (tag)}
|
||||
<div
|
||||
class="{viewType === 'compact-grid' || viewType === 'default-grid'
|
||||
? `${getGridItemSpanClass(i, data.monitorTags.length, viewType)} bg-background`
|
||||
: i < data.monitorTags.length - 1
|
||||
? 'border-b'
|
||||
: ''} px-2 py-2 sm:px-0"
|
||||
>
|
||||
<MonitorBar
|
||||
{tag}
|
||||
prefetchedData={monitorBarDataByTag[tag]}
|
||||
prefetchedError={monitorBarErrorByTag[tag]}
|
||||
days={barCount}
|
||||
{endOfDayTodayAtTz}
|
||||
groupChildTags={data.monitorGroupMembersByTag?.[tag] || []}
|
||||
compact={isCompact}
|
||||
grid={viewType === "compact-grid" || viewType === "default-grid"}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if !!showInlineEvents}
|
||||
<!-- Notification List -->
|
||||
<div class="col-span-1 overflow-hidden rounded-3xl border sm:col-span-5">
|
||||
<NotificationsList monitorTags={data.monitorTags} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
let requestVersion = 0;
|
||||
let viewType = $derived<PageMonitorLayoutStyle | undefined>(pageSettings?.monitor_layout_style);
|
||||
let isCompact = $derived(viewType === "compact-list" || viewType === "compact-grid");
|
||||
let showInlineEvents = $derived(data.eventDisplaySettings?.showInlineEvents === true);
|
||||
|
||||
function getGridItemSpanClass(index: number, total: number, type: typeof viewType): string {
|
||||
if (type === "default-grid") {
|
||||
@@ -171,7 +172,7 @@
|
||||
</div>
|
||||
{#if !!data.monitorTags.length}
|
||||
<EventsCard statusClass={data.pageStatus.statusClass} statusText={data.pageStatus.statusSummary} />
|
||||
{#if data.ongoingIncidents && data.ongoingIncidents.length > 0}
|
||||
{#if showInlineEvents && data.ongoingIncidents && data.ongoingIncidents.length > 0}
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each data.ongoingIncidents as incident, i (incident.id ?? i)}
|
||||
<div class=" rounded-3xl border p-3 sm:p-4">
|
||||
@@ -180,7 +181,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.ongoingMaintenances && data.ongoingMaintenances.length > 0}
|
||||
{#if showInlineEvents && data.ongoingMaintenances && data.ongoingMaintenances.length > 0}
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each data.ongoingMaintenances as maintenance, i (maintenance.id ?? i)}
|
||||
<div class="rounded-3xl border p-3 sm:p-4">
|
||||
@@ -189,7 +190,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.upcomingMaintenances && data.upcomingMaintenances.length > 0}
|
||||
{#if showInlineEvents && data.upcomingMaintenances && data.upcomingMaintenances.length > 0}
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each data.upcomingMaintenances as maintenance, i (maintenance.id ?? i)}
|
||||
<div class="rounded-3xl border p-3 sm:p-4">
|
||||
|
||||
@@ -36,10 +36,15 @@ export const load: PageServerLoad = async ({ params, parent }) => {
|
||||
const monitorTags = [monitor_tag];
|
||||
|
||||
const eventSettings = parentData.eventDisplaySettings;
|
||||
const showInlineEvents = eventSettings.showInlineEvents === true;
|
||||
const [ongoingIncidents, ongoingMaintenances, upcomingMaintenances] = await Promise.all([
|
||||
GetOngoingIncidentsForMonitorList(monitorTags),
|
||||
GetOngoingMaintenanceEventsForMonitorList(monitorTags),
|
||||
eventSettings.maintenances.enabled && eventSettings.maintenances.upcoming.show
|
||||
showInlineEvents && eventSettings.incidents.enabled && eventSettings.incidents.ongoing.show
|
||||
? GetOngoingIncidentsForMonitorList(monitorTags)
|
||||
: Promise.resolve([]),
|
||||
showInlineEvents && eventSettings.maintenances.enabled && eventSettings.maintenances.ongoing.show
|
||||
? GetOngoingMaintenanceEventsForMonitorList(monitorTags)
|
||||
: Promise.resolve([]),
|
||||
showInlineEvents && eventSettings.maintenances.enabled && eventSettings.maintenances.upcoming.show
|
||||
? GetUpcomingMaintenanceEventsForMonitorList(
|
||||
monitorTags,
|
||||
eventSettings.maintenances.upcoming.maxCount,
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
// State
|
||||
let descriptionExpanded = $state(false);
|
||||
let showInlineEvents = $derived(data.eventDisplaySettings?.showInlineEvents === true);
|
||||
|
||||
function toggleDescription(expanded: boolean) {
|
||||
descriptionExpanded = expanded;
|
||||
@@ -125,7 +126,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if data.ongoingIncidents && data.ongoingIncidents.length > 0}
|
||||
{#if showInlineEvents && data.ongoingIncidents && data.ongoingIncidents.length > 0}
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each data.ongoingIncidents as incident, i (incident.id ?? i)}
|
||||
<div class=" rounded-3xl border p-3 sm:p-4">
|
||||
@@ -134,7 +135,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.ongoingMaintenances && data.ongoingMaintenances.length > 0}
|
||||
{#if showInlineEvents && data.ongoingMaintenances && data.ongoingMaintenances.length > 0}
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each data.ongoingMaintenances as maintenance, i (maintenance.id ?? i)}
|
||||
<div class="rounded-3xl border p-3 sm:p-4">
|
||||
@@ -143,7 +144,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.upcomingMaintenances && data.upcomingMaintenances.length > 0}
|
||||
{#if showInlineEvents && data.upcomingMaintenances && data.upcomingMaintenances.length > 0}
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each data.upcomingMaintenances as maintenance, i (maintenance.id ?? i)}
|
||||
<div class="rounded-3xl border p-3 sm:p-4">
|
||||
|
||||
@@ -205,11 +205,7 @@
|
||||
|
||||
if (data.eventDisplaySettings) {
|
||||
try {
|
||||
const parsed =
|
||||
typeof data.eventDisplaySettings === "string"
|
||||
? JSON.parse(data.eventDisplaySettings)
|
||||
: data.eventDisplaySettings;
|
||||
eventDisplaySettings = { ...structuredClone(defaultEventDisplaySettings), ...parsed };
|
||||
eventDisplaySettings = parseEventDisplaySettings(data.eventDisplaySettings);
|
||||
} catch {
|
||||
eventDisplaySettings = structuredClone(defaultEventDisplaySettings);
|
||||
}
|
||||
@@ -509,6 +505,43 @@
|
||||
}
|
||||
}
|
||||
|
||||
function parseEventDisplaySettings(value: unknown): EventDisplaySettings {
|
||||
const parsed = (typeof value === "string" ? JSON.parse(value) : value) as Partial<EventDisplaySettings> | null;
|
||||
const defaults = structuredClone(defaultEventDisplaySettings);
|
||||
|
||||
return {
|
||||
showInlineEvents: Boolean(parsed?.showInlineEvents ?? defaults.showInlineEvents),
|
||||
incidents: {
|
||||
...defaults.incidents,
|
||||
...parsed?.incidents,
|
||||
ongoing: {
|
||||
...defaults.incidents.ongoing,
|
||||
...parsed?.incidents?.ongoing
|
||||
},
|
||||
resolved: {
|
||||
...defaults.incidents.resolved,
|
||||
...parsed?.incidents?.resolved
|
||||
}
|
||||
},
|
||||
maintenances: {
|
||||
...defaults.maintenances,
|
||||
...parsed?.maintenances,
|
||||
ongoing: {
|
||||
...defaults.maintenances.ongoing,
|
||||
...parsed?.maintenances?.ongoing
|
||||
},
|
||||
past: {
|
||||
...defaults.maintenances.past,
|
||||
...parsed?.maintenances?.past
|
||||
},
|
||||
upcoming: {
|
||||
...defaults.maintenances.upcoming,
|
||||
...parsed?.maintenances?.upcoming
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function addSitemapUrl() {
|
||||
sitemap.urls = [...sitemap.urls, { loc: "" }];
|
||||
}
|
||||
@@ -1249,6 +1282,16 @@
|
||||
<Card.Description>Configure which incidents and maintenances are shown on the site</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="space-y-6">
|
||||
<div class="flex items-center justify-between rounded-lg border p-4">
|
||||
<div class="space-y-0.5">
|
||||
<Label for="events-display-inline">Display Events Inline</Label>
|
||||
<p class="text-muted-foreground text-xs">
|
||||
Turn on to show events inline on the status page. Off shows them in the notification list.
|
||||
</p>
|
||||
</div>
|
||||
<Switch id="events-display-inline" bind:checked={eventDisplaySettings.showInlineEvents} />
|
||||
</div>
|
||||
|
||||
<!-- Incidents -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
Reference in New Issue
Block a user