Enhance table responsiveness across multiple components
This commit is contained in:
parent
d36b4ce6c8
commit
c021dc6ca5
9 changed files with 848 additions and 884 deletions
|
|
@ -397,8 +397,7 @@ export function ResourceTable(props: ResourceTableProps) {
|
||||||
title={isDisabledMetric ? 'Disabled (no alerts for this metric)' : ''}
|
title={isDisabledMetric ? 'Disabled (no alerts for this metric)' : ''}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class={`text-sm ${
|
class={`text-sm ${isDisabledMetric
|
||||||
isDisabledMetric
|
|
||||||
? 'text-gray-400 dark:text-gray-500 italic'
|
? 'text-gray-400 dark:text-gray-500 italic'
|
||||||
: metricProps.isOverridden
|
: metricProps.isOverridden
|
||||||
? 'text-gray-900 dark:text-gray-100 font-bold'
|
? 'text-gray-900 dark:text-gray-100 font-bold'
|
||||||
|
|
@ -591,8 +590,7 @@ export function ResourceTable(props: ResourceTableProps) {
|
||||||
props.setHasUnsavedChanges(true);
|
props.setHasUnsavedChanges(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
class={`w-16 px-2 py-0.5 text-sm text-center border rounded ${
|
class={`w-16 px-2 py-0.5 text-sm text-center border rounded ${isOff()
|
||||||
isOff()
|
|
||||||
? 'border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500 italic placeholder:text-gray-400 dark:placeholder:text-gray-500 placeholder:opacity-60 pointer-events-none'
|
? 'border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500 italic placeholder:text-gray-400 dark:placeholder:text-gray-500 placeholder:opacity-60 pointer-events-none'
|
||||||
: 'border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500'
|
: 'border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:border-blue-500 focus:ring-1 focus:ring-blue-500'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -908,26 +906,26 @@ export function ResourceTable(props: ResourceTableProps) {
|
||||||
</Show>
|
</Show>
|
||||||
</td>
|
</td>
|
||||||
<td class="p-1 px-2">
|
<td class="p-1 px-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
<Show
|
<Show
|
||||||
when={resource.type === 'node'}
|
when={resource.type === 'node'}
|
||||||
fallback={
|
fallback={
|
||||||
<span
|
<span
|
||||||
class={`text-sm font-medium ${resource.disabled ? 'text-gray-500 dark:text-gray-500' : 'text-gray-900 dark:text-gray-100'}`}
|
class={`text-sm font-medium truncate flex-nowrap ${resource.disabled ? 'text-gray-500 dark:text-gray-500' : 'text-gray-900 dark:text-gray-100'}`}
|
||||||
>
|
>
|
||||||
{resource.name}
|
{resource.name}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-wrap items-center gap-3"
|
class="flex items-center gap-3 min-w-0"
|
||||||
title={resource.status || undefined}
|
title={resource.status || undefined}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={resource.host}
|
when={resource.host}
|
||||||
fallback={
|
fallback={
|
||||||
<span
|
<span
|
||||||
class={`text-sm font-medium ${resource.disabled ? 'text-gray-500 dark:text-gray-500' : 'text-gray-900 dark:text-gray-100'}`}
|
class={`text-sm font-medium truncate flex-nowrap ${resource.disabled ? 'text-gray-500 dark:text-gray-500' : 'text-gray-900 dark:text-gray-100'}`}
|
||||||
>
|
>
|
||||||
{resource.type === 'node'
|
{resource.type === 'node'
|
||||||
? resource.name
|
? resource.name
|
||||||
|
|
@ -941,8 +939,7 @@ export function ResourceTable(props: ResourceTableProps) {
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
class={`text-sm font-medium transition-colors duration-150 ${
|
class={`text-sm font-medium truncate flex-nowrap transition-colors duration-150 ${resource.disabled
|
||||||
resource.disabled
|
|
||||||
? 'text-gray-500 dark:text-gray-500'
|
? 'text-gray-500 dark:text-gray-500'
|
||||||
: 'text-gray-900 dark:text-gray-100 hover:text-sky-600 dark:hover:text-sky-400'
|
: 'text-gray-900 dark:text-gray-100 hover:text-sky-600 dark:hover:text-sky-400'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -1150,8 +1147,7 @@ export function ResourceTable(props: ResourceTableProps) {
|
||||||
}
|
}
|
||||||
setActiveMetricInput(null);
|
setActiveMetricInput(null);
|
||||||
}}
|
}}
|
||||||
class={`w-16 px-2 py-0.5 text-sm text-center border rounded ${
|
class={`w-16 px-2 py-0.5 text-sm text-center border rounded ${isDisabled()
|
||||||
isDisabled()
|
|
||||||
? 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-600 border-gray-300 dark:border-gray-600'
|
? 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-600 border-gray-300 dark:border-gray-600'
|
||||||
: 'bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 border-gray-300 dark:border-gray-600'
|
: 'bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 border-gray-300 dark:border-gray-600'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -1423,9 +1419,9 @@ export function ResourceTable(props: ResourceTableProps) {
|
||||||
<Show
|
<Show
|
||||||
when={resource.type === 'node'}
|
when={resource.type === 'node'}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
<span
|
<span
|
||||||
class={`text-sm font-medium ${resource.disabled ? 'text-gray-500 dark:text-gray-500' : 'text-gray-900 dark:text-gray-100'}`}
|
class={`text-sm font-medium truncate flex-nowrap ${resource.disabled ? 'text-gray-500 dark:text-gray-500' : 'text-gray-900 dark:text-gray-100'}`}
|
||||||
>
|
>
|
||||||
{resource.name}
|
{resource.name}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -1438,14 +1434,14 @@ export function ResourceTable(props: ResourceTableProps) {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-wrap items-center gap-3"
|
class="flex items-center gap-3 min-w-0"
|
||||||
title={resource.status || undefined}
|
title={resource.status || undefined}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={resource.host}
|
when={resource.host}
|
||||||
fallback={
|
fallback={
|
||||||
<span
|
<span
|
||||||
class={`text-sm font-medium ${resource.disabled ? 'text-gray-500 dark:text-gray-500' : 'text-gray-900 dark:text-gray-100'}`}
|
class={`text-sm font-medium truncate flex-nowrap ${resource.disabled ? 'text-gray-500 dark:text-gray-500' : 'text-gray-900 dark:text-gray-100'}`}
|
||||||
>
|
>
|
||||||
{resource.type === 'node'
|
{resource.type === 'node'
|
||||||
? resource.name
|
? resource.name
|
||||||
|
|
@ -1459,8 +1455,7 @@ export function ResourceTable(props: ResourceTableProps) {
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
class={`text-sm font-medium transition-colors duration-150 ${
|
class={`text-sm font-medium truncate flex-nowrap transition-colors duration-150 ${resource.disabled
|
||||||
resource.disabled
|
|
||||||
? 'text-gray-500 dark:text-gray-500'
|
? 'text-gray-500 dark:text-gray-500'
|
||||||
: 'text-gray-900 dark:text-gray-100 hover:text-sky-600 dark:hover:text-sky-400'
|
: 'text-gray-900 dark:text-gray-100 hover:text-sky-600 dark:hover:text-sky-400'
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -1604,8 +1599,7 @@ export function ResourceTable(props: ResourceTableProps) {
|
||||||
}
|
}
|
||||||
setActiveMetricInput(null);
|
setActiveMetricInput(null);
|
||||||
}}
|
}}
|
||||||
class={`w-16 px-2 py-0.5 text-sm text-center border rounded ${
|
class={`w-16 px-2 py-0.5 text-sm text-center border rounded ${isDisabled()
|
||||||
isDisabled()
|
|
||||||
? 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-600 border-gray-300 dark:border-gray-600'
|
? 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-600 border-gray-300 dark:border-gray-600'
|
||||||
: 'bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 border-gray-300 dark:border-gray-600'
|
: 'bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 border-gray-300 dark:border-gray-600'
|
||||||
}`}
|
}`}
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,6 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
|
||||||
const selected = props.selectedHostId() === summary.host.id;
|
const selected = props.selectedHostId() === summary.host.id;
|
||||||
const online = isHostOnline(summary.host);
|
const online = isHostOnline(summary.host);
|
||||||
const uptimeLabel = summary.uptimeSeconds ? formatUptime(summary.uptimeSeconds) : '—';
|
const uptimeLabel = summary.uptimeSeconds ? formatUptime(summary.uptimeSeconds) : '—';
|
||||||
const agentLabel = summary.host.agentVersion ? summary.host.agentVersion : '—';
|
|
||||||
|
|
||||||
const rowStyle = () => {
|
const rowStyle = () => {
|
||||||
const styles: Record<string, string> = {};
|
const styles: Record<string, string> = {};
|
||||||
|
|
@ -256,21 +255,22 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
|
||||||
onClick={() => props.onSelect(summary.host.id)}
|
onClick={() => props.onSelect(summary.host.id)}
|
||||||
>
|
>
|
||||||
<td class="pr-2 py-1 pl-3 align-middle">
|
<td class="pr-2 py-1 pl-3 align-middle">
|
||||||
<div class="flex flex-wrap items-center gap-1.5 sm:flex-nowrap sm:whitespace-nowrap sm:min-w-0">
|
<div class="flex items-center gap-1.5 min-w-0">
|
||||||
<StatusDot
|
<StatusDot
|
||||||
variant={hostStatus().variant}
|
variant={hostStatus().variant}
|
||||||
title={hostStatus().label}
|
title={hostStatus().label}
|
||||||
ariaLabel={hostStatus().label}
|
ariaLabel={hostStatus().label}
|
||||||
size="xs"
|
size="xs"
|
||||||
/>
|
/>
|
||||||
<span class="font-medium text-[11px] text-gray-900 dark:text-gray-100 sm:truncate sm:max-w-[200px]">
|
<span class="font-medium text-[11px] text-gray-900 dark:text-gray-100 truncate" title={getDisplayName(summary.host)}>
|
||||||
{getDisplayName(summary.host)}
|
{getDisplayName(summary.host)}
|
||||||
</span>
|
</span>
|
||||||
<Show when={getDisplayName(summary.host) !== summary.host.hostname}>
|
<Show when={getDisplayName(summary.host) !== summary.host.hostname}>
|
||||||
<span class="hidden sm:inline text-[9px] text-gray-500 dark:text-gray-400 sm:whitespace-nowrap">
|
<span class="hidden sm:inline text-[9px] text-gray-500 dark:text-gray-400 whitespace-nowrap">
|
||||||
({summary.host.hostname})
|
({summary.host.hostname})
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
|
<div class="hidden xl:flex items-center gap-1.5 ml-1">
|
||||||
<span
|
<span
|
||||||
class={`text-[9px] px-1 py-0 rounded font-medium whitespace-nowrap ${runtimeInfo.badgeClass}`}
|
class={`text-[9px] px-1 py-0 rounded font-medium whitespace-nowrap ${runtimeInfo.badgeClass}`}
|
||||||
title={runtimeInfo.raw || runtimeInfo.label}
|
title={runtimeInfo.raw || runtimeInfo.label}
|
||||||
|
|
@ -283,45 +283,6 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 grid grid-cols-1 gap-1 text-[10px] text-gray-500 dark:text-gray-400 sm:hidden">
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<span class="font-semibold text-gray-600 dark:text-gray-300">Uptime:</span>
|
|
||||||
<span>{uptimeLabel}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-start gap-1">
|
|
||||||
<span class="font-semibold text-gray-600 dark:text-gray-300">Last:</span>
|
|
||||||
<span class="flex-1">
|
|
||||||
<span>{summary.lastSeenRelative}</span>
|
|
||||||
<Show when={summary.lastSeenAbsolute}>
|
|
||||||
<span class="block text-[9px] text-gray-400 dark:text-gray-500">
|
|
||||||
{summary.lastSeenAbsolute}
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap items-center gap-1">
|
|
||||||
<span class="font-semibold text-gray-600 dark:text-gray-300">Agent:</span>
|
|
||||||
<span
|
|
||||||
class={
|
|
||||||
agentOutdated
|
|
||||||
? 'rounded px-1 py-0.5 bg-yellow-100 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-400 font-medium'
|
|
||||||
: 'rounded px-1 py-0.5 bg-green-100 dark:bg-green-900/20 text-green-700 dark:text-green-400 font-medium'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{agentLabel}
|
|
||||||
</span>
|
|
||||||
<Show when={agentOutdated}>
|
|
||||||
<span class="text-[9px] font-medium text-yellow-600 dark:text-yellow-500">
|
|
||||||
Update recommended
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<Show when={summary.host.intervalSeconds}>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<span class="font-semibold text-gray-600 dark:text-gray-300">Interval:</span>
|
|
||||||
<span>{summary.host.intervalSeconds}s</span>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-0.5 md:px-2 py-1 align-middle">
|
<td class="px-0.5 md:px-2 py-1 align-middle">
|
||||||
|
|
|
||||||
|
|
@ -122,15 +122,15 @@ interface DockerColumnDef extends ColumnConfig {
|
||||||
// - supplementary: Visible on large screens and up (lg: 1024px+)
|
// - supplementary: Visible on large screens and up (lg: 1024px+)
|
||||||
// - detailed: Visible on extra large screens and up (xl: 1280px+)
|
// - detailed: Visible on extra large screens and up (xl: 1280px+)
|
||||||
export const DOCKER_COLUMNS: DockerColumnDef[] = [
|
export const DOCKER_COLUMNS: DockerColumnDef[] = [
|
||||||
{ id: 'resource', label: 'Resource', priority: 'essential', minWidth: '150px', flex: 2, sortKey: 'resource' },
|
{ id: 'resource', label: 'Resource', priority: 'essential', minWidth: '100px', flex: 1.5, sortKey: 'resource' },
|
||||||
{ id: 'type', label: 'Type', priority: 'primary', minWidth: '60px', maxWidth: '80px', sortKey: 'type' },
|
{ id: 'type', label: 'Type', priority: 'essential', minWidth: '24px', maxWidth: '50px', sortKey: 'type' },
|
||||||
{ id: 'image', label: 'Image / Stack', shortLabel: 'Image', priority: 'supplementary', minWidth: '100px', flex: 1, sortKey: 'image' },
|
{ id: 'image', label: 'Image / Stack', shortLabel: 'Image', priority: 'essential', minWidth: '80px', flex: 1, sortKey: 'image' },
|
||||||
{ id: 'status', label: 'Status', priority: 'essential', minWidth: '80px', maxWidth: '120px', sortKey: 'status' },
|
{ id: 'status', label: 'Status', priority: 'essential', minWidth: '60px', maxWidth: '100px', sortKey: 'status' },
|
||||||
{ id: 'cpu', label: 'CPU', priority: 'secondary', minWidth: '80px', flex: 1, sortKey: 'cpu' },
|
{ id: 'cpu', label: 'CPU', priority: 'essential', minWidth: '50px', flex: 1, sortKey: 'cpu' },
|
||||||
{ id: 'memory', label: 'Memory', shortLabel: 'Mem', priority: 'secondary', minWidth: '90px', flex: 1, sortKey: 'memory' },
|
{ id: 'memory', label: 'Memory', shortLabel: 'Mem', priority: 'essential', minWidth: '50px', flex: 1, sortKey: 'memory' },
|
||||||
{ id: 'disk', label: 'Disk', priority: 'detailed', minWidth: '90px', flex: 1, sortKey: 'disk' },
|
{ id: 'disk', label: 'Disk', priority: 'essential', minWidth: '50px', flex: 1, sortKey: 'disk' },
|
||||||
{ id: 'tasks', label: 'Tasks / Restarts', shortLabel: 'Tasks', priority: 'supplementary', minWidth: '70px', maxWidth: '100px', sortKey: 'tasks' },
|
{ id: 'tasks', label: 'Tasks / Restarts', shortLabel: 'Tasks', priority: 'essential', minWidth: '40px', maxWidth: '70px', sortKey: 'tasks' },
|
||||||
{ id: 'updated', label: 'Updated / Uptime', shortLabel: 'Updated', priority: 'primary', minWidth: '60px', maxWidth: '90px', sortKey: 'updated' },
|
{ id: 'updated', label: 'Updated / Uptime', shortLabel: 'Updated', priority: 'essential', minWidth: '50px', maxWidth: '80px', sortKey: 'updated' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Global state for currently expanded drawer (only one drawer open at a time)
|
// Global state for currently expanded drawer (only one drawer open at a time)
|
||||||
|
|
@ -1080,7 +1080,7 @@ const DockerContainerRow: Component<{
|
||||||
switch (column.id) {
|
switch (column.id) {
|
||||||
case 'resource':
|
case 'resource':
|
||||||
return (
|
return (
|
||||||
<div class={`${resourceIndent()} pr-2 py-0.5`}>
|
<div class={`${resourceIndent()} pr-2 py-0.5 overflow-hidden`}>
|
||||||
<div class="flex items-center gap-1.5 min-w-0">
|
<div class="flex items-center gap-1.5 min-w-0">
|
||||||
<StatusDot
|
<StatusDot
|
||||||
variant={containerStatusIndicator().variant}
|
variant={containerStatusIndicator().variant}
|
||||||
|
|
@ -1088,13 +1088,13 @@ const DockerContainerRow: Component<{
|
||||||
ariaLabel={containerStatusIndicator().label}
|
ariaLabel={containerStatusIndicator().label}
|
||||||
size="xs"
|
size="xs"
|
||||||
/>
|
/>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0 truncate">
|
||||||
<Show
|
<Show
|
||||||
when={isEditingUrl()}
|
when={isEditingUrl()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="flex items-center gap-1.5 flex-1 min-w-0">
|
<div class="flex items-center gap-1.5 flex-1 min-w-0">
|
||||||
<span
|
<span
|
||||||
class="text-sm font-semibold text-gray-900 dark:text-gray-100 cursor-text select-none"
|
class="text-sm font-semibold text-gray-900 dark:text-gray-100 cursor-text select-none truncate"
|
||||||
style="cursor: text;"
|
style="cursor: text;"
|
||||||
title={`${containerTitle()}${customUrl() ? ' - Click to edit URL' : ' - Click to add URL'}`}
|
title={`${containerTitle()}${customUrl() ? ' - Click to edit URL' : ' - Click to add URL'}`}
|
||||||
onClick={startEditingUrl}
|
onClick={startEditingUrl}
|
||||||
|
|
@ -1104,10 +1104,10 @@ const DockerContainerRow: Component<{
|
||||||
</span>
|
</span>
|
||||||
<Show when={podName()}>
|
<Show when={podName()}>
|
||||||
{(name) => (
|
{(name) => (
|
||||||
<span class="inline-flex items-center gap-1 rounded bg-purple-100 px-1.5 py-0.5 text-[10px] font-medium text-purple-700 dark:bg-purple-900/40 dark:text-purple-200">
|
<span class="inline-flex items-center gap-1 rounded bg-purple-100 px-1.5 py-0.5 text-[10px] font-medium text-purple-700 dark:bg-purple-900/40 dark:text-purple-200 flex-shrink-0">
|
||||||
Pod: {name()}
|
Pod: {name()}
|
||||||
<Show when={isPodInfra()}>
|
<Show when={isPodInfra()}>
|
||||||
<span class="rounded bg-purple-200 px-1 py-0.5 text-[9px] uppercase text-purple-800 dark:bg-purple-800/50 dark:text-purple-200">
|
<span class="rounded bg-purple-200 px-1 py-0.5 text-[9px] uppercase text-purple-800 dark:bg-purple-800/50 dark:text-purple-200 ml-1">
|
||||||
infra
|
infra
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -1130,11 +1130,11 @@ const DockerContainerRow: Component<{
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.showHostContext}>
|
<Show when={props.showHostContext}>
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center gap-1 rounded bg-gray-100 px-1.5 py-0.5 text-[10px] font-medium text-gray-600 dark:bg-gray-800 dark:text-gray-300"
|
class="inline-flex items-center gap-1 rounded bg-gray-100 px-1.5 py-0.5 text-[10px] font-medium text-gray-600 dark:bg-gray-800 dark:text-gray-300 flex-shrink-0 max-w-[120px]"
|
||||||
title={`Host: ${hostDisplayName()}`}
|
title={`Host: ${hostDisplayName()}`}
|
||||||
>
|
>
|
||||||
<StatusDot variant={hostStatus().variant} title={hostStatus().label} ariaLabel={hostStatus().label} size="xs" />
|
<StatusDot variant={hostStatus().variant} title={hostStatus().label} ariaLabel={hostStatus().label} size="xs" />
|
||||||
<span class="max-w-[160px] truncate">{hostDisplayName()}</span>
|
<span class="truncate">{hostDisplayName()}</span>
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1162,7 +1162,7 @@ const DockerContainerRow: Component<{
|
||||||
);
|
);
|
||||||
case 'type':
|
case 'type':
|
||||||
return (
|
return (
|
||||||
<div class="px-2 py-0.5 flex items-center">
|
<div class="px-2 py-0.5 flex items-center overflow-hidden">
|
||||||
<span class={`inline-flex items-center rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap ${runtimeInfo.badgeClass}`} title={runtimeVersion() ? `${runtimeInfo.label} ${runtimeVersion()}` : runtimeInfo.raw || runtimeInfo.label}>
|
<span class={`inline-flex items-center rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap ${runtimeInfo.badgeClass}`} title={runtimeVersion() ? `${runtimeInfo.label} ${runtimeVersion()}` : runtimeInfo.raw || runtimeInfo.label}>
|
||||||
{runtimeInfo.label}
|
{runtimeInfo.label}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -1170,19 +1170,19 @@ const DockerContainerRow: Component<{
|
||||||
);
|
);
|
||||||
case 'image':
|
case 'image':
|
||||||
return (
|
return (
|
||||||
<div class="px-2 py-0.5 text-xs text-gray-700 dark:text-gray-300 truncate">
|
<div class="px-2 py-0.5 text-xs text-gray-700 dark:text-gray-300 truncate overflow-hidden">
|
||||||
<span title={container.image}>{container.image || '—'}</span>
|
<span title={container.image}>{container.image || '—'}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'status':
|
case 'status':
|
||||||
return (
|
return (
|
||||||
<div class="px-2 py-0.5 text-xs">
|
<div class="px-2 py-0.5 text-xs overflow-hidden">
|
||||||
<span class={`rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap ${statusBadgeClass()}`}>{statusLabel()}</span>
|
<span class={`rounded px-2 py-0.5 text-[10px] font-medium whitespace-nowrap block truncate ${statusBadgeClass()}`}>{statusLabel()}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'cpu':
|
case 'cpu':
|
||||||
return (
|
return (
|
||||||
<div class="px-2 py-0.5 flex items-center">
|
<div class="px-2 py-0.5 flex items-center overflow-hidden">
|
||||||
<ResponsiveMetricCell
|
<ResponsiveMetricCell
|
||||||
value={cpuPercent()}
|
value={cpuPercent()}
|
||||||
type="cpu"
|
type="cpu"
|
||||||
|
|
@ -1195,7 +1195,7 @@ const DockerContainerRow: Component<{
|
||||||
);
|
);
|
||||||
case 'memory':
|
case 'memory':
|
||||||
return (
|
return (
|
||||||
<div class="px-2 py-0.5 flex items-center">
|
<div class="px-2 py-0.5 flex items-center overflow-hidden">
|
||||||
<ResponsiveMetricCell
|
<ResponsiveMetricCell
|
||||||
value={memPercent()}
|
value={memPercent()}
|
||||||
type="memory"
|
type="memory"
|
||||||
|
|
@ -1209,7 +1209,7 @@ const DockerContainerRow: Component<{
|
||||||
);
|
);
|
||||||
case 'disk':
|
case 'disk':
|
||||||
return (
|
return (
|
||||||
<div class="px-2 py-0.5 flex items-center">
|
<div class="px-2 py-0.5 flex items-center overflow-hidden">
|
||||||
<Show when={hasDiskStats()} fallback={<span class="text-xs text-gray-400">—</span>}>
|
<Show when={hasDiskStats()} fallback={<span class="text-xs text-gray-400">—</span>}>
|
||||||
<Show when={diskPercent() !== null} fallback={<span class="text-xs text-gray-700 dark:text-gray-300">{diskUsageLabel()}</span>}>
|
<Show when={diskPercent() !== null} fallback={<span class="text-xs text-gray-700 dark:text-gray-300">{diskUsageLabel()}</span>}>
|
||||||
<ResponsiveMetricCell
|
<ResponsiveMetricCell
|
||||||
|
|
@ -1227,7 +1227,7 @@ const DockerContainerRow: Component<{
|
||||||
);
|
);
|
||||||
case 'tasks':
|
case 'tasks':
|
||||||
return (
|
return (
|
||||||
<div class="px-2 py-0.5 text-xs text-gray-700 dark:text-gray-300">
|
<div class="px-2 py-0.5 text-xs text-gray-700 dark:text-gray-300 overflow-hidden whitespace-nowrap">
|
||||||
<Show when={isRunning()} fallback={<span class="text-gray-400">—</span>}>
|
<Show when={isRunning()} fallback={<span class="text-gray-400">—</span>}>
|
||||||
{restarts()}
|
{restarts()}
|
||||||
<span class="text-[10px] text-gray-500 dark:text-gray-400 ml-1">restarts</span>
|
<span class="text-[10px] text-gray-500 dark:text-gray-400 ml-1">restarts</span>
|
||||||
|
|
@ -1236,7 +1236,7 @@ const DockerContainerRow: Component<{
|
||||||
);
|
);
|
||||||
case 'updated':
|
case 'updated':
|
||||||
return (
|
return (
|
||||||
<div class="px-2 py-0.5 text-xs text-gray-700 dark:text-gray-300">
|
<div class="px-2 py-0.5 text-xs text-gray-700 dark:text-gray-300 overflow-hidden whitespace-nowrap">
|
||||||
<Show when={isRunning()} fallback={<span class="text-gray-400">—</span>}>
|
<Show when={isRunning()} fallback={<span class="text-gray-400">—</span>}>
|
||||||
<Show when={props.isMobile()} fallback={uptime()}>
|
<Show when={props.isMobile()} fallback={uptime()}>
|
||||||
{formatUptime(container.uptimeSeconds || 0, true)}
|
{formatUptime(container.uptimeSeconds || 0, true)}
|
||||||
|
|
@ -2411,7 +2411,7 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
{/* Header Row */}
|
{/* Header Row */}
|
||||||
<div
|
<div
|
||||||
class="grid 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 min-w-[400px] md:min-w-0"
|
class="grid 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 min-w-[520px] md:min-w-0"
|
||||||
style={{ 'grid-template-columns': gridTemplate() }}
|
style={{ 'grid-template-columns': gridTemplate() }}
|
||||||
>
|
>
|
||||||
<For each={visibleColumns()}>
|
<For each={visibleColumns()}>
|
||||||
|
|
@ -2464,7 +2464,7 @@ const DockerUnifiedTable: Component<DockerUnifiedTableProps> = (props) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Rows */}
|
{/* Rows */}
|
||||||
<div class="divide-y divide-gray-200 dark:divide-gray-700 min-w-[400px] md:min-w-0">
|
<div class="divide-y divide-gray-200 dark:divide-gray-700 min-w-[520px] md:min-w-0">
|
||||||
<Show
|
<Show
|
||||||
when={isGroupedView()}
|
when={isGroupedView()}
|
||||||
fallback={
|
fallback={
|
||||||
|
|
|
||||||
|
|
@ -255,24 +255,24 @@ export const HostsOverview: Component<HostsOverviewProps> = (props) => {
|
||||||
<tr class={rowClass()} onClick={toggleDrawer} aria-expanded={drawerOpen()}>
|
<tr class={rowClass()} onClick={toggleDrawer} aria-expanded={drawerOpen()}>
|
||||||
<td class="pl-4 pr-2 py-2">
|
<td class="pl-4 pr-2 py-2">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
<StatusDot
|
<StatusDot
|
||||||
variant={hostStatus().variant}
|
variant={hostStatus().variant}
|
||||||
title={hostStatus().label}
|
title={hostStatus().label}
|
||||||
ariaLabel={hostStatus().label}
|
ariaLabel={hostStatus().label}
|
||||||
size="xs"
|
size="xs"
|
||||||
/>
|
/>
|
||||||
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100 truncate">
|
||||||
{host.displayName || host.hostname || host.id}
|
{host.displayName || host.hostname || host.id}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Show when={host.displayName && host.displayName !== host.hostname}>
|
<Show when={host.displayName && host.displayName !== host.hostname}>
|
||||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5 truncate">
|
||||||
{host.hostname}
|
{host.hostname}
|
||||||
</p>
|
</p>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={host.lastSeen}>
|
<Show when={host.lastSeen}>
|
||||||
<p class="text-[10px] text-gray-500 dark:text-gray-400 mt-0.5">
|
<p class="text-[10px] text-gray-500 dark:text-gray-400 mt-0.5 truncate">
|
||||||
Updated {formatRelativeTime(host.lastSeen!)}
|
Updated {formatRelativeTime(host.lastSeen!)}
|
||||||
</p>
|
</p>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
||||||
|
|
@ -422,7 +422,7 @@ const MailGateway: Component = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-2 py-1.5 text-xs font-medium text-gray-900 dark:text-gray-100">{node.name}</td>
|
<td class="px-2 py-1.5 text-xs font-medium text-gray-900 dark:text-gray-100 truncate max-w-[150px]">{node.name}</td>
|
||||||
<td class="px-2 py-1.5 text-xs text-gray-700 dark:text-gray-300 capitalize">{node.role || '—'}</td>
|
<td class="px-2 py-1.5 text-xs text-gray-700 dark:text-gray-300 capitalize">{node.role || '—'}</td>
|
||||||
<td class="px-2 py-1.5">
|
<td class="px-2 py-1.5">
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ const Replication: Component = () => {
|
||||||
return (
|
return (
|
||||||
<tr class="hover:bg-gray-50/80 dark:hover:bg-gray-900/40 transition-colors">
|
<tr class="hover:bg-gray-50/80 dark:hover:bg-gray-900/40 transition-colors">
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<div class="font-medium text-gray-900 dark:text-gray-100">
|
<div class="font-medium text-gray-900 dark:text-gray-100 truncate max-w-[200px]">
|
||||||
{job.guestName || `VM ${job.guestId ?? ''}`}
|
{job.guestName || `VM ${job.guestId ?? ''}`}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
|
|
||||||
|
|
@ -319,7 +319,7 @@ export const PveNodesTable: Component<PveNodesTableProps> = (props) => {
|
||||||
<p class="font-medium text-gray-900 dark:text-gray-100 truncate">
|
<p class="font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||||
{node.name}
|
{node.name}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-gray-600 dark:text-gray-400 break-all">
|
<p class="text-xs text-gray-600 dark:text-gray-400 truncate">
|
||||||
{node.host}
|
{node.host}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -576,7 +576,7 @@ export const PbsNodesTable: Component<PbsNodesTableProps> = (props) => {
|
||||||
<p class="font-medium text-gray-900 dark:text-gray-100 truncate">
|
<p class="font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||||
{node.name}
|
{node.name}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-gray-600 dark:text-gray-400 break-all">
|
<p class="text-xs text-gray-600 dark:text-gray-400 truncate">
|
||||||
{node.host}
|
{node.host}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -759,7 +759,7 @@ export const PmgNodesTable: Component<PmgNodesTableProps> = (props) => {
|
||||||
<p class="font-medium text-gray-900 dark:text-gray-100 truncate">
|
<p class="font-medium text-gray-900 dark:text-gray-100 truncate">
|
||||||
{node.name}
|
{node.name}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-gray-600 dark:text-gray-400 break-all">
|
<p class="text-xs text-gray-600 dark:text-gray-400 truncate">
|
||||||
{node.host}
|
{node.host}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1469,6 +1469,10 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
if req.URL.Path == "/api/setup-script-url" {
|
if req.URL.Path == "/api/setup-script-url" {
|
||||||
skipCSRF = true
|
skipCSRF = true
|
||||||
}
|
}
|
||||||
|
// Skip CSRF for bootstrap token validation (used during initial setup before session exists)
|
||||||
|
if req.URL.Path == "/api/security/validate-bootstrap-token" {
|
||||||
|
skipCSRF = true
|
||||||
|
}
|
||||||
if strings.HasPrefix(req.URL.Path, "/api/") && !skipCSRF && !CheckCSRF(w, req) {
|
if strings.HasPrefix(req.URL.Path, "/api/") && !skipCSRF && !CheckCSRF(w, req) {
|
||||||
http.Error(w, "CSRF token validation failed", http.StatusForbidden)
|
http.Error(w, "CSRF token validation failed", http.StatusForbidden)
|
||||||
LogAuditEvent("csrf_failure", "", GetClientIP(req), req.URL.Path, false, "Invalid CSRF token")
|
LogAuditEvent("csrf_failure", "", GetClientIP(req), req.URL.Path, false, "Invalid CSRF token")
|
||||||
|
|
|
||||||
7
mock.env
7
mock.env
|
|
@ -1 +1,6 @@
|
||||||
PULSE_MOCK_MODE=false
|
PULSE_MOCK_MODE=true
|
||||||
|
PULSE_MOCK_NODES=7
|
||||||
|
PULSE_MOCK_VMS_PER_NODE=5
|
||||||
|
PULSE_MOCK_LXCS_PER_NODE=8
|
||||||
|
PULSE_MOCK_RANDOM_METRICS=true
|
||||||
|
PULSE_MOCK_STOPPED_PERCENT=20
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue