mirror of
				https://github.com/ClusterCockpit/cc-backend
				synced 2025-11-04 09:35:07 +01:00 
			
		
		
		
	ui changes
This commit is contained in:
		@@ -337,6 +337,7 @@ func main() {
 | 
			
		||||
		rw.Header().Add("Content-Type", "text/html; charset=utf-8")
 | 
			
		||||
		web.RenderTemplate(rw, "privacy.tmpl", &web.Page{Title: "Privacy", Build: buildInfo})
 | 
			
		||||
	})
 | 
			
		||||
	// r.NotFoundHandler
 | 
			
		||||
 | 
			
		||||
	secured := r.PathPrefix("/").Subrouter()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -35,15 +35,15 @@ type Route struct {
 | 
			
		||||
var routes []Route = []Route{
 | 
			
		||||
	{"/", "home.tmpl", "ClusterCockpit", false, setupHomeRoute},
 | 
			
		||||
	{"/config", "config.tmpl", "Settings", false, func(i InfoType, r *http.Request) InfoType { return i }},
 | 
			
		||||
	{"/monitoring/jobs/", "monitoring/jobs.tmpl", "Jobs - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { return i }},
 | 
			
		||||
	{"/monitoring/job/{id:[0-9]+}", "monitoring/job.tmpl", "Job <ID> - ClusterCockpit", false, setupJobRoute},
 | 
			
		||||
	{"/monitoring/users/", "monitoring/list.tmpl", "Users - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "USER"; return i }},
 | 
			
		||||
	{"/monitoring/projects/", "monitoring/list.tmpl", "Projects - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "PROJECT"; return i }},
 | 
			
		||||
	{"/monitoring/tags/", "monitoring/taglist.tmpl", "Tags - ClusterCockpit", false, setupTaglistRoute},
 | 
			
		||||
	{"/monitoring/user/{id}", "monitoring/user.tmpl", "User <ID> - ClusterCockpit", true, setupUserRoute},
 | 
			
		||||
	{"/monitoring/systems/{cluster}", "monitoring/systems.tmpl", "Cluster <ID> - ClusterCockpit", false, setupClusterRoute},
 | 
			
		||||
	// {"/monitoring/jobs/", "monitoring/jobs.tmpl", "Jobs - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { return i }},
 | 
			
		||||
	// {"/monitoring/job/{id:[0-9]+}", "monitoring/job.tmpl", "Job <ID> - ClusterCockpit", false, setupJobRoute},
 | 
			
		||||
	// {"/monitoring/users/", "monitoring/list.tmpl", "Users - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "USER"; return i }},
 | 
			
		||||
	// {"/monitoring/projects/", "monitoring/list.tmpl", "Projects - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "PROJECT"; return i }},
 | 
			
		||||
	// {"/monitoring/tags/", "monitoring/taglist.tmpl", "Tags - ClusterCockpit", false, setupTaglistRoute},
 | 
			
		||||
	// {"/monitoring/user/{id}", "monitoring/user.tmpl", "User <ID> - ClusterCockpit", true, setupUserRoute},
 | 
			
		||||
	// {"/monitoring/systems/{cluster}", "monitoring/systems.tmpl", "Cluster <ID> - ClusterCockpit", false, setupClusterRoute},
 | 
			
		||||
	{"/monitoring/node/{cluster}/{hostname}", "monitoring/node.tmpl", "Node <ID> - ClusterCockpit", false, setupNodeRoute},
 | 
			
		||||
	{"/monitoring/analysis/{cluster}", "monitoring/analysis.tmpl", "Analysis - ClusterCockpit", true, setupAnalysisRoute},
 | 
			
		||||
	// {"/monitoring/analysis/{cluster}", "monitoring/analysis.tmpl", "Analysis - ClusterCockpit", true, setupAnalysisRoute},
 | 
			
		||||
	{"/monitoring/control/{cluster}", "monitoring/control.tmpl", "Status of <ID> - ClusterCockpit", false, setupClusterRoute},
 | 
			
		||||
	{"/monitoring/partition/{cluster}", "partitions/systems.tmpl", "Cluster <ID> - ClusterCockpit", false, setupClusterRoute},
 | 
			
		||||
	{"/monitoring/history/", "monitoring/history.tmpl", "Cluster <ID> - ClusterCockpit", false, setupClusterRoute},
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,8 @@ import terser from '@rollup/plugin-terser';
 | 
			
		||||
import css from 'rollup-plugin-css-only';
 | 
			
		||||
import livereload from 'rollup-plugin-livereload';
 | 
			
		||||
 | 
			
		||||
const production = !process.env.ROLLUP_WATCH;
 | 
			
		||||
// const production = false
 | 
			
		||||
// const production = !process.env.ROLLUP_WATCH;
 | 
			
		||||
const production = false
 | 
			
		||||
 | 
			
		||||
const plugins = [
 | 
			
		||||
    svelte({
 | 
			
		||||
@@ -61,13 +61,14 @@ const entrypoint = (name, path) => ({
 | 
			
		||||
 | 
			
		||||
export default [
 | 
			
		||||
    entrypoint('header', 'src/header.entrypoint.js'),
 | 
			
		||||
    entrypoint('jobs', 'src/jobs.entrypoint.js'),
 | 
			
		||||
    entrypoint('user', 'src/user.entrypoint.js'),
 | 
			
		||||
    entrypoint('list', 'src/list.entrypoint.js'),
 | 
			
		||||
    entrypoint('job', 'src/job.entrypoint.js'),
 | 
			
		||||
    entrypoint('systems', 'src/systems.entrypoint.js'),
 | 
			
		||||
    entrypoint('home', 'src/home.entrypoint.js'),
 | 
			
		||||
    // entrypoint('jobs', 'src/jobs.entrypoint.js'),
 | 
			
		||||
    // entrypoint('user', 'src/user.entrypoint.js'),
 | 
			
		||||
    // entrypoint('list', 'src/list.entrypoint.js'),
 | 
			
		||||
    // entrypoint('job', 'src/job.entrypoint.js'),
 | 
			
		||||
    // entrypoint('systems', 'src/systems.entrypoint.js'),
 | 
			
		||||
    entrypoint('node', 'src/node.entrypoint.js'),
 | 
			
		||||
    entrypoint('analysis', 'src/analysis.entrypoint.js'),
 | 
			
		||||
    // entrypoint('analysis', 'src/analysis.entrypoint.js'),
 | 
			
		||||
    entrypoint('control', 'src/control.entrypoint.js'),
 | 
			
		||||
    entrypoint('config', 'src/config.entrypoint.js'),
 | 
			
		||||
    entrypoint('partitions', 'src/partitions.entrypoint.js'),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,440 +0,0 @@
 | 
			
		||||
<script>
 | 
			
		||||
    import { init, convert2uplot } from './utils.js'
 | 
			
		||||
    import { getContext, onMount } from 'svelte'
 | 
			
		||||
    import { queryStore, gql, getContextClient, mutationStore } from '@urql/svelte'
 | 
			
		||||
    import { Row, Col, Spinner, Card, Table, Icon } from 'sveltestrap'
 | 
			
		||||
    import Filters from './filters/Filters.svelte'
 | 
			
		||||
    import PlotSelection from './PlotSelection.svelte'
 | 
			
		||||
    import Histogram from './plots/Histogram.svelte'
 | 
			
		||||
    import Pie, { colors } from './plots/Pie.svelte'
 | 
			
		||||
    import { binsFromFootprint } from './utils.js'
 | 
			
		||||
    import ScatterPlot from './plots/Scatter.svelte'
 | 
			
		||||
    import PlotTable from './PlotTable.svelte'
 | 
			
		||||
    import RooflineHeatmap from './plots/RooflineHeatmap.svelte'
 | 
			
		||||
 | 
			
		||||
    const { query: initq } = init()
 | 
			
		||||
 | 
			
		||||
    export let filterPresets
 | 
			
		||||
 | 
			
		||||
    // By default, look at the jobs of the last 6 hours:
 | 
			
		||||
    if (filterPresets?.startTime == null) {
 | 
			
		||||
        if (filterPresets == null)
 | 
			
		||||
            filterPresets = {}
 | 
			
		||||
 | 
			
		||||
        let now = new Date(Date.now())
 | 
			
		||||
        let hourAgo = new Date(now)
 | 
			
		||||
        hourAgo.setHours(hourAgo.getHours() - 6)
 | 
			
		||||
        filterPresets.startTime = { from: hourAgo.toISOString(), to: now.toISOString() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let cluster;
 | 
			
		||||
    let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
 | 
			
		||||
    let jobFilters = [];
 | 
			
		||||
    let rooflineMaxY;
 | 
			
		||||
    let colWidth1, colWidth2, colWidth3, colWidth4;
 | 
			
		||||
    let numBins = 50;
 | 
			
		||||
    let maxY = -1;
 | 
			
		||||
    const ccconfig = getContext('cc-config')
 | 
			
		||||
    const metricConfig = getContext('metrics')
 | 
			
		||||
 | 
			
		||||
    let metricsInHistograms = ccconfig.analysis_view_histogramMetrics,
 | 
			
		||||
        metricsInScatterplots = ccconfig.analysis_view_scatterPlotMetrics
 | 
			
		||||
 | 
			
		||||
    $: metrics = [...new Set([...metricsInHistograms, ...metricsInScatterplots.flat()])]
 | 
			
		||||
 | 
			
		||||
    const sortOptions = [
 | 
			
		||||
        {key: 'totalWalltime',  label: 'Walltime'},
 | 
			
		||||
        {key: 'totalNodeHours', label: 'Node Hours'},
 | 
			
		||||
        {key: 'totalCoreHours', label: 'Core Hours'},
 | 
			
		||||
        {key: 'totalAccHours',  label: 'Accelerator Hours'}
 | 
			
		||||
    ]
 | 
			
		||||
    const groupOptions = [
 | 
			
		||||
        {key: 'user',  label: 'User Name'},
 | 
			
		||||
        {key: 'project', label: 'Project ID'}
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    let sortSelection = sortOptions.find((option) => option.key == ccconfig[`analysis_view_selectedTopCategory:${filterPresets.cluster}`]) || sortOptions.find((option) => option.key == ccconfig.analysis_view_selectedTopCategory)
 | 
			
		||||
    let groupSelection = groupOptions.find((option) => option.key == ccconfig[`analysis_view_selectedTopEntity:${filterPresets.cluster}`]) || groupOptions.find((option) => option.key == ccconfig.analysis_view_selectedTopEntity)
 | 
			
		||||
 | 
			
		||||
    getContext('on-init')(({ data }) => {
 | 
			
		||||
        if (data != null) {
 | 
			
		||||
            cluster = data.clusters.find(c => c.name == filterPresets.cluster)
 | 
			
		||||
            console.assert(cluster != null, `This cluster could not be found: ${filterPresets.cluster}`)
 | 
			
		||||
 | 
			
		||||
            rooflineMaxY = cluster.subClusters.reduce((max, part) => Math.max(max, part.flopRateSimd.value), 0)
 | 
			
		||||
            maxY = rooflineMaxY
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const client = getContextClient();
 | 
			
		||||
 | 
			
		||||
    $: statsQuery = queryStore({
 | 
			
		||||
        client: client,
 | 
			
		||||
        query: gql`
 | 
			
		||||
            query($jobFilters: [JobFilter!]!) {
 | 
			
		||||
                stats: jobsStatistics(filter: $jobFilters) {
 | 
			
		||||
                    totalJobs
 | 
			
		||||
                    shortJobs
 | 
			
		||||
                    totalWalltime
 | 
			
		||||
                    totalNodeHours
 | 
			
		||||
                    totalCoreHours
 | 
			
		||||
                    totalAccHours
 | 
			
		||||
                    histDuration { count, value }
 | 
			
		||||
                    histNumCores { count, value }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        `, 
 | 
			
		||||
        variables: { jobFilters }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    $: topQuery = queryStore({
 | 
			
		||||
        client: client,
 | 
			
		||||
        query: gql`
 | 
			
		||||
            query($jobFilters: [JobFilter!]!, $paging: PageRequest!, $sortBy: SortByAggregate!, $groupBy: Aggregate!) {
 | 
			
		||||
                topList: jobsStatistics(filter: $jobFilters, page: $paging, sortBy: $sortBy, groupBy: $groupBy) {
 | 
			
		||||
                    id
 | 
			
		||||
                    totalWalltime
 | 
			
		||||
                    totalNodeHours
 | 
			
		||||
                    totalCoreHours
 | 
			
		||||
                    totalAccHours
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        `, 
 | 
			
		||||
        variables: { jobFilters, paging: { itemsPerPage: 10, page: 1 }, sortBy: sortSelection.key.toUpperCase(), groupBy: groupSelection.key.toUpperCase() }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    $: footprintsQuery = queryStore({
 | 
			
		||||
        client: client,
 | 
			
		||||
        query: gql`
 | 
			
		||||
            query($jobFilters: [JobFilter!]!, $metrics: [String!]!) {
 | 
			
		||||
                footprints: jobsFootprints(filter: $jobFilters, metrics: $metrics) {
 | 
			
		||||
                    timeWeights { nodeHours, accHours, coreHours },
 | 
			
		||||
                    metrics { metric, data }
 | 
			
		||||
                }
 | 
			
		||||
            }`,
 | 
			
		||||
        variables:  { jobFilters, metrics }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    $: rooflineQuery = queryStore({
 | 
			
		||||
        client: client,
 | 
			
		||||
        query: gql`
 | 
			
		||||
            query($jobFilters: [JobFilter!]!, $rows: Int!, $cols: Int!,
 | 
			
		||||
                    $minX: Float!, $minY: Float!, $maxX: Float!, $maxY: Float!) {
 | 
			
		||||
                rooflineHeatmap(filter: $jobFilters, rows: $rows, cols: $cols,
 | 
			
		||||
                        minX: $minX, minY: $minY, maxX: $maxX, maxY: $maxY)
 | 
			
		||||
            }
 | 
			
		||||
        `,
 | 
			
		||||
        variables: { jobFilters, rows: 50, cols: 50, minX: 0.01, minY: 1., maxX: 1000., maxY }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const updateConfigurationMutation = ({ name, value }) => {
 | 
			
		||||
        return mutationStore({
 | 
			
		||||
            client: client,
 | 
			
		||||
            query: gql`
 | 
			
		||||
                mutation ($name: String!, $value: String!) {
 | 
			
		||||
                    updateConfiguration(name: $name, value: $value)
 | 
			
		||||
                }
 | 
			
		||||
            `,
 | 
			
		||||
            variables: { name, value }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function updateEntityConfiguration(select) {
 | 
			
		||||
        if (ccconfig[`analysis_view_selectedTopEntity:${filterPresets.cluster}`] != select) {
 | 
			
		||||
            updateConfigurationMutation({ name: `analysis_view_selectedTopEntity:${filterPresets.cluster}`, value: JSON.stringify(select) })
 | 
			
		||||
            .subscribe(res => {
 | 
			
		||||
                if (res.fetching === false && !res.error) {
 | 
			
		||||
                    // console.log(`analysis_view_selectedTopEntity:${filterPresets.cluster}` + ' -> Updated!')
 | 
			
		||||
                } else if (res.fetching === false && res.error) {
 | 
			
		||||
                    throw res.error
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            // console.log('No Mutation Required: Entity')
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    function updateCategoryConfiguration(select) {
 | 
			
		||||
        if (ccconfig[`analysis_view_selectedTopCategory:${filterPresets.cluster}`] != select) {
 | 
			
		||||
            updateConfigurationMutation({ name: `analysis_view_selectedTopCategory:${filterPresets.cluster}`, value: JSON.stringify(select) })
 | 
			
		||||
            .subscribe(res => {
 | 
			
		||||
                if (res.fetching === false && !res.error) {
 | 
			
		||||
                    // console.log(`analysis_view_selectedTopCategory:${filterPresets.cluster}` + ' -> Updated!')
 | 
			
		||||
                } else if (res.fetching === false && res.error) {
 | 
			
		||||
                    throw res.error
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            // console.log('No Mutation Required: Category')
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    $: updateEntityConfiguration(groupSelection.key)
 | 
			
		||||
    $: updateCategoryConfiguration(sortSelection.key)
 | 
			
		||||
 | 
			
		||||
    onMount(() => filterComponent.update())
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Row>
 | 
			
		||||
    {#if $initq.fetching || $statsQuery.fetching || $footprintsQuery.fetching}
 | 
			
		||||
        <Col xs="auto">
 | 
			
		||||
            <Spinner />
 | 
			
		||||
        </Col>
 | 
			
		||||
    {/if}
 | 
			
		||||
    <Col xs="auto">
 | 
			
		||||
        {#if $initq.error}
 | 
			
		||||
            <Card body color="danger">{$initq.error.message}</Card>
 | 
			
		||||
        {:else if cluster}
 | 
			
		||||
            <PlotSelection
 | 
			
		||||
                availableMetrics={cluster.metricConfig.map(mc => mc.name)}
 | 
			
		||||
                bind:metricsInHistograms={metricsInHistograms}
 | 
			
		||||
                bind:metricsInScatterplots={metricsInScatterplots} />
 | 
			
		||||
        {/if}
 | 
			
		||||
    </Col>
 | 
			
		||||
    <Col xs="auto">
 | 
			
		||||
        <Filters
 | 
			
		||||
            bind:this={filterComponent}
 | 
			
		||||
            filterPresets={filterPresets}
 | 
			
		||||
            disableClusterSelection={true}
 | 
			
		||||
            startTimeQuickSelect={true}
 | 
			
		||||
            on:update={({ detail }) => {
 | 
			
		||||
                jobFilters = detail.filters;
 | 
			
		||||
            }} />
 | 
			
		||||
    </Col>
 | 
			
		||||
</Row>
 | 
			
		||||
 | 
			
		||||
<br/>
 | 
			
		||||
{#if $statsQuery.error}
 | 
			
		||||
    <Row>
 | 
			
		||||
        <Col>
 | 
			
		||||
            <Card body color="danger">{$statsQuery.error.message}</Card>
 | 
			
		||||
        </Col>
 | 
			
		||||
    </Row>
 | 
			
		||||
{:else if $statsQuery.data}
 | 
			
		||||
    <Row cols={3} class="mb-4">
 | 
			
		||||
        <Col>
 | 
			
		||||
            <Table>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <th scope="col">Total Jobs</th>
 | 
			
		||||
                    <td>{$statsQuery.data.stats[0].totalJobs}</td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <th scope="col">Short Jobs</th>
 | 
			
		||||
                    <td>{$statsQuery.data.stats[0].shortJobs}</td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <th scope="col">Total Walltime</th>
 | 
			
		||||
                    <td>{$statsQuery.data.stats[0].totalWalltime}</td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <th scope="col">Total Node Hours</th>
 | 
			
		||||
                    <td>{$statsQuery.data.stats[0].totalNodeHours}</td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <th scope="col">Total Core Hours</th>
 | 
			
		||||
                    <td>{$statsQuery.data.stats[0].totalCoreHours}</td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <th scope="col">Total Accelerator Hours</th>
 | 
			
		||||
                    <td>{$statsQuery.data.stats[0].totalAccHours}</td>
 | 
			
		||||
                </tr>
 | 
			
		||||
            </Table>
 | 
			
		||||
        </Col>
 | 
			
		||||
        <Col>
 | 
			
		||||
            <div bind:clientWidth={colWidth1}>
 | 
			
		||||
            <h5>Top 
 | 
			
		||||
                <select class="p-0" bind:value={groupSelection}>
 | 
			
		||||
                    {#each groupOptions as option}
 | 
			
		||||
                        <option value={option}>
 | 
			
		||||
                            {option.key.charAt(0).toUpperCase() + option.key.slice(1)}s
 | 
			
		||||
                        </option>
 | 
			
		||||
                    {/each}
 | 
			
		||||
                </select>
 | 
			
		||||
            </h5>
 | 
			
		||||
            {#key $topQuery.data}
 | 
			
		||||
                {#if $topQuery.fetching}
 | 
			
		||||
                    <Spinner/>
 | 
			
		||||
                {:else if $topQuery.error}
 | 
			
		||||
                    <Card body color="danger">{$topQuery.error.message}</Card>
 | 
			
		||||
                {:else}                    
 | 
			
		||||
                    <Pie
 | 
			
		||||
                        size={colWidth1}
 | 
			
		||||
                        sliceLabel={sortSelection.label}
 | 
			
		||||
                        quantities={$topQuery.data.topList.map((t) => t[sortSelection.key])}
 | 
			
		||||
                        entities={$topQuery.data.topList.map((t) => t.id)}
 | 
			
		||||
                    />
 | 
			
		||||
                {/if}
 | 
			
		||||
            {/key}
 | 
			
		||||
            </div>
 | 
			
		||||
        </Col>
 | 
			
		||||
        <Col>
 | 
			
		||||
            {#key $topQuery.data}
 | 
			
		||||
                {#if $topQuery.fetching}
 | 
			
		||||
                    <Spinner/>
 | 
			
		||||
                {:else if $topQuery.error}
 | 
			
		||||
                    <Card body color="danger">{$topQuery.error.message}</Card>
 | 
			
		||||
                {:else}
 | 
			
		||||
                    <Table>
 | 
			
		||||
                        <tr class="mb-2">
 | 
			
		||||
                            <th>Legend</th>
 | 
			
		||||
                            <th>{groupSelection.label}</th>
 | 
			
		||||
                            <th>
 | 
			
		||||
                                <select class="p-0" bind:value={sortSelection}>
 | 
			
		||||
                                    {#each sortOptions as option}
 | 
			
		||||
                                        <option value={option}>
 | 
			
		||||
                                            {option.label}
 | 
			
		||||
                                        </option>
 | 
			
		||||
                                    {/each}
 | 
			
		||||
                                </select>
 | 
			
		||||
                            </th>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        {#each $topQuery.data.topList as te, i}
 | 
			
		||||
                            <tr>
 | 
			
		||||
                                <td><Icon name="circle-fill" style="color: {colors[i]};"/></td>
 | 
			
		||||
                                {#if groupSelection.key == 'user'} 
 | 
			
		||||
                                    <th scope="col"><a href="/monitoring/user/{te.id}?cluster={cluster.name}">{te.id}</a></th>
 | 
			
		||||
                                {:else}
 | 
			
		||||
                                    <th scope="col"><a href="/monitoring/jobs/?cluster={cluster.name}&project={te.id}&projectMatch=eq">{te.id}</a></th>
 | 
			
		||||
                                {/if}
 | 
			
		||||
                                <td>{te[sortSelection.key]}</td>
 | 
			
		||||
                            </tr>
 | 
			
		||||
                        {/each}
 | 
			
		||||
                    </Table>
 | 
			
		||||
                {/if}
 | 
			
		||||
            {/key}
 | 
			
		||||
        </Col>
 | 
			
		||||
    </Row>
 | 
			
		||||
    <Row cols={3} class="mb-2">
 | 
			
		||||
        <Col>
 | 
			
		||||
            {#if $rooflineQuery.fetching}
 | 
			
		||||
                <Spinner />
 | 
			
		||||
            {:else if $rooflineQuery.error}
 | 
			
		||||
                <Card body color="danger">{$rooflineQuery.error.message}</Card>
 | 
			
		||||
            {:else if $rooflineQuery.data && cluster}
 | 
			
		||||
                <div bind:clientWidth={colWidth2}>
 | 
			
		||||
                {#key $rooflineQuery.data}
 | 
			
		||||
                    <RooflineHeatmap
 | 
			
		||||
                        width={colWidth2} height={300}
 | 
			
		||||
                        tiles={$rooflineQuery.data.rooflineHeatmap}
 | 
			
		||||
                        cluster={cluster.subClusters.length == 1 ? cluster.subClusters[0] : null}
 | 
			
		||||
                        maxY={rooflineMaxY} />
 | 
			
		||||
                {/key}
 | 
			
		||||
                </div>
 | 
			
		||||
            {/if}
 | 
			
		||||
        </Col>
 | 
			
		||||
        <Col>
 | 
			
		||||
            <div bind:clientWidth={colWidth3}>
 | 
			
		||||
            {#key $statsQuery.data.stats[0].histDuration}
 | 
			
		||||
                <Histogram
 | 
			
		||||
                    width={colWidth3} height={300}
 | 
			
		||||
                    data={convert2uplot($statsQuery.data.stats[0].histDuration)}
 | 
			
		||||
                    title="Duration Distribution"
 | 
			
		||||
                    xlabel="Current Runtimes"
 | 
			
		||||
                    xunit="Hours" 
 | 
			
		||||
                    ylabel="Number of Jobs"
 | 
			
		||||
                    yunit="Jobs"/>
 | 
			
		||||
            {/key}
 | 
			
		||||
            </div>
 | 
			
		||||
        </Col>
 | 
			
		||||
        <Col>
 | 
			
		||||
            <div bind:clientWidth={colWidth4}>
 | 
			
		||||
            {#key $statsQuery.data.stats[0].histNumCores}
 | 
			
		||||
                <Histogram
 | 
			
		||||
                    width={colWidth4} height={300}
 | 
			
		||||
                    data={convert2uplot($statsQuery.data.stats[0].histNumCores)}
 | 
			
		||||
                    title="Number of Cores Distribution"
 | 
			
		||||
                    xlabel="Allocated Cores"
 | 
			
		||||
                    xunit="Cores"
 | 
			
		||||
                    ylabel="Number of Jobs"
 | 
			
		||||
                    yunit="Jobs"/>
 | 
			
		||||
            {/key}
 | 
			
		||||
            </div>
 | 
			
		||||
        </Col>
 | 
			
		||||
    </Row>
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
<hr class="my-6"/>
 | 
			
		||||
 | 
			
		||||
{#if $footprintsQuery.error}
 | 
			
		||||
    <Row>
 | 
			
		||||
        <Col>
 | 
			
		||||
            <Card body color="danger">{$footprintsQuery.error.message}</Card>
 | 
			
		||||
        </Col>
 | 
			
		||||
    </Row>
 | 
			
		||||
{:else if $footprintsQuery.data && $initq.data}
 | 
			
		||||
    <Row>
 | 
			
		||||
        <Col>
 | 
			
		||||
            <Card body>
 | 
			
		||||
                These histograms show the distribution of the averages of all jobs matching the filters. Each job/average is weighted by its node hours by default 
 | 
			
		||||
                (Accelerator hours for native accelerator scope metrics, coreHours for native core scope metrics).
 | 
			
		||||
                Note that some metrics could be disabled for specific subclusters as per metricConfig and thus could affect shown average values.
 | 
			
		||||
            </Card>
 | 
			
		||||
            <br/>
 | 
			
		||||
        </Col>
 | 
			
		||||
    </Row>
 | 
			
		||||
    <Row>
 | 
			
		||||
        <Col>
 | 
			
		||||
            <PlotTable
 | 
			
		||||
                let:item
 | 
			
		||||
                let:width
 | 
			
		||||
                renderFor="analysis"
 | 
			
		||||
                items={metricsInHistograms.map(metric => ({ metric, ...binsFromFootprint(
 | 
			
		||||
                    $footprintsQuery.data.footprints.timeWeights,
 | 
			
		||||
                    metricConfig(cluster.name, metric)?.scope,
 | 
			
		||||
                    $footprintsQuery.data.footprints.metrics.find(f => f.metric == metric).data, numBins) }))}
 | 
			
		||||
                itemsPerRow={ccconfig.plot_view_plotsPerRow}>
 | 
			
		||||
 | 
			
		||||
                <Histogram
 | 
			
		||||
                    data={convert2uplot(item.bins)}
 | 
			
		||||
                    width={width} height={250}
 | 
			
		||||
                    usesBins={true}
 | 
			
		||||
                    title="Average Distribution of '{item.metric}'"
 | 
			
		||||
                    xlabel={`${item.metric} bin maximum ${(metricConfig(cluster.name, item.metric)?.unit?.prefix ? '[' + metricConfig(cluster.name, item.metric)?.unit?.prefix : '') +
 | 
			
		||||
                                                          (metricConfig(cluster.name, item.metric)?.unit?.base   ? metricConfig(cluster.name, item.metric)?.unit?.base + ']'   : '')}`}
 | 
			
		||||
                    xunit={`${(metricConfig(cluster.name, item.metric)?.unit?.prefix ? metricConfig(cluster.name, item.metric)?.unit?.prefix : '') +
 | 
			
		||||
                              (metricConfig(cluster.name, item.metric)?.unit?.base   ? metricConfig(cluster.name, item.metric)?.unit?.base   : '')}`}
 | 
			
		||||
                    ylabel="Normalized Hours"
 | 
			
		||||
                    yunit="Hours"/>
 | 
			
		||||
            </PlotTable>
 | 
			
		||||
        </Col>
 | 
			
		||||
    </Row>
 | 
			
		||||
    <br/>
 | 
			
		||||
    <Row>
 | 
			
		||||
        <Col>
 | 
			
		||||
            <Card body>
 | 
			
		||||
                Each circle represents one job. The size of a circle is proportional to its node hours. Darker circles mean multiple jobs have the same averages for the respective metrics.
 | 
			
		||||
                Note that some metrics could be disabled for specific subclusters as per metricConfig and thus could affect shown average values.
 | 
			
		||||
            </Card>
 | 
			
		||||
            <br/>
 | 
			
		||||
        </Col>
 | 
			
		||||
    </Row>
 | 
			
		||||
    <Row>
 | 
			
		||||
        <Col>
 | 
			
		||||
            <PlotTable
 | 
			
		||||
                let:item
 | 
			
		||||
                let:width
 | 
			
		||||
                renderFor="analysis"
 | 
			
		||||
                items={metricsInScatterplots.map(([m1, m2]) => ({
 | 
			
		||||
                    m1, f1: $footprintsQuery.data.footprints.metrics.find(f => f.metric == m1).data,
 | 
			
		||||
                    m2, f2: $footprintsQuery.data.footprints.metrics.find(f => f.metric == m2).data }))}
 | 
			
		||||
                itemsPerRow={ccconfig.plot_view_plotsPerRow}>
 | 
			
		||||
 | 
			
		||||
                <ScatterPlot
 | 
			
		||||
                    width={width} height={250} color={"rgba(0, 102, 204, 0.33)"}
 | 
			
		||||
                    xLabel={`${item.m1} [${(metricConfig(cluster.name, item.m1)?.unit?.prefix ? metricConfig(cluster.name, item.m1)?.unit?.prefix : '') + 
 | 
			
		||||
                                           (metricConfig(cluster.name, item.m1)?.unit?.base   ? metricConfig(cluster.name, item.m1)?.unit?.base   : '')}]`}
 | 
			
		||||
                    yLabel={`${item.m2} [${(metricConfig(cluster.name, item.m2)?.unit?.prefix ? metricConfig(cluster.name, item.m2)?.unit?.prefix : '') + 
 | 
			
		||||
                                           (metricConfig(cluster.name, item.m2)?.unit?.base   ? metricConfig(cluster.name, item.m2)?.unit?.base   : '')}]`}
 | 
			
		||||
                    X={item.f1} Y={item.f2} S={$footprintsQuery.data.footprints.timeWeights.nodeHours} />
 | 
			
		||||
            </PlotTable>
 | 
			
		||||
        </Col>
 | 
			
		||||
    </Row>
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
    h5 {
 | 
			
		||||
        text-align: center;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -75,7 +75,7 @@
 | 
			
		||||
              <!-- <Button color="primary" on:click={nextPage}> -->
 | 
			
		||||
              <Button color="primary">Next</Button>
 | 
			
		||||
              <!-- dropdown for number of pages to  show -->
 | 
			
		||||
              <div>
 | 
			
		||||
              <!-- <div> -->
 | 
			
		||||
                <Dropdown {isOpen} toggle={() => (isOpen = !isOpen)}>
 | 
			
		||||
                    <DropdownToggle caret>
 | 
			
		||||
                      {selectedPages}
 | 
			
		||||
@@ -88,7 +88,7 @@
 | 
			
		||||
                      {/each}
 | 
			
		||||
                    </DropdownMenu>
 | 
			
		||||
                  </Dropdown>
 | 
			
		||||
              </div>
 | 
			
		||||
              <!-- </div> -->
 | 
			
		||||
            </Col>
 | 
			
		||||
          </Row>
 | 
			
		||||
        </CardBody>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								web/frontend/src/Home.root.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								web/frontend/src/Home.root.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
<script>
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<h1>Home Page</h1>
 | 
			
		||||
@@ -1,468 +0,0 @@
 | 
			
		||||
<script>
 | 
			
		||||
    import {
 | 
			
		||||
        init,
 | 
			
		||||
        groupByScope,
 | 
			
		||||
        fetchMetricsStore,
 | 
			
		||||
        checkMetricDisabled,
 | 
			
		||||
        transformDataForRoofline
 | 
			
		||||
    } from "./utils.js";
 | 
			
		||||
    import {
 | 
			
		||||
        Row,
 | 
			
		||||
        Col,
 | 
			
		||||
        Card,
 | 
			
		||||
        Spinner,
 | 
			
		||||
        TabContent,
 | 
			
		||||
        TabPane,
 | 
			
		||||
        CardBody,
 | 
			
		||||
        CardHeader,
 | 
			
		||||
        CardTitle,
 | 
			
		||||
        Button,
 | 
			
		||||
        Icon,
 | 
			
		||||
    } from "sveltestrap";
 | 
			
		||||
    import PlotTable from "./PlotTable.svelte";
 | 
			
		||||
    import Metric from "./Metric.svelte";
 | 
			
		||||
    import Polar from "./plots/Polar.svelte";
 | 
			
		||||
    import Roofline from "./plots/Roofline.svelte";
 | 
			
		||||
    import JobInfo from "./joblist/JobInfo.svelte";
 | 
			
		||||
    import TagManagement from "./TagManagement.svelte";
 | 
			
		||||
    import MetricSelection from "./MetricSelection.svelte";
 | 
			
		||||
    import StatsTable from "./StatsTable.svelte";
 | 
			
		||||
    import JobFootprint from "./JobFootprint.svelte";
 | 
			
		||||
    import { getContext } from "svelte";
 | 
			
		||||
 | 
			
		||||
    export let dbid;
 | 
			
		||||
    export let authlevel;
 | 
			
		||||
    export let roles;
 | 
			
		||||
 | 
			
		||||
    const accMetrics = ['acc_utilization', 'acc_mem_used', 'acc_power', 'nv_mem_util', 'nv_sm_clock', 'nv_temp'];
 | 
			
		||||
    let accNodeOnly
 | 
			
		||||
 | 
			
		||||
    const { query: initq } = init(`
 | 
			
		||||
        job(id: "${dbid}") {
 | 
			
		||||
            id, jobId, user, project, cluster, startTime,
 | 
			
		||||
            duration, numNodes, numHWThreads, numAcc,
 | 
			
		||||
            SMT, exclusive, partition, subCluster, arrayJobId,
 | 
			
		||||
            monitoringStatus, state, walltime,
 | 
			
		||||
            tags { id, type, name },
 | 
			
		||||
            resources { hostname, hwthreads, accelerators },
 | 
			
		||||
            metaData,
 | 
			
		||||
            userData { name, email },
 | 
			
		||||
            concurrentJobs { items { id, jobId }, count, listQuery },
 | 
			
		||||
            flopsAnyAvg, memBwAvg, loadAvg
 | 
			
		||||
        }
 | 
			
		||||
    `);
 | 
			
		||||
 | 
			
		||||
    const ccconfig = getContext("cc-config"),
 | 
			
		||||
          clusters = getContext("clusters"),
 | 
			
		||||
          metrics  = getContext("metrics")
 | 
			
		||||
 | 
			
		||||
    let isMetricsSelectionOpen = false,
 | 
			
		||||
        selectedMetrics = [],
 | 
			
		||||
        isFetched = new Set();
 | 
			
		||||
    const [jobMetrics, startFetching] = fetchMetricsStore();
 | 
			
		||||
    getContext("on-init")(() => {
 | 
			
		||||
        let job = $initq.data.job;
 | 
			
		||||
        if (!job) return;
 | 
			
		||||
 | 
			
		||||
        selectedMetrics =
 | 
			
		||||
            ccconfig[`job_view_selectedMetrics:${job.cluster}`] ||
 | 
			
		||||
            clusters
 | 
			
		||||
                .find((c) => c.name == job.cluster)
 | 
			
		||||
                .metricConfig.map((mc) => mc.name);
 | 
			
		||||
 | 
			
		||||
        let toFetch = new Set([
 | 
			
		||||
            "flops_any",
 | 
			
		||||
            "mem_bw",
 | 
			
		||||
            ...selectedMetrics,
 | 
			
		||||
            ...(ccconfig[`job_view_polarPlotMetrics:${job.cluster}`] ||
 | 
			
		||||
                ccconfig[`job_view_polarPlotMetrics`]),
 | 
			
		||||
            ...(ccconfig[`job_view_nodestats_selectedMetrics:${job.cluster}`] ||
 | 
			
		||||
                ccconfig[`job_view_nodestats_selectedMetrics`]),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Select default Scopes to load: Check before if accelerator metrics are not on accelerator scope by default
 | 
			
		||||
        accNodeOnly = [...toFetch].some(function(m) {
 | 
			
		||||
            if (accMetrics.includes(m)) {
 | 
			
		||||
                const mc = metrics(job.cluster, m)
 | 
			
		||||
                return mc.scope !== 'accelerator'
 | 
			
		||||
            } else {
 | 
			
		||||
                return false
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        if (job.numAcc === 0 || accNodeOnly === true) {
 | 
			
		||||
            // No Accels or Accels on Node Scope
 | 
			
		||||
            startFetching(
 | 
			
		||||
                job,
 | 
			
		||||
                [...toFetch],
 | 
			
		||||
                job.numNodes > 2
 | 
			
		||||
                    ? ["node"]
 | 
			
		||||
                    : ["node", "socket", "core"]
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
            // Accels and not on node scope
 | 
			
		||||
            startFetching(
 | 
			
		||||
                job,
 | 
			
		||||
                [...toFetch],
 | 
			
		||||
                job.numNodes > 2
 | 
			
		||||
                    ? ["node", "accelerator"]
 | 
			
		||||
                    : ["node", "accelerator", "socket", "core"]
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        isFetched = toFetch;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const lazyFetchMoreMetrics = () => {
 | 
			
		||||
        let notYetFetched = new Set();
 | 
			
		||||
        for (let m of selectedMetrics) {
 | 
			
		||||
            if (!isFetched.has(m)) {
 | 
			
		||||
                notYetFetched.add(m);
 | 
			
		||||
                isFetched.add(m);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (notYetFetched.size > 0)
 | 
			
		||||
            startFetching(
 | 
			
		||||
                $initq.data.job,
 | 
			
		||||
                [...notYetFetched],
 | 
			
		||||
                $initq.data.job.numNodes > 2 ? ["node"] : ["node", "core"]
 | 
			
		||||
            );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Fetch more data once required:
 | 
			
		||||
    $: if ($initq.data && $jobMetrics.data && selectedMetrics)
 | 
			
		||||
        lazyFetchMoreMetrics();
 | 
			
		||||
 | 
			
		||||
    let plots = {},
 | 
			
		||||
        jobTags,
 | 
			
		||||
        statsTable,
 | 
			
		||||
        jobFootprint;
 | 
			
		||||
 | 
			
		||||
    $: document.title = $initq.fetching
 | 
			
		||||
        ? "Loading..."
 | 
			
		||||
        : $initq.error
 | 
			
		||||
        ? "Error"
 | 
			
		||||
        : `Job ${$initq.data.job.jobId} - ClusterCockpit`;
 | 
			
		||||
 | 
			
		||||
    // Find out what metrics or hosts are missing:
 | 
			
		||||
    let missingMetrics = [],
 | 
			
		||||
        missingHosts = [],
 | 
			
		||||
        somethingMissing = false;
 | 
			
		||||
    $: if ($initq.data && $jobMetrics.data) {
 | 
			
		||||
        let job = $initq.data.job,
 | 
			
		||||
            metrics = $jobMetrics.data.jobMetrics,
 | 
			
		||||
            metricNames = clusters
 | 
			
		||||
                .find((c) => c.name == job.cluster)
 | 
			
		||||
                .metricConfig.map((mc) => mc.name);
 | 
			
		||||
 | 
			
		||||
        // Metric not found in JobMetrics && Metric not explicitly disabled: Was expected, but is Missing
 | 
			
		||||
        missingMetrics = metricNames.filter(
 | 
			
		||||
            (metric) =>
 | 
			
		||||
                !metrics.some((jm) => jm.name == metric) &&
 | 
			
		||||
                !checkMetricDisabled(
 | 
			
		||||
                    metric,
 | 
			
		||||
                    $initq.data.job.cluster,
 | 
			
		||||
                    $initq.data.job.subCluster
 | 
			
		||||
                )
 | 
			
		||||
        );
 | 
			
		||||
        missingHosts = job.resources
 | 
			
		||||
            .map(({ hostname }) => ({
 | 
			
		||||
                hostname: hostname,
 | 
			
		||||
                metrics: metricNames.filter(
 | 
			
		||||
                    (metric) =>
 | 
			
		||||
                        !metrics.some(
 | 
			
		||||
                            (jm) =>
 | 
			
		||||
                                jm.scope == "node" &&
 | 
			
		||||
                                jm.metric.series.some(
 | 
			
		||||
                                    (series) => series.hostname == hostname
 | 
			
		||||
                                )
 | 
			
		||||
                        )
 | 
			
		||||
                ),
 | 
			
		||||
            }))
 | 
			
		||||
            .filter(({ metrics }) => metrics.length > 0);
 | 
			
		||||
        somethingMissing = missingMetrics.length > 0 || missingHosts.length > 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const orderAndMap = (grouped, selectedMetrics) =>
 | 
			
		||||
        selectedMetrics.map((metric) => ({
 | 
			
		||||
            metric: metric,
 | 
			
		||||
            data: grouped.find((group) => group[0].name == metric),
 | 
			
		||||
            disabled: checkMetricDisabled(
 | 
			
		||||
                metric,
 | 
			
		||||
                $initq.data.job.cluster,
 | 
			
		||||
                $initq.data.job.subCluster
 | 
			
		||||
            ),
 | 
			
		||||
        }));
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Row>
 | 
			
		||||
    <Col>
 | 
			
		||||
        {#if $initq.error}
 | 
			
		||||
            <Card body color="danger">{$initq.error.message}</Card>
 | 
			
		||||
        {:else if $initq.data}
 | 
			
		||||
            <JobInfo job={$initq.data.job} {jobTags} />
 | 
			
		||||
        {:else}
 | 
			
		||||
            <Spinner secondary />
 | 
			
		||||
        {/if}
 | 
			
		||||
    </Col>
 | 
			
		||||
    {#if $jobMetrics.data}
 | 
			
		||||
        {#key $jobMetrics.data}
 | 
			
		||||
            <Col>
 | 
			
		||||
                <JobFootprint
 | 
			
		||||
                    bind:this={jobFootprint}
 | 
			
		||||
                    job={$initq.data.job}
 | 
			
		||||
                    jobMetrics={$jobMetrics.data.jobMetrics}
 | 
			
		||||
                />
 | 
			
		||||
            </Col>
 | 
			
		||||
        {/key}
 | 
			
		||||
    {/if}
 | 
			
		||||
    {#if $jobMetrics.data && $initq.data}
 | 
			
		||||
        {#if $initq.data.job.concurrentJobs != null && $initq.data.job.concurrentJobs.items.length != 0}
 | 
			
		||||
            {#if authlevel > roles.manager}
 | 
			
		||||
                <Col>
 | 
			
		||||
                    <h5>
 | 
			
		||||
                        Concurrent Jobs <Icon
 | 
			
		||||
                            name="info-circle"
 | 
			
		||||
                            style="cursor:help;"
 | 
			
		||||
                            title="Shared jobs running on the same node with overlapping runtimes"
 | 
			
		||||
                        />
 | 
			
		||||
                    </h5>
 | 
			
		||||
                    <ul>
 | 
			
		||||
                        <li>
 | 
			
		||||
                            <a
 | 
			
		||||
                                href="/monitoring/jobs/?{$initq.data.job
 | 
			
		||||
                                    .concurrentJobs.listQuery}"
 | 
			
		||||
                                target="_blank">See All</a
 | 
			
		||||
                            >
 | 
			
		||||
                        </li>
 | 
			
		||||
                        {#each $initq.data.job.concurrentJobs.items as pjob, index}
 | 
			
		||||
                            <li>
 | 
			
		||||
                                <a
 | 
			
		||||
                                    href="/monitoring/job/{pjob.id}"
 | 
			
		||||
                                    target="_blank">{pjob.jobId}</a
 | 
			
		||||
                                >
 | 
			
		||||
                            </li>
 | 
			
		||||
                        {/each}
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </Col>
 | 
			
		||||
            {:else}
 | 
			
		||||
                <Col>
 | 
			
		||||
                    <h5>
 | 
			
		||||
                        {$initq.data.job.concurrentJobs.items.length} Concurrent
 | 
			
		||||
                        Jobs
 | 
			
		||||
                    </h5>
 | 
			
		||||
                    <p>
 | 
			
		||||
                        Number of shared jobs on the same node with overlapping
 | 
			
		||||
                        runtimes.
 | 
			
		||||
                    </p>
 | 
			
		||||
                </Col>
 | 
			
		||||
            {/if}
 | 
			
		||||
        {/if}
 | 
			
		||||
        <Col>
 | 
			
		||||
            <Polar
 | 
			
		||||
                metrics={ccconfig[
 | 
			
		||||
                    `job_view_polarPlotMetrics:${$initq.data.job.cluster}`
 | 
			
		||||
                ] || ccconfig[`job_view_polarPlotMetrics`]}
 | 
			
		||||
                cluster={$initq.data.job.cluster}
 | 
			
		||||
                jobMetrics={$jobMetrics.data.jobMetrics}
 | 
			
		||||
            />
 | 
			
		||||
        </Col>
 | 
			
		||||
        <Col>
 | 
			
		||||
            <Roofline
 | 
			
		||||
                renderTime={true}
 | 
			
		||||
                cluster={clusters
 | 
			
		||||
                    .find((c) => c.name == $initq.data.job.cluster)
 | 
			
		||||
                    .subClusters.find(
 | 
			
		||||
                        (sc) => sc.name == $initq.data.job.subCluster
 | 
			
		||||
                    )}
 | 
			
		||||
                data={
 | 
			
		||||
                    transformDataForRoofline (
 | 
			
		||||
                        $jobMetrics.data.jobMetrics.find((m) => m.name == "flops_any" && m.scope == "node").metric,
 | 
			
		||||
                        $jobMetrics.data.jobMetrics.find((m) => m.name == "mem_bw" && m.scope == "node").metric
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
            />
 | 
			
		||||
        </Col>
 | 
			
		||||
    {:else}
 | 
			
		||||
        <Col />
 | 
			
		||||
        <Col />
 | 
			
		||||
    {/if}
 | 
			
		||||
</Row>
 | 
			
		||||
<Row class="mb-3">
 | 
			
		||||
    <Col xs="auto">
 | 
			
		||||
        {#if $initq.data}
 | 
			
		||||
            <TagManagement job={$initq.data.job} bind:jobTags />
 | 
			
		||||
        {/if}
 | 
			
		||||
    </Col>
 | 
			
		||||
    <Col xs="auto">
 | 
			
		||||
        {#if $initq.data}
 | 
			
		||||
            <Button outline on:click={() => (isMetricsSelectionOpen = true)}>
 | 
			
		||||
                <Icon name="graph-up" /> Metrics
 | 
			
		||||
            </Button>
 | 
			
		||||
        {/if}
 | 
			
		||||
    </Col>
 | 
			
		||||
<!--     <Col xs="auto">
 | 
			
		||||
        <Zoom timeseriesPlots={plots} />
 | 
			
		||||
    </Col> -->
 | 
			
		||||
</Row>
 | 
			
		||||
<Row>
 | 
			
		||||
    <Col>
 | 
			
		||||
        {#if $jobMetrics.error}
 | 
			
		||||
            {#if $initq.data.job.monitoringStatus == 0 || $initq.data.job.monitoringStatus == 2}
 | 
			
		||||
                <Card body color="warning"
 | 
			
		||||
                    >Not monitored or archiving failed</Card
 | 
			
		||||
                >
 | 
			
		||||
                <br />
 | 
			
		||||
            {/if}
 | 
			
		||||
            <Card body color="danger">{$jobMetrics.error.message}</Card>
 | 
			
		||||
        {:else if $jobMetrics.fetching}
 | 
			
		||||
            <Spinner secondary />
 | 
			
		||||
        {:else if $jobMetrics.data && $initq.data}
 | 
			
		||||
            <PlotTable
 | 
			
		||||
                let:item
 | 
			
		||||
                let:width
 | 
			
		||||
                renderFor="job"
 | 
			
		||||
                items={orderAndMap(
 | 
			
		||||
                    groupByScope($jobMetrics.data.jobMetrics),
 | 
			
		||||
                    selectedMetrics
 | 
			
		||||
                )}
 | 
			
		||||
                itemsPerRow={ccconfig.plot_view_plotsPerRow}
 | 
			
		||||
            >
 | 
			
		||||
                {#if item.data}
 | 
			
		||||
                    <Metric
 | 
			
		||||
                        bind:this={plots[item.metric]}
 | 
			
		||||
                        on:more-loaded={({ detail }) =>
 | 
			
		||||
                            statsTable.moreLoaded(detail)}
 | 
			
		||||
                        job={$initq.data.job}
 | 
			
		||||
                        metricName={item.metric}
 | 
			
		||||
                        rawData={item.data.map((x) => x.metric)}
 | 
			
		||||
                        scopes={item.data.map((x) => x.scope)}
 | 
			
		||||
                        {width}
 | 
			
		||||
                        isShared={$initq.data.job.exclusive != 1}
 | 
			
		||||
                        resources={$initq.data.job.resources}
 | 
			
		||||
                    />
 | 
			
		||||
                {:else}
 | 
			
		||||
                    <Card body color="warning"
 | 
			
		||||
                        >No dataset returned for <code>{item.metric}</code
 | 
			
		||||
                        ></Card
 | 
			
		||||
                    >
 | 
			
		||||
                {/if}
 | 
			
		||||
            </PlotTable>
 | 
			
		||||
        {/if}
 | 
			
		||||
    </Col>
 | 
			
		||||
</Row>
 | 
			
		||||
<Row class="mt-2">
 | 
			
		||||
    <Col>
 | 
			
		||||
        {#if $initq.data}
 | 
			
		||||
            <TabContent>
 | 
			
		||||
                {#if somethingMissing}
 | 
			
		||||
                    <TabPane
 | 
			
		||||
                        tabId="resources"
 | 
			
		||||
                        tab="Resources"
 | 
			
		||||
                        active={somethingMissing}
 | 
			
		||||
                    >
 | 
			
		||||
                        <div style="margin: 10px;">
 | 
			
		||||
                            <Card color="warning">
 | 
			
		||||
                                <CardHeader>
 | 
			
		||||
                                    <CardTitle
 | 
			
		||||
                                        >Missing Metrics/Reseources</CardTitle
 | 
			
		||||
                                    >
 | 
			
		||||
                                </CardHeader>
 | 
			
		||||
                                <CardBody>
 | 
			
		||||
                                    {#if missingMetrics.length > 0}
 | 
			
		||||
                                        <p>
 | 
			
		||||
                                            No data at all is available for the
 | 
			
		||||
                                            metrics: {missingMetrics.join(", ")}
 | 
			
		||||
                                        </p>
 | 
			
		||||
                                    {/if}
 | 
			
		||||
                                    {#if missingHosts.length > 0}
 | 
			
		||||
                                        <p>
 | 
			
		||||
                                            Some metrics are missing for the
 | 
			
		||||
                                            following hosts:
 | 
			
		||||
                                        </p>
 | 
			
		||||
                                        <ul>
 | 
			
		||||
                                            {#each missingHosts as missing}
 | 
			
		||||
                                                <li>
 | 
			
		||||
                                                    {missing.hostname}: {missing.metrics.join(
 | 
			
		||||
                                                        ", "
 | 
			
		||||
                                                    )}
 | 
			
		||||
                                                </li>
 | 
			
		||||
                                            {/each}
 | 
			
		||||
                                        </ul>
 | 
			
		||||
                                    {/if}
 | 
			
		||||
                                </CardBody>
 | 
			
		||||
                            </Card>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </TabPane>
 | 
			
		||||
                {/if}
 | 
			
		||||
                <TabPane
 | 
			
		||||
                    tabId="stats"
 | 
			
		||||
                    tab="Statistics Table"
 | 
			
		||||
                    active={!somethingMissing}
 | 
			
		||||
                >
 | 
			
		||||
                    {#if $jobMetrics.data}
 | 
			
		||||
                        {#key $jobMetrics.data}
 | 
			
		||||
                            <StatsTable
 | 
			
		||||
                                bind:this={statsTable}
 | 
			
		||||
                                job={$initq.data.job}
 | 
			
		||||
                                jobMetrics={$jobMetrics.data.jobMetrics}
 | 
			
		||||
                            />
 | 
			
		||||
                        {/key}
 | 
			
		||||
                    {/if}
 | 
			
		||||
                </TabPane>
 | 
			
		||||
                <TabPane tabId="job-script" tab="Job Script">
 | 
			
		||||
                    <div class="pre-wrapper">
 | 
			
		||||
                        {#if $initq.data.job.metaData?.jobScript}
 | 
			
		||||
                            <pre><code
 | 
			
		||||
                                    >{$initq.data.job.metaData?.jobScript}</code
 | 
			
		||||
                                ></pre>
 | 
			
		||||
                        {:else}
 | 
			
		||||
                            <Card body color="warning"
 | 
			
		||||
                                >No job script available</Card
 | 
			
		||||
                            >
 | 
			
		||||
                        {/if}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </TabPane>
 | 
			
		||||
                <TabPane tabId="slurm-info" tab="Slurm Info">
 | 
			
		||||
                    <div class="pre-wrapper">
 | 
			
		||||
                        {#if $initq.data.job.metaData?.slurmInfo}
 | 
			
		||||
                            <pre><code
 | 
			
		||||
                                    >{$initq.data.job.metaData?.slurmInfo}</code
 | 
			
		||||
                                ></pre>
 | 
			
		||||
                        {:else}
 | 
			
		||||
                            <Card body color="warning"
 | 
			
		||||
                                >No additional slurm information available</Card
 | 
			
		||||
                            >
 | 
			
		||||
                        {/if}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </TabPane>
 | 
			
		||||
            </TabContent>
 | 
			
		||||
        {/if}
 | 
			
		||||
    </Col>
 | 
			
		||||
</Row>
 | 
			
		||||
 | 
			
		||||
{#if $initq.data}
 | 
			
		||||
    <MetricSelection
 | 
			
		||||
        cluster={$initq.data.job.cluster}
 | 
			
		||||
        configName="job_view_selectedMetrics"
 | 
			
		||||
        bind:metrics={selectedMetrics}
 | 
			
		||||
        bind:isOpen={isMetricsSelectionOpen}
 | 
			
		||||
    />
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
    .pre-wrapper {
 | 
			
		||||
        font-size: 1.1rem;
 | 
			
		||||
        margin: 10px;
 | 
			
		||||
        border: 1px solid #bbb;
 | 
			
		||||
        border-radius: 5px;
 | 
			
		||||
        padding: 5px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ul {
 | 
			
		||||
        columns: 2;
 | 
			
		||||
        -webkit-columns: 2;
 | 
			
		||||
        -moz-columns: 2;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,232 +0,0 @@
 | 
			
		||||
<script>
 | 
			
		||||
    import { getContext } from 'svelte'
 | 
			
		||||
    import {
 | 
			
		||||
        Card,
 | 
			
		||||
        CardHeader,
 | 
			
		||||
        CardTitle,
 | 
			
		||||
        CardBody,
 | 
			
		||||
        Progress,
 | 
			
		||||
        Icon,
 | 
			
		||||
        Tooltip
 | 
			
		||||
    } from "sveltestrap";
 | 
			
		||||
    import { mean, round } from 'mathjs'
 | 
			
		||||
 | 
			
		||||
    export let job
 | 
			
		||||
    export let jobMetrics
 | 
			
		||||
    export let view = 'job'
 | 
			
		||||
    export let width = 'auto'
 | 
			
		||||
 | 
			
		||||
    const clusters         = getContext('clusters')
 | 
			
		||||
    const subclusterConfig = clusters.find((c) => c.name == job.cluster).subClusters.find((sc) => sc.name == job.subCluster)
 | 
			
		||||
 | 
			
		||||
    const footprintMetrics = (job.numAcc !== 0)
 | 
			
		||||
        ? (job.exclusive !== 1) 
 | 
			
		||||
            ? ['cpu_load', 'flops_any', 'acc_utilization']
 | 
			
		||||
            : ['cpu_load', 'flops_any', 'acc_utilization', 'mem_bw']
 | 
			
		||||
        : (job.exclusive !== 1) 
 | 
			
		||||
            ? ['cpu_load', 'flops_any', 'mem_used']
 | 
			
		||||
            : ['cpu_load', 'flops_any', 'mem_used', 'mem_bw']
 | 
			
		||||
 | 
			
		||||
    const footprintData = footprintMetrics.map((fm) => {
 | 
			
		||||
        // Mean: Primarily use backend sourced avgs from job.*, secondarily calculate/read from metricdata
 | 
			
		||||
        let mv = null
 | 
			
		||||
        if (fm === 'cpu_load' && job.loadAvg !== 0) {
 | 
			
		||||
            mv = round(job.loadAvg, 2)
 | 
			
		||||
        } else if (fm === 'flops_any' && job.flopsAnyAvg !== 0) {
 | 
			
		||||
            mv = round(job.flopsAnyAvg, 2)
 | 
			
		||||
        } else if (fm === 'mem_bw' && job.memBwAvg !== 0) {
 | 
			
		||||
            mv = round(job.memBwAvg, 2)
 | 
			
		||||
        } else { // Calculate from jobMetrics
 | 
			
		||||
            const jm  = jobMetrics.find((jm) => jm.name === fm && jm.scope === 'node')
 | 
			
		||||
            if (jm?.metric?.statisticsSeries) {
 | 
			
		||||
                mv = round(mean(jm.metric.statisticsSeries.mean), 2)
 | 
			
		||||
            } else if (jm?.metric?.series?.length > 1) {
 | 
			
		||||
                const avgs = jm.metric.series.map(jms => jms.statistics.avg)
 | 
			
		||||
                mv = round(mean(avgs), 2)
 | 
			
		||||
            } else if (jm?.metric?.series) {
 | 
			
		||||
                mv = round(jm.metric.series[0].statistics.avg, 2)
 | 
			
		||||
            } else {
 | 
			
		||||
                mv = 0.0
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Unit
 | 
			
		||||
        const fmc = getContext('metrics')(job.cluster, fm)
 | 
			
		||||
        let unit = ''
 | 
			
		||||
        if (fmc?.unit?.base) unit = fmc.unit.prefix + fmc.unit.base
 | 
			
		||||
 | 
			
		||||
        // Threshold / -Differences
 | 
			
		||||
        const fmt = findJobThresholds(job, fmc, subclusterConfig)
 | 
			
		||||
        if (fm === 'flops_any') fmt.peak = round((fmt.peak * 0.85), 0)
 | 
			
		||||
 | 
			
		||||
        // Define basic data
 | 
			
		||||
        const fmBase = {
 | 
			
		||||
            name: fm,
 | 
			
		||||
            unit: unit,
 | 
			
		||||
            avg: mv,
 | 
			
		||||
            max: fmt.peak
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (evalFootprint(fm, mv, fmt, 'alert')) {
 | 
			
		||||
            return {
 | 
			
		||||
                ...fmBase,
 | 
			
		||||
                color: 'danger',
 | 
			
		||||
                message:`Metric average way ${fm === 'mem_used' ? 'above' : 'below' } expected normal thresholds.`,
 | 
			
		||||
                impact: 3
 | 
			
		||||
            }
 | 
			
		||||
        } else if (evalFootprint(fm, mv, fmt, 'caution')) {
 | 
			
		||||
            return {
 | 
			
		||||
                ...fmBase,
 | 
			
		||||
                color: 'warning',
 | 
			
		||||
                message: `Metric average ${fm === 'mem_used' ? 'above' : 'below' } expected normal thresholds.`,
 | 
			
		||||
                impact: 2
 | 
			
		||||
            }
 | 
			
		||||
        } else if (evalFootprint(fm, mv, fmt, 'normal')) {
 | 
			
		||||
            return {
 | 
			
		||||
                ...fmBase,
 | 
			
		||||
                color: 'success',
 | 
			
		||||
                message: 'Metric average within expected thresholds.',
 | 
			
		||||
                impact: 1
 | 
			
		||||
            }
 | 
			
		||||
        } else if (evalFootprint(fm, mv, fmt, 'peak')) {
 | 
			
		||||
            return {
 | 
			
		||||
                ...fmBase,
 | 
			
		||||
                color: 'info',
 | 
			
		||||
                message: 'Metric average above expected normal thresholds: Check for artifacts recommended.',
 | 
			
		||||
                impact: 0
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            return {
 | 
			
		||||
                ...fmBase,
 | 
			
		||||
                color: 'secondary',
 | 
			
		||||
                message: 'Metric average above expected peak threshold: Check for artifacts!',
 | 
			
		||||
                impact: -1
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    function evalFootprint(metric, mean, thresholds, level) {
 | 
			
		||||
        // mem_used has inverse logic regarding threshold levels
 | 
			
		||||
        switch (level) {
 | 
			
		||||
            case 'peak':
 | 
			
		||||
                if (metric === 'mem_used') return (mean <= thresholds.peak && mean > thresholds.alert)
 | 
			
		||||
                else                       return (mean <= thresholds.peak && mean > thresholds.normal)
 | 
			
		||||
            case 'alert':
 | 
			
		||||
                if (metric === 'mem_used') return (mean <= thresholds.alert && mean > thresholds.caution)
 | 
			
		||||
                else                       return (mean <= thresholds.alert && mean >= 0)
 | 
			
		||||
            case 'caution':
 | 
			
		||||
                if (metric === 'mem_used') return (mean <= thresholds.caution && mean > thresholds.normal)
 | 
			
		||||
                else                       return (mean <= thresholds.caution && mean > thresholds.alert)
 | 
			
		||||
            case 'normal':
 | 
			
		||||
                if (metric === 'mem_used') return (mean <= thresholds.normal && mean >= 0)
 | 
			
		||||
                else                       return (mean <= thresholds.normal && mean > thresholds.caution)
 | 
			
		||||
            default:
 | 
			
		||||
                return false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<script context="module">
 | 
			
		||||
    export function findJobThresholds(job, metricConfig, subClusterConfig) {
 | 
			
		||||
 | 
			
		||||
    if (!job || !metricConfig || !subClusterConfig) {
 | 
			
		||||
        console.warn('Argument missing for findJobThresholds!')
 | 
			
		||||
        return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const subclusterThresholds = metricConfig.subClusters.find(sc => sc.name == subClusterConfig.name)
 | 
			
		||||
    const defaultThresholds = { 
 | 
			
		||||
        peak:    subclusterThresholds ? subclusterThresholds.peak    : metricConfig.peak,
 | 
			
		||||
        normal:  subclusterThresholds ? subclusterThresholds.normal  : metricConfig.normal,
 | 
			
		||||
        caution: subclusterThresholds ? subclusterThresholds.caution : metricConfig.caution,
 | 
			
		||||
        alert:   subclusterThresholds ? subclusterThresholds.alert   : metricConfig.alert
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (job.exclusive === 1) { // Exclusive: Use as defined
 | 
			
		||||
        return defaultThresholds
 | 
			
		||||
    } else { // Shared: Handle specifically
 | 
			
		||||
        if (metricConfig.name === 'cpu_load') { // Special: Avg Aggregation BUT scaled based on #hwthreads
 | 
			
		||||
            return { 
 | 
			
		||||
                peak:    job.numHWThreads,
 | 
			
		||||
                normal:  job.numHWThreads,
 | 
			
		||||
                caution: defaultThresholds.caution,
 | 
			
		||||
                alert:   defaultThresholds.alert
 | 
			
		||||
            }   
 | 
			
		||||
        } else if (metricConfig.aggregation === 'avg' ){
 | 
			
		||||
            return defaultThresholds
 | 
			
		||||
        } else if (metricConfig.aggregation === 'sum' ){
 | 
			
		||||
            const jobFraction = job.numHWThreads / subClusterConfig.topology.node.length
 | 
			
		||||
            return {
 | 
			
		||||
                peak: round((defaultThresholds.peak * jobFraction), 0),
 | 
			
		||||
                normal: round((defaultThresholds.normal * jobFraction), 0),
 | 
			
		||||
                caution: round((defaultThresholds.caution * jobFraction), 0),
 | 
			
		||||
                alert: round((defaultThresholds.alert * jobFraction), 0)
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            console.warn('Missing or unkown aggregation mode (sum/avg) for metric:', metricConfig)
 | 
			
		||||
            return null
 | 
			
		||||
        }
 | 
			
		||||
    } // Other job.exclusive cases?
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Card class="h-auto mt-1" style="width: {width}px;">
 | 
			
		||||
    {#if view === 'job'}
 | 
			
		||||
    <CardHeader>
 | 
			
		||||
        <CardTitle class="mb-0 d-flex justify-content-center">
 | 
			
		||||
            Core Metrics Footprint
 | 
			
		||||
        </CardTitle>
 | 
			
		||||
    </CardHeader>
 | 
			
		||||
    {/if}
 | 
			
		||||
    <CardBody>
 | 
			
		||||
        {#each footprintData as fpd, index}
 | 
			
		||||
            <div class="mb-1 d-flex justify-content-between">
 | 
			
		||||
                <div> <b>{fpd.name}</b></div> <!-- For symmetry, see below ...-->
 | 
			
		||||
                <div class="cursor-help d-inline-flex" id={`footprint-${job.jobId}-${index}`}>
 | 
			
		||||
                    <div class="mx-1">
 | 
			
		||||
                        <!-- Alerts Only -->
 | 
			
		||||
                        {#if fpd.impact === 3 || fpd.impact === -1}
 | 
			
		||||
                            <Icon name="exclamation-triangle-fill" class="text-danger"/>
 | 
			
		||||
                        {:else if fpd.impact === 2}
 | 
			
		||||
                            <Icon name="exclamation-triangle" class="text-warning"/>
 | 
			
		||||
                        {/if}
 | 
			
		||||
                        <!-- Emoji for all states-->
 | 
			
		||||
                        {#if fpd.impact === 3}
 | 
			
		||||
                            <Icon name="emoji-frown" class="text-danger"/>
 | 
			
		||||
                        {:else if fpd.impact === 2}
 | 
			
		||||
                            <Icon name="emoji-neutral" class="text-warning"/>
 | 
			
		||||
                        {:else if fpd.impact === 1}
 | 
			
		||||
                            <Icon name="emoji-smile" class="text-success"/>
 | 
			
		||||
                        {:else if fpd.impact === 0}
 | 
			
		||||
                            <Icon name="emoji-laughing" class="text-info"/>
 | 
			
		||||
                        {:else if fpd.impact === -1}
 | 
			
		||||
                            <Icon name="emoji-dizzy" class="text-danger"/>
 | 
			
		||||
                        {/if}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div>
 | 
			
		||||
                        <!-- Print Values -->
 | 
			
		||||
                        {fpd.avg} / {fpd.max} {fpd.unit}   <!-- To increase margin to tooltip: No other way manageable ... -->
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <Tooltip target={`footprint-${job.jobId}-${index}`} placement="right" offset={[0, 20]}>{fpd.message}</Tooltip>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="mb-2">
 | 
			
		||||
                <Progress
 | 
			
		||||
                    value={fpd.avg}
 | 
			
		||||
                    max={fpd.max}
 | 
			
		||||
                    color={fpd.color}
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
        {/each}
 | 
			
		||||
        {#if job?.metaData?.message}
 | 
			
		||||
            <hr class="mt-1 mb-2"/>
 | 
			
		||||
            {@html job.metaData.message}
 | 
			
		||||
        {/if}
 | 
			
		||||
    </CardBody>
 | 
			
		||||
</Card>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
    .cursor-help {
 | 
			
		||||
        cursor: help;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,102 +0,0 @@
 | 
			
		||||
<script>
 | 
			
		||||
    import { onMount, getContext } from 'svelte'
 | 
			
		||||
    import { init } from './utils.js'
 | 
			
		||||
    import { Row, Col, Button, Icon, Card, Spinner } from 'sveltestrap'
 | 
			
		||||
    import Filters from './filters/Filters.svelte'
 | 
			
		||||
    import JobList from './joblist/JobList.svelte'
 | 
			
		||||
    import Refresher from './joblist/Refresher.svelte'
 | 
			
		||||
    import Sorting from './joblist/SortSelection.svelte'
 | 
			
		||||
    import MetricSelection from './MetricSelection.svelte'
 | 
			
		||||
    import UserOrProject from './filters/UserOrProject.svelte'
 | 
			
		||||
 | 
			
		||||
    const { query: initq } = init()
 | 
			
		||||
 | 
			
		||||
    const ccconfig = getContext('cc-config')
 | 
			
		||||
 | 
			
		||||
    export let filterPresets = {}
 | 
			
		||||
    export let authlevel
 | 
			
		||||
    export let roles
 | 
			
		||||
 | 
			
		||||
    let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
 | 
			
		||||
    let jobList, matchedJobs = null
 | 
			
		||||
    let sorting = { field: 'startTime', order: 'DESC' }, isSortingOpen = false, isMetricsSelectionOpen = false
 | 
			
		||||
    let metrics = filterPresets.cluster
 | 
			
		||||
        ? ccconfig[`plot_list_selectedMetrics:${filterPresets.cluster}`] || ccconfig.plot_list_selectedMetrics
 | 
			
		||||
        : ccconfig.plot_list_selectedMetrics
 | 
			
		||||
    let showFootprint = filterPresets.cluster
 | 
			
		||||
        ? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
 | 
			
		||||
        : !!ccconfig.plot_list_showFootprint
 | 
			
		||||
    let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null
 | 
			
		||||
    
 | 
			
		||||
    // The filterPresets are handled by the Filters component,
 | 
			
		||||
    // so we need to wait for it to be ready before we can start a query.
 | 
			
		||||
    // This is also why JobList component starts out with a paused query.
 | 
			
		||||
    onMount(() => filterComponent.update())
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Row>
 | 
			
		||||
    {#if $initq.fetching}
 | 
			
		||||
        <Col xs="auto">
 | 
			
		||||
            <Spinner/>
 | 
			
		||||
        </Col>
 | 
			
		||||
    {:else if $initq.error}
 | 
			
		||||
        <Col xs="auto">
 | 
			
		||||
            <Card body color="danger">{$initq.error.message}</Card>
 | 
			
		||||
        </Col>
 | 
			
		||||
    {/if}
 | 
			
		||||
</Row>
 | 
			
		||||
<Row>
 | 
			
		||||
    <Col xs="auto">
 | 
			
		||||
        <Button
 | 
			
		||||
            outline color="primary"
 | 
			
		||||
            on:click={() => (isSortingOpen = true)}>
 | 
			
		||||
            <Icon name="sort-up"/> Sorting
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button
 | 
			
		||||
            outline color="primary"
 | 
			
		||||
            on:click={() => (isMetricsSelectionOpen = true)}>
 | 
			
		||||
            <Icon name="graph-up"/> Metrics
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button disabled outline>{matchedJobs == null ? 'Loading...' : `${matchedJobs} jobs`}</Button>
 | 
			
		||||
    </Col>
 | 
			
		||||
    <Col xs="auto">
 | 
			
		||||
        <Filters
 | 
			
		||||
            filterPresets={filterPresets}
 | 
			
		||||
            bind:this={filterComponent}
 | 
			
		||||
            on:update={({ detail }) => {
 | 
			
		||||
                selectedCluster = detail.filters[0]?.cluster ? detail.filters[0].cluster.eq : null 
 | 
			
		||||
                jobList.update(detail.filters)
 | 
			
		||||
            }
 | 
			
		||||
            } />
 | 
			
		||||
    </Col>
 | 
			
		||||
 | 
			
		||||
    <Col xs="3" style="margin-left: auto;">
 | 
			
		||||
        <UserOrProject bind:authlevel={authlevel} bind:roles={roles} on:update={({ detail }) => filterComponent.update(detail)}/>
 | 
			
		||||
    </Col>
 | 
			
		||||
    <Col xs="2">
 | 
			
		||||
        <Refresher on:reload={() => jobList.refresh()} />
 | 
			
		||||
    </Col>
 | 
			
		||||
</Row>
 | 
			
		||||
<br/>
 | 
			
		||||
<Row>
 | 
			
		||||
    <Col>
 | 
			
		||||
        <JobList
 | 
			
		||||
            bind:metrics={metrics}
 | 
			
		||||
            bind:sorting={sorting}
 | 
			
		||||
            bind:matchedJobs={matchedJobs}
 | 
			
		||||
            bind:this={jobList}
 | 
			
		||||
            bind:showFootprint={showFootprint} />
 | 
			
		||||
    </Col>
 | 
			
		||||
</Row>
 | 
			
		||||
 | 
			
		||||
<Sorting
 | 
			
		||||
    bind:sorting={sorting}
 | 
			
		||||
    bind:isOpen={isSortingOpen} />
 | 
			
		||||
 | 
			
		||||
<MetricSelection
 | 
			
		||||
    bind:cluster={selectedCluster}
 | 
			
		||||
    configName="plot_list_selectedMetrics"
 | 
			
		||||
    bind:metrics={metrics}
 | 
			
		||||
    bind:isOpen={isMetricsSelectionOpen}
 | 
			
		||||
    bind:showFootprint={showFootprint}
 | 
			
		||||
    view='list'/>
 | 
			
		||||
@@ -1,242 +0,0 @@
 | 
			
		||||
<!--
 | 
			
		||||
    @component List of users or projects
 | 
			
		||||
 -->
 | 
			
		||||
<script>
 | 
			
		||||
    import { onMount } from "svelte";
 | 
			
		||||
    import { init } from "./utils.js";
 | 
			
		||||
    import {
 | 
			
		||||
        Row,
 | 
			
		||||
        Col,
 | 
			
		||||
        Button,
 | 
			
		||||
        Icon,
 | 
			
		||||
        Table,
 | 
			
		||||
        Card,
 | 
			
		||||
        Spinner,
 | 
			
		||||
        InputGroup,
 | 
			
		||||
        Input,
 | 
			
		||||
    } from "sveltestrap";
 | 
			
		||||
    import Filters from "./filters/Filters.svelte";
 | 
			
		||||
    import { queryStore, gql, getContextClient } from "@urql/svelte";
 | 
			
		||||
    import { scramble, scrambleNames } from "./joblist/JobInfo.svelte";
 | 
			
		||||
 | 
			
		||||
    const {} = init();
 | 
			
		||||
 | 
			
		||||
    export let type;
 | 
			
		||||
    export let filterPresets;
 | 
			
		||||
 | 
			
		||||
    // By default, look at the jobs of the last 30 days:
 | 
			
		||||
    if (filterPresets?.startTime == null) {
 | 
			
		||||
        if (filterPresets == null)
 | 
			
		||||
                filterPresets = {}
 | 
			
		||||
 | 
			
		||||
            const lastMonth = (new Date(Date.now() - (30*24*60*60*1000))).toISOString()
 | 
			
		||||
            const now = (new Date(Date.now())).toISOString()
 | 
			
		||||
            filterPresets.startTime = { from: lastMonth, to: now, text: 'Last 30 Days', url: 'last30d' }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.assert(
 | 
			
		||||
        type == "USER" || type == "PROJECT",
 | 
			
		||||
        "Invalid list type provided!"
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
 | 
			
		||||
    let jobFilters = [];
 | 
			
		||||
    let nameFilter = "";
 | 
			
		||||
    let sorting = { field: "totalJobs", direction: "down" };
 | 
			
		||||
 | 
			
		||||
    const client = getContextClient();
 | 
			
		||||
    $: stats = queryStore({
 | 
			
		||||
        client: client,
 | 
			
		||||
        query: gql`
 | 
			
		||||
            query($jobFilters: [JobFilter!]!) {
 | 
			
		||||
            rows: jobsStatistics(filter: $jobFilters, groupBy: ${type}) {
 | 
			
		||||
                id
 | 
			
		||||
                name
 | 
			
		||||
                totalJobs
 | 
			
		||||
                totalWalltime
 | 
			
		||||
                totalCoreHours
 | 
			
		||||
                totalAccHours
 | 
			
		||||
            }
 | 
			
		||||
        }`,
 | 
			
		||||
        variables: { jobFilters }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function changeSorting(event, field) {
 | 
			
		||||
        let target = event.target;
 | 
			
		||||
        while (target.tagName != "BUTTON") target = target.parentElement;
 | 
			
		||||
 | 
			
		||||
        let direction = target.children[0].className.includes("up")
 | 
			
		||||
            ? "down"
 | 
			
		||||
            : "up";
 | 
			
		||||
        target.children[0].className = `bi-sort-numeric-${direction}`;
 | 
			
		||||
        sorting = { field, direction };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function sort(stats, sorting, nameFilter) {
 | 
			
		||||
        const cmp =
 | 
			
		||||
            sorting.field == "id"
 | 
			
		||||
                ? sorting.direction == "up"
 | 
			
		||||
                    ? (a, b) => a.id < b.id
 | 
			
		||||
                    : (a, b) => a.id > b.id
 | 
			
		||||
                : 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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onMount(() => filterComponent.update());
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Row>
 | 
			
		||||
    <Col xs="auto">
 | 
			
		||||
        <InputGroup>
 | 
			
		||||
            <Button disabled outline>
 | 
			
		||||
                Search {type.toLowerCase()}s
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Input
 | 
			
		||||
                bind:value={nameFilter}
 | 
			
		||||
                placeholder="Filter by {{
 | 
			
		||||
                    USER: 'username',
 | 
			
		||||
                    PROJECT: 'project',
 | 
			
		||||
                }[type]}"
 | 
			
		||||
            />
 | 
			
		||||
        </InputGroup>
 | 
			
		||||
    </Col>
 | 
			
		||||
    <Col xs="auto">
 | 
			
		||||
        <Filters
 | 
			
		||||
            bind:this={filterComponent}
 | 
			
		||||
            {filterPresets}
 | 
			
		||||
            startTimeQuickSelect={true}
 | 
			
		||||
            menuText="Only {type.toLowerCase()}s with jobs that match the filters will show up"
 | 
			
		||||
            on:update={({ detail }) => {
 | 
			
		||||
                jobFilters = detail.filters;
 | 
			
		||||
            }}
 | 
			
		||||
        />
 | 
			
		||||
    </Col>
 | 
			
		||||
</Row>
 | 
			
		||||
<Table>
 | 
			
		||||
    <thead>
 | 
			
		||||
        <tr>
 | 
			
		||||
            <th scope="col">
 | 
			
		||||
                {({
 | 
			
		||||
                    USER: "Username",
 | 
			
		||||
                    PROJECT: "Project Name",
 | 
			
		||||
                })[type]}
 | 
			
		||||
                <Button
 | 
			
		||||
                    color={sorting.field == "id" ? "primary" : "light"}
 | 
			
		||||
                    size="sm"
 | 
			
		||||
                    on:click={(e) => changeSorting(e, "id")}
 | 
			
		||||
                >
 | 
			
		||||
                    <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"}
 | 
			
		||||
                    size="sm"
 | 
			
		||||
                    on:click={(e) => changeSorting(e, "totalJobs")}
 | 
			
		||||
                >
 | 
			
		||||
                    <Icon name="sort-numeric-down" />
 | 
			
		||||
                </Button>
 | 
			
		||||
            </th>
 | 
			
		||||
            <th scope="col">
 | 
			
		||||
                Total Walltime
 | 
			
		||||
                <Button
 | 
			
		||||
                    color={sorting.field == "totalWalltime"
 | 
			
		||||
                        ? "primary"
 | 
			
		||||
                        : "light"}
 | 
			
		||||
                    size="sm"
 | 
			
		||||
                    on:click={(e) => changeSorting(e, "totalWalltime")}
 | 
			
		||||
                >
 | 
			
		||||
                    <Icon name="sort-numeric-down" />
 | 
			
		||||
                </Button>
 | 
			
		||||
            </th>
 | 
			
		||||
            <th scope="col">
 | 
			
		||||
                Total Core Hours
 | 
			
		||||
                <Button
 | 
			
		||||
                    color={sorting.field == "totalCoreHours"
 | 
			
		||||
                        ? "primary"
 | 
			
		||||
                        : "light"}
 | 
			
		||||
                    size="sm"
 | 
			
		||||
                    on:click={(e) => changeSorting(e, "totalCoreHours")}
 | 
			
		||||
                >
 | 
			
		||||
                    <Icon name="sort-numeric-down" />
 | 
			
		||||
                </Button>
 | 
			
		||||
            </th>
 | 
			
		||||
            <th scope="col">
 | 
			
		||||
                Total Accelerator Hours
 | 
			
		||||
                <Button
 | 
			
		||||
                    color={sorting.field == "totalAccHours"
 | 
			
		||||
                        ? "primary"
 | 
			
		||||
                        : "light"}
 | 
			
		||||
                    size="sm"
 | 
			
		||||
                    on:click={(e) => changeSorting(e, "totalAccHours")}
 | 
			
		||||
                >
 | 
			
		||||
                    <Icon name="sort-numeric-down" />
 | 
			
		||||
                </Button>
 | 
			
		||||
            </th>
 | 
			
		||||
        </tr>
 | 
			
		||||
    </thead>
 | 
			
		||||
    <tbody>
 | 
			
		||||
        {#if $stats.fetching}
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td colspan="4" style="text-align: center;"
 | 
			
		||||
                    ><Spinner secondary /></td
 | 
			
		||||
                >
 | 
			
		||||
            </tr>
 | 
			
		||||
        {:else if $stats.error}
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td colspan="4"
 | 
			
		||||
                    ><Card body color="danger" class="mb-3"
 | 
			
		||||
                        >{$stats.error.message}</Card
 | 
			
		||||
                    ></td
 | 
			
		||||
                >
 | 
			
		||||
            </tr>
 | 
			
		||||
        {:else if $stats.data}
 | 
			
		||||
            {#each sort($stats.data.rows, sorting, nameFilter) as row (row.id)}
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        {#if type == "USER"}
 | 
			
		||||
                            <a href="/monitoring/user/{row.id}"
 | 
			
		||||
                                >{scrambleNames ? scramble(row.id) : row.id}</a
 | 
			
		||||
                            >
 | 
			
		||||
                        {:else if type == "PROJECT"}
 | 
			
		||||
                            <a href="/monitoring/jobs/?project={row.id}"
 | 
			
		||||
                                >{scrambleNames ? scramble(row.id) : row.id}</a
 | 
			
		||||
                            >
 | 
			
		||||
                        {:else}
 | 
			
		||||
                            {row.id}
 | 
			
		||||
                        {/if}
 | 
			
		||||
                    </td>
 | 
			
		||||
                    {#if type == "USER"}
 | 
			
		||||
                        <td>{scrambleNames ? scramble(row?.name?row.name:"-") : row?.name?row.name:"-"}</td>
 | 
			
		||||
                    {/if}
 | 
			
		||||
                    <td>{row.totalJobs}</td>
 | 
			
		||||
                    <td>{row.totalWalltime}</td>
 | 
			
		||||
                    <td>{row.totalCoreHours}</td>
 | 
			
		||||
                    <td>{row.totalAccHours}</td>
 | 
			
		||||
                </tr>
 | 
			
		||||
            {:else}
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td colspan="4"
 | 
			
		||||
                        ><i>No {type.toLowerCase()}s/jobs found</i></td
 | 
			
		||||
                    >
 | 
			
		||||
                </tr>
 | 
			
		||||
            {/each}
 | 
			
		||||
        {/if}
 | 
			
		||||
    </tbody>
 | 
			
		||||
</Table>
 | 
			
		||||
@@ -1,160 +0,0 @@
 | 
			
		||||
<script>
 | 
			
		||||
    import { init, checkMetricDisabled } from './utils.js'
 | 
			
		||||
    import Refresher from './joblist/Refresher.svelte'
 | 
			
		||||
    import { Row, Col, Input, InputGroup, InputGroupText, Icon, Spinner, Card } from 'sveltestrap'
 | 
			
		||||
    import { queryStore, gql, getContextClient } from '@urql/svelte'
 | 
			
		||||
    import TimeSelection from './filters/TimeSelection.svelte'
 | 
			
		||||
    import PlotTable from './PlotTable.svelte'
 | 
			
		||||
    import MetricPlot from './plots/MetricPlot.svelte'
 | 
			
		||||
    import { getContext } from 'svelte'
 | 
			
		||||
 | 
			
		||||
    export let cluster
 | 
			
		||||
    export let from = null
 | 
			
		||||
    export let to = null
 | 
			
		||||
 | 
			
		||||
    const { query: initq } = init()
 | 
			
		||||
 | 
			
		||||
    if (from == null || to == null) {
 | 
			
		||||
        to = new Date(Date.now())
 | 
			
		||||
        from = new Date(to.getTime())
 | 
			
		||||
        from.setMinutes(from.getMinutes() - 30)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const clusters = getContext('clusters')
 | 
			
		||||
    console.log(clusters)
 | 
			
		||||
    const ccconfig = getContext('cc-config')
 | 
			
		||||
    const metricConfig = getContext('metrics')
 | 
			
		||||
 | 
			
		||||
    let plotHeight = 300
 | 
			
		||||
    let hostnameFilter = ''
 | 
			
		||||
    let selectedMetric = ccconfig.system_view_selectedMetric
 | 
			
		||||
 | 
			
		||||
    const client = getContextClient();
 | 
			
		||||
    $: nodesQuery = queryStore({
 | 
			
		||||
        client: client,
 | 
			
		||||
        query: gql`query($cluster: String!, $metrics: [String!], $from: Time!, $to: Time!) {
 | 
			
		||||
            nodeMetrics(cluster: $cluster, metrics: $metrics, from: $from, to: $to) {
 | 
			
		||||
                host
 | 
			
		||||
                subCluster
 | 
			
		||||
                metrics {
 | 
			
		||||
                    name
 | 
			
		||||
                    scope
 | 
			
		||||
                    metric {
 | 
			
		||||
                        timestep
 | 
			
		||||
                        unit { base, prefix }
 | 
			
		||||
                        series {
 | 
			
		||||
                            statistics { min, avg, max }
 | 
			
		||||
                            data
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }`,
 | 
			
		||||
        variables: {
 | 
			
		||||
            cluster: cluster,
 | 
			
		||||
            metrics: [selectedMetric],
 | 
			
		||||
            from: from.toISOString(),
 | 
			
		||||
            to: to.toISOString()
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    let metricUnits = {}
 | 
			
		||||
    $: if ($nodesQuery.data) {
 | 
			
		||||
        let thisCluster = clusters.find(c => c.name == cluster)
 | 
			
		||||
        if (thisCluster) {
 | 
			
		||||
            for (let metric of thisCluster.metricConfig) {
 | 
			
		||||
                if (metric.unit.prefix || metric.unit.base) {
 | 
			
		||||
                    metricUnits[metric.name] = '(' + (metric.unit.prefix ? metric.unit.prefix : '') + (metric.unit.base ? metric.unit.base : '') + ')'
 | 
			
		||||
                } else { // If no unit defined: Omit Unit Display
 | 
			
		||||
                    metricUnits[metric.name] = ''
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Row>
 | 
			
		||||
    {#if $initq.error}
 | 
			
		||||
        <Card body color="danger">{$initq.error.message}</Card>
 | 
			
		||||
    {:else if $initq.fetching}
 | 
			
		||||
        <Spinner/>
 | 
			
		||||
    {:else}
 | 
			
		||||
        <Col>
 | 
			
		||||
            <Refresher on:reload={() => {
 | 
			
		||||
                const diff = Date.now() - to
 | 
			
		||||
                from = new Date(from.getTime() + diff)
 | 
			
		||||
                to = new Date(to.getTime() + diff)
 | 
			
		||||
            }} />
 | 
			
		||||
        </Col>
 | 
			
		||||
        <Col>
 | 
			
		||||
            <TimeSelection
 | 
			
		||||
                bind:from={from}
 | 
			
		||||
                bind:to={to} />
 | 
			
		||||
        </Col>
 | 
			
		||||
        <Col>
 | 
			
		||||
            <InputGroup>
 | 
			
		||||
                <InputGroupText><Icon name="graph-up" /></InputGroupText>
 | 
			
		||||
                <InputGroupText>Metric</InputGroupText>
 | 
			
		||||
                <select class="form-select" bind:value={selectedMetric}>
 | 
			
		||||
                    {#each clusters.find(c => c.name == cluster).metricConfig as metric}
 | 
			
		||||
                        <option value={metric.name}>{metric.name} {metricUnits[metric.name]}</option>
 | 
			
		||||
                    {/each}
 | 
			
		||||
                </select>
 | 
			
		||||
            </InputGroup>
 | 
			
		||||
        </Col>
 | 
			
		||||
        <Col>
 | 
			
		||||
            <InputGroup>
 | 
			
		||||
                <InputGroupText><Icon name="hdd" /></InputGroupText>
 | 
			
		||||
                <InputGroupText>Find Node</InputGroupText>
 | 
			
		||||
                <Input placeholder="hostname..." type="text" bind:value={hostnameFilter} />
 | 
			
		||||
            </InputGroup>
 | 
			
		||||
        </Col>
 | 
			
		||||
    {/if}
 | 
			
		||||
</Row>
 | 
			
		||||
<br/>
 | 
			
		||||
<Row>
 | 
			
		||||
    <Col>
 | 
			
		||||
        {#if $nodesQuery.error}
 | 
			
		||||
            <Card body color="danger">{$nodesQuery.error.message}</Card>
 | 
			
		||||
        {:else if $nodesQuery.fetching || $initq.fetching}
 | 
			
		||||
            <Spinner/>
 | 
			
		||||
        {:else}
 | 
			
		||||
            <PlotTable
 | 
			
		||||
                let:item
 | 
			
		||||
                let:width
 | 
			
		||||
                renderFor="systems"
 | 
			
		||||
                itemsPerRow={ccconfig.plot_view_plotsPerRow}
 | 
			
		||||
                items={$nodesQuery.data.nodeMetrics
 | 
			
		||||
                    .filter(h => h.host.includes(hostnameFilter) && h.metrics.some(m => m.name == selectedMetric && m.scope == 'node'))
 | 
			
		||||
                    .map(h => ({
 | 
			
		||||
                        host: h.host,
 | 
			
		||||
                        subCluster: h.subCluster,
 | 
			
		||||
                        data: h.metrics.find(m => m.name == selectedMetric && m.scope == 'node'),
 | 
			
		||||
                        disabled: checkMetricDisabled(selectedMetric, cluster, h.subCluster)
 | 
			
		||||
                    }))
 | 
			
		||||
                    .sort((a, b) => a.host.localeCompare(b.host))
 | 
			
		||||
                }>
 | 
			
		||||
 | 
			
		||||
                    <h4 style="width: 100%; text-align: center;"><a style="display: block;padding-top: 15px;" href="/monitoring/node/{cluster}/{item.host}">{item.host} ({item.subCluster})</a></h4>
 | 
			
		||||
                    {#if item.disabled === false && item.data}
 | 
			
		||||
                        <MetricPlot
 | 
			
		||||
                            width={width}
 | 
			
		||||
                            height={plotHeight}
 | 
			
		||||
                            timestep={item.data.metric.timestep}
 | 
			
		||||
                            series={item.data.metric.series}
 | 
			
		||||
                            metric={item.data.name}
 | 
			
		||||
                            cluster={clusters.find(c => c.name == cluster)}
 | 
			
		||||
                            subCluster={item.subCluster} 
 | 
			
		||||
                            resources={[{hostname: item.host}]}
 | 
			
		||||
                            forNode={true}/>
 | 
			
		||||
                    {:else if item.disabled === true && item.data}
 | 
			
		||||
                        <Card style="margin-left: 2rem;margin-right: 2rem;" body color="info">Metric disabled for subcluster <code>{selectedMetric}:{item.subCluster}</code></Card>
 | 
			
		||||
                    {:else}
 | 
			
		||||
                        <Card style="margin-left: 2rem;margin-right: 2rem;" body color="warning">No dataset returned for <code>{selectedMetric}</code></Card>
 | 
			
		||||
                    {/if}
 | 
			
		||||
            </PlotTable>
 | 
			
		||||
        {/if}
 | 
			
		||||
    </Col>
 | 
			
		||||
</Row>
 | 
			
		||||
 | 
			
		||||
@@ -1,237 +0,0 @@
 | 
			
		||||
<script>
 | 
			
		||||
    import { onMount, getContext } from 'svelte'
 | 
			
		||||
    import { init, convert2uplot } from './utils.js'
 | 
			
		||||
    import { Table, Row, Col, Button, Icon, Card, Spinner } from 'sveltestrap'
 | 
			
		||||
    import { queryStore, gql, getContextClient } from '@urql/svelte'
 | 
			
		||||
    import Filters from './filters/Filters.svelte'
 | 
			
		||||
    import JobList from './joblist/JobList.svelte'
 | 
			
		||||
    import Sorting from './joblist/SortSelection.svelte'
 | 
			
		||||
    import Refresher from './joblist/Refresher.svelte'
 | 
			
		||||
    import Histogram from './plots/Histogram.svelte'
 | 
			
		||||
    import MetricSelection from './MetricSelection.svelte'
 | 
			
		||||
    import HistogramSelection from './HistogramSelection.svelte'
 | 
			
		||||
    import PlotTable from './PlotTable.svelte'
 | 
			
		||||
    import { scramble, scrambleNames } from './joblist/JobInfo.svelte'
 | 
			
		||||
 | 
			
		||||
    const { query: initq } = init()
 | 
			
		||||
 | 
			
		||||
    const ccconfig = getContext('cc-config')
 | 
			
		||||
 | 
			
		||||
    export let user
 | 
			
		||||
    export let filterPresets
 | 
			
		||||
 | 
			
		||||
    let filterComponent; // see why here: https://stackoverflow.com/questions/58287729/how-can-i-export-a-function-from-a-svelte-component-that-changes-a-value-in-the
 | 
			
		||||
    let jobList;
 | 
			
		||||
    let jobFilters = [];
 | 
			
		||||
    let sorting = { field: 'startTime', order: 'DESC' }, isSortingOpen = false
 | 
			
		||||
    let metrics = ccconfig.plot_list_selectedMetrics, isMetricsSelectionOpen = false
 | 
			
		||||
    let w1, w2, histogramHeight = 250, isHistogramSelectionOpen = false
 | 
			
		||||
    let selectedCluster = filterPresets?.cluster ? filterPresets.cluster : null
 | 
			
		||||
    let showFootprint = filterPresets.cluster
 | 
			
		||||
        ? !!ccconfig[`plot_list_showFootprint:${filterPresets.cluster}`]
 | 
			
		||||
        : !!ccconfig.plot_list_showFootprint
 | 
			
		||||
 | 
			
		||||
    $: metricsInHistograms = selectedCluster ? (ccconfig[`user_view_histogramMetrics:${selectedCluster}`] || []) : (ccconfig.user_view_histogramMetrics || [])
 | 
			
		||||
 | 
			
		||||
    const client = getContextClient();
 | 
			
		||||
    $: stats = queryStore({
 | 
			
		||||
        client: client,
 | 
			
		||||
        query: gql`
 | 
			
		||||
            query($jobFilters: [JobFilter!]!, $metricsInHistograms: [String!]) {
 | 
			
		||||
            jobsStatistics(filter: $jobFilters, metrics: $metricsInHistograms) {
 | 
			
		||||
                totalJobs
 | 
			
		||||
                shortJobs
 | 
			
		||||
                totalWalltime
 | 
			
		||||
                totalCoreHours
 | 
			
		||||
                histDuration { count, value }
 | 
			
		||||
                histNumNodes { count, value }
 | 
			
		||||
                histMetrics  { metric, unit, data { min, max, count, bin } }
 | 
			
		||||
            }}`,
 | 
			
		||||
        variables: { jobFilters, metricsInHistograms }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    onMount(() => filterComponent.update())
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Row>
 | 
			
		||||
    {#if $initq.fetching}
 | 
			
		||||
        <Col>
 | 
			
		||||
            <Spinner/>
 | 
			
		||||
        </Col>
 | 
			
		||||
    {:else if $initq.error}
 | 
			
		||||
        <Col xs="auto">
 | 
			
		||||
            <Card body color="danger">{$initq.error.message}</Card>
 | 
			
		||||
        </Col>
 | 
			
		||||
    {/if}
 | 
			
		||||
 | 
			
		||||
    <Col xs="auto">
 | 
			
		||||
        <Button
 | 
			
		||||
            outline color="primary"
 | 
			
		||||
            on:click={() => (isSortingOpen = true)}>
 | 
			
		||||
            <Icon name="sort-up"/> Sorting
 | 
			
		||||
        </Button>
 | 
			
		||||
 | 
			
		||||
        <Button
 | 
			
		||||
            outline color="primary"
 | 
			
		||||
            on:click={() => (isMetricsSelectionOpen = true)}>
 | 
			
		||||
            <Icon name="graph-up"/> Metrics
 | 
			
		||||
        </Button>
 | 
			
		||||
 | 
			
		||||
        <Button
 | 
			
		||||
            outline color="secondary"
 | 
			
		||||
            on:click={() => (isHistogramSelectionOpen = true)}>
 | 
			
		||||
            <Icon name="bar-chart-line"/> Select Histograms
 | 
			
		||||
        </Button>
 | 
			
		||||
    </Col>
 | 
			
		||||
    <Col xs="auto">
 | 
			
		||||
        <Filters
 | 
			
		||||
            filterPresets={filterPresets}
 | 
			
		||||
            startTimeQuickSelect={true}
 | 
			
		||||
            bind:this={filterComponent}
 | 
			
		||||
            on:update={({ detail }) => {
 | 
			
		||||
                jobFilters = [...detail.filters, { user: { eq: user.username } }]
 | 
			
		||||
                selectedCluster = jobFilters[0]?.cluster ? jobFilters[0].cluster.eq : null 
 | 
			
		||||
                jobList.update(jobFilters)
 | 
			
		||||
            }} />
 | 
			
		||||
    </Col>
 | 
			
		||||
    <Col xs="auto" style="margin-left: auto;">
 | 
			
		||||
        <Refresher on:reload={() => jobList.refresh()} />
 | 
			
		||||
    </Col>
 | 
			
		||||
</Row>
 | 
			
		||||
<br/>
 | 
			
		||||
<Row>
 | 
			
		||||
    {#if $stats.error}
 | 
			
		||||
        <Col>
 | 
			
		||||
            <Card body color="danger">{$stats.error.message}</Card>
 | 
			
		||||
        </Col>
 | 
			
		||||
    {:else if !$stats.data}
 | 
			
		||||
        <Col>
 | 
			
		||||
            <Spinner secondary />
 | 
			
		||||
        </Col>
 | 
			
		||||
    {:else}
 | 
			
		||||
        <Col xs="4">
 | 
			
		||||
            <Table>
 | 
			
		||||
                <tbody>
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th scope="row">Username</th>
 | 
			
		||||
                        <td>{scrambleNames ? scramble(user.username) : user.username}</td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                    {#if user.name}
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th scope="row">Name</th>
 | 
			
		||||
                            <td>{scrambleNames ? scramble(user.name) : user.name}</td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    {/if}
 | 
			
		||||
                    {#if user.email}
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <th scope="row">Email</th>
 | 
			
		||||
                            <td>{user.email}</td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    {/if}
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th scope="row">Total Jobs</th>
 | 
			
		||||
                        <td>{$stats.data.jobsStatistics[0].totalJobs}</td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th scope="row">Short Jobs</th>
 | 
			
		||||
                        <td>{$stats.data.jobsStatistics[0].shortJobs}</td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th scope="row">Total Walltime</th>
 | 
			
		||||
                        <td>{$stats.data.jobsStatistics[0].totalWalltime}</td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <th scope="row">Total Core Hours</th>
 | 
			
		||||
                        <td>{$stats.data.jobsStatistics[0].totalCoreHours}</td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                </tbody>
 | 
			
		||||
            </Table>
 | 
			
		||||
        </Col>
 | 
			
		||||
        <div class="col-4 text-center" bind:clientWidth={w1}>
 | 
			
		||||
            {#key $stats.data.jobsStatistics[0].histDuration}
 | 
			
		||||
                <Histogram
 | 
			
		||||
                    data={convert2uplot($stats.data.jobsStatistics[0].histDuration)}
 | 
			
		||||
                    width={w1 - 25} height={histogramHeight}
 | 
			
		||||
                    title="Duration Distribution"
 | 
			
		||||
                    xlabel="Current Runtimes"
 | 
			
		||||
                    xunit="Hours" 
 | 
			
		||||
                    ylabel="Number of Jobs"
 | 
			
		||||
                    yunit="Jobs"/>
 | 
			
		||||
            {/key}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-4 text-center" bind:clientWidth={w2}>
 | 
			
		||||
            {#key $stats.data.jobsStatistics[0].histNumNodes}
 | 
			
		||||
                <Histogram
 | 
			
		||||
                    data={convert2uplot($stats.data.jobsStatistics[0].histNumNodes)}
 | 
			
		||||
                    width={w2 - 25} height={histogramHeight}
 | 
			
		||||
                    title="Number of Nodes Distribution"
 | 
			
		||||
                    xlabel="Allocated Nodes"
 | 
			
		||||
                    xunit="Nodes"
 | 
			
		||||
                    ylabel="Number of Jobs"
 | 
			
		||||
                    yunit="Jobs"/>
 | 
			
		||||
            {/key}
 | 
			
		||||
        </div>
 | 
			
		||||
    {/if}
 | 
			
		||||
</Row>
 | 
			
		||||
{#if metricsInHistograms}
 | 
			
		||||
    <Row>
 | 
			
		||||
        {#if $stats.error}
 | 
			
		||||
            <Col>
 | 
			
		||||
                <Card body color="danger">{$stats.error.message}</Card>
 | 
			
		||||
            </Col>
 | 
			
		||||
        {:else if !$stats.data}
 | 
			
		||||
            <Col>
 | 
			
		||||
                <Spinner secondary />
 | 
			
		||||
            </Col>
 | 
			
		||||
        {:else}
 | 
			
		||||
            <Col>
 | 
			
		||||
                {#key $stats.data.jobsStatistics[0].histMetrics}
 | 
			
		||||
                    <PlotTable
 | 
			
		||||
                        let:item
 | 
			
		||||
                        let:width
 | 
			
		||||
                        renderFor="user"
 | 
			
		||||
                        items={$stats.data.jobsStatistics[0].histMetrics}
 | 
			
		||||
                        itemsPerRow={3}>
 | 
			
		||||
 | 
			
		||||
                        <Histogram
 | 
			
		||||
                            data={convert2uplot(item.data)}
 | 
			
		||||
                            usesBins={true}
 | 
			
		||||
                            width={width} height={250}
 | 
			
		||||
                            title="Distribution of '{item.metric}' averages"
 | 
			
		||||
                            xlabel={`${item.metric} bin maximum ${item?.unit ? `[${item.unit}]` : ``}`}
 | 
			
		||||
                            xunit={item.unit}
 | 
			
		||||
                            ylabel="Number of Jobs"
 | 
			
		||||
                            yunit="Jobs"/>
 | 
			
		||||
                    </PlotTable>
 | 
			
		||||
                {/key}
 | 
			
		||||
            </Col>
 | 
			
		||||
        {/if}
 | 
			
		||||
    </Row>
 | 
			
		||||
{/if}
 | 
			
		||||
<br/>
 | 
			
		||||
<Row>
 | 
			
		||||
    <Col>
 | 
			
		||||
        <JobList
 | 
			
		||||
            bind:metrics={metrics}
 | 
			
		||||
            bind:sorting={sorting}
 | 
			
		||||
            bind:this={jobList}
 | 
			
		||||
            bind:showFootprint={showFootprint} />
 | 
			
		||||
    </Col>
 | 
			
		||||
</Row>
 | 
			
		||||
 | 
			
		||||
<Sorting
 | 
			
		||||
    bind:sorting={sorting}
 | 
			
		||||
    bind:isOpen={isSortingOpen} />
 | 
			
		||||
 | 
			
		||||
<MetricSelection 
 | 
			
		||||
    bind:cluster={selectedCluster}
 | 
			
		||||
    configName="plot_list_selectedMetrics"
 | 
			
		||||
    bind:metrics={metrics}
 | 
			
		||||
    bind:isOpen={isMetricsSelectionOpen}
 | 
			
		||||
    bind:showFootprint={showFootprint}
 | 
			
		||||
    view='list'/>
 | 
			
		||||
      
 | 
			
		||||
<HistogramSelection
 | 
			
		||||
    bind:cluster={selectedCluster}
 | 
			
		||||
    bind:metricsInHistograms={metricsInHistograms}
 | 
			
		||||
    bind:isOpen={isHistogramSelectionOpen} />
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
import {} from './header.entrypoint.js'
 | 
			
		||||
import Analysis from './Analysis.root.svelte'
 | 
			
		||||
 | 
			
		||||
filterPresets.cluster = cluster
 | 
			
		||||
 | 
			
		||||
new Analysis({
 | 
			
		||||
    target: document.getElementById('svelte-app'),
 | 
			
		||||
    props: {
 | 
			
		||||
        filterPresets: filterPresets
 | 
			
		||||
    },
 | 
			
		||||
    context: new Map([
 | 
			
		||||
            ['cc-config', clusterCockpitConfig]
 | 
			
		||||
    ])
 | 
			
		||||
})
 | 
			
		||||
@@ -1,11 +1,10 @@
 | 
			
		||||
import {} from './header.entrypoint.js'
 | 
			
		||||
import User from './User.root.svelte'
 | 
			
		||||
import Home from './Home.root.svelte'
 | 
			
		||||
 | 
			
		||||
new User({
 | 
			
		||||
new Home({
 | 
			
		||||
    target: document.getElementById('svelte-app'),
 | 
			
		||||
    props: {
 | 
			
		||||
        filterPresets: filterPresets,
 | 
			
		||||
        user: userInfos
 | 
			
		||||
        isAdmin: isAdmin
 | 
			
		||||
    },
 | 
			
		||||
    context: new Map([
 | 
			
		||||
            ['cc-config', clusterCockpitConfig]
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
import {} from './header.entrypoint.js'
 | 
			
		||||
import Job from './Job.root.svelte'
 | 
			
		||||
 | 
			
		||||
new Job({
 | 
			
		||||
    target: document.getElementById('svelte-app'),
 | 
			
		||||
    props: {
 | 
			
		||||
        dbid: jobInfos.id,
 | 
			
		||||
        authlevel: authlevel,
 | 
			
		||||
        roles: roles
 | 
			
		||||
    },
 | 
			
		||||
    context: new Map([
 | 
			
		||||
            ['cc-config', clusterCockpitConfig]
 | 
			
		||||
    ])
 | 
			
		||||
})
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
import {} from './header.entrypoint.js'
 | 
			
		||||
import Jobs from './Jobs.root.svelte'
 | 
			
		||||
 | 
			
		||||
new Jobs({
 | 
			
		||||
    target: document.getElementById('svelte-app'),
 | 
			
		||||
    props: {
 | 
			
		||||
        filterPresets: filterPresets,
 | 
			
		||||
        authlevel: authlevel,
 | 
			
		||||
        roles: roles
 | 
			
		||||
    },
 | 
			
		||||
    context: new Map([
 | 
			
		||||
            ['cc-config', clusterCockpitConfig]
 | 
			
		||||
    ])
 | 
			
		||||
})
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
import {} from './header.entrypoint.js'
 | 
			
		||||
import List from './List.root.svelte'
 | 
			
		||||
 | 
			
		||||
new List({
 | 
			
		||||
    target: document.getElementById('svelte-app'),
 | 
			
		||||
    props: {
 | 
			
		||||
        filterPresets: filterPresets,
 | 
			
		||||
        type: listType,
 | 
			
		||||
    },
 | 
			
		||||
    context: new Map([
 | 
			
		||||
            ['cc-config', clusterCockpitConfig]
 | 
			
		||||
    ])
 | 
			
		||||
})
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
import {} from './header.entrypoint.js'
 | 
			
		||||
import Systems from './Systems.root.svelte'
 | 
			
		||||
 | 
			
		||||
new Systems({
 | 
			
		||||
    target: document.getElementById('svelte-app'),
 | 
			
		||||
    props: {
 | 
			
		||||
        cluster: infos.cluster,
 | 
			
		||||
        from: infos.from,
 | 
			
		||||
        to: infos.to
 | 
			
		||||
    },
 | 
			
		||||
    context: new Map([
 | 
			
		||||
            ['cc-config', clusterCockpitConfig]
 | 
			
		||||
    ])
 | 
			
		||||
})
 | 
			
		||||
@@ -1,57 +1,15 @@
 | 
			
		||||
{{define "content"}}
 | 
			
		||||
{{if .Infos.message }}
 | 
			
		||||
<div class="row justify-content-center">
 | 
			
		||||
    <div class="col-6">
 | 
			
		||||
        <div class="alert alert-info p-3" role="alert">
 | 
			
		||||
            <div class="row  align-items-center">
 | 
			
		||||
                <div class="col-2">
 | 
			
		||||
                    <h2><i class="bi-info-circle-fill m-3"></i></h2>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="col-10">
 | 
			
		||||
                {{.Infos.message}}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
    <div id="svelte-app"></div>
 | 
			
		||||
{{end}}
 | 
			
		||||
<div class="row">
 | 
			
		||||
    <div class="col">
 | 
			
		||||
        <h2>Clusters</h2>
 | 
			
		||||
        <table class="table">
 | 
			
		||||
            <thead>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <th>Name</th>
 | 
			
		||||
                    <th>Running Jobs</th>
 | 
			
		||||
                    <th>Total Jobs</th>
 | 
			
		||||
                    {{if .User.HasRole .Roles.admin}}
 | 
			
		||||
                        <th>Status View</th>
 | 
			
		||||
                        <th>System View</th>
 | 
			
		||||
                    {{end}}
 | 
			
		||||
                </tr>
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody>
 | 
			
		||||
                {{if .User.HasRole .Roles.admin}}
 | 
			
		||||
                    {{range .Infos.clusters}}
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td>{{.ID}}</td>
 | 
			
		||||
                            <td><a href="/monitoring/jobs/?cluster={{.ID}}&state=running">{{.RunningJobs}} jobs</a></td>
 | 
			
		||||
                            <td><a href="/monitoring/jobs/?cluster={{.ID}}">{{.TotalJobs}} jobs</a></td>
 | 
			
		||||
                            <td><a href="/monitoring/status/{{.ID}}">Status View</a></td>
 | 
			
		||||
                            <td><a href="/monitoring/systems/{{.ID}}">System View</a></td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    {{end}}
 | 
			
		||||
                {{else}}
 | 
			
		||||
                    {{range .Infos.clusters}}
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td>{{.ID}}</td>
 | 
			
		||||
                            <td><a href="/monitoring/jobs/?cluster={{.ID}}&state=running">{{.RunningJobs}} jobs</a></td>
 | 
			
		||||
                            <td><a href="/monitoring/jobs/?cluster={{.ID}}">{{.TotalJobs}} jobs</a></td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    {{end}}
 | 
			
		||||
                {{end}}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{{define "stylesheets"}}
 | 
			
		||||
    <link rel='stylesheet' href='/build/home.css'>
 | 
			
		||||
{{end}}
 | 
			
		||||
{{define "javascript"}}
 | 
			
		||||
    <script>
 | 
			
		||||
        const isAdmin = {{ .User.HasRole .Roles.admin }};
 | 
			
		||||
        const filterPresets = {{ .FilterPresets }};
 | 
			
		||||
        const clusterCockpitConfig = {{ .Config }};
 | 
			
		||||
    </script>
 | 
			
		||||
    <script src='/build/home.js'></script>
 | 
			
		||||
{{end}}
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
{{define "content"}}
 | 
			
		||||
    <div id="svelte-app"></div>
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
{{define "stylesheets"}}
 | 
			
		||||
    <link rel='stylesheet' href='/build/analysis.css'>
 | 
			
		||||
{{end}}
 | 
			
		||||
{{define "javascript"}}
 | 
			
		||||
    <script>
 | 
			
		||||
        const cluster = {{ .Infos.cluster }};
 | 
			
		||||
        const filterPresets = {{ .FilterPresets }};
 | 
			
		||||
        const clusterCockpitConfig = {{ .Config }};
 | 
			
		||||
    </script>
 | 
			
		||||
    <script src='/build/analysis.js'></script>
 | 
			
		||||
{{end}}
 | 
			
		||||
@@ -1,20 +0,0 @@
 | 
			
		||||
{{define "content"}}
 | 
			
		||||
    <div id="svelte-app"></div>
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
{{define "stylesheets"}}
 | 
			
		||||
    <link rel='stylesheet' href='/build/job.css'>
 | 
			
		||||
{{end}}
 | 
			
		||||
{{define "javascript"}}
 | 
			
		||||
    <script>
 | 
			
		||||
        const jobInfos = {
 | 
			
		||||
            id: "{{ .Infos.id }}",
 | 
			
		||||
            jobId: "{{ .Infos.jobId }}",
 | 
			
		||||
            clusterId: "{{ .Infos.clusterId }}"
 | 
			
		||||
        };
 | 
			
		||||
        const clusterCockpitConfig = {{ .Config }};
 | 
			
		||||
        const authlevel = {{ .User.GetAuthLevel }};
 | 
			
		||||
        const roles = {{ .Roles }};
 | 
			
		||||
    </script>
 | 
			
		||||
    <script src='/build/job.js'></script>
 | 
			
		||||
{{end}}
 | 
			
		||||
@@ -1,17 +0,0 @@
 | 
			
		||||
{{define "content"}}
 | 
			
		||||
    <div id="svelte-app"></div>
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
{{define "stylesheets"}}
 | 
			
		||||
    <link rel='stylesheet' href='/build/jobs.css'>
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
{{define "javascript"}}
 | 
			
		||||
    <script>
 | 
			
		||||
        const filterPresets = {{ .FilterPresets }};
 | 
			
		||||
        const clusterCockpitConfig = {{ .Config }};
 | 
			
		||||
        const authlevel = {{ .User.GetAuthLevel }};
 | 
			
		||||
        const roles = {{ .Roles }};
 | 
			
		||||
    </script>
 | 
			
		||||
    <script src='/build/jobs.js'></script>
 | 
			
		||||
{{end}}
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
{{define "content"}}
 | 
			
		||||
    <div id="svelte-app"></div>
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
{{define "stylesheets"}}
 | 
			
		||||
    <link rel='stylesheet' href='/build/list.css'>
 | 
			
		||||
{{end}}
 | 
			
		||||
{{define "javascript"}}
 | 
			
		||||
    <script>
 | 
			
		||||
        const listType = {{ .Infos.listType }};
 | 
			
		||||
        const filterPresets = {{ .FilterPresets }};
 | 
			
		||||
        const clusterCockpitConfig = {{ .Config }};
 | 
			
		||||
    </script>
 | 
			
		||||
    <script src='/build/list.js'></script>
 | 
			
		||||
{{end}}
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
{{define "content"}}
 | 
			
		||||
    <div id="svelte-app"></div>
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
{{define "stylesheets"}}
 | 
			
		||||
    <link rel='stylesheet' href='/build/systems.css'>
 | 
			
		||||
{{end}}
 | 
			
		||||
{{define "javascript"}}
 | 
			
		||||
    <script>
 | 
			
		||||
        const infos = {{ .Infos }};
 | 
			
		||||
        const clusterCockpitConfig = {{ .Config }};
 | 
			
		||||
    </script>
 | 
			
		||||
    <script src='/build/systems.js'></script>
 | 
			
		||||
{{end}}
 | 
			
		||||
@@ -1,17 +0,0 @@
 | 
			
		||||
{{define "content"}}
 | 
			
		||||
    <div class="container">
 | 
			
		||||
        <div class="row  justify-content-center">
 | 
			
		||||
            <div class="col-10">
 | 
			
		||||
            {{ range $tagType, $tagList := .Infos.tagmap }}
 | 
			
		||||
                <div class="my-3 p-2 bg-secondary text-white text-capitalize">
 | 
			
		||||
                {{ $tagType }}
 | 
			
		||||
                </div>
 | 
			
		||||
                {{ range $tagList }}
 | 
			
		||||
                <a class="btn btn-lg btn-warning" href="/monitoring/jobs/?tag={{ .id }}" role="button">
 | 
			
		||||
                    {{ .name }} <span class="badge bg-light text-dark">{{ .count }}</span> </a>
 | 
			
		||||
                {{end}}
 | 
			
		||||
            {{end}}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
{{end}}
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
{{define "content"}}
 | 
			
		||||
    <div id="svelte-app"></div>
 | 
			
		||||
{{end}}
 | 
			
		||||
 | 
			
		||||
{{define "stylesheets"}}
 | 
			
		||||
    <link rel='stylesheet' href='/build/user.css'>
 | 
			
		||||
{{end}}
 | 
			
		||||
{{define "javascript"}}
 | 
			
		||||
    <script>
 | 
			
		||||
        const userInfos = {{ .Infos }};
 | 
			
		||||
        const filterPresets = {{ .FilterPresets }};
 | 
			
		||||
        const clusterCockpitConfig = {{ .Config }};
 | 
			
		||||
    </script>
 | 
			
		||||
    <script src='/build/user.js'></script>
 | 
			
		||||
{{end}}
 | 
			
		||||
		Reference in New Issue
	
	Block a user