mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2025-01-13 13:09:05 +01:00
Add vscode @component comment to every svelte file, remove unused js exports
This commit is contained in:
parent
6a1cb51c2f
commit
e65100cdc8
@ -1,3 +1,10 @@
|
||||
<!--
|
||||
@component Main analysis view component
|
||||
|
||||
Properties:
|
||||
- `filterPresets Object`: Optional predefined filter values
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { init, convert2uplot } from "./utils.js";
|
||||
import { getContext, onMount } from "svelte";
|
||||
@ -286,7 +293,7 @@
|
||||
$: updateEntityConfiguration(groupSelection.key);
|
||||
$: updateCategoryConfiguration(sortSelection.key);
|
||||
|
||||
onMount(() => filterComponent.update());
|
||||
onMount(() => filterComponent.updateFilters());
|
||||
</script>
|
||||
|
||||
<Row>
|
||||
@ -312,7 +319,7 @@
|
||||
{filterPresets}
|
||||
disableClusterSelection={true}
|
||||
startTimeQuickSelect={true}
|
||||
on:update={({ detail }) => {
|
||||
on:update-filters={({ detail }) => {
|
||||
jobFilters = detail.filters;
|
||||
}}
|
||||
/>
|
||||
@ -445,7 +452,7 @@
|
||||
width={colWidth2}
|
||||
height={300}
|
||||
tiles={$rooflineQuery.data.rooflineHeatmap}
|
||||
cluster={cluster.subClusters.length == 1
|
||||
subCluster={cluster.subClusters.length == 1
|
||||
? cluster.subClusters[0]
|
||||
: null}
|
||||
maxY={rooflineMaxY}
|
||||
|
@ -1,3 +1,12 @@
|
||||
<!--
|
||||
@component Main Config Option Component, Wrapper for admin and user sub-components
|
||||
|
||||
Properties:
|
||||
- `ìsAdmin Bool!`: Is currently logged in user admin authority
|
||||
- `isApi Bool!`: Is currently logged in user api authority
|
||||
- `username String!`: Empty string if auth. is disabled, otherwise the username as string
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { Card, CardHeader, CardTitle } from "@sveltestrap/sveltestrap";
|
||||
|
||||
|
@ -1,3 +1,13 @@
|
||||
<!--
|
||||
@component Main navbar component; handles view display based on user roles
|
||||
|
||||
Properties:
|
||||
- `username String`: Empty string if auth. is disabled, otherwise the username as string
|
||||
- `authlevel Number`: The current users authentication level
|
||||
- `clusters [String]`: List of cluster names
|
||||
- `roles [Number]`: Enum containing available roles
|
||||
-->
|
||||
|
||||
<script>
|
||||
import {
|
||||
Icon,
|
||||
@ -13,10 +23,10 @@
|
||||
import NavbarLinks from "./NavbarLinks.svelte";
|
||||
import NavbarTools from "./NavbarTools.svelte";
|
||||
|
||||
export let username; // empty string if auth. is disabled, otherwise the username as string
|
||||
export let authlevel; // Integer
|
||||
export let clusters; // array of names
|
||||
export let roles; // Role Enum-Like
|
||||
export let username;
|
||||
export let authlevel;
|
||||
export let clusters;
|
||||
export let roles;
|
||||
|
||||
let isOpen = false;
|
||||
let screenSize;
|
||||
|
@ -1,3 +1,12 @@
|
||||
<!--
|
||||
@component Selector component for (footprint) metrics to be displayed as histogram
|
||||
|
||||
Properties:
|
||||
- `cluster String`: Currently selected cluster
|
||||
- `metricsInHistograms [String]`: The currently selected metrics to display as histogram
|
||||
- ìsOpen Bool`: Is selection opened
|
||||
-->
|
||||
|
||||
<script>
|
||||
import {
|
||||
Modal,
|
||||
|
@ -1,3 +1,13 @@
|
||||
<!--
|
||||
@component Main single job display component; displays plots for every metric as well as various information
|
||||
|
||||
Properties:
|
||||
- `username String`: Empty string if auth. is disabled, otherwise the username as string
|
||||
- `authlevel Number`: The current users authentication level
|
||||
- `clusters [String]`: List of cluster names
|
||||
- `roles [Number]`: Enum containing available roles
|
||||
-->
|
||||
|
||||
<script>
|
||||
import {
|
||||
init,
|
||||
@ -49,8 +59,7 @@
|
||||
|
||||
let plots = {},
|
||||
jobTags,
|
||||
statsTable,
|
||||
jobFootprint;
|
||||
statsTable
|
||||
|
||||
let missingMetrics = [],
|
||||
missingHosts = [],
|
||||
@ -235,7 +244,6 @@
|
||||
{#if $initq.data && showFootprint}
|
||||
<Col>
|
||||
<JobFootprint
|
||||
bind:this={jobFootprint}
|
||||
job={$initq.data.job}
|
||||
/>
|
||||
</Col>
|
||||
@ -292,7 +300,7 @@
|
||||
<Col>
|
||||
<Roofline
|
||||
renderTime={true}
|
||||
cluster={$initq.data.clusters
|
||||
subCluster={$initq.data.clusters
|
||||
.find((c) => c.name == $initq.data.job.cluster)
|
||||
.subClusters.find((sc) => sc.name == $initq.data.job.subCluster)}
|
||||
data={transformDataForRoofline(
|
||||
|
@ -1,5 +1,15 @@
|
||||
<!--
|
||||
@component Footprint component; Displays job.footprint data as bars in relation to thresholds
|
||||
|
||||
Properties:
|
||||
- `job Object`: The GQL job object
|
||||
- `displayTitle Bool?`: If to display cardHeader with title [Default: true]
|
||||
- `width String?`: Width of the card [Default: 'auto']
|
||||
- `height String?`: Height of the card [Default: '310px']
|
||||
-->
|
||||
|
||||
<script context="module">
|
||||
export function findJobThresholds(job, metricConfig) {
|
||||
function findJobThresholds(job, metricConfig) {
|
||||
if (!job || !metricConfig) {
|
||||
console.warn("Argument missing for findJobThresholds!");
|
||||
return null;
|
||||
@ -52,7 +62,7 @@
|
||||
import { round } from "mathjs";
|
||||
|
||||
export let job;
|
||||
export let view = "job";
|
||||
export let displayTitle = true;
|
||||
export let width = "auto";
|
||||
export let height = "310px";
|
||||
|
||||
@ -140,7 +150,7 @@
|
||||
</script>
|
||||
|
||||
<Card class="mt-1 overflow-auto" style="width: {width}; height: {height}">
|
||||
{#if view === "job"}
|
||||
{#if displayTitle}
|
||||
<CardHeader>
|
||||
<CardTitle class="mb-0 d-flex justify-content-center">
|
||||
Core Metrics Footprint
|
||||
|
@ -1,3 +1,12 @@
|
||||
<!--
|
||||
@component Main job list component
|
||||
|
||||
Properties:
|
||||
- `filterPresets Object?`: Optional predefined filter values [Default: {}]
|
||||
- `authlevel Number`: The current users authentication level
|
||||
- `roles [Number]`: Enum containing available roles
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { onMount, getContext } from "svelte";
|
||||
import { init } from "./utils.js";
|
||||
@ -43,7 +52,7 @@
|
||||
// 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());
|
||||
onMount(() => filterComponent.updateFilters());
|
||||
</script>
|
||||
|
||||
<Row>
|
||||
@ -77,11 +86,11 @@
|
||||
<Filters
|
||||
{filterPresets}
|
||||
bind:this={filterComponent}
|
||||
on:update={({ detail }) => {
|
||||
on:update-filters={({ detail }) => {
|
||||
selectedCluster = detail.filters[0]?.cluster
|
||||
? detail.filters[0].cluster.eq
|
||||
: null;
|
||||
jobList.update(detail.filters);
|
||||
jobList.queryJobs(detail.filters);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
@ -91,11 +100,14 @@
|
||||
{presetProject}
|
||||
bind:authlevel
|
||||
bind:roles
|
||||
on:update={({ detail }) => filterComponent.update(detail)}
|
||||
on:set-filter={({ detail }) => filterComponent.updateFilters(detail)}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs="2">
|
||||
<Refresher on:reload={() => jobList.refresh()} />
|
||||
<Refresher on:refresh={() => {
|
||||
jobList.refreshJobs()
|
||||
jobList.refreshAllMetrics()
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
@ -119,5 +131,5 @@
|
||||
bind:metrics
|
||||
bind:isOpen={isMetricsSelectionOpen}
|
||||
bind:showFootprint
|
||||
view="list"
|
||||
footprintSelect={true}
|
||||
/>
|
||||
|
@ -1,9 +1,14 @@
|
||||
<!--
|
||||
@component List of users or projects
|
||||
@component Main component for listing users or projects
|
||||
|
||||
Properties:
|
||||
- `type String?`: The type of list ['USER' || 'PROJECT']
|
||||
- `filterPresets Object?`: Optional predefined filter values [Default: {}]
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { init } from "./utils.js";
|
||||
import { init, scramble, scrambleNames } from "./utils.js";
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
@ -17,7 +22,6 @@
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import Filters from "./filters/Filters.svelte";
|
||||
import { queryStore, gql, getContextClient } from "@urql/svelte";
|
||||
import { scramble, scrambleNames } from "./joblist/JobInfo.svelte";
|
||||
|
||||
const {} = init();
|
||||
|
||||
@ -89,7 +93,7 @@
|
||||
return stats.filter((u) => u.id.includes(nameFilter)).sort(cmp);
|
||||
}
|
||||
|
||||
onMount(() => filterComponent.update());
|
||||
onMount(() => filterComponent.updateFilters());
|
||||
</script>
|
||||
|
||||
<Row>
|
||||
@ -113,7 +117,7 @@
|
||||
{filterPresets}
|
||||
startTimeQuickSelect={true}
|
||||
menuText="Only {type.toLowerCase()}s with jobs that match the filters will show up"
|
||||
on:update={({ detail }) => {
|
||||
on:update-filters={({ detail }) => {
|
||||
jobFilters = detail.filters;
|
||||
}}
|
||||
/>
|
||||
|
@ -1,3 +1,17 @@
|
||||
<!--
|
||||
@component Metric plot wrapper with user scope selection; used in job detail view
|
||||
|
||||
Properties:
|
||||
- `job Object`: The GQL job object
|
||||
- `metricName String`: The metrics name
|
||||
- `metricUnit Object`: The metrics GQL unit object
|
||||
- `nativeScope String`: The metrics native scope
|
||||
- `scopes [String]`: The scopes returned for this metric
|
||||
- `width Number`: Nested plot width
|
||||
- `rawData [Object]`: Metric data for all scopes returned for this metric
|
||||
- `isShared Bool?`: If this job used shared resources; will adapt threshold indicators accordingly in downstream plots [Default: false]
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import Timeseries from "./plots/MetricPlot.svelte";
|
||||
@ -43,13 +57,6 @@
|
||||
(series) => selectedHost == null || series.hostname == selectedHost,
|
||||
);
|
||||
|
||||
let from = null,
|
||||
to = null;
|
||||
export function setTimeRange(f, t) {
|
||||
(from = f), (to = t);
|
||||
}
|
||||
|
||||
$: if (plot != null) plot.setTimeRange(from, to);
|
||||
$: if (selectedScope == "load-all") dispatch("load-all");
|
||||
</script>
|
||||
|
||||
|
@ -1,10 +1,14 @@
|
||||
<!--
|
||||
@component
|
||||
@component Metric selector component; allows reorder via drag and drop
|
||||
|
||||
Properties:
|
||||
- metrics: [String] (changes from inside, needs to be initialised, list of selected metrics)
|
||||
- isOpen: Boolean (can change from inside and outside)
|
||||
- configName: String (constant)
|
||||
- `metrics [String]`: (changes from inside, needs to be initialised, list of selected metrics)
|
||||
- `isOpen Bool`: (can change from inside and outside)
|
||||
- `configName String`: The config key for the last saved selection (constant)
|
||||
- `allMetrics [String]?`: List of all available metrics [Default: null]
|
||||
- `cluster String?`: The currently selected cluster [Default: null]
|
||||
- `showFootprint Bool?`: Upstream state of wether to render footpritn card [Default: false]
|
||||
- `footprintSelect Bool?`: Render checkbox for footprint display in upstream component [Default: false]
|
||||
-->
|
||||
|
||||
<script>
|
||||
@ -25,7 +29,7 @@
|
||||
export let allMetrics = null;
|
||||
export let cluster = null;
|
||||
export let showFootprint = false;
|
||||
export let view = "job";
|
||||
export let footprintSelect = false;
|
||||
|
||||
const onInit = getContext("on-init")
|
||||
const globalMetrics = getContext("globalMetrics")
|
||||
@ -131,7 +135,7 @@
|
||||
<ModalHeader>Configure columns (Metric availability shown)</ModalHeader>
|
||||
<ModalBody>
|
||||
<ListGroup>
|
||||
{#if view === "list"}
|
||||
{#if footprintSelect}
|
||||
<li class="list-group-item">
|
||||
<input type="checkbox" bind:checked={pendingShowFootprint} /> Show Footprint
|
||||
</li>
|
||||
|
@ -1,3 +1,11 @@
|
||||
<!--
|
||||
@component Navbar component; renders in app navigation links as received from upstream
|
||||
|
||||
Properties:
|
||||
- `clusters [String]`: List of cluster names
|
||||
- `links [Object]`: Pre-filtered link objects based on user auth
|
||||
-->
|
||||
|
||||
<script>
|
||||
import {
|
||||
Icon,
|
||||
@ -8,8 +16,8 @@
|
||||
DropdownItem,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let clusters; // array of names
|
||||
export let links; // array of nav links
|
||||
export let clusters;
|
||||
export let links;
|
||||
</script>
|
||||
|
||||
{#each links as item}
|
||||
|
@ -1,3 +1,13 @@
|
||||
<!--
|
||||
@component Navbar component; renders in app resource links and user dropdown
|
||||
|
||||
Properties:
|
||||
- `username String!`: Empty string if auth. is disabled, otherwise the username as string
|
||||
- `authlevel Number`: The current users authentication level
|
||||
- `roles [Number]`: Enum containing available roles
|
||||
- `screenSize Number`: The current window size, will trigger different render variants
|
||||
-->
|
||||
|
||||
<script>
|
||||
import {
|
||||
Icon,
|
||||
@ -12,10 +22,10 @@
|
||||
Col,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
|
||||
export let username; // empty string if auth. is disabled, otherwise the username as string
|
||||
export let authlevel; // Integer
|
||||
export let roles; // Role Enum-Like
|
||||
export let screenSize; // screensize
|
||||
export let username;
|
||||
export let authlevel;
|
||||
export let roles;
|
||||
export let screenSize;
|
||||
</script>
|
||||
|
||||
<Nav navbar>
|
||||
|
@ -1,3 +1,13 @@
|
||||
<!--
|
||||
@component System-View subcomponent; renders all current metrics for specified node
|
||||
|
||||
Properties:
|
||||
- `cluster String`: Currently selected cluster
|
||||
- `hostname String`: Currently selected host (== node)
|
||||
- `from Date?`: Custom Time Range selection 'from' [Default: null]
|
||||
- `to Date?`: Custom Time Range selection 'to' [Default: null]
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { init, checkMetricDisabled } from "./utils.js";
|
||||
import {
|
||||
@ -141,7 +151,7 @@
|
||||
</Col>
|
||||
<Col>
|
||||
<Refresher
|
||||
on:reload={() => {
|
||||
on:refresh={() => {
|
||||
const diff = Date.now() - to;
|
||||
from = new Date(from.getTime() + diff);
|
||||
to = new Date(to.getTime() + diff);
|
||||
|
@ -1,3 +1,12 @@
|
||||
<!--
|
||||
@component Analysis-View subcomponent; allows selection for normalized histograms and scatterplots
|
||||
|
||||
Properties:
|
||||
- `availableMetrics [String]`: Available metrics in selected cluster
|
||||
- `metricsInHistograms [String]`: The currently selected metrics to display as histogram
|
||||
- `metricsInScatterplots [[String, String]]`: The currently selected metrics to display as scatterplot
|
||||
-->
|
||||
|
||||
<script>
|
||||
import {
|
||||
Modal,
|
||||
|
@ -1,9 +1,11 @@
|
||||
<!--
|
||||
@component
|
||||
@component Organized display of plots as table
|
||||
|
||||
Properties:
|
||||
- itemsPerRow: Number
|
||||
- items: [Any]
|
||||
- `itemsPerRow Number`: Elements to render per row
|
||||
- `items [Any]`: List of plot components to render
|
||||
- `padding Number`: Padding between plot elements
|
||||
- `renderFor String`: If 'job', filter disabled metrics
|
||||
-->
|
||||
|
||||
<script>
|
||||
|
@ -1,3 +1,11 @@
|
||||
<!--
|
||||
@component Job-View subcomponent; display table of metric data statistics with selectable scopes
|
||||
|
||||
Properties:
|
||||
- `job Object`: The job object
|
||||
- `jobMetrics [Object]`: The jobs metricdata
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
import {
|
||||
@ -52,7 +60,7 @@
|
||||
};
|
||||
}
|
||||
|
||||
export function sortBy(metric, stat) {
|
||||
function sortBy(metric, stat) {
|
||||
let s = sorting[metric][stat];
|
||||
if (s.active) {
|
||||
s.dir = s.dir == "up" ? "down" : "up";
|
||||
|
@ -1,3 +1,13 @@
|
||||
<!--
|
||||
@component Job-View subcomponent; Single Statistics entry component fpr statstable
|
||||
|
||||
Properties:
|
||||
- `host String`: The hostname (== node)
|
||||
- `metric String`: The metric name
|
||||
- `scope String`: The selected scope
|
||||
- `jobMetrics [Object]`: The jobs metricdata
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { Icon } from "@sveltestrap/sveltestrap";
|
||||
|
||||
|
@ -1,3 +1,10 @@
|
||||
<!--
|
||||
@component Main cluster status view component; renders current system-usage information
|
||||
|
||||
Properties:
|
||||
- `cluster String`: The cluster to show status information for
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
import Refresher from "./joblist/Refresher.svelte";
|
||||
@ -331,7 +338,7 @@
|
||||
<Col class="mt-2 mt-md-0">
|
||||
<Refresher
|
||||
initially={120}
|
||||
on:reload={() => {
|
||||
on:refresh={() => {
|
||||
from = new Date(Date.now() - 5 * 60 * 1000);
|
||||
to = new Date(Date.now());
|
||||
}}
|
||||
@ -442,7 +449,7 @@
|
||||
allowSizeChange={true}
|
||||
width={plotWidths[i] - 10}
|
||||
height={300}
|
||||
cluster={subCluster}
|
||||
subCluster={subCluster}
|
||||
data={transformPerNodeDataForRoofline(
|
||||
$mainQuery.data.nodeMetrics.filter(
|
||||
(data) => data.subCluster == subCluster.name,
|
||||
|
@ -1,3 +1,12 @@
|
||||
<!--
|
||||
@component Main cluster metric status view component; renders current state of metrics / nodes
|
||||
|
||||
Properties:
|
||||
- `cluster String`: The cluster to show status information for
|
||||
- `from Date?`: Custom Time Range selection 'from' [Default: null]
|
||||
- `to Date?`: Custom Time Range selection 'to' [Default: null]
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { init, checkMetricDisabled } from "./utils.js";
|
||||
import Refresher from "./joblist/Refresher.svelte";
|
||||
@ -103,7 +112,7 @@
|
||||
{:else}
|
||||
<Col>
|
||||
<Refresher
|
||||
on:reload={() => {
|
||||
on:refresh={() => {
|
||||
const diff = Date.now() - to;
|
||||
from = new Date(from.getTime() + diff);
|
||||
to = new Date(to.getTime() + diff);
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
@component
|
||||
@component Single tag pill component
|
||||
|
||||
Properties:
|
||||
- id: ID! (if the tag-id is known but not the tag type/name, this can be used)
|
||||
|
@ -1,3 +1,10 @@
|
||||
<!--
|
||||
@component Job View Subcomponent; allows management of job tags by deletion or new entries
|
||||
|
||||
Properties:
|
||||
- `job Object`: The job object
|
||||
- `jobTags [Number]`: The array of currently designated tags
|
||||
-->
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
import { gql, getContextClient, mutationStore } from "@urql/svelte";
|
||||
|
@ -1,6 +1,14 @@
|
||||
<!--
|
||||
@component Main user jobs list display component; displays job list and additional information for a given user
|
||||
|
||||
Properties:
|
||||
- `user Object`: The GraphQL user object
|
||||
- `filterPresets Object`: Optional predefined filter values
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { onMount, getContext } from "svelte";
|
||||
import { init, convert2uplot } from "./utils.js";
|
||||
import { init, convert2uplot, scramble, scrambleNames } from "./utils.js";
|
||||
import {
|
||||
Table,
|
||||
Row,
|
||||
@ -20,7 +28,6 @@
|
||||
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();
|
||||
|
||||
@ -84,7 +91,7 @@
|
||||
variables: { jobFilters, metricsInHistograms },
|
||||
});
|
||||
|
||||
onMount(() => filterComponent.update());
|
||||
onMount(() => filterComponent.updateFilters());
|
||||
</script>
|
||||
|
||||
<Row>
|
||||
@ -124,22 +131,25 @@
|
||||
{filterPresets}
|
||||
startTimeQuickSelect={true}
|
||||
bind:this={filterComponent}
|
||||
on:update={({ detail }) => {
|
||||
on:update-filters={({ detail }) => {
|
||||
jobFilters = [...detail.filters, { user: { eq: user.username } }];
|
||||
selectedCluster = jobFilters[0]?.cluster
|
||||
? jobFilters[0].cluster.eq
|
||||
: null;
|
||||
jobList.update(jobFilters);
|
||||
jobList.queryJobs(jobFilters);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs="auto" style="margin-left: auto;">
|
||||
<TextFilter
|
||||
on:update={({ detail }) => filterComponent.update(detail)}
|
||||
on:set-filter={({ detail }) => filterComponent.updateFilters(detail)}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs="auto">
|
||||
<Refresher on:reload={() => jobList.refresh()} />
|
||||
<Refresher on:refresh={() => {
|
||||
jobList.refreshJobs()
|
||||
jobList.refreshAllMetrics()
|
||||
}} />
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
@ -273,7 +283,7 @@
|
||||
bind:metrics
|
||||
bind:isOpen={isMetricsSelectionOpen}
|
||||
bind:showFootprint
|
||||
view="list"
|
||||
footprintSelect={true}
|
||||
/>
|
||||
|
||||
<HistogramSelection
|
||||
|
@ -1,3 +1,7 @@
|
||||
<!--
|
||||
@component Admin settings wrapper
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { Row, Col } from "@sveltestrap/sveltestrap";
|
||||
import { onMount } from "svelte";
|
||||
|
@ -1,3 +1,11 @@
|
||||
<!--
|
||||
@component User settings wrapper
|
||||
|
||||
Properties:
|
||||
- `username String!`: Empty string if auth. is disabled, otherwise the username as string
|
||||
- `isApi Bool!`: Is currently logged in user api authority
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { getContext } from "svelte";
|
||||
import UserOptions from "./user/UserOptions.svelte";
|
||||
@ -41,6 +49,6 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<UserOptions config={ccconfig} {username} {isApi} bind:message bind:displayMessage on:update={(e) => handleSettingSubmit(e)}/>
|
||||
<PlotRenderOptions config={ccconfig} bind:message bind:displayMessage on:update={(e) => handleSettingSubmit(e)}/>
|
||||
<PlotColorScheme config={ccconfig} bind:message bind:displayMessage on:update={(e) => handleSettingSubmit(e)}/>
|
||||
<UserOptions config={ccconfig} {username} {isApi} bind:message bind:displayMessage on:update-config={(e) => handleSettingSubmit(e)}/>
|
||||
<PlotRenderOptions config={ccconfig} bind:message bind:displayMessage on:update-config={(e) => handleSettingSubmit(e)}/>
|
||||
<PlotColorScheme config={ccconfig} bind:message bind:displayMessage on:update-config={(e) => handleSettingSubmit(e)}/>
|
||||
|
@ -1,3 +1,13 @@
|
||||
<!--
|
||||
@component User creation form card
|
||||
|
||||
Properties:
|
||||
- `roles [String]!`: List of roles used in app as strings
|
||||
|
||||
Events:
|
||||
- `reload`: Trigger upstream reload of user list after user creation
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { Button, Card, CardTitle } from "@sveltestrap/sveltestrap";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
@ -8,7 +18,7 @@
|
||||
let message = { msg: "", color: "#d63384" };
|
||||
let displayMessage = false;
|
||||
|
||||
export let roles = [];
|
||||
export let roles;
|
||||
|
||||
async function handleUserSubmit() {
|
||||
let form = document.querySelector("#create-user-form");
|
||||
|
@ -1,3 +1,10 @@
|
||||
<!--
|
||||
@component User managed project edit form card
|
||||
|
||||
Events:
|
||||
- `reload`: Trigger upstream reload of user list after project update
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
@ -1,3 +1,13 @@
|
||||
<!--
|
||||
@component User role edit form card
|
||||
|
||||
Properties:
|
||||
- `roles [String]!`: List of roles used in app as strings
|
||||
|
||||
Events:
|
||||
- `reload`: Trigger upstream reload of user list after role edit
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { Card, CardTitle, CardBody } from "@sveltestrap/sveltestrap";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
@ -8,7 +18,7 @@
|
||||
let message = { msg: "", color: "#d63384" };
|
||||
let displayMessage = false;
|
||||
|
||||
export let roles = [];
|
||||
export let roles;
|
||||
|
||||
async function handleAddRole() {
|
||||
const username = document.querySelector("#role-username").value;
|
||||
|
@ -1,3 +1,7 @@
|
||||
<!--
|
||||
@component Admin option select card
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { Card, CardBody, CardTitle } from "@sveltestrap/sveltestrap";
|
||||
|
@ -1,3 +1,13 @@
|
||||
<!--
|
||||
@component User management table
|
||||
|
||||
Properties:
|
||||
- `users [Object]?`: List of users
|
||||
|
||||
Events:
|
||||
- `reload`: Trigger upstream reload of user list
|
||||
-->
|
||||
|
||||
<script>
|
||||
import {
|
||||
Button,
|
||||
|
@ -1,3 +1,11 @@
|
||||
<!--
|
||||
@component User data row for table
|
||||
|
||||
Properties:
|
||||
- `user Object!`: User Object
|
||||
- {username: String, name: String, roles: [String], projects: String, email: String}
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { Button } from "@sveltestrap/sveltestrap";
|
||||
import { fetchJwt } from "../../utils.js"
|
||||
|
@ -1,3 +1,15 @@
|
||||
<!--
|
||||
@component Plot color scheme selection for users
|
||||
|
||||
Properties:
|
||||
- `config Object`: Current cc-config
|
||||
- `message Object`: Message to display on success or error
|
||||
- `displayMessage Bool`: If to display message content
|
||||
|
||||
Events:
|
||||
- `update-config, {selector: String, target: String}`: Trigger upstream update of the config option
|
||||
-->
|
||||
|
||||
<script>
|
||||
import {
|
||||
Table,
|
||||
@ -15,7 +27,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
function updateSetting(selector, target) {
|
||||
dispatch('update', {
|
||||
dispatch('update-config', {
|
||||
selector: selector,
|
||||
target: target
|
||||
});
|
||||
|
@ -1,3 +1,15 @@
|
||||
<!--
|
||||
@component Plot render option selection for users
|
||||
|
||||
Properties:
|
||||
- `config Object`: Current cc-config
|
||||
- `message Object`: Message to display on success or error
|
||||
- `displayMessage Bool`: If to display message content
|
||||
|
||||
Events:
|
||||
- `update-config, {selector: String, target: String}`: Trigger upstream update of the config option
|
||||
-->
|
||||
|
||||
<script>
|
||||
import {
|
||||
Button,
|
||||
@ -15,7 +27,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
function updateSetting(selector, target) {
|
||||
dispatch('update', {
|
||||
dispatch('update-config', {
|
||||
selector: selector,
|
||||
target: target
|
||||
});
|
||||
|
@ -1,3 +1,17 @@
|
||||
<!--
|
||||
@component General option selection for users
|
||||
|
||||
Properties:
|
||||
- `config Object`: Current cc-config
|
||||
- `message Object`: Message to display on success or error
|
||||
- `displayMessage Bool`: If to display message content
|
||||
- `username String!`: Empty string if auth. is disabled, otherwise the username as string
|
||||
- `isApi Bool!`: Is currently logged in user api authority
|
||||
|
||||
Events:
|
||||
- `update-config, {selector: String, target: String}`: Trigger upstream update of the config option
|
||||
-->
|
||||
|
||||
<script>
|
||||
import {
|
||||
Button,
|
||||
@ -37,7 +51,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
function updateSetting(selector, target) {
|
||||
dispatch('update', {
|
||||
dispatch('update-config', {
|
||||
selector: selector,
|
||||
target: target
|
||||
});
|
||||
|
@ -1,3 +1,17 @@
|
||||
<!--
|
||||
@component Filter sub-component for selecting cluster and subCluster
|
||||
|
||||
Properties:
|
||||
- `disableClusterSelection Bool?`: Is the selection disabled [Default: false]
|
||||
- `isModified Bool?`: Is this filter component modified [Default: false]
|
||||
- `isOpen Bool?`: Is this filter component opened [Default: false]
|
||||
- `cluster String?`: The currently selected cluster [Default: null]
|
||||
- `partition String?`: The currently selected partition (i.e. subCluster) [Default: null]
|
||||
|
||||
Events:
|
||||
- `set-filter, {String?, String?}`: Set 'cluster, subCluster' filter in upstream component
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import {
|
||||
@ -78,7 +92,7 @@
|
||||
isOpen = false;
|
||||
cluster = pendingCluster;
|
||||
partition = pendingPartition;
|
||||
dispatch("update", { cluster, partition });
|
||||
dispatch("set-filter", { cluster, partition });
|
||||
}}>Close & Apply</Button
|
||||
>
|
||||
<Button
|
||||
@ -87,7 +101,7 @@
|
||||
isOpen = false;
|
||||
cluster = pendingCluster = null;
|
||||
partition = pendingPartition = null;
|
||||
dispatch("update", { cluster, partition });
|
||||
dispatch("set-filter", { cluster, partition });
|
||||
}}>Reset</Button
|
||||
>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
|
@ -11,6 +11,7 @@ Changes: remove dependency, text inputs, configurable value ranges, on:change ev
|
||||
- max: Number
|
||||
- firstSlider: Number (Starting position of slider #1)
|
||||
- secondSlider: Number (Starting position of slider #2)
|
||||
|
||||
Events:
|
||||
- `change`: [Number, Number] (Positions of the two sliders)
|
||||
-->
|
||||
|
@ -1,3 +1,17 @@
|
||||
<!--
|
||||
@component Filter sub-component for selecting job duration
|
||||
|
||||
Properties:
|
||||
- `isOpen Bool?`: Is this filter component opened [Default: false]
|
||||
- `lessThan Number?`: Amount of seconds [Default: null]
|
||||
- `moreThan Number?`: Amount of seconds [Default: null]
|
||||
- `from Number?`: Epoch time in seconds [Default: null]
|
||||
- `to Number?`: Epoch time in seconds [Default: null]
|
||||
|
||||
Events:
|
||||
- `set-filter, {Number, Number, Number, Number}`: Set 'lessThan, moreThan, from, to' filter in upstream component
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import {
|
||||
@ -212,7 +226,7 @@
|
||||
moreThan = hoursAndMinsToSecs(pendingMoreThan);
|
||||
from = hoursAndMinsToSecs(pendingFrom);
|
||||
to = hoursAndMinsToSecs(pendingTo);
|
||||
dispatch("update", { lessThan, moreThan, from, to });
|
||||
dispatch("set-filter", { lessThan, moreThan, from, to });
|
||||
}}
|
||||
>
|
||||
Close & Apply
|
||||
@ -236,7 +250,7 @@
|
||||
from = null;
|
||||
to = null;
|
||||
reset();
|
||||
dispatch("update", { lessThan, moreThan, from, to });
|
||||
dispatch("set-filter", { lessThan, moreThan, from, to });
|
||||
}}>Reset Filter</Button
|
||||
>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
|
@ -1,14 +1,19 @@
|
||||
<!--
|
||||
@component
|
||||
@component Main filter component; handles filter object on sub-component changes before dispatching it
|
||||
|
||||
Properties:
|
||||
- menuText: String? (Optional text to show in the dropdown menu)
|
||||
- filterPresets: Object? (Optional predefined filter values)
|
||||
- `menuText String?`: Optional text to show in the dropdown menu [Default: null]
|
||||
- `filterPresets Object?`: Optional predefined filter values [Default: {}]
|
||||
- `disableClusterSelection Bool?`: Is the selection disabled [Default: false]
|
||||
- `startTimeQuickSelect Bool?`: Render startTime quick selections [Default: false]
|
||||
|
||||
Events:
|
||||
- 'update': The detail's 'filters' prop are new filter items to be applied
|
||||
- `update-filters, {filters: [Object]?}`: The detail's 'filters' prop are new filter items to be applied
|
||||
|
||||
Functions:
|
||||
- void update(additionalFilters: Object?): Triggers an update
|
||||
- `void updateFilters (additionalFilters: Object?)`: Handles new filters from nested components, triggers upstream update event
|
||||
-->
|
||||
|
||||
<script>
|
||||
import {
|
||||
Row,
|
||||
@ -29,7 +34,6 @@
|
||||
import Duration from "./Duration.svelte";
|
||||
import Resources from "./Resources.svelte";
|
||||
import Statistics from "./Stats.svelte";
|
||||
// import TimeSelection from './TimeSelection.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@ -83,7 +87,7 @@
|
||||
isAccsModified = false;
|
||||
|
||||
// Can be called from the outside to trigger a 'update' event from this component.
|
||||
export function update(additionalFilters = null) {
|
||||
export function updateFilters(additionalFilters = null) {
|
||||
if (additionalFilters != null)
|
||||
for (let key in additionalFilters) filters[key] = additionalFilters[key];
|
||||
|
||||
@ -139,7 +143,7 @@
|
||||
if (filters.stats.length != 0)
|
||||
items.push({ metricStats: filters.stats.map((st) => { return { metricName: st.field, range: { from: st.from, to: st.to }} }) });
|
||||
|
||||
dispatch("update", { filters: items });
|
||||
dispatch("update-filters", { filters: items });
|
||||
changeURL();
|
||||
return items;
|
||||
}
|
||||
@ -249,7 +253,7 @@
|
||||
).toISOString();
|
||||
filters.startTime.to = new Date(Date.now()).toISOString();
|
||||
(filters.startTime.text = text), (filters.startTime.url = url);
|
||||
update();
|
||||
updateFilters();
|
||||
}}
|
||||
>
|
||||
<Icon name="calendar-range" />
|
||||
@ -363,23 +367,23 @@
|
||||
bind:isOpen={isClusterOpen}
|
||||
bind:cluster={filters.cluster}
|
||||
bind:partition={filters.partition}
|
||||
on:update={() => update()}
|
||||
on:set-filter={() => updateFilters()}
|
||||
/>
|
||||
|
||||
<JobStates
|
||||
bind:isOpen={isJobStatesOpen}
|
||||
bind:states={filters.states}
|
||||
on:update={() => update()}
|
||||
on:set-filter={() => updateFilters()}
|
||||
/>
|
||||
|
||||
<StartTime
|
||||
bind:isOpen={isStartTimeOpen}
|
||||
bind:from={filters.startTime.from}
|
||||
bind:to={filters.startTime.to}
|
||||
on:update={() => {
|
||||
on:set-filter={() => {
|
||||
delete filters.startTime["text"];
|
||||
delete filters.startTime["url"];
|
||||
update();
|
||||
updateFilters();
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -389,13 +393,13 @@
|
||||
bind:moreThan={filters.duration.moreThan}
|
||||
bind:from={filters.duration.from}
|
||||
bind:to={filters.duration.to}
|
||||
on:update={() => update()}
|
||||
on:set-filter={() => updateFilters()}
|
||||
/>
|
||||
|
||||
<Tags
|
||||
bind:isOpen={isTagsOpen}
|
||||
bind:tags={filters.tags}
|
||||
on:update={() => update()}
|
||||
on:set-filter={() => updateFilters()}
|
||||
/>
|
||||
|
||||
<Resources
|
||||
@ -408,13 +412,13 @@
|
||||
bind:isNodesModified
|
||||
bind:isHwthreadsModified
|
||||
bind:isAccsModified
|
||||
on:update={() => update()}
|
||||
on:set-filter={() => updateFilters()}
|
||||
/>
|
||||
|
||||
<Statistics
|
||||
bind:isOpen={isStatsOpen}
|
||||
bind:stats={filters.stats}
|
||||
on:update={() => update()}
|
||||
on:set-filter={() => updateFilters()}
|
||||
/>
|
||||
|
||||
<style>
|
||||
|
@ -1,3 +1,11 @@
|
||||
<!--
|
||||
@component Info pill displayed for active filters
|
||||
|
||||
Properties:
|
||||
- `icon String`: Sveltestrap icon name
|
||||
- `modified Bool?`: Optional if filter is modified [Default: false]
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { Button, Icon } from "@sveltestrap/sveltestrap";
|
||||
|
||||
|
@ -1,3 +1,18 @@
|
||||
<!--
|
||||
@component Filter sub-component for selecting job states
|
||||
|
||||
Properties:
|
||||
- `isModified Bool?`: Is this filter component modified [Default: false]
|
||||
- `isOpen Bool?`: Is this filter component opened [Default: false]
|
||||
- `states [String]?`: The currently selected states [Default: [...allJobStates]]
|
||||
|
||||
Events:
|
||||
- `set-filter, {[String]}`: Set 'states' filter in upstream component
|
||||
|
||||
Exported:
|
||||
- `const allJobStates [String]`: List of all available job states used in cc-backend
|
||||
-->
|
||||
|
||||
<script context="module">
|
||||
export const allJobStates = [
|
||||
"running",
|
||||
@ -59,7 +74,7 @@
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
states = [...pendingStates];
|
||||
dispatch("update", { states });
|
||||
dispatch("set-filter", { states });
|
||||
}}>Close & Apply</Button
|
||||
>
|
||||
<Button
|
||||
@ -68,7 +83,7 @@
|
||||
isOpen = false;
|
||||
states = [...allJobStates];
|
||||
pendingStates = [...allJobStates];
|
||||
dispatch("update", { states });
|
||||
dispatch("set-filter", { states });
|
||||
}}>Reset</Button
|
||||
>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
|
@ -1,3 +1,21 @@
|
||||
<!--
|
||||
@component Filter sub-component for selecting job resources
|
||||
|
||||
Properties:
|
||||
- `cluster Object?`: The currently selected cluster config [Default: null]
|
||||
- `isOpen Bool?`: Is this filter component opened [Default: false]
|
||||
- `numNodes Object?`: The currently selected numNodes filter [Default: {from:null, to:null}]
|
||||
- `numHWThreads Object?`: The currently selected numHWTreads filter [Default: {from:null, to:null}]
|
||||
- `numAccelerators Object?`: The currently selected numAccelerators filter [Default: {from:null, to:null}]
|
||||
- `isNodesModified Bool?`: Is the node filter modified [Default: false]
|
||||
- `isHwtreadsModified Bool?`: Is the Hwthreads filter modified [Default: false]
|
||||
- `isAccsModified Bool?`: Is the Accelerator filter modified [Default: false]
|
||||
- `namedNode String?`: The currently selected single named node (= hostname) [Default: null]
|
||||
|
||||
Events:
|
||||
- `set-filter, {Object, Object, Object, String}`: Set 'numNodes, numHWThreads, numAccelerators, namedNode' filter in upstream component
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import {
|
||||
@ -197,7 +215,7 @@
|
||||
to: pendingNumAccelerators.to,
|
||||
};
|
||||
namedNode = pendingNamedNode;
|
||||
dispatch("update", {
|
||||
dispatch("set-filter", {
|
||||
numNodes,
|
||||
numHWThreads,
|
||||
numAccelerators,
|
||||
@ -228,7 +246,7 @@
|
||||
isHwthreadsModified = false;
|
||||
isAccsModified = false;
|
||||
namedNode = pendingNamedNode;
|
||||
dispatch("update", {
|
||||
dispatch("set-filter", {
|
||||
numNodes,
|
||||
numHWThreads,
|
||||
numAccelerators,
|
||||
|
@ -1,3 +1,16 @@
|
||||
<!--
|
||||
@component Filter sub-component for selecting job starttime
|
||||
|
||||
Properties:
|
||||
- `isModified Bool?`: Is this filter component modified [Default: false]
|
||||
- `isOpen Bool?`: Is this filter component opened [Default: false]
|
||||
- `from Object?`: The currently selected from startime [Default: null]
|
||||
- `to Object?`: The currently selected to starttime (i.e. subCluster) [Default: null]
|
||||
|
||||
Events:
|
||||
- `set-filter, {String?, String?}`: Set 'from, to' filter in upstream component
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { parse, format, sub } from "date-fns";
|
||||
@ -101,7 +114,7 @@
|
||||
isOpen = false;
|
||||
from = toRFC3339(pendingFrom);
|
||||
to = toRFC3339(pendingTo, "59");
|
||||
dispatch("update", { from, to });
|
||||
dispatch("set-filter", { from, to });
|
||||
}}
|
||||
>
|
||||
Close & Apply
|
||||
@ -113,7 +126,7 @@
|
||||
from = null;
|
||||
to = null;
|
||||
reset();
|
||||
dispatch("update", { from, to });
|
||||
dispatch("set-filter", { from, to });
|
||||
}}>Reset</Button
|
||||
>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
|
@ -1,3 +1,15 @@
|
||||
<!--
|
||||
@component Filter sub-component for selecting job statistics
|
||||
|
||||
Properties:
|
||||
- `isModified Bool?`: Is this filter component modified [Default: false]
|
||||
- `isOpen Bool?`: Is this filter component opened [Default: false]
|
||||
- `stats [Object]?`: The currently selected statistics filter [Default: []]
|
||||
|
||||
Events:
|
||||
- `set-filter, {[Object]}`: Set 'stats' filter in upstream component
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import { getStatsItems } from "../utils.js";
|
||||
@ -66,7 +78,7 @@
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
stats = statistics.filter((stat) => stat.enabled);
|
||||
dispatch("update", { stats });
|
||||
dispatch("set-filter", { stats });
|
||||
}}>Close & Apply</Button
|
||||
>
|
||||
<Button
|
||||
@ -75,7 +87,7 @@
|
||||
isOpen = false;
|
||||
resetRanges();
|
||||
stats = [];
|
||||
dispatch("update", { stats });
|
||||
dispatch("set-filter", { stats });
|
||||
}}>Reset</Button
|
||||
>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
|
@ -1,3 +1,15 @@
|
||||
<!--
|
||||
@component Filter sub-component for selecting tags
|
||||
|
||||
Properties:
|
||||
- `isModified Bool?`: Is this filter component modified [Default: false]
|
||||
- `isOpen Bool?`: Is this filter component opened [Default: false]
|
||||
- `tags [Number]?`: The currently selected tags (as IDs) [Default: []]
|
||||
|
||||
Events:
|
||||
- `set-filter, {[Number]}`: Set 'tag' filter in upstream component
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { createEventDispatcher, getContext } from "svelte";
|
||||
import {
|
||||
@ -72,7 +84,7 @@
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
tags = [...pendingTags];
|
||||
dispatch("update", { tags });
|
||||
dispatch("set-filter", { tags });
|
||||
}}>Close & Apply</Button
|
||||
>
|
||||
<Button
|
||||
@ -81,7 +93,7 @@
|
||||
isOpen = false;
|
||||
tags = [];
|
||||
pendingTags = [];
|
||||
dispatch("update", { tags });
|
||||
dispatch("set-filter", { tags });
|
||||
}}>Reset</Button
|
||||
>
|
||||
<Button on:click={() => (isOpen = false)}>Close</Button>
|
||||
|
@ -1,7 +1,19 @@
|
||||
<!--
|
||||
@component Search Field for Job-Lists with separate mode if project filter is active
|
||||
|
||||
Properties:
|
||||
- `presetProject String?`: Currently active project filter [Default: '']
|
||||
- `authlevel Number?`: The current users authentication level [Default: null]
|
||||
- `roles [Number]?`: Enum containing available roles [Default: null]
|
||||
|
||||
Events:
|
||||
- `set-filter, {String?, String?, String?}`: Set 'user, project, jobName' filter in upstream component
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { InputGroup, Input, Button, Icon } from "@sveltestrap/sveltestrap";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { scramble, scrambleNames } from "../joblist/JobInfo.svelte";
|
||||
import { scramble, scrambleNames } from "../utils.js";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@ -40,7 +52,7 @@
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
dispatch("update", {
|
||||
dispatch("set-filter", {
|
||||
user,
|
||||
project,
|
||||
jobName
|
||||
@ -53,7 +65,7 @@
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
dispatch("update", {
|
||||
dispatch("set-filter", {
|
||||
project,
|
||||
jobName
|
||||
});
|
||||
|
@ -1,3 +1,16 @@
|
||||
<!--
|
||||
@component Filter sub-component for selecting specified real time ranges for data cutoff; used in systems and nodes view
|
||||
|
||||
Properties:
|
||||
- `from Date`: The datetime to start data display from
|
||||
- `to Date`: The datetime to end data display at
|
||||
- `customEnabled Bool?`: Allow custom time window selection [Default: true]
|
||||
- `options Object? {String:Number}`: The quick time selection options [Default: {..., "Last 24hrs": 24*60*60}]
|
||||
|
||||
Events:
|
||||
- `change, {Date, Date}`: Set 'from, to' values in upstream component
|
||||
-->
|
||||
|
||||
<script>
|
||||
import {
|
||||
Icon,
|
||||
@ -10,7 +23,6 @@
|
||||
export let from;
|
||||
export let to;
|
||||
export let customEnabled = true;
|
||||
export let anyEnabled = false;
|
||||
export let options = {
|
||||
"Last quarter hour": 15 * 60,
|
||||
"Last half hour": 30 * 60,
|
||||
@ -25,21 +37,15 @@
|
||||
$: pendingTo = to;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let timeRange =
|
||||
to && from ? (to.getTime() - from.getTime()) / 1000 : anyEnabled ? -2 : -1;
|
||||
let timeRange = // If both times set, return diff, else: display custom select
|
||||
(to && from) ? ((to.getTime() - from.getTime()) / 1000) : -1;
|
||||
|
||||
function updateTimeRange(event) {
|
||||
function updateTimeRange() {
|
||||
if (timeRange == -1) {
|
||||
pendingFrom = null;
|
||||
pendingTo = null;
|
||||
return;
|
||||
}
|
||||
if (timeRange == -2) {
|
||||
from = pendingFrom = null;
|
||||
to = pendingTo = null;
|
||||
dispatch("change", { from, to });
|
||||
return;
|
||||
}
|
||||
|
||||
let now = Date.now(),
|
||||
t = timeRange * 1000;
|
||||
@ -63,9 +69,6 @@
|
||||
|
||||
<InputGroup class="inline-from">
|
||||
<InputGroupText><Icon name="clock-history" /></InputGroupText>
|
||||
<!-- <InputGroupText>
|
||||
Time
|
||||
</InputGroupText> -->
|
||||
<select
|
||||
class="form-select"
|
||||
bind:value={timeRange}
|
||||
@ -74,9 +77,6 @@
|
||||
{#if customEnabled}
|
||||
<option value={-1}>Custom</option>
|
||||
{/if}
|
||||
{#if anyEnabled}
|
||||
<option value={-2}>Any</option>
|
||||
{/if}
|
||||
{#each Object.entries(options) as [name, seconds]}
|
||||
<option value={seconds}>{name}</option>
|
||||
{/each}
|
||||
|
@ -1,25 +1,15 @@
|
||||
<!--
|
||||
@component
|
||||
@component Displays job metaData, serves links to detail pages
|
||||
|
||||
Properties:
|
||||
- job: GraphQL.Job
|
||||
- jobTags: Defaults to job.tags, usefull for dynamically updating the tags.
|
||||
- `job Object`: The Job Object (GraphQL.Job)
|
||||
- `jobTags [Number]?`: The jobs tags as IDs, default useful for dynamically updating the tags [Default: job.tags]
|
||||
-->
|
||||
<script context="module">
|
||||
export const scrambleNames = window.localStorage.getItem("cc-scramble-names");
|
||||
export const scramble = function (str) {
|
||||
if (str === "-") return str;
|
||||
else
|
||||
return [...str]
|
||||
.reduce((x, c, i) => x * 7 + c.charCodeAt(0) * i * 21, 5)
|
||||
.toString(32)
|
||||
.substr(0, 6);
|
||||
};
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Tag from "../Tag.svelte";
|
||||
import { Badge, Icon } from "@sveltestrap/sveltestrap";
|
||||
import { scrambleNames, scramble } from "../utils.js";
|
||||
|
||||
export let job;
|
||||
export let jobTags = job.tags;
|
||||
|
@ -1,13 +1,18 @@
|
||||
<!--
|
||||
@component
|
||||
@component Main jobList component; lists jobs according to set filters
|
||||
|
||||
Properties:
|
||||
- metrics: [String] (can change from outside)
|
||||
- sorting: { field: String, type: String, order: "DESC" | "ASC" } (can change from outside)
|
||||
- matchedJobs: Number (changes from inside)
|
||||
- `sorting Object?`: Currently active sorting [Default: {field: "startTime", type: "col", order: "DESC"}]
|
||||
- `matchedJobs Number?`: Number of matched jobs for selected filters [Default: 0]
|
||||
- `metrics [String]?`: The currently selected metrics [Default: User-Configured Selection]
|
||||
- `showFootprint Bool`: If to display the jobFootprint component
|
||||
|
||||
Functions:
|
||||
- update(filters?: [JobFilter])
|
||||
- `refreshJobs()`: Load jobs data with unchanged parameters and 'network-only' keyword
|
||||
- `refreshAllMetrics()`: Trigger downstream refresh of all running jobs' metric data
|
||||
- `queryJobs(filters?: [JobFilter])`: Load jobs data with new filters, starts from page 1
|
||||
-->
|
||||
|
||||
<script>
|
||||
import {
|
||||
queryStore,
|
||||
@ -35,6 +40,7 @@
|
||||
let page = 1;
|
||||
let paging = { itemsPerPage, page };
|
||||
let filter = [];
|
||||
let triggerMetricRefresh = false;
|
||||
|
||||
function getUnit(m) {
|
||||
const rawUnit = globalMetrics.find((gm) => gm.name === m)?.unit
|
||||
@ -106,7 +112,7 @@
|
||||
$: matchedJobs = $jobsStore.data != null ? $jobsStore.data.jobs.count : 0;
|
||||
|
||||
// Force refresh list with existing unchanged variables (== usually would not trigger reactivity)
|
||||
export function refresh() {
|
||||
export function refreshJobs() {
|
||||
jobsStore = queryStore({
|
||||
client: client,
|
||||
query: query,
|
||||
@ -115,8 +121,16 @@
|
||||
});
|
||||
}
|
||||
|
||||
// (Re-)query and optionally set new filters.
|
||||
export function update(filters) {
|
||||
export function refreshAllMetrics() {
|
||||
// Refresh Job Metrics (Downstream will only query for running jobs)
|
||||
triggerMetricRefresh = true
|
||||
setTimeout(function () {
|
||||
triggerMetricRefresh = false;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// (Re-)query and optionally set new filters; Query will be started reactively.
|
||||
export function queryJobs(filters) {
|
||||
if (filters != null) {
|
||||
let minRunningFor = ccconfig.plot_list_hideShortRunningJobs;
|
||||
if (minRunningFor && minRunningFor > 0) {
|
||||
@ -240,7 +254,7 @@
|
||||
</tr>
|
||||
{:else}
|
||||
{#each jobs as job (job)}
|
||||
<JobListRow {job} {metrics} {plotWidth} {showFootprint} />
|
||||
<JobListRow bind:triggerMetricRefresh {job} {metrics} {plotWidth} {showFootprint} />
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan={metrics.length + 1}> No jobs found </td>
|
||||
@ -267,7 +281,7 @@
|
||||
{itemsPerPage}
|
||||
itemText="Jobs"
|
||||
totalItems={matchedJobs}
|
||||
on:update={({ detail }) => {
|
||||
on:update-paging={({ detail }) => {
|
||||
if (detail.itemsPerPage != itemsPerPage) {
|
||||
updateConfiguration(detail.itemsPerPage.toString(), detail.page);
|
||||
} else {
|
||||
|
@ -1,12 +1,13 @@
|
||||
<!--
|
||||
@component
|
||||
@component Pagination selection component
|
||||
|
||||
Properties:
|
||||
- page: Number (changes from inside)
|
||||
- itemsPerPage: Number (changes from inside)
|
||||
- totalItems: Number (only displayed)
|
||||
|
||||
Events:
|
||||
- "update": { page: Number, itemsPerPage: Number }
|
||||
- "update-paging": { page: Number, itemsPerPage: Number }
|
||||
- Dispatched once immediately and then each time page or itemsPerPage changes
|
||||
-->
|
||||
|
||||
@ -60,7 +61,7 @@
|
||||
itemsPerPage = Number(itemsPerPage);
|
||||
}
|
||||
|
||||
dispatch("update", { itemsPerPage, page });
|
||||
dispatch("update-paging", { itemsPerPage, page });
|
||||
}
|
||||
$: backButtonDisabled = (page === 1);
|
||||
$: nextButtonDisabled = (page >= (totalItems / itemsPerPage));
|
||||
|
@ -1,8 +1,11 @@
|
||||
<!--
|
||||
@component
|
||||
@component Triggers upstream data refresh in selectable intervals
|
||||
|
||||
Properties:
|
||||
- `initially Number?`: Initial refresh interval on component mount, in seconds [Default: null]
|
||||
|
||||
Events:
|
||||
- 'reload': When fired, the parent component shoud refresh its contents
|
||||
- `refresh`: When fired, the upstream component refreshes its contents
|
||||
-->
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
@ -17,10 +20,11 @@
|
||||
|
||||
if (refreshInterval == null) return;
|
||||
|
||||
refreshIntervalId = setInterval(() => dispatch("reload"), refreshInterval);
|
||||
refreshIntervalId = setInterval(() => dispatch("refresh"), refreshInterval);
|
||||
}
|
||||
|
||||
export let initially = null;
|
||||
|
||||
if (initially != null) {
|
||||
refreshInterval = initially * 1000;
|
||||
refreshIntervalChanged();
|
||||
@ -30,17 +34,17 @@
|
||||
<InputGroup>
|
||||
<Button
|
||||
outline
|
||||
on:click={() => dispatch("reload")}
|
||||
on:click={() => dispatch("refresh")}
|
||||
disabled={refreshInterval != null}
|
||||
>
|
||||
<Icon name="arrow-clockwise" /> Reload
|
||||
<Icon name="arrow-clockwise" /> Refresh
|
||||
</Button>
|
||||
<select
|
||||
class="form-select"
|
||||
bind:value={refreshInterval}
|
||||
on:change={refreshIntervalChanged}
|
||||
>
|
||||
<option value={null}>No periodic reload</option>
|
||||
<option value={null}>No periodic refresh</option>
|
||||
<option value={30 * 1000}>Update every 30 seconds</option>
|
||||
<option value={60 * 1000}>Update every minute</option>
|
||||
<option value={2 * 60 * 1000}>Update every two minutes</option>
|
||||
|
@ -1,11 +1,13 @@
|
||||
<!--
|
||||
@component
|
||||
@component Data row for a single job displaying metric plots
|
||||
|
||||
Properties:
|
||||
- job: GraphQL.Job (constant/key)
|
||||
- metrics: [String] (can change)
|
||||
- plotWidth: Number
|
||||
- plotHeight: Number
|
||||
- `job Object`: The job object (GraphQL.Job)
|
||||
- `metrics [String]`: Currently selected metrics
|
||||
- `plotWidth Number`: Width of the sub-components
|
||||
- `plotHeight Number?`: Height of the sub-components [Default: 275]
|
||||
- `showFootprint Bool`: Display of footprint component for job
|
||||
- `triggerMetricRefresh Bool?`: If changed to true from upstream, will trigger metric query
|
||||
-->
|
||||
|
||||
<script>
|
||||
@ -22,6 +24,7 @@
|
||||
export let plotWidth;
|
||||
export let plotHeight = 275;
|
||||
export let showFootprint;
|
||||
export let triggerMetricRefresh = false;
|
||||
|
||||
let { id } = job;
|
||||
let scopes = job.numNodes == 1
|
||||
@ -69,7 +72,7 @@
|
||||
variables: { id, metrics, scopes },
|
||||
});
|
||||
|
||||
export function refresh() {
|
||||
function refreshMetrics() {
|
||||
metricsQuery = queryStore({
|
||||
client: client,
|
||||
query: query,
|
||||
@ -78,6 +81,11 @@
|
||||
});
|
||||
}
|
||||
|
||||
$: if (job.state === 'running' && triggerMetricRefresh === true) {
|
||||
refreshMetrics();
|
||||
}
|
||||
|
||||
// Helper
|
||||
const selectScope = (jobMetrics) =>
|
||||
jobMetrics.reduce(
|
||||
(a, b) =>
|
||||
@ -113,7 +121,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
if (job.monitoringStatus) refresh();
|
||||
</script>
|
||||
|
||||
<tr>
|
||||
@ -143,7 +150,7 @@
|
||||
{job}
|
||||
width={plotWidth}
|
||||
height="{plotHeight}px"
|
||||
view="list"
|
||||
displayTitle={false}
|
||||
/>
|
||||
</td>
|
||||
{/if}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
@component
|
||||
@component Selector for sorting field and direction
|
||||
|
||||
Properties:
|
||||
- sorting: { field: String, order: "DESC" | "ASC" } (changes from inside)
|
||||
|
@ -1,7 +1,16 @@
|
||||
<!--
|
||||
@component
|
||||
@component Histogram Plot based on uPlot Bars
|
||||
|
||||
Properties:
|
||||
- Todo
|
||||
- `data [[],[]]`: uPlot data structure array ( [[],[]] == [X, Y] )
|
||||
- `usesBins Bool?`: If X-Axis labels are bins ("XX-YY") [Default: false]
|
||||
- `width Number?`: Plot width (reactively adaptive) [Default: 500]
|
||||
- `height Number?`: Plot height (reactively adaptive) [Default: 300]
|
||||
- `title String?`: Plot title [Default: ""]
|
||||
- `xlabel String?`: Plot X axis label [Default: ""]
|
||||
- `xunit String?`: Plot X axis unit [Default: ""]
|
||||
- `ylabel String?`: Plot Y axis label [Default: ""]
|
||||
- `yunit String?`: Plot Y axis unit [Default: ""]
|
||||
-->
|
||||
|
||||
<script>
|
||||
@ -16,9 +25,9 @@
|
||||
export let height = 300;
|
||||
export let title = "";
|
||||
export let xlabel = "";
|
||||
export let xunit = "X";
|
||||
export let xunit = "";
|
||||
export let ylabel = "";
|
||||
export let yunit = "Y";
|
||||
export let yunit = "";
|
||||
|
||||
const { bars } = uPlot.paths;
|
||||
|
||||
|
@ -1,3 +1,26 @@
|
||||
<!--
|
||||
@component Main plot component, based on uPlot; metricdata values by time
|
||||
|
||||
Only width/height should change reactively.
|
||||
|
||||
Properties:
|
||||
- `metric String`: The metric name
|
||||
- `scope String?`: Scope of the displayed data [Default: node]
|
||||
- `resources [GraphQL.Resource]`: List of resources used for parent job
|
||||
- `width Number`: The plot width
|
||||
- `height Number`: The plot height
|
||||
- `timestep Number`: The timestep used for X-axis rendering
|
||||
- `series [GraphQL.Series]`: The metric data object
|
||||
- `useStatsSeries Bool?`: If this plot uses the statistics Min/Max/Median representation; automatically set to according bool [Default: null]
|
||||
- `statisticsSeries [GraphQL.StatisticsSeries]?`: Min/Max/Median representation of metric data [Default: null]
|
||||
- `cluster GraphQL.Cluster`: Cluster Object of the parent job
|
||||
- `subCluster String`: Name of the subCluster of the parent job
|
||||
- `isShared Bool?`: If this job used shared resources; will adapt threshold indicators accordingly [Default: false]
|
||||
- `forNode Bool?`: If this plot is used for node data display; will render x-axis as negative time with $now as maximum [Default: false]
|
||||
- `numhwthreads Number?`: Number of job HWThreads [Default: 0]
|
||||
- `numaccs Number?`: Number of job Accelerators [Default: 0]
|
||||
-->
|
||||
|
||||
<script context="module">
|
||||
function formatTime(t, forNode = false) {
|
||||
if (t !== null) {
|
||||
@ -87,28 +110,6 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--
|
||||
@component
|
||||
|
||||
Only width/height should change reactively.
|
||||
|
||||
Properties:
|
||||
- width: Number
|
||||
- height: Number
|
||||
- timestep: Number
|
||||
- series: [GraphQL.Series]
|
||||
- statisticsSeries: [GraphQL.StatisticsSeries]
|
||||
- cluster: GraphQL.Cluster
|
||||
- subCluster: String
|
||||
- metric: String
|
||||
- scope: String
|
||||
- useStatsSeries: Boolean
|
||||
|
||||
Functions:
|
||||
- setTimeRange(from, to): Void
|
||||
|
||||
// TODO: Move helper functions to module context?
|
||||
-->
|
||||
<script>
|
||||
import uPlot from "uplot";
|
||||
import { formatNumber } from "../units.js";
|
||||
@ -498,14 +499,6 @@
|
||||
|
||||
if (timeoutId != null) clearTimeout(timeoutId);
|
||||
});
|
||||
|
||||
// `from` and `to` must be numbers between 0 and 1.
|
||||
export function setTimeRange(from, to) {
|
||||
if (!uplot || from > to) return false;
|
||||
|
||||
uplot.setScale("x", { min: from * maxX, max: to * maxX });
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if series[0].data.length > 0}
|
||||
|
@ -1,3 +1,17 @@
|
||||
<!--
|
||||
@component Pie Plot based on uPlot Pie
|
||||
|
||||
Properties:
|
||||
- `size Number`: X and Y size of the plot, for square shape
|
||||
- `sliceLabel String`: Label used in segment legends
|
||||
- `quantities [Number]`: Data values
|
||||
- `entities [String]`: Data identifiers
|
||||
- `displayLegend?`: Display uPlot legend [Default: false]
|
||||
|
||||
Exported:
|
||||
- `colors ['rgb(x,y,z)', ...]`: Color range used for segments; upstream used for legend
|
||||
-->
|
||||
|
||||
<script context="module">
|
||||
// http://tsitsul.in/blog/coloropt/ : 12 colors normal
|
||||
export const colors = [
|
||||
|
@ -1,3 +1,14 @@
|
||||
<!--
|
||||
@component Polar Plot based on chartJS Radar
|
||||
|
||||
Properties:
|
||||
- `metrics [String]`: Metric names to display as polar plot
|
||||
- `cluster GraphQL.Cluster`: Cluster Object of the parent job
|
||||
- `subCluster GraphQL.SubCluster`: SubCluster Object of the parent job
|
||||
- `jobMetrics [GraphQL.JobMetricWithName]`: Metric data
|
||||
- `height Number?`: Plot height [Default: 365]
|
||||
-->
|
||||
|
||||
<script>
|
||||
import { getContext } from 'svelte'
|
||||
import { Radar } from 'svelte-chartjs';
|
||||
|
@ -1,3 +1,27 @@
|
||||
<!--
|
||||
@component Roofline Model Plot based on uPlot
|
||||
|
||||
Properties:
|
||||
- `data [null, [], []]`: Roofline Data Structure, see below for details [Default: null]
|
||||
- `renderTime Bool?`: If time information should be rendered as colored dots [Default: false]
|
||||
- `allowSizeChange Bool?`: If dimensions of rendered plot can change [Default: false]
|
||||
- `subCluster GraphQL.SubCluster?`: SubCluster Object; contains required topology information [Default: null]
|
||||
- `width Number?`: Plot width (reactively adaptive) [Default: 600]
|
||||
- `height Number?`: Plot height (reactively adaptive) [Default: 350]
|
||||
|
||||
Data Format:
|
||||
- `data = [null, [], []]`
|
||||
- Index 0: null-axis required for scatter
|
||||
- Index 1: Array of XY-Arrays for Scatter
|
||||
- Index 2: Optional Time Info
|
||||
- `data[1][0] = [100, 200, 500, ...]`
|
||||
- X Axis: Intensity (Vals up to clusters' flopRateScalar value)
|
||||
- `data[1][1] = [1000, 2000, 1500, ...]`
|
||||
- Y Axis: Performance (Vals up to clusters' flopRateSimd value)
|
||||
- `data[2] = [0.1, 0.15, 0.2, ...]`
|
||||
- Color Code: Time Information (Floats from 0 to 1) (Optional)
|
||||
-->
|
||||
|
||||
<script>
|
||||
import uPlot from "uplot";
|
||||
import { formatNumber } from "../units.js";
|
||||
@ -7,7 +31,7 @@
|
||||
export let data = null;
|
||||
export let renderTime = false;
|
||||
export let allowSizeChange = false;
|
||||
export let cluster = null;
|
||||
export let subCluster = null;
|
||||
export let width = 600;
|
||||
export let height = 350;
|
||||
|
||||
@ -17,12 +41,7 @@
|
||||
|
||||
const lineWidth = clusterCockpitConfig.plot_general_lineWidth;
|
||||
|
||||
/* Data Format
|
||||
* data = [null, [], []] // 0: null-axis required for scatter, 1: Array of XY-Array for Scatter, 2: Optional Time Info
|
||||
* data[1][0] = [100, 200, 500, ...] // X Axis -> Intensity (Vals up to clusters' flopRateScalar value)
|
||||
* data[1][1] = [1000, 2000, 1500, ...] // Y Axis -> Performance (Vals up to clusters' flopRateSimd value)
|
||||
* data[2] = [0.1, 0.15, 0.2, ...] // Color Code -> Time Information (Floats from 0 to 1) (Optional)
|
||||
*/
|
||||
|
||||
|
||||
// Helpers
|
||||
function getGradientR(x) {
|
||||
@ -189,8 +208,8 @@
|
||||
y: {
|
||||
range: [
|
||||
1.0,
|
||||
cluster?.flopRateSimd?.value
|
||||
? nearestThousand(cluster.flopRateSimd.value)
|
||||
subCluster?.flopRateSimd?.value
|
||||
? nearestThousand(subCluster.flopRateSimd.value)
|
||||
: 10000,
|
||||
],
|
||||
distr: 3, // Render as log
|
||||
@ -208,30 +227,30 @@
|
||||
],
|
||||
draw: [
|
||||
(u) => {
|
||||
// draw roofs when cluster set
|
||||
if (cluster != null) {
|
||||
// draw roofs when subCluster set
|
||||
if (subCluster != null) {
|
||||
const padding = u._padding; // [top, right, bottom, left]
|
||||
|
||||
u.ctx.strokeStyle = "black";
|
||||
u.ctx.lineWidth = lineWidth;
|
||||
u.ctx.beginPath();
|
||||
|
||||
const ycut = 0.01 * cluster.memoryBandwidth.value;
|
||||
const ycut = 0.01 * subCluster.memoryBandwidth.value;
|
||||
const scalarKnee =
|
||||
(cluster.flopRateScalar.value - ycut) /
|
||||
cluster.memoryBandwidth.value;
|
||||
(subCluster.flopRateScalar.value - ycut) /
|
||||
subCluster.memoryBandwidth.value;
|
||||
const simdKnee =
|
||||
(cluster.flopRateSimd.value - ycut) /
|
||||
cluster.memoryBandwidth.value;
|
||||
(subCluster.flopRateSimd.value - ycut) /
|
||||
subCluster.memoryBandwidth.value;
|
||||
const scalarKneeX = u.valToPos(scalarKnee, "x", true), // Value, axis, toCanvasPixels
|
||||
simdKneeX = u.valToPos(simdKnee, "x", true),
|
||||
flopRateScalarY = u.valToPos(
|
||||
cluster.flopRateScalar.value,
|
||||
subCluster.flopRateScalar.value,
|
||||
"y",
|
||||
true,
|
||||
),
|
||||
flopRateSimdY = u.valToPos(
|
||||
cluster.flopRateSimd.value,
|
||||
subCluster.flopRateSimd.value,
|
||||
"y",
|
||||
true,
|
||||
);
|
||||
|
@ -1,6 +1,14 @@
|
||||
<div class="cc-plot">
|
||||
<canvas bind:this={canvasElement} width="{prevWidth}" height="{prevHeight}"></canvas>
|
||||
</div>
|
||||
<!--
|
||||
@component Roofline Model Plot as Heatmap of multiple Jobs based on Canvas
|
||||
|
||||
Properties:
|
||||
- `subCluster GraphQL.SubCluster?`: SubCluster Object; contains required topology information [Default: null]
|
||||
- **Note**: Object of first subCluster is used, how to handle multiple topologies within one cluster? [TODO]
|
||||
- `tiles [[Float!]!]?`: Data tiles to be rendered [Default: null]
|
||||
- `maxY Number?`: maximum flopRateSimd of all subClusters [Default: null]
|
||||
- `width Number?`: Plot width (reactively adaptive) [Default: 500]
|
||||
- `height Number?`: Plot height (reactively adaptive) [Default: 300]
|
||||
-->
|
||||
|
||||
<script context="module">
|
||||
const axesColor = '#aaaaaa'
|
||||
@ -33,11 +41,11 @@
|
||||
return 2
|
||||
}
|
||||
|
||||
function render(ctx, data, cluster, width, height, defaultMaxY) {
|
||||
function render(ctx, data, subCluster, width, height, defaultMaxY) {
|
||||
if (width <= 0)
|
||||
return
|
||||
|
||||
const [minX, maxX, minY, maxY] = [0.01, 1000, 1., cluster?.flopRateSimd?.value || defaultMaxY]
|
||||
const [minX, maxX, minY, maxY] = [0.01, 1000, 1., subCluster?.flopRateSimd?.value || defaultMaxY]
|
||||
const w = width - paddingLeft - paddingRight
|
||||
const h = height - paddingTop - paddingBottom
|
||||
|
||||
@ -138,14 +146,14 @@
|
||||
ctx.strokeStyle = 'black'
|
||||
ctx.lineWidth = 2
|
||||
ctx.beginPath()
|
||||
if (cluster != null) {
|
||||
const ycut = 0.01 * cluster.memoryBandwidth.value
|
||||
const scalarKnee = (cluster.flopRateScalar.value - ycut) / cluster.memoryBandwidth.value
|
||||
const simdKnee = (cluster.flopRateSimd.value - ycut) / cluster.memoryBandwidth.value
|
||||
if (subCluster != null) {
|
||||
const ycut = 0.01 * subCluster.memoryBandwidth.value
|
||||
const scalarKnee = (subCluster.flopRateScalar.value - ycut) / subCluster.memoryBandwidth.value
|
||||
const simdKnee = (subCluster.flopRateSimd.value - ycut) / subCluster.memoryBandwidth.value
|
||||
const scalarKneeX = getCanvasX(scalarKnee),
|
||||
simdKneeX = getCanvasX(simdKnee),
|
||||
flopRateScalarY = getCanvasY(cluster.flopRateScalar.value),
|
||||
flopRateSimdY = getCanvasY(cluster.flopRateSimd.value)
|
||||
flopRateScalarY = getCanvasY(subCluster.flopRateScalar.value),
|
||||
flopRateSimdY = getCanvasY(subCluster.flopRateSimd.value)
|
||||
|
||||
if (scalarKneeX < width - paddingRight) {
|
||||
ctx.moveTo(scalarKneeX, flopRateScalarY)
|
||||
@ -182,7 +190,7 @@
|
||||
import { onMount } from 'svelte'
|
||||
import { formatNumber } from '../units.js'
|
||||
|
||||
export let cluster = null
|
||||
export let subCluster = null
|
||||
export let tiles = null
|
||||
export let maxY = null
|
||||
export let width = 500
|
||||
@ -206,7 +214,7 @@
|
||||
|
||||
canvasElement.width = width
|
||||
canvasElement.height = height
|
||||
render(ctx, data, cluster, width, height, maxY)
|
||||
render(ctx, data, subCluster, width, height, maxY)
|
||||
})
|
||||
|
||||
let timeoutId = null
|
||||
@ -226,9 +234,13 @@
|
||||
timeoutId = null
|
||||
canvasElement.width = width
|
||||
canvasElement.height = height
|
||||
render(ctx, data, cluster, width, height, maxY)
|
||||
render(ctx, data, subCluster, width, height, maxY)
|
||||
}, 250)
|
||||
}
|
||||
|
||||
$: sizeChanged(width, height)
|
||||
</script>
|
||||
|
||||
<div class="cc-plot">
|
||||
<canvas bind:this={canvasElement} width="{prevWidth}" height="{prevHeight}"></canvas>
|
||||
</div>
|
@ -1,6 +1,16 @@
|
||||
<div class="cc-plot">
|
||||
<canvas bind:this={canvasElement} width="{width}" height="{height}"></canvas>
|
||||
</div>
|
||||
<!--
|
||||
@component Scatter plot of two metrics at identical timesteps, based on canvas
|
||||
|
||||
Properties:
|
||||
- `X [Number]`: Data from first selected metric as X-values
|
||||
- `Y [Number]`: Data from second selected metric as Y-values
|
||||
- `S GraphQl.TimeWeights.X?`: Float to scale the data with [Default: null]
|
||||
- `color String`: Color of the drawn scatter circles
|
||||
- `width Number`:
|
||||
- `height Number`:
|
||||
- `xLabel String`:
|
||||
- `yLabel String`:
|
||||
-->
|
||||
|
||||
<script context="module">
|
||||
import { formatNumber } from '../units.js'
|
||||
@ -169,3 +179,7 @@
|
||||
$: sizeChanged(width, height);
|
||||
|
||||
</script>
|
||||
|
||||
<div class="cc-plot">
|
||||
<canvas bind:this={canvasElement} width="{width}" height="{height}"></canvas>
|
||||
</div>
|
||||
|
627
web/frontend/src/plots/Scatteruplot.svelte
Normal file
627
web/frontend/src/plots/Scatteruplot.svelte
Normal file
@ -0,0 +1,627 @@
|
||||
<script>
|
||||
import Quadtree from '@timohausmann/quadtree-js' // https://github.com/timohausmann/quadtree-js
|
||||
import uPlot from 'uplot'
|
||||
import { formatNumber } from '../units.js'
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { Card } from 'sveltestrap'
|
||||
|
||||
let plotWrapper = null
|
||||
let uplot = null
|
||||
let timeoutId = null
|
||||
|
||||
function randInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function filledArr(len, val) {
|
||||
let arr = Array(len);
|
||||
|
||||
if (typeof val == "function") {
|
||||
for (let i = 0; i < len; ++i)
|
||||
arr[i] = val(i);
|
||||
}
|
||||
else {
|
||||
for (let i = 0; i < len; ++i)
|
||||
arr[i] = val;
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
let points = 10000;
|
||||
let series = 5;
|
||||
|
||||
console.time("prep");
|
||||
|
||||
let data = filledArr(series, v => [
|
||||
filledArr(points, i => randInt(0,500)),
|
||||
filledArr(points, i => randInt(0,500)),
|
||||
]);
|
||||
|
||||
data[0] = null;
|
||||
|
||||
console.timeEnd("prep");
|
||||
|
||||
console.log(data);
|
||||
|
||||
const drawPoints = (u, seriesIdx, idx0, idx1) => {
|
||||
const size = 5 * devicePixelRatio;
|
||||
|
||||
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
|
||||
let d = u.data[seriesIdx];
|
||||
|
||||
u.ctx.fillStyle = series.stroke();
|
||||
|
||||
let deg360 = 2 * Math.PI;
|
||||
|
||||
console.time("points");
|
||||
|
||||
// let cir = new Path2D();
|
||||
// cir.moveTo(0, 0);
|
||||
// arc(cir, 0, 0, 3, 0, deg360);
|
||||
|
||||
// Create transformation matrix that moves 200 points to the right
|
||||
// let m = document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGMatrix();
|
||||
// m.a = 1; m.b = 0;
|
||||
// m.c = 0; m.d = 1;
|
||||
// m.e = 200; m.f = 0;
|
||||
|
||||
|
||||
let p = new Path2D();
|
||||
|
||||
for (let i = 0; i < d[0].length; i++) {
|
||||
let xVal = d[0][i];
|
||||
let yVal = d[1][i];
|
||||
|
||||
if (xVal >= scaleX.min && xVal <= scaleX.max && yVal >= scaleY.min && yVal <= scaleY.max) {
|
||||
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
||||
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
||||
|
||||
p.moveTo(cx + size/2, cy);
|
||||
// arc(p, cx, cy, 3, 0, deg360);
|
||||
arc(p, cx, cy, size/2, 0, deg360);
|
||||
|
||||
// m.e = cx;
|
||||
// m.f = cy;
|
||||
// p.addPath(cir, m);
|
||||
|
||||
// qt.add({x: cx - 1.5, y: cy - 1.5, w: 3, h: 3, sidx: seriesIdx, didx: i});
|
||||
}
|
||||
}
|
||||
|
||||
console.timeEnd("points");
|
||||
|
||||
u.ctx.fill(p);
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const drawPoints2 = (u, seriesIdx, idx0, idx1) => {
|
||||
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
|
||||
let d = u.data[seriesIdx];
|
||||
|
||||
u.ctx.fillStyle = series.fill();
|
||||
|
||||
let deg360 = 2 * Math.PI;
|
||||
|
||||
console.time("points");
|
||||
|
||||
// let cir = new Path2D();
|
||||
// cir.moveTo(0, 0);
|
||||
// arc(cir, 0, 0, 3, 0, deg360);
|
||||
|
||||
// Create transformation matrix that moves 200 points to the right
|
||||
// let m = document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGMatrix();
|
||||
// m.a = 1; m.b = 0;
|
||||
// m.c = 0; m.d = 1;
|
||||
// m.e = 200; m.f = 0;
|
||||
|
||||
|
||||
let p = new Path2D();
|
||||
|
||||
let strokeWidth = 1;
|
||||
|
||||
for (let i = 0; i < d[0].length; i++) {
|
||||
let cx = valToPosX(d[0][i], scaleX, xDim, xOff);
|
||||
let cy = valToPosY(d[1][i], scaleY, yDim, yOff);
|
||||
|
||||
let size = d[2][i] * devicePixelRatio;
|
||||
|
||||
p.moveTo(cx + size, cy);
|
||||
arc(p, cx, cy, size, 0, deg360);
|
||||
qt.add({
|
||||
x: cx - size - strokeWidth/2 - u.bbox.left,
|
||||
y: cy - size - strokeWidth/2 - u.bbox.top,
|
||||
w: size * 2 + strokeWidth,
|
||||
h: size * 2 + strokeWidth,
|
||||
sidx: seriesIdx,
|
||||
didx: i
|
||||
});
|
||||
}
|
||||
|
||||
console.timeEnd("points");
|
||||
|
||||
u.ctx.fill(p);
|
||||
|
||||
u.ctx.lineWidth = strokeWidth;
|
||||
u.ctx.strokeStyle = series.stroke();
|
||||
u.ctx.stroke(p);
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// shape?
|
||||
const makeDrawPoints3 = (opts) => {
|
||||
let {/*size,*/ disp, each = () => {}} = opts;
|
||||
|
||||
return (u, seriesIdx, idx0, idx1) => {
|
||||
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim, moveTo, lineTo, rect, arc) => {
|
||||
let d = u.data[seriesIdx];
|
||||
|
||||
let strokeWidth = 1;
|
||||
|
||||
u.ctx.save();
|
||||
|
||||
u.ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
|
||||
u.ctx.clip();
|
||||
|
||||
u.ctx.fillStyle = series.fill();
|
||||
u.ctx.strokeStyle = series.stroke();
|
||||
u.ctx.lineWidth = strokeWidth;
|
||||
|
||||
let deg360 = 2 * Math.PI;
|
||||
|
||||
console.time("points");
|
||||
|
||||
// let cir = new Path2D();
|
||||
// cir.moveTo(0, 0);
|
||||
// arc(cir, 0, 0, 3, 0, deg360);
|
||||
|
||||
// Create transformation matrix that moves 200 points to the right
|
||||
// let m = document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGMatrix();
|
||||
// m.a = 1; m.b = 0;
|
||||
// m.c = 0; m.d = 1;
|
||||
// m.e = 200; m.f = 0;
|
||||
|
||||
// compute bubble dims
|
||||
let sizes = disp.size.values(u, seriesIdx, idx0, idx1);
|
||||
|
||||
// todo: this depends on direction & orientation
|
||||
// todo: calc once per redraw, not per path
|
||||
let filtLft = u.posToVal(-maxSize / 2, scaleX.key);
|
||||
let filtRgt = u.posToVal(u.bbox.width / devicePixelRatio + maxSize / 2, scaleX.key);
|
||||
let filtBtm = u.posToVal(u.bbox.height / devicePixelRatio + maxSize / 2, scaleY.key);
|
||||
let filtTop = u.posToVal(-maxSize / 2, scaleY.key);
|
||||
|
||||
for (let i = 0; i < d[0].length; i++) {
|
||||
let xVal = d[0][i];
|
||||
let yVal = d[1][i];
|
||||
let size = sizes[i] * devicePixelRatio;
|
||||
|
||||
if (xVal >= filtLft && xVal <= filtRgt && yVal >= filtBtm && yVal <= filtTop) {
|
||||
let cx = valToPosX(xVal, scaleX, xDim, xOff);
|
||||
let cy = valToPosY(yVal, scaleY, yDim, yOff);
|
||||
|
||||
u.ctx.moveTo(cx + size/2, cy);
|
||||
u.ctx.beginPath();
|
||||
u.ctx.arc(cx, cy, size/2, 0, deg360);
|
||||
u.ctx.fill();
|
||||
u.ctx.stroke();
|
||||
|
||||
each(u, seriesIdx, i,
|
||||
cx - size/2 - strokeWidth/2,
|
||||
cy - size/2 - strokeWidth/2,
|
||||
size + strokeWidth,
|
||||
size + strokeWidth
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.timeEnd("points");
|
||||
|
||||
u.ctx.restore();
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
};
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/isPointInPath
|
||||
let qt;
|
||||
|
||||
let pxRatio;
|
||||
|
||||
function setPxRatio() {
|
||||
pxRatio = devicePixelRatio;
|
||||
}
|
||||
|
||||
function guardedRange(u, min, max) {
|
||||
if (max == min) {
|
||||
if (min == null) {
|
||||
min = 0;
|
||||
max = 100;
|
||||
}
|
||||
else {
|
||||
let delta = Math.abs(max) || 100;
|
||||
max += delta;
|
||||
min -= delta;
|
||||
}
|
||||
}
|
||||
|
||||
return [min, max];
|
||||
}
|
||||
|
||||
setPxRatio();
|
||||
|
||||
window.addEventListener('dppxchange', setPxRatio);
|
||||
|
||||
const opts = {
|
||||
title: "Scatter Plot",
|
||||
mode: 2,
|
||||
width: 1920,
|
||||
height: 600,
|
||||
legend: {
|
||||
live: false,
|
||||
},
|
||||
hooks: {
|
||||
drawClear: [
|
||||
u => {
|
||||
// qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);
|
||||
|
||||
// qt.clear();
|
||||
|
||||
// force-clear the path cache to cause drawBars() to rebuild new quadtree
|
||||
u.series.forEach((s, i) => {
|
||||
if (i > 0)
|
||||
s._paths = null;
|
||||
});
|
||||
},
|
||||
],
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
time: false,
|
||||
// auto: false,
|
||||
// range: [0, 500],
|
||||
// remove any scale padding, use raw data limits
|
||||
range: guardedRange,
|
||||
},
|
||||
y: {
|
||||
// auto: false,
|
||||
// range: [0, 500],
|
||||
// remove any scale padding, use raw data limits
|
||||
range: guardedRange,
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
/*
|
||||
stroke: "red",
|
||||
fill: "rgba(255,0,0,0.1)",
|
||||
paths: (u, seriesIdx, idx0, idx1) => {
|
||||
uPlot.orient(u, seriesIdx, (series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
|
||||
let d = u.data[seriesIdx];
|
||||
|
||||
console.log(d);
|
||||
});
|
||||
|
||||
return null;
|
||||
},
|
||||
*/
|
||||
},
|
||||
{
|
||||
stroke: "red",
|
||||
fill: "rgba(255,0,0,0.1)",
|
||||
paths: drawPoints,
|
||||
},
|
||||
{
|
||||
stroke: "green",
|
||||
fill: "rgba(0,255,0,0.1)",
|
||||
paths: drawPoints,
|
||||
},
|
||||
{
|
||||
stroke: "blue",
|
||||
fill: "rgba(0,0,255,0.1)",
|
||||
paths: drawPoints,
|
||||
},
|
||||
{
|
||||
stroke: "magenta",
|
||||
fill: "rgba(0,0,255,0.1)",
|
||||
paths: drawPoints,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let u = new uPlot(opts, data, document.body);
|
||||
|
||||
|
||||
|
||||
points = 50;
|
||||
|
||||
// size range in pixels (diameter)
|
||||
let minSize = 6;
|
||||
let maxSize = 60;
|
||||
|
||||
let maxArea = Math.PI * (maxSize / 2) ** 2;
|
||||
let minArea = Math.PI * (minSize / 2) ** 2;
|
||||
|
||||
// quadratic scaling (px area)
|
||||
function getSize(value, minValue, maxValue) {
|
||||
let pct = value / maxValue;
|
||||
// clamp to min area
|
||||
//let area = Math.max(maxArea * pct, minArea);
|
||||
let area = maxArea * pct;
|
||||
return Math.sqrt(area / Math.PI) * 2;
|
||||
}
|
||||
|
||||
function getSizeMinMax(u) {
|
||||
let minValue = Infinity;
|
||||
let maxValue = -Infinity;
|
||||
|
||||
for (let i = 1; i < u.series.length; i++) {
|
||||
let sizeData = u.data[i][2];
|
||||
|
||||
for (let j = 0; j < sizeData.length; j++) {
|
||||
minValue = Math.min(minValue, sizeData[j]);
|
||||
maxValue = Math.max(maxValue, sizeData[j]);
|
||||
}
|
||||
}
|
||||
|
||||
return [minValue, maxValue];
|
||||
}
|
||||
|
||||
let drawPoints3 = makeDrawPoints3({
|
||||
disp: {
|
||||
size: {
|
||||
unit: 3, // raw CSS pixels
|
||||
// discr: true,
|
||||
values: (u, seriesIdx, idx0, idx1) => {
|
||||
// TODO: only run once per setData() call
|
||||
let [minValue, maxValue] = getSizeMinMax(u);
|
||||
return u.data[seriesIdx][2].map(v => getSize(v, minValue, maxValue));
|
||||
},
|
||||
},
|
||||
},
|
||||
each: (u, seriesIdx, dataIdx, lft, top, wid, hgt) => {
|
||||
// we get back raw canvas coords (included axes & padding). translate to the plotting area origin
|
||||
lft -= u.bbox.left;
|
||||
top -= u.bbox.top;
|
||||
qt.add({x: lft, y: top, w: wid, h: hgt, sidx: seriesIdx, didx: dataIdx});
|
||||
},
|
||||
});
|
||||
|
||||
let data2 = filledArr(series, v => [
|
||||
filledArr(points, i => randInt(0,500)),
|
||||
filledArr(points, i => randInt(0,500)),
|
||||
filledArr(points, i => randInt(1,10000)), // bubble size, population
|
||||
filledArr(points, i => (Math.random() + 1).toString(36).substring(7)), // label / country name
|
||||
]);
|
||||
|
||||
data2[0] = null;
|
||||
data2[1][1] = data2[1][1].map(v => v == 0 ? v : v * -1);
|
||||
|
||||
/*
|
||||
const legendValue = (u, val, seriesIdx, idx) => {
|
||||
let [x, y, size] = u.data[seriesIdx];
|
||||
return `${x[idx]}, ${y[idx]}, ${size[idx]}`;
|
||||
};
|
||||
*/
|
||||
|
||||
const legendValues = (u, seriesIdx, dataIdx) => {
|
||||
// when data null, it's initial schema probe (also u.status == 0)
|
||||
if (u.data == null || dataIdx == null || hRect == null || hRect.sidx != seriesIdx) {
|
||||
return {
|
||||
"Country": '<NUM COUNTRIES>',
|
||||
"Population": '<TOTAL POP>',
|
||||
"GDP": '<TOTAL GDP>',
|
||||
"Income": '<AVG INCOME>'
|
||||
};
|
||||
}
|
||||
|
||||
let [x, y, size, label] = u.data[seriesIdx];
|
||||
|
||||
return {
|
||||
"Country": label[hRect.didx],
|
||||
"Population": size[hRect.didx],
|
||||
"GDP": '$' + x[hRect.didx],
|
||||
"Income": '$' + y[hRect.didx],
|
||||
};
|
||||
};
|
||||
|
||||
// hovered
|
||||
let hRect;
|
||||
|
||||
const opts2 = {
|
||||
title: "Bubble Plot",
|
||||
mode: 2,
|
||||
width: 1920,
|
||||
height: 600,
|
||||
legend: {
|
||||
// live: false,
|
||||
},
|
||||
cursor: {
|
||||
dataIdx: (u, seriesIdx) => {
|
||||
if (seriesIdx == 1) {
|
||||
hRect = null;
|
||||
|
||||
let dist = Infinity;
|
||||
let cx = u.cursor.left * pxRatio;
|
||||
let cy = u.cursor.top * pxRatio;
|
||||
|
||||
// !!! https://github.com/timohausmann/quadtree-js/blob/master/docs/simple.html
|
||||
// Rewrite mouseOver based on this example
|
||||
|
||||
// let overlaps = qt.retrieve({x:cx, y:cy, width:1, height:1})
|
||||
|
||||
// for(var i=0;i<overlaps.length;i=i+1) {
|
||||
// overlaps[i].check = true;
|
||||
// }
|
||||
|
||||
// OLD!
|
||||
qt.get(cx, cy, 1, 1, o => {
|
||||
if (pointWithin(cx, cy, o.x, o.y, o.x + o.w, o.y + o.h)) {
|
||||
let ocx = o.x + o.w / 2;
|
||||
let ocy = o.y + o.h / 2;
|
||||
|
||||
let dx = ocx - cx;
|
||||
let dy = ocy - cy;
|
||||
|
||||
let d = Math.sqrt(dx ** 2 + dy ** 2);
|
||||
|
||||
// test against radius for actual hover
|
||||
if (d <= o.w / 2) {
|
||||
// only hover bbox with closest distance
|
||||
if (d <= dist) {
|
||||
dist = d;
|
||||
hRect = o;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return hRect && seriesIdx == hRect.sidx ? hRect.didx : null;
|
||||
},
|
||||
points: {
|
||||
size: (u, seriesIdx) => {
|
||||
return hRect && seriesIdx == hRect.sidx ? hRect.w / devicePixelRatio : 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
hooks: {
|
||||
drawClear: [
|
||||
u => {
|
||||
qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);
|
||||
|
||||
qt.clear();
|
||||
|
||||
// force-clear the path cache to cause drawBars() to rebuild new quadtree
|
||||
u.series.forEach((s, i) => {
|
||||
if (i > 0)
|
||||
s._paths = null;
|
||||
});
|
||||
},
|
||||
],
|
||||
},
|
||||
axes: [
|
||||
{
|
||||
label: "GDP",
|
||||
},
|
||||
{
|
||||
label: "Income 1",
|
||||
},
|
||||
{
|
||||
side: 1,
|
||||
scale: 'y2',
|
||||
stroke: "red",
|
||||
label: "Income 2",
|
||||
grid: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
scales: {
|
||||
x: {
|
||||
time: false,
|
||||
// auto: false,
|
||||
// range: [0, 500],
|
||||
|
||||
// remove any scale padding, use raw data limits
|
||||
range: guardedRange,
|
||||
},
|
||||
y: {
|
||||
// auto: false,
|
||||
// range: [0, 500],
|
||||
|
||||
// remove any scale padding, use raw data limits
|
||||
range: guardedRange,
|
||||
},
|
||||
y2: {
|
||||
dir: 1,
|
||||
ori: 1,
|
||||
range: guardedRange,
|
||||
}
|
||||
},
|
||||
series: [
|
||||
null,
|
||||
{
|
||||
facets: [
|
||||
{
|
||||
scale: 'x',
|
||||
auto: true,
|
||||
},
|
||||
{
|
||||
scale: 'y2',
|
||||
auto: true,
|
||||
}
|
||||
],
|
||||
label: "Region A",
|
||||
stroke: "red",
|
||||
fill: "rgba(255,0,0,0.3)",
|
||||
paths: drawPoints3,
|
||||
values: legendValues,
|
||||
},
|
||||
{
|
||||
facets: [
|
||||
{
|
||||
scale: 'x',
|
||||
auto: true,
|
||||
},
|
||||
{
|
||||
scale: 'y',
|
||||
auto: true,
|
||||
}
|
||||
],
|
||||
label: "Region B",
|
||||
stroke: "green",
|
||||
fill: "rgba(0,255,0,0.3)",
|
||||
paths: drawPoints3,
|
||||
values: legendValues,
|
||||
},
|
||||
{
|
||||
facets: [
|
||||
{
|
||||
scale: 'x',
|
||||
auto: true,
|
||||
},
|
||||
{
|
||||
scale: 'y',
|
||||
auto: true,
|
||||
}
|
||||
],
|
||||
label: "Region C",
|
||||
stroke: "blue",
|
||||
fill: "rgba(0,0,255,0.3)",
|
||||
paths: drawPoints3,
|
||||
values: legendValues,
|
||||
},
|
||||
{
|
||||
facets: [
|
||||
{
|
||||
scale: 'x',
|
||||
auto: true,
|
||||
},
|
||||
{
|
||||
scale: 'y',
|
||||
auto: true,
|
||||
}
|
||||
],
|
||||
label: "Region E",
|
||||
stroke: "orange",
|
||||
fill: "rgba(255,128,0,0.3)",
|
||||
paths: drawPoints3,
|
||||
values: legendValues,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let u2 = new uPlot(opts2, data2, document.body);
|
||||
</script>
|
||||
|
||||
{#if data != null}
|
||||
<div bind:this={plotWrapper}/>
|
||||
{:else}
|
||||
<Card class="mx-4" body color="warning">Cannot render scatter: No data!</Card>
|
||||
{/if}
|
@ -108,18 +108,18 @@ export function init(extraInitQuery = "") {
|
||||
setContext("clusters", clusters);
|
||||
setContext("globalMetrics", globalMetrics);
|
||||
setContext("getMetricConfig", (cluster, subCluster, metric) => {
|
||||
// Load objects if input is string
|
||||
if (typeof cluster !== "object")
|
||||
cluster = clusters.find((c) => c.name == cluster);
|
||||
|
||||
if (typeof subCluster !== "object")
|
||||
subCluster = cluster.subClusters.find((sc) => sc.name == subCluster);
|
||||
|
||||
return subCluster.metricConfig.find((m) => m.name == metric);
|
||||
});
|
||||
setContext("getHardwareTopology", (cluster, subCluster) => {
|
||||
// Load objects if input is string
|
||||
if (typeof cluster !== "object")
|
||||
cluster = clusters.find((c) => c.name == cluster);
|
||||
|
||||
if (typeof subCluster !== "object")
|
||||
subCluster = cluster.subClusters.find((sc) => sc.name == subCluster);
|
||||
|
||||
@ -175,6 +175,17 @@ export function distinct(value, index, array) {
|
||||
return array.indexOf(value) === index;
|
||||
}
|
||||
|
||||
// Load Local Bool and Handle Scrambling of input string
|
||||
export const scrambleNames = window.localStorage.getItem("cc-scramble-names");
|
||||
export const scramble = function (str) {
|
||||
if (str === "-") return str;
|
||||
else
|
||||
return [...str]
|
||||
.reduce((x, c, i) => x * 7 + c.charCodeAt(0) * i * 21, 5)
|
||||
.toString(32)
|
||||
.substr(0, 6);
|
||||
};
|
||||
|
||||
export function fuzzySearchTags(term, tags) {
|
||||
if (!tags) return [];
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user