mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: finish the homepage plugin, move service links and canvas out of core (B19)
This commit is contained in:
1 parent
21937a6b02
commit
d4ec444558
256 files changed
+27365
-9077
No files matched your search
@@ -9,6 +9,7 @@ import {
|
||||
import { PRESETS, type UiPreset } from "@/types/ui-preferences";
|
||||
import { sanitizeHostSidebarPreferences } from "@/types/host-sidebar-preferences";
|
||||
import { sanitizeCredentialSidebarPreferences } from "@/types/credential-sidebar-preferences";
|
||||
import { getRegisteredDashboardCard } from "@/dashboard/dashboard-cards-registry";
|
||||
|
||||
/**
|
||||
* Seeding a preset into the stores that already own their settings.
|
||||
@@ -25,31 +26,36 @@ import { sanitizeCredentialSidebarPreferences } from "@/types/credential-sidebar
|
||||
* overwrites layouts the user may have arranged by hand.
|
||||
*/
|
||||
|
||||
const DASHBOARD_SLOT_HEIGHTS: Record<string, number | null> = {
|
||||
// Core's own cards. A plugin's card carries its own height and panel on its
|
||||
// registration (registerDashboardCard's defaultHeight/defaultPanel), read
|
||||
// below, so this preset never has to spell the plugin's card id itself.
|
||||
const CORE_DASHBOARD_SLOT_HEIGHTS: Record<string, number | null> = {
|
||||
stats_bar: 96,
|
||||
counters_bar: 48,
|
||||
quick_actions: 160,
|
||||
host_status: null,
|
||||
recent_activity: null,
|
||||
network_graph: 360,
|
||||
service_links: 200,
|
||||
homepage_preview: 320,
|
||||
};
|
||||
|
||||
/** Cards that belong in the narrower side column rather than the main one. */
|
||||
const DASHBOARD_SIDE_CARDS = new Set(["recent_activity", "service_links"]);
|
||||
/** Core cards that belong in the narrower side column rather than the main one. */
|
||||
const CORE_DASHBOARD_SIDE_CARDS = new Set(["recent_activity"]);
|
||||
|
||||
function buildDashboardSlots(cardIds: string[]) {
|
||||
export function buildDashboardSlots(cardIds: string[]) {
|
||||
let mainOrder = 0;
|
||||
let sideOrder = 0;
|
||||
return cardIds.map((id) => {
|
||||
const panel = DASHBOARD_SIDE_CARDS.has(id) ? "side" : "main";
|
||||
const registered = getRegisteredDashboardCard(id);
|
||||
const panel =
|
||||
registered?.defaultPanel ??
|
||||
(CORE_DASHBOARD_SIDE_CARDS.has(id) ? "side" : "main");
|
||||
const height =
|
||||
registered?.defaultHeight ?? CORE_DASHBOARD_SLOT_HEIGHTS[id] ?? null;
|
||||
return {
|
||||
key: `${id}_0`,
|
||||
id,
|
||||
panel,
|
||||
order: panel === "side" ? sideOrder++ : mainOrder++,
|
||||
height: DASHBOARD_SLOT_HEIGHTS[id] ?? null,
|
||||
height,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
export function normalizeServiceLinkUrl(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return "";
|
||||
return /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
|
||||
? trimmed
|
||||
: `http://${trimmed}`;
|
||||
}
|
||||
|
||||
export function isValidServiceLinkUrl(value: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(normalizeServiceLinkUrl(value));
|
||||
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -36,18 +36,6 @@ export const DASHBOARD_CARDS: DashboardCardConfig[] = [
|
||||
description: "Feed of recent connection events",
|
||||
defaultEnabled: true,
|
||||
},
|
||||
{
|
||||
id: "service_links",
|
||||
label: "Service Links",
|
||||
description: "Clickable buttons linking to services on your servers",
|
||||
defaultEnabled: false,
|
||||
},
|
||||
{
|
||||
id: "homepage_preview",
|
||||
label: "Homepage",
|
||||
description: "Scaled preview of your Homepage canvas",
|
||||
defaultEnabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const ACCENT_PRESET_COLORS = [
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* setInterval that pauses while the document is hidden and fires once on resume.
|
||||
* Returns a cleanup function for useEffect.
|
||||
*/
|
||||
export function runVisibleInterval(
|
||||
tick: () => void,
|
||||
intervalMs: number,
|
||||
): () => void {
|
||||
let intervalId: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const start = () => {
|
||||
if (intervalId !== null || intervalMs <= 0) return;
|
||||
intervalId = setInterval(() => {
|
||||
if (document.visibilityState === "hidden") return;
|
||||
tick();
|
||||
}, intervalMs);
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
if (intervalId === null) return;
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
};
|
||||
|
||||
const onVisibility = () => {
|
||||
if (document.visibilityState === "hidden") {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
tick();
|
||||
start();
|
||||
};
|
||||
|
||||
if (
|
||||
typeof document === "undefined" ||
|
||||
document.visibilityState !== "hidden"
|
||||
) {
|
||||
start();
|
||||
}
|
||||
if (typeof document !== "undefined") {
|
||||
document.addEventListener("visibilitychange", onVisibility);
|
||||
}
|
||||
|
||||
return () => {
|
||||
stop();
|
||||
if (typeof document !== "undefined") {
|
||||
document.removeEventListener("visibilitychange", onVisibility);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
interface WidgetTitleProps {
|
||||
title: string | null | undefined;
|
||||
icon?: React.ReactElement<LucideIcon>;
|
||||
fallback?: string;
|
||||
}
|
||||
|
||||
export function WidgetTitle({ title, icon, fallback }: WidgetTitleProps) {
|
||||
const label = title || fallback;
|
||||
if (!label) return null;
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 px-2 py-1.5 border-b border-border/50 shrink-0">
|
||||
{icon && <span className="text-accent-brand shrink-0">{icon}</span>}
|
||||
<span className="text-[10px] font-semibold text-muted-foreground uppercase tracking-wider truncate">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user