Import svelte web frontend

This commit is contained in:
Jan Eitzinger
2022-06-22 11:20:57 +02:00
parent 9217780760
commit 68d1f5fc3f
60 changed files with 6661 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
<script>
import { createEventDispatcher, getContext } from 'svelte'
import { Button, ListGroup, ListGroupItem,
Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
const clusters = getContext('clusters'),
initialized = getContext('initialized'),
dispatch = createEventDispatcher()
export let disableClusterSelection = false
export let isModified = false
export let isOpen = false
export let cluster = null
export let partition = null
let pendingCluster = cluster, pendingPartition = partition
$: isModified = pendingCluster != cluster || pendingPartition != partition
</script>
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Select Cluster & Slurm Partition
</ModalHeader>
<ModalBody>
{#if $initialized}
<h4>Cluster</h4>
<ListGroup>
<ListGroupItem
disabled={disableClusterSelection}
active={pendingCluster == null}
on:click={() => (pendingCluster = null, pendingPartition = null)}>
Any Cluster
</ListGroupItem>
{#each clusters as cluster}
<ListGroupItem
disabled={disableClusterSelection}
active={pendingCluster == cluster.name}
on:click={() => (pendingCluster = cluster.name, pendingPartition = null)}>
{cluster.name}
</ListGroupItem>
{/each}
</ListGroup>
{/if}
{#if $initialized && pendingCluster != null}
<br/>
<h4>Partiton</h4>
<ListGroup>
<ListGroupItem
active={pendingPartition == null}
on:click={() => (pendingPartition = null)}>
Any Partition
</ListGroupItem>
{#each clusters.find(c => c.name == pendingCluster).partitions as partition}
<ListGroupItem
active={pendingPartition == partition}
on:click={() => (pendingPartition = partition)}>
{partition}
</ListGroupItem>
{/each}
</ListGroup>
{/if}
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={() => {
isOpen = false
cluster = pendingCluster
partition = pendingPartition
dispatch('update', { cluster, partition })
}}>Close & Apply</Button>
<Button color="danger" on:click={() => {
isOpen = false
cluster = pendingCluster = null
partition = pendingPartition = null
dispatch('update', { cluster, partition })
}}>Reset</Button>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@@ -0,0 +1,302 @@
<!--
Copyright (c) 2021 Michael Keller
Originally created by Michael Keller (https://github.com/mhkeller/svelte-double-range-slider)
Changes: remove dependency, text inputs, configurable value ranges, on:change event
-->
<!--
@component
Properties:
- min: Number
- max: Number
- firstSlider: Number (Starting position of slider #1)
- secondSlider: Number (Starting position of slider #2)
Events:
- `change`: [Number, Number] (Positions of the two sliders)
-->
<script>
import { createEventDispatcher } from "svelte";
export let min;
export let max;
export let firstSlider;
export let secondSlider;
const dispatch = createEventDispatcher();
let values;
let start, end; /* Positions of sliders from 0 to 1 */
$: values = [firstSlider, secondSlider]; /* Avoid feedback loop */
$: start = Math.max(((firstSlider == null ? min : firstSlider) - min) / (max - min), 0);
$: end = Math.min(((secondSlider == null ? min : secondSlider) - min) / (max - min), 1);
let leftHandle;
let body;
let slider;
let inputFieldFrom, inputFieldTo;
let timeoutId = null;
function queueChangeEvent() {
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
timeoutId = null;
// Show selection but avoid feedback loop
if (values[0] != null && inputFieldFrom.value != values[0].toString())
inputFieldFrom.value = values[0].toString();
if (values[1] != null && inputFieldTo.value != values[1].toString())
inputFieldTo.value = values[1].toString();
dispatch('change', values);
}, 250);
}
function update() {
values = [
Math.floor(min + start * (max - min)),
Math.floor(min + end * (max - min))
];
queueChangeEvent();
}
function inputChanged(idx, event) {
let val = Number.parseInt(event.target.value);
if (Number.isNaN(val) || val < min) {
event.target.classList.add('bad');
return;
}
values[idx] = val;
event.target.classList.remove('bad');
if (idx == 0)
start = clamp((val - min) / (max - min), 0., 1.);
else
end = clamp((val - min) / (max - min), 0., 1.);
queueChangeEvent();
}
function clamp(x, min, max) {
return x < min
? min
: (x < max ? x : max);
}
function draggable(node) {
let x;
let y;
function handleMousedown(event) {
if (event.type === 'touchstart') {
event = event.touches[0];
}
x = event.clientX;
y = event.clientY;
node.dispatchEvent(new CustomEvent('dragstart', {
detail: { x, y }
}));
window.addEventListener('mousemove', handleMousemove);
window.addEventListener('mouseup', handleMouseup);
window.addEventListener('touchmove', handleMousemove);
window.addEventListener('touchend', handleMouseup);
}
function handleMousemove(event) {
if (event.type === 'touchmove') {
event = event.changedTouches[0];
}
const dx = event.clientX - x;
const dy = event.clientY - y;
x = event.clientX;
y = event.clientY;
node.dispatchEvent(new CustomEvent('dragmove', {
detail: { x, y, dx, dy }
}));
}
function handleMouseup(event) {
x = event.clientX;
y = event.clientY;
node.dispatchEvent(new CustomEvent('dragend', {
detail: { x, y }
}));
window.removeEventListener('mousemove', handleMousemove);
window.removeEventListener('mouseup', handleMouseup);
window.removeEventListener('touchmove', handleMousemove);
window.removeEventListener('touchend', handleMouseup);
}
node.addEventListener('mousedown', handleMousedown);
node.addEventListener('touchstart', handleMousedown);
return {
destroy() {
node.removeEventListener('mousedown', handleMousedown);
node.removeEventListener('touchstart', handleMousedown);
}
};
}
function setHandlePosition (which) {
return function (evt) {
const { left, right } = slider.getBoundingClientRect();
const parentWidth = right - left;
const p = Math.min(Math.max((evt.detail.x - left) / parentWidth, 0), 1);
if (which === 'start') {
start = p;
end = Math.max(end, p);
} else {
start = Math.min(p, start);
end = p;
}
update();
}
}
function setHandlesFromBody (evt) {
const { width } = body.getBoundingClientRect();
const { left, right } = slider.getBoundingClientRect();
const parentWidth = right - left;
const leftHandleLeft = leftHandle.getBoundingClientRect().left;
const pxStart = clamp((leftHandleLeft + event.detail.dx) - left, 0, parentWidth - width);
const pxEnd = clamp(pxStart + width, width, parentWidth);
const pStart = pxStart / parentWidth;
const pEnd = pxEnd / parentWidth;
start = pStart;
end = pEnd;
update();
}
</script>
<div class="double-range-container">
<div class="header">
<input class="form-control" type="text" placeholder="from..." bind:this={inputFieldFrom}
on:input={(e) => inputChanged(0, e)} />
<span>Full Range: <b> {min} </b> - <b> {max} </b></span>
<input class="form-control" type="text" placeholder="to..." bind:this={inputFieldTo}
on:input={(e) => inputChanged(1, e)} />
</div>
<div class="slider" bind:this={slider}>
<div
class="body"
bind:this={body}
use:draggable
on:dragmove|preventDefault|stopPropagation="{setHandlesFromBody}"
style="
left: {100 * start}%;
right: {100 * (1 - end)}%;
"
></div>
<div
class="handle"
bind:this={leftHandle}
data-which="start"
use:draggable
on:dragmove|preventDefault|stopPropagation="{setHandlePosition('start')}"
style="
left: {100 * start}%
"
></div>
<div
class="handle"
data-which="end"
use:draggable
on:dragmove|preventDefault|stopPropagation="{setHandlePosition('end')}"
style="
left: {100 * end}%
"
></div>
</div>
</div>
<style>
.header {
width: 100%;
display: flex;
justify-content: space-between;
margin-bottom: -5px;
}
.header :nth-child(2) {
padding-top: 10px;
}
.header input {
height: 25px;
border-radius: 5px;
width: 100px;
}
:global(.double-range-container .header input[type="text"].bad) {
color: #ff5c33;
border-color: #ff5c33;
}
.double-range-container {
width: 100%;
height: 50px;
user-select: none;
box-sizing: border-box;
white-space: nowrap
}
.slider {
position: relative;
width: 100%;
height: 6px;
top: 10px;
transform: translate(0, -50%);
background-color: #e2e2e2;
box-shadow: inset 0 7px 10px -5px #4a4a4a, inset 0 -1px 0px 0px #9c9c9c;
border-radius: 6px;
}
.handle {
position: absolute;
top: 50%;
width: 0;
height: 0;
}
.handle:after {
content: ' ';
box-sizing: border-box;
position: absolute;
border-radius: 50%;
width: 16px;
height: 16px;
background-color: #fdfdfd;
border: 1px solid #7b7b7b;
transform: translate(-50%, -50%)
}
/* .handle[data-which="end"]:after{
transform: translate(-100%, -50%);
} */
.handle:active:after {
background-color: #ddd;
z-index: 9;
}
.body {
top: 0;
position: absolute;
background-color: #34a1ff;
bottom: 0;
}
</style>

View File

@@ -0,0 +1,95 @@
<script>
import { createEventDispatcher } from 'svelte'
import { Row, Col, Button, Modal, ModalBody, ModalHeader, ModalFooter, FormGroup } from 'sveltestrap'
const dispatch = createEventDispatcher()
export let isOpen = false
export let from = null
export let to = null
let pendingFrom, pendingTo
function reset() {
pendingFrom = from == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(from)
pendingTo = to == null ? { hours: 0, mins: 0 } : secsToHoursAndMins(to)
}
reset()
function secsToHoursAndMins(duration) {
const hours = Math.floor(duration / 3600)
duration -= hours * 3600
const mins = Math.floor(duration / 60)
return { hours, mins }
}
function hoursAndMinsToSecs({ hours, mins }) {
return hours * 3600 + mins * 60
}
</script>
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Select Start Time
</ModalHeader>
<ModalBody>
<h4>Between</h4>
<Row>
<Col>
<div class="input-group mb-2 mr-sm-2">
<input type="number" class="form-control" bind:value={pendingFrom.hours}>
<div class="input-group-append">
<div class="input-group-text">h</div>
</div>
</div>
</Col>
<Col>
<div class="input-group mb-2 mr-sm-2">
<input type="number" class="form-control" bind:value={pendingFrom.mins}>
<div class="input-group-append">
<div class="input-group-text">m</div>
</div>
</div>
</Col>
</Row>
<h4>and</h4>
<Row>
<Col>
<div class="input-group mb-2 mr-sm-2">
<input type="number" class="form-control" bind:value={pendingTo.hours}>
<div class="input-group-append">
<div class="input-group-text">h</div>
</div>
</div>
</Col>
<Col>
<div class="input-group mb-2 mr-sm-2">
<input type="number" class="form-control" bind:value={pendingTo.mins}>
<div class="input-group-append">
<div class="input-group-text">m</div>
</div>
</div>
</Col>
</Row>
</ModalBody>
<ModalFooter>
<Button color="primary"
on:click={() => {
isOpen = false
from = hoursAndMinsToSecs(pendingFrom)
to = hoursAndMinsToSecs(pendingTo)
dispatch('update', { from, to })
}}>
Close & Apply
</Button>
<Button color="danger" on:click={() => {
isOpen = false
from = null
to = null
reset()
dispatch('update', { from, to })
}}>Reset</Button>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@@ -0,0 +1,323 @@
<!--
@component
Properties:
- menuText: String? (Optional text to show in the dropdown menu)
- filterPresets: Object? (Optional predefined filter values)
Events:
- 'update': The detail's 'filters' prop are new filter items to be applied
Functions:
- void update(additionalFilters: Object?): Triggers an update
-->
<script>
import { Row, Col, DropdownItem, DropdownMenu,
DropdownToggle, ButtonDropdown, Icon } from 'sveltestrap'
import { createEventDispatcher } from 'svelte'
import Info from './InfoBox.svelte'
import Cluster from './Cluster.svelte'
import JobStates, { allJobStates } from './JobStates.svelte'
import StartTime from './StartTime.svelte'
import Tags from './Tags.svelte'
import Tag from '../Tag.svelte'
import Duration from './Duration.svelte'
import Resources from './Resources.svelte'
import Statistics from './Stats.svelte'
// import TimeSelection from './TimeSelection.svelte'
const dispatch = createEventDispatcher()
export let menuText = null
export let filterPresets = {}
export let disableClusterSelection = false
export let startTimeQuickSelect = false
let filters = {
projectMatch: filterPresets.projectMatch || 'contains',
userMatch: filterPresets.userMatch || 'contains',
cluster: filterPresets.cluster || null,
partition: filterPresets.partition || null,
states: filterPresets.states || filterPresets.state ? [filterPresets.state].flat() : allJobStates,
startTime: filterPresets.startTime || { from: null, to: null },
tags: filterPresets.tags || [],
duration: filterPresets.duration || { from: null, to: null },
jobId: filterPresets.jobId || '',
arrayJobId: filterPresets.arrayJobId || null,
user: filterPresets.user || '',
project: filterPresets.project || '',
numNodes: filterPresets.numNodes || { from: null, to: null },
numHWThreads: filterPresets.numHWThreads || { from: null, to: null },
numAccelerators: filterPresets.numAccelerators || { from: null, to: null },
stats: [],
}
let isClusterOpen = false,
isJobStatesOpen = false,
isStartTimeOpen = false,
isTagsOpen = false,
isDurationOpen = false,
isResourcesOpen = false,
isStatsOpen = false
// Can be called from the outside to trigger a 'update' event from this component.
export function update(additionalFilters = null) {
if (additionalFilters != null)
for (let key in additionalFilters)
filters[key] = additionalFilters[key]
let items = []
if (filters.cluster)
items.push({ cluster: { eq: filters.cluster } })
if (filters.partition)
items.push({ partition: { eq: filters.partition } })
if (filters.states.length != allJobStates.length)
items.push({ state: filters.states })
if (filters.startTime.from || filters.startTime.to)
items.push({ startTime: { from: filters.startTime.from, to: filters.startTime.to } })
if (filters.tags.length != 0)
items.push({ tags: filters.tags })
if (filters.duration.from || filters.duration.to)
items.push({ duration: { from: filters.duration.from, to: filters.duration.to } })
if (filters.jobId)
items.push({ jobId: { eq: filters.jobId } })
if (filters.arrayJobId != null)
items.push({ arrayJobId: filters.arrayJobId })
if (filters.numNodes.from != null || filters.numNodes.to != null)
items.push({ numNodes: { from: filters.numNodes.from, to: filters.numNodes.to } })
if (filters.numHWThreads.from != null || filters.numHWThreads.to != null)
items.push({ numHWThreads: { from: filters.numHWThreads.from, to: filters.numHWThreads.to } })
if (filters.numAccelerators.from != null || filters.numAccelerators.to != null)
items.push({ numAccelerators: { from: filters.numAccelerators.from, to: filters.numAccelerators.to } })
if (filters.user)
items.push({ user: { [filters.userMatch]: filters.user } })
if (filters.project)
items.push({ project: { [filters.projectMatch]: filters.project } })
for (let stat of filters.stats)
items.push({ [stat.field]: { from: stat.from, to: stat.to } })
dispatch('update', { filters: items })
changeURL()
return items
}
function changeURL() {
const dateToUnixEpoch = (rfc3339) => Math.floor(Date.parse(rfc3339) / 1000)
let opts = []
if (filters.cluster)
opts.push(`cluster=${filters.cluster}`)
if (filters.partition)
opts.push(`partition=${filters.partition}`)
if (filters.states.length != allJobStates.length)
for (let state of filters.states)
opts.push(`state=${state}`)
if (filters.startTime.from && filters.startTime.to)
opts.push(`startTime=${dateToUnixEpoch(filters.startTime.from)}-${dateToUnixEpoch(filters.startTime.to)}`)
for (let tag of filters.tags)
opts.push(`tag=${tag}`)
if (filters.duration.from && filters.duration.to)
opts.push(`duration=${filters.duration.from}-${filters.duration.to}`)
if (filters.numNodes.from && filters.numNodes.to)
opts.push(`numNodes=${filters.numNodes.from}-${filters.numNodes.to}`)
if (filters.numAccelerators.from && filters.numAccelerators.to)
opts.push(`numAccelerators=${filters.numAccelerators.from}-${filters.numAccelerators.to}`)
if (filters.user)
opts.push(`user=${filters.user}`)
if (filters.userMatch != 'contains')
opts.push(`userMatch=${filters.userMatch}`)
if (filters.project)
opts.push(`project=${filters.project}`)
if (filters.projectMatch != 'contains')
opts.push(`projectMatch=${filters.projectMatch}`)
if (opts.length == 0 && window.location.search.length <= 1)
return
let newurl = `${window.location.pathname}?${opts.join('&')}`
window.history.replaceState(null, '', newurl)
}
</script>
<Row>
<Col xs="auto">
<ButtonDropdown class="cc-dropdown-on-hover">
<DropdownToggle outline caret color="success">
<Icon name="sliders"/>
Filters
</DropdownToggle>
<DropdownMenu>
<DropdownItem header>
Manage Filters
</DropdownItem>
{#if menuText}
<DropdownItem disabled>{menuText}</DropdownItem>
<DropdownItem divider />
{/if}
<DropdownItem on:click={() => (isClusterOpen = true)}>
<Icon name="cpu"/> Cluster/Partition
</DropdownItem>
<DropdownItem on:click={() => (isJobStatesOpen = true)}>
<Icon name="gear-fill"/> Job States
</DropdownItem>
<DropdownItem on:click={() => (isStartTimeOpen = true)}>
<Icon name="calendar-range"/> Start Time
</DropdownItem>
<DropdownItem on:click={() => (isDurationOpen = true)}>
<Icon name="stopwatch"/> Duration
</DropdownItem>
<DropdownItem on:click={() => (isTagsOpen = true)}>
<Icon name="tags"/> Tags
</DropdownItem>
<DropdownItem on:click={() => (isResourcesOpen = true)}>
<Icon name="hdd-stack"/> Nodes/Accelerators
</DropdownItem>
<DropdownItem on:click={() => (isStatsOpen = true)}>
<Icon name="bar-chart" on:click={() => (isStatsOpen = true)}/> Statistics
</DropdownItem>
{#if startTimeQuickSelect}
<DropdownItem divider/>
<DropdownItem disabled>Start Time Qick Selection</DropdownItem>
{#each [
{ text: 'Last 6hrs', seconds: 6*60*60 },
{ text: 'Last 12hrs', seconds: 12*60*60 },
{ text: 'Last 24hrs', seconds: 24*60*60 },
{ text: 'Last 48hrs', seconds: 48*60*60 },
{ text: 'Last 7 days', seconds: 7*24*60*60 },
{ text: 'Last 30 days', seconds: 30*24*60*60 }
] as {text, seconds}}
<DropdownItem on:click={() => {
filters.startTime.from = (new Date(Date.now() - seconds * 1000)).toISOString()
filters.startTime.to = (new Date(Date.now())).toISOString()
update()
}}>
<Icon name="calendar-range"/> {text}
</DropdownItem>
{/each}
{/if}
</DropdownMenu>
</ButtonDropdown>
</Col>
<!-- {#if startTimeQuickSelect}
<Col xs="auto">
<TimeSelection customEnabled={false} anyEnabled={true}
from={filters.startTime.from ? new Date(filters.startTime.from) : null}
to={filters.startTime.to ? new Date(filters.startTime.to) : null}
options={{
'Last 6hrs': 6*60*60,
'Last 12hrs': 12*60*60,
'Last 24hrs': 24*60*60,
'Last 48hrs': 48*60*60,
'Last 7 days': 7*24*60*60,
'Last 30 days': 30*24*60*60}}
on:change={({ detail: { from, to } }) => {
filters.startTime.from = from?.toISOString()
filters.startTime.to = to?.toISOString()
console.log(filters.startTime)
update()
}}
/>
</Col>
{/if} -->
<Col xs="auto">
{#if filters.cluster}
<Info icon="cpu" on:click={() => (isClusterOpen = true)}>
{filters.cluster}
{#if filters.partition}
({filters.partition})
{/if}
</Info>
{/if}
{#if filters.states.length != allJobStates.length}
<Info icon="gear-fill" on:click={() => (isJobStatesOpen = true)}>
{filters.states.join(', ')}
</Info>
{/if}
{#if filters.startTime.from || filters.startTime.to}
<Info icon="calendar-range" on:click={() => (isStartTimeOpen = true)}>
{new Date(filters.startTime.from).toLocaleString()} - {new Date(filters.startTime.to).toLocaleString()}
</Info>
{/if}
{#if filters.duration.from || filters.duration.to}
<Info icon="stopwatch" on:click={() => (isDurationOpen = true)}>
{Math.floor(filters.duration.from / 3600)}h:{Math.floor(filters.duration.from % 3600 / 60)}m
-
{Math.floor(filters.duration.to / 3600)}h:{Math.floor(filters.duration.to % 3600 / 60)}m
</Info>
{/if}
{#if filters.tags.length != 0}
<Info icon="tags" on:click={() => (isTagsOpen = true)}>
{#each filters.tags as tagId}
<Tag id={tagId} clickable={false} />
{/each}
</Info>
{/if}
{#if filters.numNodes.from != null || filters.numNodes.to != null}
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
Nodes: {filters.numNodes.from} - {filters.numNodes.to}
</Info>
{/if}
{#if filters.stats.length > 0}
<Info icon="bar-chart" on:click={() => (isStatsOpen = true)}>
{filters.stats.map(stat => `${stat.text}: ${stat.from} - ${stat.to}`).join(', ')}
</Info>
{/if}
</Col>
</Row>
<Cluster
disableClusterSelection={disableClusterSelection}
bind:isOpen={isClusterOpen}
bind:cluster={filters.cluster}
bind:partition={filters.partition}
on:update={() => update()} />
<JobStates
bind:isOpen={isJobStatesOpen}
bind:states={filters.states}
on:update={() => update()} />
<StartTime
bind:isOpen={isStartTimeOpen}
bind:from={filters.startTime.from}
bind:to={filters.startTime.to}
on:update={() => update()} />
<Duration
bind:isOpen={isDurationOpen}
bind:from={filters.duration.from}
bind:to={filters.duration.to}
on:update={() => update()} />
<Tags
bind:isOpen={isTagsOpen}
bind:tags={filters.tags}
on:update={() => update()} />
<Resources cluster={filters.cluster}
bind:isOpen={isResourcesOpen}
bind:numNodes={filters.numNodes}
bind:numHWThreads={filters.numHWThreads}
bind:numAccelerators={filters.numAccelerators}
on:update={() => update()} />
<Statistics cluster={filters.cluster}
bind:isOpen={isStatsOpen}
bind:stats={filters.stats}
on:update={() => update()} />
<style>
:global(.cc-dropdown-on-hover:hover .dropdown-menu) {
display: block;
margin-top: 0px;
padding-top: 0px;
transform: none !important;
}
</style>

View File

@@ -0,0 +1,11 @@
<script>
import { Button, Icon } from 'sveltestrap'
export let icon
export let modified = false
</script>
<Button outline color={modified ? 'warning' : 'primary'} on:click>
<Icon name={icon}/>
<slot />
</Button>

View File

@@ -0,0 +1,47 @@
<script context="module">
export const allJobStates = [ 'running', 'completed', 'failed', 'cancelled', 'stopped', 'timeout', 'preempted', 'out_of_memory' ]
</script>
<script>
import { createEventDispatcher } from 'svelte'
import { Button, ListGroup, ListGroupItem,
Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
const dispatch = createEventDispatcher()
export let isModified = false
export let isOpen = false
export let states = [...allJobStates]
let pendingStates = [...states]
$: isModified = states.length != pendingStates.length || !states.every(state => pendingStates.includes(state))
</script>
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Select Job States
</ModalHeader>
<ModalBody>
<ListGroup>
{#each allJobStates as state}
<ListGroupItem>
<input type=checkbox bind:group={pendingStates} name="flavours" value={state}>
{state}
</ListGroupItem>
{/each}
</ListGroup>
</ModalBody>
<ModalFooter>
<Button color="primary" disabled={pendingStates.length == 0} on:click={() => {
isOpen = false
states = [...pendingStates]
dispatch('update', { states })
}}>Close & Apply</Button>
<Button color="danger" on:click={() => {
isOpen = false
states = [...allJobStates]
pendingStates = [...allJobStates]
dispatch('update', { states })
}}>Reset</Button>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@@ -0,0 +1,99 @@
<script>
import { createEventDispatcher, getContext } from 'svelte'
import { Button, Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
import DoubleRangeSlider from './DoubleRangeSlider.svelte'
const clusters = getContext('clusters'),
initialized = getContext('initialized'),
dispatch = createEventDispatcher()
export let cluster = null
export let isModified = false
export let isOpen = false
export let numNodes = { from: null, to: null }
export let numHWThreads = { from: null, to: null }
export let numAccelerators = { from: null, to: null }
let pendingNumNodes = numNodes, pendingNumHWThreads = numHWThreads, pendingNumAccelerators = numAccelerators
$: isModified = pendingNumNodes.from != numNodes.from || pendingNumNodes.to != numNodes.to
|| pendingNumHWThreads.from != numHWThreads.from || pendingNumHWThreads.to != numHWThreads.to
|| pendingNumAccelerators.from != numAccelerators.from || pendingNumAccelerators.to != numAccelerators.to
const findMaxNumAccels = clusters => clusters.reduce((max, cluster) => Math.max(max,
cluster.subClusters.reduce((max, sc) => Math.max(max, sc.topology.accelerators?.length || 0), 0)), 0)
let minNumNodes = 1, maxNumNodes = 0, minNumHWThreads = 1, maxNumHWThreads = 0, minNumAccelerators = 0, maxNumAccelerators = 0
$: {
if ($initialized) {
if (cluster != null) {
const { filterRanges, subClusters } = clusters.find(c => c.name == cluster)
minNumNodes = filterRanges.numNodes.from
maxNumNodes = filterRanges.numNodes.to
maxNumAccelerators = findMaxNumAccels([{ subClusters }])
} else if (clusters.length > 0) {
const { filterRanges } = clusters[0]
minNumNodes = filterRanges.numNodes.from
maxNumNodes = filterRanges.numNodes.to
maxNumAccelerators = findMaxNumAccels(clusters)
for (let cluster of clusters) {
const { filterRanges } = cluster
minNumNodes = Math.min(minNumNodes, filterRanges.numNodes.from)
maxNumNodes = Math.max(maxNumNodes, filterRanges.numNodes.to)
}
}
}
}
$: {
if (isOpen && $initialized && pendingNumNodes.from == null && pendingNumNodes.to == null) {
pendingNumNodes = { from: 0, to: maxNumNodes }
}
}
</script>
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Select Number of Nodes, HWThreads and Accelerators
</ModalHeader>
<ModalBody>
<h4>Number of Nodes</h4>
<DoubleRangeSlider
on:change={({ detail }) => (pendingNumNodes = { from: detail[0], to: detail[1] })}
min={minNumNodes} max={maxNumNodes}
firstSlider={pendingNumNodes.from} secondSlider={pendingNumNodes.to} />
<!-- <DoubleRangeSlider
on:change={({ detail }) => (pendingNumHWThreads = { from: detail[0], to: detail[1] })}
min={minNumHWThreads} max={maxNumHWThreads}
firstSlider={pendingNumHWThreads.from} secondSlider={pendingNumHWThreads.to} /> -->
{#if maxNumAccelerators != null && maxNumAccelerators > 1}
<DoubleRangeSlider
on:change={({ detail }) => (pendingNumAccelerators = { from: detail[0], to: detail[1] })}
min={minNumAccelerators} max={maxNumAccelerators}
firstSlider={pendingNumAccelerators.from} secondSlider={pendingNumAccelerators.to} />
{/if}
</ModalBody>
<ModalFooter>
<Button color="primary"
disabled={pendingNumNodes.from == null || pendingNumNodes.to == null}
on:click={() => {
isOpen = false
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to }
numHWThreads = { from: pendingNumHWThreads.from, to: pendingNumHWThreads.to }
numAccelerators = { from: pendingNumAccelerators.from, to: pendingNumAccelerators.to }
dispatch('update', { numNodes, numHWThreads, numAccelerators })
}}>
Close & Apply
</Button>
<Button color="danger" on:click={() => {
isOpen = false
pendingNumNodes = { from: null, to: null }
pendingNumHWThreads = { from: null, to: null }
pendingNumAccelerators = { from: null, to: null }
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to }
numHWThreads = { from: pendingNumHWThreads.from, to: pendingNumHWThreads.to }
numAccelerators = { from: pendingNumAccelerators.from, to: pendingNumAccelerators.to }
dispatch('update', { numNodes, numHWThreads, numAccelerators })
}}>Reset</Button>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@@ -0,0 +1,90 @@
<script>
import { createEventDispatcher, getContext } from 'svelte'
import { Row, Button, Input, Modal, ModalBody, ModalHeader, ModalFooter, FormGroup } from 'sveltestrap'
const dispatch = createEventDispatcher()
export let isModified = false
export let isOpen = false
export let from = null
export let to = null
let pendingFrom, pendingTo
function reset() {
pendingFrom = from == null ? { date: '0000-00-00', time: '00:00' } : fromRFC3339(from)
pendingTo = to == null ? { date: '0000-00-00', time: '00:00' } : fromRFC3339(to)
}
reset()
function toRFC3339({ date, time }, secs = 0) {
const dparts = date.split('-')
const tparts = time.split(':')
const d = new Date(
Number.parseInt(dparts[0]),
Number.parseInt(dparts[1]) - 1,
Number.parseInt(dparts[2]),
Number.parseInt(tparts[0]),
Number.parseInt(tparts[1]), secs)
return d.toISOString()
}
function fromRFC3339(rfc3339) {
const d = new Date(rfc3339)
const pad = (n) => n.toString().padStart(2, '0')
const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
const time = `${pad(d.getHours())}:${pad(d.getMinutes())}`
return { date, time }
}
$: isModified = (from != toRFC3339(pendingFrom) || to != toRFC3339(pendingTo, 59))
&& !(from == null && pendingFrom.date == '0000-00-00' && pendingFrom.time == '00:00')
&& !(to == null && pendingTo.date == '0000-00-00' && pendingTo.time == '00:00')
</script>
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Select Start Time
</ModalHeader>
<ModalBody>
<h4>From</h4>
<Row>
<FormGroup class="col">
<Input type="date" bind:value={pendingFrom.date}/>
</FormGroup>
<FormGroup class="col">
<Input type="time" bind:value={pendingFrom.time}/>
</FormGroup>
</Row>
<h4>To</h4>
<Row>
<FormGroup class="col">
<Input type="date" bind:value={pendingTo.date}/>
</FormGroup>
<FormGroup class="col">
<Input type="time" bind:value={pendingTo.time}/>
</FormGroup>
</Row>
</ModalBody>
<ModalFooter>
<Button color="primary"
disabled={pendingFrom.date == '0000-00-00' || pendingTo.date == '0000-00-00'}
on:click={() => {
isOpen = false
from = toRFC3339(pendingFrom)
to = toRFC3339(pendingTo, 59)
dispatch('update', { from, to })
}}>
Close & Apply
</Button>
<Button color="danger" on:click={() => {
isOpen = false
from = null
to = null
reset()
dispatch('update', { from, to })
}}>Reset</Button>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@@ -0,0 +1,113 @@
<script>
import { createEventDispatcher, getContext } from 'svelte'
import { Button, Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
import DoubleRangeSlider from './DoubleRangeSlider.svelte'
const clusters = getContext('clusters'),
initialized = getContext('initialized'),
dispatch = createEventDispatcher()
export let cluster = null
export let isModified = false
export let isOpen = false
export let stats = []
let statistics = [
{
field: 'flopsAnyAvg',
text: 'FLOPs (Avg.)',
metric: 'flops_any',
from: 0, to: 0, peak: 0,
enabled: false
},
{
field: 'memBwAvg',
text: 'Mem. Bw. (Avg.)',
metric: 'mem_bw',
from: 0, to: 0, peak: 0,
enabled: false
},
{
field: 'loadAvg',
text: 'Load (Avg.)',
metric: 'cpu_load',
from: 0, to: 0, peak: 0,
enabled: false
},
{
field: 'memUsedMax',
text: 'Mem. used (Max.)',
metric: 'mem_used',
from: 0, to: 0, peak: 0,
enabled: false
}
]
$: isModified = !statistics.every(a => {
let b = stats.find(s => s.field == a.field)
if (b == null)
return !a.enabled
return a.from == b.from && a.to == b.to
})
function getPeak(cluster, metric) {
const mc = cluster.metricConfig.find(mc => mc.name == metric)
return mc ? mc.peak : 0
}
function resetRange(isInitialized, cluster) {
if (!isInitialized)
return
if (cluster != null) {
let c = clusters.find(c => c.name == cluster)
for (let stat of statistics) {
stat.peak = getPeak(c, stat.metric)
stat.from = 0
stat.to = stat.peak
}
} else {
for (let stat of statistics) {
for (let c of clusters) {
stat.peak = Math.max(stat.peak, getPeak(c, stat.metric))
}
stat.from = 0
stat.to = stat.peak
}
}
statistics = [...statistics]
}
$: resetRange($initialized, cluster)
</script>
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Filter based on statistics (of non-running jobs)
</ModalHeader>
<ModalBody>
{#each statistics as stat}
<h4>{stat.text}</h4>
<DoubleRangeSlider
on:change={({ detail }) => (stat.from = detail[0], stat.to = detail[1], stat.enabled = true)}
min={0} max={stat.peak}
firstSlider={stat.from} secondSlider={stat.to} />
{/each}
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={() => {
isOpen = false
stats = statistics.filter(stat => stat.enabled)
dispatch('update', { stats })
}}>Close & Apply</Button>
<Button color="danger" on:click={() => {
isOpen = false
statistics.forEach(stat => stat.enabled = false)
stats = []
dispatch('update', { stats })
}}>Reset</Button>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@@ -0,0 +1,67 @@
<script>
import { createEventDispatcher, getContext } from 'svelte'
import { Button, ListGroup, ListGroupItem, Input,
Modal, ModalBody, ModalHeader, ModalFooter, Icon } from 'sveltestrap'
import { fuzzySearchTags } from '../utils.js'
import Tag from '../Tag.svelte'
const allTags = getContext('tags'),
initialized = getContext('initialized'),
dispatch = createEventDispatcher()
export let isModified = false
export let isOpen = false
export let tags = []
let pendingTags = [...tags]
$: isModified = tags.length != pendingTags.length || !tags.every(tagId => pendingTags.includes(tagId))
let searchTerm = ''
</script>
<Modal isOpen={isOpen} toggle={() => (isOpen = !isOpen)}>
<ModalHeader>
Select Tags
</ModalHeader>
<ModalBody>
<Input type="text" placeholder="Search" bind:value={searchTerm} />
<br/>
<ListGroup>
{#if $initialized}
{#each fuzzySearchTags(searchTerm, allTags) as tag (tag)}
<ListGroupItem>
{#if pendingTags.includes(tag.id)}
<Button outline color="danger"
on:click={() => (pendingTags = pendingTags.filter(id => id != tag.id))}>
<Icon name="dash-circle" />
</Button>
{:else}
<Button outline color="success"
on:click={() => (pendingTags = [...pendingTags, tag.id])}>
<Icon name="plus-circle" />
</Button>
{/if}
<Tag tag={tag} />
</ListGroupItem>
{:else}
<ListGroupItem disabled>No Tags</ListGroupItem>
{/each}
{/if}
</ListGroup>
</ModalBody>
<ModalFooter>
<Button color="primary" on:click={() => {
isOpen = false
tags = [...pendingTags]
dispatch('update', { tags })
}}>Close & Apply</Button>
<Button color="danger" on:click={() => {
isOpen = false
tags = []
pendingTags = []
dispatch('update', { tags })
}}>Reset</Button>
<Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter>
</Modal>

View File

@@ -0,0 +1,80 @@
<script>
import { Icon, Input, InputGroup, InputGroupText } from 'sveltestrap'
import { createEventDispatcher } from "svelte"
export let from
export let to
export let customEnabled = true
export let anyEnabled = false
export let options = {
'Last half hour': 30*60,
'Last hour': 60*60,
'Last 2hrs': 2*60*60,
'Last 4hrs': 4*60*60,
'Last 12hrs': 12*60*60,
'Last 24hrs': 24*60*60
}
$: pendingFrom = from
$: pendingTo = to
const dispatch = createEventDispatcher()
let timeRange = to && from
? (to.getTime() - from.getTime()) / 1000
: (anyEnabled ? -2 : -1)
function updateTimeRange(event) {
if (timeRange == -1) {
pendingFrom = null
pendingTo = null
return
}
if (timeRange == -2) {
from = pendingFrom = null
to = pendingTo = null
dispatch('change', { from, to })
return
}
let now = Date.now(), t = timeRange * 1000
from = pendingFrom = new Date(now - t)
to = pendingTo = new Date(now)
dispatch('change', { from, to })
}
function updateExplicitTimeRange(type, event) {
let d = new Date(Date.parse(event.target.value));
if (type == 'from') pendingFrom = d
else pendingTo = d
if (pendingFrom != null && pendingTo != null) {
from = pendingFrom
to = pendingTo
dispatch('change', { from, to })
}
}
</script>
<InputGroup class="inline-from">
<InputGroupText><Icon name="clock-history"/></InputGroupText>
<!-- <InputGroupText>
Time
</InputGroupText> -->
<select class="form-select" bind:value={timeRange} on:change={updateTimeRange}>
{#if customEnabled}
<option value={-1}>Custom</option>
{/if}
{#if anyEnabled}
<option value={-2}>Any</option>
{/if}
{#each Object.entries(options) as [name, seconds]}
<option value={seconds}>{name}</option>
{/each}
</select>
{#if timeRange == -1}
<InputGroupText>from</InputGroupText>
<Input type="datetime-local" on:change={(event) => updateExplicitTimeRange('from', event)}></Input>
<InputGroupText>to</InputGroupText>
<Input type="datetime-local" on:change={(event) => updateExplicitTimeRange('to', event)}></Input>
{/if}
</InputGroup>

View File

@@ -0,0 +1,51 @@
<script>
import { InputGroup, Input } from 'sveltestrap'
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
export let user = ''
export let project = ''
let mode = 'user', term = ''
const throttle = 500
function modeChanged() {
if (mode == 'user') {
project = term
term = user
} else {
user = term
term = project
}
termChanged(0)
}
let timeoutId = null
function termChanged(sleep = throttle) {
if (mode == 'user')
user = term
else
project = term
if (timeoutId != null)
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
dispatch('update', {
user,
project
})
}, sleep)
}
</script>
<InputGroup>
<select style="max-width: 175px;" class="form-select"
bind:value={mode} on:change={modeChanged}>
<option value={'user'}>Search User</option>
<option value={'project'}>Search Project</option>
</select>
<Input
type="text" bind:value={term} on:change={() => termChanged()} on:keyup={(event) => termChanged(event.key == 'Enter' ? 0 : throttle)}
placeholder={mode == 'user' ? 'filter username...' : 'filter project...'} />
</InputGroup>