mirror of
https://github.com/kunkunsh/kunkun-ext-neohtop.git
synced 2025-04-11 17:29:45 +00:00
Merge pull request #80 from Abdenasser/resizable-columns
adds resizable columns + other enhancements
This commit is contained in:
commit
478d7467bb
@ -3,6 +3,7 @@
|
|||||||
faThumbtack,
|
faThumbtack,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faXmark,
|
faXmark,
|
||||||
|
faGripLinesVertical,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import Fa from "svelte-fa";
|
import Fa from "svelte-fa";
|
||||||
import type { Process, Column } from "$lib/types";
|
import type { Process, Column } from "$lib/types";
|
||||||
@ -19,6 +20,88 @@
|
|||||||
export let onShowDetails: (process: Process) => void;
|
export let onShowDetails: (process: Process) => void;
|
||||||
export let onKillProcess: (process: Process) => void;
|
export let onKillProcess: (process: Process) => void;
|
||||||
|
|
||||||
|
interface ResizeState {
|
||||||
|
leftColumnId: string | null;
|
||||||
|
rightColumnId: string | null;
|
||||||
|
startX: number;
|
||||||
|
leftStartWidth: number;
|
||||||
|
rightStartWidth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let resizing: ResizeState = {
|
||||||
|
leftColumnId: null,
|
||||||
|
rightColumnId: null,
|
||||||
|
startX: 0,
|
||||||
|
leftStartWidth: 0,
|
||||||
|
rightStartWidth: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store column widths
|
||||||
|
let columnWidths: Record<string, number> = {};
|
||||||
|
|
||||||
|
// Initialize default widths
|
||||||
|
$: {
|
||||||
|
columns.forEach((col) => {
|
||||||
|
if (!columnWidths[col.id]) {
|
||||||
|
columnWidths[col.id] = 100; // Default width
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResizeStart(
|
||||||
|
event: MouseEvent,
|
||||||
|
leftColId: string,
|
||||||
|
rightColId: string,
|
||||||
|
) {
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
resizing = {
|
||||||
|
leftColumnId: leftColId,
|
||||||
|
rightColumnId: rightColId,
|
||||||
|
startX: event.pageX,
|
||||||
|
leftStartWidth: columnWidths[leftColId],
|
||||||
|
rightStartWidth: columnWidths[rightColId],
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", handleResizeMove);
|
||||||
|
document.addEventListener("mouseup", handleResizeEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResizeMove(event: MouseEvent) {
|
||||||
|
if (!resizing.leftColumnId || !resizing.rightColumnId) return;
|
||||||
|
|
||||||
|
const delta = event.pageX - resizing.startX;
|
||||||
|
|
||||||
|
// Ensure minimum width (50px) for both columns
|
||||||
|
const newLeftWidth = Math.max(50, resizing.leftStartWidth + delta);
|
||||||
|
const totalWidth = resizing.leftStartWidth + resizing.rightStartWidth;
|
||||||
|
const newRightWidth = Math.max(50, totalWidth - newLeftWidth);
|
||||||
|
|
||||||
|
// Only update if both columns maintain minimum width
|
||||||
|
if (newLeftWidth >= 50 && newRightWidth >= 50) {
|
||||||
|
columnWidths = {
|
||||||
|
...columnWidths,
|
||||||
|
[resizing.leftColumnId]: newLeftWidth,
|
||||||
|
[resizing.rightColumnId]: newRightWidth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleResizeEnd() {
|
||||||
|
resizing = {
|
||||||
|
leftColumnId: null,
|
||||||
|
rightColumnId: null,
|
||||||
|
startX: 0,
|
||||||
|
leftStartWidth: 0,
|
||||||
|
rightStartWidth: 0,
|
||||||
|
};
|
||||||
|
document.removeEventListener("mousemove", handleResizeMove);
|
||||||
|
document.removeEventListener("mouseup", handleResizeEnd);
|
||||||
|
|
||||||
|
// Optional: Save to localStorage
|
||||||
|
localStorage.setItem("columnWidths", JSON.stringify(columnWidths));
|
||||||
|
}
|
||||||
|
|
||||||
function getSortIndicator(field: keyof Process) {
|
function getSortIndicator(field: keyof Process) {
|
||||||
if (sortConfig.field !== field) return "↕";
|
if (sortConfig.field !== field) return "↕";
|
||||||
return sortConfig.direction === "asc" ? "↑" : "↓";
|
return sortConfig.direction === "asc" ? "↑" : "↓";
|
||||||
@ -90,20 +173,38 @@
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{#each columns.filter((col) => col.visible) as column}
|
{#each columns.filter((col) => col.visible) as column, i}
|
||||||
<th class="sortable" on:click={() => onToggleSort(column.id)}>
|
<th
|
||||||
<div class="th-content">
|
data-column={column.id}
|
||||||
{column.label}
|
style="width: {columnWidths[column.id]}px"
|
||||||
<span
|
>
|
||||||
class="sort-indicator"
|
<div class="th-content" on:click={() => onToggleSort(column.id)}>
|
||||||
class:active={sortConfig.field === column.id}
|
<div class="th-label">
|
||||||
>
|
{column.label}
|
||||||
{getSortIndicator(column.id)}
|
<span
|
||||||
</span>
|
class="sort-indicator"
|
||||||
|
class:active={sortConfig.field === column.id}
|
||||||
|
>
|
||||||
|
{getSortIndicator(column.id)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{#if i < columns.filter((col) => col.visible).length - 1}
|
||||||
|
<div
|
||||||
|
class="resize-handle"
|
||||||
|
on:mousedown|stopPropagation={(e) =>
|
||||||
|
handleResizeStart(
|
||||||
|
e,
|
||||||
|
column.id,
|
||||||
|
columns.filter((col) => col.visible)[i + 1].id,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Fa icon={faGripLinesVertical} size="xs" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
{/each}
|
{/each}
|
||||||
<th>Actions</th>
|
<th class="col-actions">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -252,27 +353,63 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.th-content {
|
.th-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.th-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sort-indicator {
|
.resize-handle {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: col-resize;
|
||||||
color: var(--overlay0);
|
color: var(--overlay0);
|
||||||
font-size: 12px;
|
opacity: 0;
|
||||||
opacity: 0.5;
|
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sort-indicator.active {
|
/* Show handle on header hover */
|
||||||
|
th:hover .resize-handle {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle:hover {
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active state during resize */
|
||||||
|
.resize-handle:active {
|
||||||
color: var(--blue);
|
color: var(--blue);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sortable:hover .sort-indicator {
|
.sort-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-indicator.active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
th:hover .sort-indicator {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.high-usage {
|
.high-usage {
|
||||||
background-color: color-mix(in srgb, var(--red) 10%, transparent);
|
background-color: color-mix(in srgb, var(--red) 10%, transparent);
|
||||||
}
|
}
|
||||||
@ -306,7 +443,6 @@
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
background: var(--base);
|
|
||||||
border-left: 1px solid var(--surface0);
|
border-left: 1px solid var(--surface0);
|
||||||
width: 120px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
@ -427,4 +563,66 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
position: relative;
|
||||||
|
min-width: 50px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: col-resize;
|
||||||
|
color: var(--overlay0);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show handle on header hover */
|
||||||
|
th:hover .resize-handle {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle:hover {
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active state during resize */
|
||||||
|
.resize-handle:active {
|
||||||
|
color: var(--blue);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optional: Add a subtle background on hover */
|
||||||
|
.resize-handle:hover::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -4px;
|
||||||
|
background: var(--surface0);
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make sure the table doesn't shrink columns */
|
||||||
|
table {
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
transition: width 0.05s ease;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -2,6 +2,11 @@
|
|||||||
import { themeStore } from "$lib/stores";
|
import { themeStore } from "$lib/stores";
|
||||||
import { themes } from "$lib/styles";
|
import { themes } from "$lib/styles";
|
||||||
import { fade } from "svelte/transition";
|
import { fade } from "svelte/transition";
|
||||||
|
import Fa from "svelte-fa";
|
||||||
|
import {
|
||||||
|
faChevronDown,
|
||||||
|
faChevronRight,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
let showMenu = false;
|
let showMenu = false;
|
||||||
|
|
||||||
@ -65,7 +70,13 @@
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="icon">{showMenu ? "▼" : "▶"}</span>
|
<span class="icon">
|
||||||
|
{#if showMenu}
|
||||||
|
<Fa icon={faChevronDown} />
|
||||||
|
{:else}
|
||||||
|
<Fa icon={faChevronRight} />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if showMenu}
|
{#if showMenu}
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import AppInfo from "./AppInfo.svelte";
|
import AppInfo from "./AppInfo.svelte";
|
||||||
import { statusMap } from "$lib/utils";
|
import { statusMap } from "$lib/utils";
|
||||||
|
import Fa from "svelte-fa";
|
||||||
|
import {
|
||||||
|
faPlay,
|
||||||
|
faPause,
|
||||||
|
faChevronDown,
|
||||||
|
faChevronRight,
|
||||||
|
faArrowRotateRight,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
export let searchTerm: string;
|
export let searchTerm: string;
|
||||||
export let statusFilter: string = "all";
|
export let statusFilter: string = "all";
|
||||||
export let itemsPerPage: number;
|
export let itemsPerPage: number;
|
||||||
@ -118,7 +126,13 @@
|
|||||||
aria-label="Toggle columns"
|
aria-label="Toggle columns"
|
||||||
>
|
>
|
||||||
Columns
|
Columns
|
||||||
<span class="icon">{showColumnMenu ? "▼" : "▶"}</span>
|
<span class="icon">
|
||||||
|
{#if showColumnMenu}
|
||||||
|
<Fa icon={faChevronDown} />
|
||||||
|
{:else}
|
||||||
|
<Fa icon={faChevronRight} />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if showColumnMenu}
|
{#if showColumnMenu}
|
||||||
@ -146,9 +160,7 @@
|
|||||||
disabled={isFrozen}
|
disabled={isFrozen}
|
||||||
>
|
>
|
||||||
{#each refreshRateOptions as option}
|
{#each refreshRateOptions as option}
|
||||||
<option value={option.value}>
|
<option value={option.value}>{option.label}</option>
|
||||||
{option.label}
|
|
||||||
</option>
|
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
@ -158,9 +170,9 @@
|
|||||||
title={isFrozen ? "Resume Updates" : "Pause Updates"}
|
title={isFrozen ? "Resume Updates" : "Pause Updates"}
|
||||||
>
|
>
|
||||||
{#if isFrozen}
|
{#if isFrozen}
|
||||||
▶
|
<Fa icon={faPlay} />
|
||||||
{:else}
|
{:else}
|
||||||
⏸
|
<Fa icon={faPause} />
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -332,6 +344,27 @@
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-menu::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-menu::-webkit-scrollbar-track {
|
||||||
|
background: var(--mantle);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-menu::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--surface2);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-menu::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--surface1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-option {
|
.column-option {
|
||||||
@ -386,6 +419,11 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.refresh-controls :global(svg) {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--subtext0);
|
||||||
|
}
|
||||||
|
|
||||||
.btn-action {
|
.btn-action {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -53,7 +53,7 @@
|
|||||||
},
|
},
|
||||||
{ id: "command", label: "Command", visible: false },
|
{ id: "command", label: "Command", visible: false },
|
||||||
{ id: "ppid", label: "Parent PID", visible: false },
|
{ id: "ppid", label: "Parent PID", visible: false },
|
||||||
{ id: "environ", label: "Environment", visible: false },
|
{ id: "environ", label: "Environment Variables", visible: false },
|
||||||
{ id: "root", label: "Root", visible: false },
|
{ id: "root", label: "Root", visible: false },
|
||||||
{
|
{
|
||||||
id: "virtual_memory",
|
id: "virtual_memory",
|
||||||
@ -81,7 +81,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "disk_usage",
|
id: "disk_usage",
|
||||||
label: "Disk Usage",
|
label: "Disk Usage read/write",
|
||||||
visible: false,
|
visible: false,
|
||||||
format: (v) =>
|
format: (v) =>
|
||||||
`${(v[0] / (1024 * 1024)).toFixed(1)} / ${(v[1] / (1024 * 1024)).toFixed(1)} MB`,
|
`${(v[0] / (1024 * 1024)).toFixed(1)} / ${(v[1] / (1024 * 1024)).toFixed(1)} MB`,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user