mirror of
https://github.com/kunkunsh/kunkun-ext-neohtop.git
synced 2025-06-06 02:25:02 +00:00
ui refactoring
This commit is contained in:
parent
cb07a3bfcf
commit
d464775c4a
@ -1,430 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
faThumbtack,
|
||||
faInfoCircle,
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import Fa from "svelte-fa";
|
||||
import type { Process, Column } from "$lib/types";
|
||||
import * as SimpleIcons from "simple-icons";
|
||||
|
||||
export let processes: Process[];
|
||||
export let columns: Column[];
|
||||
export let systemStats: { memory_total: number } | null;
|
||||
export let sortConfig: { field: keyof Process; direction: "asc" | "desc" };
|
||||
export let pinnedProcesses: Set<string>;
|
||||
|
||||
export let onToggleSort: (field: keyof Process) => void;
|
||||
export let onTogglePin: (command: string) => void;
|
||||
export let onShowDetails: (process: Process) => void;
|
||||
export let onKillProcess: (process: Process) => void;
|
||||
|
||||
function getSortIndicator(field: keyof Process) {
|
||||
if (sortConfig.field !== field) return "↕";
|
||||
return sortConfig.direction === "asc" ? "↑" : "↓";
|
||||
}
|
||||
|
||||
function handleImageError(event: Event) {
|
||||
const img = event.target as HTMLImageElement;
|
||||
img.src = getIconForProcess("default"); // Fallback to default icon
|
||||
img.onerror = null; // Prevent infinite loop if default icon also fails
|
||||
}
|
||||
|
||||
function getIconForProcess(name: string): string {
|
||||
// First try with com.company.something pattern
|
||||
if (name.startsWith("com.")) {
|
||||
const companyName = name.replace(/^com\.([^.]+)\..*$/, "$1");
|
||||
const formattedCompanyName =
|
||||
companyName.charAt(0).toUpperCase() + companyName.slice(1);
|
||||
const companyIconKey = `si${formattedCompanyName}`;
|
||||
const companyIcon =
|
||||
SimpleIcons[companyIconKey as keyof typeof SimpleIcons];
|
||||
|
||||
if (companyIcon) {
|
||||
// Use theme color instead of brand color
|
||||
const color = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--text")
|
||||
.trim();
|
||||
const svg =
|
||||
typeof companyIcon === "object" && "svg" in companyIcon
|
||||
? companyIcon.svg
|
||||
: "";
|
||||
const svgWithColor = svg.replace("<svg", `<svg fill="${color}"`);
|
||||
return `data:image/svg+xml;base64,${btoa(svgWithColor)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// If no company icon found, fall back to original implementation
|
||||
const cleanName = name
|
||||
.replace(/\.(app|exe)$/i, "")
|
||||
.replace(/[-_./\\]/g, " ")
|
||||
.split(" ")[0]
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
const formattedName =
|
||||
cleanName.charAt(0).toUpperCase() + cleanName.slice(1);
|
||||
const iconKey = `si${formattedName}`;
|
||||
let simpleIcon = SimpleIcons[iconKey as keyof typeof SimpleIcons];
|
||||
|
||||
// Default icon if no match found
|
||||
if (!simpleIcon) {
|
||||
simpleIcon = SimpleIcons.siGhostery;
|
||||
}
|
||||
|
||||
// Use theme color instead of brand color
|
||||
const color = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--text")
|
||||
.trim();
|
||||
|
||||
const svg =
|
||||
typeof simpleIcon === "object" && "svg" in simpleIcon
|
||||
? simpleIcon.svg
|
||||
: "";
|
||||
const svgWithColor = svg.replace("<svg", `<svg fill="${color}"`);
|
||||
return `data:image/svg+xml;base64,${btoa(svgWithColor)}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{#each columns.filter((col) => col.visible) as column}
|
||||
<th class="sortable" on:click={() => onToggleSort(column.id)}>
|
||||
<div class="th-content">
|
||||
{column.label}
|
||||
<span
|
||||
class="sort-indicator"
|
||||
class:active={sortConfig.field === column.id}
|
||||
>
|
||||
{getSortIndicator(column.id)}
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
{/each}
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each processes as process (process.pid)}
|
||||
<tr
|
||||
class:high-usage={process.cpu_usage > 50 ||
|
||||
process.memory_usage / (systemStats?.memory_total || 0) > 0.1}
|
||||
class:pinned={pinnedProcesses.has(process.command)}
|
||||
>
|
||||
{#each columns.filter((col) => col.visible) as column}
|
||||
<td class="truncate">
|
||||
{#if column.id === "name"}
|
||||
<div class="name-cell">
|
||||
<img
|
||||
class="process-icon"
|
||||
src={getIconForProcess(process.name)}
|
||||
alt=""
|
||||
height="16"
|
||||
width="16"
|
||||
on:error={handleImageError}
|
||||
/>
|
||||
<span class="process-name">{process.name}</span>
|
||||
</div>
|
||||
{:else if column.format}
|
||||
{@html column.format(process[column.id])}
|
||||
{:else}
|
||||
{process[column.id]}
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
<td class="col-actions">
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
class="btn-action pin-btn"
|
||||
class:pinned={pinnedProcesses.has(process.command)}
|
||||
on:click={() => onTogglePin(process.command)}
|
||||
title={pinnedProcesses.has(process.command) ? "Unpin" : "Pin"}
|
||||
>
|
||||
<Fa icon={faThumbtack} />
|
||||
</button>
|
||||
<button
|
||||
class="btn-action info-btn"
|
||||
on:click={() => onShowDetails(process)}
|
||||
title="Show Details"
|
||||
>
|
||||
<Fa icon={faInfoCircle} />
|
||||
</button>
|
||||
<button
|
||||
class="btn-action kill-btn"
|
||||
on:click={() => onKillProcess(process)}
|
||||
title="End Process"
|
||||
>
|
||||
<Fa icon={faXmark} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.table-container {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
|
||||
/* Scrollbar styles */
|
||||
scrollbar-width: thin; /* Firefox */
|
||||
scrollbar-color: var(--surface2) var(--mantle); /* Firefox */
|
||||
}
|
||||
|
||||
/* Webkit scrollbar styles (Chrome, Safari, Edge) */
|
||||
.table-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-track {
|
||||
background: var(--mantle);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-thumb {
|
||||
background: var(--surface2);
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--surface1);
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-corner {
|
||||
background: var(--mantle);
|
||||
}
|
||||
|
||||
/* When both scrollbars are present, add some padding to prevent overlap */
|
||||
.table-container::-webkit-scrollbar-corner {
|
||||
background-color: var(--mantle);
|
||||
}
|
||||
|
||||
table {
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--mantle);
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
font-weight: 500;
|
||||
color: var(--subtext0);
|
||||
border-bottom: 1px solid var(--surface0);
|
||||
transition: background-color 0.2s ease;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 6px 12px;
|
||||
border-bottom: 1px solid var(--surface0);
|
||||
color: var(--text);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: var(--surface0);
|
||||
}
|
||||
|
||||
.truncate {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 0;
|
||||
}
|
||||
|
||||
.sortable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.th-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sort-indicator {
|
||||
color: var(--overlay0);
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sort-indicator.active {
|
||||
color: var(--blue);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sortable:hover .sort-indicator {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.high-usage {
|
||||
background-color: color-mix(in srgb, var(--red) 10%, transparent);
|
||||
}
|
||||
|
||||
.high-usage:hover {
|
||||
background-color: color-mix(in srgb, var(--red) 15%, transparent);
|
||||
}
|
||||
|
||||
tr.pinned {
|
||||
background-color: color-mix(in srgb, var(--blue) 10%, transparent);
|
||||
}
|
||||
|
||||
tr.pinned:hover {
|
||||
background-color: color-mix(in srgb, var(--blue) 15%, transparent);
|
||||
}
|
||||
|
||||
th:last-child {
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
max-width: 120px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.col-actions {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
background: var(--base);
|
||||
border-left: 1px solid var(--surface0);
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-action::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.1;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-action:hover::before {
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
.pin-btn {
|
||||
color: var(--sapphire);
|
||||
}
|
||||
|
||||
.pin-btn::before {
|
||||
background: var(--sapphire);
|
||||
}
|
||||
|
||||
.pin-btn.pinned {
|
||||
color: var(--blue);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.pin-btn.pinned::before {
|
||||
background: var(--blue);
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
.info-btn {
|
||||
color: var(--lavender);
|
||||
}
|
||||
|
||||
.info-btn::before {
|
||||
background: var(--lavender);
|
||||
}
|
||||
|
||||
.kill-btn {
|
||||
color: var(--red);
|
||||
border: 1px solid color-mix(in srgb, var(--red) 30%, transparent);
|
||||
}
|
||||
|
||||
.kill-btn:hover {
|
||||
color: var(--base);
|
||||
background: var(--red);
|
||||
}
|
||||
|
||||
.kill-btn:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-action:hover {
|
||||
box-shadow: 0 0 12px color-mix(in srgb, currentColor 20%, transparent);
|
||||
}
|
||||
|
||||
.btn-action:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.pin-btn.pinned:active {
|
||||
transform: rotate(45deg) translateY(1px);
|
||||
}
|
||||
|
||||
.btn-action:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 30%, transparent);
|
||||
}
|
||||
|
||||
.btn-action:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-action:disabled:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-action:disabled::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.process-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
@ -1,461 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Fa from "svelte-fa";
|
||||
import {
|
||||
faMicrochip,
|
||||
faMemory,
|
||||
faServer,
|
||||
faNetworkWired,
|
||||
faHardDrive,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import type { SystemStats } from "$lib/types";
|
||||
import {
|
||||
formatUptime,
|
||||
formatMemorySize,
|
||||
formatPercentage,
|
||||
getUsageClass,
|
||||
} from "$lib/utils";
|
||||
|
||||
export let systemStats: SystemStats | null = null;
|
||||
$: memoryPercentage = systemStats
|
||||
? (systemStats.memory_used / systemStats.memory_total) * 100
|
||||
: 0;
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let value = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (value >= 1024 && unitIndex < units.length - 1) {
|
||||
value /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${value.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
$: diskUsagePercentage = systemStats
|
||||
? (systemStats.disk_used_bytes / systemStats.disk_total_bytes) * 100
|
||||
: 0;
|
||||
</script>
|
||||
|
||||
<div class="dashboard-stats">
|
||||
{#if systemStats}
|
||||
<div class="stats-layout">
|
||||
<!-- CPU Panel -->
|
||||
<div class="stat-panel">
|
||||
<div class="panel-header">
|
||||
<Fa icon={faMicrochip} />
|
||||
<h3>CPU Usage</h3>
|
||||
<div class="usage-pill">
|
||||
{formatPercentage(
|
||||
systemStats.cpu_usage.reduce((a, b) => a + b, 0) /
|
||||
systemStats.cpu_usage.length,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-content cpu-grid">
|
||||
{#each systemStats.cpu_usage as usage, i}
|
||||
<div class="stat-item with-progress">
|
||||
<div class="progress-container">
|
||||
<span class="label">Core {i}</span>
|
||||
<div class="bar-container">
|
||||
<div
|
||||
class="usage-bar {getUsageClass(usage)}"
|
||||
style="transform: translateX({usage - 100}%);"
|
||||
></div>
|
||||
</div>
|
||||
<span class="value">{Math.round(usage)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Memory Panel -->
|
||||
<div class="stat-panel">
|
||||
<div class="panel-header">
|
||||
<Fa icon={faMemory} />
|
||||
<h3>Memory</h3>
|
||||
<div class="usage-pill">{formatPercentage(memoryPercentage)}</div>
|
||||
</div>
|
||||
<div class="stats-content">
|
||||
<div class="stat-item with-progress">
|
||||
<div class="memory-progress-container">
|
||||
<span class="label">Memory usage</span>
|
||||
<div class="bar-container">
|
||||
<div
|
||||
class="usage-bar {getUsageClass(memoryPercentage)}"
|
||||
style="transform: translateX({memoryPercentage - 100}%);"
|
||||
></div>
|
||||
</div>
|
||||
<span class="value">{formatPercentage(memoryPercentage)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span>Total</span>
|
||||
<span>{formatMemorySize(systemStats.memory_total)}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span>Used</span>
|
||||
<span>{formatMemorySize(systemStats.memory_used)}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span>Free</span>
|
||||
<span>{formatMemorySize(systemStats.memory_free)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Storage Panel -->
|
||||
<div class="stat-panel">
|
||||
<div class="panel-header">
|
||||
<Fa icon={faHardDrive} />
|
||||
<h3>Storage</h3>
|
||||
<div class="usage-pill">
|
||||
{formatPercentage(diskUsagePercentage)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-content">
|
||||
<div class="stat-item">
|
||||
<span>Total</span>
|
||||
<span>{formatBytes(systemStats.disk_total_bytes)}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span>Used</span>
|
||||
<span>{formatBytes(systemStats.disk_used_bytes)}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span>Free</span>
|
||||
<span>{formatBytes(systemStats.disk_free_bytes)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Panel -->
|
||||
<div class="stat-panel">
|
||||
<div class="panel-header">
|
||||
<Fa icon={faServer} />
|
||||
<h3>System</h3>
|
||||
</div>
|
||||
<div class="system-grid">
|
||||
<div class="stat-item">
|
||||
<span>Uptime</span>
|
||||
<span>{formatUptime(systemStats.uptime)}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span>1m Load</span>
|
||||
<span>{systemStats.load_avg[0].toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span>5m Load</span>
|
||||
<span>{systemStats.load_avg[1].toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span>15m Load</span>
|
||||
<span>{systemStats.load_avg[2].toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Panel -->
|
||||
<div class="stat-panel">
|
||||
<div class="panel-header">
|
||||
<Fa icon={faNetworkWired} />
|
||||
<h3>Network I/O</h3>
|
||||
</div>
|
||||
<div class="network-stats">
|
||||
<div class="stat-item">
|
||||
<span>↓ Receiving</span>
|
||||
<span>{formatBytes(systemStats.network_rx_bytes)}/s</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span>↑ Sending</span>
|
||||
<span>{formatBytes(systemStats.network_tx_bytes)}/s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dashboard-stats {
|
||||
padding: 0.5rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.stats-layout {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stat-panel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background-color: var(--mantle);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
/* box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--surface0);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.panel-header :global(svg) {
|
||||
color: var(--blue);
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
}
|
||||
|
||||
.usage-pill {
|
||||
margin-left: auto;
|
||||
background: var(--surface0);
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cpu-layout {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
overflow-y: auto;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.cpu-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.cpu-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.bar-container {
|
||||
height: 8px;
|
||||
background: var(--surface0);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bar-container.main-bar {
|
||||
height: 6px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.usage-bar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
transition: transform 0.3s ease;
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
.usage-bar.low {
|
||||
background: var(--blue);
|
||||
}
|
||||
.usage-bar.medium {
|
||||
background: var(--yellow);
|
||||
}
|
||||
.usage-bar.high {
|
||||
background: var(--peach);
|
||||
}
|
||||
.usage-bar.critical {
|
||||
background: var(--red);
|
||||
}
|
||||
|
||||
.memory-grid,
|
||||
.disk-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.system-grid,
|
||||
.network-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.load-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.network-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.stat-item span:first-child {
|
||||
color: var(--subtext0);
|
||||
}
|
||||
|
||||
.stat-item span:last-child {
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.network-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.stat-item.with-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.7rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 4rem 1fr 3rem;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.memory-progress-container {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 5rem 1fr 2.5rem;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.memory-progress-container .label {
|
||||
color: var(--subtext0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.memory-progress-container .value {
|
||||
color: var(--text);
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.progress-container .label {
|
||||
color: var(--subtext0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.progress-container .value {
|
||||
color: var(--text);
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bar-container {
|
||||
height: 8px;
|
||||
background: var(--surface0);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.usage-bar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
transition: transform 0.3s ease;
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
.cpu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.4rem 2rem;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 2.5rem 1fr 2.5rem;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
/* Make CPU panel take more space */
|
||||
.stat-panel:first-child {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
/* Customize flex distribution for each panel */
|
||||
.stat-panel:nth-child(1) {
|
||||
flex: 2.5; /* CPU: more space for 2 columns */
|
||||
}
|
||||
.stat-panel:nth-child(2) {
|
||||
flex: 2; /* Memory: more space for details */
|
||||
}
|
||||
.stat-panel:nth-child(3),
|
||||
.stat-panel:nth-child(4),
|
||||
.stat-panel:nth-child(5) {
|
||||
flex: 0.8; /* Storage, System, and Network: less space */
|
||||
min-width: 125px;
|
||||
}
|
||||
</style>
|
147
src/lib/components/process/ActionButtons.svelte
Normal file
147
src/lib/components/process/ActionButtons.svelte
Normal file
@ -0,0 +1,147 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
faThumbtack,
|
||||
faInfoCircle,
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import Fa from "svelte-fa";
|
||||
import type { Process } from "$lib/types";
|
||||
|
||||
export let process: Process;
|
||||
export let isPinned: boolean;
|
||||
export let onTogglePin: (command: string) => void;
|
||||
export let onShowDetails: (process: Process) => void;
|
||||
export let onKillProcess: (process: Process) => void;
|
||||
</script>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
class="btn-action pin-btn"
|
||||
class:pinned={isPinned}
|
||||
on:click={() => onTogglePin(process.command)}
|
||||
title={isPinned ? "Unpin" : "Pin"}
|
||||
>
|
||||
<Fa icon={faThumbtack} />
|
||||
</button>
|
||||
<button
|
||||
class="btn-action info-btn"
|
||||
on:click={() => onShowDetails(process)}
|
||||
title="Show Details"
|
||||
>
|
||||
<Fa icon={faInfoCircle} />
|
||||
</button>
|
||||
<button
|
||||
class="btn-action kill-btn"
|
||||
on:click={() => onKillProcess(process)}
|
||||
title="End Process"
|
||||
>
|
||||
<Fa icon={faXmark} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-action::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.1;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-action:hover::before {
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
.pin-btn {
|
||||
color: var(--sapphire);
|
||||
}
|
||||
|
||||
.pin-btn::before {
|
||||
background: var(--sapphire);
|
||||
}
|
||||
|
||||
.pin-btn.pinned {
|
||||
color: var(--blue);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.pin-btn.pinned::before {
|
||||
background: var(--blue);
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
.info-btn {
|
||||
color: var(--lavender);
|
||||
}
|
||||
|
||||
.info-btn::before {
|
||||
background: var(--lavender);
|
||||
}
|
||||
|
||||
.kill-btn {
|
||||
color: var(--red);
|
||||
border: 1px solid color-mix(in srgb, var(--red) 30%, transparent);
|
||||
}
|
||||
|
||||
.kill-btn:hover {
|
||||
color: var(--base);
|
||||
background: var(--red);
|
||||
}
|
||||
|
||||
.kill-btn:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-action:hover {
|
||||
box-shadow: 0 0 12px color-mix(in srgb, currentColor 20%, transparent);
|
||||
}
|
||||
|
||||
.btn-action:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.pin-btn.pinned:active {
|
||||
transform: rotate(45deg) translateY(1px);
|
||||
}
|
||||
|
||||
.btn-action:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 30%, transparent);
|
||||
}
|
||||
|
||||
.btn-action:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-action:disabled:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-action:disabled::before {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
70
src/lib/components/process/ProcessIcon.svelte
Normal file
70
src/lib/components/process/ProcessIcon.svelte
Normal file
@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import * as SimpleIcons from "simple-icons";
|
||||
|
||||
export let processName: string;
|
||||
|
||||
function handleImageError(event: Event) {
|
||||
const img = event.target as HTMLImageElement;
|
||||
img.src = getIconForProcess("default");
|
||||
img.onerror = null;
|
||||
}
|
||||
|
||||
function getIconForProcess(name: string): string {
|
||||
// First try with com.company.something pattern
|
||||
if (name.startsWith("com.")) {
|
||||
const companyName = name.replace(/^com\.([^.]+)\..*$/, "$1");
|
||||
const formattedCompanyName =
|
||||
companyName.charAt(0).toUpperCase() + companyName.slice(1);
|
||||
const companyIconKey = `si${formattedCompanyName}`;
|
||||
const companyIcon =
|
||||
SimpleIcons[companyIconKey as keyof typeof SimpleIcons];
|
||||
|
||||
if (companyIcon) {
|
||||
return createSvgDataUrl(companyIcon);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to process name icon
|
||||
const cleanName = name
|
||||
.replace(/\.(app|exe)$/i, "")
|
||||
.replace(/[-_./\\]/g, " ")
|
||||
.split(" ")[0]
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
const formattedName =
|
||||
cleanName.charAt(0).toUpperCase() + cleanName.slice(1);
|
||||
const iconKey = `si${formattedName}`;
|
||||
const simpleIcon =
|
||||
SimpleIcons[iconKey as keyof typeof SimpleIcons] ||
|
||||
SimpleIcons.siGhostery;
|
||||
|
||||
return createSvgDataUrl(simpleIcon);
|
||||
}
|
||||
|
||||
function createSvgDataUrl(icon: any): string {
|
||||
const color = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue("--text")
|
||||
.trim();
|
||||
const svg = typeof icon === "object" && "svg" in icon ? icon.svg : "";
|
||||
const svgWithColor = svg.replace("<svg", `<svg fill="${color}"`);
|
||||
return `data:image/svg+xml;base64,${btoa(svgWithColor)}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<img
|
||||
class="process-icon"
|
||||
src={getIconForProcess(processName)}
|
||||
alt=""
|
||||
height="16"
|
||||
width="16"
|
||||
on:error={handleImageError}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.process-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
60
src/lib/components/process/ProcessRow.svelte
Normal file
60
src/lib/components/process/ProcessRow.svelte
Normal file
@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import type { Process, Column } from "$lib/types";
|
||||
import ProcessCell from "./cells/ProcessCell.svelte";
|
||||
import ActionButtons from "./ActionButtons.svelte";
|
||||
|
||||
export let process: Process;
|
||||
export let columns: Column[];
|
||||
export let isPinned: boolean;
|
||||
export let isHighUsage: boolean;
|
||||
export let onTogglePin: (command: string) => void;
|
||||
export let onShowDetails: (process: Process) => void;
|
||||
export let onKillProcess: (process: Process) => void;
|
||||
</script>
|
||||
|
||||
<tr class:high-usage={isHighUsage} class:pinned={isPinned}>
|
||||
{#each columns.filter((col) => col.visible) as column}
|
||||
<ProcessCell {process} field={column.id} format={column.format} />
|
||||
{/each}
|
||||
<td class="col-actions">
|
||||
<ActionButtons
|
||||
{process}
|
||||
{isPinned}
|
||||
{onTogglePin}
|
||||
{onShowDetails}
|
||||
{onKillProcess}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<style>
|
||||
tr:hover {
|
||||
background-color: var(--surface0);
|
||||
}
|
||||
|
||||
.high-usage {
|
||||
background-color: color-mix(in srgb, var(--red) 10%, transparent);
|
||||
}
|
||||
|
||||
.high-usage:hover {
|
||||
background-color: color-mix(in srgb, var(--red) 15%, transparent);
|
||||
}
|
||||
|
||||
tr.pinned {
|
||||
background-color: color-mix(in srgb, var(--blue) 10%, transparent);
|
||||
}
|
||||
|
||||
tr.pinned:hover {
|
||||
background-color: color-mix(in srgb, var(--blue) 15%, transparent);
|
||||
}
|
||||
|
||||
.col-actions {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
background: var(--base);
|
||||
border-left: 1px solid var(--surface0);
|
||||
width: 120px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
</style>
|
86
src/lib/components/process/ProcessTable.svelte
Normal file
86
src/lib/components/process/ProcessTable.svelte
Normal file
@ -0,0 +1,86 @@
|
||||
<script lang="ts">
|
||||
import type { Process, Column } from "$lib/types";
|
||||
import TableHeader from "./TableHeader.svelte";
|
||||
import ProcessRow from "./ProcessRow.svelte";
|
||||
|
||||
export let processes: Process[];
|
||||
export let columns: Column[];
|
||||
export let systemStats: { memory_total: number } | null;
|
||||
export let sortConfig: { field: keyof Process; direction: "asc" | "desc" };
|
||||
export let pinnedProcesses: Set<string>;
|
||||
|
||||
export let onToggleSort: (field: keyof Process) => void;
|
||||
export let onTogglePin: (command: string) => void;
|
||||
export let onShowDetails: (process: Process) => void;
|
||||
export let onKillProcess: (process: Process) => void;
|
||||
</script>
|
||||
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<TableHeader {columns} {sortConfig} {onToggleSort} />
|
||||
<tbody>
|
||||
{#each processes as process (process.pid)}
|
||||
<ProcessRow
|
||||
{process}
|
||||
{columns}
|
||||
isPinned={pinnedProcesses.has(process.command)}
|
||||
isHighUsage={process.cpu_usage > 50 ||
|
||||
process.memory_usage / (systemStats?.memory_total || 0) > 0.1}
|
||||
{onTogglePin}
|
||||
{onShowDetails}
|
||||
{onKillProcess}
|
||||
/>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.table-container {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
|
||||
/* Scrollbar styles */
|
||||
scrollbar-width: thin; /* Firefox */
|
||||
scrollbar-color: var(--surface2) var(--mantle); /* Firefox */
|
||||
}
|
||||
|
||||
/* Webkit scrollbar styles (Chrome, Safari, Edge) */
|
||||
.table-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-track {
|
||||
background: var(--mantle);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-thumb {
|
||||
background: var(--surface2);
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--surface1);
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-corner {
|
||||
background: var(--mantle);
|
||||
}
|
||||
|
||||
/* When both scrollbars are present, add some padding to prevent overlap */
|
||||
.table-container::-webkit-scrollbar-corner {
|
||||
background-color: var(--mantle);
|
||||
}
|
||||
|
||||
table {
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
136
src/lib/components/process/TableHeader.svelte
Normal file
136
src/lib/components/process/TableHeader.svelte
Normal file
@ -0,0 +1,136 @@
|
||||
<script lang="ts">
|
||||
import type { Process, Column } from "$lib/types";
|
||||
|
||||
export let columns: Column[];
|
||||
export let sortConfig: { field: keyof Process; direction: "asc" | "desc" };
|
||||
export let onToggleSort: (field: keyof Process) => void;
|
||||
|
||||
function getSortIndicator(field: keyof Process) {
|
||||
if (sortConfig.field !== field) return "↕";
|
||||
return sortConfig.direction === "asc" ? "↑" : "↓";
|
||||
}
|
||||
</script>
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
{#each columns.filter((col) => col.visible) as column}
|
||||
<th
|
||||
class="sortable"
|
||||
data-column={column.id}
|
||||
on:click={() => onToggleSort(column.id)}
|
||||
>
|
||||
<div class="th-content">
|
||||
{column.label}
|
||||
<span
|
||||
class="sort-indicator"
|
||||
class:active={sortConfig.field === column.id}
|
||||
>
|
||||
{getSortIndicator(column.id)}
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
{/each}
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<style>
|
||||
th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--mantle);
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
font-weight: 500;
|
||||
color: var(--subtext0);
|
||||
border-bottom: 1px solid var(--surface0);
|
||||
transition: background-color 0.2s ease;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
th:last-child {
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.sortable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.th-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sort-indicator {
|
||||
color: var(--overlay0);
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sort-indicator.active {
|
||||
color: var(--blue);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sortable:hover .sort-indicator {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Column-specific widths */
|
||||
th[data-column="name"] {
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
th[data-column="pid"] {
|
||||
width: 70px;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
th[data-column="status"] {
|
||||
width: 90px;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
th[data-column="user"] {
|
||||
width: 100px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
th[data-column="cpu_usage"] {
|
||||
width: 80px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
th[data-column="memory_usage"],
|
||||
th[data-column="virtual_memory"] {
|
||||
width: 90px;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
th[data-column="disk_usage"] {
|
||||
width: 100px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
th[data-column="command"],
|
||||
th[data-column="environ"] {
|
||||
width: 200px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
th[data-column="start_time"] {
|
||||
width: 150px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
th[data-column="run_time"] {
|
||||
width: 100px;
|
||||
min-width: 100px;
|
||||
}
|
||||
</style>
|
289
src/lib/components/process/cells/ProcessCell.svelte
Normal file
289
src/lib/components/process/cells/ProcessCell.svelte
Normal file
@ -0,0 +1,289 @@
|
||||
<script lang="ts">
|
||||
import type { Process } from "$lib/types";
|
||||
import ProcessIcon from "../ProcessIcon.svelte";
|
||||
|
||||
export let process: Process;
|
||||
export let field: keyof Process;
|
||||
export let format: ((value: any) => string) | undefined = undefined;
|
||||
|
||||
function formatValue(value: any): string | undefined {
|
||||
if (format) {
|
||||
return format(value);
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<td class="truncate" data-column={field}>
|
||||
{#if field === "name"}
|
||||
<div class="name-cell">
|
||||
<ProcessIcon processName={process.name} />
|
||||
<span class="process-name">{process.name}</span>
|
||||
</div>
|
||||
{:else}
|
||||
{formatValue(process[field])}
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<style>
|
||||
.table-container {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
|
||||
/* Scrollbar styles */
|
||||
scrollbar-width: thin; /* Firefox */
|
||||
scrollbar-color: var(--surface2) var(--mantle); /* Firefox */
|
||||
}
|
||||
|
||||
/* Webkit scrollbar styles (Chrome, Safari, Edge) */
|
||||
.table-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-track {
|
||||
background: var(--mantle);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-thumb {
|
||||
background: var(--surface2);
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--surface1);
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar-corner {
|
||||
background: var(--mantle);
|
||||
}
|
||||
|
||||
/* When both scrollbars are present, add some padding to prevent overlap */
|
||||
.table-container::-webkit-scrollbar-corner {
|
||||
background-color: var(--mantle);
|
||||
}
|
||||
|
||||
table {
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--mantle);
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
font-weight: 500;
|
||||
color: var(--subtext0);
|
||||
border-bottom: 1px solid var(--surface0);
|
||||
transition: background-color 0.2s ease;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 6px 12px;
|
||||
border-bottom: 1px solid var(--surface0);
|
||||
color: var(--text);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: var(--surface0);
|
||||
}
|
||||
|
||||
.truncate {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 0;
|
||||
}
|
||||
|
||||
.sortable {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.th-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sort-indicator {
|
||||
color: var(--overlay0);
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sort-indicator.active {
|
||||
color: var(--blue);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sortable:hover .sort-indicator {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.high-usage {
|
||||
background-color: color-mix(in srgb, var(--red) 10%, transparent);
|
||||
}
|
||||
|
||||
.high-usage:hover {
|
||||
background-color: color-mix(in srgb, var(--red) 15%, transparent);
|
||||
}
|
||||
|
||||
tr.pinned {
|
||||
background-color: color-mix(in srgb, var(--blue) 10%, transparent);
|
||||
}
|
||||
|
||||
tr.pinned:hover {
|
||||
background-color: color-mix(in srgb, var(--blue) 15%, transparent);
|
||||
}
|
||||
|
||||
th:last-child {
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
max-width: 120px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.col-actions {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
background: var(--base);
|
||||
border-left: 1px solid var(--surface0);
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-action::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.1;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-action:hover::before {
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
.pin-btn {
|
||||
color: var(--sapphire);
|
||||
}
|
||||
|
||||
.pin-btn::before {
|
||||
background: var(--sapphire);
|
||||
}
|
||||
|
||||
.pin-btn.pinned {
|
||||
color: var(--blue);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.pin-btn.pinned::before {
|
||||
background: var(--blue);
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
.info-btn {
|
||||
color: var(--lavender);
|
||||
}
|
||||
|
||||
.info-btn::before {
|
||||
background: var(--lavender);
|
||||
}
|
||||
|
||||
.kill-btn {
|
||||
color: var(--red);
|
||||
border: 1px solid color-mix(in srgb, var(--red) 30%, transparent);
|
||||
}
|
||||
|
||||
.kill-btn:hover {
|
||||
color: var(--base);
|
||||
background: var(--red);
|
||||
}
|
||||
|
||||
.kill-btn:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-action:hover {
|
||||
box-shadow: 0 0 12px color-mix(in srgb, currentColor 20%, transparent);
|
||||
}
|
||||
|
||||
.btn-action:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.pin-btn.pinned:active {
|
||||
transform: rotate(45deg) translateY(1px);
|
||||
}
|
||||
|
||||
.btn-action:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 30%, transparent);
|
||||
}
|
||||
|
||||
.btn-action:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-action:disabled:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-action:disabled::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.process-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
6
src/lib/components/process/index.ts
Normal file
6
src/lib/components/process/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export { default as ProcessTable } from "./ProcessTable.svelte";
|
||||
export { default as ProcessRow } from "./ProcessRow.svelte";
|
||||
export { default as TableHeader } from "./TableHeader.svelte";
|
||||
export { default as ProcessCell } from "./cells/ProcessCell.svelte";
|
||||
export { default as ActionButtons } from "./ActionButtons.svelte";
|
||||
export { default as ProcessIcon } from "./ProcessIcon.svelte";
|
60
src/lib/components/stats/CpuPanel.svelte
Normal file
60
src/lib/components/stats/CpuPanel.svelte
Normal file
@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { faMicrochip } from "@fortawesome/free-solid-svg-icons";
|
||||
import PanelHeader from "./PanelHeader.svelte";
|
||||
import ProgressBar from "./ProgressBar.svelte";
|
||||
import { formatPercentage } from "$lib/utils";
|
||||
|
||||
export let cpuUsage: number[];
|
||||
|
||||
$: averageUsage = formatPercentage(
|
||||
cpuUsage.reduce((a, b) => a + b, 0) / cpuUsage.length,
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="stat-panel">
|
||||
<PanelHeader icon={faMicrochip} title="CPU Usage" usageValue={averageUsage} />
|
||||
<div class="stats-content cpu-grid">
|
||||
{#each cpuUsage as usage, i}
|
||||
<div class="stat-item with-progress">
|
||||
<ProgressBar
|
||||
label={`Core ${i}`}
|
||||
value={usage}
|
||||
labelWidth="2.5rem"
|
||||
valueWidth="2.5rem"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.stat-panel {
|
||||
flex: 2.5;
|
||||
min-width: 0;
|
||||
background-color: var(--mantle);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.cpu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.4rem 2rem;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.stat-item.with-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
</style>
|
58
src/lib/components/stats/MemoryPanel.svelte
Normal file
58
src/lib/components/stats/MemoryPanel.svelte
Normal file
@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { faMemory } from "@fortawesome/free-solid-svg-icons";
|
||||
import PanelHeader from "./PanelHeader.svelte";
|
||||
import ProgressBar from "./ProgressBar.svelte";
|
||||
import StatItem from "./StatItem.svelte";
|
||||
import { formatMemorySize, formatPercentage } from "$lib/utils";
|
||||
|
||||
export let memoryTotal: number;
|
||||
export let memoryUsed: number;
|
||||
export let memoryFree: number;
|
||||
|
||||
$: memoryPercentage = (memoryUsed / memoryTotal) * 100;
|
||||
</script>
|
||||
|
||||
<div class="stat-panel">
|
||||
<PanelHeader
|
||||
icon={faMemory}
|
||||
title="Memory"
|
||||
usageValue={formatPercentage(memoryPercentage)}
|
||||
/>
|
||||
<div class="stats-content">
|
||||
<div class="stat-item with-progress">
|
||||
<ProgressBar
|
||||
label="Memory usage"
|
||||
value={memoryPercentage}
|
||||
labelWidth="5rem"
|
||||
valueWidth="2.5rem"
|
||||
/>
|
||||
</div>
|
||||
<StatItem label="Total" value={formatMemorySize(memoryTotal)} />
|
||||
<StatItem label="Used" value={formatMemorySize(memoryUsed)} />
|
||||
<StatItem label="Free" value={formatMemorySize(memoryFree)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.stat-panel {
|
||||
flex: 2;
|
||||
min-width: 0;
|
||||
background-color: var(--mantle);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.stat-item.with-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
</style>
|
47
src/lib/components/stats/NetworkPanel.svelte
Normal file
47
src/lib/components/stats/NetworkPanel.svelte
Normal file
@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { faNetworkWired } from "@fortawesome/free-solid-svg-icons";
|
||||
import PanelHeader from "./PanelHeader.svelte";
|
||||
import StatItem from "./StatItem.svelte";
|
||||
|
||||
export let networkRxBytes: number;
|
||||
export let networkTxBytes: number;
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let value = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (value >= 1024 && unitIndex < units.length - 1) {
|
||||
value /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${value.toFixed(1)} ${units[unitIndex]}/s`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="stat-panel">
|
||||
<PanelHeader icon={faNetworkWired} title="Network I/O" />
|
||||
<div class="network-stats">
|
||||
<StatItem label="↓ Receiving" value={formatBytes(networkRxBytes)} />
|
||||
<StatItem label="↑ Sending" value={formatBytes(networkTxBytes)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.stat-panel {
|
||||
flex: 0.8;
|
||||
min-width: 125px;
|
||||
background-color: var(--mantle);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.network-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
</style>
|
50
src/lib/components/stats/PanelHeader.svelte
Normal file
50
src/lib/components/stats/PanelHeader.svelte
Normal file
@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import Fa from "svelte-fa";
|
||||
import type { IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
export let icon: IconDefinition;
|
||||
export let title: string;
|
||||
export let usageValue: string | null = null;
|
||||
</script>
|
||||
|
||||
<div class="panel-header">
|
||||
<Fa {icon} />
|
||||
<h3>{title}</h3>
|
||||
{#if usageValue}
|
||||
<div class="usage-pill">{usageValue}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--surface0);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.panel-header :global(svg) {
|
||||
color: var(--blue);
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
}
|
||||
|
||||
.usage-pill {
|
||||
margin-left: auto;
|
||||
background: var(--surface0);
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
80
src/lib/components/stats/ProgressBar.svelte
Normal file
80
src/lib/components/stats/ProgressBar.svelte
Normal file
@ -0,0 +1,80 @@
|
||||
<script lang="ts">
|
||||
export let label: string;
|
||||
export let value: number;
|
||||
export let labelWidth = "2.5rem";
|
||||
export let valueWidth = "2.5rem";
|
||||
|
||||
function getUsageClass(usage: number): string {
|
||||
if (usage > 90) return "critical";
|
||||
if (usage > 75) return "high";
|
||||
if (usage > 50) return "medium";
|
||||
return "low";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="progress-container"
|
||||
style="--label-width: {labelWidth}; --value-width: {valueWidth}"
|
||||
>
|
||||
<span class="label">{label}</span>
|
||||
<div class="bar-container">
|
||||
<div
|
||||
class="usage-bar {getUsageClass(value)}"
|
||||
style="transform: translateX({value - 100}%);"
|
||||
/>
|
||||
</div>
|
||||
<span class="value">{Math.round(value)}%</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: var(--label-width) 1fr var(--value-width);
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--subtext0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: var(--text);
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bar-container {
|
||||
height: 8px;
|
||||
background: var(--surface0);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.usage-bar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
transition: transform 0.3s ease;
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
.usage-bar.low {
|
||||
background: var(--blue);
|
||||
}
|
||||
.usage-bar.medium {
|
||||
background: var(--yellow);
|
||||
}
|
||||
.usage-bar.high {
|
||||
background: var(--peach);
|
||||
}
|
||||
.usage-bar.critical {
|
||||
background: var(--red);
|
||||
}
|
||||
</style>
|
30
src/lib/components/stats/StatItem.svelte
Normal file
30
src/lib/components/stats/StatItem.svelte
Normal file
@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
export let label: string;
|
||||
export let value: string;
|
||||
</script>
|
||||
|
||||
<div class="stat-item">
|
||||
<span>{label}</span>
|
||||
<span>{value}</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.stat-item span:first-child {
|
||||
color: var(--subtext0);
|
||||
}
|
||||
|
||||
.stat-item span:last-child {
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
34
src/lib/components/stats/StatPanel.svelte
Normal file
34
src/lib/components/stats/StatPanel.svelte
Normal file
@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
export let title: string;
|
||||
export let flex = 1;
|
||||
</script>
|
||||
|
||||
<div class="stat-panel" style="--flex: {flex}">
|
||||
<h3 class="panel-title">{title}</h3>
|
||||
<div class="panel-content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.stat-panel {
|
||||
flex: var(--flex);
|
||||
min-width: 125px;
|
||||
background: var(--mantle);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--subtext0);
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
50
src/lib/components/stats/StatsBar.svelte
Normal file
50
src/lib/components/stats/StatsBar.svelte
Normal file
@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import type { SystemStats } from "$lib/types";
|
||||
import CpuPanel from "./CpuPanel.svelte";
|
||||
import MemoryPanel from "./MemoryPanel.svelte";
|
||||
import StoragePanel from "./StoragePanel.svelte";
|
||||
import SystemPanel from "./SystemPanel.svelte";
|
||||
import NetworkPanel from "./NetworkPanel.svelte";
|
||||
|
||||
export let systemStats: SystemStats | null = null;
|
||||
</script>
|
||||
|
||||
<div class="dashboard-stats">
|
||||
{#if systemStats}
|
||||
<div class="stats-layout">
|
||||
<CpuPanel cpuUsage={systemStats.cpu_usage} />
|
||||
|
||||
<MemoryPanel
|
||||
memoryTotal={systemStats.memory_total}
|
||||
memoryUsed={systemStats.memory_used}
|
||||
memoryFree={systemStats.memory_free}
|
||||
/>
|
||||
|
||||
<StoragePanel
|
||||
diskTotalBytes={systemStats.disk_total_bytes}
|
||||
diskUsedBytes={systemStats.disk_used_bytes}
|
||||
diskFreeBytes={systemStats.disk_free_bytes}
|
||||
/>
|
||||
|
||||
<SystemPanel uptime={systemStats.uptime} loadAvg={systemStats.load_avg} />
|
||||
|
||||
<NetworkPanel
|
||||
networkRxBytes={systemStats.network_rx_bytes}
|
||||
networkTxBytes={systemStats.network_tx_bytes}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dashboard-stats {
|
||||
padding: 0.5rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.stats-layout {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
56
src/lib/components/stats/StoragePanel.svelte
Normal file
56
src/lib/components/stats/StoragePanel.svelte
Normal file
@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { faHardDrive } from "@fortawesome/free-solid-svg-icons";
|
||||
import PanelHeader from "./PanelHeader.svelte";
|
||||
import StatItem from "./StatItem.svelte";
|
||||
import { formatPercentage } from "$lib/utils";
|
||||
|
||||
export let diskTotalBytes: number;
|
||||
export let diskUsedBytes: number;
|
||||
export let diskFreeBytes: number;
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let value = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (value >= 1024 && unitIndex < units.length - 1) {
|
||||
value /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${value.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
$: diskUsagePercentage = (diskUsedBytes / diskTotalBytes) * 100;
|
||||
</script>
|
||||
|
||||
<div class="stat-panel">
|
||||
<PanelHeader
|
||||
icon={faHardDrive}
|
||||
title="Storage"
|
||||
usageValue={formatPercentage(diskUsagePercentage)}
|
||||
/>
|
||||
<div class="stats-content">
|
||||
<StatItem label="Total" value={formatBytes(diskTotalBytes)} />
|
||||
<StatItem label="Used" value={formatBytes(diskUsedBytes)} />
|
||||
<StatItem label="Free" value={formatBytes(diskFreeBytes)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.stat-panel {
|
||||
flex: 0.8;
|
||||
min-width: 125px;
|
||||
background-color: var(--mantle);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
</style>
|
38
src/lib/components/stats/SystemPanel.svelte
Normal file
38
src/lib/components/stats/SystemPanel.svelte
Normal file
@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
||||
import PanelHeader from "./PanelHeader.svelte";
|
||||
import StatItem from "./StatItem.svelte";
|
||||
import { formatUptime } from "$lib/utils";
|
||||
|
||||
export let uptime: number;
|
||||
export let loadAvg: [number, number, number];
|
||||
</script>
|
||||
|
||||
<div class="stat-panel">
|
||||
<PanelHeader icon={faServer} title="System" />
|
||||
<div class="system-grid">
|
||||
<StatItem label="Uptime" value={formatUptime(uptime)} />
|
||||
<StatItem label="1m Load" value={loadAvg[0].toFixed(2)} />
|
||||
<StatItem label="5m Load" value={loadAvg[1].toFixed(2)} />
|
||||
<StatItem label="15m Load" value={loadAvg[2].toFixed(2)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.stat-panel {
|
||||
flex: 0.8;
|
||||
min-width: 125px;
|
||||
background-color: var(--mantle);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.system-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
9
src/lib/components/stats/index.ts
Normal file
9
src/lib/components/stats/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export { default as StatsBar } from "./StatsBar.svelte";
|
||||
export { default as CpuPanel } from "./CpuPanel.svelte";
|
||||
export { default as MemoryPanel } from "./MemoryPanel.svelte";
|
||||
export { default as StoragePanel } from "./StoragePanel.svelte";
|
||||
export { default as SystemPanel } from "./SystemPanel.svelte";
|
||||
export { default as NetworkPanel } from "./NetworkPanel.svelte";
|
||||
export { default as PanelHeader } from "./PanelHeader.svelte";
|
||||
export { default as ProgressBar } from "./ProgressBar.svelte";
|
||||
export { default as StatItem } from "./StatItem.svelte";
|
@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import StatsBar from "$lib/components/StatsBar.svelte";
|
||||
import { StatsBar } from "$lib/components/stats";
|
||||
import ToolBar from "$lib/components/ToolBar.svelte";
|
||||
import ProcessTable from "$lib/components/ProcessTable.svelte";
|
||||
import ProcessTable from "$lib/components/process/ProcessTable.svelte";
|
||||
import ProcessDetailsModal from "$lib/components/ProcessDetailsModal.svelte";
|
||||
import KillProcessModal from "$lib/components/KillProcessModal.svelte";
|
||||
import { formatMemorySize, formatStatus } from "$lib/utils";
|
||||
@ -35,7 +35,7 @@
|
||||
id: "status",
|
||||
label: "Status",
|
||||
visible: true,
|
||||
format: formatStatus,
|
||||
format: (v) => v,
|
||||
},
|
||||
{ id: "user", label: "User", visible: true },
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user