From 6d7aef616b2dc54f512af1dd1f540e566f8198e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20SZKIBA?= Date: Tue, 15 Aug 2023 19:56:53 +0200 Subject: [PATCH] feat: Save report to single responsible HTML file --- .gitignore | 2 +- assets/assets.go | 6 +- assets/assets_test.go | 17 +- assets/brief/boot.js | 186 ++++ assets/brief/index.html | 31 + assets/brief/init.js | 1 + assets/packages/brief/.gitignore | 28 + assets/packages/brief/.testcontext.js | 20 + assets/packages/brief/index.html | 19 + assets/packages/brief/package.json | 30 + assets/packages/brief/public/boot.js | 186 ++++ assets/packages/brief/public/init.js | 1 + assets/packages/brief/src/Brief.css | 10 + assets/packages/brief/src/Brief.jsx | 80 ++ assets/packages/brief/src/Chart.css | 32 + assets/packages/brief/src/Chart.jsx | 55 + assets/packages/brief/src/Digest.css | 43 + assets/packages/brief/src/Digest.jsx | 52 + assets/packages/brief/src/Summary.css | 5 + assets/packages/brief/src/Summary.jsx | 40 + assets/packages/brief/src/data.js | 26 + assets/packages/brief/src/index.css | 37 + assets/packages/brief/src/main.jsx | 15 + assets/packages/brief/src/metrics-uplot.js | 75 ++ assets/packages/brief/src/metrics.js | 55 + assets/packages/brief/src/styles.scss | 23 + assets/packages/brief/src/summary.js | 85 ++ assets/packages/brief/src/util.js | 11 + assets/packages/brief/vite.config.js | 16 + assets/packages/brief/yarn.lock | 1071 ++++++++++++++++++++ assets/packages/ui/index.html | 32 +- assets/ui/index.html | 7 - dashboard/brief.go | 226 +++++ dashboard/event.go | 26 + dashboard/extension.go | 17 +- dashboard/extension_test.go | 6 +- dashboard/options.go | 3 + dashboard/options_test.go | 4 +- dashboard/replay_test.go | 1 + dashboard/sse.go | 8 + go.mod | 2 +- go.sum | 4 +- magefiles/magefile.go | 22 +- register.go | 2 +- script-hour.js | 36 + 45 files changed, 2612 insertions(+), 42 deletions(-) create mode 100644 assets/brief/boot.js create mode 100644 assets/brief/index.html create mode 100644 assets/brief/init.js create mode 100644 assets/packages/brief/.gitignore create mode 100644 assets/packages/brief/.testcontext.js create mode 100644 assets/packages/brief/index.html create mode 100644 assets/packages/brief/package.json create mode 100644 assets/packages/brief/public/boot.js create mode 100644 assets/packages/brief/public/init.js create mode 100644 assets/packages/brief/src/Brief.css create mode 100644 assets/packages/brief/src/Brief.jsx create mode 100644 assets/packages/brief/src/Chart.css create mode 100644 assets/packages/brief/src/Chart.jsx create mode 100644 assets/packages/brief/src/Digest.css create mode 100644 assets/packages/brief/src/Digest.jsx create mode 100644 assets/packages/brief/src/Summary.css create mode 100644 assets/packages/brief/src/Summary.jsx create mode 100644 assets/packages/brief/src/data.js create mode 100644 assets/packages/brief/src/index.css create mode 100644 assets/packages/brief/src/main.jsx create mode 100644 assets/packages/brief/src/metrics-uplot.js create mode 100644 assets/packages/brief/src/metrics.js create mode 100644 assets/packages/brief/src/styles.scss create mode 100644 assets/packages/brief/src/summary.js create mode 100644 assets/packages/brief/src/util.js create mode 100644 assets/packages/brief/vite.config.js create mode 100644 assets/packages/brief/yarn.lock create mode 100644 dashboard/brief.go create mode 100644 script-hour.js diff --git a/.gitignore b/.gitignore index 37d48b4..ab10aff 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ k6 -test_result.* +test_result* coverage.txt diff --git a/assets/assets.go b/assets/assets.go index 793c42f..d554089 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -9,13 +9,17 @@ import ( "io/fs" ) -//go:embed ui +//go:embed ui brief var distFS embed.FS func DirUI() fs.FS { return dir("ui") } +func DirBrief() fs.FS { + return dir("brief") +} + func dir(dirname string) fs.FS { subfs, err := fs.Sub(distFS, dirname) if err != nil { diff --git a/assets/assets_test.go b/assets/assets_test.go index cd94c47..7c71271 100644 --- a/assets/assets_test.go +++ b/assets/assets_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestGetFS(t *testing.T) { +func TestDirUI(t *testing.T) { t.Parallel() fs := DirUI() @@ -24,3 +24,18 @@ func TestGetFS(t *testing.T) { assert.NoError(t, file.Close()) } + +func TestDirBrief(t *testing.T) { + t.Parallel() + + fs := DirBrief() + + assert.NotNil(t, fs) + + file, err := fs.Open("index.html") + + assert.NoError(t, err) + assert.NotNil(t, file) + + assert.NoError(t, file.Close()) +} diff --git a/assets/brief/boot.js b/assets/brief/boot.js new file mode 100644 index 0000000..3dd7dbf --- /dev/null +++ b/assets/brief/boot.js @@ -0,0 +1,186 @@ +const overviewPanels = [ + { + id: 'iterations', + title: 'Iterations', + metric: 'iterations_counter_count', + format: 'counter' + }, + { + id: 'vus', + title: 'Virtual Users', + metric: 'vus_gauge_value', + format: 'counter' + }, + { + id: 'http_reqs', + title: 'Request Rate', + metric: 'http_reqs_counter_rate', + format: 'rps' + }, + { + id: 'http_req_duration', + title: 'Request Duration', + metric: 'http_req_duration_trend_avg', + format: 'duration' + }, + { + id: 'data_received', + title: 'Received Rate', + metric: 'data_received_counter_rate', + format: 'bps' + }, + { + id: 'data_sent', + title: 'Sent Rate', + metric: 'data_sent_counter_rate', + format: 'bps' + } +] + +const overviewCharts = [ + { + id: 'http_reqs', + title: 'Generated Load', + series: { + vus_gauge_value: { label: 'user count', width: 2, scale: 'n' }, + http_reqs_counter_rate: { label: 'request rate', scale: '1/s' } + }, + axes: [{}, { scale: 'n' }, { scale: '1/s', side: 1 }], + scales: [{}, {}, {}] + }, + { + id: 'data', + title: 'Transfer Rate (byte/sec)', + series: { + data_sent_counter_rate: { label: 'data sent', rate: true, scale: 'sent' }, + data_received_counter_rate: { + label: 'data received', + rate: true, + with: 2, + scale: 'received' + } + }, + axes: [{}, { scale: 'sent' }, { scale: 'received', side: 1 }] + }, + { + id: 'http_req_duration', + title: 'Request Duration (ms)', + series: { + http_req_duration_trend_avg: { label: 'avg', width: 2 }, + 'http_req_duration_trend_p(90)': { label: 'p(90)' }, + 'http_req_duration_trend_p(95)': { label: 'p(95)' } + }, + axes: [{}, {}, { side: 1 }] + }, + { + id: 'iteration_duration', + title: 'Iteration Duration (ms)', + series: { + iteration_duration_trend_avg: { label: 'avg', width: 2 }, + 'iteration_duration_trend_p(90)': { label: 'p(90)' }, + 'iteration_duration_trend_p(95)': { label: 'p(95)' } + }, + axes: [{}, {}, { side: 1 }] + } +] + +function suffix (event) { + return event == 'snapshot' ? '' : ' (cum)' +} + +function reportable (event) { + return event == 'snapshot' +} + +function tabOverview (event) { + return { + id: `overview_${event}`, + title: `Overview${suffix(event)}`, + event: event, + panels: overviewPanels, + charts: overviewCharts, + description: + 'This section provides an overview of the most important metrics of the test run. Graphs plot the value of metrics over time.' + } +} + +function chartTimings (metric, title) { + return { + id: metric, + title: title, + series: { + [`${metric}_trend_avg`]: { label: 'avg', width: 2 }, + [`${metric}_trend_p(90)`]: { label: 'p(90)' }, + [`${metric}_trend_p(95)`]: { label: 'p(95)' } + }, + axes: [{}, {}, { side: 1 }], + height: 224 + } +} + +function tabTimings (event) { + return { + id: `timings_${event}`, + title: `Timings${suffix(event)}`, + event: event, + charts: [ + chartTimings('http_req_duration', 'Request Duration (ms)'), + chartTimings('http_req_waiting', 'Request Waiting (ms)'), + chartTimings('http_req_tls_handshaking', 'TLS handshaking (ms)'), + chartTimings('http_req_sending', 'Request Sending (ms)'), + chartTimings('http_req_connecting', 'Request Connecting (ms)'), + chartTimings('http_req_receiving', 'Request Receiving (ms)') + ], + report: reportable(event), + description: + 'This section provides an overview of test run HTTP timing metrics. Graphs plot the value of metrics over time.' + } +} + +const defaultConfig = { + title: 'k6 dashboard', + tabs: [ + tabOverview('snapshot'), + tabOverview('cumulative'), + tabTimings('snapshot'), + tabTimings('cumulative'), + ], + + tab (id) { + let tab = null + + for (const t of this.tabs) { + if (t.id == id) { + tab = t + + break + } + } + + if (tab == null) { + tab = { id: id } + + this.tabs.push(tab) + } + + let lookup = (collection, id) => { + for (const item of collection) { + if (item.id == id) { + return item + } + } + + let item = { id: id } + collection.push(item) + + return item + } + + tab.chart = id => lookup(tab.charts, id) + tab.panel = id => lookup(tab.panels, id) + + return tab + } +} + +window.defaultConfig = defaultConfig diff --git a/assets/brief/index.html b/assets/brief/index.html new file mode 100644 index 0000000..7917e21 --- /dev/null +++ b/assets/brief/index.html @@ -0,0 +1,31 @@ + + + + + + + + k6 report + + + + + + +
+ + + + + diff --git a/assets/brief/init.js b/assets/brief/init.js new file mode 100644 index 0000000..6377104 --- /dev/null +++ b/assets/brief/init.js @@ -0,0 +1 @@ +window.config = window.config || window.defaultConfig diff --git a/assets/packages/brief/.gitignore b/assets/packages/brief/.gitignore new file mode 100644 index 0000000..c1ffd2e --- /dev/null +++ b/assets/packages/brief/.gitignore @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2023 Iván Szkiba +# +# SPDX-License-Identifier: MIT + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/assets/packages/brief/.testcontext.js b/assets/packages/brief/.testcontext.js new file mode 100644 index 0000000..dafa506 --- /dev/null +++ b/assets/packages/brief/.testcontext.js @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2023 Iván Szkiba +// +// SPDX-License-Identifier: MIT + +import {readFileSync} from 'fs'; + +let testconfig = "" +let testdata = "" + +if (process.env.NODE_ENV != "production") { + let head = readFileSync("./public/boot.js").toString() + let cfg = readFileSync("../../../.dashboard.js").toString() + let tail = readFileSync("./public/init.js").toString() + + testconfig = head + cfg + tail + + testdata = "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" +} + +export default {testdata,testconfig} diff --git a/assets/packages/brief/index.html b/assets/packages/brief/index.html new file mode 100644 index 0000000..bf48430 --- /dev/null +++ b/assets/packages/brief/index.html @@ -0,0 +1,19 @@ + + + + + + + + k6 report + + + + +
+ + + + + diff --git a/assets/packages/brief/package.json b/assets/packages/brief/package.json new file mode 100644 index 0000000..6f06f08 --- /dev/null +++ b/assets/packages/brief/package.json @@ -0,0 +1,30 @@ +{ + "name": "xk6-dashboard-brief", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build --emptyOutDir", + "preview": "vite preview" + }, + "dependencies": { + "bootstrap": "^5.3.1", + "byte-size": "^8.1.1", + "humanize-duration": "^3.28.0", + "numeral": "^2.0.6", + "preact": "^10.16.0", + "pretty-bytes": "^6.1.0", + "pretty-ms": "^8.0.0", + "round-to": "^6.0.0", + "uplot": "^1.6.24", + "uplot-react": "^1.1.4" + }, + "devDependencies": { + "@preact/preset-vite": "^2.5.0", + "sass": "^1.65.1", + "vite": "^4.4.5", + "vite-plugin-handlebars": "^1.6.0", + "vite-plugin-singlefile": "^0.13.5" + } +} diff --git a/assets/packages/brief/public/boot.js b/assets/packages/brief/public/boot.js new file mode 100644 index 0000000..3dd7dbf --- /dev/null +++ b/assets/packages/brief/public/boot.js @@ -0,0 +1,186 @@ +const overviewPanels = [ + { + id: 'iterations', + title: 'Iterations', + metric: 'iterations_counter_count', + format: 'counter' + }, + { + id: 'vus', + title: 'Virtual Users', + metric: 'vus_gauge_value', + format: 'counter' + }, + { + id: 'http_reqs', + title: 'Request Rate', + metric: 'http_reqs_counter_rate', + format: 'rps' + }, + { + id: 'http_req_duration', + title: 'Request Duration', + metric: 'http_req_duration_trend_avg', + format: 'duration' + }, + { + id: 'data_received', + title: 'Received Rate', + metric: 'data_received_counter_rate', + format: 'bps' + }, + { + id: 'data_sent', + title: 'Sent Rate', + metric: 'data_sent_counter_rate', + format: 'bps' + } +] + +const overviewCharts = [ + { + id: 'http_reqs', + title: 'Generated Load', + series: { + vus_gauge_value: { label: 'user count', width: 2, scale: 'n' }, + http_reqs_counter_rate: { label: 'request rate', scale: '1/s' } + }, + axes: [{}, { scale: 'n' }, { scale: '1/s', side: 1 }], + scales: [{}, {}, {}] + }, + { + id: 'data', + title: 'Transfer Rate (byte/sec)', + series: { + data_sent_counter_rate: { label: 'data sent', rate: true, scale: 'sent' }, + data_received_counter_rate: { + label: 'data received', + rate: true, + with: 2, + scale: 'received' + } + }, + axes: [{}, { scale: 'sent' }, { scale: 'received', side: 1 }] + }, + { + id: 'http_req_duration', + title: 'Request Duration (ms)', + series: { + http_req_duration_trend_avg: { label: 'avg', width: 2 }, + 'http_req_duration_trend_p(90)': { label: 'p(90)' }, + 'http_req_duration_trend_p(95)': { label: 'p(95)' } + }, + axes: [{}, {}, { side: 1 }] + }, + { + id: 'iteration_duration', + title: 'Iteration Duration (ms)', + series: { + iteration_duration_trend_avg: { label: 'avg', width: 2 }, + 'iteration_duration_trend_p(90)': { label: 'p(90)' }, + 'iteration_duration_trend_p(95)': { label: 'p(95)' } + }, + axes: [{}, {}, { side: 1 }] + } +] + +function suffix (event) { + return event == 'snapshot' ? '' : ' (cum)' +} + +function reportable (event) { + return event == 'snapshot' +} + +function tabOverview (event) { + return { + id: `overview_${event}`, + title: `Overview${suffix(event)}`, + event: event, + panels: overviewPanels, + charts: overviewCharts, + description: + 'This section provides an overview of the most important metrics of the test run. Graphs plot the value of metrics over time.' + } +} + +function chartTimings (metric, title) { + return { + id: metric, + title: title, + series: { + [`${metric}_trend_avg`]: { label: 'avg', width: 2 }, + [`${metric}_trend_p(90)`]: { label: 'p(90)' }, + [`${metric}_trend_p(95)`]: { label: 'p(95)' } + }, + axes: [{}, {}, { side: 1 }], + height: 224 + } +} + +function tabTimings (event) { + return { + id: `timings_${event}`, + title: `Timings${suffix(event)}`, + event: event, + charts: [ + chartTimings('http_req_duration', 'Request Duration (ms)'), + chartTimings('http_req_waiting', 'Request Waiting (ms)'), + chartTimings('http_req_tls_handshaking', 'TLS handshaking (ms)'), + chartTimings('http_req_sending', 'Request Sending (ms)'), + chartTimings('http_req_connecting', 'Request Connecting (ms)'), + chartTimings('http_req_receiving', 'Request Receiving (ms)') + ], + report: reportable(event), + description: + 'This section provides an overview of test run HTTP timing metrics. Graphs plot the value of metrics over time.' + } +} + +const defaultConfig = { + title: 'k6 dashboard', + tabs: [ + tabOverview('snapshot'), + tabOverview('cumulative'), + tabTimings('snapshot'), + tabTimings('cumulative'), + ], + + tab (id) { + let tab = null + + for (const t of this.tabs) { + if (t.id == id) { + tab = t + + break + } + } + + if (tab == null) { + tab = { id: id } + + this.tabs.push(tab) + } + + let lookup = (collection, id) => { + for (const item of collection) { + if (item.id == id) { + return item + } + } + + let item = { id: id } + collection.push(item) + + return item + } + + tab.chart = id => lookup(tab.charts, id) + tab.panel = id => lookup(tab.panels, id) + + return tab + } +} + +window.defaultConfig = defaultConfig diff --git a/assets/packages/brief/public/init.js b/assets/packages/brief/public/init.js new file mode 100644 index 0000000..6377104 --- /dev/null +++ b/assets/packages/brief/public/init.js @@ -0,0 +1 @@ +window.config = window.config || window.defaultConfig diff --git a/assets/packages/brief/src/Brief.css b/assets/packages/brief/src/Brief.css new file mode 100644 index 0000000..d5e9ef8 --- /dev/null +++ b/assets/packages/brief/src/Brief.css @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2023 Iván Szkiba + * + * SPDX-License-Identifier: MIT + */ + +.usage { + color: #808080; + font-style: italic; +} \ No newline at end of file diff --git a/assets/packages/brief/src/Brief.jsx b/assets/packages/brief/src/Brief.jsx new file mode 100644 index 0000000..4e8149f --- /dev/null +++ b/assets/packages/brief/src/Brief.jsx @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2023 Iván Szkiba +// +// SPDX-License-Identifier: MIT + +import "./Brief.css"; +import { iterable } from "./util"; +import SummarySection from "./Summary"; + +import { Chart } from "./Chart"; + +function reportSections(samples, conf) { + const all = []; + + if (!iterable(conf)) { + return all; + } + + for (let i = 0; i < conf.length; i++) { + if (conf[i].event != "snapshot") { + continue; + } + + all.push(); + } + + return all; +} + +function charts(samples, conf) { + const all = []; + + if (!iterable(conf)) { + return all; + } + + for (const chart of conf) { + let c = { ...chart, metrics: samples }; + all.push(
{Chart(c)}
); + } + + return all; +} + +function ReportSection(props) { + return ( +
+

{props.title}

+

{props.description}

+
+ {charts(props.samples, props.charts)} +
+
+ ); +} + +function UsageSection(props) { + return ( +
+
+

+ Select a time interval by holding down the mouse on any graph to zoom. To cancel zoom, double click on any graph. +

+
+ ); +} + +export function Brief(props) { + return ( +
+

k6 report

+
{reportSections(props.data.metrics, props.config.tabs)}
+ + +
+ ); +} diff --git a/assets/packages/brief/src/Chart.css b/assets/packages/brief/src/Chart.css new file mode 100644 index 0000000..dafa2cc --- /dev/null +++ b/assets/packages/brief/src/Chart.css @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2023 Iván Szkiba + * + * SPDX-License-Identifier: MIT + */ + +.u-title, +.u-label { + font-weight: 300 !important; +} + +.uplot { + break-inside: avoid; +} + +@media screen { + .u-title { + font-weight: 400 !important; + } + + div.chart { + margin-top: 2rem; + margin-bottom: 1rem; + } +} + +@media print { + div.chart { + padding-top: 1rem; + padding-bottom: 1rem; + } +} diff --git a/assets/packages/brief/src/Chart.jsx b/assets/packages/brief/src/Chart.jsx new file mode 100644 index 0000000..fbafd9f --- /dev/null +++ b/assets/packages/brief/src/Chart.jsx @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2023 Iván Szkiba +// +// SPDX-License-Identifier: MIT + +import { useRef, useState, useLayoutEffect } from 'preact/hooks' +import { MetricsUplot } from './metrics-uplot' +import UplotReact from 'uplot-react'; +import 'uplot/dist/uPlot.min.css'; +import uPlot from 'uplot'; +import './Chart.css' + +const sync = uPlot.sync("chart"); + +function Chart(props) { + const model = new MetricsUplot(props.metrics, props.series) + const ref = useRef(null); + + //const { width } = useParentSize(ref); + + const [width, setWidth] = useState(0) + + useLayoutEffect(()=> { + let updateWidth = () => setWidth(ref.current.offsetWidth) + updateWidth() + window.addEventListener("resize", updateWidth); + + return () => window.removeEventListener("resize", updateWidth); + }) + + if (model.data.length < (props.series.length + 1)) { + return () + } + + let options = { + width: props.width || width, + height: props.height || 250, + title: props.title, + cursor: { + sync: { key: sync.key }, + }, + series: model.series, + } + + if (props.axes) { + options.axes = props.axes + } + + return ( +
+ +
+ ) +} + +export { Chart } diff --git a/assets/packages/brief/src/Digest.css b/assets/packages/brief/src/Digest.css new file mode 100644 index 0000000..fe5ea43 --- /dev/null +++ b/assets/packages/brief/src/Digest.css @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2023 Iván Szkiba + * + * SPDX-License-Identifier: MIT + */ + +.container { + padding: 0 !important; +} + +tr:hover {background-color: #f8f8f8;} + +thead tr th { + font-weight: 700; +} + +tr th:not(:first-child), td { + text-align: right; +} + +tr th { + font-weight: 500; +} + +caption { + text-align: center !important; + font-weight: 400; + color: black !important; + padding: 4px !important; +} + +@media screen { + caption { + background-color: #7b65fa20; + border-bottom: 1px solid #7b65fa; + } +} + +@media print { + caption { + border-bottom: 1px solid lightgrey !important; + } +} \ No newline at end of file diff --git a/assets/packages/brief/src/Digest.jsx b/assets/packages/brief/src/Digest.jsx new file mode 100644 index 0000000..c9cc7eb --- /dev/null +++ b/assets/packages/brief/src/Digest.jsx @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2023 Iván Szkiba +// +// SPDX-License-Identifier: MIT + +import './Digest.css' + +import { iterable } from './util'; + +const propertyNames = { + 'trend': ["avg", "min", "med", "max", "p(90)", "p(95)", "p(99)"], + 'counter': ["rate", "count"], + 'rate': ["rate"], + 'gauge': ["value"] +} + +export function Digest(props) { + const { summary, type, series } = props + + const filter = (key) => (!iterable(series) || series.includes(key)) && (summary.values[key].type == type) + + return ( + + + + + + { + propertyNames[type].map((name) => ()) + } + + + + { + Object.keys(summary.values).filter(filter).map(key => ( + + + { + propertyNames[type].map((name) => ( + + )) + } + + )) + } + +
{props.caption}
metric{name}
{key} + { + summary.values[key].format(name) + } +
+ ) +} diff --git a/assets/packages/brief/src/Summary.css b/assets/packages/brief/src/Summary.css new file mode 100644 index 0000000..c080d7b --- /dev/null +++ b/assets/packages/brief/src/Summary.css @@ -0,0 +1,5 @@ +/* + * SPDX-FileCopyrightText: 2023 Iván Szkiba + * + * SPDX-License-Identifier: MIT + */ diff --git a/assets/packages/brief/src/Summary.jsx b/assets/packages/brief/src/Summary.jsx new file mode 100644 index 0000000..e832735 --- /dev/null +++ b/assets/packages/brief/src/Summary.jsx @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 Iván Szkiba +// +// SPDX-License-Identifier: MIT + +import "./Summary.css"; + +import { Digest } from "./Digest"; + +export default function SummarySection(props) { + const { summary, title, description } = props; + + return ( +
+

