feat: implement position management for page monitors and update related functionality

This commit is contained in:
Raj Nandan Sharma
2026-03-13 09:40:54 +05:30
parent ba459e61ad
commit df94755c6b
10 changed files with 149 additions and 22 deletions
@@ -0,0 +1,19 @@
import type { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn("pages_monitors", "position");
if (!hasColumn) {
await knex.schema.alterTable("pages_monitors", (table) => {
table.integer("position").unsigned().notNullable().defaultTo(0);
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn("pages_monitors", "position");
if (hasColumn) {
await knex.schema.alterTable("pages_monitors", (table) => {
table.dropColumn("position");
});
}
}
+2
View File
@@ -30,6 +30,7 @@ export async function seed(knex: Knex): Promise<void> {
page_id: pageId,
monitor_tag: "earth",
monitor_settings_json: "",
position: 0,
created_at: knex.fn.now(),
updated_at: knex.fn.now(),
});
@@ -42,6 +43,7 @@ export async function seed(knex: Knex): Promise<void> {
page_id: pageId,
monitor_tag: "kener",
monitor_settings_json: "",
position: 1,
created_at: knex.fn.now(),
updated_at: knex.fn.now(),
});
@@ -107,6 +107,7 @@ export async function AddMonitorToPage(
page_id: number,
monitor_tag: string,
monitor_settings_json?: string | null,
position?: number,
): Promise<void> {
// Check if page exists
const page = await db.getPageById(page_id);
@@ -120,13 +121,38 @@ export async function AddMonitorToPage(
throw new Error(`Monitor "${monitor_tag}" already exists on this page`);
}
// If no position specified, append at end
let finalPosition = position;
if (finalPosition === undefined) {
const existing = await db.getPageMonitors(page_id);
finalPosition = existing.length > 0 ? Math.max(...existing.map((m) => m.position)) + 1 : 0;
}
await db.addMonitorToPage({
page_id,
monitor_tag,
monitor_settings_json: monitor_settings_json || null,
position: finalPosition,
});
}
/**
* Reorder monitors on a page
*/
export async function ReorderPageMonitors(page_id: number, monitor_tags: string[]): Promise<void> {
const page = await db.getPageById(page_id);
if (!page) {
throw new Error(`Page with id ${page_id} not found`);
}
const monitorPositions = monitor_tags.map((tag, index) => ({
monitor_tag: tag,
position: index,
}));
await db.updatePageMonitorPositions(page_id, monitorPositions);
}
/**
* Remove monitor from a page
*/
+2
View File
@@ -212,6 +212,7 @@ class DbImpl {
monitorExistsOnPage!: PagesRepository["monitorExistsOnPage"];
deletePageMonitorsByTag!: PagesRepository["deletePageMonitorsByTag"];
deletePageMonitorsByPageId!: PagesRepository["deletePageMonitorsByPageId"];
updatePageMonitorPositions!: PagesRepository["updatePageMonitorPositions"];
// ============ Maintenances ============
createMaintenance!: MaintenancesRepository["createMaintenance"];
@@ -554,6 +555,7 @@ class DbImpl {
this.monitorExistsOnPage = this.pages.monitorExistsOnPage.bind(this.pages);
this.deletePageMonitorsByTag = this.pages.deletePageMonitorsByTag.bind(this.pages);
this.deletePageMonitorsByPageId = this.pages.deletePageMonitorsByPageId.bind(this.pages);
this.updatePageMonitorPositions = this.pages.updatePageMonitorPositions.bind(this.pages);
}
private bindMaintenancesMethods(): void {
+16 -2
View File
@@ -65,6 +65,7 @@ export class PagesRepository extends BaseRepository {
page_id: data.page_id,
monitor_tag: data.monitor_tag,
monitor_settings_json: data.monitor_settings_json,
position: data.position ?? 0,
created_at: this.knex.fn.now(),
updated_at: this.knex.fn.now(),
});
@@ -75,7 +76,7 @@ export class PagesRepository extends BaseRepository {
}
async getPageMonitors(page_id: number): Promise<PageMonitorRecord[]> {
return await this.knex("pages_monitors").where("page_id", page_id).orderBy("created_at", "desc");
return await this.knex("pages_monitors").where("page_id", page_id).orderBy("position", "asc");
}
async getPageMonitorsExcludeHidden(page_id: number): Promise<PageMonitorRecord[]> {
@@ -84,7 +85,7 @@ export class PagesRepository extends BaseRepository {
.where("pages_monitors.page_id", page_id)
.andWhere("monitors.is_hidden", "NO")
.andWhere("monitors.status", "ACTIVE")
.orderBy("pages_monitors.created_at", "desc")
.orderBy("pages_monitors.position", "asc")
.select("pages_monitors.*");
}
@@ -115,4 +116,17 @@ export class PagesRepository extends BaseRepository {
async deletePageMonitorsByPageId(page_id: number): Promise<number> {
return await this.knex("pages_monitors").where({ page_id }).del();
}
async updatePageMonitorPositions(
page_id: number,
monitorPositions: { monitor_tag: string; position: number }[],
): Promise<void> {
await this.knex.transaction(async (trx) => {
for (const mp of monitorPositions) {
await trx("pages_monitors")
.where({ page_id, monitor_tag: mp.monitor_tag })
.update({ position: mp.position, updated_at: trx.fn.now() });
}
});
}
}
+3
View File
@@ -463,6 +463,7 @@ export interface PageMonitorRecord {
page_id: number;
monitor_tag: string;
monitor_settings_json: string | null;
position: number;
created_at: Date;
updated_at: Date;
}
@@ -471,12 +472,14 @@ export interface PageMonitorRecordInsert {
page_id: number;
monitor_tag: string;
monitor_settings_json?: string | null;
position?: number;
}
export interface PageMonitorRecordTyped {
page_id: number;
monitor_tag: string;
monitor_settings: Record<string, unknown> | null;
position: number;
created_at: Date;
updated_at: Date;
}
+4 -3
View File
@@ -105,7 +105,7 @@ async function formatPageResponse(page: PageRecord): Promise<PageResponse> {
page_subheader: page.page_subheader,
page_logo: page.page_logo,
page_settings: pageSettings,
monitors: pageMonitors.map((pm) => ({ monitor_tag: pm.monitor_tag })),
monitors: pageMonitors.map((pm) => ({ monitor_tag: pm.monitor_tag, position: pm.position })),
created_at: formatDateToISO(page.created_at),
updated_at: formatDateToISO(page.updated_at),
};
@@ -221,11 +221,12 @@ export const POST: RequestHandler = async ({ request }) => {
// Add monitors to the page
if (body.monitors && Array.isArray(body.monitors)) {
for (const monitorTag of body.monitors) {
for (let i = 0; i < body.monitors.length; i++) {
await db.addMonitorToPage({
page_id: createdPage.id,
monitor_tag: monitorTag,
monitor_tag: body.monitors[i],
monitor_settings_json: null,
position: i,
});
}
}
@@ -107,7 +107,7 @@ async function formatPageResponse(page: PageRecord): Promise<PageResponse> {
page_subheader: page.page_subheader,
page_logo: page.page_logo,
page_settings: pageSettings,
monitors: pageMonitors.map((pm) => ({ monitor_tag: pm.monitor_tag })),
monitors: pageMonitors.map((pm) => ({ monitor_tag: pm.monitor_tag, position: pm.position })),
created_at: formatDateToISO(page.created_at),
updated_at: formatDateToISO(page.updated_at),
};
@@ -291,11 +291,12 @@ export const PATCH: RequestHandler = async ({ locals, request }) => {
await db.deletePageMonitorsByPageId(page.id);
// Add new monitors
for (const monitorTag of body.monitors) {
for (let i = 0; i < body.monitors.length; i++) {
await db.addMonitorToPage({
page_id: page.id,
monitor_tag: monitorTag,
monitor_tag: body.monitors[i],
monitor_settings_json: null,
position: i,
});
}
}
@@ -61,6 +61,7 @@ import {
AddMonitorToPage,
RemoveMonitorFromPage,
GetPageMonitors,
ReorderPageMonitors,
} from "$lib/server/controllers/pagesController.js";
import {
CreateMaintenance,
@@ -409,6 +410,10 @@ export async function POST({ request, cookies }) {
AdminEditorCan(userDB.role);
await RemoveMonitorFromPage(data.page_id, data.monitor_tag);
resp = { success: true };
} else if (action == "reorderPageMonitors") {
AdminEditorCan(userDB.role);
await ReorderPageMonitors(data.page_id, data.monitor_tags);
resp = { success: true };
}
// ============ Maintenance Actions ============
else if (action == "getMaintenances") {
@@ -14,6 +14,8 @@
import SaveIcon from "@lucide/svelte/icons/save";
import PlusIcon from "@lucide/svelte/icons/plus";
import XIcon from "@lucide/svelte/icons/x";
import ArrowUpIcon from "@lucide/svelte/icons/arrow-up";
import ArrowDownIcon from "@lucide/svelte/icons/arrow-down";
import UploadIcon from "@lucide/svelte/icons/upload";
import ImageIcon from "@lucide/svelte/icons/image";
import TrashIcon from "@lucide/svelte/icons/trash";
@@ -68,6 +70,7 @@
let selectedMonitors = $state<string[]>([]);
let addingMonitor = $state(false);
let removingMonitor = $state<string | null>(null);
let reordering = $state(false);
// Delete state
let deleteConfirmText = $state("");
@@ -231,7 +234,7 @@
toast.error(result.error);
} else {
toast.success("Monitor added to page");
selectedMonitors = [selectedMonitorTag, ...selectedMonitors.filter((tag) => tag !== selectedMonitorTag)];
selectedMonitors = [...selectedMonitors, selectedMonitorTag];
selectedMonitorTag = "";
}
} catch (e) {
@@ -303,6 +306,39 @@
// Get available monitors (not already on the current page)
const availableMonitors = $derived(monitors.filter((m) => !selectedMonitors.includes(m.tag)));
async function moveMonitor(index: number, direction: "up" | "down") {
const newIndex = direction === "up" ? index - 1 : index + 1;
if (newIndex < 0 || newIndex >= selectedMonitors.length) return;
const updated = [...selectedMonitors];
[updated[index], updated[newIndex]] = [updated[newIndex], updated[index]];
selectedMonitors = updated;
if (!currentPage) return;
reordering = true;
try {
const response = await fetch(clientResolver(resolve, "/manage/api"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "reorderPageMonitors",
data: {
page_id: currentPage.id,
monitor_tags: selectedMonitors
}
})
});
const result = await response.json();
if (result.error) {
toast.error(result.error);
}
} catch (e) {
toast.error("Failed to reorder monitors");
} finally {
reordering = false;
}
}
// Image upload functions
async function handleLogoUpload(event: Event): Promise<void> {
const input = event.target as HTMLInputElement;
@@ -614,25 +650,43 @@
<Label>Current Monitors</Label>
{#if selectedMonitors.length > 0}
<div class="space-y-2">
{#each selectedMonitors as monitorTag (monitorTag)}
{#each selectedMonitors as monitorTag, i (monitorTag)}
{@const monitor = monitors.find((m) => m.tag === monitorTag)}
<div class="bg-muted flex items-center justify-between rounded-lg p-3">
<div>
<p class="font-medium">{monitor?.name || monitorTag}</p>
<p class="text-muted-foreground text-xs">{monitorTag}</p>
</div>
<Button
variant="ghost"
size="sm"
onclick={() => removeMonitorFromPage(monitorTag)}
disabled={removingMonitor === monitorTag}
>
{#if removingMonitor === monitorTag}
<Loader class="h-4 w-4 animate-spin" />
{:else}
<XIcon class="h-4 w-4" />
{/if}
</Button>
<div class="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
onclick={() => moveMonitor(i, "up")}
disabled={i === 0 || reordering}
>
<ArrowUpIcon class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onclick={() => moveMonitor(i, "down")}
disabled={i === selectedMonitors.length - 1 || reordering}
>
<ArrowDownIcon class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onclick={() => removeMonitorFromPage(monitorTag)}
disabled={removingMonitor === monitorTag}
>
{#if removingMonitor === monitorTag}
<Loader class="h-4 w-4 animate-spin" />
{:else}
<XIcon class="h-4 w-4" />
{/if}
</Button>
</div>
</div>
{/each}
</div>