Merge pull request #80 from Abdenasser/resizable-columns

adds resizable columns + other enhancements
This commit is contained in:
Abdenasser Elidrissi 2024-11-09 18:26:28 +01:00 committed by GitHub
commit 478d7467bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 273 additions and 26 deletions

View File

@ -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>

View File

@ -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}

View File

@ -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;

View File

@ -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`,