Merge pull request #799 from lokiee0/feat/795-monitoring-status-filter

Feat/795 monitoring status filter
This commit is contained in:
Raj Nandan Sharma
2026-08-03 10:16:38 +05:30
committed by GitHub
14 changed files with 107 additions and 19 deletions
+4 -4
View File
@@ -12,16 +12,16 @@ env:
DOCKERHUB_IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }} DOCKERHUB_IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}
GITHUB_IMAGE_NAME: ${{ github.repository }} GITHUB_IMAGE_NAME: ${{ github.repository }}
concurrency:
group: main-docker-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
build-and-push-main: build-and-push-main:
name: Build and push main Docker images (with docs) name: Build and push main Docker images (with docs)
strategy: strategy:
fail-fast: false
matrix: matrix:
variant: [debian, alpine] variant: [debian, alpine]
concurrency:
group: main-docker-${{ github.ref }}-${{ matrix.variant }}
cancel-in-progress: true
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
+2 -1
View File
@@ -1,5 +1,6 @@
import type { TimestampStatusCount } from "$lib/server/types/db"; import type { TimestampStatusCount } from "$lib/server/types/db";
import GC, { PAGE_STATUS_MESSAGES, type StatusType } from "$lib/global-constants"; import GC, { PAGE_STATUS_MESSAGES } from "$lib/global-constants";
import type { StatusType } from "$lib/types/status";
function ParseLatency(latencyMs: number): string { function ParseLatency(latencyMs: number): string {
if (!!!latencyMs) { if (!!!latencyMs) {
+13
View File
@@ -0,0 +1,13 @@
import { describe, expect, it } from "vitest";
import { isMonitoringStatus } from "./global-constants.js";
describe("isMonitoringStatus", () => {
it("accepts only statuses supported by the monitoring filter", () => {
expect(isMonitoringStatus("UP")).toBe(true);
expect(isMonitoringStatus("DOWN")).toBe(true);
expect(isMonitoringStatus("DEGRADED")).toBe(true);
expect(isMonitoringStatus("MAINTENANCE")).toBe(false);
expect(isMonitoringStatus("")).toBe(false);
expect(isMonitoringStatus("invalid")).toBe(false);
});
});
+7 -1
View File
@@ -1,9 +1,15 @@
import type { MonitoringStatus } from "$lib/types/status.js";
// Badge styles supported by badge-maker // Badge styles supported by badge-maker
export const BADGE_STYLES = ["flat", "plastic", "flat-square", "for-the-badge", "social"] as const; export const BADGE_STYLES = ["flat", "plastic", "flat-square", "for-the-badge", "social"] as const;
export type BadgeStyle = (typeof BADGE_STYLES)[number]; export type BadgeStyle = (typeof BADGE_STYLES)[number];
// Status types // Status types
export type StatusType = "UP" | "DOWN" | "DEGRADED" | "MAINTENANCE" | "NO_DATA"; export const MONITORING_STATUSES = ["UP", "DOWN", "DEGRADED"] as const satisfies readonly MonitoringStatus[];
export function isMonitoringStatus(value: unknown): value is MonitoringStatus {
return typeof value === "string" && MONITORING_STATUSES.includes(value as MonitoringStatus);
}
// Incident states // Incident states
export type IncidentState = "INVESTIGATING" | "IDENTIFIED" | "MONITORING" | "RESOLVED"; export type IncidentState = "INVESTIGATING" | "IDENTIFIED" | "MONITORING" | "RESOLVED";
+1 -1
View File
@@ -2,7 +2,7 @@ import { json, error } from "@sveltejs/kit";
import type { APIServerRequest } from "$lib/server/types/api-server"; import type { APIServerRequest } from "$lib/server/types/api-server";
import db from "$lib/server/db/db"; import db from "$lib/server/db/db";
import { GetMinuteStartNowTimestampUTC } from "$lib/server/tool"; import { GetMinuteStartNowTimestampUTC } from "$lib/server/tool";
import type { StatusType } from "$lib/global-constants"; import type { StatusType } from "$lib/types/status";
import type { TimestampStatusCount } from "$lib/server/types/db"; import type { TimestampStatusCount } from "$lib/server/types/db";
import { buildMonitorBarResponse } from "./shared"; import { buildMonitorBarResponse } from "./shared";
@@ -1,5 +1,6 @@
import db from "$lib/server/db/db"; import db from "$lib/server/db/db";
import GC, { type StatusType } from "$lib/global-constants"; import GC from "$lib/global-constants";
import type { StatusType } from "$lib/types/status";
import type { MonitorRecord, TimestampStatusCount } from "$lib/server/types/db"; import type { MonitorRecord, TimestampStatusCount } from "$lib/server/types/db";
import { UptimeCalculator } from "$lib/server/tool"; import { UptimeCalculator } from "$lib/server/tool";
import type { MonitorBarResponse } from "./get"; import type { MonitorBarResponse } from "./get";
@@ -2,7 +2,7 @@ import { json, error } from "@sveltejs/kit";
import type { APIServerRequest } from "$lib/server/types/api-server"; import type { APIServerRequest } from "$lib/server/types/api-server";
import db from "$lib/server/db/db"; import db from "$lib/server/db/db";
import { GetMinuteStartNowTimestampUTC } from "$lib/server/tool"; import { GetMinuteStartNowTimestampUTC } from "$lib/server/tool";
import type { StatusType } from "$lib/global-constants"; import type { StatusType } from "$lib/types/status";
import GC from "$lib/global-constants"; import GC from "$lib/global-constants";
import type { MonitorBarResponse } from "$lib/server/api-server/monitor-bar/get"; import type { MonitorBarResponse } from "$lib/server/api-server/monitor-bar/get";
import { buildMonitorBarResponseFromRawData } from "$lib/server/api-server/monitor-bar/shared"; import { buildMonitorBarResponseFromRawData } from "$lib/server/api-server/monitor-bar/shared";
@@ -22,6 +22,7 @@ import type { MonitorFilter } from "../db/repositories/base.js";
import db from "../db/db.js"; import db from "../db/db.js";
import type { PaginationInput } from "../../types/common.js"; import type { PaginationInput } from "../../types/common.js";
import GC, { getBadgeStyle, type BadgeStyle } from "../../global-constants.js"; import GC, { getBadgeStyle, type BadgeStyle } from "../../global-constants.js";
import type { MonitoringStatus } from "../../types/status.js";
import { makeBadge } from "badge-maker"; import { makeBadge } from "badge-maker";
import { ErrorSvg } from "../../anywhere.js"; import { ErrorSvg } from "../../anywhere.js";
import { GetLastMonitoringValue, SetLastHeartbeat, DeleteMonitorCaches } from "../cache/setGet.js"; import { GetLastMonitoringValue, SetLastHeartbeat, DeleteMonitorCaches } from "../cache/setGet.js";
@@ -490,7 +491,7 @@ export const GetStatusCountsByInterval = async (
export const GetMonitoringDataPaginated = async ( export const GetMonitoringDataPaginated = async (
page: number, page: number,
limit: number, limit: number,
filter?: { monitor_tag?: string; start_time?: number; end_time?: number }, filter?: { monitor_tag?: string; status?: MonitoringStatus; start_time?: number; end_time?: number },
): Promise<{ data: MonitoringData[]; total: number }> => { ): Promise<{ data: MonitoringData[]; total: number }> => {
const data = await db.getMonitoringDataPaginated(page, limit, filter); const data = await db.getMonitoringDataPaginated(page, limit, filter);
const countResult = await db.getMonitoringDataCount(filter); const countResult = await db.getMonitoringDataCount(filter);
+15 -2
View File
@@ -1,6 +1,7 @@
import type { Knex as KnexType } from "knex"; import type { Knex as KnexType } from "knex";
import { BaseRepository } from "./base.js"; import { BaseRepository } from "./base.js";
import GC from "../../../global-constants.js"; import GC from "../../../global-constants.js";
import type { MonitoringStatus } from "../../../types/status.js";
import { GetMinuteStartNowTimestampUTC } from "../../tool.js"; import { GetMinuteStartNowTimestampUTC } from "../../tool.js";
import type { import type {
MonitoringData, MonitoringData,
@@ -82,7 +83,7 @@ export class MonitoringRepository extends BaseRepository {
async getMonitoringDataPaginated( async getMonitoringDataPaginated(
page: number, page: number,
limit: number, limit: number,
filter?: { monitor_tag?: string; start_time?: number; end_time?: number }, filter?: { monitor_tag?: string; status?: MonitoringStatus; start_time?: number; end_time?: number },
): Promise<MonitoringData[]> { ): Promise<MonitoringData[]> {
let query = this.knex("monitoring_data").select("*"); let query = this.knex("monitoring_data").select("*");
@@ -90,6 +91,10 @@ export class MonitoringRepository extends BaseRepository {
query = query.where("monitor_tag", filter.monitor_tag); query = query.where("monitor_tag", filter.monitor_tag);
} }
if (filter?.status) {
query = query.where("status", filter.status);
}
if (filter?.start_time) { if (filter?.start_time) {
query = query.where("timestamp", ">=", filter.start_time); query = query.where("timestamp", ">=", filter.start_time);
} }
@@ -106,6 +111,7 @@ export class MonitoringRepository extends BaseRepository {
async getMonitoringDataCount(filter?: { async getMonitoringDataCount(filter?: {
monitor_tag?: string; monitor_tag?: string;
status?: MonitoringStatus;
start_time?: number; start_time?: number;
end_time?: number; end_time?: number;
}): Promise<{ count: number }> { }): Promise<{ count: number }> {
@@ -115,6 +121,10 @@ export class MonitoringRepository extends BaseRepository {
query = query.where("monitor_tag", filter.monitor_tag); query = query.where("monitor_tag", filter.monitor_tag);
} }
if (filter?.status) {
query = query.where("status", filter.status);
}
if (filter?.start_time) { if (filter?.start_time) {
query = query.where("timestamp", ">=", filter.start_time); query = query.where("timestamp", ">=", filter.start_time);
} }
@@ -476,7 +486,7 @@ export class MonitoringRepository extends BaseRepository {
}); });
} }
async deleteMonitorDataByTag(tag?: string, start?: number, end?: number): Promise<number> { async deleteMonitorDataByTag(tag?: string, start?: number, end?: number, status?: MonitoringStatus): Promise<number> {
const query = this.knex("monitoring_data"); const query = this.knex("monitoring_data");
if (tag) { if (tag) {
query.where("monitor_tag", tag); query.where("monitor_tag", tag);
@@ -487,6 +497,9 @@ export class MonitoringRepository extends BaseRepository {
if (end !== undefined) { if (end !== undefined) {
query.where("timestamp", "<=", end); query.where("timestamp", "<=", end);
} }
if (status) {
query.where("status", status);
}
return await query.del(); return await query.del();
} }
+1
View File
@@ -2,3 +2,4 @@ export * from "$lib/types/api";
export * from "$lib/types/monitor"; export * from "$lib/types/monitor";
export * from "$lib/types/result"; export * from "$lib/types/result";
export * from "$lib/types/common"; export * from "$lib/types/common";
export * from "$lib/types/status";
+2
View File
@@ -0,0 +1,2 @@
export type MonitoringStatus = "UP" | "DOWN" | "DEGRADED";
export type StatusType = MonitoringStatus | "MAINTENANCE" | "NO_DATA";
@@ -1,7 +1,8 @@
import { GetMonitors, GetLatestMonitoringData, GetLatestStatusActiveAll } from "$lib/server/controllers/controller.js"; import { GetMonitors, GetLatestMonitoringData, GetLatestStatusActiveAll } from "$lib/server/controllers/controller.js";
import StatusColor, { type StatusColors } from "$lib/color.js"; import StatusColor, { type StatusColors } from "$lib/color.js";
import { ErrorSvg } from "$lib/anywhere.js"; import { ErrorSvg } from "$lib/anywhere.js";
import GC, { type StatusType } from "$lib/global-constants.js"; import GC from "$lib/global-constants.js";
import type { StatusType } from "$lib/types/status.js";
import { GetLastMonitoringValue } from "$lib/server/cache/setGet.js"; import { GetLastMonitoringValue } from "$lib/server/cache/setGet.js";
import type { RequestHandler } from "./$types"; import type { RequestHandler } from "./$types";
+17 -3
View File
@@ -4,7 +4,8 @@ import { format } from "date-fns";
import sharp from "sharp"; import sharp from "sharp";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import db from "$lib/server/db/db"; import db from "$lib/server/db/db";
import GC from "$lib/global-constants.js"; import GC, { isMonitoringStatus } from "$lib/global-constants.js";
import type { MonitoringStatus } from "$lib/types/status.js";
import { import {
CreateUpdateMonitor, CreateUpdateMonitor,
UpdateMonitoringData, UpdateMonitoringData,
@@ -219,7 +220,14 @@ export async function POST({ request, cookies }) {
} else if (action == "deleteMonitor") { } else if (action == "deleteMonitor") {
resp = await DeleteMonitorCompletelyUsingTag(data.tag); resp = await DeleteMonitorCompletelyUsingTag(data.tag);
} else if (action == "deleteMonitorData") { } else if (action == "deleteMonitorData") {
await db.deleteMonitorDataByTag(data.tag || undefined, data.start, data.end); let status: MonitoringStatus | undefined;
if (data.status !== undefined && data.status !== null && data.status !== "ALL") {
if (!isMonitoringStatus(data.status)) {
return json({ error: "Invalid monitoring status" }, { status: 400 });
}
status = data.status;
}
await db.deleteMonitorDataByTag(data.tag || undefined, data.start, data.end, status);
resp = { success: true }; resp = { success: true };
} else if (action == "cloneMonitor") { } else if (action == "cloneMonitor") {
resp = await CloneMonitor({ resp = await CloneMonitor({
@@ -245,10 +253,16 @@ export async function POST({ request, cookies }) {
} else if (action == "getMonitoringDataPaginated") { } else if (action == "getMonitoringDataPaginated") {
const page = parseInt(String(data.page)) || 1; const page = parseInt(String(data.page)) || 1;
const limit = parseInt(String(data.limit)) || 50; const limit = parseInt(String(data.limit)) || 50;
const filter: { monitor_tag?: string; start_time?: number; end_time?: number } = {}; const filter: { monitor_tag?: string; status?: MonitoringStatus; start_time?: number; end_time?: number } = {};
if (data.monitor_tag && data.monitor_tag !== "ALL") { if (data.monitor_tag && data.monitor_tag !== "ALL") {
filter.monitor_tag = data.monitor_tag; filter.monitor_tag = data.monitor_tag;
} }
if (data.status !== undefined && data.status !== null && data.status !== "ALL") {
if (!isMonitoringStatus(data.status)) {
return json({ error: "Invalid monitoring status" }, { status: 400 });
}
filter.status = data.status;
}
if (data.start_time) { if (data.start_time) {
filter.start_time = parseInt(String(data.start_time)); filter.start_time = parseInt(String(data.start_time));
} }
@@ -19,6 +19,8 @@
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import { resolve } from "$app/paths"; import { resolve } from "$app/paths";
import clientResolver from "$lib/client/resolver.js"; import clientResolver from "$lib/client/resolver.js";
import GC, { isMonitoringStatus } from "$lib/global-constants";
import type { MonitoringStatus } from "$lib/types/status.js";
// Types // Types
interface MonitoringData { interface MonitoringData {
@@ -35,6 +37,8 @@
name: string; name: string;
} }
type StatusFilter = "ALL" | MonitoringStatus;
// Helper to format datetime as YYYY-MM-DDTHH:mm for datetime-local input // Helper to format datetime as YYYY-MM-DDTHH:mm for datetime-local input
function formatDateTimeForInput(date: Date): string { function formatDateTimeForInput(date: Date): string {
return format(date, "yyyy-MM-dd'T'HH:mm"); return format(date, "yyyy-MM-dd'T'HH:mm");
@@ -68,12 +72,14 @@
let pageNo = $state(1); let pageNo = $state(1);
let showFilters = $state(false); let showFilters = $state(false);
let monitorTagFilter = $state("ALL"); let monitorTagFilter = $state("ALL");
let statusFilter = $state<StatusFilter>("ALL");
let startDateTime = $state(formatDateTimeForInput(yesterday)); let startDateTime = $state(formatDateTimeForInput(yesterday));
let endDateTime = $state(formatDateTimeForInput(now)); let endDateTime = $state(formatDateTimeForInput(now));
const limit = 50; const limit = 50;
const hasActiveFilters = $derived( const hasActiveFilters = $derived(
monitorTagFilter !== "ALL" || monitorTagFilter !== "ALL" ||
statusFilter !== "ALL" ||
startDateTime !== formatDateTimeForInput(yesterday) || startDateTime !== formatDateTimeForInput(yesterday) ||
endDateTime !== formatDateTimeForInput(now) endDateTime !== formatDateTimeForInput(now)
); );
@@ -113,6 +119,7 @@
function clearFilters() { function clearFilters() {
monitorTagFilter = "ALL"; monitorTagFilter = "ALL";
statusFilter = "ALL";
startDateTime = formatDateTimeForInput(yesterday); startDateTime = formatDateTimeForInput(yesterday);
endDateTime = formatDateTimeForInput(now); endDateTime = formatDateTimeForInput(now);
pageNo = 1; pageNo = 1;
@@ -139,6 +146,7 @@
action: "deleteMonitorData", action: "deleteMonitorData",
data: { data: {
tag: monitorTagFilter === "ALL" ? "" : monitorTagFilter, tag: monitorTagFilter === "ALL" ? "" : monitorTagFilter,
status: statusFilter === "ALL" ? undefined : statusFilter,
start: startTs, start: startTs,
end: endTs end: endTs
} }
@@ -189,12 +197,14 @@
page: number; page: number;
limit: number; limit: number;
monitor_tag: string; monitor_tag: string;
status: StatusFilter;
start_time?: number; start_time?: number;
end_time?: number; end_time?: number;
} = { } = {
page: pageNo, page: pageNo,
limit, limit,
monitor_tag: monitorTagFilter monitor_tag: monitorTagFilter,
status: statusFilter
}; };
if (startDateTime) { if (startDateTime) {
@@ -241,6 +251,12 @@
} }
} }
function handleStatusChange(value: string | undefined) {
if (value === "ALL" || isMonitoringStatus(value)) {
statusFilter = value;
}
}
// Pagination // Pagination
function goToPage(page: number) { function goToPage(page: number) {
pageNo = page; pageNo = page;
@@ -304,6 +320,20 @@
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
</div> </div>
<div class="flex flex-col gap-1">
<span class="text-muted-foreground text-xs font-medium">Status</span>
<Select.Root type="single" value={statusFilter} onValueChange={handleStatusChange}>
<Select.Trigger class="w-36" aria-label="Status">
{statusFilter === "ALL" ? "All Statuses" : statusFilter}
</Select.Trigger>
<Select.Content>
<Select.Item value="ALL">All Statuses</Select.Item>
<Select.Item value={GC.UP}>UP</Select.Item>
<Select.Item value={GC.DOWN}>DOWN</Select.Item>
<Select.Item value={GC.DEGRADED}>DEGRADED</Select.Item>
</Select.Content>
</Select.Root>
</div>
<Button size="sm" onclick={applyFilters}> <Button size="sm" onclick={applyFilters}>
<SearchIcon class="size-4" /> <SearchIcon class="size-4" />
Search Search
@@ -437,11 +467,16 @@
<AlertDialog.Header> <AlertDialog.Header>
<AlertDialog.Title>Delete Monitoring Data</AlertDialog.Title> <AlertDialog.Title>Delete Monitoring Data</AlertDialog.Title>
<AlertDialog.Description> <AlertDialog.Description>
This will delete monitoring data for
{#if monitorTagFilter === "ALL"} {#if monitorTagFilter === "ALL"}
This will delete monitoring data for <strong>all monitors</strong> from {startDateTime} to {endDateTime}. <strong>all monitors</strong>
{:else} {:else}
This will delete monitoring data for <strong>{monitorTagFilter}</strong> from {startDateTime} to {endDateTime}. <strong>{monitorTagFilter}</strong>
{/if} {/if}
{#if statusFilter !== "ALL"}
with status <strong>{statusFilter}</strong>
{/if}
from {startDateTime} to {endDateTime}.
This action cannot be undone. This action cannot be undone.
</AlertDialog.Description> </AlertDialog.Description>
</AlertDialog.Header> </AlertDialog.Header>