From 68d1f5fc3ff537c60ba6a87cbe5080db88ec58c0 Mon Sep 17 00:00:00 2001 From: Jan Eitzinger Date: Wed, 22 Jun 2022 11:20:57 +0200 Subject: [PATCH] Import svelte web frontend --- web/frontend/README.md | 31 ++ web/frontend/package.json | 25 + web/frontend/public/favicon.png | Bin 0 -> 11162 bytes web/frontend/public/global.css | 54 ++ web/frontend/public/img/logo.png | Bin 0 -> 16474 bytes web/frontend/public/uPlot.min.css | 1 + web/frontend/rollup.config.js | 70 +++ web/frontend/src/Analysis.root.svelte | 265 ++++++++++ web/frontend/src/Header.svelte | 73 +++ web/frontend/src/Job.root.svelte | 224 ++++++++ web/frontend/src/Jobs.root.svelte | 88 ++++ web/frontend/src/List.root.svelte | 151 ++++++ web/frontend/src/Metric.svelte | 88 ++++ web/frontend/src/MetricSelection.svelte | 126 +++++ web/frontend/src/Node.root.svelte | 94 ++++ web/frontend/src/PlotSelection.svelte | 133 +++++ web/frontend/src/PlotTable.svelte | 50 ++ web/frontend/src/StatsTable.svelte | 122 +++++ web/frontend/src/StatsTableEntry.svelte | 37 ++ web/frontend/src/Status.root.svelte | 184 +++++++ web/frontend/src/Systems.root.svelte | 118 +++++ web/frontend/src/Tag.svelte | 44 ++ web/frontend/src/TagManagement.svelte | 173 ++++++ web/frontend/src/User.root.svelte | 172 ++++++ web/frontend/src/Zoom.svelte | 60 +++ web/frontend/src/analysis.entrypoint.js | 14 + web/frontend/src/cache-exchange.js | 72 +++ web/frontend/src/filters/Cluster.svelte | 77 +++ .../src/filters/DoubleRangeSlider.svelte | 302 +++++++++++ web/frontend/src/filters/Duration.svelte | 95 ++++ web/frontend/src/filters/Filters.svelte | 323 ++++++++++++ web/frontend/src/filters/InfoBox.svelte | 11 + web/frontend/src/filters/JobStates.svelte | 47 ++ web/frontend/src/filters/Resources.svelte | 99 ++++ web/frontend/src/filters/StartTime.svelte | 90 ++++ web/frontend/src/filters/Stats.svelte | 113 ++++ web/frontend/src/filters/Tags.svelte | 67 +++ web/frontend/src/filters/TimeSelection.svelte | 80 +++ web/frontend/src/filters/UserOrProject.svelte | 51 ++ web/frontend/src/header.entrypoint.js | 10 + web/frontend/src/job.entrypoint.js | 12 + web/frontend/src/joblist/JobInfo.svelte | 88 ++++ web/frontend/src/joblist/JobList.svelte | 190 +++++++ web/frontend/src/joblist/Pagination.svelte | 230 ++++++++ web/frontend/src/joblist/Refresher.svelte | 43 ++ web/frontend/src/joblist/Row.svelte | 101 ++++ web/frontend/src/joblist/SortSelection.svelte | 71 +++ web/frontend/src/jobs.entrypoint.js | 12 + web/frontend/src/list.entrypoint.js | 13 + web/frontend/src/node.entrypoint.js | 15 + web/frontend/src/plots/Histogram.svelte | 210 ++++++++ web/frontend/src/plots/MetricPlot.svelte | 306 +++++++++++ web/frontend/src/plots/Polar.svelte | 190 +++++++ web/frontend/src/plots/Roofline.svelte | 355 +++++++++++++ web/frontend/src/plots/Scatter.svelte | 171 ++++++ web/frontend/src/status.entrypoint.js | 12 + web/frontend/src/systems.entrypoint.js | 14 + web/frontend/src/user.entrypoint.js | 13 + web/frontend/src/utils.js | 288 ++++++++++ web/frontend/yarn.lock | 493 ++++++++++++++++++ 60 files changed, 6661 insertions(+) create mode 100644 web/frontend/README.md create mode 100644 web/frontend/package.json create mode 100644 web/frontend/public/favicon.png create mode 100644 web/frontend/public/global.css create mode 100644 web/frontend/public/img/logo.png create mode 120000 web/frontend/public/uPlot.min.css create mode 100644 web/frontend/rollup.config.js create mode 100644 web/frontend/src/Analysis.root.svelte create mode 100644 web/frontend/src/Header.svelte create mode 100644 web/frontend/src/Job.root.svelte create mode 100644 web/frontend/src/Jobs.root.svelte create mode 100644 web/frontend/src/List.root.svelte create mode 100644 web/frontend/src/Metric.svelte create mode 100644 web/frontend/src/MetricSelection.svelte create mode 100644 web/frontend/src/Node.root.svelte create mode 100644 web/frontend/src/PlotSelection.svelte create mode 100644 web/frontend/src/PlotTable.svelte create mode 100644 web/frontend/src/StatsTable.svelte create mode 100644 web/frontend/src/StatsTableEntry.svelte create mode 100644 web/frontend/src/Status.root.svelte create mode 100644 web/frontend/src/Systems.root.svelte create mode 100644 web/frontend/src/Tag.svelte create mode 100644 web/frontend/src/TagManagement.svelte create mode 100644 web/frontend/src/User.root.svelte create mode 100644 web/frontend/src/Zoom.svelte create mode 100644 web/frontend/src/analysis.entrypoint.js create mode 100644 web/frontend/src/cache-exchange.js create mode 100644 web/frontend/src/filters/Cluster.svelte create mode 100644 web/frontend/src/filters/DoubleRangeSlider.svelte create mode 100644 web/frontend/src/filters/Duration.svelte create mode 100644 web/frontend/src/filters/Filters.svelte create mode 100644 web/frontend/src/filters/InfoBox.svelte create mode 100644 web/frontend/src/filters/JobStates.svelte create mode 100644 web/frontend/src/filters/Resources.svelte create mode 100644 web/frontend/src/filters/StartTime.svelte create mode 100644 web/frontend/src/filters/Stats.svelte create mode 100644 web/frontend/src/filters/Tags.svelte create mode 100644 web/frontend/src/filters/TimeSelection.svelte create mode 100644 web/frontend/src/filters/UserOrProject.svelte create mode 100644 web/frontend/src/header.entrypoint.js create mode 100644 web/frontend/src/job.entrypoint.js create mode 100644 web/frontend/src/joblist/JobInfo.svelte create mode 100644 web/frontend/src/joblist/JobList.svelte create mode 100644 web/frontend/src/joblist/Pagination.svelte create mode 100644 web/frontend/src/joblist/Refresher.svelte create mode 100644 web/frontend/src/joblist/Row.svelte create mode 100644 web/frontend/src/joblist/SortSelection.svelte create mode 100644 web/frontend/src/jobs.entrypoint.js create mode 100644 web/frontend/src/list.entrypoint.js create mode 100644 web/frontend/src/node.entrypoint.js create mode 100644 web/frontend/src/plots/Histogram.svelte create mode 100644 web/frontend/src/plots/MetricPlot.svelte create mode 100644 web/frontend/src/plots/Polar.svelte create mode 100644 web/frontend/src/plots/Roofline.svelte create mode 100644 web/frontend/src/plots/Scatter.svelte create mode 100644 web/frontend/src/status.entrypoint.js create mode 100644 web/frontend/src/systems.entrypoint.js create mode 100644 web/frontend/src/user.entrypoint.js create mode 100644 web/frontend/src/utils.js create mode 100644 web/frontend/yarn.lock diff --git a/web/frontend/README.md b/web/frontend/README.md new file mode 100644 index 0000000..4d54384 --- /dev/null +++ b/web/frontend/README.md @@ -0,0 +1,31 @@ +# cc-svelte-datatable + +[![Build](https://github.com/ClusterCockpit/cc-svelte-datatable/actions/workflows/build.yml/badge.svg)](https://github.com/ClusterCockpit/cc-svelte-datatable/actions/workflows/build.yml) + +A frontend for [ClusterCockpit](https://github.com/ClusterCockpit/ClusterCockpit) and [cc-backend](https://github.com/ClusterCockpit/cc-backend). Backend specific configuration can de done using the constants defined in the `intro` section in `./rollup.config.js`. + +Builds on: +* [Svelte](https://svelte.dev/) +* [SvelteStrap](https://sveltestrap.js.org/) +* [Bootstrap 5](https://getbootstrap.com/) +* [urql](https://github.com/FormidableLabs/urql) + +## Get started + +[Yarn](https://yarnpkg.com/) is recommended for package management. +Due to an issue with Yarn v2 you have to stick to Yarn v1. + +Install the dependencies... + +```bash +yarn install +``` + +...then start [Rollup](https://rollupjs.org): + +```bash +yarn run dev +``` + +Edit a component file in `src`, save it, and reload the page to see your changes. + diff --git a/web/frontend/package.json b/web/frontend/package.json new file mode 100644 index 0000000..2f2ab55 --- /dev/null +++ b/web/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "svelte-app", + "version": "1.0.0", + "scripts": { + "build": "rollup -c", + "dev": "rollup -c -w" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^17.0.0", + "@rollup/plugin-node-resolve": "^11.0.0", + "rollup": "^2.3.4", + "rollup-plugin-css-only": "^3.1.0", + "rollup-plugin-svelte": "^7.0.0", + "rollup-plugin-terser": "^7.0.0", + "svelte": "^3.42.6" + }, + "dependencies": { + "@rollup/plugin-replace": "^2.4.1", + "@urql/svelte": "^1.3.0", + "graphql": "^15.6.0", + "sveltestrap": "^5.6.1", + "uplot": "^1.6.7", + "wonka": "^4.0.15" + } +} diff --git a/web/frontend/public/favicon.png b/web/frontend/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..fa7bf5c4375196deed3d8cb5ebe8f258ef921ab9 GIT binary patch literal 11162 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 literal 0 HcmV?d00001 diff --git a/web/frontend/public/global.css b/web/frontend/public/global.css new file mode 100644 index 0000000..8feecf6 --- /dev/null +++ b/web/frontend/public/global.css @@ -0,0 +1,54 @@ +html, body { + position: relative; + width: 100%; + height: 100%; +} + +body { + color: #333; + margin: 0; + padding: 8px; + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; +} + +.container { + max-width: 100vw; +} + +.site { + display: flex; + flex-direction: column; + height: 100%; +} + +.site-content { + flex: 1 0 auto; + margin-top: 80px; +} + +.site-footer { + flex: none; +} + +footer { + width: 100%; + padding: 0.1rem 1.0rem; + line-height: 1.5; +} + +.footer-list { + list-style-type: none; + padding-left: 0; + width: 100%; + display: flex; + flex-wrap: wrap; + justify-content: center; + margin-top: 5px; + margin-bottom: 5px; +} + +.footer-list-item { + margin: 0rem 0.8rem; + white-space: nowrap; +} diff --git a/web/frontend/public/img/logo.png b/web/frontend/public/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2ad4fd6f9c8d14b1bb4515ae78818f6a7d5a4a99 GIT binary patch literal 16474 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 N<#Q$J5=oQ5{|E5MnMD8q literal 0 HcmV?d00001 diff --git a/web/frontend/public/uPlot.min.css b/web/frontend/public/uPlot.min.css new file mode 120000 index 0000000..b11d327 --- /dev/null +++ b/web/frontend/public/uPlot.min.css @@ -0,0 +1 @@ +../node_modules/uplot/dist/uPlot.min.css \ No newline at end of file diff --git a/web/frontend/rollup.config.js b/web/frontend/rollup.config.js new file mode 100644 index 0000000..13d988a --- /dev/null +++ b/web/frontend/rollup.config.js @@ -0,0 +1,70 @@ +import svelte from 'rollup-plugin-svelte'; +import replace from "@rollup/plugin-replace"; +import commonjs from '@rollup/plugin-commonjs'; +import resolve from '@rollup/plugin-node-resolve'; +import { terser } from 'rollup-plugin-terser'; +import css from 'rollup-plugin-css-only'; + +const production = !process.env.ROLLUP_WATCH; + +const plugins = [ + svelte({ + compilerOptions: { + // enable run-time checks when not in production + dev: !production + } + }), + + // If you have external dependencies installed from + // npm, you'll most likely need these plugins. In + // some cases you'll need additional configuration - + // consult the documentation for details: + // https://github.com/rollup/plugins/tree/master/packages/commonjs + resolve({ + browser: true, + dedupe: ['svelte'] + }), + commonjs(), + + // If we're building for production (npm run build + // instead of npm run dev), minify + production && terser(), + + replace({ + "process.env.NODE_ENV": JSON.stringify("development"), + preventAssignment: true + }) +]; + +const entrypoint = (name, path) => ({ + input: path, + output: { + sourcemap: false, + format: 'iife', + name: 'app', + file: `public/build/${name}.js` + }, + plugins: [ + ...plugins, + + // we'll extract any component CSS out into + // a separate file - better for performance + css({ output: `${name}.css` }), + ], + watch: { + clearScreen: false + } +}); + +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('node', 'src/node.entrypoint.js'), + entrypoint('analysis', 'src/analysis.entrypoint.js'), + entrypoint('status', 'src/status.entrypoint.js') +]; + diff --git a/web/frontend/src/Analysis.root.svelte b/web/frontend/src/Analysis.root.svelte new file mode 100644 index 0000000..a92aea7 --- /dev/null +++ b/web/frontend/src/Analysis.root.svelte @@ -0,0 +1,265 @@ + + + + {#if $initq.fetching || $statsQuery.fetching || $footprintsQuery.fetching} + + + + {/if} + + {#if $initq.error} + {$initq.error.message} + {:else if cluster} + mc.name)} + bind:metricsInHistograms={metricsInHistograms} + bind:metricsInScatterplots={metricsInScatterplots} /> + {/if} + + + { + $statsQuery.context.pause = false + $statsQuery.variables = { filter: detail.filters } + $footprintsQuery.context.pause = false + $footprintsQuery.variables = { metrics, filter: detail.filters } + $rooflineQuery.variables = { ...$rooflineQuery.variables, filter: detail.filters } + }} /> + + + +
+{#if $statsQuery.error} + + + {$statsQuery.error.message} + + +{:else if $statsQuery.data} + +
+
+ + + + + + + + + + + + + + + + + +
Total Jobs{$statsQuery.data.stats[0].totalJobs}
Short Jobs (< 2m){$statsQuery.data.stats[0].shortJobs}
Total Walltime{$statsQuery.data.stats[0].totalWalltime}
Total Core Hours{$statsQuery.data.stats[0].totalCoreHours}
+
+
+ {#key $statsQuery.data.topUsers} +

Top Users (by node hours)

+ b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))} + label={(x) => x < $statsQuery.data.topUsers.length ? $statsQuery.data.topUsers[Math.floor(x)].name : '0'} /> + {/key} +
+
+
+ {#key $statsQuery.data.stats[0].histDuration} +

Walltime Distribution

+ + {/key} +
+
+ {#key $statsQuery.data.stats[0].histNumNodes} +

Number of Nodes Distribution

+ + {/key} +
+
+ {#if $rooflineQuery.fetching} + + {:else if $rooflineQuery.error} + {$rooflineQuery.error.message} + {:else if $rooflineQuery.data && cluster} + {#key $rooflineQuery.data} + + {/key} + {/if} +
+
+{/if} + +
+{#if $footprintsQuery.error} + + + {$footprintsQuery.error.message} + + +{:else if $footprintsQuery.data && $initq.data} + + + + These histograms show the distribution of the averages of all jobs matching the filters. Each job/average is weighted by its node hours. + +
+ +
+ + + ({ metric, ...binsFromFootprint( + $footprintsQuery.data.footprints.nodehours, + $footprintsQuery.data.footprints.metrics.find(f => f.metric == metric).data, numBins) }))} + itemsPerRow={ccconfig.plot_view_plotsPerRow}> +

{item.metric} [{metricConfig(cluster.name, item.metric)?.unit}]

+ + +
+ +
+
+ + + + 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. + +
+ +
+ + + ({ + 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}> + + + + + +{/if} + + diff --git a/web/frontend/src/Header.svelte b/web/frontend/src/Header.svelte new file mode 100644 index 0000000..f99956a --- /dev/null +++ b/web/frontend/src/Header.svelte @@ -0,0 +1,73 @@ + + + + + ClusterCockpit Logo + + (isOpen = !isOpen)} /> + (isOpen = detail.isOpen)}> + + +
+
+ + + + +
+ {#if username} +
+ +
+ {/if} + +
+
diff --git a/web/frontend/src/Job.root.svelte b/web/frontend/src/Job.root.svelte new file mode 100644 index 0000000..58c0d56 --- /dev/null +++ b/web/frontend/src/Job.root.svelte @@ -0,0 +1,224 @@ + + +
+ + + {#if $initq.error} + {$initq.error.message} + {:else if $initq.data} + + {:else} + + {/if} + + {#if $jobMetrics.data && $initq.data} + + + + + c.name == $initq.data.job.cluster).subClusters + .find(sc => sc.name == $initq.data.job.subCluster)} + flopsAny={$jobMetrics.data.jobMetrics.find(m => m.name == 'flops_any' && m.metric.scope == 'node').metric} + memBw={$jobMetrics.data.jobMetrics.find(m => m.name == 'mem_bw' && m.metric.scope == 'node').metric} /> + + {:else} + + + {/if} + +
+ + + {#if $initq.data} + + {/if} + + + {#if $initq.data} + + {/if} + + + + + +
+ + + {#if $jobMetrics.error} + {#if $initq.data.job.monitoringStatus == 0 || $initq.data.job.monitoringStatus == 2} + Not monitored or archiving failed +
+ {/if} + {$jobMetrics.error.message} + {:else if $jobMetrics.fetching} + + {:else if $jobMetrics.data && $initq.data} + + {#if item.data} + statsTable.moreLoaded(detail)} + job={$initq.data.job} + metric={item.metric} + scopes={item.data.map(x => x.metric)} + width={width}/> + {:else} + No data for {item.metric} + {/if} + + {/if} + +
+
+ + + {#if $initq.data} + + {#if somethingMissing} + +
+ + Missing Metrics/Reseources + + + {#if missingMetrics.length > 0} +

No data at all is available for the metrics: {missingMetrics.join(', ')}

+ {/if} + {#if missingHosts.length > 0} +

Some metrics are missing for the following hosts:

+
    + {#each missingHosts as missing} +
  • {missing.hostname}: {missing.metrics.join(', ')}
  • + {/each} +
+ {/if} +
+
+
+ {/if} + + {#if $jobMetrics.data} + + {/if} + + +
+ {#if $initq.data.job.metaData?.jobScript} +
{$initq.data.job.metaData?.jobScript}
+ {:else} + No job script available + {/if} +
+
+ +
+ {#if $initq.data.job.metaData?.slurmInfo} +
{$initq.data.job.metaData?.slurmInfo}
+ {:else} + No additional slurm information available + {/if} +
+
+
+ {/if} + +
+ +{#if $initq.data} + +{/if} + + diff --git a/web/frontend/src/Jobs.root.svelte b/web/frontend/src/Jobs.root.svelte new file mode 100644 index 0000000..9ecaafa --- /dev/null +++ b/web/frontend/src/Jobs.root.svelte @@ -0,0 +1,88 @@ + + + + {#if $initq.fetching} + + + + {:else if $initq.error} + + {$initq.error.message} + + {/if} + + + + + + + + + jobList.update(detail.filters)} /> + + + + filters.update(detail)}/> + + + jobList.update()} /> + + +
+ + + + + + + + + diff --git a/web/frontend/src/List.root.svelte b/web/frontend/src/List.root.svelte new file mode 100644 index 0000000..7d973e4 --- /dev/null +++ b/web/frontend/src/List.root.svelte @@ -0,0 +1,151 @@ + + + + + + + + + + + + { + $stats.variables = { filter: detail.filters } + $stats.context.pause = false + $stats.reexecute() + }} /> + + + + + + + + + + + + + {#if $stats.fetching} + + + + {:else if $stats.error} + + + + {:else if $stats.data} + {#each sort($stats.data.rows, sorting, nameFilter) as row (row.id)} + + + + + + + {:else} + + + + {/each} + {/if} + +
+ {({ USER: 'Username', PROJECT: 'Project Name' })[type]} + + + Total Jobs + + + Total Walltime + + + Total Core Hours + +
{$stats.error.message}
+ {#if type == 'USER'} + {scrambleNames ? scramble(row.id) : row.id} + {:else if type == 'PROJECT'} + {row.id} + {:else} + {row.id} + {/if} + {row.totalJobs}{row.totalWalltime}{row.totalCoreHours}
No {type.toLowerCase()}s/jobs found
\ No newline at end of file diff --git a/web/frontend/src/Metric.svelte b/web/frontend/src/Metric.svelte new file mode 100644 index 0000000..f414827 --- /dev/null +++ b/web/frontend/src/Metric.svelte @@ -0,0 +1,88 @@ + + + + {metric} ({metricConfig?.unit}) + + + {#if job.resources.length > 1} + + {/if} + +{#key series} + {#if fetching == true} + + {:else if error != null} + {error.message} + {:else if series != null} + + {/if} +{/key} diff --git a/web/frontend/src/MetricSelection.svelte b/web/frontend/src/MetricSelection.svelte new file mode 100644 index 0000000..4119256 --- /dev/null +++ b/web/frontend/src/MetricSelection.svelte @@ -0,0 +1,126 @@ + + + + + + + (isOpen = !isOpen)}> + + Configure columns + + + + {#each newMetricsOrder as metric, index (metric)} +
  • columnsDragStart(event, index)} + on:drop|preventDefault={event => columnsDrag(event, index)} + on:dragenter={() => columnHovering = index} + class:is-active={columnHovering === index}> + {#if unorderedMetrics.includes(metric)} + + {:else} + + {/if} + {metric} + + {cluster == null ? clusters + .filter(cluster => cluster.metricConfig.find(m => m.name == metric) != null) + .map(cluster => cluster.name).join(', ') : ''} + +
  • + {/each} +
    +
    + + + +
    diff --git a/web/frontend/src/Node.root.svelte b/web/frontend/src/Node.root.svelte new file mode 100644 index 0000000..9534bd7 --- /dev/null +++ b/web/frontend/src/Node.root.svelte @@ -0,0 +1,94 @@ + + + + {#if $initq.error} + {$initq.error.message} + {:else if $initq.fetching} + + {:else} + + + + {hostname} ({cluster}) + + + + + + {/if} + +
    + + + {#if $nodesQuery.error} + {$nodesQuery.error.message} + {:else if $nodesQuery.fetching || $initq.fetching} + + {:else} + a.name.localeCompare(b.name))}> +

    {item.name}

    + c.name == cluster)} subCluster={$nodesQuery.data.nodeMetrics[0].subCluster} + series={item.metric.series} /> +
    + {/if} + +
    diff --git a/web/frontend/src/PlotSelection.svelte b/web/frontend/src/PlotSelection.svelte new file mode 100644 index 0000000..0205c27 --- /dev/null +++ b/web/frontend/src/PlotSelection.svelte @@ -0,0 +1,133 @@ + + + + + + + (isHistogramConfigOpen = !isHistogramConfigOpen)}> + + Select metrics presented in histograms + + + + {#each availableMetrics as metric (metric)} + + updateConfiguration({ + name: 'analysis_view_histogramMetrics', + value: metricsInHistograms + })} /> + + {metric} + + {/each} + + + + + + + + (isScatterPlotConfigOpen = !isScatterPlotConfigOpen)}> + + Select metric pairs presented in scatter plots + + + + {#each metricsInScatterplots as pair} + + {pair[0]} / {pair[1]} + + + + {/each} + + +
    + + + + + + + +
    + + + +
    diff --git a/web/frontend/src/PlotTable.svelte b/web/frontend/src/PlotTable.svelte new file mode 100644 index 0000000..208c4af --- /dev/null +++ b/web/frontend/src/PlotTable.svelte @@ -0,0 +1,50 @@ + + + + + + {#each rows as row} + + {#each row as item (item)} + + {/each} + + {/each} +
    + {#if item != PLACEHOLDER && plotWidth > 0} + + {/if} +
    diff --git a/web/frontend/src/StatsTable.svelte b/web/frontend/src/StatsTable.svelte new file mode 100644 index 0000000..e9400ac --- /dev/null +++ b/web/frontend/src/StatsTable.svelte @@ -0,0 +1,122 @@ + + + + + + + {#each selectedMetrics as metric} + + {/each} + + + + {#each selectedMetrics as metric} + {#if selectedScopes[metric] != 'node'} + + {/if} + {#each ['min', 'avg', 'max'] as stat} + + {/each} + {/each} + + + + {#each hosts as host (host)} + + + {#each selectedMetrics as metric (metric)} + + {/each} + + {/each} + +
    + + + + + {metric} + + + +
    NodeId sortBy(metric, stat)}> + {stat} + {#if selectedScopes[metric] == 'node'} + + {/if} +
    {host}
    + +
    + + diff --git a/web/frontend/src/StatsTableEntry.svelte b/web/frontend/src/StatsTableEntry.svelte new file mode 100644 index 0000000..93cd9f0 --- /dev/null +++ b/web/frontend/src/StatsTableEntry.svelte @@ -0,0 +1,37 @@ + + +{#if series == null || series.length == 0} + No data +{:else if series.length == 1 && scope == 'node'} + + {series[0].statistics.min} + + + {series[0].statistics.avg} + + + {series[0].statistics.max} + +{:else} + + + {#each series as s, i} + + + + + + + {/each} +
    {s.id ?? i}{s.statistics.min}{s.statistics.avg}{s.statistics.max}
    + +{/if} diff --git a/web/frontend/src/Status.root.svelte b/web/frontend/src/Status.root.svelte new file mode 100644 index 0000000..26842c8 --- /dev/null +++ b/web/frontend/src/Status.root.svelte @@ -0,0 +1,184 @@ + + + + + {#if $initq.fetching || $mainQuery.fetching} + + {:else if $initq.error} + {$initq.error.message} + {:else} + + {/if} + + + { + console.log('reload...') + + from = new Date(Date.now() - 5 * 60 * 1000) + to = new Date(Date.now()) + + $mainQuery.variables = { ...$mainQuery.variables, from: from, to: to } + $mainQuery.reexecute({ requestPolicy: 'network-only' }) + }} /> + + +{#if $mainQuery.error} + + + {$mainQuery.error.message} + + +{/if} +{#if $initq.data && $mainQuery.data} + {#each $initq.data.clusters.find(c => c.name == cluster).subClusters as subCluster, i} + + + + + + + + + + + + + + + + + + + + + + +
    SubCluster{subCluster.name}
    Allocated Nodes
    ({allocatedNodes[subCluster.name]} / {subCluster.numberOfNodes})
    Flop Rate
    ({flopRate[subCluster.name]} / {subCluster.flopRateSimd * subCluster.numberOfNodes})
    MemBw Rate
    ({memBwRate[subCluster.name]} / {subCluster.memoryBandwidth * subCluster.numberOfNodes})
    + +
    + {#key $mainQuery.data.nodeMetrics} + data.subCluster == subCluster.name))} /> + {/key} +
    +
    + {/each} + +
    +

    Top Users

    + {#key $mainQuery.data} + b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))} + label={(x) => x < $mainQuery.data.topUsers.length ? $mainQuery.data.topUsers[Math.floor(x)].name : '0'} /> + {/key} +
    +
    + + + {#each $mainQuery.data.topUsers.sort((a, b) => b.count - a.count) as { name, count }} + + + + + {/each} +
    NameNumber of Nodes
    {name}{count}
    +
    +
    +

    Top Projects

    + {#key $mainQuery.data} + b.count - a.count).map(({ count }, idx) => ({ count, value: idx }))} + label={(x) => x < $mainQuery.data.topProjects.length ? $mainQuery.data.topProjects[Math.floor(x)].name : '0'} /> + {/key} +
    +
    + + + {#each $mainQuery.data.topProjects.sort((a, b) => b.count - a.count) as { name, count }} + + {/each} +
    NameNumber of Nodes
    {name}{count}
    +
    +
    + +
    +

    Duration Distribution

    + {#key $mainQuery.data.stats} + + {/key} +
    +
    +

    Number of Nodes Distribution

    + {#key $mainQuery.data.stats} + + {/key} +
    +
    +{/if} diff --git a/web/frontend/src/Systems.root.svelte b/web/frontend/src/Systems.root.svelte new file mode 100644 index 0000000..fc2db8b --- /dev/null +++ b/web/frontend/src/Systems.root.svelte @@ -0,0 +1,118 @@ + + + + {#if $initq.error} + {$initq.error.message} + {:else if $initq.fetching} + + {:else} + + + + + + + Metric + + + + + + + Find Node + + + + {/if} + +
    + + + {#if $nodesQuery.error} + {$nodesQuery.error.message} + {:else if $nodesQuery.fetching || $initq.fetching} + + {:else} + h.host.includes(hostnameFilter) && h.metrics.some(m => m.name == selectedMetric && m.metric.scope == 'node')) + .map(h => ({ host: h.host, subCluster: h.subCluster, data: h.metrics.find(m => m.name == selectedMetric && m.metric.scope == 'node') })) + .sort((a, b) => a.host.localeCompare(b.host))}> + +

    {item.host} ({item.subCluster})

    + c.name == cluster)} + subCluster={item.subCluster} /> +
    + {/if} + +
    + diff --git a/web/frontend/src/Tag.svelte b/web/frontend/src/Tag.svelte new file mode 100644 index 0000000..76a94ec --- /dev/null +++ b/web/frontend/src/Tag.svelte @@ -0,0 +1,44 @@ + + + + + + + + {#if tag} + {tag.type}: {tag.name} + {:else} + Loading... + {/if} + diff --git a/web/frontend/src/TagManagement.svelte b/web/frontend/src/TagManagement.svelte new file mode 100644 index 0000000..747b092 --- /dev/null +++ b/web/frontend/src/TagManagement.svelte @@ -0,0 +1,173 @@ + + + + + (isOpen = !isOpen)}> + + Manage Tags + {#if pendingChange !== false} + + {:else} + + {/if} + + + + +
    + + + Search using "type: name". If no tag matches your search, + a button for creating a new one will appear. + + +
      + {#each allTagsFiltered as tag} + + + + + {#if pendingChange === tag.id} + + {:else if job.tags.find(t => t.id == tag.id)} + + {:else} + + {/if} + + + {:else} + + No tags matching + + {/each} +
    +
    + {#if newTagType && newTagName && isNewTag(newTagType, newTagName)} + + {:else if allTagsFiltered.length == 0} + Search Term is not a valid Tag (type: name) + {/if} +
    + + + +
    + + diff --git a/web/frontend/src/User.root.svelte b/web/frontend/src/User.root.svelte new file mode 100644 index 0000000..5a8d14d --- /dev/null +++ b/web/frontend/src/User.root.svelte @@ -0,0 +1,172 @@ + + + + {#if $initq.fetching} + + + + {:else if $initq.error} + + {$initq.error.message} + + {/if} + + + + + + + + { + let filters = [...detail.filters, { user: { eq: user.username } }] + $stats.variables = { filter: filters } + $stats.context.pause = false + $stats.reexecute() + jobList.update(filters) + }} /> + + + jobList.update()} /> + + +
    + + {#if $stats.error} + + {$stats.error.message} + + {:else if !$stats.data} + + + + {:else} + + + + + + + + {#if user.name} + + + + + {/if} + {#if user.email} + + + + + {/if} + + + + + + + + + + + + + + + + + +
    Username{scrambleNames ? scramble(user.username) : user.username}
    Name{scrambleNames ? scramble(user.name) : user.name}
    Email{user.email}
    Total Jobs{$stats.data.jobsStatistics[0].totalJobs}
    Short Jobs{$stats.data.jobsStatistics[0].shortJobs}
    Total Walltime{$stats.data.jobsStatistics[0].totalWalltime}
    Total Core Hours{$stats.data.jobsStatistics[0].totalCoreHours}
    + +
    + Walltime + {#key $stats.data.jobsStatistics[0].histDuration} + + {/key} +
    +
    + Number of Nodes + {#key $stats.data.jobsStatistics[0].histNumNodes} + + {/key} +
    + {/if} +
    +
    + + + + + + + + + \ No newline at end of file diff --git a/web/frontend/src/Zoom.svelte b/web/frontend/src/Zoom.svelte new file mode 100644 index 0000000..ae842fc --- /dev/null +++ b/web/frontend/src/Zoom.svelte @@ -0,0 +1,60 @@ + + +
    + + + + + + Window Size: + + + ({windowSize}%) + + + + Window Position: + + + +
    diff --git a/web/frontend/src/analysis.entrypoint.js b/web/frontend/src/analysis.entrypoint.js new file mode 100644 index 0000000..d889144 --- /dev/null +++ b/web/frontend/src/analysis.entrypoint.js @@ -0,0 +1,14 @@ +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] + ]) +}) diff --git a/web/frontend/src/cache-exchange.js b/web/frontend/src/cache-exchange.js new file mode 100644 index 0000000..c52843e --- /dev/null +++ b/web/frontend/src/cache-exchange.js @@ -0,0 +1,72 @@ +import { filter, map, merge, pipe, share, tap } from 'wonka'; + +/* + * Alternative to the default cacheExchange from urql (A GraphQL client). + * Mutations do not invalidate cached results, so in that regard, this + * implementation is inferior to the default one. Most people should probably + * use the standard cacheExchange and @urql/exchange-request-policy. This cache + * also ignores the 'network-and-cache' request policy. + * + * Options: + * ttl: How long queries are allowed to be cached (in milliseconds) + * maxSize: Max number of results cached. The oldest queries are removed first. + */ +export const expiringCacheExchange = ({ ttl, maxSize }) => ({ forward }) => { + const cache = new Map(); + const isCached = (operation) => { + if (operation.kind !== 'query' || operation.context.requestPolicy === 'network-only') + return false; + + if (!cache.has(operation.key)) + return false; + + let cacheEntry = cache.get(operation.key); + return Date.now() < cacheEntry.expiresAt; + }; + + return operations => { + let shared = share(operations); + return merge([ + pipe( + shared, + filter(operation => isCached(operation)), + map(operation => cache.get(operation.key).response) + ), + pipe( + shared, + filter(operation => !isCached(operation)), + forward, + tap(response => { + if (!response.operation || response.operation.kind !== 'query') + return; + + if (!response.data) + return; + + let now = Date.now(); + for (let cacheEntry of cache.values()) { + if (cacheEntry.expiresAt < now) { + cache.delete(cacheEntry.response.operation.key); + } + } + + if (cache.size > maxSize) { + let n = cache.size - maxSize + 1; + for (let key of cache.keys()) { + if (n-- == 0) + break; + + cache.delete(key); + } + } + + cache.set(response.operation.key, { + expiresAt: now + ttl, + response: response + }); + }) + ) + ]); + }; +}; + diff --git a/web/frontend/src/filters/Cluster.svelte b/web/frontend/src/filters/Cluster.svelte new file mode 100644 index 0000000..83c4d91 --- /dev/null +++ b/web/frontend/src/filters/Cluster.svelte @@ -0,0 +1,77 @@ + + + (isOpen = !isOpen)}> + + Select Cluster & Slurm Partition + + + {#if $initialized} +

    Cluster

    + + (pendingCluster = null, pendingPartition = null)}> + Any Cluster + + {#each clusters as cluster} + (pendingCluster = cluster.name, pendingPartition = null)}> + {cluster.name} + + {/each} + + {/if} + {#if $initialized && pendingCluster != null} +
    +

    Partiton

    + + (pendingPartition = null)}> + Any Partition + + {#each clusters.find(c => c.name == pendingCluster).partitions as partition} + (pendingPartition = partition)}> + {partition} + + {/each} + + {/if} +
    + + + + + +
    diff --git a/web/frontend/src/filters/DoubleRangeSlider.svelte b/web/frontend/src/filters/DoubleRangeSlider.svelte new file mode 100644 index 0000000..aca460a --- /dev/null +++ b/web/frontend/src/filters/DoubleRangeSlider.svelte @@ -0,0 +1,302 @@ + + + + +
    +
    + inputChanged(0, e)} /> + + Full Range: {min} - {max} + + inputChanged(1, e)} /> +
    +
    +
    +
    +
    +
    +
    + + diff --git a/web/frontend/src/filters/Duration.svelte b/web/frontend/src/filters/Duration.svelte new file mode 100644 index 0000000..b482b9c --- /dev/null +++ b/web/frontend/src/filters/Duration.svelte @@ -0,0 +1,95 @@ + + + (isOpen = !isOpen)}> + + Select Start Time + + +

    Between

    + + +
    + +
    +
    h
    +
    +
    + + +
    + +
    +
    m
    +
    +
    + +
    +

    and

    + + +
    + +
    +
    h
    +
    +
    + + +
    + +
    +
    m
    +
    +
    + +
    +
    + + + + + +
    diff --git a/web/frontend/src/filters/Filters.svelte b/web/frontend/src/filters/Filters.svelte new file mode 100644 index 0000000..410f445 --- /dev/null +++ b/web/frontend/src/filters/Filters.svelte @@ -0,0 +1,323 @@ + + + + + + + + + Filters + + + + Manage Filters + + {#if menuText} + {menuText} + + {/if} + (isClusterOpen = true)}> + Cluster/Partition + + (isJobStatesOpen = true)}> + Job States + + (isStartTimeOpen = true)}> + Start Time + + (isDurationOpen = true)}> + Duration + + (isTagsOpen = true)}> + Tags + + (isResourcesOpen = true)}> + Nodes/Accelerators + + (isStatsOpen = true)}> + (isStatsOpen = true)}/> Statistics + + {#if startTimeQuickSelect} + + Start Time Qick Selection + {#each [ + { text: 'Last 6hrs', seconds: 6*60*60 }, + { text: 'Last 12hrs', seconds: 12*60*60 }, + { text: 'Last 24hrs', seconds: 24*60*60 }, + { text: 'Last 48hrs', seconds: 48*60*60 }, + { text: 'Last 7 days', seconds: 7*24*60*60 }, + { text: 'Last 30 days', seconds: 30*24*60*60 } + ] as {text, seconds}} + { + filters.startTime.from = (new Date(Date.now() - seconds * 1000)).toISOString() + filters.startTime.to = (new Date(Date.now())).toISOString() + update() + }}> + {text} + + {/each} + {/if} + + + + + + {#if filters.cluster} + (isClusterOpen = true)}> + {filters.cluster} + {#if filters.partition} + ({filters.partition}) + {/if} + + {/if} + + {#if filters.states.length != allJobStates.length} + (isJobStatesOpen = true)}> + {filters.states.join(', ')} + + {/if} + + {#if filters.startTime.from || filters.startTime.to} + (isStartTimeOpen = true)}> + {new Date(filters.startTime.from).toLocaleString()} - {new Date(filters.startTime.to).toLocaleString()} + + {/if} + + {#if filters.duration.from || filters.duration.to} + (isDurationOpen = true)}> + {Math.floor(filters.duration.from / 3600)}h:{Math.floor(filters.duration.from % 3600 / 60)}m + - + {Math.floor(filters.duration.to / 3600)}h:{Math.floor(filters.duration.to % 3600 / 60)}m + + {/if} + + {#if filters.tags.length != 0} + (isTagsOpen = true)}> + {#each filters.tags as tagId} + + {/each} + + {/if} + + {#if filters.numNodes.from != null || filters.numNodes.to != null} + (isResourcesOpen = true)}> + Nodes: {filters.numNodes.from} - {filters.numNodes.to} + + {/if} + + {#if filters.stats.length > 0} + (isStatsOpen = true)}> + {filters.stats.map(stat => `${stat.text}: ${stat.from} - ${stat.to}`).join(', ')} + + {/if} + + + + update()} /> + + update()} /> + + update()} /> + + update()} /> + + update()} /> + + update()} /> + + update()} /> + + diff --git a/web/frontend/src/filters/InfoBox.svelte b/web/frontend/src/filters/InfoBox.svelte new file mode 100644 index 0000000..58fc8a5 --- /dev/null +++ b/web/frontend/src/filters/InfoBox.svelte @@ -0,0 +1,11 @@ + + + diff --git a/web/frontend/src/filters/JobStates.svelte b/web/frontend/src/filters/JobStates.svelte new file mode 100644 index 0000000..4e5db2e --- /dev/null +++ b/web/frontend/src/filters/JobStates.svelte @@ -0,0 +1,47 @@ + + + + (isOpen = !isOpen)}> + + Select Job States + + + + {#each allJobStates as state} + + + {state} + + {/each} + + + + + + + + diff --git a/web/frontend/src/filters/Resources.svelte b/web/frontend/src/filters/Resources.svelte new file mode 100644 index 0000000..4f895b5 --- /dev/null +++ b/web/frontend/src/filters/Resources.svelte @@ -0,0 +1,99 @@ + + + (isOpen = !isOpen)}> + + Select Number of Nodes, HWThreads and Accelerators + + +

    Number of Nodes

    + (pendingNumNodes = { from: detail[0], to: detail[1] })} + min={minNumNodes} max={maxNumNodes} + firstSlider={pendingNumNodes.from} secondSlider={pendingNumNodes.to} /> + + {#if maxNumAccelerators != null && maxNumAccelerators > 1} + (pendingNumAccelerators = { from: detail[0], to: detail[1] })} + min={minNumAccelerators} max={maxNumAccelerators} + firstSlider={pendingNumAccelerators.from} secondSlider={pendingNumAccelerators.to} /> + {/if} +
    + + + + + +
    diff --git a/web/frontend/src/filters/StartTime.svelte b/web/frontend/src/filters/StartTime.svelte new file mode 100644 index 0000000..c89851d --- /dev/null +++ b/web/frontend/src/filters/StartTime.svelte @@ -0,0 +1,90 @@ + + + (isOpen = !isOpen)}> + + Select Start Time + + +

    From

    + + + + + + + + +

    To

    + + + + + + + + +
    + + + + + +
    diff --git a/web/frontend/src/filters/Stats.svelte b/web/frontend/src/filters/Stats.svelte new file mode 100644 index 0000000..e7b658d --- /dev/null +++ b/web/frontend/src/filters/Stats.svelte @@ -0,0 +1,113 @@ + + + (isOpen = !isOpen)}> + + Filter based on statistics (of non-running jobs) + + + {#each statistics as stat} +

    {stat.text}

    + (stat.from = detail[0], stat.to = detail[1], stat.enabled = true)} + min={0} max={stat.peak} + firstSlider={stat.from} secondSlider={stat.to} /> + {/each} +
    + + + + + +
    diff --git a/web/frontend/src/filters/Tags.svelte b/web/frontend/src/filters/Tags.svelte new file mode 100644 index 0000000..b5a145a --- /dev/null +++ b/web/frontend/src/filters/Tags.svelte @@ -0,0 +1,67 @@ + + + (isOpen = !isOpen)}> + + Select Tags + + + +
    + + {#if $initialized} + {#each fuzzySearchTags(searchTerm, allTags) as tag (tag)} + + {#if pendingTags.includes(tag.id)} + + {:else} + + {/if} + + + + {:else} + No Tags + {/each} + {/if} + +
    + + + + + +
    diff --git a/web/frontend/src/filters/TimeSelection.svelte b/web/frontend/src/filters/TimeSelection.svelte new file mode 100644 index 0000000..7d7cca4 --- /dev/null +++ b/web/frontend/src/filters/TimeSelection.svelte @@ -0,0 +1,80 @@ + + + + + + + {#if timeRange == -1} + from + updateExplicitTimeRange('from', event)}> + to + updateExplicitTimeRange('to', event)}> + {/if} + diff --git a/web/frontend/src/filters/UserOrProject.svelte b/web/frontend/src/filters/UserOrProject.svelte new file mode 100644 index 0000000..7f9f183 --- /dev/null +++ b/web/frontend/src/filters/UserOrProject.svelte @@ -0,0 +1,51 @@ + + + + + termChanged()} on:keyup={(event) => termChanged(event.key == 'Enter' ? 0 : throttle)} + placeholder={mode == 'user' ? 'filter username...' : 'filter project...'} /> + diff --git a/web/frontend/src/header.entrypoint.js b/web/frontend/src/header.entrypoint.js new file mode 100644 index 0000000..25ff134 --- /dev/null +++ b/web/frontend/src/header.entrypoint.js @@ -0,0 +1,10 @@ +import Header from './Header.svelte' + +const headerDomTarget = document.getElementById('svelte-header') + +if (headerDomTarget != null) { + new Header({ + target: headerDomTarget, + props: { ...header }, + }) +} diff --git a/web/frontend/src/job.entrypoint.js b/web/frontend/src/job.entrypoint.js new file mode 100644 index 0000000..f7bceb8 --- /dev/null +++ b/web/frontend/src/job.entrypoint.js @@ -0,0 +1,12 @@ +import {} from './header.entrypoint.js' +import Job from './Job.root.svelte' + +new Job({ + target: document.getElementById('svelte-app'), + props: { + dbid: jobInfos.id + }, + context: new Map([ + ['cc-config', clusterCockpitConfig] + ]) +}) diff --git a/web/frontend/src/joblist/JobInfo.svelte b/web/frontend/src/joblist/JobInfo.svelte new file mode 100644 index 0000000..58472e5 --- /dev/null +++ b/web/frontend/src/joblist/JobInfo.svelte @@ -0,0 +1,88 @@ + + + + +
    +

    + {job.jobId} ({job.cluster}) + {#if job.metaData?.jobName} +
    + {job.metaData.jobName} + {/if} + {#if job.arrayJobId} + Array Job: #{job.arrayJobId} + {/if} +

    + +

    + + + {scrambleNames ? scramble(job.user) : job.user} + + {#if job.userData && job.userData.name} + ({scrambleNames ? scramble(job.userData.name) : job.userData.name}) + {/if} + {#if job.project && job.project != 'no project'} +
    + {job.project} + {/if} +

    + +

    + {job.numNodes} + {#if job.exclusive != 1} + (shared) + {/if} + {#if job.numAcc > 0} + , {job.numAcc} + {/if} + {#if job.numHWThreads > 0} + , {job.numHWThreads} + {/if} +

    + +

    + Start: {(new Date(job.startTime)).toLocaleString()} +
    + Duration: {formatDuration(job.duration)} + {#if job.state == 'running'} + running + {:else if job.state != 'completed'} + {job.state} + {/if} + {#if job.walltime} +
    + Walltime: {formatDuration(job.walltime)} + {/if} +

    + +

    + {#each jobTags as tag} + + {/each} +

    +
    diff --git a/web/frontend/src/joblist/JobList.svelte b/web/frontend/src/joblist/JobList.svelte new file mode 100644 index 0000000..8cdca26 --- /dev/null +++ b/web/frontend/src/joblist/JobList.svelte @@ -0,0 +1,190 @@ + + + + +
    + + + + + {#each metrics as metric (metric)} + + {/each} + + + + {#if $jobs.error} + + + + {:else if $jobs.fetching || !$jobs.data} + + + + {:else if $jobs.data && $initialized} + {#each $jobs.data.jobs.items as job (job)} + + {:else} + + + + {/each} + {/if} + +
    + Job Info + + {metric} + {#if $initialized} + ({clusters + .map(cluster => cluster.metricConfig.find(m => m.name == metric)) + .filter(m => m != null).map(m => m.unit) + .reduce((arr, unit) => arr.includes(unit) ? arr : [...arr, unit], []) + .join(', ')}) + {/if} +
    +

    {$jobs.error.message}

    +
    + +
    + No jobs found +
    +
    +
    + + { + if (detail.itemsPerPage != itemsPerPage) { + itemsPerPage = detail.itemsPerPage + updateConfiguration({ + name: "plot_list_jobsPerPage", + value: itemsPerPage.toString() + }).then(res => { + if (res.error) + console.error(res.error); + }) + } + + paging = { itemsPerPage: detail.itemsPerPage, page: detail.page } + }} /> + + diff --git a/web/frontend/src/joblist/Pagination.svelte b/web/frontend/src/joblist/Pagination.svelte new file mode 100644 index 0000000..f7b7453 --- /dev/null +++ b/web/frontend/src/joblist/Pagination.svelte @@ -0,0 +1,230 @@ + + +
    +
    + +
    + + +
    + + { (page - 1) * itemsPerPage } - { Math.min((page - 1) * itemsPerPage + itemsPerPage, totalItems) } of { totalItems } { itemText } + +
    +
    + {#if !backButtonDisabled} + + + {/if} + {#if !nextButtonDisabled} + + {/if} +
    +
    + + + + diff --git a/web/frontend/src/joblist/Refresher.svelte b/web/frontend/src/joblist/Refresher.svelte new file mode 100644 index 0000000..2587711 --- /dev/null +++ b/web/frontend/src/joblist/Refresher.svelte @@ -0,0 +1,43 @@ + + + + + + + \ No newline at end of file diff --git a/web/frontend/src/joblist/Row.svelte b/web/frontend/src/joblist/Row.svelte new file mode 100644 index 0000000..b3a3655 --- /dev/null +++ b/web/frontend/src/joblist/Row.svelte @@ -0,0 +1,101 @@ + + + + + + + + + {#if job.monitoringStatus == 0 || job.monitoringStatus == 2} + + Not monitored or archiving failed + + {:else if $metricsQuery.fetching} + + + + {:else if $metricsQuery.error} + + + {$metricsQuery.error.message.length > 500 + ? $metricsQuery.error.message.substring(0, 499)+'...' + : $metricsQuery.error.message} + + + {:else} + {#each sortAndSelectScope($metricsQuery.data.jobMetrics) as metric, i (metric || i)} + + {#if metric != null} + + {:else} + Missing Data + {/if} + + {/each} + {/if} + diff --git a/web/frontend/src/joblist/SortSelection.svelte b/web/frontend/src/joblist/SortSelection.svelte new file mode 100644 index 0000000..5941964 --- /dev/null +++ b/web/frontend/src/joblist/SortSelection.svelte @@ -0,0 +1,71 @@ + + + + + { isOpen = !isOpen }}> + + Sort rows + + + + {#each sortableColumns as col, i (col)} + + + + {col.text} + + {/each} + + + + + + + + \ No newline at end of file diff --git a/web/frontend/src/jobs.entrypoint.js b/web/frontend/src/jobs.entrypoint.js new file mode 100644 index 0000000..1763a8b --- /dev/null +++ b/web/frontend/src/jobs.entrypoint.js @@ -0,0 +1,12 @@ +import {} from './header.entrypoint.js' +import Jobs from './Jobs.root.svelte' + +new Jobs({ + target: document.getElementById('svelte-app'), + props: { + filterPresets: filterPresets + }, + context: new Map([ + ['cc-config', clusterCockpitConfig] + ]) +}) diff --git a/web/frontend/src/list.entrypoint.js b/web/frontend/src/list.entrypoint.js new file mode 100644 index 0000000..21c8f5d --- /dev/null +++ b/web/frontend/src/list.entrypoint.js @@ -0,0 +1,13 @@ +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] + ]) +}) diff --git a/web/frontend/src/node.entrypoint.js b/web/frontend/src/node.entrypoint.js new file mode 100644 index 0000000..e6e6f9a --- /dev/null +++ b/web/frontend/src/node.entrypoint.js @@ -0,0 +1,15 @@ +import {} from './header.entrypoint.js' +import Node from './Node.root.svelte' + +new Node({ + target: document.getElementById('svelte-app'), + props: { + cluster: infos.cluster, + hostname: infos.hostname, + from: infos.from, + to: infos.to + }, + context: new Map([ + ['cc-config', clusterCockpitConfig] + ]) +}) diff --git a/web/frontend/src/plots/Histogram.svelte b/web/frontend/src/plots/Histogram.svelte new file mode 100644 index 0000000..c00de12 --- /dev/null +++ b/web/frontend/src/plots/Histogram.svelte @@ -0,0 +1,210 @@ + + +
    (infoText = '')}> + {infoText} + +
    + + + + + + \ No newline at end of file diff --git a/web/frontend/src/plots/MetricPlot.svelte b/web/frontend/src/plots/MetricPlot.svelte new file mode 100644 index 0000000..d47d813 --- /dev/null +++ b/web/frontend/src/plots/MetricPlot.svelte @@ -0,0 +1,306 @@ + + + + +
    + diff --git a/web/frontend/src/plots/Polar.svelte b/web/frontend/src/plots/Polar.svelte new file mode 100644 index 0000000..6731d8a --- /dev/null +++ b/web/frontend/src/plots/Polar.svelte @@ -0,0 +1,190 @@ +
    + +
    + + diff --git a/web/frontend/src/plots/Roofline.svelte b/web/frontend/src/plots/Roofline.svelte new file mode 100644 index 0000000..d385f0d --- /dev/null +++ b/web/frontend/src/plots/Roofline.svelte @@ -0,0 +1,355 @@ +
    + +
    + + + + diff --git a/web/frontend/src/plots/Scatter.svelte b/web/frontend/src/plots/Scatter.svelte new file mode 100644 index 0000000..f3c955c --- /dev/null +++ b/web/frontend/src/plots/Scatter.svelte @@ -0,0 +1,171 @@ +
    + +
    + + + + diff --git a/web/frontend/src/status.entrypoint.js b/web/frontend/src/status.entrypoint.js new file mode 100644 index 0000000..39c374b --- /dev/null +++ b/web/frontend/src/status.entrypoint.js @@ -0,0 +1,12 @@ +import {} from './header.entrypoint.js' +import Status from './Status.root.svelte' + +new Status({ + target: document.getElementById('svelte-app'), + props: { + cluster: infos.cluster, + }, + context: new Map([ + ['cc-config', clusterCockpitConfig] + ]) +}) diff --git a/web/frontend/src/systems.entrypoint.js b/web/frontend/src/systems.entrypoint.js new file mode 100644 index 0000000..846bd36 --- /dev/null +++ b/web/frontend/src/systems.entrypoint.js @@ -0,0 +1,14 @@ +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] + ]) +}) diff --git a/web/frontend/src/user.entrypoint.js b/web/frontend/src/user.entrypoint.js new file mode 100644 index 0000000..0bff82a --- /dev/null +++ b/web/frontend/src/user.entrypoint.js @@ -0,0 +1,13 @@ +import {} from './header.entrypoint.js' +import User from './User.root.svelte' + +new User({ + target: document.getElementById('svelte-app'), + props: { + filterPresets: filterPresets, + user: userInfos + }, + context: new Map([ + ['cc-config', clusterCockpitConfig] + ]) +}) diff --git a/web/frontend/src/utils.js b/web/frontend/src/utils.js new file mode 100644 index 0000000..decfdc6 --- /dev/null +++ b/web/frontend/src/utils.js @@ -0,0 +1,288 @@ +import { expiringCacheExchange } from './cache-exchange.js' +import { initClient } from '@urql/svelte' +import { setContext, getContext, hasContext, onDestroy, tick } from 'svelte' +import { dedupExchange, fetchExchange } from '@urql/core' +import { readable } from 'svelte/store' + +/* + * Call this function only at component initialization time! + * + * It does several things: + * - Initialize the GraphQL client + * - Creates a readable store 'initialization' which indicates when the values below can be used. + * - Adds 'tags' to the context (list of all tags) + * - Adds 'clusters' to the context (object with cluster names as keys) + * - Adds 'metrics' to the context, a function that takes a cluster and metric name and returns the MetricConfig (or undefined) + */ +export function init(extraInitQuery = '') { + const jwt = hasContext('jwt') + ? getContext('jwt') + : getContext('cc-config')['jwt'] + + const client = initClient({ + url: `${window.location.origin}/query`, + fetchOptions: jwt != null + ? { headers: { 'Authorization': `Bearer ${jwt}` } } : {}, + exchanges: [ + dedupExchange, + expiringCacheExchange({ + ttl: 5 * 60 * 1000, + maxSize: 150, + }), + fetchExchange + ] + }) + + const query = client.query(`query { + clusters { + name, + metricConfig { + name, unit, peak, + normal, caution, alert, + timestep, scope, + aggregation, + subClusters { name, peak, normal, caution, alert } + } + filterRanges { + duration { from, to } + numNodes { from, to } + startTime { from, to } + } + partitions + subClusters { + name, processorType + socketsPerNode + coresPerSocket + threadsPerCore + flopRateScalar + flopRateSimd + memoryBandwidth + numberOfNodes + topology { + node, socket, core + accelerators { id } + } + } + } + tags { id, name, type } + ${extraInitQuery} + }`).toPromise() + + let state = { fetching: true, error: null, data: null } + let subscribers = [] + const subscribe = (callback) => { + callback(state) + subscribers.push(callback) + return () => { + subscribers = subscribers.filter(cb => cb != callback) + } + }; + + const tags = [], clusters = [] + setContext('tags', tags) + setContext('clusters', clusters) + setContext('metrics', (cluster, metric) => { + if (typeof cluster !== 'object') + cluster = clusters.find(c => c.name == cluster) + + return cluster.metricConfig.find(m => m.name == metric) + }) + setContext('on-init', callback => state.fetching + ? subscribers.push(callback) + : callback(state)) + setContext('initialized', readable(false, (set) => + subscribers.push(() => set(true)))) + + query.then(({ error, data }) => { + state.fetching = false + if (error != null) { + console.error(error) + state.error = error + tick().then(() => subscribers.forEach(cb => cb(state))) + return + } + + for (let tag of data.tags) + tags.push(tag) + + for (let cluster of data.clusters) + clusters.push(cluster) + + state.data = data + tick().then(() => subscribers.forEach(cb => cb(state))) + }) + + return { + query: { subscribe }, + tags, + clusters, + } +} + +export function formatNumber(x) { + let suffix = '' + if (x >= 1000000000) { + x /= 1000000 + suffix = 'G' + } else if (x >= 1000000) { + x /= 1000000 + suffix = 'M' + } else if (x >= 1000) { + x /= 1000 + suffix = 'k' + } + + return `${(Math.round(x * 100) / 100)}${suffix}` +} + +// Use https://developer.mozilla.org/en-US/docs/Web/API/structuredClone instead? +export function deepCopy(x) { + return JSON.parse(JSON.stringify(x)) +} + +function fuzzyMatch(term, string) { + return string.toLowerCase().includes(term) +} + +export function fuzzySearchTags(term, tags) { + if (!tags) + return [] + + let results = [] + let termparts = term.split(':').map(s => s.trim()).filter(s => s.length > 0) + + if (termparts.length == 0) { + results = tags.slice() + } else if (termparts.length == 1) { + for (let tag of tags) + if (fuzzyMatch(termparts[0], tag.type) + || fuzzyMatch(termparts[0], tag.name)) + results.push(tag) + } else if (termparts.length == 2) { + for (let tag of tags) + if (fuzzyMatch(termparts[0], tag.type) + && fuzzyMatch(termparts[1], tag.name)) + results.push(tag) + } + + return results.sort((a, b) => { + if (a.type < b.type) return -1 + if (a.type > b.type) return 1 + if (a.name < b.name) return -1 + if (a.name > b.name) return 1 + return 0 + }) +} + +export function groupByScope(jobMetrics) { + let metrics = new Map() + for (let metric of jobMetrics) { + if (metrics.has(metric.name)) + metrics.get(metric.name).push(metric) + else + metrics.set(metric.name, [metric]) + } + + return [...metrics.values()].sort((a, b) => a[0].name.localeCompare(b[0].name)) +} + +const scopeGranularity = { + "node": 10, + "socket": 5, + "accelerator": 5, + "core": 2, + "hwthread": 1 +}; + +export function maxScope(scopes) { + console.assert(scopes.length > 0 && scopes.every(x => scopeGranularity[x] != null)) + let sm = scopes[0], gran = scopeGranularity[scopes[0]] + for (let scope of scopes) { + let otherGran = scopeGranularity[scope] + if (otherGran > gran) { + sm = scope + gran = otherGran + } + } + return sm +} + +export function minScope(scopes) { + console.assert(scopes.length > 0 && scopes.every(x => scopeGranularity[x] != null)) + let sm = scopes[0], gran = scopeGranularity[scopes[0]] + for (let scope of scopes) { + let otherGran = scopeGranularity[scope] + if (otherGran < gran) { + sm = scope + gran = otherGran + } + } + return sm +} + +export async function fetchMetrics(job, metrics, scopes) { + if (job.monitoringStatus == 0) + return null + + let query = [] + if (metrics != null) { + for (let metric of metrics) { + query.push(`metric=${metric}`) + } + } + if (scopes != null) { + for (let scope of scopes) { + query.push(`scope=${scope}`) + } + } + + try { + let res = await fetch(`/api/jobs/metrics/${job.id}${(query.length > 0) ? '?' : ''}${query.join('&')}`) + if (res.status != 200) { + return { error: { status: res.status, message: await res.text() } } + } + + return await res.json() + } catch (e) { + return { error: e } + } +} + +export function fetchMetricsStore() { + let set = null + return [ + readable({ fetching: true, error: null, data: null }, (_set) => { set = _set }), + (job, metrics, scopes) => fetchMetrics(job, metrics, scopes).then(res => set({ + fetching: false, + error: res.error, + data: res.data + })) + ] +} + +export function stickyHeader(datatableHeaderSelector, updatePading) { + const header = document.querySelector('header > nav.navbar') + if (!header) + return + + let ticking = false, datatableHeader = null + const onscroll = event => { + if (ticking) + return + + ticking = true + window.requestAnimationFrame(() => { + ticking = false + if (!datatableHeader) + datatableHeader = document.querySelector(datatableHeaderSelector) + + const top = datatableHeader.getBoundingClientRect().top + updatePading(top < header.clientHeight + ? (header.clientHeight - top) + 10 + : 10) + }) + } + + document.addEventListener('scroll', onscroll) + onDestroy(() => document.removeEventListener('scroll', onscroll)) +} diff --git a/web/frontend/yarn.lock b/web/frontend/yarn.lock new file mode 100644 index 0000000..f80e078 --- /dev/null +++ b/web/frontend/yarn.lock @@ -0,0 +1,493 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.10.4": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.0.tgz#0dfc80309beec8411e65e706461c408b0bb9b431" + integrity sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA== + dependencies: + "@babel/highlight" "^7.16.0" + +"@babel/helper-validator-identifier@^7.15.7": + version "7.15.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" + integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== + +"@babel/highlight@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.0.tgz#6ceb32b2ca4b8f5f361fb7fd821e3fddf4a1725a" + integrity sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g== + dependencies: + "@babel/helper-validator-identifier" "^7.15.7" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@graphql-typed-document-node/core@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.1.tgz#076d78ce99822258cf813ecc1e7fa460fa74d052" + integrity sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg== + +"@popperjs/core@^2.9.2": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.0.tgz#6734f8ebc106a0860dff7f92bf90df193f0935d7" + integrity sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ== + +"@rollup/plugin-commonjs@^17.0.0": + version "17.1.0" + resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz#757ec88737dffa8aa913eb392fade2e45aef2a2d" + integrity sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew== + dependencies: + "@rollup/pluginutils" "^3.1.0" + commondir "^1.0.1" + estree-walker "^2.0.1" + glob "^7.1.6" + is-reference "^1.2.1" + magic-string "^0.25.7" + resolve "^1.17.0" + +"@rollup/plugin-node-resolve@^11.0.0": + version "11.2.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz#82aa59397a29cd4e13248b106e6a4a1880362a60" + integrity sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + "@types/resolve" "1.17.1" + builtin-modules "^3.1.0" + deepmerge "^4.2.2" + is-module "^1.0.0" + resolve "^1.19.0" + +"@rollup/plugin-replace@^2.4.1": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz#a2d539314fbc77c244858faa523012825068510a" + integrity sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + magic-string "^0.25.7" + +"@rollup/pluginutils@4": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.1.tgz#1d4da86dd4eded15656a57d933fda2b9a08d47ec" + integrity sha512-clDjivHqWGXi7u+0d2r2sBi4Ie6VLEAzWMIkvJLnDmxoOhBYOTfzGbOQBA32THHm11/LiJbd01tJUpJsbshSWQ== + dependencies: + estree-walker "^2.0.1" + picomatch "^2.2.2" + +"@rollup/pluginutils@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" + integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== + dependencies: + "@types/estree" "0.0.39" + estree-walker "^1.0.1" + picomatch "^2.2.2" + +"@types/estree@*": + version "0.0.50" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" + integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== + +"@types/estree@0.0.39": + version "0.0.39" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== + +"@types/node@*": + version "16.11.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.12.tgz#ac7fb693ac587ee182c3780c26eb65546a1a3c10" + integrity sha512-+2Iggwg7PxoO5Kyhvsq9VarmPbIelXP070HMImEpbtGCoyWNINQj4wzjbQCXzdHTRXnqufutJb5KAURZANNBAw== + +"@types/resolve@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" + integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== + dependencies: + "@types/node" "*" + +"@urql/core@^2.3.4": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@urql/core/-/core-2.3.5.tgz#eb1cbbfe23236615ecb8e65850bb772d4f61b6b5" + integrity sha512-kM/um4OjXmuN6NUS/FSm7dESEKWT7By1kCRCmjvU4+4uEoF1cd4TzIhQ7J1I3zbDAFhZzmThq9X0AHpbHAn3bA== + dependencies: + "@graphql-typed-document-node/core" "^3.1.0" + wonka "^4.0.14" + +"@urql/svelte@^1.3.0": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@urql/svelte/-/svelte-1.3.2.tgz#7fc16253a36669dddec39755fc9c31077a9c279a" + integrity sha512-L/fSKb+jTrxfeKbnA4+7T69sL0XlzMv4d9i0j9J+fCkBCpUOGgPsYzsyBttbVbjrlaw61Wrc6J2NKuokrd570w== + dependencies: + "@urql/core" "^2.3.4" + wonka "^4.0.14" + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +builtin-modules@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" + integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA== + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +estree-walker@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" + integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== + +estree-walker@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" + integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== + +estree-walker@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +glob@^7.1.6: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +graphql@^15.6.0: + version "15.8.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" + integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-core-module@^2.2.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548" + integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw== + dependencies: + has "^1.0.3" + +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= + +is-reference@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" + integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== + dependencies: + "@types/estree" "*" + +jest-worker@^26.2.1: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +magic-string@^0.25.7: + version "0.25.7" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" + integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== + dependencies: + sourcemap-codec "^1.4.4" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-parse@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picomatch@^2.2.2: + version "2.3.0" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" + integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +require-relative@^0.8.7: + version "0.8.7" + resolved "https://registry.yarnpkg.com/require-relative/-/require-relative-0.8.7.tgz#7999539fc9e047a37928fa196f8e1563dabd36de" + integrity sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4= + +resolve@^1.17.0, resolve@^1.19.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + +rollup-plugin-css-only@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-css-only/-/rollup-plugin-css-only-3.1.0.tgz#6a701cc5b051c6b3f0961e69b108a9a118e1b1df" + integrity sha512-TYMOE5uoD76vpj+RTkQLzC9cQtbnJNktHPB507FzRWBVaofg7KhIqq1kGbcVOadARSozWF883Ho9KpSPKH8gqA== + dependencies: + "@rollup/pluginutils" "4" + +rollup-plugin-svelte@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-svelte/-/rollup-plugin-svelte-7.1.0.tgz#d45f2b92b1014be4eb46b55aa033fb9a9c65f04d" + integrity sha512-vopCUq3G+25sKjwF5VilIbiY6KCuMNHP1PFvx2Vr3REBNMDllKHFZN2B9jwwC+MqNc3UPKkjXnceLPEjTjXGXg== + dependencies: + require-relative "^0.8.7" + rollup-pluginutils "^2.8.2" + +rollup-plugin-terser@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d" + integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== + dependencies: + "@babel/code-frame" "^7.10.4" + jest-worker "^26.2.1" + serialize-javascript "^4.0.0" + terser "^5.0.0" + +rollup-pluginutils@^2.8.2: + version "2.8.2" + resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" + integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ== + dependencies: + estree-walker "^0.6.1" + +rollup@^2.3.4: + version "2.61.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.61.0.tgz#ccd927bcd6cc0c78a4689c918627a717977208f4" + integrity sha512-teQ+T1mUYbyvGyUavCodiyA9hD4DxwYZJwr/qehZGhs1Z49vsmzelMVYMxGU4ZhGRKxYPupHuz5yzm/wj7VpWA== + optionalDependencies: + fsevents "~2.3.2" + +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@~0.7.2: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + +sourcemap-codec@^1.4.4: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +svelte@^3.42.6: + version "3.44.2" + resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.44.2.tgz#3e69be2598308dfc8354ba584cec54e648a50f7f" + integrity sha512-jrZhZtmH3ZMweXg1Q15onb8QlWD+a5T5Oca4C1jYvSURp2oD35h4A5TV6t6MEa93K4LlX6BkafZPdQoFjw/ylA== + +sveltestrap@^5.6.1: + version "5.6.3" + resolved "https://registry.yarnpkg.com/sveltestrap/-/sveltestrap-5.6.3.tgz#afb81b00d0b378719988e5339f92254dce41194f" + integrity sha512-/geTKJbPmJGzwHFKYC3NkUNDk/GKxrppgdSxcg58w/qcxs0S6RiN4PaQ1tgBKsdSrZDfbHfkFF+dybHAyUlV0A== + dependencies: + "@popperjs/core" "^2.9.2" + +terser@^5.0.0: + version "5.10.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.10.0.tgz#b86390809c0389105eb0a0b62397563096ddafcc" + integrity sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA== + dependencies: + commander "^2.20.0" + source-map "~0.7.2" + source-map-support "~0.5.20" + +uplot@^1.6.7: + version "1.6.17" + resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.6.17.tgz#1f8fc07a0e48008798beca463523621ad66dcc46" + integrity sha512-WHNHvDCXURn+Qwb3QUUzP6rOxx+3kUZUspREyhkqmXCxFIND99l5z9intTh+uPEt+/EEu7lCaMjSd1uTfuTXfg== + +wonka@^4.0.14, wonka@^4.0.15: + version "4.0.15" + resolved "https://registry.yarnpkg.com/wonka/-/wonka-4.0.15.tgz#9aa42046efa424565ab8f8f451fcca955bf80b89" + integrity sha512-U0IUQHKXXn6PFo9nqsHphVCE5m3IntqZNB9Jjn7EB1lrR7YTDY3YWgFvEvwniTzXSvOH/XMzAZaIfJF/LvHYXg== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=