Merge branch 'master' into import-data-sanitation

This commit is contained in:
Jan Eitzinger
2023-04-07 08:57:42 +02:00
110 changed files with 8191 additions and 1464 deletions

View File

@@ -10,11 +10,11 @@
const ccconfig = getContext('cc-config')
export let user
export let isAdmin
</script>
{#if user.IsAdmin}
{#if isAdmin == true}
<Card style="margin-bottom: 1.5em;">
<CardHeader>
<CardTitle class="mb-1">Admin Options</CardTitle>

View File

@@ -1,26 +1,48 @@
<script>
import { Icon, Button, InputGroup, Input, Collapse,
Navbar, NavbarBrand, Nav, NavItem, NavLink, NavbarToggler,
Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'sveltestrap'
Dropdown, DropdownToggle, DropdownMenu, DropdownItem, InputGroupText } from 'sveltestrap'
export let username // empty string if auth. is disabled, otherwise the username as string
export let isAdmin // boolean
export let authlevel // Integer
export let clusters // array of names
export let roles // Role Enum-Like
let isOpen = false
const views = [
isAdmin
? { title: 'Jobs', adminOnly: false, href: '/monitoring/jobs/', icon: 'card-list' }
: { title: 'My Jobs', adminOnly: false, href: `/monitoring/user/${username}`, icon: 'bar-chart-line-fill' },
{ title: 'Users', adminOnly: true, href: '/monitoring/users/', icon: 'people-fill' },
{ title: 'Projects', adminOnly: true, href: '/monitoring/projects/', icon: 'folder' },
{ title: 'Tags', adminOnly: false, href: '/monitoring/tags/', icon: 'tags' }
const userviews = [
{ title: 'My Jobs', href: `/monitoring/user/${username}`, icon: 'bar-chart-line-fill' },
{ title: `Job Search`, href: '/monitoring/jobs/', icon: 'card-list' },
{ title: 'Tags', href: '/monitoring/tags/', icon: 'tags' }
]
const managerviews = [
{ title: 'My Jobs', href: `/monitoring/user/${username}`, icon: 'bar-chart-line-fill' },
{ title: `Managed Jobs`, href: '/monitoring/jobs/', icon: 'card-list' },
{ title: `Managed Users`, href: '/monitoring/users/', icon: 'people-fill' },
{ title: 'Tags', href: '/monitoring/tags/', icon: 'tags' }
]
const supportviews = [
{ title: 'My Jobs', href: `/monitoring/user/${username}`, icon: 'bar-chart-line-fill' },
{ title: 'Jobs', href: '/monitoring/jobs/', icon: 'card-list' },
{ title: 'Users', href: '/monitoring/users/', icon: 'people-fill' },
{ title: 'Projects', href: '/monitoring/projects/', icon: 'folder' },
{ title: 'Tags', href: '/monitoring/tags/', icon: 'tags' }
]
const adminviews = [
{ title: 'My Jobs', href: `/monitoring/user/${username}`, icon: 'bar-chart-line-fill' },
{ title: 'Jobs', href: '/monitoring/jobs/', icon: 'card-list' },
{ title: 'Users', href: '/monitoring/users/', icon: 'people-fill' },
{ title: 'Projects', href: '/monitoring/projects/', icon: 'folder' },
{ title: 'Tags', href: '/monitoring/tags/', icon: 'tags' }
]
const viewsPerCluster = [
{ title: 'Analysis', adminOnly: true, href: '/monitoring/analysis/', icon: 'graph-up' },
{ title: 'Systems', adminOnly: true, href: '/monitoring/systems/', icon: 'cpu' },
{ title: 'Status', adminOnly: true, href: '/monitoring/status/', icon: 'cpu' },
{ title: 'Analysis', requiredRole: roles.support, href: '/monitoring/analysis/', icon: 'graph-up' },
{ title: 'Systems', requiredRole: roles.admin, href: '/monitoring/systems/', icon: 'cpu' },
{ title: 'Status', requiredRole: roles.admin, href: '/monitoring/status/', icon: 'cpu' },
]
</script>
@@ -31,10 +53,26 @@
<NavbarToggler on:click={() => (isOpen = !isOpen)} />
<Collapse {isOpen} navbar expand="lg" on:update={({ detail }) => (isOpen = detail.isOpen)}>
<Nav pills>
{#each views.filter(item => isAdmin || !item.adminOnly) as item}
<NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink>
{/each}
{#each viewsPerCluster.filter(item => !item.adminOnly || isAdmin) as item}
{#if authlevel == roles.admin}
{#each adminviews as item}
<NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink>
{/each}
{:else if authlevel == roles.support}
{#each supportviews as item}
<NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink>
{/each}
{:else if authlevel == roles.manager}
{#each managerviews as item}
<NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink>
{/each}
{:else if authlevel == roles.user}
{#each userviews as item}
<NavLink href={item.href} active={window.location.pathname == item.href}><Icon name={item.icon}/> {item.title}</NavLink>
{/each}
{:else}
<p>API User or Unauthorized!</p>
{/if}
{#each viewsPerCluster.filter(item => item.requiredRole <= authlevel) as item}
<NavItem>
<Dropdown nav inNavbar>
<DropdownToggle nav caret>
@@ -55,8 +93,9 @@
<div class="d-flex">
<form method="GET" action="/search">
<InputGroup>
<Input type="text" placeholder={isAdmin ? "Search jobId / username" : "Search jobId"} name="searchId"/>
<Input type="text" placeholder="Search 'type:<query>' ..." name="searchId"/>
<Button outline type="submit"><Icon name="search"/></Button>
<InputGroupText style="cursor:help;" title={(authlevel >= roles.support) ? "Example: 'projectId:a100cd', Types are: jobId | jobName | projectId | username | name" : "Example: 'jobName:myjob', Types are jobId | jobName | projectId"}><Icon name="info-circle"/></InputGroupText>
</InputGroup>
</form>
{#if username}

View File

@@ -14,6 +14,8 @@
const ccconfig = getContext('cc-config')
export let filterPresets = {}
export let authlevel
export let roles
let filters = []
let jobList, matchedJobs = null
@@ -67,7 +69,7 @@
</Col>
<Col xs="3" style="margin-left: auto;">
<UserOrProject on:update={({ detail }) => filters.update(detail)}/>
<UserOrProject bind:authlevel={authlevel} bind:roles={roles} on:update={({ detail }) => filters.update(detail)}/>
</Col>
<Col xs="2">
<Refresher on:reload={() => jobList.update()} />

View File

@@ -1,4 +1,4 @@
<!--
<!--
@component List of users or projects
-->
<script>
@@ -20,6 +20,7 @@
const stats = operationStore(`query($filter: [JobFilter!]!) {
rows: jobsStatistics(filter: $filter, groupBy: ${type}) {
id
name
totalJobs
totalWalltime
totalCoreHours
@@ -54,7 +55,7 @@
: (sorting.direction == 'up'
? (a, b) => a[sorting.field] - b[sorting.field]
: (a, b) => b[sorting.field] - a[sorting.field])
return stats.filter(u => u.id.includes(nameFilter)).sort(cmp)
}
@@ -93,6 +94,15 @@
<Icon name="sort-numeric-down" />
</Button>
</th>
{#if type == 'USER'}
<th scope="col">
Name
<Button color="{sorting.field == 'name' ? 'primary' : 'light'}"
size="sm" on:click={e => changeSorting(e, 'name')}>
<Icon name="sort-numeric-down" />
</Button>
</th>
{/if}
<th scope="col">
Total Jobs
<Button color="{sorting.field == 'totalJobs' ? 'primary' : 'light'}"
@@ -137,6 +147,9 @@
{row.id}
{/if}
</td>
{#if type == 'USER'}
<td>{row?.name ? row.name : ''}</td>
{/if}
<td>{row.totalJobs}</td>
<td>{row.totalWalltime}</td>
<td>{row.totalCoreHours}</td>
@@ -148,4 +161,4 @@
{/each}
{/if}
</tbody>
</Table>
</Table>

View File

@@ -62,7 +62,7 @@
query(nodesQuery)
$: console.log($nodesQuery?.data?.nodeMetrics[0].metrics)
// $: console.log($nodesQuery?.data?.nodeMetrics[0].metrics)
</script>
<Row>

View File

@@ -48,7 +48,7 @@
if (s1 == null || s2 == null)
return -1
return s.dir != 'up' ? s1[stat] - s2[stat] : s2[stat] - s1[stat]
return s.dir != 'up' ? s1[stat] - s2[stat] : s2[stat] - s1[stat]
})
}
@@ -61,7 +61,7 @@
<thead>
<tr>
<th>
<Button outline on:click={() => (isMetricSelectionOpen = true, console.log(isMetricSelectionOpen))}>
<Button outline on:click={() => (isMetricSelectionOpen = true)}> <!-- log to click ', console.log(isMetricSelectionOpen)' -->
Metrics
</Button>
</th>

View File

@@ -4,7 +4,7 @@ import Config from './Config.root.svelte'
new Config({
target: document.getElementById('svelte-app'),
props: {
user: user
isAdmin: isAdmin
},
context: new Map([
['cc-config', clusterCockpitConfig]

View File

@@ -2,11 +2,13 @@
import { Row, Col } from 'sveltestrap'
import { onMount } from 'svelte'
import EditRole from './admin/EditRole.svelte'
import EditProject from './admin/EditProject.svelte'
import AddUser from './admin/AddUser.svelte'
import ShowUsers from './admin/ShowUsers.svelte'
import Options from './admin/Options.svelte'
let users = []
let roles = []
function getUserList() {
fetch('/api/users/?via-ldap=false&not-just-user=true')
@@ -16,19 +18,35 @@
})
}
onMount(() => getUserList())
function getValidRoles() {
fetch('/api/roles/')
.then(res => res.json())
.then(rolesRaw => {
roles = rolesRaw
})
}
function initAdmin() {
getUserList()
getValidRoles()
}
onMount(() => initAdmin())
</script>
<Row cols={2} class="p-2 g-2" >
<Col class="mb-1">
<AddUser on:reload={getUserList}/>
<AddUser roles={roles} on:reload={getUserList}/>
</Col>
<Col class="mb-1">
<ShowUsers on:reload={getUserList} bind:users={users}/>
</Col>
<Col>
<EditRole on:reload={getUserList}/>
<EditRole roles={roles} on:reload={getUserList}/>
</Col>
<Col>
<EditProject on:reload={getUserList}/>
</Col>
<Col>
<Options/>

View File

@@ -8,6 +8,8 @@
let message = {msg: '', color: '#d63384'}
let displayMessage = false
export let roles = []
async function handleUserSubmit() {
let form = document.querySelector('#create-user-form')
let formData = new FormData(form)
@@ -45,17 +47,7 @@
<form id="create-user-form" method="post" action="/api/users/" class="card-body" on:submit|preventDefault={handleUserSubmit}>
<CardTitle class="mb-3">Create User</CardTitle>
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" aria-describedby="nameHelp"/>
<div id="nameHelp" class="form-text">Optional, can be blank.</div>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email address</label>
<input type="email" class="form-control" id="email" name="email" aria-describedby="emailHelp"/>
<div id="emailHelp" class="form-text">Optional, can be blank.</div>
</div>
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<label for="username" class="form-label">Username (ID)</label>
<input type="text" class="form-control" id="username" name="username" aria-describedby="usernameHelp"/>
<div id="usernameHelp" class="form-text">Must be unique.</div>
</div>
@@ -64,24 +56,38 @@
<input type="password" class="form-control" id="password" name="password" aria-describedby="passwordHelp"/>
<div id="passwordHelp" class="form-text">Only API users are allowed to have a blank password. Users with a blank password can only authenticate via Tokens.</div>
</div>
<div class="mb-3">
<label for="name" class="form-label">Project</label>
<input type="text" class="form-control" id="project" name="project" aria-describedby="projectHelp"/>
<div id="projectHelp" class="form-text">Only Manager users can have a project. Allows to inspect jobs and users of given project.</div>
</div>
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" aria-describedby="nameHelp"/>
<div id="nameHelp" class="form-text">Optional, can be blank.</div>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email address</label>
<input type="email" class="form-control" id="email" name="email" aria-describedby="emailHelp"/>
<div id="emailHelp" class="form-text">Optional, can be blank.</div>
</div>
<div class="mb-3">
<p>Role:</p>
<div>
<input type="radio" id="user" name="role" value="user" checked/>
<label for="user">User (regular user, same as if created via LDAP sync.)</label>
</div>
<div>
<input type="radio" id="api" name="role" value="api"/>
<label for="api">API</label>
</div>
<div>
<input type="radio" id="support" name="role" value="support"/>
<label for="support">Support</label>
</div>
<div>
<input type="radio" id="admin" name="role" value="admin"/>
<label for="admin">Admin</label>
</div>
{#each roles as role, i}
{#if i == 0}
<div>
<input type="radio" id={role} name="role" value={role} checked/>
<label for={role}>{role.charAt(0).toUpperCase() + role.slice(1)} (regular user, same as if created via LDAP sync.)</label>
</div>
{:else}
<div>
<input type="radio" id={role} name="role" value={role}/>
<label for={role}>{role.charAt(0).toUpperCase() + role.slice(1)}</label>
</div>
{/if}
{/each}
</div>
<p style="display: flex; align-items: center;">
<Button type="submit" color="primary">Submit</Button>

View File

@@ -0,0 +1,97 @@
<script>
import { Card, CardTitle, CardBody } from 'sveltestrap'
import { createEventDispatcher } from 'svelte'
import { fade } from 'svelte/transition'
const dispatch = createEventDispatcher()
let message = {msg: '', color: '#d63384'}
let displayMessage = false
async function handleAddProject() {
const username = document.querySelector('#project-username').value
const project = document.querySelector('#project-id').value
if (username == "" || project == "") {
alert('Please fill in a username and select a project.')
return
}
let formData = new FormData()
formData.append('username', username)
formData.append('add-project', project)
try {
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
if (res.ok) {
let text = await res.text()
popMessage(text, '#048109')
reloadUserList()
} else {
let text = await res.text()
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
}
} catch (err) {
popMessage(err, '#d63384')
}
}
async function handleRemoveProject() {
const username = document.querySelector('#project-username').value
const project = document.querySelector('#project-id').value
if (username == "" || project == "") {
alert('Please fill in a username and select a project.')
return
}
let formData = new FormData()
formData.append('username', username)
formData.append('remove-project', project)
try {
const res = await fetch(`/api/user/${username}`, { method: 'POST', body: formData })
if (res.ok) {
let text = await res.text()
popMessage(text, '#048109')
reloadUserList()
} else {
let text = await res.text()
// console.log(res.statusText)
throw new Error('Response Code ' + res.status + '-> ' + text)
}
} catch (err) {
popMessage(err, '#d63384')
}
}
function popMessage(response, rescolor) {
message = {msg: response, color: rescolor}
displayMessage = true
setTimeout(function() {
displayMessage = false
}, 3500)
}
function reloadUserList() {
dispatch('reload')
}
</script>
<Card>
<CardBody>
<CardTitle class="mb-3">Edit Project Managed By User (Manager Only)</CardTitle>
<div class="input-group mb-3">
<input type="text" class="form-control" placeholder="username" id="project-username"/>
<input type="text" class="form-control" placeholder="project-id" id="project-id"/>
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->
<button class="btn btn-primary" type="button" id="add-project-button" on:click|preventDefault={handleAddProject}>Add</button>
<button class="btn btn-danger" type="button" id="remove-project-button" on:click|preventDefault={handleRemoveProject}>Remove</button>
</div>
<p>
{#if displayMessage}<b><code style="color: {message.color};" out:fade>Update: {message.msg}</code></b>{/if}
</p>
</CardBody>
</Card>

View File

@@ -8,6 +8,8 @@
let message = {msg: '', color: '#d63384'}
let displayMessage = false
export let roles = []
async function handleAddRole() {
const username = document.querySelector('#role-username').value
const role = document.querySelector('#role-select').value
@@ -86,10 +88,9 @@
<input type="text" class="form-control" placeholder="username" id="role-username"/>
<select class="form-select" id="role-select">
<option selected value="">Role...</option>
<option value="user">User</option>
<option value="support">Support</option>
<option value="admin">Admin</option>
<option value="api">API</option>
{#each roles as role}
<option value={role}>{role.charAt(0).toUpperCase() + role.slice(1)}</option>
{/each}
</select>
<!-- PreventDefault on Sveltestrap-Button more complex to achieve than just use good ol' html button -->
<!-- see: https://stackoverflow.com/questions/69630422/svelte-how-to-use-event-modifiers-in-my-own-components -->

View File

@@ -41,6 +41,7 @@
<tr>
<th>Username</th>
<th>Name</th>
<th>Project(s)</th>
<th>Email</th>
<th>Roles</th>
<th>JWT</th>

View File

@@ -16,6 +16,7 @@
<td>{user.username}</td>
<td>{user.name}</td>
<td>{user.projects}</td>
<td>{user.email}</td>
<td><code>{user.roles.join(', ')}</code></td>
<td>

View File

@@ -45,6 +45,7 @@
arrayJobId: filterPresets.arrayJobId || null,
user: filterPresets.user || '',
project: filterPresets.project || '',
jobName: filterPresets.jobName || '',
numNodes: filterPresets.numNodes || { from: null, to: null },
numHWThreads: filterPresets.numHWThreads || { from: null, to: null },
@@ -94,6 +95,8 @@
items.push({ user: { [filters.userMatch]: filters.user } })
if (filters.project)
items.push({ project: { [filters.projectMatch]: filters.project } })
if (filters.jobName)
items.push({ jobName: { contains: filters.jobName } })
for (let stat of filters.stats)
items.push({ [stat.field]: { from: stat.from, to: stat.to } })
@@ -115,7 +118,7 @@
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)
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}`)
@@ -123,12 +126,19 @@
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.user.length != 0)
if (filters.userMatch != 'in') {
opts.push(`user=${filters.user}`)
} else {
for (let singleUser of filters.user)
opts.push(`user=${singleUser}`)
}
if (filters.userMatch != 'contains')
opts.push(`userMatch=${filters.userMatch}`)
if (filters.project)
opts.push(`project=${filters.project}`)
if (filters.jobName)
opts.push(`jobName=${filters.jobName}`)
if (filters.projectMatch != 'contains')
opts.push(`projectMatch=${filters.projectMatch}`)
@@ -214,7 +224,7 @@
on:change={({ detail: { from, to } }) => {
filters.startTime.from = from?.toISOString()
filters.startTime.to = to?.toISOString()
console.log(filters.startTime)
// console.log(filters.startTime)
update()
}}
/>

View File

@@ -1,7 +1,7 @@
<script>
import { createEventDispatcher, getContext } from 'svelte'
import { Button, Modal, ModalBody, ModalHeader, ModalFooter } from 'sveltestrap'
import Header from '../Header.svelte';
import Header from '../Header.svelte';
import DoubleRangeSlider from './DoubleRangeSlider.svelte'
const clusters = getContext('clusters'),
@@ -23,7 +23,7 @@ import Header from '../Header.svelte';
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)
console.log(header)
// console.log(header)
let minNumNodes = 1, maxNumNodes = 0, minNumHWThreads = 1, maxNumHWThreads = 0, minNumAccelerators = 0, maxNumAccelerators = 0
$: {
if ($initialized) {

View File

@@ -6,6 +6,8 @@
export let user = ''
export let project = ''
export let authlevel
export let roles
let mode = 'user', term = ''
const throttle = 500
@@ -22,30 +24,53 @@
let timeoutId = null
function termChanged(sleep = throttle) {
if (mode == 'user')
user = term
else
if (authlevel == roles.user) {
project = term
if (timeoutId != null)
clearTimeout(timeoutId)
if (timeoutId != null)
clearTimeout(timeoutId)
timeoutId = setTimeout(() => {
dispatch('update', {
user,
project
})
}, sleep)
timeoutId = setTimeout(() => {
dispatch('update', {
project
})
}, sleep)
} else if (authlevel >= roles.manager) {
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>
{#if authlevel == roles.user}
<InputGroup>
<Input
type="text" bind:value={term} on:change={() => termChanged()} on:keyup={(event) => termChanged(event.key == 'Enter' ? 0 : throttle)} placeholder='filter project...'
/>
</InputGroup>
{:else if authlevel >= roles.manager}
<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>
{:else}
Unauthorized
{/if}

View File

@@ -1,4 +1,4 @@
<!--
<!--
@component
Properties:
@@ -31,7 +31,11 @@
<span class="fw-bold"><a href="/monitoring/job/{job.id}" target="_blank">{job.jobId}</a> ({job.cluster})</span>
{#if job.metaData?.jobName}
<br/>
{job.metaData.jobName}
{#if job.metaData?.jobName.length <= 25}
<div>{job.metaData.jobName}</div>
{:else}
<div class="truncate" style="cursor:help; width:230px;" title={job.metaData.jobName}>{job.metaData.jobName}</div>
{/if}
{/if}
{#if job.arrayJobId}
Array Job: <a href="/monitoring/jobs/?arrayJobId={job.arrayJobId}&cluster={job.cluster}" target="_blank">#{job.arrayJobId}</a>
@@ -48,12 +52,20 @@
{/if}
{#if job.project && job.project != 'no project'}
<br/>
<Icon name="people-fill"/> {job.project}
<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>
{job.numNodes} <Icon name="pc-horizontal"/>
{#if job.numNodes == 1}
{job.resources[0].hostname}
{:else}
{job.numNodes}
{/if}
<Icon name="pc-horizontal"/>
{#if job.exclusive != 1}
(shared)
{/if}
@@ -63,6 +75,8 @@
{#if job.numHWThreads > 0}
, {job.numHWThreads} <Icon name="cpu"/>
{/if}
<br/>
{job.subCluster}
</p>
<p>
@@ -86,3 +100,11 @@
{/each}
</p>
</div>
<style>
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@@ -27,13 +27,14 @@
let itemsPerPage = ccconfig.plot_list_jobsPerPage
let page = 1
let paging = { itemsPerPage, page }
let filter = []
const jobs = operationStore(`
query($filter: [JobFilter!]!, $sorting: OrderByInput!, $paging: PageRequest! ){
jobs(filter: $filter, order: $sorting, page: $paging) {
items {
id, jobId, user, project, cluster, subCluster, startTime,
duration, numNodes, numHWThreads, numAcc, walltime,
id, jobId, user, project, jobName, cluster, subCluster, startTime,
duration, numNodes, numHWThreads, numAcc, walltime, resources { hostname },
SMT, exclusive, partition, arrayJobId,
monitoringStatus, state,
tags { id, type, name }
@@ -45,7 +46,7 @@
}`, {
paging,
sorting,
filter: []
filter,
}, {
pause: true
})
@@ -68,7 +69,7 @@
}
$jobs.variables.filter = filters
console.log('filters:', ...filters.map(f => Object.entries(f)).flat(2))
// console.log('filters:', ...filters.map(f => Object.entries(f)).flat(2))
}
page = 1

View File

@@ -4,7 +4,9 @@ import Jobs from './Jobs.root.svelte'
new Jobs({
target: document.getElementById('svelte-app'),
props: {
filterPresets: filterPresets
filterPresets: filterPresets,
authlevel: authlevel,
roles: roles
},
context: new Map([
['cc-config', clusterCockpitConfig]

View File

@@ -1,4 +1,4 @@
<!--
<!--
@component
Only width/height should change reactively.
@@ -285,7 +285,7 @@
else if (scope == 'hwthread')
divisor = subCluster.topology.node.length
else {
console.log('TODO: how to calc thresholds for ', scope)
// console.log('TODO: how to calc thresholds for ', scope)
return null
}

View File

@@ -243,19 +243,24 @@
/* c will contain values from 0 to 1 representing the time */
const x = [], y = [], c = []
for (let i = 0; i < nodes; i++) {
const flopsData = flopsAny.series[i].data
const memBwData = memBw.series[i].data
for (let j = 0; j < timesteps; j++) {
const f = flopsData[j], m = memBwData[j]
const intensity = f / m
if (Number.isNaN(intensity) || !Number.isFinite(intensity))
continue
x.push(intensity)
y.push(f)
c.push(colorDots ? j / timesteps : 0)
if (flopsAny && memBw) {
for (let i = 0; i < nodes; i++) {
const flopsData = flopsAny.series[i].data
const memBwData = memBw.series[i].data
for (let j = 0; j < timesteps; j++) {
const f = flopsData[j], m = memBwData[j]
const intensity = f / m
if (Number.isNaN(intensity) || !Number.isFinite(intensity))
continue
x.push(intensity)
y.push(f)
c.push(colorDots ? j / timesteps : 0)
}
}
} else {
console.warn("transformData: metrics for 'mem_bw' and/or 'flops_any' missing!")
}
return {
@@ -270,10 +275,12 @@
export function transformPerNodeData(nodes) {
const x = [], y = [], c = []
for (let node of nodes) {
let flopsAny = node.metrics.find(m => m.name == 'flops_any' && m.scope == 'node')?.metric
let memBw = node.metrics.find(m => m.name == 'mem_bw' && m.scope == 'node')?.metric
if (!flopsAny || !memBw)
let flopsAny = node.metrics.find(m => m.name == 'flops_any' && m.metric.scope == 'node')?.metric
let memBw = node.metrics.find(m => m.name == 'mem_bw' && m.metric.scope == 'node')?.metric
if (!flopsAny || !memBw) {
console.warn("transformPerNodeData: metrics for 'mem_bw' and/or 'flops_any' missing!")
continue
}
let flopsData = flopsAny.series[0].data, memBwData = memBw.series[0].data
const f = flopsData[flopsData.length - 1], m = memBwData[flopsData.length - 1]
@@ -312,7 +319,7 @@
let ctx, canvasElement, prevWidth = width, prevHeight = height
data = data != null ? data : (flopsAny && memBw
? transformData(flopsAny, memBw, colorDots)
? transformData(flopsAny.metric, memBw.metric, colorDots)
: {
tiles: tiles,
xLabel: 'Intensity [FLOPS/byte]',