Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94ffa9e930 | ||
|
|
491c0a9c7c | ||
|
|
1d51c5c887 | ||
|
|
2c84a4c9c2 | ||
|
|
6ab3054d9a | ||
|
|
7549658fbb |
@@ -169,7 +169,4 @@ body {
|
|||||||
grid-column: 1 / -1 !important;
|
grid-column: 1 / -1 !important;
|
||||||
grid-row: auto !important;
|
grid-row: auto !important;
|
||||||
}
|
}
|
||||||
.pn-live-label {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
|
import { GlobeSimple } from "@phosphor-icons/react/ssr";
|
||||||
import { BRAND_ICON_PATHS } from "@/lib/brand-icons";
|
import { BRAND_ICON_PATHS } from "@/lib/brand-icons";
|
||||||
|
|
||||||
|
// Generic (non-brand) icons that don't belong in BRAND_ICON_PATHS - for
|
||||||
|
// widgets that link to something with no single logo of its own.
|
||||||
|
const GENERIC_ICONS = {
|
||||||
|
globe: GlobeSimple,
|
||||||
|
};
|
||||||
|
|
||||||
export function BrandIcon({ slug, size = 18 }: { slug?: string; size?: number }) {
|
export function BrandIcon({ slug, size = 18 }: { slug?: string; size?: number }) {
|
||||||
if (!slug) return null;
|
if (!slug) return null;
|
||||||
|
|
||||||
|
const GenericIcon = GENERIC_ICONS[slug as keyof typeof GENERIC_ICONS];
|
||||||
|
if (GenericIcon) {
|
||||||
|
return <GenericIcon size={size} className="shrink-0 text-fg-muted" aria-hidden />;
|
||||||
|
}
|
||||||
|
|
||||||
const path = BRAND_ICON_PATHS[slug];
|
const path = BRAND_ICON_PATHS[slug];
|
||||||
if (!path) return null;
|
if (!path) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ type BookmarkWidget = Extract<Widget, { type: "bookmark" }>;
|
|||||||
export function BookmarkWidget({ widget }: { widget: BookmarkWidget }) {
|
export function BookmarkWidget({ widget }: { widget: BookmarkWidget }) {
|
||||||
if (widget.links && widget.links.length > 0) {
|
if (widget.links && widget.links.length > 0) {
|
||||||
return (
|
return (
|
||||||
<div className={PN_CARD_CLASS} style={{ gridColumn: "span 3" }}>
|
<div className={PN_CARD_CLASS} style={{ gridColumn: "span 4", gridRow: "span 2" }}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<BrandIcon slug={widget.icon} />
|
<BrandIcon slug={widget.icon} />
|
||||||
<span className="min-w-0 flex-1 truncate text-[15px] font-medium text-fg">{widget.name}</span>
|
<span className="min-w-0 flex-1 truncate text-[15px] font-medium text-fg">{widget.name}</span>
|
||||||
</div>
|
</div>
|
||||||
{widget.description && <span className="text-xs text-fg-muted">{widget.description}</span>}
|
{widget.description && <span className="text-xs text-fg-muted">{widget.description}</span>}
|
||||||
<div className="-mx-1 flex flex-col divide-y divide-border">
|
<div className="-mx-1 flex min-h-0 flex-1 flex-col divide-y divide-border overflow-y-auto">
|
||||||
{widget.links.map((link) => (
|
{widget.links.map((link) => (
|
||||||
<a
|
<a
|
||||||
key={link.href}
|
key={link.href}
|
||||||
|
|||||||
@@ -60,15 +60,6 @@ export function ServiceWidget({ widget, widgetId }: { widget: ServiceWidget; wid
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{data.detail && data.detail.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
|
||||||
{data.detail.map((entry) => (
|
|
||||||
<StatusTag key={entry} tone="neutral">
|
|
||||||
{entry}
|
|
||||||
</StatusTag>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{data.statError && <StatusTag tone="degraded">{data.statError}</StatusTag>}
|
{data.statError && <StatusTag tone="degraded">{data.statError}</StatusTag>}
|
||||||
{widget.showStats && data.docker.cpuPercent !== null && memPercent !== null && (
|
{widget.showStats && data.docker.cpuPercent !== null && memPercent !== null && (
|
||||||
<div className="mt-0.5 flex gap-4">
|
<div className="mt-0.5 flex gap-4">
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -9,7 +9,7 @@ import { collectUmami } from "./services/umami";
|
|||||||
import { collectHeadscale } from "./services/headscale";
|
import { collectHeadscale } from "./services/headscale";
|
||||||
import { collectTraefikStat } from "./services/traefik";
|
import { collectTraefikStat } from "./services/traefik";
|
||||||
|
|
||||||
function collectServiceStat(widget: ServiceWidget): Promise<{ stats: ServiceStat[]; detail?: string[] }> {
|
function collectServiceStat(widget: ServiceWidget): Promise<{ stats: ServiceStat[] }> {
|
||||||
switch (widget.service) {
|
switch (widget.service) {
|
||||||
case "gitea":
|
case "gitea":
|
||||||
return collectGitea(widget);
|
return collectGitea(widget);
|
||||||
|
|||||||
@@ -2,12 +2,6 @@ import type { CoolifyServiceWidget } from "@/lib/config/schema";
|
|||||||
import type { ServiceStat } from "@/lib/types/service-result";
|
import type { ServiceStat } from "@/lib/types/service-result";
|
||||||
import { serviceBaseUrl } from "./base-url";
|
import { serviceBaseUrl } from "./base-url";
|
||||||
|
|
||||||
const DETAIL_LIMIT = 5;
|
|
||||||
|
|
||||||
interface CoolifyProject {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchCoolify<T>(base: string, path: string, apiToken: string): Promise<T> {
|
async function fetchCoolify<T>(base: string, path: string, apiToken: string): Promise<T> {
|
||||||
const response = await fetch(`${base}${path}`, {
|
const response = await fetch(`${base}${path}`, {
|
||||||
headers: { Authorization: `Bearer ${apiToken}` },
|
headers: { Authorization: `Bearer ${apiToken}` },
|
||||||
@@ -19,22 +13,17 @@ async function fetchCoolify<T>(base: string, path: string, apiToken: string): Pr
|
|||||||
return (await response.json()) as T;
|
return (await response.json()) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function collectCoolify(
|
export async function collectCoolify(widget: CoolifyServiceWidget): Promise<{ stats: ServiceStat[] }> {
|
||||||
widget: CoolifyServiceWidget
|
|
||||||
): Promise<{ stats: ServiceStat[]; detail?: string[] }> {
|
|
||||||
const base = serviceBaseUrl(widget);
|
const base = serviceBaseUrl(widget);
|
||||||
const [projects, resources] = await Promise.all([
|
const [projects, resources] = await Promise.all([
|
||||||
fetchCoolify<CoolifyProject[]>(base, "/api/v1/projects", widget.apiToken),
|
fetchCoolify<unknown[]>(base, "/api/v1/projects", widget.apiToken),
|
||||||
fetchCoolify<unknown[]>(base, "/api/v1/resources", widget.apiToken),
|
fetchCoolify<unknown[]>(base, "/api/v1/resources", widget.apiToken),
|
||||||
]);
|
]);
|
||||||
const names = projects.slice(0, DETAIL_LIMIT).map((p) => p.name);
|
|
||||||
if (projects.length > DETAIL_LIMIT) names.push(`+${projects.length - DETAIL_LIMIT} more`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stats: [
|
stats: [
|
||||||
{ label: "Projects", value: String(projects.length) },
|
{ label: "Projects", value: String(projects.length) },
|
||||||
{ label: "Resources", value: String(resources.length) },
|
{ label: "Resources", value: String(resources.length) },
|
||||||
],
|
],
|
||||||
detail: names.length > 0 ? names : undefined,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,5 @@ export interface ServiceStat {
|
|||||||
export interface ServiceWidgetResult {
|
export interface ServiceWidgetResult {
|
||||||
docker: DockerContainerResult;
|
docker: DockerContainerResult;
|
||||||
stats: ServiceStat[];
|
stats: ServiceStat[];
|
||||||
detail?: string[];
|
|
||||||
statError?: string;
|
statError?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulsenode",
|
"name": "pulsenode",
|
||||||
"version": "0.4.4",
|
"version": "0.4.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch server.ts",
|
"dev": "tsx watch server.ts",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ ICONS = {
|
|||||||
"postgresql": "postgresql",
|
"postgresql": "postgresql",
|
||||||
"redis": "redis",
|
"redis": "redis",
|
||||||
"headscale": "tailscale",
|
"headscale": "tailscale",
|
||||||
|
"coder": "coder",
|
||||||
}
|
}
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|||||||
Reference in New Issue
Block a user