{title}

+

{description}

+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ ); +} diff --git a/assets/packages/brief/src/data.js b/assets/packages/brief/src/data.js new file mode 100644 index 0000000..bc792ff --- /dev/null +++ b/assets/packages/brief/src/data.js @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2023 Iván Szkiba +// +// SPDX-License-Identifier: MIT + +import { Metrics } from './metrics' +import { Summary } from './summary' + +export default async function () { + var text = document.getElementById("data").innerText + var blob = new Blob([Uint8Array.from(atob(text), m => m.codePointAt(0))]) + var stream = blob.stream().pipeThrough(new DecompressionStream("gzip")) + + var data = await new Response(stream).json() + + var metrics = new Metrics() + + for (var i = 0; i < data.snapshot.length; i++) { + metrics.push(data.snapshot[i]) + } + + var summary = new Summary() + + summary.update(data.cumulative) + + return { metrics, summary } +} diff --git a/assets/packages/brief/src/index.css b/assets/packages/brief/src/index.css new file mode 100644 index 0000000..4183860 --- /dev/null +++ b/assets/packages/brief/src/index.css @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2023 Iván Szkiba + * + * SPDX-License-Identifier: MIT + */ + +@media print { + section { + break-after: page; + } + + .usage { + display: none; + } +} + +@media screen { + .container { + max-width: unset !important; + } + + body { + margin: 1rem; + } +} + +h1, h2, h3 { + font-weight: 400; +} + +h2 { + border-bottom: 1px solid #e0e0e0; +} + +h1 { + text-align: center; +} \ No newline at end of file diff --git a/assets/packages/brief/src/main.jsx b/assets/packages/brief/src/main.jsx new file mode 100644 index 0000000..5d93e04 --- /dev/null +++ b/assets/packages/brief/src/main.jsx @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2023 Iván Szkiba +// +// SPDX-License-Identifier: MIT + +import { render } from 'preact' +import { Brief } from './Brief.jsx' +import data from './data' + +import 'uplot/dist/uPlot.min.css'; + +import './styles.scss' + +import './index.css' + +data().then(d => render(, document.getElementById('root'))) diff --git a/assets/packages/brief/src/metrics-uplot.js b/assets/packages/brief/src/metrics-uplot.js new file mode 100644 index 0000000..45af553 --- /dev/null +++ b/assets/packages/brief/src/metrics-uplot.js @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2023 Iván Szkiba +// +// SPDX-License-Identifier: MIT + +import { propTime } from './metrics' + +const palette = [ + '#7b65fa', + '#65d1fa', + '#af8b47', + '#fa7765', + '#4792af', + '#af5347', + '#4f5aaf', + '#9e65fa', // + '#d95f02', + '#1b9e77', + '#7570b3', + '#e7298a', + '#66a61e', + '#e6ab02', + '#a6761d', + '#666666' +] + +class MetricsUplot { + constructor (samples, series) { + this.data = MetricsUplot.buildData(samples, series) + this.series = MetricsUplot.buildSeries(this.data, series) + } + + static buildData (samples, series) { + const values = samples.values + + let data = [] + let time = values[propTime] + + if (!Array.isArray(time)) { + return data + } + + data.push(time) + + for (var key in series) { + if (!Array.isArray(values[key])) { + data.push(Array(time.length)) + continue + } + + data.push(values[key]) + } + + return data + } + + static buildSeries (data, input) { + const series = [{}] + const keys = Object.keys(input) + + for (var i = 0; i < keys.length; i++) { + var pidx = i % palette.length + + series.push({ + stroke: palette[pidx], + fill: `${palette[pidx]}20`, + ...input[keys[i]], + show: data.length > i && Array.isArray(data[i + 1]) + }) + } + + return series + } +} + +export { MetricsUplot } diff --git a/assets/packages/brief/src/metrics.js b/assets/packages/brief/src/metrics.js new file mode 100644 index 0000000..efc404f --- /dev/null +++ b/assets/packages/brief/src/metrics.js @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2023 Iván Szkiba +// +// SPDX-License-Identifier: MIT + +import { roundTo } from 'round-to' + +const propTime = 'time' +const propType = 'type' + +class Metrics { + constructor ({ capacity = 10000, values = {}, progress = 0, lastEventId = 0 } = {}) { + this.capacity = capacity + this.values = values + this.length = values[propTime] ? values[propTime].length : 0 + this.progress = progress + this.lastEventId = lastEventId + } + + pushOne (key, value) { + if (!this.values.hasOwnProperty(key)) { + this.values[key] = Array(this.length) + } + + this.values[key].push(roundTo(value, 4)) + + if (this.length == this.capacity) { + this.values[key].shift() + } + } + + push (data) { + for (const key in data) { + if (key == propTime) { + this.pushOne(key, Math.floor(data[key].sample.value / 1000)) + this.progress = data[key].sample.pct + + continue + } + + const typeTag = data[key].hasOwnProperty(propType) + ? `_${data[key][propType]}` + : '' + + for (const prop in data[key].sample) { + this.pushOne(key + typeTag + '_' + prop, data[key].sample[prop]) + } + } + + if (this.length < this.capacity) { + this.length++ + } + } +} + +export { Metrics, propTime } diff --git a/assets/packages/brief/src/styles.scss b/assets/packages/brief/src/styles.scss new file mode 100644 index 0000000..ea94f5c --- /dev/null +++ b/assets/packages/brief/src/styles.scss @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2023 Iván Szkiba + * + * SPDX-License-Identifier: MIT + */ + +// Configuration +@import "bootstrap/scss/functions"; +@import "bootstrap/scss/variables"; +@import "bootstrap/scss/variables-dark"; +@import "bootstrap/scss/maps"; +@import "bootstrap/scss/mixins"; +@import "bootstrap/scss/utilities"; + +// Layout & components +@import "bootstrap/scss/root"; +@import "bootstrap/scss/reboot"; +@import "bootstrap/scss/type"; +@import "bootstrap/scss/containers"; +@import "bootstrap/scss/grid"; +@import "bootstrap/scss/tables"; + + diff --git a/assets/packages/brief/src/summary.js b/assets/packages/brief/src/summary.js new file mode 100644 index 0000000..ec75b3c --- /dev/null +++ b/assets/packages/brief/src/summary.js @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2023 Iván Szkiba +// +// SPDX-License-Identifier: MIT + +import prettyMilliseconds from 'pretty-ms' +import prettyBytes from 'pretty-bytes' +import byteSize from 'byte-size' +import { roundTo } from 'round-to' + +const propTime = 'time' + +class Summary { + constructor () { + this.values = {} + this.time = 0 + } + + update (data) { + let values = {} + let time = 0 + + for (const key in data) { + if (key == propTime) { + time = Math.floor(data[key].sample.value / 1000) + + continue + } + + values[key] = data[key] + values[key].format = format + + for (const prop in values[key].sample) { + let value = values[key].sample[prop] + if (Number.isInteger(value)) { + continue + } + + values[key].sample[prop] = parseFloat(value.toFixed(4)) + } + } + + this.values = values + this.time = time + } +} + +const customUnits = { + simple: [ + { from: 0, to: 1e3, unit: ' ', long: ' ' }, + { from: 1e3, to: 1e6, unit: 'k', long: 'kilo' }, + { from: 1e6, to: 1e9, unit: 'M', long: 'mega' }, + { from: 1e9, to: 1e12, unit: 'G', long: 'giga' }, + { from: 1e12, to: 1e15, unit: 'T', long: 'tera' }, + { from: 1e15, to: 1e18, unit: 'P', long: 'peta' }, + { from: 1e18, to: 1e21, unit: 'E', long: 'exa' }, + { from: 1e21, to: 1e24, unit: 'Z', long: 'zetta' }, + { from: 1e24, to: 1e27, unit: 'Y', long: 'yotta' } + ] +} + +function format (prop) { + if (this.contains == 'time') { + return prettyMilliseconds(this.sample[prop], { + formatSubMilliseconds: true, + compact: true + }) + } + + if (this.contains == 'data') { + let str = prettyBytes(this.sample[prop]) + + return prop == 'rate' ? str + '/s' : str + } + + const { value, unit } = byteSize(this.sample[prop], { + customUnits, + units: 'simple' + }) + + let str = `${roundTo(parseFloat(value), 2)} ${unit}` + + return prop == 'rate' ? str + '/s' : str +} + +export { Summary } diff --git a/assets/packages/brief/src/util.js b/assets/packages/brief/src/util.js new file mode 100644 index 0000000..450354f --- /dev/null +++ b/assets/packages/brief/src/util.js @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Iván Szkiba +// +// SPDX-License-Identifier: MIT + +export function iterable (input) { + if (input === null || input === undefined) { + return false + } + + return typeof input[Symbol.iterator] === 'function' +} diff --git a/assets/packages/brief/vite.config.js b/assets/packages/brief/vite.config.js new file mode 100644 index 0000000..209ce88 --- /dev/null +++ b/assets/packages/brief/vite.config.js @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2023 Iván Szkiba +// +// SPDX-License-Identifier: MIT + +import { defineConfig } from 'vite' +import preact from '@preact/preset-vite' +import { viteSingleFile } from "vite-plugin-singlefile" +import handlebars from 'vite-plugin-handlebars'; +import testcontext from './.testcontext' + +export default defineConfig({ + plugins: [preact(), viteSingleFile(), handlebars({context: testcontext})], + build: { + outDir: '../../brief' + } +}) diff --git a/assets/packages/brief/yarn.lock b/assets/packages/brief/yarn.lock new file mode 100644 index 0000000..1dac1a5 --- /dev/null +++ b/assets/packages/brief/yarn.lock @@ -0,0 +1,1071 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@^7.22.10", "@babel/code-frame@^7.22.5": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.10.tgz#1c20e612b768fefa75f6e90d6ecb86329247f0a3" + integrity sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA== + dependencies: + "@babel/highlight" "^7.22.10" + chalk "^2.4.2" + +"@babel/compat-data@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730" + integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ== + +"@babel/core@^7.22.1": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.10.tgz#aad442c7bcd1582252cb4576747ace35bc122f35" + integrity sha512-fTmqbbUBAwCcre6zPzNngvsI0aNrPZe77AeqvDxWM9Nm+04RrJ3CAmGHA9f7lJQY6ZMhRztNemy4uslDxTX4Qw== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.22.10" + "@babel/generator" "^7.22.10" + "@babel/helper-compilation-targets" "^7.22.10" + "@babel/helper-module-transforms" "^7.22.9" + "@babel/helpers" "^7.22.10" + "@babel/parser" "^7.22.10" + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.10" + "@babel/types" "^7.22.10" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.2" + semver "^6.3.1" + +"@babel/generator@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.10.tgz#c92254361f398e160645ac58831069707382b722" + integrity sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A== + dependencies: + "@babel/types" "^7.22.10" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/helper-annotate-as-pure@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" + integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-compilation-targets@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz#01d648bbc25dd88f513d862ee0df27b7d4e67024" + integrity sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-validator-option" "^7.22.5" + browserslist "^4.21.9" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-environment-visitor@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" + integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q== + +"@babel/helper-function-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz#ede300828905bb15e582c037162f99d5183af1be" + integrity sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ== + dependencies: + "@babel/template" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-imports@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c" + integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-transforms@^7.22.9": + version "7.22.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz#92dfcb1fbbb2bc62529024f72d942a8c97142129" + integrity sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.5" + +"@babel/helper-plugin-utils@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + +"@babel/helper-validator-identifier@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" + integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ== + +"@babel/helper-validator-option@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac" + integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw== + +"@babel/helpers@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.10.tgz#ae6005c539dfbcb5cd71fb51bfc8a52ba63bc37a" + integrity sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw== + dependencies: + "@babel/template" "^7.22.5" + "@babel/traverse" "^7.22.10" + "@babel/types" "^7.22.10" + +"@babel/highlight@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.10.tgz#02a3f6d8c1cb4521b2fd0ab0da8f4739936137d7" + integrity sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ== + dependencies: + "@babel/helper-validator-identifier" "^7.22.5" + chalk "^2.4.2" + js-tokens "^4.0.0" + +"@babel/parser@^7.22.10", "@babel/parser@^7.22.5": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.10.tgz#e37634f9a12a1716136c44624ef54283cabd3f55" + integrity sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ== + +"@babel/plugin-syntax-jsx@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz#a6b68e84fb76e759fc3b93e901876ffabbe1d918" + integrity sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-react-jsx-development@^7.16.7": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz#e716b6edbef972a92165cd69d92f1255f7e73e87" + integrity sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.22.5" + +"@babel/plugin-transform-react-jsx@^7.14.9", "@babel/plugin-transform-react-jsx@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.5.tgz#932c291eb6dd1153359e2a90cb5e557dcf068416" + integrity sha512-rog5gZaVbUip5iWDMTYbVM15XQq+RkUKhET/IHR6oizR+JEoN6CAfTTuHcK4vwUyzca30qqHqEpzBOnaRMWYMA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-jsx" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/template@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" + integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw== + dependencies: + "@babel/code-frame" "^7.22.5" + "@babel/parser" "^7.22.5" + "@babel/types" "^7.22.5" + +"@babel/traverse@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.10.tgz#20252acb240e746d27c2e82b4484f199cf8141aa" + integrity sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig== + dependencies: + "@babel/code-frame" "^7.22.10" + "@babel/generator" "^7.22.10" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.22.10" + "@babel/types" "^7.22.10" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.22.10", "@babel/types@^7.22.5": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.10.tgz#4a9e76446048f2c66982d1a989dd12b8a2d2dc03" + integrity sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.5" + to-fast-properties "^2.0.0" + +"@esbuild/android-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" + integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ== + +"@esbuild/android-arm@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" + integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw== + +"@esbuild/android-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2" + integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg== + +"@esbuild/darwin-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1" + integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA== + +"@esbuild/darwin-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d" + integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ== + +"@esbuild/freebsd-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54" + integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw== + +"@esbuild/freebsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e" + integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ== + +"@esbuild/linux-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0" + integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA== + +"@esbuild/linux-arm@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0" + integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg== + +"@esbuild/linux-ia32@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7" + integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA== + +"@esbuild/linux-loong64@0.14.54": + version "0.14.54" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028" + integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw== + +"@esbuild/linux-loong64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" + integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg== + +"@esbuild/linux-mips64el@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231" + integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ== + +"@esbuild/linux-ppc64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb" + integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA== + +"@esbuild/linux-riscv64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6" + integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A== + +"@esbuild/linux-s390x@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071" + integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ== + +"@esbuild/linux-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338" + integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w== + +"@esbuild/netbsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1" + integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A== + +"@esbuild/openbsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae" + integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg== + +"@esbuild/sunos-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d" + integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ== + +"@esbuild/win32-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9" + integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg== + +"@esbuild/win32-ia32@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102" + integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g== + +"@esbuild/win32-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" + integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== + +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.19" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" + integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@preact/preset-vite@^2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@preact/preset-vite/-/preset-vite-2.5.0.tgz#6ff815558c16062a36e2d5da4b1225d7b216478d" + integrity sha512-BUhfB2xQ6ex0yPkrT1Z3LbfPzjpJecOZwQ/xJrXGFSZD84+ObyS//41RdEoQCMWsM0t7UHGaujUxUBub7WM1Jw== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.14.9" + "@babel/plugin-transform-react-jsx-development" "^7.16.7" + "@prefresh/vite" "^2.2.8" + "@rollup/pluginutils" "^4.1.1" + babel-plugin-transform-hook-names "^1.0.2" + debug "^4.3.1" + kolorist "^1.2.10" + resolve "^1.20.0" + +"@prefresh/babel-plugin@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@prefresh/babel-plugin/-/babel-plugin-0.5.0.tgz#61d8ef959007390077c9eddb7e9307c46e19277c" + integrity sha512-joAwpkUDwo7ZqJnufXRGzUb+udk20RBgfA8oLPBh5aJH2LeStmV1luBfeJTztPdyCscC2j2SmZ/tVxFRMIxAEw== + +"@prefresh/core@^1.5.1": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@prefresh/core/-/core-1.5.1.tgz#2f51c0dd509a7b302d67ee889815653abdf4c0d1" + integrity sha512-e0mB0Oxtog6ZpKPDBYbzFniFJDIktuKMzOHp7sguntU+ot0yi6dbhJRE9Css1qf0u16wdSZjpL2W2ODWuU05Cw== + +"@prefresh/utils@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@prefresh/utils/-/utils-1.2.0.tgz#cbdfe549b207041e38bb6cc382408b30cd24fec8" + integrity sha512-KtC/fZw+oqtwOLUFM9UtiitB0JsVX0zLKNyRTA332sqREqSALIIQQxdUCS1P3xR/jT1e2e8/5rwH6gdcMLEmsQ== + +"@prefresh/vite@^2.2.8": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@prefresh/vite/-/vite-2.4.1.tgz#c565ae2f8ec2c5ea03611969810dd02a779c2581" + integrity sha512-vthWmEqu8TZFeyrBNc9YE5SiC3DVSzPgsOCp/WQ7FqdHpOIJi7Z8XvCK06rBPOtG4914S52MjG9Ls22eVAiuqQ== + dependencies: + "@babel/core" "^7.22.1" + "@prefresh/babel-plugin" "0.5.0" + "@prefresh/core" "^1.5.1" + "@prefresh/utils" "^1.2.0" + "@rollup/pluginutils" "^4.2.1" + +"@rollup/pluginutils@^4.1.1", "@rollup/pluginutils@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" + integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ== + dependencies: + estree-walker "^2.0.1" + picomatch "^2.2.2" + +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" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +babel-plugin-transform-hook-names@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz#0d75c2d78e8bbcdb258241131562b9cf07f010f3" + integrity sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +bootstrap@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.1.tgz#8ca07040ad15d7f75891d1504cf14c5dedfb1cfe" + integrity sha512-jzwza3Yagduci2x0rr9MeFSORjcHpt0lRZukZPZQJT1Dth5qzV7XcgGqYzi39KGAVYR8QEDVoO0ubFKOxzMG+g== + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@^4.21.9: + version "4.21.10" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" + integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ== + dependencies: + caniuse-lite "^1.0.30001517" + electron-to-chromium "^1.4.477" + node-releases "^2.0.13" + update-browserslist-db "^1.0.11" + +byte-size@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/byte-size/-/byte-size-8.1.1.tgz#3424608c62d59de5bfda05d31e0313c6174842ae" + integrity sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg== + +caniuse-lite@^1.0.30001517: + version "1.0.30001519" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz#3e7b8b8a7077e78b0eb054d69e6edf5c7df35601" + integrity sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg== + +chalk@^2.4.2: + 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" + +"chokidar@>=3.0.0 <4.0.0": + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +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 sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +convert-source-map@^1.7.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + +debug@^4.1.0, debug@^4.3.1: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +electron-to-chromium@^1.4.477: + version "1.4.488" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.488.tgz#442b1855f8c84fb1ed79f518985c65db94f64cc9" + integrity sha512-Dv4sTjiW7t/UWGL+H8ZkgIjtUAVZDgb/PwGWvMsCT7jipzUV/u5skbLXPFKb6iV0tiddVi/bcS2/kUrczeWgIQ== + +esbuild-android-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be" + integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ== + +esbuild-android-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771" + integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg== + +esbuild-darwin-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25" + integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug== + +esbuild-darwin-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73" + integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw== + +esbuild-freebsd-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d" + integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg== + +esbuild-freebsd-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48" + integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q== + +esbuild-linux-32@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5" + integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw== + +esbuild-linux-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652" + integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg== + +esbuild-linux-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b" + integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig== + +esbuild-linux-arm@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59" + integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw== + +esbuild-linux-mips64le@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34" + integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw== + +esbuild-linux-ppc64le@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e" + integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ== + +esbuild-linux-riscv64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8" + integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg== + +esbuild-linux-s390x@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6" + integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA== + +esbuild-netbsd-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81" + integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w== + +esbuild-openbsd-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b" + integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw== + +esbuild-sunos-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da" + integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw== + +esbuild-windows-32@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31" + integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w== + +esbuild-windows-64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4" + integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ== + +esbuild-windows-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982" + integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg== + +esbuild@^0.14.27: + version "0.14.54" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2" + integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA== + optionalDependencies: + "@esbuild/linux-loong64" "0.14.54" + esbuild-android-64 "0.14.54" + esbuild-android-arm64 "0.14.54" + esbuild-darwin-64 "0.14.54" + esbuild-darwin-arm64 "0.14.54" + esbuild-freebsd-64 "0.14.54" + esbuild-freebsd-arm64 "0.14.54" + esbuild-linux-32 "0.14.54" + esbuild-linux-64 "0.14.54" + esbuild-linux-arm "0.14.54" + esbuild-linux-arm64 "0.14.54" + esbuild-linux-mips64le "0.14.54" + esbuild-linux-ppc64le "0.14.54" + esbuild-linux-riscv64 "0.14.54" + esbuild-linux-s390x "0.14.54" + esbuild-netbsd-64 "0.14.54" + esbuild-openbsd-64 "0.14.54" + esbuild-sunos-64 "0.14.54" + esbuild-windows-32 "0.14.54" + esbuild-windows-64 "0.14.54" + esbuild-windows-arm64 "0.14.54" + +esbuild@^0.18.10: + version "0.18.20" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6" + integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA== + optionalDependencies: + "@esbuild/android-arm" "0.18.20" + "@esbuild/android-arm64" "0.18.20" + "@esbuild/android-x64" "0.18.20" + "@esbuild/darwin-arm64" "0.18.20" + "@esbuild/darwin-x64" "0.18.20" + "@esbuild/freebsd-arm64" "0.18.20" + "@esbuild/freebsd-x64" "0.18.20" + "@esbuild/linux-arm" "0.18.20" + "@esbuild/linux-arm64" "0.18.20" + "@esbuild/linux-ia32" "0.18.20" + "@esbuild/linux-loong64" "0.18.20" + "@esbuild/linux-mips64el" "0.18.20" + "@esbuild/linux-ppc64" "0.18.20" + "@esbuild/linux-riscv64" "0.18.20" + "@esbuild/linux-s390x" "0.18.20" + "@esbuild/linux-x64" "0.18.20" + "@esbuild/netbsd-x64" "0.18.20" + "@esbuild/openbsd-x64" "0.18.20" + "@esbuild/sunos-x64" "0.18.20" + "@esbuild/win32-arm64" "0.18.20" + "@esbuild/win32-ia32" "0.18.20" + "@esbuild/win32-x64" "0.18.20" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +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 sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +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== + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +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== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +handlebars@^4.7.6: + version "4.7.8" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9" + integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.2" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +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" + +humanize-duration@^3.28.0: + version "3.29.0" + resolved "https://registry.yarnpkg.com/humanize-duration/-/humanize-duration-3.29.0.tgz#beffaf7938388cd0f38c494f8970d6faebecf3c0" + integrity sha512-G5wZGwYTLaQAmYqhfK91aw3xt6wNbJW1RnWDh4qP1PvF4T/jnkjx2RVhG5kzB2PGsYGTn+oSDBQp+dMdILLxcg== + +immutable@^4.0.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.2.tgz#f89d910f8dfb6e15c03b2cae2faaf8c1f66455fe" + integrity sha512-oGXzbEDem9OOpDWZu88jGiYCvIsLHMvGw+8OXlpsvTFvIQplQbjg1B1cvKg8f7Hoch6+NGjpPsH1Fr+Mc2D1aA== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" + integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== + dependencies: + has "^1.0.3" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +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== + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json5@^2.2.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +kolorist@^1.2.10: + version "1.8.0" + resolved "https://registry.yarnpkg.com/kolorist/-/kolorist-1.8.0.tgz#edddbbbc7894bc13302cdf740af6374d4a04743c" + integrity sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +micromatch@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +minimist@^1.2.5: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +nanoid@^3.3.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-releases@^2.0.13: + version "2.0.13" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" + integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +numeral@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/numeral/-/numeral-2.0.6.tgz#4ad080936d443c2561aed9f2197efffe25f4e506" + integrity sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA== + +parse-ms@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-3.0.0.tgz#3ea24a934913345fcc3656deda72df921da3a70e" + integrity sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +postcss@^8.4.13, postcss@^8.4.27: + version "8.4.27" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057" + integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +preact@^10.16.0: + version "10.16.0" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.16.0.tgz#68a06d70b191b8a313ea722d61e09c6b2a79a37e" + integrity sha512-XTSj3dJ4roKIC93pald6rWuB2qQJO9gO2iLLyTe87MrjQN+HklueLsmskbywEWqCHlclgz3/M4YLL2iBr9UmMA== + +pretty-bytes@^6.1.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.1.1.tgz#38cd6bb46f47afbf667c202cfc754bffd2016a3b" + integrity sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ== + +pretty-ms@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-8.0.0.tgz#a35563b2a02df01e595538f86d7de54ca23194a3" + integrity sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q== + dependencies: + parse-ms "^3.0.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +resolve@^1.20.0, resolve@^1.22.0: + version "1.22.4" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34" + integrity sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +"rollup@>=2.59.0 <2.78.0": + version "2.77.3" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.77.3.tgz#8f00418d3a2740036e15deb653bed1a90ee0cc12" + integrity sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g== + optionalDependencies: + fsevents "~2.3.2" + +rollup@^3.27.1: + version "3.27.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.27.2.tgz#59adc973504408289be89e5978e938ce852c9520" + integrity sha512-YGwmHf7h2oUHkVBT248x0yt6vZkYQ3/rvE5iQuVBh3WO8GcJ6BNeOkpoX1yMHIiBm18EMLjBPIoUDkhgnyxGOQ== + optionalDependencies: + fsevents "~2.3.2" + +round-to@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/round-to/-/round-to-6.0.0.tgz#c12a8dee3c78cbc981d161ba8ff0214abd6cae53" + integrity sha512-jFvBgyRueGU0QVa7EqXZOkarkzrqEnF3VTCzATRcBkzxXJ4/+pzDf1iouqOqGsx6ZpnIIu5gvFDGnyzoX58ldQ== + +sass@^1.65.1: + version "1.65.1" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.65.1.tgz#8f283b0c26335a88246a448d22e1342ba2ea1432" + integrity sha512-9DINwtHmA41SEd36eVPQ9BJKpn7eKDQmUHmpI0y5Zv2Rcorrh0zS+cFrt050hdNbmmCNKTW3hV5mWfuegNRsEA== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +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-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +uglify-js@^3.1.4: + version "3.17.4" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" + integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== + +update-browserslist-db@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" + integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uplot-react@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/uplot-react/-/uplot-react-1.1.4.tgz#02b9918a199da9983fc0d375fb44e443749e2ac0" + integrity sha512-qO1UkQwjVKdj5vTm3O3yldvu1T6hwY4++rH4KznLhjqpnLdncq1zsRxq/zQz/HUHPVD0j7WBcEISbNM61JsuAQ== + +uplot@^1.6.24: + version "1.6.24" + resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.6.24.tgz#dfa213fa7da92763261920ea972ed1a5f9f6af12" + integrity sha512-WpH2BsrFrqxkMu+4XBvc0eCDsRBhzoq9crttYeSI0bfxpzR5YoSVzZXOKFVWcVC7sp/aDXrdDPbDZGCtck2PVg== + +vite-plugin-handlebars@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/vite-plugin-handlebars/-/vite-plugin-handlebars-1.6.0.tgz#fe74b8321d81a9267594d807c2d6135581e7a3fe" + integrity sha512-/TZ2FadScvJW6fmQ+3m3stm6ns+tDZ3VAgzEkSQYQurAnaQ/3MJfidhmTXzD1Hu1iwgkI3lNuEqybzjjKemCTg== + dependencies: + handlebars "^4.7.6" + vite "^2.0.0" + +vite-plugin-singlefile@^0.13.5: + version "0.13.5" + resolved "https://registry.yarnpkg.com/vite-plugin-singlefile/-/vite-plugin-singlefile-0.13.5.tgz#9465dbb0b06afb2a73600a50fcce4b51c8d10999" + integrity sha512-y/aRGh8qHmw2f1IhaI/C6PJAaov47ESYDvUv1am1YHMhpY+19B5k5Odp8P+tgs+zhfvak6QB1ykrALQErEAo7g== + dependencies: + micromatch "^4.0.5" + +vite@^2.0.0: + version "2.9.16" + resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.16.tgz#daf7ba50f5cc37a7bf51b118ba06bc36e97898e9" + integrity sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA== + dependencies: + esbuild "^0.14.27" + postcss "^8.4.13" + resolve "^1.22.0" + rollup ">=2.59.0 <2.78.0" + optionalDependencies: + fsevents "~2.3.2" + +vite@^4.4.5: + version "4.4.9" + resolved "https://registry.yarnpkg.com/vite/-/vite-4.4.9.tgz#1402423f1a2f8d66fd8d15e351127c7236d29d3d" + integrity sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA== + dependencies: + esbuild "^0.18.10" + postcss "^8.4.27" + rollup "^3.27.1" + optionalDependencies: + fsevents "~2.3.2" + +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== diff --git a/assets/packages/ui/index.html b/assets/packages/ui/index.html index 321b88e..e786644 100644 --- a/assets/packages/ui/index.html +++ b/assets/packages/ui/index.html @@ -1,22 +1,18 @@ + - + +
+ + - - - - - - - - k6 dashboard - - -
- - - + \ No newline at end of file diff --git a/assets/ui/index.html b/assets/ui/index.html index 9dce650..a18b2b2 100644 --- a/assets/ui/index.html +++ b/assets/ui/index.html @@ -1,11 +1,4 @@ - - - diff --git a/dashboard/brief.go b/dashboard/brief.go new file mode 100644 index 0000000..88579eb --- /dev/null +++ b/dashboard/brief.go @@ -0,0 +1,226 @@ +// SPDX-FileCopyrightText: 2023 Iván Szkiba +// +// SPDX-License-Identifier: MIT + +package dashboard + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "encoding/json" + "io" + "io/fs" + "os" + "sync" + + "github.com/sirupsen/logrus" +) + +type briefer struct { + assets fs.FS + uiConfig []byte + output string + logger logrus.FieldLogger + buff bytes.Buffer + mu sync.RWMutex + encoder *json.Encoder + cumulative interface{} +} + +var _ eventListener = (*briefer)(nil) + +func newBriefer(assets fs.FS, uiConfig []byte, output string, logger logrus.FieldLogger) *briefer { + brf := &briefer{ // nolint:exhaustruct + assets: assets, + uiConfig: uiConfig, + output: output, + logger: logger, + } + + brf.encoder = json.NewEncoder(&brf.buff) + + return brf +} + +func (brf *briefer) onStart() error { + return nil +} + +func (brf *briefer) onStop() error { + file, err := os.Create(brf.output) + if err != nil { + return err + } + + if err := brf.exportHTML(file); err != nil { + return err + } + + return file.Close() +} + +func (brf *briefer) onEvent(name string, data interface{}) { + brf.mu.Lock() + defer brf.mu.Unlock() + + if name == "cumulative" { + brf.cumulative = data + + return + } + + if brf.buff.Len() != 0 { + if _, err := brf.buff.WriteRune(','); err != nil { + brf.logger.Error(err) + + return + } + } + + if err := brf.encoder.Encode(data); err != nil { + brf.logger.Error(err) + } +} + +func (brf *briefer) exportJSON(out io.Writer) error { + brf.mu.RLock() + defer brf.mu.RUnlock() + + bin, err := json.Marshal(brf.cumulative) + if err != nil { + return err + } + + if _, err := out.Write([]byte(`{"cumulative":`)); err != nil { + return err + } + + if _, err := out.Write(bin); err != nil { + return err + } + + if _, err := out.Write([]byte(`,"snapshot":[`)); err != nil { + return err + } + + if _, err := out.Write(brf.buff.Bytes()); err != nil { + return err + } + + _, err = out.Write([]byte("]}")) + + return err +} + +func (brf *briefer) exportBase64(out io.Writer) error { + outB64 := base64.NewEncoder(base64.StdEncoding, out) + outGZ := gzip.NewWriter(outB64) + + if err := brf.exportJSON(outGZ); err != nil { + return err + } + + if err := outGZ.Close(); err != nil { + return err + } + + return outB64.Close() +} + +func (brf *briefer) exportHTML(out io.Writer) error { + file, err := brf.assets.Open("index.html") + if err != nil { + return err + } + + html, err := io.ReadAll(file) + if err != nil { + return err + } + + html, err = brf.injectConfig(out, html) + if err != nil { + return err + } + + html, err = brf.injectData(out, html) + if err != nil { + return err + } + + if _, err := out.Write(html); err != nil { + return err + } + + return nil +} + +func (brf *briefer) injectFile(out io.Writer, filename string) error { + file, err := brf.assets.Open(filename) + if err != nil { + return err + } + + data, err := io.ReadAll(file) + if err != nil { + return err + } + + _, err = out.Write(data) + + return err +} + +func (brf *briefer) injectConfig(out io.Writer, html []byte) ([]byte, error) { + idx := bytes.Index(html, configTag) + + if idx < 0 { + panic("invalid brief HTML, no config tag") + } + + idx += len(configTag) + + if _, err := out.Write(html[:idx]); err != nil { + return nil, err + } + + if err := brf.injectFile(out, "boot.js"); err != nil { + return nil, err + } + + if _, err := out.Write(brf.uiConfig); err != nil { + return nil, err + } + + if err := brf.injectFile(out, "init.js"); err != nil { + return nil, err + } + + return html[idx:], nil +} + +func (brf *briefer) injectData(out io.Writer, html []byte) ([]byte, error) { + idx := bytes.Index(html, dataTag) + + if idx < 0 { + panic("invalid brief HTML, no data tag") + } + + idx += len(dataTag) + + if _, err := out.Write(html[:idx]); err != nil { + return nil, err + } + + if err := brf.exportBase64(out); err != nil { + return nil, err + } + + return html[idx:], nil +} + +var ( + configTag = []byte(`