mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-07-23 21:01:40 +02:00
Merge branch 'master' into import-data-sanitation
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# cc-svelte-datatable
|
||||
# cc-frontend
|
||||
|
||||
[](https://github.com/ClusterCockpit/cc-svelte-datatable/actions/workflows/build.yml)
|
||||
|
||||
@@ -21,11 +21,9 @@ Install the dependencies...
|
||||
yarn install
|
||||
```
|
||||
|
||||
...then start [Rollup](https://rollupjs.org):
|
||||
...then build using [Rollup](https://rollupjs.org):
|
||||
|
||||
```bash
|
||||
yarn run dev
|
||||
yarn build
|
||||
```
|
||||
|
||||
Edit a component file in `src`, save it, and reload the page to see your changes.
|
||||
|
||||
|
1704
web/frontend/public/bootstrap-icons.css
vendored
Normal file
1704
web/frontend/public/bootstrap-icons.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
web/frontend/public/bootstrap.min.css
vendored
Normal file
7
web/frontend/public/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/frontend/public/bootstrap.min.css.map
Normal file
1
web/frontend/public/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
BIN
web/frontend/public/fonts/bootstrap-icons.woff
Normal file
BIN
web/frontend/public/fonts/bootstrap-icons.woff
Normal file
Binary file not shown.
BIN
web/frontend/public/fonts/bootstrap-icons.woff2
Normal file
BIN
web/frontend/public/fonts/bootstrap-icons.woff2
Normal file
Binary file not shown.
@@ -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>
|
||||
|
@@ -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}
|
||||
|
@@ -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()} />
|
||||
|
@@ -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>
|
||||
|
@@ -62,7 +62,7 @@
|
||||
|
||||
query(nodesQuery)
|
||||
|
||||
$: console.log($nodesQuery?.data?.nodeMetrics[0].metrics)
|
||||
// $: console.log($nodesQuery?.data?.nodeMetrics[0].metrics)
|
||||
</script>
|
||||
|
||||
<Row>
|
||||
|
@@ -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>
|
||||
|
@@ -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]
|
||||
|
@@ -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¬-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/>
|
||||
|
@@ -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>
|
||||
|
97
web/frontend/src/config/admin/EditProject.svelte
Normal file
97
web/frontend/src/config/admin/EditProject.svelte
Normal 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>
|
@@ -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 -->
|
||||
|
@@ -41,6 +41,7 @@
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Name</th>
|
||||
<th>Project(s)</th>
|
||||
<th>Email</th>
|
||||
<th>Roles</th>
|
||||
<th>JWT</th>
|
||||
|
@@ -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>
|
||||
|
@@ -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()
|
||||
}}
|
||||
/>
|
||||
|
@@ -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) {
|
||||
|
@@ -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}
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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]
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -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]',
|
||||
|
@@ -230,7 +230,7 @@ commondir@^1.0.1:
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||
|
||||
deepmerge@^4.2.2:
|
||||
version "4.2.2"
|
||||
@@ -365,9 +365,9 @@ merge-stream@^2.0.0:
|
||||
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
|
||||
|
||||
minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
|
Reference in New Issue
Block a user