mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 03:20:11 +00:00
feat(ui): optimize mobile view for Alerts, Storage, and Navigation
- Implement compact mobile navigation with label truncation on xs screens - Optimize Alerts Overview with tighter spacing and better description truncation - Enhance Storage table mobile view: consolidate Shared column, use StatusDots, and increase bar thickness - Increase Node Summary table row height and column min-widths for readability - Add xs (400px) breakpoint for granular mobile styling
This commit is contained in:
parent
3eedbff6e6
commit
03d680365c
18 changed files with 1614 additions and 1586 deletions
|
|
@ -860,7 +860,7 @@ function App() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => aiChatStore.toggle()}
|
||||
class="fixed right-0 top-1/2 -translate-y-1/2 z-40 flex items-center gap-1.5 pl-2 pr-1.5 py-3 rounded-l-xl bg-gradient-to-r from-purple-600 to-purple-700 text-white shadow-lg hover:from-purple-700 hover:to-purple-800 transition-all duration-200 group"
|
||||
class="fixed right-0 top-1/2 -translate-y-1/2 z-40 flex items-center gap-1.5 pl-2 pr-1.5 py-3 rounded-l-xl bg-gradient-to-r from-purple-600 to-purple-700 text-white shadow-lg hover:from-purple-700 hover:to-purple-800 transition-all duration-200 group sm:top-1/2 sm:translate-y-[-50%] top-auto bottom-20 translate-y-0"
|
||||
title={aiChatStore.context.context?.name ? `AI Assistant - ${aiChatStore.context.context.name}` : 'AI Assistant (⌘K)'}
|
||||
aria-label="Expand AI Assistant"
|
||||
>
|
||||
|
|
@ -1285,7 +1285,7 @@ function AppLayout(props: {
|
|||
const isActive = () => getActiveTab() === platform.id;
|
||||
const disabled = () => !platform.enabled;
|
||||
const baseClasses =
|
||||
'tab relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium flex items-center gap-1 sm:gap-1.5 rounded-t border border-transparent transition-colors whitespace-nowrap cursor-pointer';
|
||||
'tab relative px-1.5 sm:px-3 py-1.5 text-xs sm:text-sm font-medium flex items-center gap-1 sm:gap-1.5 rounded-t border border-transparent transition-colors whitespace-nowrap cursor-pointer';
|
||||
|
||||
const className = () => {
|
||||
if (isActive()) {
|
||||
|
|
@ -1311,19 +1311,20 @@ function AppLayout(props: {
|
|||
title={title()}
|
||||
>
|
||||
{platform.icon}
|
||||
<span>{platform.label}</span>
|
||||
<span class="hidden xs:inline">{platform.label}</span>
|
||||
<span class="xs:hidden">{platform.label.charAt(0)}</span>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
<div class="flex items-end gap-2 ml-auto" role="group" aria-label="System">
|
||||
<div class="flex items-end gap-1 pl-3 sm:pl-4">
|
||||
<div class="flex items-end gap-1 ml-auto" role="group" aria-label="System">
|
||||
<div class="flex items-end gap-1 pl-1 sm:pl-4">
|
||||
<For each={utilityTabs()}>
|
||||
{(tab) => {
|
||||
const isActive = () => getActiveTab() === tab.id;
|
||||
const baseClasses =
|
||||
'tab relative px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium flex items-center gap-1 sm:gap-1.5 rounded-t border border-transparent transition-colors whitespace-nowrap cursor-pointer';
|
||||
'tab relative px-1.5 sm:px-3 py-1.5 text-xs sm:text-sm font-medium flex items-center gap-1 sm:gap-1.5 rounded-t border border-transparent transition-colors whitespace-nowrap cursor-pointer';
|
||||
|
||||
const className = () => {
|
||||
if (isActive()) {
|
||||
|
|
@ -1341,8 +1342,9 @@ function AppLayout(props: {
|
|||
title={tab.tooltip}
|
||||
>
|
||||
{tab.icon}
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span>{tab.label}</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="hidden xs:inline">{tab.label}</span>
|
||||
<span class="xs:hidden">{tab.label.charAt(0)}</span>
|
||||
{tab.id === 'alerts' && (() => {
|
||||
const total = tab.count ?? 0;
|
||||
if (total <= 0) {
|
||||
|
|
|
|||
|
|
@ -146,13 +146,13 @@ export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
|
|||
commitSearchToHistory(e.currentTarget.value);
|
||||
}
|
||||
}}
|
||||
class="w-full pl-9 pr-20 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
class="w-full pl-8 sm:pl-9 pr-14 sm:pr-20 py-1.5 sm:py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500
|
||||
focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 dark:focus:border-blue-400 outline-none transition-all"
|
||||
title="Search backups by name, VMID, or filter by node"
|
||||
/>
|
||||
<svg
|
||||
class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 dark:text-gray-500"
|
||||
class="absolute left-2.5 sm:left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 dark:text-gray-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
|
|
@ -167,7 +167,7 @@ export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
|
|||
<Show when={props.search()}>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-14 top-1/2 -translate-y-1/2 transform p-1 rounded-full
|
||||
class="absolute right-12 sm:right-14 top-1/2 -translate-y-1/2 transform p-1 rounded-full
|
||||
bg-gray-200 dark:bg-gray-600 text-gray-500 dark:text-gray-300
|
||||
hover:bg-red-100 hover:text-red-600 dark:hover:bg-red-900/50 dark:hover:text-red-400
|
||||
transition-all duration-150 active:scale-90"
|
||||
|
|
@ -297,7 +297,7 @@ export const BackupsFilter: Component<BackupsFilterProps> = (props) => {
|
|||
</div>
|
||||
|
||||
{/* Row 2: Filters - Compact horizontal layout */}
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="flex flex-wrap items-center gap-1.5 sm:gap-2">
|
||||
{/* Source Filter */}
|
||||
<div class="inline-flex rounded-lg bg-gray-100 dark:bg-gray-700 p-0.5">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -171,9 +171,9 @@ const UnifiedBackups: Component = () => {
|
|||
{ id: 'node', label: 'Node', priority: 'essential' },
|
||||
{ id: 'owner', label: 'Owner', priority: 'secondary', toggleable: true },
|
||||
{ id: 'backupTime', label: 'Time', priority: 'essential' },
|
||||
{ id: 'size', label: 'Size', priority: 'secondary', toggleable: true },
|
||||
{ id: 'size', label: 'Size', priority: 'primary', toggleable: true },
|
||||
{ id: 'backupType', label: 'Backup Type', priority: 'essential' },
|
||||
{ id: 'storage', label: 'Location', priority: 'secondary', toggleable: true },
|
||||
{ id: 'storage', label: 'Location', priority: 'primary', toggleable: true },
|
||||
{ id: 'verified', label: 'Verified', priority: 'secondary', toggleable: true },
|
||||
{ id: 'comment', label: 'Comment', priority: 'supplementary', toggleable: true },
|
||||
{ id: 'details', label: 'Details', priority: 'essential' },
|
||||
|
|
@ -2100,58 +2100,58 @@ const UnifiedBackups: Component = () => {
|
|||
{/* Mobile Card View removed in favor of scrollable table */}
|
||||
|
||||
{/* Desktop Table View */}
|
||||
<table class="backup-table" style={{ "min-width": "1200px" }}>
|
||||
<table class="backup-table" style={{ "min-width": "900px" }}>
|
||||
<thead>
|
||||
<tr class="bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-300 border-b border-gray-200 dark:border-gray-600">
|
||||
<th
|
||||
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
class="px-1.5 sm:px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
onClick={() => handleSort('vmid')}
|
||||
>
|
||||
{hasHostBackups() ? 'ID' : 'VMID'}{' '}
|
||||
{sortKey() === 'vmid' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
</th>
|
||||
<th
|
||||
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
class="px-1.5 sm:px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
onClick={() => handleSort('type')}
|
||||
>
|
||||
Type {sortKey() === 'type' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
</th>
|
||||
<th
|
||||
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
class="px-1.5 sm:px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
Name {sortKey() === 'name' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
</th>
|
||||
<th
|
||||
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
class="px-1.5 sm:px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
onClick={() => handleSort('node')}
|
||||
>
|
||||
Node {sortKey() === 'node' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
</th>
|
||||
<Show when={isColumnVisible('owner') && (backupTypeFilter() === 'all' || backupTypeFilter() === 'remote')}>
|
||||
<th
|
||||
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
class="px-1.5 sm:px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
onClick={() => handleSort('owner')}
|
||||
>
|
||||
Owner {sortKey() === 'owner' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
</th>
|
||||
</Show>
|
||||
<th
|
||||
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
class="px-1.5 sm:px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
onClick={() => handleSort('backupTime')}
|
||||
>
|
||||
Time {sortKey() === 'backupTime' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
</th>
|
||||
<Show when={isColumnVisible('size') && backupTypeFilter() !== 'snapshot'}>
|
||||
<th
|
||||
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
class="px-1.5 sm:px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
onClick={() => handleSort('size')}
|
||||
>
|
||||
Size {sortKey() === 'size' && (sortDirection() === 'asc' ? '▲' : '▼')}
|
||||
</th>
|
||||
</Show>
|
||||
<th
|
||||
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
class="px-1.5 sm:px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
onClick={() => handleSort('backupType')}
|
||||
>
|
||||
Backup{' '}
|
||||
|
|
@ -2159,7 +2159,7 @@ const UnifiedBackups: Component = () => {
|
|||
</th>
|
||||
<Show when={isColumnVisible('storage') && backupTypeFilter() !== 'snapshot'}>
|
||||
<th
|
||||
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
class="px-1.5 sm:px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||
onClick={() => handleSort('storage')}
|
||||
>
|
||||
Location{' '}
|
||||
|
|
@ -2209,8 +2209,8 @@ const UnifiedBackups: Component = () => {
|
|||
<For each={group.items}>
|
||||
{(item) => (
|
||||
<tr class="border-t border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="p-0.5 pl-5 pr-1.5 text-sm align-middle">{item.vmid}</td>
|
||||
<td class="p-0.5 px-1.5 align-middle">
|
||||
<td class="p-0.5 pl-3 sm:pl-5 pr-1 sm:pr-1.5 text-xs sm:text-sm align-middle">{item.vmid}</td>
|
||||
<td class="p-0.5 px-1 sm:px-1.5 align-middle">
|
||||
<span
|
||||
class={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium ${item.type === 'VM'
|
||||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300'
|
||||
|
|
@ -2247,7 +2247,7 @@ const UnifiedBackups: Component = () => {
|
|||
{item.size ? formatBytes(item.size) : '-'}
|
||||
</td>
|
||||
</Show>
|
||||
<td class="p-0.5 px-1.5 align-middle">
|
||||
<td class="p-0.5 px-1 sm:px-1.5 align-middle">
|
||||
<div class="flex items-center gap-1">
|
||||
<span
|
||||
class={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium ${item.backupType === 'snapshot'
|
||||
|
|
|
|||
|
|
@ -1098,7 +1098,7 @@ export function Dashboard(props: DashboardProps) {
|
|||
<ComponentErrorBoundary name="Guest Table">
|
||||
<Card padding="none" tone="glass" class="mb-4 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full border-collapse whitespace-nowrap" style={{ "min-width": isMobile() ? "100%" : "900px" }}>
|
||||
<table class="w-full border-collapse whitespace-nowrap" style={{ "min-width": isMobile() ? "800px" : "900px" }}>
|
||||
<thead>
|
||||
<tr class="bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-300 border-b border-gray-200 dark:border-gray-700">
|
||||
<For each={visibleColumns()}>
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
|
|||
commitSearchToHistory(e.currentTarget.value);
|
||||
}
|
||||
}}
|
||||
class={`w-full pl-9 pr-16 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
class={`w-full pl-8 sm:pl-9 pr-14 sm:pr-16 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500
|
||||
focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 dark:focus:border-blue-400 outline-none transition-all`}
|
||||
/>
|
||||
|
|
@ -158,7 +158,7 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
|
|||
<Show when={props.search()}>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-[4.5rem] top-1/2 -translate-y-1/2 transform p-1 rounded-full
|
||||
class="absolute right-[3.5rem] sm:right-[4.5rem] top-1/2 -translate-y-1/2 transform p-1 rounded-full
|
||||
bg-gray-200 dark:bg-gray-600 text-gray-500 dark:text-gray-300
|
||||
hover:bg-red-100 hover:text-red-600 dark:hover:bg-red-900/50 dark:hover:text-red-400
|
||||
transition-all duration-150 active:scale-90"
|
||||
|
|
@ -301,7 +301,7 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
|
|||
</div>
|
||||
|
||||
{/* Row 2: Filters - grouped for logical wrapping */}
|
||||
<div class="flex flex-wrap items-center gap-x-2 gap-y-2">
|
||||
<div class="flex flex-wrap items-center gap-x-1.5 sm:gap-x-2 gap-y-2">
|
||||
{/* Problems Toggle - Prominent "Show me issues" button */}
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -427,7 +427,7 @@ export const DashboardFilter: Component<DashboardFilterProps> = (props) => {
|
|||
{/* End Primary Filters Group */}
|
||||
|
||||
{/* Secondary Controls Group: Grouping, View, Columns, Reset */}
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="flex flex-wrap items-center gap-x-1.5 sm:gap-x-2 gap-y-2">
|
||||
|
||||
{/* Grouping Mode Toggle */}
|
||||
<div class="inline-flex rounded-lg bg-gray-100 dark:bg-gray-700 p-0.5">
|
||||
|
|
|
|||
|
|
@ -2806,7 +2806,7 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
|
|||
>
|
||||
<Card padding="none" tone="glass" class="overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full border-collapse whitespace-nowrap" style={{ "min-width": isMobile() ? "100%" : "800px" }}>
|
||||
<table class="w-full border-collapse whitespace-nowrap" style={{ "min-width": "800px" }}>
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-300 text-[11px] sm:text-xs font-medium uppercase tracking-wider sticky top-0 z-20">
|
||||
<For each={DOCKER_COLUMNS}>
|
||||
|
|
|
|||
|
|
@ -856,7 +856,7 @@ export const HostsOverview: Component = () => {
|
|||
<Card padding="none" tone="glass" class="overflow-hidden">
|
||||
<div class="overflow-x-auto" style="scrollbar-width: none; -ms-overflow-style: none;">
|
||||
<style>{`.overflow-x-auto::-webkit-scrollbar { display: none; }`}</style>
|
||||
<table class="w-full border-collapse whitespace-nowrap" style={{ "min-width": isMobile() ? "100%" : "900px" }}>
|
||||
<table class="w-full border-collapse whitespace-nowrap" style={{ "min-width": "800px" }}>
|
||||
<thead>
|
||||
<tr class="bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-300 border-b border-gray-200 dark:border-gray-700">
|
||||
{/* Essential columns */}
|
||||
|
|
@ -1168,7 +1168,7 @@ const HostRow: Component<HostRowProps> = (props) => {
|
|||
<EnhancedCPUBar
|
||||
usage={cpuPercent}
|
||||
loadAverage={host.loadAverage?.[0]}
|
||||
cores={host.cpuCount}
|
||||
cores={props.isMobile() ? undefined : host.cpuCount}
|
||||
/>
|
||||
</Show>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -60,17 +60,19 @@ export const ProxmoxSectionNav: Component<ProxmoxSectionNavProps> = (props) => {
|
|||
const hasCeph = state.cephClusters && state.cephClusters.length > 0;
|
||||
const hasReplication = state.replicationJobs && state.replicationJobs.length > 0;
|
||||
return allSections.filter((section) =>
|
||||
(section.id !== 'mail' || hasPMG) &&
|
||||
(section.id !== 'ceph' || hasCeph) &&
|
||||
(section.id !== 'replication' || hasReplication)
|
||||
section.id === props.current || (
|
||||
(section.id !== 'mail' || hasPMG) &&
|
||||
(section.id !== 'ceph' || hasCeph) &&
|
||||
(section.id !== 'replication' || hasReplication)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const baseClasses =
|
||||
'inline-flex items-center px-2 sm:px-3 py-1 text-xs sm:text-sm font-medium border-b-2 border-transparent text-gray-600 dark:text-gray-400 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400/60 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900';
|
||||
'inline-flex items-center px-1.5 sm:px-3 py-1 text-[11px] sm:text-sm font-medium border-b-2 border-transparent text-gray-600 dark:text-gray-400 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400/60 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900';
|
||||
|
||||
return (
|
||||
<div class={`flex flex-wrap items-center gap-3 sm:gap-4 ${props.class ?? ''}`} aria-label="Proxmox sections">
|
||||
<div class={`flex flex-wrap items-center gap-1.5 sm:gap-4 ${props.class ?? ''}`} aria-label="Proxmox sections">
|
||||
<For each={sections()}>{(section) => {
|
||||
const isActive = section.id === props.current;
|
||||
const classes = isActive
|
||||
|
|
|
|||
|
|
@ -292,7 +292,7 @@ const findMatchingHostAgent = (
|
|||
export const PveNodesTable: Component<PveNodesTableProps> = (props) => {
|
||||
return (
|
||||
<Card padding="none" tone="glass" class="overflow-x-auto rounded-lg">
|
||||
<table class="min-w-[800px] divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||
<table class="min-w-[900px] divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800/70">
|
||||
<tr>
|
||||
<th scope="col" class="py-2 pl-4 pr-3 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
|
|
@ -594,7 +594,7 @@ const resolvePbsStatusMeta = (
|
|||
export const PbsNodesTable: Component<PbsNodesTableProps> = (props) => {
|
||||
return (
|
||||
<Card padding="none" tone="glass" class="overflow-x-auto rounded-lg">
|
||||
<table class="min-w-[800px] divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||
<table class="min-w-[900px] divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800/70">
|
||||
<tr>
|
||||
<th scope="col" class="py-2 pl-4 pr-3 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
|
|
@ -790,7 +790,7 @@ const resolvePmgStatusMeta = (
|
|||
export const PmgNodesTable: Component<PmgNodesTableProps> = (props) => {
|
||||
return (
|
||||
<Card padding="none" tone="glass" class="overflow-x-auto rounded-lg">
|
||||
<table class="min-w-[800px] divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||
<table class="min-w-[900px] divide-y divide-gray-200 dark:divide-gray-700 text-sm">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800/70">
|
||||
<tr>
|
||||
<th scope="col" class="py-2 pl-4 pr-3 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
|
|
|
|||
|
|
@ -2406,7 +2406,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
<Show when={flatTabs.length > 0}>
|
||||
<div class="lg:hidden border-b border-gray-200 dark:border-gray-700">
|
||||
<div
|
||||
class="flex gap-1 px-2 py-1 w-full overflow-x-auto scrollbar-hide"
|
||||
class="flex gap-1 px-2 py-1 w-full overflow-x-auto"
|
||||
style="-webkit-overflow-scrolling: touch;"
|
||||
>
|
||||
<For each={flatTabs}>
|
||||
|
|
@ -2489,9 +2489,9 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
</div>
|
||||
</Show>
|
||||
<Show when={initialLoadComplete()}>
|
||||
<Card padding="lg">
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<Card padding="none" tone="glass">
|
||||
<div class="px-3 py-4 sm:px-6 sm:py-6 space-y-4">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-gray-100">
|
||||
Proxmox VE nodes
|
||||
</h4>
|
||||
|
|
@ -2779,9 +2779,9 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
</div>
|
||||
</Show>
|
||||
<Show when={initialLoadComplete()}>
|
||||
<Card padding="lg">
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<Card padding="none" tone="glass">
|
||||
<div class="px-3 py-4 sm:px-6 sm:py-6 space-y-4">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-gray-100">
|
||||
Proxmox Backup Server nodes
|
||||
</h4>
|
||||
|
|
@ -2909,7 +2909,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
No PBS nodes configured
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Add a Proxmox Backup Server to monitor your backup infrastructure
|
||||
Add a Proxmox Backup Server to monitor your backups
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
|
@ -3066,9 +3066,9 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
</Show>
|
||||
|
||||
<Show when={initialLoadComplete()}>
|
||||
<Card padding="lg">
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<Card padding="none" tone="glass">
|
||||
<div class="px-3 py-4 sm:px-6 sm:py-6 space-y-4">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-gray-100">
|
||||
Proxmox Mail Gateway nodes
|
||||
</h4>
|
||||
|
|
@ -3174,7 +3174,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
globalTemperatureMonitoringEnabled={temperatureMonitoringEnabled()}
|
||||
onTestConnection={testNodeConnection}
|
||||
onEdit={(node) => {
|
||||
setEditingNode(nodes().find((n) => n.id === node.id) ?? null);
|
||||
setEditingNode(node);
|
||||
setCurrentNodeType('pmg');
|
||||
setModalResetKey((prev) => prev + 1);
|
||||
setShowNodeModal(true);
|
||||
|
|
@ -3192,8 +3192,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
No PMG nodes configured
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Add a Proxmox Mail Gateway to monitor mail queue and quarantine
|
||||
metrics
|
||||
Add a Proxmox Mail Gateway node to start monitoring
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
|
@ -3515,7 +3514,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
<DiagnosticsPanel />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
</Card >
|
||||
</div >
|
||||
|
||||
|
|
@ -3793,7 +3792,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
</Show >
|
||||
|
||||
{/* Update Confirmation Modal */}
|
||||
<UpdateConfirmationModal
|
||||
< UpdateConfirmationModal
|
||||
isOpen={showUpdateConfirmation()}
|
||||
onClose={() => setShowUpdateConfirmation(false)}
|
||||
onConfirm={handleConfirmUpdate}
|
||||
|
|
|
|||
|
|
@ -17,29 +17,29 @@ const allSections: Array<{
|
|||
label: string;
|
||||
icon: typeof Server;
|
||||
}> = [
|
||||
{
|
||||
id: 'pve',
|
||||
label: 'Virtual Environment',
|
||||
icon: Server,
|
||||
},
|
||||
{
|
||||
id: 'pbs',
|
||||
label: 'Backup Server',
|
||||
icon: HardDrive,
|
||||
},
|
||||
{
|
||||
id: 'pmg',
|
||||
label: 'Mail Gateway',
|
||||
icon: Mail,
|
||||
},
|
||||
];
|
||||
{
|
||||
id: 'pve',
|
||||
label: 'Virtual Environment',
|
||||
icon: Server,
|
||||
},
|
||||
{
|
||||
id: 'pbs',
|
||||
label: 'Backup Server',
|
||||
icon: HardDrive,
|
||||
},
|
||||
{
|
||||
id: 'pmg',
|
||||
label: 'Mail Gateway',
|
||||
icon: Mail,
|
||||
},
|
||||
];
|
||||
|
||||
export const SettingsSectionNav: Component<SettingsSectionNavProps> = (props) => {
|
||||
const baseClasses =
|
||||
'inline-flex items-center gap-2 px-2 sm:px-3 py-1 text-xs sm:text-sm font-medium border-b-2 border-transparent text-gray-600 dark:text-gray-400 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-400/60 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-gray-900';
|
||||
|
||||
return (
|
||||
<div class={`flex flex-wrap items-center gap-3 sm:gap-4 ${props.class ?? ''}`} aria-label="Settings sections">
|
||||
<div class={`flex flex-wrap items-center gap-2 sm:gap-4 ${props.class ?? ''}`} aria-label="Settings sections">
|
||||
<For each={allSections}>
|
||||
{(section) => {
|
||||
const isActive = section.id === props.current;
|
||||
|
|
@ -56,8 +56,8 @@ export const SettingsSectionNav: Component<SettingsSectionNavProps> = (props) =>
|
|||
onClick={() => props.onSelect(section.id)}
|
||||
aria-pressed={isActive}
|
||||
>
|
||||
<Icon size={16} stroke-width={2} />
|
||||
<span>{section.label}</span>
|
||||
<Icon size={14} stroke-width={2} class="sm:w-4 sm:h-4" />
|
||||
<span class="whitespace-nowrap">{section.label}</span>
|
||||
</button>
|
||||
);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export function EnhancedStorageBar(props: EnhancedStorageBarProps) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} class="metric-text w-full h-4 flex items-center justify-center">
|
||||
<div ref={containerRef} class="metric-text w-full h-5 flex items-center justify-center">
|
||||
<div
|
||||
class="relative w-full max-w-[150px] h-full overflow-hidden bg-gray-200 dark:bg-gray-600 rounded"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useWebSocket } from '@/App';
|
|||
import { getAlertStyles } from '@/utils/alerts';
|
||||
import { formatBytes, formatPercent } from '@/utils/format';
|
||||
import type { Storage as StorageType, CephCluster } from '@/types/api';
|
||||
import { StatusDot } from '@/components/shared/StatusDot';
|
||||
import { ComponentErrorBoundary } from '@/components/ErrorBoundary';
|
||||
import { UnifiedNodeSelector } from '@/components/shared/UnifiedNodeSelector';
|
||||
import { StorageFilter } from './StorageFilter';
|
||||
|
|
@ -809,7 +810,7 @@ const Storage: Component = () => {
|
|||
<style>{`
|
||||
.overflow-x-auto::-webkit-scrollbar { display: none; }
|
||||
`}</style>
|
||||
<table class="w-full" style={{ "min-width": "750px" }}>
|
||||
<table class="w-full" style={{ "min-width": "800px" }}>
|
||||
<thead>
|
||||
<tr class="bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-300 border-b border-gray-200 dark:border-gray-600">
|
||||
<th class="px-1.5 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-auto">
|
||||
|
|
@ -826,16 +827,16 @@ const Storage: Component = () => {
|
|||
</th>
|
||||
</Show>
|
||||
<Show when={isColumnVisible('status')}>
|
||||
<th class="px-1.5 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[10%]">
|
||||
<th class="px-1.5 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[10%] min-w-[70px]">
|
||||
Status
|
||||
</th>
|
||||
</Show>
|
||||
<Show when={viewMode() === 'node' && isColumnVisible('shared')}>
|
||||
<th class="px-1.5 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[6%]">
|
||||
<th class="hidden sm:table-cell px-1.5 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[6%]">
|
||||
Shared
|
||||
</th>
|
||||
</Show>
|
||||
<th class="px-1.5 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[25%] min-w-[120px]">
|
||||
<th class="px-1.5 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-[25%] min-w-[130px]">
|
||||
Usage
|
||||
</th>
|
||||
<Show when={isColumnVisible('free')}>
|
||||
|
|
@ -1187,9 +1188,16 @@ const Storage: Component = () => {
|
|||
</td>
|
||||
<Show when={isColumnVisible('type')}>
|
||||
<td class="p-0.5 px-1.5">
|
||||
<span class="inline-block px-1.5 py-0.5 text-[10px] font-medium rounded bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300">
|
||||
{storage.type}
|
||||
</span>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="inline-block px-1.5 py-0.5 text-[10px] font-medium rounded bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300">
|
||||
{storage.type}
|
||||
</span>
|
||||
<Show when={storage.shared}>
|
||||
<svg class="h-3 w-3 text-blue-500 sm:hidden" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M3 6a3 3 0 013-3h10a1 1 0 01.8 1.6L14.25 8l2.55 3.4A1 1 0 0116 13H6a1 1 0 00-1 1v3a1 1 0 11-2 0V6z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</Show>
|
||||
</div>
|
||||
</td>
|
||||
</Show>
|
||||
<Show when={isColumnVisible('content')}>
|
||||
|
|
@ -1204,18 +1212,24 @@ const Storage: Component = () => {
|
|||
</Show>
|
||||
<Show when={isColumnVisible('status')}>
|
||||
<td class="p-0.5 px-1.5 text-xs whitespace-nowrap">
|
||||
<span
|
||||
class={`${storage.status === 'available'
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
: 'text-red-600 dark:text-red-400'
|
||||
}`}
|
||||
>
|
||||
{storage.status || 'unknown'}
|
||||
</span>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<StatusDot
|
||||
variant={storage.status === 'available' ? 'success' : 'danger'}
|
||||
size="xs"
|
||||
/>
|
||||
<span
|
||||
class={`${storage.status === 'available'
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
: 'text-red-600 dark:text-red-400'
|
||||
}`}
|
||||
>
|
||||
{storage.status || 'unknown'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</Show>
|
||||
<Show when={viewMode() === 'node' && isColumnVisible('shared')}>
|
||||
<td class="p-0.5 px-1.5">
|
||||
<td class="hidden sm:table-cell p-0.5 px-1.5">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{storage.shared ? '✓' : '-'}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ export const StorageFilter: Component<StorageFilterProps> = (props) => {
|
|||
commitSearchToHistory(e.currentTarget.value);
|
||||
}
|
||||
}}
|
||||
class="w-full pl-9 pr-20 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
class="w-full pl-8 sm:pl-9 pr-14 sm:pr-20 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg
|
||||
bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500
|
||||
focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 dark:focus:border-blue-400 outline-none transition-all"
|
||||
title="Search storage by name or filter by node"
|
||||
|
|
@ -167,7 +167,7 @@ export const StorageFilter: Component<StorageFilterProps> = (props) => {
|
|||
<Show when={props.search()}>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-14 top-1/2 -translate-y-1/2 transform p-1 rounded-full
|
||||
class="absolute right-[3.5rem] sm:right-14 top-1/2 -translate-y-1/2 transform p-1 rounded-full
|
||||
bg-gray-200 dark:bg-gray-600 text-gray-500 dark:text-gray-300
|
||||
hover:bg-red-100 hover:text-red-600 dark:hover:bg-red-900/50 dark:hover:text-red-400
|
||||
transition-all duration-150 active:scale-90"
|
||||
|
|
@ -297,7 +297,7 @@ export const StorageFilter: Component<StorageFilterProps> = (props) => {
|
|||
</div>
|
||||
|
||||
{/* Row 2: Filters - Compact horizontal layout */}
|
||||
<div class="flex flex-wrap items-center gap-x-2 gap-y-2">
|
||||
<div class="flex flex-wrap items-center gap-x-1.5 sm:gap-x-2 gap-y-2">
|
||||
{/* Group By Filter */}
|
||||
<Show when={props.groupBy && props.setGroupBy}>
|
||||
<div class="inline-flex rounded-lg bg-gray-100 dark:bg-gray-700 p-0.5">
|
||||
|
|
|
|||
|
|
@ -348,7 +348,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
|
|||
return (
|
||||
<Card padding="none" tone="glass" class="mb-4 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full border-collapse whitespace-nowrap" style={{ "min-width": isMobile() ? "100%" : "800px" }}>
|
||||
<table class="w-full border-collapse whitespace-nowrap" style={{ "min-width": "800px" }}>
|
||||
<thead>
|
||||
<tr class="bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-300 border-b border-gray-200 dark:border-gray-700">
|
||||
<th
|
||||
|
|
@ -360,13 +360,13 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
|
|||
<th class={thClass} style={{ "min-width": '80px' }} onClick={() => handleSort('uptime')}>
|
||||
Uptime {renderSortIndicator('uptime')}
|
||||
</th>
|
||||
<th class={thClass} style={isMobile() ? { "min-width": "60px" } : { width: "200px", "min-width": "200px", "max-width": "200px" }} onClick={() => handleSort('cpu')}>
|
||||
<th class={thClass} style={isMobile() ? { "min-width": "80px" } : { width: "140px", "min-width": "140px", "max-width": "140px" }} onClick={() => handleSort('cpu')}>
|
||||
CPU {renderSortIndicator('cpu')}
|
||||
</th>
|
||||
<th class={thClass} style={isMobile() ? { "min-width": "60px" } : { width: "200px", "min-width": "200px", "max-width": "200px" }} onClick={() => handleSort('memory')}>
|
||||
<th class={thClass} style={isMobile() ? { "min-width": "80px" } : { width: "140px", "min-width": "140px", "max-width": "140px" }} onClick={() => handleSort('memory')}>
|
||||
Memory {renderSortIndicator('memory')}
|
||||
</th>
|
||||
<th class={thClass} style={isMobile() ? { "min-width": "60px" } : { width: "200px", "min-width": "200px", "max-width": "200px" }} onClick={() => handleSort('disk')}>
|
||||
<th class={thClass} style={isMobile() ? { "min-width": "80px" } : { width: "140px", "min-width": "140px", "max-width": "140px" }} onClick={() => handleSort('disk')}>
|
||||
Disk {renderSortIndicator('disk')}
|
||||
</th>
|
||||
<Show when={hasAnyTemperatureData()}>
|
||||
|
|
@ -483,7 +483,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
|
|||
return (
|
||||
<tr
|
||||
class={rowClass()}
|
||||
style={{ ...rowStyle(), height: '29px', 'max-height': '29px' }}
|
||||
style={{ ...rowStyle(), 'min-height': '36px' }}
|
||||
onClick={() => props.onNodeClick(nodeId, isPVEItem ? 'pve' : 'pbs')}
|
||||
>
|
||||
{/* Name */}
|
||||
|
|
@ -575,8 +575,8 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
|
|||
</td>
|
||||
|
||||
{/* CPU */}
|
||||
<td class={tdClass} style={isMobile() ? { "min-width": "60px" } : metricColumnStyle}>
|
||||
<div class="h-4">
|
||||
<td class={tdClass} style={isMobile() ? { "min-width": "80px" } : metricColumnStyle}>
|
||||
<div class="h-5">
|
||||
<EnhancedCPUBar
|
||||
usage={cpuPercentValue}
|
||||
loadAverage={isPVEItem ? node!.loadAverage?.[0] : undefined}
|
||||
|
|
@ -588,8 +588,8 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
|
|||
</td>
|
||||
|
||||
{/* Memory */}
|
||||
<td class={tdClass} style={isMobile() ? { "min-width": "60px" } : metricColumnStyle}>
|
||||
<div class="h-4">
|
||||
<td class={tdClass} style={isMobile() ? { "min-width": "80px" } : metricColumnStyle}>
|
||||
<div class="h-5">
|
||||
<Show when={isPVEItem} fallback={
|
||||
<ResponsiveMetricCell
|
||||
value={memoryPercentValue}
|
||||
|
|
@ -626,8 +626,8 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
|
|||
</td>
|
||||
|
||||
{/* Disk */}
|
||||
<td class={tdClass} style={isMobile() ? { "min-width": "60px" } : metricColumnStyle}>
|
||||
<div class="h-4">
|
||||
<td class={tdClass} style={isMobile() ? { "min-width": "80px" } : metricColumnStyle}>
|
||||
<div class="h-5">
|
||||
<Show when={isPVEItem} fallback={
|
||||
<ResponsiveMetricCell
|
||||
value={diskPercentValue}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { createSignal, onMount, onCleanup, createMemo, Accessor } from 'solid-js
|
|||
* These match Tailwind's default breakpoints
|
||||
*/
|
||||
export const BREAKPOINTS = {
|
||||
xs: 0,
|
||||
xs: 400,
|
||||
sm: 640,
|
||||
md: 768,
|
||||
lg: 1024,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -7,6 +7,14 @@ export default {
|
|||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
screens: {
|
||||
'xs': '400px',
|
||||
'sm': '640px',
|
||||
'md': '768px',
|
||||
'lg': '1024px',
|
||||
'xl': '1280px',
|
||||
'2xl': '1536px',
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
gray: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue