mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-01 03:03:50 +02:00
218 lines
6.1 KiB
Svelte
218 lines
6.1 KiB
Svelte
<!--
|
|
@component Displays job metaData, serves links to detail pages
|
|
|
|
Properties:
|
|
- `job Object`: The Job Object (GraphQL.Job)
|
|
- `jobTags [Number]?`: The jobs tags as IDs, default useful for dynamically updating the tags [Default: job.tags]
|
|
-->
|
|
|
|
<script>
|
|
import { Badge, Button, Icon, Tooltip } from "@sveltestrap/sveltestrap";
|
|
import { scrambleNames, scramble } from "../utils.js";
|
|
import Tag from "../helper/Tag.svelte";
|
|
import TagManagement from "../helper/TagManagement.svelte";
|
|
|
|
/* Svelte 5 Props */
|
|
let {
|
|
job,
|
|
jobTags = job.tags,
|
|
showTagedit = false,
|
|
username = null,
|
|
authlevel= null,
|
|
roles = null,
|
|
isSelected = null,
|
|
showSelect = false,
|
|
} = $props();
|
|
|
|
/* State Init */
|
|
let displayCheck = $state(false);
|
|
|
|
/* Functions */
|
|
function formatDuration(duration) {
|
|
const hours = Math.floor(duration / 3600);
|
|
duration -= hours * 3600;
|
|
const minutes = Math.floor(duration / 60);
|
|
duration -= minutes * 60;
|
|
const seconds = duration;
|
|
return `${hours}:${("0" + minutes).slice(-2)}:${("0" + seconds).slice(-2)}`;
|
|
}
|
|
|
|
function getStateColor(state) {
|
|
switch (state) {
|
|
case "running":
|
|
return "success";
|
|
case "completed":
|
|
return "primary";
|
|
default:
|
|
return "danger";
|
|
}
|
|
}
|
|
|
|
function clipJobId(jid) {
|
|
|
|
// Navigator clipboard api needs a secure context (https)
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
navigator.clipboard
|
|
.writeText(jid)
|
|
.catch((reason) => console.error(reason));
|
|
} else {
|
|
// Workaround: Create, Fill, And Copy Content of Textarea
|
|
const textArea = document.createElement("textarea");
|
|
textArea.value = jid;
|
|
textArea.style.position = "absolute";
|
|
textArea.style.left = "-999999px";
|
|
document.body.prepend(textArea);
|
|
textArea.select();
|
|
try {
|
|
document.execCommand('copy');
|
|
} catch (error) {
|
|
console.error(error);
|
|
} finally {
|
|
textArea.remove();
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div>
|
|
<p class="mb-2 text-truncate">
|
|
<span class="d-flex justify-content-between">
|
|
<span class="align-self-center fw-bold mr-2">
|
|
<a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a>
|
|
({job.cluster})
|
|
</span>
|
|
<span>
|
|
{#if showSelect}
|
|
<Button id={`${job.cluster}-${job.jobId}-select`} outline={!isSelected} color={isSelected? `success`: `secondary`} size="sm" class="mr-2"
|
|
onclick={() => {
|
|
isSelected = !isSelected
|
|
}}>
|
|
{#if isSelected}
|
|
<Icon name="check-square"/>
|
|
{:else if isSelected == false}
|
|
<Icon name="square"/>
|
|
{:else}
|
|
<Icon name="plus-square-dotted" />
|
|
{/if}
|
|
</Button>
|
|
<Tooltip
|
|
target={`${job.cluster}-${job.jobId}-select`}
|
|
placement="left">
|
|
{ 'Add or Remove Job to/from Comparison Selection' }
|
|
</Tooltip>
|
|
{/if}
|
|
<Button id={`${job.cluster}-${job.jobId}-clipboard`} outline color="secondary" size="sm" onclick={() => {
|
|
displayCheck = true;
|
|
clipJobId(job.jobId);
|
|
setTimeout(function () {
|
|
displayCheck = false;
|
|
}, 1000);
|
|
}}>
|
|
{#if displayCheck}
|
|
<Icon name="clipboard2-check-fill"/>
|
|
{:else}
|
|
<Icon name="clipboard2"/>
|
|
{/if}
|
|
</Button>
|
|
<Tooltip
|
|
target={`${job.cluster}-${job.jobId}-clipboard`}
|
|
placement="right">
|
|
{ displayCheck ? 'Copied!' : 'Copy Job ID to Clipboard' }
|
|
</Tooltip>
|
|
</span>
|
|
</span>
|
|
{#if job.metaData?.jobName}
|
|
{#if job.metaData?.jobName.length <= 20}
|
|
<span>{job.metaData.jobName}</span>
|
|
{:else}
|
|
<span
|
|
style="cursor:help;"
|
|
title={job.metaData.jobName}
|
|
>
|
|
{job.metaData.jobName}
|
|
</span>
|
|
{/if}
|
|
{/if}
|
|
{#if job.arrayJobId}
|
|
Array Job: <a
|
|
href="/monitoring/jobs/?arrayJobId={job.arrayJobId}&cluster={job.cluster}"
|
|
target="_blank">#{job.arrayJobId}</a
|
|
>
|
|
{/if}
|
|
</p>
|
|
|
|
<p class="mb-2">
|
|
<Icon name="person-fill" />
|
|
<a class="fst-italic" href="/monitoring/user/{job.user}" target="_blank">
|
|
{scrambleNames ? scramble(job.user) : job.user}
|
|
</a>
|
|
{#if job.userData && job.userData.name}
|
|
({scrambleNames ? scramble(job.userData.name) : job.userData.name})
|
|
{/if}
|
|
{#if job.project && job.project != "no project"}
|
|
<br />
|
|
<Icon name="people-fill" />
|
|
<a
|
|
class="fst-italic"
|
|
href="/monitoring/jobs/?project={job.project}&projectMatch=eq"
|
|
target="_blank"
|
|
>
|
|
{scrambleNames ? scramble(job.project) : job.project}
|
|
</a>
|
|
{/if}
|
|
</p>
|
|
|
|
<p class="mb-2">
|
|
{#if job.numNodes == 1}
|
|
{job.resources[0].hostname}
|
|
{:else}
|
|
{job.numNodes}
|
|
{/if}
|
|
<Icon name="pc-horizontal" />
|
|
{#if job.exclusive != 1}
|
|
(shared)
|
|
{/if}
|
|
{#if job.numAcc > 0}
|
|
, {job.numAcc} <Icon name="gpu-card" />
|
|
{/if}
|
|
{#if job.numHWThreads > 0}
|
|
, {job.numHWThreads} <Icon name="cpu" />
|
|
{/if}
|
|
<br />
|
|
{job.subCluster}
|
|
</p>
|
|
|
|
<p class="mb-2">
|
|
Start: <span class="fw-bold"
|
|
>{new Date(job.startTime).toLocaleString()}</span
|
|
>
|
|
<br />
|
|
Duration: <span class="fw-bold">{formatDuration(job.duration)}</span>
|
|
<Badge color={getStateColor(job.state)}>{job.state}</Badge>
|
|
{#if job.walltime}
|
|
<br />
|
|
Walltime: <span class="fw-bold">{formatDuration(job.walltime)}</span>
|
|
{/if}
|
|
</p>
|
|
|
|
{#if showTagedit}
|
|
<hr class="mt-0 mb-2"/>
|
|
<p class="mb-1">
|
|
<TagManagement bind:jobTags {job} {username} {authlevel} {roles} renderModal/> :
|
|
{#if jobTags?.length > 0}
|
|
{#each jobTags as tag}
|
|
<Tag {tag}/>
|
|
{/each}
|
|
{:else}
|
|
<span style="font-size: 0.9rem; background-color: lightgray;" class="my-1 badge text-dark">No Tags</span>
|
|
{/if}
|
|
</p>
|
|
{:else}
|
|
<p class="mb-1">
|
|
{#each jobTags as tag}
|
|
<Tag {tag} />
|
|
{/each}
|
|
</p>
|
|
{/if}
|
|
</div>
|