mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-30 08:36:55 +09:00
feat(servers): /servers on the shared list chrome, with 4 new presets
Replaces the toolbar's Views/Sort/Group/Columns buttons with the same chrome as /domains, /services and /cron: the saved view is the page heading, column rules get a chip bar and the Filters/Fields drawer, and sort/group/hide move into each column's own menu. Presets: "Not reporting" (has an agent but stopped answering, longest silence first -- `pending` excluded, that means no agent was ever installed), "Under load" (saturated on EITHER axis, because a box swapping at 95% memory with calm CPU is the one that falls over), "Disk pressure", and "Ungrouped" -- fleet commands target a group_id, so an ungrouped server is silently skipped by every bulk action. Only `gt` is used on cpu/memory/disk, and there is a comment saying why: those sortValues are null for a non-live row, so a `lt` rule would sweep in every offline and pending server. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b113bd0af9
commit
09cecff92f
1 file changed
+103
-35
+103
-35
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ChevronRight, Folder, Plus, RefreshCw, Server as ServerLucideIcon, X } from 'lucide-react';
|
||||
import api from '../services/api';
|
||||
@@ -8,12 +8,15 @@ import Modal from '@/components/Modal';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
ColumnsMenu, DataTable, DataTableFooter, Drawer, Gauge, GroupMenu, ListToolbar, Pill, SearchField, SegControl, SortChipBar, SortMenu, ViewMenu,
|
||||
DataTable, DataTableFooter, Drawer, Gauge, ListToolbar, Pill, SearchField, SegControl,
|
||||
} from '@/components/ds';
|
||||
import { useTopbarActions } from '@/hooks/useTopbarActions';
|
||||
import { useTableSort } from '@/hooks/useTableSort';
|
||||
import { useColumnVisibility } from '@/hooks/useColumnVisibility';
|
||||
import { useTableViews } from '@/hooks/useTableViews';
|
||||
import {
|
||||
useTableChrome, GridViewPicker, GridChips, GridFilterButton,
|
||||
GridToolsMenu, GridFilterDrawer,
|
||||
} from '@/components/ds/grid';
|
||||
import useFocusParam from '@/hooks/useFocusParam';
|
||||
import LinkPanelForm from '../components/servers/LinkPanelForm';
|
||||
|
||||
@@ -31,10 +34,68 @@ const STATUS_FILTERS = ['all', 'online', 'offline', 'connecting', 'pending'];
|
||||
|
||||
// Built-in saved views. States only use real SegControl values
|
||||
// (STATUS_FILTERS), group values ('all') and column keys (SERVER_COLUMNS).
|
||||
// NOTE for anyone adding a metric preset: only ever use `gt` on cpu/memory/disk.
|
||||
// Their sortValue is null for a non-live row, and a `lt` rule would match every
|
||||
// offline and pending server (see the null guard in ds/grid/fields.js).
|
||||
const SERVER_BUILTIN_VIEWS = [
|
||||
{ name: 'Online', state: { status: 'online', group: 'all', search: '', sorts: [], hiddenKeys: [] } },
|
||||
{ name: 'Offline', state: { status: 'offline', group: 'all', search: '', sorts: [], hiddenKeys: [] } },
|
||||
{ name: 'By CPU', state: { status: 'all', group: 'all', search: '', sorts: [{ key: 'cpu', direction: 'desc' }], hiddenKeys: [] } },
|
||||
{
|
||||
// Machines that HAVE an agent but stopped answering, longest silence
|
||||
// first. `pending` is excluded — that means nobody ever installed one.
|
||||
// CPU/Memory/Disk are hidden because no row here is live, so all three
|
||||
// would render an em-dash.
|
||||
name: 'Not reporting',
|
||||
state: {
|
||||
status: 'all', group: 'all', search: '', groupBy: null,
|
||||
sorts: [{ key: 'lastSeen', direction: 'asc' }],
|
||||
hiddenKeys: ['cpu', 'memory', 'disk'],
|
||||
columnFilters: {
|
||||
match: 'all',
|
||||
rules: [{ id: 'nr1', field: 'status', op: 'any', value: ['offline', 'connecting'] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Saturated on EITHER axis — a box swapping at 95% memory with calm CPU
|
||||
// is the one that falls over, so match:any rather than all.
|
||||
name: 'Under load',
|
||||
state: {
|
||||
status: 'online', group: 'all', search: '', groupBy: null, hiddenKeys: [],
|
||||
sorts: [{ key: 'cpu', direction: 'desc' }],
|
||||
columnFilters: {
|
||||
match: 'any',
|
||||
rules: [
|
||||
{ id: 'ul1', field: 'cpu', op: 'gt', value: 85 },
|
||||
{ id: 'ul2', field: 'memory', op: 'gt', value: 90 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// The one fleet metric that never recovers on its own.
|
||||
name: 'Disk pressure',
|
||||
state: {
|
||||
status: 'online', group: 'all', search: '', groupBy: null,
|
||||
sorts: [{ key: 'disk', direction: 'desc' }],
|
||||
hiddenKeys: ['agent', 'cpu', 'memory'],
|
||||
columnFilters: {
|
||||
match: 'all',
|
||||
rules: [{ id: 'dp1', field: 'disk', op: 'gt', value: 85 }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Fleet commands target a group_id, so an ungrouped server is silently
|
||||
// skipped by every bulk action. This view is the worklist that fixes it.
|
||||
name: 'Ungrouped',
|
||||
state: {
|
||||
status: 'all', group: 'ungrouped', search: '', groupBy: null, hiddenKeys: [],
|
||||
sorts: [{ key: 'name', direction: 'asc' }],
|
||||
columnFilters: { match: 'all', rules: [] },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const formatLastSeen = (timestamp) => {
|
||||
@@ -163,7 +224,7 @@ const Servers = () => {
|
||||
const [bulkBusy, setBulkBusy] = useState(false);
|
||||
const { sorts, setSorts } = useTableSort({ storageKey: 'serverkit-table-servers-sort' });
|
||||
const {
|
||||
hiddenKeys, setHiddenKeys, toggleColumn, showAllColumns,
|
||||
hiddenKeys, setHiddenKeys,
|
||||
} = useColumnVisibility({ storageKey: 'serverkit-table-servers-cols' });
|
||||
const [groupBy, setGroupBy] = useState(() => {
|
||||
try {
|
||||
@@ -184,33 +245,20 @@ const Servers = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Saved views: capture/apply the table chrome state (status filter, group
|
||||
// filter, search, sort levels, hidden columns). Every key is guarded so a
|
||||
// partial view state only touches what it sets.
|
||||
const captureView = useCallback(() => ({
|
||||
// Page-owned view state (the status segment, the group select, search);
|
||||
// useTableChrome owns the rest — sorts, hidden columns, grouping, column
|
||||
// rules and order — and gives back the shared list chrome.
|
||||
const extraViewState = useMemo(() => ({
|
||||
status: selectedStatus,
|
||||
group: selectedGroup,
|
||||
search: searchTerm,
|
||||
sorts,
|
||||
hiddenKeys,
|
||||
groupBy,
|
||||
}), [selectedStatus, selectedGroup, searchTerm, sorts, hiddenKeys, groupBy]);
|
||||
}), [selectedStatus, selectedGroup, searchTerm]);
|
||||
|
||||
const applyView = useCallback((state) => {
|
||||
const applyExtraViewState = useCallback((state) => {
|
||||
if (state.status !== undefined) setSelectedStatus(state.status);
|
||||
if (state.group !== undefined) setSelectedGroup(state.group);
|
||||
if (state.search !== undefined) setSearchTerm(state.search);
|
||||
if (Array.isArray(state.sorts)) setSorts(state.sorts);
|
||||
if (Array.isArray(state.hiddenKeys)) setHiddenKeys(state.hiddenKeys);
|
||||
if (state.groupBy !== undefined) changeGroupBy(state.groupBy);
|
||||
}, [setSorts, setHiddenKeys]);
|
||||
|
||||
const tableViews = useTableViews({
|
||||
page: 'servers',
|
||||
builtinViews: SERVER_BUILTIN_VIEWS,
|
||||
capture: captureView,
|
||||
apply: applyView,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -273,6 +321,22 @@ const Servers = () => {
|
||||
</>
|
||||
), [searchTerm, loading]);
|
||||
|
||||
const chrome = useTableChrome({
|
||||
columns: SERVER_COLUMNS,
|
||||
rows: filteredServers,
|
||||
viewPageKey: 'servers',
|
||||
builtinViews: SERVER_BUILTIN_VIEWS,
|
||||
noun: 'servers',
|
||||
sorts,
|
||||
setSorts,
|
||||
hiddenKeys,
|
||||
setHiddenKeys,
|
||||
groupBy,
|
||||
setGroupBy: changeGroupBy,
|
||||
extraState: extraViewState,
|
||||
applyExtra: applyExtraViewState,
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="sk-tabgroup__inner servers-page">
|
||||
@@ -283,6 +347,12 @@ const Servers = () => {
|
||||
|
||||
return (
|
||||
<div className="sk-tabgroup__inner servers-page">
|
||||
<GridViewPicker
|
||||
views={chrome.views}
|
||||
label="servers"
|
||||
total={`${filteredServers.length} of ${servers.length} servers`}
|
||||
onCreate={chrome.createView}
|
||||
/>
|
||||
<ListToolbar
|
||||
filters={(
|
||||
<SegControl
|
||||
@@ -305,15 +375,11 @@ const Servers = () => {
|
||||
)}
|
||||
tools={(
|
||||
<>
|
||||
<ViewMenu views={tableViews} />
|
||||
<GroupMenu columns={SERVER_COLUMNS} groupBy={groupBy} onChange={changeGroupBy} />
|
||||
<SortMenu columns={SERVER_COLUMNS} sorts={sorts} onChange={setSorts} />
|
||||
<ColumnsMenu
|
||||
columns={SERVER_COLUMNS}
|
||||
hiddenKeys={hiddenKeys}
|
||||
onToggle={toggleColumn}
|
||||
onShowAll={showAllColumns}
|
||||
<GridFilterButton
|
||||
count={chrome.filterCount}
|
||||
onClick={() => chrome.setDrawerOpen(true)}
|
||||
/>
|
||||
<GridToolsMenu {...chrome.toolsProps} onRefresh={loadData} />
|
||||
</>
|
||||
)}
|
||||
>
|
||||
@@ -331,7 +397,7 @@ const Servers = () => {
|
||||
)}
|
||||
</ListToolbar>
|
||||
|
||||
<SortChipBar columns={SERVER_COLUMNS} sorts={sorts} onChange={setSorts} />
|
||||
<GridChips {...chrome.chipProps} />
|
||||
|
||||
{filteredServers.length === 0 ? (
|
||||
<EmptyState
|
||||
@@ -359,12 +425,12 @@ const Servers = () => {
|
||||
/>
|
||||
) : (
|
||||
<DataTable
|
||||
columns={SERVER_COLUMNS}
|
||||
columns={chrome.columns}
|
||||
data={filteredServers}
|
||||
keyField="id"
|
||||
sorts={sorts}
|
||||
onSortsChange={setSorts}
|
||||
hiddenKeys={hiddenKeys}
|
||||
{...chrome.tableProps}
|
||||
groupBy={groupBy}
|
||||
onGroupByChange={changeGroupBy}
|
||||
selectable
|
||||
@@ -393,6 +459,8 @@ const Servers = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<GridFilterDrawer {...chrome.drawerProps} />
|
||||
|
||||
{/* Bulk edit: assign the selected servers to a group. */}
|
||||
{selectedIds.size > 0 && (
|
||||
<div className="sk-bulkbar" role="status">
|
||||
|
||||
Reference in new issue
Block a user