fix(aria): replace plain Refresh button with canonical circular auto-refresh control
CD - Develop / build-and-deploy (push) Failing after 13m3s
CD - Develop / build-and-deploy (push) Failing after 13m3s
All three list pages (ThreatEvents, PrecursorAlerts, DevicePosture) now use the template's btn-manual-refresh icon button + SVG countdown timer in header-controls, matching the canonical TablePage.svelte pattern. Plain text Refresh button removed from toolbar-right. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { ariaApi, type DeviceSecurityPosture, type OsEolStatus } from '../lib/api';
|
||||
import { addToast } from '../lib/stores';
|
||||
import Pagination from './common/Pagination.svelte';
|
||||
@@ -88,7 +88,42 @@
|
||||
return !!(d.goldImageHash && d.currentImageHash && d.goldImageHash !== d.currentImageHash);
|
||||
}
|
||||
|
||||
onMount(() => load(0));
|
||||
// Auto-refresh
|
||||
const REFRESH_SECONDS = 60;
|
||||
const CIRCUMFERENCE = 2 * Math.PI * 14;
|
||||
let secondsLeft = REFRESH_SECONDS;
|
||||
let refreshing = false;
|
||||
let autoRefreshEnabled = true;
|
||||
let tickInterval: ReturnType<typeof setInterval>;
|
||||
|
||||
$: dashOffset = CIRCUMFERENCE * (1 - secondsLeft / REFRESH_SECONDS);
|
||||
|
||||
function toggleAutoRefresh() {
|
||||
autoRefreshEnabled = !autoRefreshEnabled;
|
||||
if (autoRefreshEnabled) secondsLeft = REFRESH_SECONDS;
|
||||
}
|
||||
|
||||
async function doRefresh() {
|
||||
refreshing = true;
|
||||
await load(currentPage);
|
||||
refreshing = false;
|
||||
secondsLeft = REFRESH_SECONDS;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
load(0);
|
||||
tickInterval = setInterval(() => {
|
||||
if (!autoRefreshEnabled) return;
|
||||
if (secondsLeft > 1) {
|
||||
secondsLeft -= 1;
|
||||
} else {
|
||||
secondsLeft = REFRESH_SECONDS;
|
||||
doRefresh();
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onDestroy(() => clearInterval(tickInterval));
|
||||
</script>
|
||||
|
||||
<div class="page-wrapper">
|
||||
@@ -98,6 +133,34 @@
|
||||
<h1>Device Posture</h1>
|
||||
<p>Per-device security compliance — disk encryption, image integrity, OS currency</p>
|
||||
</div>
|
||||
<div class="header-controls">
|
||||
<button class="btn-manual-refresh" on:click={doRefresh} disabled={refreshing}
|
||||
title="Refresh now" aria-label="Refresh">{refreshing ? '⟳' : '↻'}</button>
|
||||
<button class="refresh-countdown" class:paused={!autoRefreshEnabled}
|
||||
on:click={toggleAutoRefresh}
|
||||
title={autoRefreshEnabled ? `Auto-refresh in ${secondsLeft}s — click to pause` : 'Auto-refresh paused — click to resume'}
|
||||
aria-label="Toggle auto-refresh">
|
||||
<svg width="72" height="72" viewBox="0 0 36 36">
|
||||
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="3"/>
|
||||
{#if autoRefreshEnabled}
|
||||
<circle cx="18" cy="18" r="14" fill="none"
|
||||
stroke={secondsLeft <= 5 ? '#f87171' : 'rgba(255,255,255,0.85)'}
|
||||
stroke-width="3" stroke-dasharray={CIRCUMFERENCE} stroke-dashoffset={dashOffset}
|
||||
stroke-linecap="round" transform="rotate(-90 18 18)" class:pulsing={secondsLeft <= 5}/>
|
||||
{:else}
|
||||
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.3)"
|
||||
stroke-width="3" stroke-dasharray="4 3" transform="rotate(-90 18 18)"/>
|
||||
{/if}
|
||||
{#if refreshing}
|
||||
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">⟳</text>
|
||||
{:else if autoRefreshEnabled}
|
||||
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">{secondsLeft}</text>
|
||||
{:else}
|
||||
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.5)" font-family="sans-serif">⏸</text>
|
||||
{/if}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-body">
|
||||
@@ -179,9 +242,6 @@
|
||||
|
||||
<div class="toolbar">
|
||||
<span class="toolbar-count">{totalElements} device{totalElements !== 1 ? 's' : ''}</span>
|
||||
<div class="toolbar-right">
|
||||
<button class="btn-refresh" on:click={() => load(currentPage)}>Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
@@ -462,12 +522,25 @@
|
||||
}
|
||||
.toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; flex-shrink: 0; }
|
||||
.toolbar-count { font-size: var(--font-size-body-sm); color: #6b7280; }
|
||||
.toolbar-right { display: flex; gap: 0.5rem; }
|
||||
.btn-refresh {
|
||||
background: #f3f4f6; color: #374151; border: 1px solid #d1d5db;
|
||||
border-radius: 6px; padding: 6px 12px; font-size: var(--font-size-body-sm); font-weight: 500; cursor: pointer;
|
||||
.header-controls { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
|
||||
.btn-manual-refresh {
|
||||
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
|
||||
color: white; border-radius: 50%; width: 36px; height: 36px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.1rem; cursor: pointer; transition: background 0.15s; flex-shrink: 0;
|
||||
}
|
||||
.btn-refresh:hover { background: #e5e7eb; }
|
||||
.btn-manual-refresh:hover:not(:disabled) { background: rgba(255,255,255,0.28); }
|
||||
.btn-manual-refresh:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.refresh-countdown {
|
||||
flex-shrink: 0; background: none; border: none; cursor: pointer; padding: 0;
|
||||
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||||
opacity: 0.9; transition: opacity 0.15s, transform 0.15s;
|
||||
}
|
||||
.refresh-countdown:hover { opacity: 1; transform: scale(1.08); }
|
||||
.refresh-countdown.paused { opacity: 0.5; }
|
||||
.refresh-countdown.paused:hover { opacity: 0.8; }
|
||||
@keyframes countdown-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||||
:global(.pulsing) { animation: countdown-pulse 0.8s ease-in-out infinite; }
|
||||
|
||||
/* Table card — canonical */
|
||||
.table-card {
|
||||
@@ -590,7 +663,6 @@
|
||||
:global(.dark-mode) .filter-tag-remove { background: #374151; color: #e5e7eb; }
|
||||
|
||||
:global(.dark-mode) .toolbar-count { color: #9ca3af; }
|
||||
:global(.dark-mode) .btn-refresh { background: #374151; color: #e5e7eb; border-color: #4b5563; }
|
||||
|
||||
:global(.dark-mode) .table-card { background: #1f2937; }
|
||||
:global(.dark-mode) thead { background: #111827; border-bottom-color: #374151; }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { ariaApi, type PrecursorSequenceAlert, type SequenceType, type ThreatEvent } from '../lib/api';
|
||||
import { addToast } from '../lib/stores';
|
||||
import Pagination from './common/Pagination.svelte';
|
||||
@@ -108,7 +108,42 @@
|
||||
return Math.round(c * 100) + '%';
|
||||
}
|
||||
|
||||
onMount(() => load(0));
|
||||
// Auto-refresh
|
||||
const REFRESH_SECONDS = 60;
|
||||
const CIRCUMFERENCE = 2 * Math.PI * 14;
|
||||
let secondsLeft = REFRESH_SECONDS;
|
||||
let refreshing = false;
|
||||
let autoRefreshEnabled = true;
|
||||
let tickInterval: ReturnType<typeof setInterval>;
|
||||
|
||||
$: dashOffset = CIRCUMFERENCE * (1 - secondsLeft / REFRESH_SECONDS);
|
||||
|
||||
function toggleAutoRefresh() {
|
||||
autoRefreshEnabled = !autoRefreshEnabled;
|
||||
if (autoRefreshEnabled) secondsLeft = REFRESH_SECONDS;
|
||||
}
|
||||
|
||||
async function doRefresh() {
|
||||
refreshing = true;
|
||||
await load(currentPage);
|
||||
refreshing = false;
|
||||
secondsLeft = REFRESH_SECONDS;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
load(0);
|
||||
tickInterval = setInterval(() => {
|
||||
if (!autoRefreshEnabled) return;
|
||||
if (secondsLeft > 1) {
|
||||
secondsLeft -= 1;
|
||||
} else {
|
||||
secondsLeft = REFRESH_SECONDS;
|
||||
doRefresh();
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onDestroy(() => clearInterval(tickInterval));
|
||||
</script>
|
||||
|
||||
<script lang="ts" context="module">
|
||||
@@ -126,6 +161,34 @@
|
||||
<h1>Precursor Alerts</h1>
|
||||
<p>Multi-phase attack sequence detection — jackpotting and skimming precursors</p>
|
||||
</div>
|
||||
<div class="header-controls">
|
||||
<button class="btn-manual-refresh" on:click={doRefresh} disabled={refreshing}
|
||||
title="Refresh now" aria-label="Refresh">{refreshing ? '⟳' : '↻'}</button>
|
||||
<button class="refresh-countdown" class:paused={!autoRefreshEnabled}
|
||||
on:click={toggleAutoRefresh}
|
||||
title={autoRefreshEnabled ? `Auto-refresh in ${secondsLeft}s — click to pause` : 'Auto-refresh paused — click to resume'}
|
||||
aria-label="Toggle auto-refresh">
|
||||
<svg width="72" height="72" viewBox="0 0 36 36">
|
||||
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="3"/>
|
||||
{#if autoRefreshEnabled}
|
||||
<circle cx="18" cy="18" r="14" fill="none"
|
||||
stroke={secondsLeft <= 5 ? '#f87171' : 'rgba(255,255,255,0.85)'}
|
||||
stroke-width="3" stroke-dasharray={CIRCUMFERENCE} stroke-dashoffset={dashOffset}
|
||||
stroke-linecap="round" transform="rotate(-90 18 18)" class:pulsing={secondsLeft <= 5}/>
|
||||
{:else}
|
||||
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.3)"
|
||||
stroke-width="3" stroke-dasharray="4 3" transform="rotate(-90 18 18)"/>
|
||||
{/if}
|
||||
{#if refreshing}
|
||||
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">⟳</text>
|
||||
{:else if autoRefreshEnabled}
|
||||
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">{secondsLeft}</text>
|
||||
{:else}
|
||||
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.5)" font-family="sans-serif">⏸</text>
|
||||
{/if}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-body">
|
||||
@@ -220,9 +283,6 @@
|
||||
|
||||
<div class="toolbar">
|
||||
<span class="toolbar-count">{totalElements} alert{totalElements !== 1 ? 's' : ''}</span>
|
||||
<div class="toolbar-right">
|
||||
<button class="btn-refresh" on:click={() => load(currentPage)}>Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-card">
|
||||
@@ -488,12 +548,26 @@
|
||||
}
|
||||
.toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; flex-shrink: 0; }
|
||||
.toolbar-count { font-size: var(--font-size-body-sm); color: #6b7280; }
|
||||
.toolbar-right { display: flex; gap: 0.5rem; }
|
||||
.btn-refresh {
|
||||
background: #f3f4f6; color: #374151; border: 1px solid #d1d5db;
|
||||
border-radius: 6px; padding: 6px 12px; font-size: var(--font-size-body-sm); font-weight: 500; cursor: pointer;
|
||||
|
||||
.header-controls { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
|
||||
.btn-manual-refresh {
|
||||
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
|
||||
color: white; border-radius: 50%; width: 36px; height: 36px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.1rem; cursor: pointer; transition: background 0.15s; flex-shrink: 0;
|
||||
}
|
||||
.btn-refresh:hover { background: #e5e7eb; }
|
||||
.btn-manual-refresh:hover:not(:disabled) { background: rgba(255,255,255,0.28); }
|
||||
.btn-manual-refresh:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.refresh-countdown {
|
||||
flex-shrink: 0; background: none; border: none; cursor: pointer; padding: 0;
|
||||
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||||
opacity: 0.9; transition: opacity 0.15s, transform 0.15s;
|
||||
}
|
||||
.refresh-countdown:hover { opacity: 1; transform: scale(1.08); }
|
||||
.refresh-countdown.paused { opacity: 0.5; }
|
||||
.refresh-countdown.paused:hover { opacity: 0.8; }
|
||||
@keyframes countdown-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||||
:global(.pulsing) { animation: countdown-pulse 0.8s ease-in-out infinite; }
|
||||
|
||||
/* Table card — canonical */
|
||||
.table-card {
|
||||
@@ -631,7 +705,6 @@
|
||||
:global(.dark-mode) .filter-tag-remove { background: #374151; color: #e5e7eb; }
|
||||
|
||||
:global(.dark-mode) .toolbar-count { color: #9ca3af; }
|
||||
:global(.dark-mode) .btn-refresh { background: #374151; color: #e5e7eb; border-color: #4b5563; }
|
||||
|
||||
:global(.dark-mode) .table-card { background: #1f2937; }
|
||||
:global(.dark-mode) thead { background: #111827; border-bottom-color: #374151; }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { ariaApi, type ThreatEvent, type ThreatSeverity } from '../lib/api';
|
||||
import { addToast } from '../lib/stores';
|
||||
import Pagination from './common/Pagination.svelte';
|
||||
@@ -89,7 +89,42 @@
|
||||
load();
|
||||
}
|
||||
|
||||
onMount(load);
|
||||
// Auto-refresh
|
||||
const REFRESH_SECONDS = 60;
|
||||
const CIRCUMFERENCE = 2 * Math.PI * 14;
|
||||
let secondsLeft = REFRESH_SECONDS;
|
||||
let refreshing = false;
|
||||
let autoRefreshEnabled = true;
|
||||
let tickInterval: ReturnType<typeof setInterval>;
|
||||
|
||||
$: dashOffset = CIRCUMFERENCE * (1 - secondsLeft / REFRESH_SECONDS);
|
||||
|
||||
function toggleAutoRefresh() {
|
||||
autoRefreshEnabled = !autoRefreshEnabled;
|
||||
if (autoRefreshEnabled) secondsLeft = REFRESH_SECONDS;
|
||||
}
|
||||
|
||||
async function doRefresh() {
|
||||
refreshing = true;
|
||||
await load();
|
||||
refreshing = false;
|
||||
secondsLeft = REFRESH_SECONDS;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
load();
|
||||
tickInterval = setInterval(() => {
|
||||
if (!autoRefreshEnabled) return;
|
||||
if (secondsLeft > 1) {
|
||||
secondsLeft -= 1;
|
||||
} else {
|
||||
secondsLeft = REFRESH_SECONDS;
|
||||
doRefresh();
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onDestroy(() => clearInterval(tickInterval));
|
||||
</script>
|
||||
|
||||
<div class="page-wrap">
|
||||
@@ -98,6 +133,34 @@
|
||||
<h1>Threat Events</h1>
|
||||
<p>IOC match events detected across the fleet</p>
|
||||
</div>
|
||||
<div class="header-controls">
|
||||
<button class="btn-manual-refresh" on:click={doRefresh} disabled={refreshing}
|
||||
title="Refresh now" aria-label="Refresh">{refreshing ? '⟳' : '↻'}</button>
|
||||
<button class="refresh-countdown" class:paused={!autoRefreshEnabled}
|
||||
on:click={toggleAutoRefresh}
|
||||
title={autoRefreshEnabled ? `Auto-refresh in ${secondsLeft}s — click to pause` : 'Auto-refresh paused — click to resume'}
|
||||
aria-label="Toggle auto-refresh">
|
||||
<svg width="72" height="72" viewBox="0 0 36 36">
|
||||
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="3"/>
|
||||
{#if autoRefreshEnabled}
|
||||
<circle cx="18" cy="18" r="14" fill="none"
|
||||
stroke={secondsLeft <= 5 ? '#f87171' : 'rgba(255,255,255,0.85)'}
|
||||
stroke-width="3" stroke-dasharray={CIRCUMFERENCE} stroke-dashoffset={dashOffset}
|
||||
stroke-linecap="round" transform="rotate(-90 18 18)" class:pulsing={secondsLeft <= 5}/>
|
||||
{:else}
|
||||
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.3)"
|
||||
stroke-width="3" stroke-dasharray="4 3" transform="rotate(-90 18 18)"/>
|
||||
{/if}
|
||||
{#if refreshing}
|
||||
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">⟳</text>
|
||||
{:else if autoRefreshEnabled}
|
||||
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">{secondsLeft}</text>
|
||||
{:else}
|
||||
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.5)" font-family="sans-serif">⏸</text>
|
||||
{/if}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-body">
|
||||
@@ -229,6 +292,26 @@
|
||||
padding: 24px 24px 0;
|
||||
}
|
||||
|
||||
.header-controls { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
|
||||
.btn-manual-refresh {
|
||||
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
|
||||
color: white; border-radius: 50%; width: 36px; height: 36px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.1rem; cursor: pointer; transition: background 0.15s; flex-shrink: 0;
|
||||
}
|
||||
.btn-manual-refresh:hover:not(:disabled) { background: rgba(255,255,255,0.28); }
|
||||
.btn-manual-refresh:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.refresh-countdown {
|
||||
flex-shrink: 0; background: none; border: none; cursor: pointer; padding: 0;
|
||||
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||||
opacity: 0.9; transition: opacity 0.15s, transform 0.15s;
|
||||
}
|
||||
.refresh-countdown:hover { opacity: 1; transform: scale(1.08); }
|
||||
.refresh-countdown.paused { opacity: 0.5; }
|
||||
.refresh-countdown.paused:hover { opacity: 0.8; }
|
||||
@keyframes countdown-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||||
:global(.pulsing) { animation: countdown-pulse 0.8s ease-in-out infinite; }
|
||||
|
||||
.page-body {
|
||||
display: flex; flex: 1; overflow: hidden; min-height: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user