mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-03-04 07:07:30 +01:00
Merge branch 'dev' into add_GetMemoryDomainsBySocket_2026
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
import ShowUsers from "./admin/ShowUsers.svelte";
|
||||
import Options from "./admin/Options.svelte";
|
||||
import NoticeEdit from "./admin/NoticeEdit.svelte";
|
||||
import RunTaggers from "./admin/RunTaggers.svelte";
|
||||
|
||||
/* Svelte 5 Props */
|
||||
let {
|
||||
@@ -70,4 +71,5 @@
|
||||
</Col>
|
||||
<Options config={ccconfig} {clusterNames}/>
|
||||
<NoticeEdit {ncontent}/>
|
||||
<RunTaggers />
|
||||
</Row>
|
||||
|
||||
142
web/frontend/src/config/admin/RunTaggers.svelte
Normal file
142
web/frontend/src/config/admin/RunTaggers.svelte
Normal file
@@ -0,0 +1,142 @@
|
||||
<!--
|
||||
@component Admin card for running individual job taggers on all jobs
|
||||
-->
|
||||
|
||||
<script>
|
||||
import {
|
||||
Col,
|
||||
Card,
|
||||
CardTitle,
|
||||
CardBody,
|
||||
Spinner,
|
||||
Badge,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { fade } from "svelte/transition";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
|
||||
/* State Init */
|
||||
let taggers = $state([]);
|
||||
let message = $state({ msg: "", color: "#d63384" });
|
||||
let displayMessage = $state(false);
|
||||
let pollTimer = $state(null);
|
||||
|
||||
/* Functions */
|
||||
async function fetchTaggers() {
|
||||
try {
|
||||
const res = await fetch("/config/taggers/");
|
||||
if (res.ok) {
|
||||
taggers = await res.json();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch taggers:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function runTagger(name) {
|
||||
let formData = new FormData();
|
||||
formData.append("name", name);
|
||||
|
||||
try {
|
||||
const res = await fetch("/config/taggers/run/", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
if (res.ok) {
|
||||
let text = await res.text();
|
||||
popMessage(text, "#048109");
|
||||
startPolling();
|
||||
await fetchTaggers();
|
||||
} else {
|
||||
let text = await res.text();
|
||||
throw new Error("Response Code " + res.status + " -> " + text);
|
||||
}
|
||||
} catch (err) {
|
||||
popMessage(err, "#d63384");
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
if (pollTimer) return;
|
||||
pollTimer = setInterval(async () => {
|
||||
await fetchTaggers();
|
||||
const anyRunning = taggers.some((t) => t.running);
|
||||
if (!anyRunning) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function popMessage(response, rescolor) {
|
||||
message = { msg: response, color: rescolor };
|
||||
displayMessage = true;
|
||||
setTimeout(function () {
|
||||
displayMessage = false;
|
||||
}, 3500);
|
||||
}
|
||||
|
||||
/* Lifecycle */
|
||||
onMount(async () => {
|
||||
await fetchTaggers();
|
||||
const anyRunning = taggers.some((t) => t.running);
|
||||
if (anyRunning) startPolling();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Col>
|
||||
<Card class="h-100">
|
||||
<CardBody>
|
||||
<CardTitle class="mb-3">Job Taggers</CardTitle>
|
||||
<p>Run individual taggers on all existing jobs.</p>
|
||||
{#if taggers.length === 0}
|
||||
<p class="text-muted">No taggers available.</p>
|
||||
{:else}
|
||||
<table class="table table-sm mb-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each taggers as tagger}
|
||||
<tr>
|
||||
<td>{tagger.name}</td>
|
||||
<td><Badge color="secondary">{tagger.type}</Badge></td>
|
||||
<td>
|
||||
{#if tagger.running}
|
||||
<Spinner size="sm" color="primary" /> Running
|
||||
{:else}
|
||||
<span class="text-muted">Idle</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
disabled={tagger.running}
|
||||
onclick={() => runTagger(tagger.name)}
|
||||
>
|
||||
Run
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
<p>
|
||||
{#if displayMessage}<b
|
||||
><code style="color: {message.color};" out:fade
|
||||
>{message.msg}</code
|
||||
></b
|
||||
>{/if}
|
||||
</p>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -32,15 +32,6 @@
|
||||
|
||||
/* Const Init */
|
||||
const client = getContextClient();
|
||||
const stateOptions = [
|
||||
"all",
|
||||
"allocated",
|
||||
"idle",
|
||||
"down",
|
||||
"mixed",
|
||||
"reserved",
|
||||
"unknown",
|
||||
];
|
||||
const healthOptions = [
|
||||
"all",
|
||||
"full",
|
||||
@@ -52,12 +43,10 @@
|
||||
let pieWidth = $state(0);
|
||||
let querySorting = $state({ field: "startTime", type: "col", order: "DESC" })
|
||||
let tableHostFilter = $state("");
|
||||
let tableStateFilter = $state(stateOptions[0]);
|
||||
let tableHealthFilter = $state(healthOptions[0]);
|
||||
let healthTableSorting = $state(
|
||||
{
|
||||
schedulerState: { dir: "down", active: true },
|
||||
healthState: { dir: "down", active: false },
|
||||
healthState: { dir: "up", active: true },
|
||||
hostname: { dir: "down", active: false },
|
||||
}
|
||||
);
|
||||
@@ -79,9 +68,7 @@
|
||||
hostname
|
||||
cluster
|
||||
subCluster
|
||||
schedulerState
|
||||
healthState
|
||||
metaData
|
||||
healthData
|
||||
}
|
||||
}
|
||||
@@ -102,7 +89,7 @@
|
||||
let healthTableData = $derived.by(() => {
|
||||
if ($statusQuery?.data) {
|
||||
return [...$statusQuery.data.nodes.items].sort((n1, n2) => {
|
||||
return n1['schedulerState'].localeCompare(n2['schedulerState'])
|
||||
return n1['healthState'].localeCompare(n2['healthState'])
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
@@ -114,21 +101,12 @@
|
||||
if (tableHostFilter != "") {
|
||||
pendingTableData = pendingTableData.filter((e) => e.hostname.includes(tableHostFilter))
|
||||
}
|
||||
if (tableStateFilter != "all") {
|
||||
pendingTableData = pendingTableData.filter((e) => e.schedulerState.includes(tableStateFilter))
|
||||
}
|
||||
if (tableHealthFilter != "all") {
|
||||
pendingTableData = pendingTableData.filter((e) => e.healthState.includes(tableHealthFilter))
|
||||
}
|
||||
return pendingTableData
|
||||
});
|
||||
|
||||
const refinedStateData = $derived.by(() => {
|
||||
return $statusQuery?.data?.nodeStates.
|
||||
filter((e) => ['allocated', 'reserved', 'idle', 'mixed','down', 'unknown'].includes(e.state)).
|
||||
sort((a, b) => b.count - a.count)
|
||||
});
|
||||
|
||||
const refinedHealthData = $derived.by(() => {
|
||||
return $statusQuery?.data?.nodeStates.
|
||||
filter((e) => ['full', 'partial', 'failed'].includes(e.state)).
|
||||
@@ -296,7 +274,7 @@
|
||||
<thead>
|
||||
<!-- Header Row 1: Titles and Sorting -->
|
||||
<tr>
|
||||
<th style="width: 9%; min-width: 100px; max-width:10%;" onclick={() => sortBy('hostname')}>
|
||||
<th style="width: 10%; min-width: 100px; max-width:12%;" onclick={() => sortBy('hostname')}>
|
||||
Hosts ({filteredTableData.length})
|
||||
<Icon
|
||||
name="caret-{healthTableSorting['hostname'].dir}{healthTableSorting['hostname']
|
||||
@@ -304,17 +282,8 @@
|
||||
? '-fill'
|
||||
: ''}"
|
||||
/>
|
||||
</th>
|
||||
<th style="width: 9%; min-width: 100px; max-width:10%;" onclick={() => sortBy('schedulerState')}>
|
||||
Scheduler State
|
||||
<Icon
|
||||
name="caret-{healthTableSorting['schedulerState'].dir}{healthTableSorting['schedulerState']
|
||||
.active
|
||||
? '-fill'
|
||||
: ''}"
|
||||
/>
|
||||
</th>
|
||||
<th style="width: 9%; min-width: 100px; max-width:10%;" onclick={() => sortBy('healthState')}>
|
||||
<th style="width: 10%; min-width: 100px; max-width:12%;" onclick={() => sortBy('healthState')}>
|
||||
Health State
|
||||
<Icon
|
||||
name="caret-{healthTableSorting['healthState'].dir}{healthTableSorting['healthState']
|
||||
@@ -324,7 +293,6 @@
|
||||
/>
|
||||
</th>
|
||||
<th>Metric Availability</th>
|
||||
<th>Meta Information</th>
|
||||
</tr>
|
||||
<!-- Header Row 2: Filters -->
|
||||
<tr>
|
||||
@@ -337,53 +305,27 @@
|
||||
</InputGroup>
|
||||
</th>
|
||||
<th>
|
||||
<InputGroup size="sm">
|
||||
<Input type="select" bind:value={tableStateFilter}>
|
||||
{#each stateOptions as so}
|
||||
<option value={so}>{so}</option>
|
||||
{/each}
|
||||
</Input>
|
||||
<InputGroupText>
|
||||
<Icon name="search"></Icon>
|
||||
</InputGroupText>
|
||||
</InputGroup>
|
||||
</th>
|
||||
<th>
|
||||
<InputGroup size="sm">
|
||||
<Input type="select" bind:value={tableHealthFilter}>
|
||||
<Input size="sm" type="select" bind:value={tableHealthFilter}>
|
||||
{#each healthOptions as ho}
|
||||
<option value={ho}>{ho}</option>
|
||||
{/each}
|
||||
</Input>
|
||||
<InputGroupText>
|
||||
<Icon name="search"></Icon>
|
||||
</InputGroupText>
|
||||
</InputGroup>
|
||||
</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each filteredTableData as host (host.hostname)}
|
||||
<tr>
|
||||
<th scope="row"><b><a href="/monitoring/node/{cluster}/{host.hostname}" target="_blank">{host.hostname}</a></b></th>
|
||||
<td>{host.schedulerState}</td>
|
||||
<td>{host.healthState}</td>
|
||||
<td style="max-width: 250px;">
|
||||
<td style="max-width: 76%;">
|
||||
{#each Object.keys(host.healthData) as hkey}
|
||||
<p>
|
||||
<b>{hkey}</b>: {host.healthData[hkey]}
|
||||
</p>
|
||||
{/each}
|
||||
</td>
|
||||
<td style="max-width: 250px;">
|
||||
{#each Object.keys(host.metaData) as mkey}
|
||||
<p>
|
||||
<b>{mkey}</b>: {host.metaData[mkey]}
|
||||
</p>
|
||||
{/each}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user