ui: tighten portal layout and status polling
This commit is contained in:
parent
35f7b77c1b
commit
d62ac0fd45
@ -14,7 +14,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, onUnmounted, ref } from "vue";
|
||||||
import TopBar from "./components/TopBar.vue";
|
import TopBar from "./components/TopBar.vue";
|
||||||
import { fallbackHardware, fallbackServices, fallbackNetwork, fallbackMetrics } from "./data/sample.js";
|
import { fallbackHardware, fallbackServices, fallbackNetwork, fallbackMetrics } from "./data/sample.js";
|
||||||
|
|
||||||
@ -27,12 +27,13 @@ const metricsData = ref(fallbackMetrics());
|
|||||||
const statusLoading = ref(true);
|
const statusLoading = ref(true);
|
||||||
const statusFetching = ref(false);
|
const statusFetching = ref(false);
|
||||||
const statusError = ref("");
|
const statusError = ref("");
|
||||||
|
let pollTimerId = null;
|
||||||
|
|
||||||
async function refreshLabStatus() {
|
async function refreshLabStatus() {
|
||||||
if (statusFetching.value) return;
|
if (statusFetching.value) return;
|
||||||
statusFetching.value = true;
|
statusFetching.value = true;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = window.setTimeout(() => controller.abort(), 6000);
|
const timeoutId = window.setTimeout(() => controller.abort(), 10000);
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/api/lab/status", {
|
const resp = await fetch("/api/lab/status", {
|
||||||
headers: { Accept: "application/json" },
|
headers: { Accept: "application/json" },
|
||||||
@ -52,13 +53,23 @@ async function refreshLabStatus() {
|
|||||||
window.clearTimeout(timeoutId);
|
window.clearTimeout(timeoutId);
|
||||||
statusLoading.value = false;
|
statusLoading.value = false;
|
||||||
statusFetching.value = false;
|
statusFetching.value = false;
|
||||||
|
scheduleNextPoll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
refreshLabStatus();
|
refreshLabStatus();
|
||||||
window.setInterval(refreshLabStatus, 30000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (pollTimerId) window.clearTimeout(pollTimerId);
|
||||||
|
});
|
||||||
|
|
||||||
|
function scheduleNextPoll() {
|
||||||
|
if (pollTimerId) window.clearTimeout(pollTimerId);
|
||||||
|
const delayMs = labStatus.value ? 30000 : 8000;
|
||||||
|
pollTimerId = window.setTimeout(refreshLabStatus, delayMs);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -39,6 +39,8 @@ const svgContent = ref("");
|
|||||||
const renderKey = ref(props.cardId || `mermaid-${Math.random().toString(36).slice(2)}`);
|
const renderKey = ref(props.cardId || `mermaid-${Math.random().toString(36).slice(2)}`);
|
||||||
const isOpen = ref(false);
|
const isOpen = ref(false);
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
let scheduledHandle = null;
|
||||||
|
let scheduledKind = "";
|
||||||
|
|
||||||
const renderDiagram = async () => {
|
const renderDiagram = async () => {
|
||||||
if (!props.diagram) return;
|
if (!props.diagram) return;
|
||||||
@ -65,10 +67,38 @@ const renderDiagram = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(renderDiagram);
|
function cancelScheduledRender() {
|
||||||
|
if (!scheduledHandle) return;
|
||||||
|
if (scheduledKind === "idle" && window.cancelIdleCallback) {
|
||||||
|
window.cancelIdleCallback(scheduledHandle);
|
||||||
|
} else {
|
||||||
|
window.clearTimeout(scheduledHandle);
|
||||||
|
}
|
||||||
|
scheduledHandle = null;
|
||||||
|
scheduledKind = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleRenderDiagram() {
|
||||||
|
cancelScheduledRender();
|
||||||
|
if (!props.diagram) return;
|
||||||
|
const runner = () => {
|
||||||
|
scheduledHandle = null;
|
||||||
|
scheduledKind = "";
|
||||||
|
renderDiagram();
|
||||||
|
};
|
||||||
|
if (window.requestIdleCallback) {
|
||||||
|
scheduledKind = "idle";
|
||||||
|
scheduledHandle = window.requestIdleCallback(runner, { timeout: 1500 });
|
||||||
|
} else {
|
||||||
|
scheduledKind = "timeout";
|
||||||
|
scheduledHandle = window.setTimeout(runner, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(scheduleRenderDiagram);
|
||||||
watch(
|
watch(
|
||||||
() => props.diagram,
|
() => props.diagram,
|
||||||
() => renderDiagram()
|
() => scheduleRenderDiagram()
|
||||||
);
|
);
|
||||||
|
|
||||||
const onKeyDown = (event) => {
|
const onKeyDown = (event) => {
|
||||||
@ -76,6 +106,7 @@ const onKeyDown = (event) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const open = () => {
|
const open = () => {
|
||||||
|
if (!svgContent.value) scheduleRenderDiagram();
|
||||||
isOpen.value = true;
|
isOpen.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -94,6 +125,7 @@ watch(isOpen, (value) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
cancelScheduledRender();
|
||||||
document.body.style.overflow = "";
|
document.body.style.overflow = "";
|
||||||
window.removeEventListener("keydown", onKeyDown);
|
window.removeEventListener("keydown", onKeyDown);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -587,16 +587,18 @@ h1 {
|
|||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-stack {
|
.account-stack {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-stack .module {
|
.account-stack .module {
|
||||||
flex: 1;
|
flex: 1 1 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -181,6 +181,10 @@ const sections = [
|
|||||||
.section-grid {
|
.section-grid {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tiles {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
@ -201,7 +205,8 @@ const sections = [
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
min-height: 92px;
|
height: 92px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group + .group {
|
.group + .group {
|
||||||
@ -226,7 +231,7 @@ const sections = [
|
|||||||
|
|
||||||
.tiles {
|
.tiles {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
grid-template-columns: 1fr;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,7 +243,7 @@ const sections = [
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
background: rgba(255, 255, 255, 0.02);
|
background: rgba(255, 255, 255, 0.02);
|
||||||
transition: border-color 160ms ease, transform 160ms ease;
|
transition: border-color 160ms ease, transform 160ms ease;
|
||||||
min-height: 112px;
|
min-height: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile:hover {
|
.tile:hover {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user