From 6026e3cbab420d21a0294df0702ca629d2b24513 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Fri, 25 Jun 2021 16:41:15 +0200 Subject: [PATCH 001/191] Update webpack --- src/js/components/BinnedPlots/BinnedPlots.js | 2 +- .../components/BinnedPlots/CorrelationPlot.js | 2 +- src/js/components/CompoundAnnotation.js | 2 +- src/js/components/CompoundCheck.js | 2 +- src/js/components/CompoundForm.js | 4 +-- src/js/components/CorrelationForm.js | 2 +- src/js/components/Filter.js | 2 +- src/js/components/GeneAnnotationQuery.js | 4 +-- src/js/components/Histogram/Histogram.js | 2 +- .../HistogramBasedOnSimPlot.js | 4 +-- src/js/components/SampleSelection.js | 2 +- src/js/components/SampleTable/SampleInfo.js | 2 +- src/js/components/SignatureCheck.js | 4 ++- src/js/components/SignatureForm.js | 2 +- src/js/components/SignatureGenerator.js | 3 +-- .../SimilarityPlot/SimilarityPlot.js | 4 +-- src/js/components/Table.js | 2 +- src/js/components/TargetCheck.js | 2 +- src/js/components/TargetForm.js | 2 +- src/js/pages/compound.js | 2 +- src/js/pages/correlation.js | 2 +- src/js/pages/disease.js | 4 +-- src/js/pages/target.js | 2 +- webpack.config.js | 26 +++++++++---------- 24 files changed, 43 insertions(+), 42 deletions(-) diff --git a/src/js/components/BinnedPlots/BinnedPlots.js b/src/js/components/BinnedPlots/BinnedPlots.js index e960fc74..0df9ad33 100644 --- a/src/js/components/BinnedPlots/BinnedPlots.js +++ b/src/js/components/BinnedPlots/BinnedPlots.js @@ -7,7 +7,7 @@ import { clone, equals, omit } from 'ramda'; import { histogramVegaSpec } from './HistogramSpec.js' import { similarityPlotVegaSpec } from './SimilarityPlotSpec.js' import { widthStream } from '../../utils/utils' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../../utils/logger' import { parse } from 'vega-parser' // const elementID = '#hist' diff --git a/src/js/components/BinnedPlots/CorrelationPlot.js b/src/js/components/BinnedPlots/CorrelationPlot.js index 836d543d..46af1bcf 100644 --- a/src/js/components/BinnedPlots/CorrelationPlot.js +++ b/src/js/components/BinnedPlots/CorrelationPlot.js @@ -6,7 +6,7 @@ import { h, p, div, br, label, input, code, table, tr, td, b, h2, button, svg, h import { clone, equals, omit } from 'ramda'; import { CorrelationVegaSpec } from './CorrelationSpec.js' import { widthStream } from '../../utils/utils' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../../utils/logger' import { parse } from 'vega-parser' // Granular access to the settings, only api and sim keys diff --git a/src/js/components/CompoundAnnotation.js b/src/js/components/CompoundAnnotation.js index 97c6f997..2a56ef23 100644 --- a/src/js/components/CompoundAnnotation.js +++ b/src/js/components/CompoundAnnotation.js @@ -1,4 +1,4 @@ -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../utils/logger' import xs from 'xstream' import { keys, values, filter, head, equals, map, prop, clone, omit, merge } from 'ramda' import dropRepeats from 'xstream/extra/dropRepeats' diff --git a/src/js/components/CompoundCheck.js b/src/js/components/CompoundCheck.js index a111099e..b20a5447 100644 --- a/src/js/components/CompoundCheck.js +++ b/src/js/components/CompoundCheck.js @@ -6,7 +6,7 @@ import { logThis, log } from '../utils/logger' import { ENTER_KEYCODE } from '../utils/keycodes.js' import dropRepeats from 'xstream/extra/dropRepeats' import debounce from 'xstream/extra/debounce' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../utils/logger' import { titleCase } from '../utils/utils' const checkLens = { diff --git a/src/js/components/CompoundForm.js b/src/js/components/CompoundForm.js index a16c4ab0..df95c88d 100644 --- a/src/js/components/CompoundForm.js +++ b/src/js/components/CompoundForm.js @@ -10,7 +10,7 @@ import { SampleSelection, sampleSelectionLens } from './SampleSelection' import { mergeWith, merge } from 'ramda' import { SignatureGenerator, signatureLens } from './SignatureGenerator' import { stateDebug } from '../utils/utils' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../utils/logger' function CompoundForm(sources) { @@ -82,4 +82,4 @@ function CompoundForm(sources) { } } -export { CompoundForm } \ No newline at end of file +export { CompoundForm } diff --git a/src/js/components/CorrelationForm.js b/src/js/components/CorrelationForm.js index f4ee4532..daad020c 100644 --- a/src/js/components/CorrelationForm.js +++ b/src/js/components/CorrelationForm.js @@ -7,7 +7,7 @@ import { logThis, log } from '../utils/logger' import { ENTER_KEYCODE } from '../utils/keycodes.js' import { SignatureCheck, checkLens1, checkLens2 } from '../components/SignatureCheck' import dropRepeats from 'xstream/extra/dropRepeats' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../utils/logger' const stateTemplate = { form: { diff --git a/src/js/components/Filter.js b/src/js/components/Filter.js index 265fa2a9..cf1e4cad 100644 --- a/src/js/components/Filter.js +++ b/src/js/components/Filter.js @@ -5,7 +5,7 @@ import { clone, merge, mergeAll } from 'ramda'; import xs from 'xstream'; import { ENTER_KEYCODE } from '../utils/keycodes.js' import dropRepeats from 'xstream/extra/dropRepeats' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../utils/logger' import { difference, keys, head, prop, assocPath, equals } from 'ramda' import { initSettings } from '../configuration' diff --git a/src/js/components/GeneAnnotationQuery.js b/src/js/components/GeneAnnotationQuery.js index 68731519..012fca57 100644 --- a/src/js/components/GeneAnnotationQuery.js +++ b/src/js/components/GeneAnnotationQuery.js @@ -1,4 +1,4 @@ -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../utils/logger' import xs from 'xstream' import { keys, values, filter, head, equals, map, prop, clone, omit, merge } from 'ramda' import dropRepeats from 'xstream/extra/dropRepeats' @@ -72,4 +72,4 @@ function GeneAnnotationQuery(sources, id = ".genePopup") { } -export { GeneAnnotationQuery } \ No newline at end of file +export { GeneAnnotationQuery } diff --git a/src/js/components/Histogram/Histogram.js b/src/js/components/Histogram/Histogram.js index 24c39f54..988b9b48 100644 --- a/src/js/components/Histogram/Histogram.js +++ b/src/js/components/Histogram/Histogram.js @@ -6,7 +6,7 @@ import { h, p, div, br, label, input, code, table, tr, td, b, h2, button, svg, h import { clone, equals } from 'ramda'; import { vegaSpec } from './spec.js' import { widthStream } from '../../utils/utils' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../../utils/logger' import { parse } from 'vega-parser' // Element ID for this vega component diff --git a/src/js/components/HistogramBasedOnSimPlot/HistogramBasedOnSimPlot.js b/src/js/components/HistogramBasedOnSimPlot/HistogramBasedOnSimPlot.js index 9b76ecba..744950c5 100644 --- a/src/js/components/HistogramBasedOnSimPlot/HistogramBasedOnSimPlot.js +++ b/src/js/components/HistogramBasedOnSimPlot/HistogramBasedOnSimPlot.js @@ -7,7 +7,7 @@ import { clone, equals, omit } from 'ramda'; import { vegaSpec } from './spec.js' import { widthStream } from '../../utils/utils' import { stateDebug } from '../../utils/utils' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../../utils/logger' import { parse } from 'vega-parser' const elementID = '#hist' @@ -240,4 +240,4 @@ function Histogram(sources) { } -export { Histogram, histLens } \ No newline at end of file +export { Histogram, histLens } diff --git a/src/js/components/SampleSelection.js b/src/js/components/SampleSelection.js index 7f15a2a4..6ea04b8b 100644 --- a/src/js/components/SampleSelection.js +++ b/src/js/components/SampleSelection.js @@ -7,7 +7,7 @@ import { ENTER_KEYCODE } from '../utils/keycodes.js' import dropRepeats from 'xstream/extra/dropRepeats' import debounce from 'xstream/extra/debounce' import delay from 'xstream/extra/delay' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../utils/logger' import { CompoundAnnotation } from '../components/CompoundAnnotation' import { safeModelToUi } from '../modelTranslations' diff --git a/src/js/components/SampleTable/SampleInfo.js b/src/js/components/SampleTable/SampleInfo.js index 92d53476..b399595b 100644 --- a/src/js/components/SampleTable/SampleInfo.js +++ b/src/js/components/SampleTable/SampleInfo.js @@ -78,7 +78,7 @@ export function SampleInfo(sources) { let urlSourire = props.sourire.url let bgcolor = (sample.zhang >= 0) ? 'rgba(44,123,182, 0.08)' : 'rgba(215,25,28, 0.08)' let url = urlSourire + encodeURIComponent(sample.compound_smiles).replace(/%20/g, '+') - let zhangRounded = (sample.zhang != null) ? sample.zhang.toFixed(3) : 'NA' + let zhangRounded = (sample.zhang != null) ? parseFloat(sample.zhang).toFixed(3) : 'NA' return li('.collection-item .zoom', { style: { 'background-color': bgcolor } }, [ div('.row', { style: { fontWeight: 'small' } }, [ diff --git a/src/js/components/SignatureCheck.js b/src/js/components/SignatureCheck.js index d95e43ab..128c1c60 100644 --- a/src/js/components/SignatureCheck.js +++ b/src/js/components/SignatureCheck.js @@ -6,7 +6,7 @@ import {log, logThis} from '../utils/logger' import {ENTER_KEYCODE} from '../utils/keycodes.js' import dropRepeats from 'xstream/extra/dropRepeats' import debounce from 'xstream/extra/debounce' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../utils/logger' import { check, flash, play_arrow } from 'webpack-material-design-icons' @@ -60,6 +60,7 @@ function SignatureCheck(sources) { 'category' : 'checkSignature' }}) .remember() + .debug() // Catch the response in a stream // Handle errors by returning an empty object @@ -74,6 +75,7 @@ function SignatureCheck(sources) { const data$ = response$ .map(res => res.body) .map(json => json.result.data) + .debug() // Helper function for rendering the table, based on the state const makeTable = (data) => { diff --git a/src/js/components/SignatureForm.js b/src/js/components/SignatureForm.js index f75a2c09..271b877f 100644 --- a/src/js/components/SignatureForm.js +++ b/src/js/components/SignatureForm.js @@ -7,7 +7,7 @@ import { logThis, log } from '../utils/logger' import { ENTER_KEYCODE } from '../utils/keycodes.js' import { SignatureCheck, checkLens } from '../components/SignatureCheck' import dropRepeats from 'xstream/extra/dropRepeats' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../utils/logger' // Granular access to global state and parts of settings const formLens = { diff --git a/src/js/components/SignatureGenerator.js b/src/js/components/SignatureGenerator.js index 5c78b695..54dac154 100644 --- a/src/js/components/SignatureGenerator.js +++ b/src/js/components/SignatureGenerator.js @@ -7,9 +7,8 @@ import { logThis, log } from '../utils/logger' import { ENTER_KEYCODE } from '../utils/keycodes.js' import dropRepeats from 'xstream/extra/dropRepeats' import delay from 'xstream/extra/delay' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../utils/logger' import { stringify } from 'querystring'; -import { isNullOrUndefined } from 'util'; import { GeneAnnotationQuery } from './GeneAnnotationQuery' import { absGene } from '../utils/utils' diff --git a/src/js/components/SimilarityPlot/SimilarityPlot.js b/src/js/components/SimilarityPlot/SimilarityPlot.js index c4361552..8a0633f1 100644 --- a/src/js/components/SimilarityPlot/SimilarityPlot.js +++ b/src/js/components/SimilarityPlot/SimilarityPlot.js @@ -7,7 +7,7 @@ import { clone, equals, omit } from 'ramda'; import { vegaSpec } from './spec.js' import { widthStream } from '../../utils/utils' import { stateDebug } from '../../utils/utils' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../../utils/logger' import { parse } from 'vega-parser' const elementID = '#vega' @@ -230,4 +230,4 @@ function SimilarityPlot(sources) { } -export { SimilarityPlot, simLens }; \ No newline at end of file +export { SimilarityPlot, simLens }; diff --git a/src/js/components/Table.js b/src/js/components/Table.js index d37e28c6..8b05e668 100644 --- a/src/js/components/Table.js +++ b/src/js/components/Table.js @@ -10,7 +10,7 @@ import isolate from '@cycle/isolate' import dropRepeats from 'xstream/extra/dropRepeats' import dropUntil from 'xstream/extra/dropUntil' import { stateDebug } from '../utils/utils' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../utils/logger' import { convertToCSV } from '../utils/export' import delay from 'xstream/extra/delay' diff --git a/src/js/components/TargetCheck.js b/src/js/components/TargetCheck.js index 3440a8b7..3a9ef0e6 100644 --- a/src/js/components/TargetCheck.js +++ b/src/js/components/TargetCheck.js @@ -4,7 +4,7 @@ import { clone, equals, mergeAll } from 'ramda'; import xs from 'xstream'; import dropRepeats from 'xstream/extra/dropRepeats' import debounce from 'xstream/extra/debounce' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../utils/logger' const checkLens = { get: state => ({ core: (typeof state.form !== 'undefined') ? state.form.check : {}, settings: state.settings }), diff --git a/src/js/components/TargetForm.js b/src/js/components/TargetForm.js index 4db85631..0ac8befa 100644 --- a/src/js/components/TargetForm.js +++ b/src/js/components/TargetForm.js @@ -10,7 +10,7 @@ import { SampleSelection, sampleSelectionLens } from './SampleSelection' import { mergeWith, merge } from 'ramda' import { SignatureGenerator, signatureLens } from './SignatureGenerator' import { stateDebug } from '../utils/utils' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../utils/logger' function TargetForm(sources) { diff --git a/src/js/pages/compound.js b/src/js/pages/compound.js index 2565ca38..94dd47f7 100644 --- a/src/js/pages/compound.js +++ b/src/js/pages/compound.js @@ -10,7 +10,7 @@ import { makeTable, headTableLens, tailTableLens } from '../components/Table' import { BinnedPlots, plotsLens } from '../components/BinnedPlots/BinnedPlots' import { Filter, compoundFilterLens } from '../components/Filter' import concat from 'xstream/extra/dropRepeats' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../utils/logger' import { SampleTable, sampleTableLens } from '../components/SampleTable/SampleTable' // Support for ghost mode diff --git a/src/js/pages/correlation.js b/src/js/pages/correlation.js index e992b740..26c75252 100644 --- a/src/js/pages/correlation.js +++ b/src/js/pages/correlation.js @@ -11,7 +11,7 @@ import { CorrelationPlot, correlationPlotsLens } from '../components/BinnedPlots import { makeTable, headTableLens, tailTableLens } from '../components/Table' import { initSettings } from './settings' import { Filter, compoundFilterLens } from '../components/Filter' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../utils/logger' import { SampleTable, sampleTableLens } from '../components/SampleTable/SampleTable' // Support for ghost mode diff --git a/src/js/pages/disease.js b/src/js/pages/disease.js index ff2f049c..a416b290 100644 --- a/src/js/pages/disease.js +++ b/src/js/pages/disease.js @@ -11,7 +11,7 @@ import { BinnedPlots, plotsLens } from '../components/BinnedPlots/BinnedPlots' import { makeTable, headTableLens, tailTableLens } from '../components/Table' import { initSettings } from './settings' import { Filter, compoundFilterLens } from '../components/Filter' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../utils/logger' import { SampleTable, sampleTableLens } from '../components/SampleTable/SampleTable' // Support for ghost mode @@ -152,4 +152,4 @@ function DiseaseWorkflow(sources) { }; } -export default DiseaseWorkflow; \ No newline at end of file +export default DiseaseWorkflow; diff --git a/src/js/pages/target.js b/src/js/pages/target.js index b43f0eeb..2146e8c8 100644 --- a/src/js/pages/target.js +++ b/src/js/pages/target.js @@ -17,7 +17,7 @@ import { pick, mix } from 'cycle-onionify'; import { initSettings } from './settings' import debounce from 'xstream/extra/debounce' import dropRepeats from 'xstream/extra/dropRepeats' -import { loggerFactory } from '~/../../src/js/utils/logger' +import { loggerFactory } from '../utils/logger' import isolate from '@cycle/isolate' import { SignatureForm, formLens } from '../components/SignatureForm' import { Filter, compoundFilterLens } from '../components/Filter' diff --git a/webpack.config.js b/webpack.config.js index 4ac58e0c..5fb3ce5e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -14,7 +14,8 @@ module.exports = { inline: true, historyApiFallback: true, contentBase: './', - hot: false + hot: false, + port: 4040 }, // externals: { // deployments: './deployments.json' @@ -24,7 +25,7 @@ module.exports = { { test: /\.js$/, exclude: /node_modules/, - loaders: ['babel-loader'] + use: ['babel-loader'] }, { test: /\.scss$/, @@ -36,23 +37,22 @@ module.exports = { } ] }, - { test: /\.css$/, loader: "style-loader!css-loader" }, + { test: /\.css$/, use: ["style-loader", "css-loader"] }, { test: /\.(jpe?g|woff2?|ttf|eot|svg|png|gif)(\?v=\d+\.\d+\.\d+)?$/, use: [ - { - loader: 'file-loader', - options: { - name: '[name].[ext]', - outputPath: 'fonts/' + { + loader: 'file-loader', + options: { + name: '[name].[ext]', + outputPath: 'fonts/' + } } - } - ] + ] }, { test: /\.ico$/, - loader: "url-loader", - query: { mimetype: "image/x-icon" } + use: [ "url-loader" ] } ] }, @@ -65,7 +65,7 @@ module.exports = { 'window.jQuery': 'jquery', }), new webpack.HotModuleReplacementPlugin(), - new webpack.NamedModulesPlugin(), + // new webpack.NamedModulesPlugin(), new webpack.DefinePlugin({ VERSION: JSON.stringify(require("./package.json").version) }) ], From 9db3f2e102af89b001aed2c7a12b1d78b15c49dc Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Fri, 25 Jun 2021 16:41:47 +0200 Subject: [PATCH 002/191] Version updates --- package.json | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 1ec4ae32..faf4905f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ ], "main": "server.js", "scripts": { - "serve": "webpack-dev-server -d --progress --colors --inline --host 0.0.0.0", + "serve": "webpack serve --mode development --open", "prod": "NODE_ENV=production webpack-dev-server -d --progress --colors --inline --host 0.0.0.0", "start": "npm install && node server.js", "electron": "electron .", @@ -58,21 +58,19 @@ "babel-loader": "^7.1.5", "babel-preset-es2015": "^6.24.1", "babel-preset-stage-0": "^6.24.1", - "bootstrap-sass": "^3.3.7", "css-loader": "^0.26.4", "fibers": "^4.0.2", "file-loader": "^5.0.2", "jquery": "^3.3.1", "materialize-loader": "^3.0.1", "mini-css-extract-plugin": "^0.8.0", - "node-sass": "^4.13.0", - "sass": "^1.23.7", - "sass-loader": "^8.0.0", + "sass": "^1.35.1", + "sass-loader": "^11.0.1", "style-loader": "^1.0.1", "url-loader": "^3.0.0", - "webpack": "^4.41.2", - "webpack-cli": "^3.3.10", - "webpack-dev-server": "^3.9.0", + "webpack": "^5.36.2", + "webpack-cli": "^4.6.0", + "webpack-dev-server": "^3.11.2", "webpack-material-design-icons": "^0.1.0" }, "dependencies": { From 35c48994dd5e318f71ddcd56ef94ad4b31b70874 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Mon, 28 Jun 2021 09:33:41 +0200 Subject: [PATCH 003/191] Minor refactoring --- src/js/components/SampleTable/SampleTable.js | 47 ++++++++++++-------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/js/components/SampleTable/SampleTable.js b/src/js/components/SampleTable/SampleTable.js index 0eed614b..e600c0b2 100644 --- a/src/js/components/SampleTable/SampleTable.js +++ b/src/js/components/SampleTable/SampleTable.js @@ -11,24 +11,35 @@ const sampleTableLens = { function SampleTable(sources) { - const array$ = sources.onion.state$ - - const childrenSinks$ = array$.map(array => { - return array.map((_, index) => isolate(SampleInfo, index)(sources)) - }); - - const vdom$ = childrenSinks$ - .compose(pick('DOM')) - .compose(mix(xs.combine)) - .map(itemVNodes => { - return ul('.collection', {style : {'margin-top' : '0px', 'margin-bottom':'0px'}}, [ - ].concat(itemVNodes)) - }) - .startWith(ul('.collection', [li('.collection-item .center-align .grey-text','no query yet...')])) - - return { - DOM: vdom$, - }; + const array$ = sources.onion.state$ + + const childrenSinks$ = array$.map(array => { + return array.map((_, index) => isolate(SampleInfo, index)(sources)) + }); + + const listStyle = {style : {'margin-top' : '0px', 'margin-bottom':'0px'}} + + const vdom$ = + childrenSinks$ + .compose(pick('DOM')) + .compose(mix(xs.combine)) + .map(itemVNodes => + ul( + '.collection', + listStyle, + [].concat(itemVNodes) + ) + ) + .startWith( + ul( + '.collection', + [ li('.collection-item .center-align .grey-text','no query yet...') ] + ) + ) + + return { + DOM: vdom$, + }; } From ebc0ec2c9574929ed8246ae87651fa060cc719e2 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Mon, 28 Jun 2021 12:47:21 +0200 Subject: [PATCH 004/191] Align to updated attrite names --- src/js/components/SampleTable/SampleInfo.js | 170 ++++++++++---------- 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/src/js/components/SampleTable/SampleInfo.js b/src/js/components/SampleTable/SampleInfo.js index b399595b..6c53df00 100644 --- a/src/js/components/SampleTable/SampleInfo.js +++ b/src/js/components/SampleTable/SampleInfo.js @@ -8,98 +8,98 @@ import { safeModelToUi } from '../../modelTranslations' export function SampleInfo(sources) { - const state$ = sources.onion.state$ - const props$ = sources.props + const state$ = sources.onion.state$ + const props$ = sources.props - const click$ = sources.DOM.select('.zoom').events('click').mapTo(1) - const zoomed$ = click$ - .fold((x, y) => x + y, 0) - .map(count => (count % 2 == 0) ? false : true) + const click$ = sources.DOM.select('.zoom').events('click').mapTo(1) + const zoomed$ = click$ + .fold((x, y) => x + y, 0) + .map(count => (count % 2 == 0) ? false : true) - function entry(key, value) { - return [ - span('.col .s4 .grey-text.text-darken-1', { style: { fontWeight: 'lighter' } }, key), - span('.col .s8', { style : { overflow: 'hidden', 'text-overflow': 'ellipsis' }}, (value.length != 0) ? value : '') - ] - } + function entry(key, value) { + return [ + span('.col .s4 .grey-text.text-darken-1', { style: { fontWeight: 'lighter' } }, key), + span('.col .s8', { style : { overflow: 'hidden', 'text-overflow': 'ellipsis' }}, (value.length != 0) ? value : '') + ] + } - function entrySmall(key, value) { - return [ - span('.col .s6 .l2', { style: { fontWeight: 'lighter' } }, key), - span('.col .s6 .l2', (value.length != 0) ? value : '') - ] - } + function entrySmall(key, value) { + return [ + span('.col .s6 .l2', { style: { fontWeight: 'lighter' } }, key), + span('.col .s6 .l2', (value.length != 0) ? value : '') + ] + } - const blur$ = props$ - .filter(props => props.common.blur != undefined) - .filter(props => props.common.blur) - .map(props => ({ filter: 'blur(' + props.common.amountBlur + 'px)' })) - .startWith({ filter: 'blur(0px)' }) + const blur$ = props$ + .filter(props => props.common.blur != undefined) + .filter(props => props.common.blur) + .map(props => ({ filter: 'blur(' + props.common.amountBlur + 'px)' })) + .startWith({ filter: 'blur(0px)' }) - const detail = (sample, props, blur) => { - let hStyle = { style: { margin: '0px', fontWeight: 'bold' } } - let pStyle = { style: { margin: '0px' } } - // let hStylewBlur = { style: merge(blur, { margin: '0px', fontWeight: 'bold' }) } - let pStylewBlur = { style: merge(blur, { margin: '0px' }) } - let urlSourire = props.sourire.url - let url = urlSourire + encodeURIComponent(sample.compound_smiles).replace(/%20/g, '+') - const _filters = (sample.filters != undefined) ? sample.filters : [] - return div('.col .s12', [ - div('.col .s6 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ - p('.col .s12 .grey-text', hStyle, 'Sample Info:'), - p(pStyle, entry('Sample ID: ', sample.id)), - p(pStyle, entry('protocolname: ', sample.protocolname)), - p(pStyle, entry('Concentration: ', sample.concentration)), - p(pStyle, entry('Time: ', sample.time)), - p(pStyle, entry('Year: ', sample.year)), - p(pStyle, entry('Plate ID: ', sample.plateid)), - ]), - div('.col .s6 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ - p('.col .s12 .grey-text', hStyle, 'Compound Info:'), - p(pStylewBlur, entry('Name: ', sample.compound_name)), - p(pStylewBlur, entry(safeModelToUi('id', props.common.modelTranslations) + ": ", sample.compound_id)), - p(pStyle, entry('Type: ', sample.compound_type)), - p('.s12', entry('Targets: ', sample.compound_targets.join(', '))), - ]), - div('.col .s12 .offset-s8 .l4', { style: merge(blur, { margin: '20px 0px 0px 0px' }) }, [ - (sample.compound_smiles != null && sample.compound_smiles != 'NA' && sample.compound_smiles != 'No Smiles') ? - img('.col .s12 .valign', { props: { src: url } }) : - '' - ]), - div('.col .s12 .l12', { style: { margin: '15px 0px 0px 0px' } }, - [p('.col .s12.grey-text', hStyle, 'Filter Info:')] - .concat(_filters.map( x => p(pStyle, entrySmall(x.key, x.value)) )) - ) - ]) - } + const detail = (sample, props, blur) => { + let hStyle = { style: { margin: '0px', fontWeight: 'bold' } } + let pStyle = { style: { margin: '0px' } } + // let hStylewBlur = { style: merge(blur, { margin: '0px', fontWeight: 'bold' }) } + let pStylewBlur = { style: merge(blur, { margin: '0px' }) } + let urlSourire = props.sourire.url + let url = urlSourire + encodeURIComponent(sample.smiles).replace(/%20/g, '+') + const _filters = (sample.filters != undefined) ? sample.filters : [] + return div('.col .s12', [ + div('.col .s6 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ + p('.col .s12 .grey-text', hStyle, 'Sample Info:'), + p(pStyle, entry('Sample ID: ', sample.id)), + p(pStyle, entry('Cell: ', sample.cell)), + p(pStyle, entry('Dose: ', sample.dose)), + p(pStyle, entry('Time: ', sample.time)), + p(pStyle, entry('Year: ', sample.year)), + p(pStyle, entry('Plate: ', sample.plate)), + ]), + div('.col .s6 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ + p('.col .s12 .grey-text', hStyle, 'Treatment Info:'), + p(pStylewBlur, entry('Name: ', sample.trt_name)), + p(pStylewBlur, entry(safeModelToUi('id', props.common.modelTranslations) + ": ", sample.trt_id)), + p(pStyle, entry('Type: ', sample.trt)), + p('.s12', entry('Targets: ', sample.targets.join(', '))), + ]), + div('.col .s12 .offset-s8 .l4', { style: merge(blur, { margin: '20px 0px 0px 0px' }) }, [ + (sample.smiles != null && sample.smiles != 'NA' && sample.smiles != 'No Smiles') ? + img('.col .s12 .valign', { props: { src: url } }) : + '' + ]), + div('.col .s12 .l12', { style: { margin: '15px 0px 0px 0px' } }, + [p('.col .s12.grey-text', hStyle, 'Filter Info:')] + .concat(_filters.map( x => p(pStyle, entrySmall(x.key, x.value)) )) + ) + ]) + } - const vdom$ = xs.combine(state$, zoomed$, props$, blur$) - .map(([sample, zoom, props, blur]) => { - let urlSourire = props.sourire.url - let bgcolor = (sample.zhang >= 0) ? 'rgba(44,123,182, 0.08)' : 'rgba(215,25,28, 0.08)' - let url = urlSourire + encodeURIComponent(sample.compound_smiles).replace(/%20/g, '+') - let zhangRounded = (sample.zhang != null) ? parseFloat(sample.zhang).toFixed(3) : 'NA' + const vdom$ = xs.combine(state$, zoomed$, props$, blur$) + .map(([sample, zoom, props, blur]) => { + let urlSourire = props.sourire.url + let bgcolor = (sample.zhang >= 0) ? 'rgba(44,123,182, 0.08)' : 'rgba(215,25,28, 0.08)' + let url = urlSourire + encodeURIComponent(sample.smiles).replace(/%20/g, '+') + let zhangRounded = (sample.zhang != null) ? parseFloat(sample.zhang).toFixed(3) : 'NA' - return li('.collection-item .zoom', { style: { 'background-color': bgcolor } }, [ - div('.row', { style: { fontWeight: 'small' } }, [ - div('.col .s1 .left-align', { style: { fontWeight: 'bold' } }, [zhangRounded]), - div('.col .s2', { style : { overflow: 'hidden', 'text-overflow': 'ellipsis' }}, [sample.id]), - div('.col .s1', [sample.protocolname]), - div('.col .s2', { style: blur }, [(sample.compound_id != "NA") ? sample.compound_id : '']), - div('.col .s3', { style: blur }, [sample.compound_name]), - div('.col .s3 .center-align', { style: blur }, [ - ((sample.compound_smiles != null && sample.compound_smiles != 'NA' && sample.compound_smiles != 'No Smiles') && zoom == false) ? - img({ props: { src: url, height: 50, 'object-fit': 'contain' } }) : - '' - ]), - ]), - (zoom) ? div('.row', [detail(sample, props, blur)]) : div() - ]) - }) - .startWith(li('.collection-itm .zoom', [p('Just one item!!!')])) + return li('.collection-item .zoom', { style: { 'background-color': bgcolor } }, [ + div('.row', { style: { fontWeight: 'small' } }, [ + div('.col .s1 .left-align', { style: { fontWeight: 'bold' } }, [zhangRounded]), + div('.col .s2', { style : { overflow: 'hidden', 'text-overflow': 'ellipsis' }}, [sample.id]), + div('.col .s1', [sample.cell]), + div('.col .s2', { style: blur }, [(sample.trt_id != "NA") ? sample.trt_id : '']), + div('.col .s3', { style: blur }, [sample.trt_name]), + div('.col .s3 .center-align', { style: blur }, [ + ((sample.smiles != null && sample.smiles != 'N/A' && sample.smiles != 'No Smiles') && zoom == false) ? + img({ props: { src: url, height: 50, 'object-fit': 'contain' } }) : + '' + ]), + ]), + (zoom) ? div('.row', [detail(sample, props, blur)]) : div() + ]) + }) + .startWith(li('.collection-itm .zoom', [p('Just one item!!!')])) - return { - DOM: vdom$ - }; + return { + DOM: vdom$ + }; } From ca8843a93d97d03b198db45b05cd5ef75553115a Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Mon, 28 Jun 2021 14:03:23 +0200 Subject: [PATCH 005/191] Include trt_type in sample row --- src/js/components/SampleTable/SampleInfo.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/js/components/SampleTable/SampleInfo.js b/src/js/components/SampleTable/SampleInfo.js index 6c53df00..db6f40e6 100644 --- a/src/js/components/SampleTable/SampleInfo.js +++ b/src/js/components/SampleTable/SampleInfo.js @@ -87,7 +87,8 @@ export function SampleInfo(sources) { div('.col .s1', [sample.cell]), div('.col .s2', { style: blur }, [(sample.trt_id != "NA") ? sample.trt_id : '']), div('.col .s3', { style: blur }, [sample.trt_name]), - div('.col .s3 .center-align', { style: blur }, [ + div('.col .s1', { style: blur }, [sample.trt]), + div('.col .s2 .center-align', { style: blur }, [ ((sample.smiles != null && sample.smiles != 'N/A' && sample.smiles != 'No Smiles') && zoom == false) ? img({ props: { src: url, height: 50, 'object-fit': 'contain' } }) : '' From 05ce633e3b1d709c44766eb0f73ee51f87d2e9fe Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Mon, 28 Jun 2021 14:05:17 +0200 Subject: [PATCH 006/191] Remove debug statements --- src/js/components/SignatureCheck.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/js/components/SignatureCheck.js b/src/js/components/SignatureCheck.js index 128c1c60..93eb44b5 100644 --- a/src/js/components/SignatureCheck.js +++ b/src/js/components/SignatureCheck.js @@ -60,7 +60,6 @@ function SignatureCheck(sources) { 'category' : 'checkSignature' }}) .remember() - .debug() // Catch the response in a stream // Handle errors by returning an empty object @@ -75,7 +74,6 @@ function SignatureCheck(sources) { const data$ = response$ .map(res => res.body) .map(json => json.result.data) - .debug() // Helper function for rendering the table, based on the state const makeTable = (data) => { From 5a013658231e15624102b7227e6a6da92d24e02b Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Mon, 2 Aug 2021 11:23:45 +0200 Subject: [PATCH 007/191] Correct naming in CompoundWF --- src/js/components/CompoundForm.js | 4 ++-- src/js/components/SampleSelection.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/js/components/CompoundForm.js b/src/js/components/CompoundForm.js index df95c88d..1d952be5 100644 --- a/src/js/components/CompoundForm.js +++ b/src/js/components/CompoundForm.js @@ -33,8 +33,8 @@ function CompoundForm(sources) { SignatureGeneratorSink.DOM, ) .map(([ - formDom, - selectionDOM, + formDom, + selectionDOM, signatureDOM, ]) => div([ diff --git a/src/js/components/SampleSelection.js b/src/js/components/SampleSelection.js index 6ea04b8b..c8653d29 100644 --- a/src/js/components/SampleSelection.js +++ b/src/js/components/SampleSelection.js @@ -119,16 +119,16 @@ function SampleSelection(sources) { span(['']) ]) ]), - td('.compoundPopup' + selectedClass(entry.use), blurStyle, entry.compound_id), + td('.compoundPopup' + selectedClass(entry.use), blurStyle, entry.trt_id), td(selectedClass(entry.use), blurStyle, - (entry.compound_name.length > 20) ? entry.compound_name.substring(0, 20) + '...' : entry.compound_name + (entry.trt_name.length > 20) ? entry.trt_name.substring(0, 20) + '...' : entry.trt_name ), td(".left-align" + selectedClass(entry.use), (entry.id.length > 30) ? entry.id.substring(0, 30) + '...' : entry.id ), - td(selectedClass(entry.use), entry.protocolname), + td(selectedClass(entry.use), entry.cell), td(selectedClass(entry.use), - (entry.concentration.length > 6) ? entry.concentration.substring(0, 6) + '...' : entry.concentration + (entry.dose.length > 6) ? entry.dose.substring(0, 6) + '...' : entry.dose ), td(selectedClass(entry.use), entry.batch), td(selectedClass(entry.use), entry.year), From f2f6e85eb9bd8e15c6b2c42a16673637ca69e1a8 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Mon, 2 Aug 2021 11:25:04 +0200 Subject: [PATCH 008/191] Change port 4040 -> 3000 --- webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index 5fb3ce5e..a0b98120 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -15,7 +15,7 @@ module.exports = { historyApiFallback: true, contentBase: './', hot: false, - port: 4040 + port: 3000 }, // externals: { // deployments: './deployments.json' From 0aa9c085d18cec1140a996fbc195862b861c07cf Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Mon, 2 Aug 2021 17:30:31 +0200 Subject: [PATCH 009/191] Multiple perturbagen types in toptables (WIP) --- src/js/components/SampleTable/SampleInfo.js | 196 ++++++++++++++------ 1 file changed, 144 insertions(+), 52 deletions(-) diff --git a/src/js/components/SampleTable/SampleInfo.js b/src/js/components/SampleTable/SampleInfo.js index db6f40e6..818a6cf8 100644 --- a/src/js/components/SampleTable/SampleInfo.js +++ b/src/js/components/SampleTable/SampleInfo.js @@ -1,9 +1,6 @@ import xs from 'xstream' import { i, a, h, p, div, br, label, input, pre, code, table, tr, td, b, h2, button, svg, h1, th, thead, tbody, li, span, img, em } from '@cycle/dom' -import { log } from '../../utils/logger' import { merge } from 'ramda' -import dropRepeats from 'xstream/extra/dropRepeats' -import { stateDebug } from '../../utils/utils' import { safeModelToUi } from '../../modelTranslations' export function SampleInfo(sources) { @@ -36,65 +33,160 @@ export function SampleInfo(sources) { .map(props => ({ filter: 'blur(' + props.common.amountBlur + 'px)' })) .startWith({ filter: 'blur(0px)' }) - const detail = (sample, props, blur) => { + function sourireUrl(base, smiles) { + let url = base + encodeURIComponent(smiles).replace(/%20/g, '+') + return url + } + + const row = (sample, props, blur, zoom) => { + let zhangRounded = (sample.zhang != null) ? parseFloat(sample.zhang).toFixed(3) : 'NA' + return { + trt_cp: + div('.row', {style: {fontWeight: 'small'}}, [ + div('.col .s1 .left-align', {style: {fontWeight: 'bold'}}, [zhangRounded]), + div('.col .s2', {style: {overflow: 'hidden', 'text-overflow': 'ellipsis'}}, [sample.id]), + div('.col .s1', [sample.cell]), + div('.col .s2', {style: blur}, [(sample.trt_id != "NA") ? sample.trt_id : '']), + div('.col .s3', {style: blur}, [sample.trt_name]), + div('.col .s1', {style: blur}, [sample.trt]), + div('.col .s2 .center-align', {style: blur}, [ + ((sample.smiles != null && sample.smiles != 'N/A' && sample.smiles != 'No Smiles') && zoom == false) ? + img({props: {src: sourireUrl(props.sourire.url, sample.smiles), height: 50, 'object-fit': 'contain'}}) : + '' + ]) + ]), + trt_sh: + div('.row', {style: {fontWeight: 'small'}}, [ + div('.col .s1 .left-align', {style: {fontWeight: 'bold'}}, [zhangRounded]), + div('.col .s2', {style: {overflow: 'hidden', 'text-overflow': 'ellipsis'}}, [sample.id]), + div('.col .s1', [sample.cell]), + div('.col .s2', {style: blur}, [(sample.trt_id != "NA") ? sample.trt_id : '']), + div('.col .s3', {style: blur}, [sample.trt_name]), + div('.col .s1', {style: blur}, [sample.trt]), + div('.col .s2 .center-align', {style: blur}, [ + ((sample.trt_name != null && sample.trt_name != 'N/A') && zoom == false) ? + span({ style: { color: 'black', opacity: 0.4, "font-size": "clamp(16px, 5vw, 26px)", height: 50, display: "block", "font-family": 'Nova Mono', 'object-fit': 'contain', fontWeight: "bold" } }, [sample.trt_name]): + '' + ]) + ]), + _default: + div('.row', {style: {fontWeight: 'small'}}, [ + div('.col .s1 .left-align', {style: {fontWeight: 'bold'}}, [zhangRounded]), + div('.col .s2', {style: {overflow: 'hidden', 'text-overflow': 'ellipsis'}}, [sample.id]), + div('.col .s9', ["Treatment type not yet implemented"]), + ]) + } + } + + const rowDetail = (sample, props, blur) => { let hStyle = { style: { margin: '0px', fontWeight: 'bold' } } let pStyle = { style: { margin: '0px' } } - // let hStylewBlur = { style: merge(blur, { margin: '0px', fontWeight: 'bold' }) } let pStylewBlur = { style: merge(blur, { margin: '0px' }) } - let urlSourire = props.sourire.url - let url = urlSourire + encodeURIComponent(sample.smiles).replace(/%20/g, '+') const _filters = (sample.filters != undefined) ? sample.filters : [] - return div('.col .s12', [ - div('.col .s6 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ - p('.col .s12 .grey-text', hStyle, 'Sample Info:'), - p(pStyle, entry('Sample ID: ', sample.id)), - p(pStyle, entry('Cell: ', sample.cell)), - p(pStyle, entry('Dose: ', sample.dose)), - p(pStyle, entry('Time: ', sample.time)), - p(pStyle, entry('Year: ', sample.year)), - p(pStyle, entry('Plate: ', sample.plate)), - ]), - div('.col .s6 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ - p('.col .s12 .grey-text', hStyle, 'Treatment Info:'), - p(pStylewBlur, entry('Name: ', sample.trt_name)), - p(pStylewBlur, entry(safeModelToUi('id', props.common.modelTranslations) + ": ", sample.trt_id)), - p(pStyle, entry('Type: ', sample.trt)), - p('.s12', entry('Targets: ', sample.targets.join(', '))), - ]), - div('.col .s12 .offset-s8 .l4', { style: merge(blur, { margin: '20px 0px 0px 0px' }) }, [ - (sample.smiles != null && sample.smiles != 'NA' && sample.smiles != 'No Smiles') ? - img('.col .s12 .valign', { props: { src: url } }) : - '' - ]), - div('.col .s12 .l12', { style: { margin: '15px 0px 0px 0px' } }, - [p('.col .s12.grey-text', hStyle, 'Filter Info:')] - .concat(_filters.map( x => p(pStyle, entrySmall(x.key, x.value)) )) - ) - ]) + return { + trt_cp: + div('.col .s12', [ + div('.col .s6 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ + p('.col .s12 .grey-text', hStyle, 'Sample Info:'), + p(pStyle, entry('Sample ID: ', sample.id)), + p(pStyle, entry('Cell: ', sample.cell)), + p(pStyle, entry('Dose: ', sample.dose)), + p(pStyle, entry('Time: ', sample.time)), + p(pStyle, entry('Year: ', sample.year)), + p(pStyle, entry('Plate: ', sample.plate)), + ]), + div('.col .s6 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ + p('.col .s12 .grey-text', hStyle, 'Treatment Info:'), + p(pStylewBlur, entry('Name: ', sample.trt_name)), + p(pStylewBlur, entry(safeModelToUi('id', props.common.modelTranslations) + ": ", sample.trt_id)), + p(pStyle, entry('Type: ', sample.trt)), + p('.s12', entry('Targets: ', sample.targets.join(', '))), + ]), + div('.col .s12 .offset-s8 .l4', { style: merge(blur, { margin: '20px 0px 0px 0px' }) }, [ + (sample.smiles != null && sample.smiles != 'N/A' && sample.smiles != 'No Smiles') ? + img('.col .s12 .valign', { props: { src: sourireUrl(props.sourire.url, sample.smiles) } }) : + '' + ]), + div('.col .s12 .l12', { style: { margin: '15px 0px 0px 0px' } }, + [p('.col .s12.grey-text', hStyle, 'Filter Info:')] + .concat(_filters.map( x => p(pStyle, entrySmall(x.key, x.value)) )) + ) + ]), + trt_sh: + div([ + div('.row', [ + div('.col .s4 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ + p('.col .s12 .grey-text', hStyle, 'Sample Info:'), + p(pStyle, entry('Sample ID: ', sample.id)), + p(pStyle, entry('Cell: ', sample.cell)), + p(pStyle, entry('Dose: ', sample.dose)), + p(pStyle, entry('Time: ', sample.time)), + p(pStyle, entry('Year: ', sample.year)), + p(pStyle, entry('Plate: ', sample.plate)), + ]), + div('.col .s4 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ + p('.col .s12 .grey-text', hStyle, 'Treatment Info:'), + p(pStylewBlur, entry('Name: ', sample.trt_name)), + p(pStylewBlur, entry(safeModelToUi('id', props.common.modelTranslations) + ": ", sample.trt_id)), + p(pStyle, entry('Type: ', sample.trt)), + p('.s12', entry('Targets: ', sample.targets.join(', '))), + ]), + div('.col .s4 .l4', { style: merge(blur, { height: '100%', margin: '30px 0px 0px 0px' }) }, [ + ((sample.trt_name != null && sample.trt_name != 'N/A')) + ? div('.col .s12', {style: {color: 'black', opacity: 0.4, "font-size": "clamp(16px, 5vw, 50px)", "font-family": 'Nova Mono', 'object-fit': 'contain', fontWeight: "bold"}}, [sample.trt_name]) + : div() + ]) + ]), + div('.row', { style: { margin: '15px 0px 0px 0px' } }, + [p('.col .s12.grey-text', hStyle, 'Filter Info:')] + .concat(_filters.map( x => p(pStyle, entrySmall(x.key, x.value)) )) + ) + ]), + _default: + div('.row', {style: {fontWeight: 'small'}}, [ + div('.col .s12', [ + div('.col .s6 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ + p('.col .s12 .grey-text', hStyle, 'Sample Info:'), + p(pStyle, entry('Sample ID: ', sample.id)), + p(pStyle, entry('Cell: ', sample.cell)), + p(pStyle, entry('Dose: ', sample.dose)), + p(pStyle, entry('Time: ', sample.time)), + p(pStyle, entry('Year: ', sample.year)), + p(pStyle, entry('Plate: ', sample.plate)), + ]), + div('.col .s6 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ + p('.col .s12 .grey-text', hStyle, 'Treatment Info:'), + p(pStylewBlur, entry('Name: ', sample.trt_name)), + p(pStylewBlur, entry(safeModelToUi('id', props.common.modelTranslations) + ": ", sample.trt_id)), + p(pStyle, entry('Type: ', sample.trt)), + p('.s12', entry('Targets: ', sample.targets.join(', '))), + ]), + div('.col .s12 .offset-s8 .l4', { style: merge(blur, { margin: '20px 0px 0px 0px' }) }, [ + (sample.smiles != null && sample.smiles != 'N/A' && sample.smiles != 'No Smiles') ? + img('.col .s12 .valign', { props: { src: sourireUrl(props.sourire.url, sample.smiles) } }) : + '' + ]), + div('.col .s12 .l12', { style: { margin: '15px 0px 0px 0px' } }, + [p('.col .s12.grey-text', hStyle, 'Filter Info:')] + .concat(_filters.map( x => p(pStyle, entrySmall(x.key, x.value)) )) + ) + ]) + ]) + + } } const vdom$ = xs.combine(state$, zoomed$, props$, blur$) .map(([sample, zoom, props, blur]) => { - let urlSourire = props.sourire.url let bgcolor = (sample.zhang >= 0) ? 'rgba(44,123,182, 0.08)' : 'rgba(215,25,28, 0.08)' - let url = urlSourire + encodeURIComponent(sample.smiles).replace(/%20/g, '+') - let zhangRounded = (sample.zhang != null) ? parseFloat(sample.zhang).toFixed(3) : 'NA' + const updtProps = {...props, bgColor: bgcolor} - return li('.collection-item .zoom', { style: { 'background-color': bgcolor } }, [ - div('.row', { style: { fontWeight: 'small' } }, [ - div('.col .s1 .left-align', { style: { fontWeight: 'bold' } }, [zhangRounded]), - div('.col .s2', { style : { overflow: 'hidden', 'text-overflow': 'ellipsis' }}, [sample.id]), - div('.col .s1', [sample.cell]), - div('.col .s2', { style: blur }, [(sample.trt_id != "NA") ? sample.trt_id : '']), - div('.col .s3', { style: blur }, [sample.trt_name]), - div('.col .s1', { style: blur }, [sample.trt]), - div('.col .s2 .center-align', { style: blur }, [ - ((sample.smiles != null && sample.smiles != 'N/A' && sample.smiles != 'No Smiles') && zoom == false) ? - img({ props: { src: url, height: 50, 'object-fit': 'contain' } }) : - '' - ]), - ]), - (zoom) ? div('.row', [detail(sample, props, blur)]) : div() + const thisRow = row(sample, updtProps, blur, zoom) + const thisRowDetail = rowDetail(sample, updtProps, blur) + + return li('.collection-item .zoom', {style: {'background-color': bgcolor}}, [ + thisRow[sample.trt] ? thisRow[sample.trt] : thisRow["_default"], + (zoom) ? div('.row', [thisRowDetail[sample.trt] ? thisRowDetail[sample.trt] : thisRowDetail["_default"]]) : div() ]) }) .startWith(li('.collection-itm .zoom', [p('Just one item!!!')])) From 05b87e76758715ec1d5461d9db0a9a657c09d8e9 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Tue, 17 Aug 2021 16:52:03 +0200 Subject: [PATCH 010/191] Bump version to 5.0.0-alpha2 --- package.json | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/package.json b/package.json index faf4905f..85705f04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "LuciusWeb", - "version": "4.2.1", + "version": "5.0.0-alpha2", "description": "Web interface for ComPass aka Lucius", "repository": { "type": "git", @@ -21,34 +21,6 @@ "dist": "build", "startelectron": "npm install && electron ." }, - "build": { - "appId": "yourappid", - "category": "your.app.category.type", - "dmg": { - "contents": [ - { - "x": 110, - "y": 150 - }, - { - "x": 240, - "y": 150, - "type": "link", - "path": "/Applications" - } - ] - }, - "linux": { - "target": [ - "AppImage", - "deb" - ] - }, - "win": { - "target": "squirrel", - "icon": "build/icon.ico" - } - }, "author": "Toni Verbeiren", "license": "Apache v2", "devDependencies": { From 6aaaddf1888d32363409af6ebbea3bf68503f31c Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Tue, 7 Sep 2021 15:06:35 +0200 Subject: [PATCH 011/191] Add dynamic filter dialogs --- deployments.json | 133 +++++- src/js/components/FetchFilters.js | 40 ++ src/js/components/Filter.js | 701 ++++++++++++++++-------------- 3 files changed, 550 insertions(+), 324 deletions(-) create mode 100644 src/js/components/FetchFilters.js diff --git a/deployments.json b/deployments.json index dc96b5a7..34c17e9c 100644 --- a/deployments.json +++ b/deployments.json @@ -1,14 +1,99 @@ [ { "name": "default", + "customizations": { + "wi": "https://github.com/data-intuitive/LuciusWeb123" + }, + "services": { + "filter": { + "values": { + "dose": [ + "sample_entry" + ], + "cell": [ + "sample_entry" + ], + "trtType": [ + "sample_entry" + ] + } + }, + "common": { + "hourglass": { + "compound" : "BRD-A17655518", + "signature" : "-WRONG HSPA1A DNAJB1 DDIT4 -TSEN2", + "target" : "MELK" + }, + "modelTranslations": [ + { + "ui": "ID", + "model": "id", + "comment": "" + }, + { + "ui": "Secondary ID", + "model": "jnjb", + "comment": "" + } + ] + }, + "stats" : { + "endpoint": "classPath=com.dataintuitive.luciusapi.statistics" + }, + "api" : { + "url": "http://localhost:8090/jobs?context=luciusapi&appName=luciusapi&sync=true&timeout=30" + }, + "sourire": { + "url": "http://localhost:9999/molecule/" + }, + "geneAnnotations" : { + "url" : "http://localhost:8082/gene/symbol/" + }, + "compoundAnnotations" : { + "url" : "http://localhost:8082/ca/" + } + } + }, + { + "name": "gsk", "customizations": { "wi": "https://github.com/data-intuitive/LuciusWeb" }, "services": { "filter": { "values": { - "concentration": ["0.5", "4.0", "1.5", "-0.5", "-6.0", "4.5", "-2.0", "5.0", "-8.0", "0.0", "3.5", "-Infinity", "-4.0", "-5.5", "-7.0", "3.0", "-1.5", "-1.0", "-2.5", "-6.5", "1.0", "2.5", "-3.0", "-4.5", "2.0"] , - "protocol": ["breast", "N/A", "bone", "adipose", "skin", "central nervous system", "prostate", "ovary", "endometrium", "haematopoietic", "liver", "lung", "large intestine", "stomach", "blood", "other", "muscle", "kidney"], + "concentration": [ + "<= 4.0", + "(4.0, 4.5]", + "(4.5, 5.0]", + "(5.0, 5.5]", + "(5.5, 6.0]", + "(6.0, 6.5]", + "(6.5, 7.0]", + "(7.0, 7.5]", + "> 7.5", + "Other" + ], + "protocol": [ + "endometrium", + "adipose", + "blood", + "skin", + "central nervous system", + "-666", + "bone", + "large intestine", + "prostate", + "haematopoietic and lymphoid tissue", + "breast", + "lung", + "muscle", + "large instestine", + "stomach", + "kidney", + "liver", + "ovary" + ], "type": ["NA"] } }, @@ -29,6 +114,50 @@ "stats" : { "endpoint": "classPath=com.dataintuitive.luciusapi.statistics" }, + "api" : { + "url": "https://compass.data-intuitive.app:445/jobs?context=luciusapi&appName=luciusapi&sync=true&timeout=30" + }, + "sourire": { + "url": "https://compass.data-intuitive.app/molecule/" + }, + "geneAnnotations" : { + "url" : "https://compass.data-intuitive.app/gene/symbol/" + }, + "compoundAnnotations" : { + "url" : "http://localhost:8082/ca/" + } + } + }, + { + "name": "local-jnj", + "customizations": { + "wi": "https://github.com/data-intuitive/LuciusCore" + }, + "services": { + "filter": { + "values": { + "concentration": ["0.1", "1", "10", "30"], + "protocol": ["MCF7", "PBMC"], + "type": ["test", "poscon"] + } + }, + "common": { + "modelTranslations": [ + { + "ui": "jnjs", + "model": "id", + "comment": "" + }, + { + "ui": "jnjb", + "model": "jnjb", + "comment": "" + } + ] + }, + "stats" : { + "endpoint": "classPath=com.dataintuitive.luciusapi.statistics" + }, "api" : { "url": "http://localhost:8090/jobs?context=luciusapi&appName=luciusapi&sync=true&timeout=30" }, diff --git a/src/js/components/FetchFilters.js b/src/js/components/FetchFilters.js new file mode 100644 index 00000000..75d6a386 --- /dev/null +++ b/src/js/components/FetchFilters.js @@ -0,0 +1,40 @@ +import xs from 'xstream' + +function FetchFilters(sources) { + + const state$ = sources.onion.state$ + + // Combine with deployments to the up-to-date endpoint config + const triggerQuery$ = state$.take(1) + + const request$ = triggerQuery$ + .map(state => ({ + url: state.settings.api.url + '&classPath=com.dataintuitive.luciusapi.filters', + method: 'POST', + send: {}, + 'category': 'filters' + }) + ) + .remember() + + const response$$ = sources.HTTP + .select('filters') + + // TODO: fall back to default filters set in config file + const validResponse$ = response$$ + .map(response$ => + response$ + .replaceError(error => xs.empty()) + ) + .flatten() + .map(result => result.body.result.data) + + return { + // We don't initialize the stream here so we know exactly when the + // information is available. + filters: validResponse$, + HTTP: request$ + } +} + +export { FetchFilters } diff --git a/src/js/components/Filter.js b/src/js/components/Filter.js index cf1e4cad..911ed341 100644 --- a/src/js/components/Filter.js +++ b/src/js/components/Filter.js @@ -8,10 +8,21 @@ import dropRepeats from 'xstream/extra/dropRepeats' import { loggerFactory } from '../utils/logger' import { difference, keys, head, prop, assocPath, equals } from 'ramda' import { initSettings } from '../configuration' - -export const compoundFilterLens = { - get: state => ({ core: state.filter, settings: state.settings.filter }), - set: (state, childState) => ({ ...state, filter: childState.core }) +import {FetchFilters} from './FetchFilters' + +// A typical Lens with one exception: +// We allow the child state settings for filter to propagate to +// the global state because the filter values are fetched in the child. +export const filterLens = { + get: state => ({core: state.filter, settings: { filter: state.settings.filter, api: state.settings.api}} ), + set: (state, childState) => ({ + ...state, + filter: childState.core, + settings: { + ...state.settings, + filter: childState.settings.filter + } + }) } /** @@ -25,337 +36,383 @@ export const compoundFilterLens = { */ function Filter(sources) { - const logger = loggerFactory('filter', sources.onion.state$, 'settings.filter.debug') + const logger = loggerFactory('filter', sources.onion.state$, 'settings.filter.debug') - const state$ = sources.onion.state$ + const state$ = sources.onion.state$ - const input$ = sources.input + const input$ = sources.input - // When the component should not be shown, including empty signature - const isEmptyState = (state) => { - if (typeof state.core === 'undefined') { - return true - } else { - if (typeof state.core.input === 'undefined') { - return true - } else { - if (state.core.input === '') { - return true - } else { - return false - } - } - } - } + const filterValues = FetchFilters(sources) - const emptyState$ = state$ - .filter(input => isEmptyState(input)) - - const modifiedState$ = xs.combine(input$, state$) - .filter(([_, state]) => !isEmptyState(state)) - .map(([i, state]) => ({ ...state, core: { ...state.core, input: i } })) - .compose(dropRepeats(equals)) - - // This for ghost mode, inject changes via external state updates... - const ghostChanges$ = modifiedState$ - .filter(state => typeof state.core.ghost !== 'undefined') - .compose(dropRepeats()) - - const expandAnyGhost$ = ghostChanges$.map(state => state.core.ghost.expand).startWith(false) - - const expandConcentrationUI$ = sources.DOM - .select('.concentration') - .events('click') - .fold((x, y) => !x, false) - .startWith(false) - - const expandConcentration$ = xs.merge(expandConcentrationUI$, expandAnyGhost$).remember() - - const expandProtocolUI$ = sources.DOM - .select('.protocol') - .events('click') - .fold((x, _) => !x, false) - .startWith(false) - - const expandProtocol$ = xs.merge(expandProtocolUI$, expandAnyGhost$).remember() - - const expandTypeUI$ = sources.DOM - .select('.type') - .events('click') - .fold((x, _) => !x, false) - .startWith(false) - - const expandType$ = xs.merge(expandTypeUI$, expandAnyGhost$).remember() - - // Helper functions for options: all unset or all set - const noFilter = (selectedOptions, possibleOptions) => (selectedOptions.length == possibleOptions.length) - // const allFilter = (selectedOptions, possibleOptions) => (selectedOptions.length == 0) - - /** - * Given an option from a list of options (x) and a selection, return the properties for a checkbox - * @param {*} option The current option in the list - * @param {*} selection Array with selection - */ - const isSelectedProps = (option, selectedOptions) => { - if (selectedOptions.includes(option)) { - return { props: { type: 'checkbox', checked: true, id: option } } + // When the component should not be shown, including empty signature + const isEmptyState = (state) => { + if (typeof state.core === 'undefined') { + return true + } else { + if (typeof state.core.input === 'undefined') { + return true + } else { + if (state.core.input === '') { + return true } else { - return { props: { type: 'checkbox', checked: false, id: option } } + return false } + } } - - /** - * A line in a table with checkboxes for use with togglableFilter - * @param {*} filter String representing the filter, to be used for selection later - * @param {*} option The current option in the list - * @param {*} selection Array with selection - */ - const filterSwitch = (filter, option, selectedOptions) => div( - '.collection-item ' + '.' + filter + '-options', { props: { id: option }}, [ - label({ props: { id: option } }, [ - input(isSelectedProps(option, selectedOptions), ''), - span({ props: { id: option }, style: { padding : 2} }, [option]) - ]) + } + + const emptyState$ = state$ + .filter(input => isEmptyState(input)) + + const modifiedState$ = xs.combine(input$, state$) + .filter(([_, state]) => !isEmptyState(state)) + .map(([i, state]) => ({ ...state, core: { ...state.core, input: i } })) + .compose(dropRepeats(equals)) + + // This for ghost mode, inject changes via external state updates... + const ghostChanges$ = modifiedState$ + .filter(state => typeof state.core.ghost !== 'undefined') + .compose(dropRepeats()) + + const expandAnyGhost$ = ghostChanges$.map(state => state.core.ghost.expand).startWith(false) + + const expandDoseUI$ = sources.DOM + .select('.dose') + .events('click') + .fold((x, y) => !x, false) + .startWith(false) + + const expandDose$ = xs.merge(expandDoseUI$, expandAnyGhost$).remember() + + const expandProtocolUI$ = sources.DOM + .select('.protocol') + .events('click') + .fold((x, _) => !x, false) + .startWith(false) + + const expandProtocol$ = xs.merge(expandProtocolUI$, expandAnyGhost$).remember() + + const expandTypeUI$ = sources.DOM + .select('.type') + .events('click') + .fold((x, _) => !x, false) + .startWith(false) + + const expandType$ = xs.merge(expandTypeUI$, expandAnyGhost$).remember() + + // Helper functions for options: all unset or all set + const noFilter = (selectedOptions, possibleOptions) => (selectedOptions.length == possibleOptions.length) + // const allFilter = (selectedOptions, possibleOptions) => (selectedOptions.length == 0) + + /** + * Given an option from a list of options (x) and a selection, return the properties for a checkbox + * @param {*} option The current option in the list + * @param {*} selection Array with selection + */ + const isSelectedProps = (option, selectedOptions) => { + if (selectedOptions.includes(option)) { + return { props: { type: 'checkbox', checked: true, id: option } } + } else { + return { props: { type: 'checkbox', checked: false, id: option } } + } + } + + /** + * A line in a table with checkboxes for use with togglableFilter + * @param {*} filter String representing the filter, to be used for selection later + * @param {*} option The current option in the list + * @param {*} selection Array with selection + */ + const filterSwitch = (filter, option, selectedOptions) => div( + '.collection-item ' + '.' + filter + '-options', { props: { id: option }}, [ + label({ props: { id: option } }, [ + input(isSelectedProps(option, selectedOptions), ''), + span({ props: { id: option }, style: { padding : 2} }, [option]) + ]) + ]) + + /** + * A table of check boxes that is only shown if needed + * @param {*} toggle Visible or not? + * @param {*} options Array of possible options + * @param {*} selection Array of selections (multiple) + */ + const togglableFilter = (filter, toggle, possibleOptions, selectedOptions) => + toggle + ? div('.col .s12', [ + div('.col.l6.s12', [ + div('.collection .selection', + possibleOptions + .filter((_,i) => i < possibleOptions.length/2) + .map(option => filterSwitch(filter, option, selectedOptions)) + )]), + div('.col.l6.s12', [ + div('.collection .selection', + possibleOptions + .filter((_,i) => i >= possibleOptions.length/2) + .map(option => filterSwitch(filter, option, selectedOptions)) + ) ]) - - /** - * A table of check boxes that is only shown if needed - * @param {*} toggle Visible or not? - * @param {*} options Array of possible options - * @param {*} selection Array of selections (multiple) - */ - const togglableFilter = (filter, toggle, possibleOptions, selectedOptions) => - toggle - ? div('.col .s12', [ - div('.col.l6.s12', [ - div('.collection .selection', - possibleOptions - .filter((_,i) => i < possibleOptions.length/2) - .map(option => filterSwitch(filter, option, selectedOptions)) - )]), - div('.col.l6.s12', [ - div('.collection .selection', - possibleOptions - .filter((_,i) => i >= possibleOptions.length/2) - .map(option => filterSwitch(filter, option, selectedOptions)) - ) - ]) + ]) + : div('.protocol .col .s10 .offset-s1', ['']) + + const emptyVdom$ = emptyState$.mapTo(div()) + + + const loadedVdom$ = + xs.combine( + modifiedState$, + expandDose$, + expandProtocol$, + expandType$, + filterValues.filters + ).map(([state, toggleDose, toggleProtocol, toggleType, fvs]) => { + + const possibleDoses = + (fvs.dose == undefined) ? ["Populating filter dialog..."] : fvs.dose + const possibleProtocols = + (fvs.cell == undefined) ? ["Populating filter dialog..."] : fvs.cell + const possibleTypes = + (fvs.trtType == undefined) ? ["Populating filter dialog..."] : fvs.trtType + + const selectedDoses = + (state.core.output.dose == undefined) + ? possibleDoses + : state.core.output.dose + const selectedProtocols = + (state.core.output.cell == undefined) + ? possibleProtocols + : state.core.output.cell + const selectedTypes = + (state.core.output.trtType == undefined) + ? possibleTypes + : state.core.output.trtType + + return div([ + // DEBUG -- debugging purposes, remove when no longer necessary !!! + // div('.col .s12', [ div('', [ code('', JSON.stringify(fvs)) ] ) ]), + // div('.col .s12', [ div('', [ code('', JSON.stringify(state.settings.filter.values)) ] ) ]), + // div('.col .s12', [ div('', [ code('', JSON.stringify(state.core.output)) ] ) ]), + div('.col .s12', [ + div('.chip .dose .col .s12', [ + span('.dose .blue-grey-text', [ + noFilter(selectedDoses, possibleDoses) + ? 'No Dose Filter' + : 'Doses: ' + selectedDoses.join(', ') ]) - : div('.protocol .col .s10 .offset-s1', ['']) - - const emptyVdom$ = emptyState$.mapTo(div()) - - const loadedVdom$ = xs.combine(modifiedState$, expandConcentration$, expandProtocol$, expandType$) - .map(([state, toggleConcentration, toggleProtocol, toggleType]) => { - const possibleConcentrations = state.settings.values.concentration - const possibleProtocols = state.settings.values.protocol - const possibleTypes = state.settings.values.type - - const selectedConcentrations = - (state.core.output.concentration == undefined) - ? possibleConcentrations - : state.core.output.concentration - const selectedProtocols = - (state.core.output.protocol == undefined) - ? possibleProtocols - : state.core.output.protocol - const selectedTypes = - (state.core.output.type == undefined) - ? possibleTypes - : state.core.output.type - - return div([ - div('.col .s12', [ - div('.chip .concentration .col .s12', [ - span('.concentration .blue-grey-text', [ - noFilter(selectedConcentrations, possibleConcentrations) - // (selectedConcentrations == undefined) - ? 'No Concentration Filter' - : 'Concentrations: ' + selectedConcentrations.join(', ') - ]) - ]), - togglableFilter('concentration', toggleConcentration, possibleConcentrations, selectedConcentrations) - ]), - div('.col .s12', [ - div('.chip .protocol .col .s12', [ - span('.protocol .blue-grey-text', [ - noFilter(selectedProtocols, possibleProtocols) - // (selectedProtocols == undefined) - ? 'No Protocol Filter' - : 'Protocols: ' + selectedProtocols.join(', ') - ]) - ]), - togglableFilter('protocol', toggleProtocol, possibleProtocols, selectedProtocols) - - ]), - div('.col .s12', [ - div('.chip .type .col .s12', [ - span('.type .blue-grey-text', [ - noFilter(selectedTypes, possibleTypes) - // (selectedTypes == undefined) - ? 'No Type Filter' - : 'Types: ' + selectedTypes.join(', ') - ]) - ]), - togglableFilter('type', toggleType, possibleTypes, selectedTypes) - ]) + ]), + togglableFilter('dose', toggleDose, possibleDoses, selectedDoses) + ]), + div('.col .s12', [ + div('.chip .protocol .col .s12', [ + span('.protocol .blue-grey-text', [ + noFilter(selectedProtocols, possibleProtocols) + ? 'No Protocol Filter' + : 'Protocols: ' + selectedProtocols.join(', ') ]) + ]), + togglableFilter('protocol', toggleProtocol, possibleProtocols, selectedProtocols) + + ]), + div('.col .s12', [ + div('.chip .type .col .s12', [ + span('.type .blue-grey-text', [ + noFilter(selectedTypes, possibleTypes) + ? 'No Type Filter' + : 'Types: ' + selectedTypes.join(', ') + ]) + ]), + togglableFilter('type', toggleType, possibleTypes, selectedTypes) + ]) + ]) + }) + + const vdom$ = xs.merge( + emptyVdom$, + loadedVdom$//.startWith(div()) + ) + + /** + * A query should only be launched when all filter dialogs are closed (again). + * Unfortonately, redrawing the interface causes this to be triggered again. + */ + const expandAny$ = xs.combine(expandDose$, expandProtocol$, expandType$) + .compose(dropRepeats(equals)) + .filter(([doseExpanded, protocolExpanded, typeExpanded]) => !doseExpanded && !protocolExpanded && !typeExpanded) + + // Push filter through as output field ONLY when filter fields are collapsed + // This is to avoid too frequent updates + // merge with the first state update in order to have at a value during initialization + // When the filter is not set, ALL values are present and we transform that into NO values + const filter$ = xs.merge( + // Necessary for the first query, when filters are not yet retrieved + state$.take(1).map(state => ({...state, core: {...state.core, output: state.settings.filter.values}})), + // Necessary triggering queries when filters are set + expandAny$.compose(sampleCombine(modifiedState$)).map(([_, state]) => state) + ) + .map(state => { + const filterKeys = keys(state.settings.filter.values) + var o = {} + // Mutable approach to transforming the values + filterKeys + .forEach(key => { + // If no values are present, ALL values are selected + const actual = + (prop(key, state.core.output) == undefined) + ? prop(key, state.settings.filter.values) + : prop(key, state.core.output) + if (difference(prop(key, state.settings.filter.values), actual).length != 0) { + o[key] = prop(key, state.core.output) + } }) + return o + }) + .remember() + + // Toggles for filter options + const toggledGhost$ = + ghostChanges$ + .filter(state => typeof state.core.ghost.deselect !== 'undefined') + .map(state => state.core.ghost.deselect) + .compose(dropRepeats(equals)) + + const doseToggled$ = + sources.DOM.select('.dose-options') + .events('click') + .map(function (ev) { ev.preventDefault(); return ev; }) + .map(ev => ev.ownerTarget.id) + .map(value => ({dose: value})) + + const protocolToggled$ = + sources.DOM.select('.protocol-options') + .events('click') + .map(function (ev) { ev.preventDefault(); return ev; }) + .map(ev => ev.ownerTarget.id) + .map(value => ({ cell: value })) + + const typeToggled$ = + sources.DOM.select('.type-options') + .events('click') + .map(function (ev) { ev.preventDefault(); return ev; }) + .map(ev => ev.ownerTarget.id) + .map(value => ({ trtType: value })) + + const aDown$ = + sources.DOM.select('document') + .events('keydown') + .map(ev => ev.code) + .filter(code => code == "KeyA") + .mapTo(true) + .startWith(false) + + // A modifier stream + const aUp$ = + sources.DOM.select('document') + .events('keyup') + .map(ev => ev.code) + .filter(code => code == "KeyA") + .mapTo(false) + + const a$ = + xs.merge(aDown$, aUp$) + .compose(dropRepeats(equals)) + .remember() + + /** + * Reducers + */ + // Add the filter values from the settings (and originally from deployments.json) to the current values + const defaultReducer$ = xs.of(prevState => + ({...prevState, core: {...prevState.core, output: {} } }) + ) + + // When the query for the current filter values returns we want to update + // the settings... + const filterOptionsReducer$ = + filterValues.filters + .map(fvs => prevState => { + return ({ + ...prevState, + settings: { + ...prevState.settings, + filter: { + ...prevState.settings.filter, + values: fvs + } + }, + core: { + ...prevState.core, + output: fvs + } + }) + } + ) - const vdom$ = xs.merge( - emptyVdom$, - loadedVdom$//.startWith(div()) + const inputReducer$ = input$.map(i => prevState => ({ ...prevState, core: { ...prevState.core, input: i } })) + + const toggleReducer$ = + xs.merge( + doseToggled$, + protocolToggled$, + typeToggled$, + toggledGhost$) + .compose(sampleCombine(a$)) + .map(([clickedFilter, a]) => prevState => { + // a is a modifier key. If it's not pressed it's the usual behaviour + if (a == false) { + // We want this function to work for 3 the different filters, + // so first get the appropriate one + const filterKey = head(keys(clickedFilter)) + const filterValue = prop(filterKey, clickedFilter) + // if already included, remove it from the list + // take into account that no filter means ALL values included + const currentArrayForFilterKey = + (prop(filterKey, prevState.core.output) == undefined) + ? prop(filterKey, prevState.settings.filter.values) + : prop(filterKey, prevState.core.output) + const alreadyIncluded = currentArrayForFilterKey.includes(filterValue) + // does the value have to be removed from the list? + const newArrayForFilterKey = alreadyIncluded + ? currentArrayForFilterKey.filter(el => el != filterValue) + : currentArrayForFilterKey.concat(filterValue) // the value has to be added to the list + const updatedState = assocPath(['core', 'output', filterKey], newArrayForFilterKey, prevState) + return updatedState + } else { + // If a is pressed during the click, toggle ALL values + const filterKey = head(keys(clickedFilter)) + // values currently selected + const currentValues = + (prop(filterKey, prevState.core.output) != undefined) + ? prop(filterKey, prevState.core.output) + : prop(filterKey, prevState.settings.filter.values) + // possible values + const allValues = prop(filterKey, prevState.settings.filter.values) + // possible - current values + const newValues = allValues.filter(v => !currentValues.includes(v)) + const updatedState = assocPath(['core', 'output', filterKey], newValues, prevState) + return updatedState + } + }) + + const outputReducer$ = + filter$ + .map(i => prevState => ({ ...prevState, core: { ...prevState.core, output: i } })) + + return { + log: xs.merge( + logger(state$, 'state$'), + ), + DOM: vdom$, + output: filter$, + HTTP: filterValues.HTTP, + onion: xs.merge( + defaultReducer$, + filterOptionsReducer$, + inputReducer$, + outputReducer$, + toggleReducer$ ) - - // Trigger when all filter fields are collapsed - const expandAny$ = xs.combine(expandConcentration$, expandProtocol$, expandType$) - .filter(([concentrationExpanded, protocolExpanded, typeExpanded]) => !concentrationExpanded && !protocolExpanded && !typeExpanded) - - // Push filter through as output field ONLY when filter fields are collapsed - // This is to avoid too frequent updates - // merge with the first state update in order to have at a value during initialization - // When the filter is not set, ALL values are present and we transform that into NO values - const filter$ = xs.merge( - state$.take(1).map(state => ({...state, core : {...state.core, output : state.settings.values}})), - expandAny$.compose(sampleCombine(modifiedState$)).map(([_, state]) => state) - ) - // .map(state => state.settings.values) - .map(state => { - const filterKeys = keys(state.settings.values) - var o = {} - // Mutable approach to transforming the values - filterKeys - .forEach(key => { - // If no values are present, ALL values are selected - const actual = - (prop(key, state.core.output) == undefined) - ? prop(key, state.settings.values) - : prop(key, state.core.output) - if (difference(prop(key, state.settings.values), actual).length != 0) { - o[key] = prop(key, state.core.output) - } - }) - return o - }) - .remember() - - // Toggles for filter options - const toggledGhost$ = - ghostChanges$ - .filter(state => typeof state.core.ghost.deselect !== 'undefined') - .map(state => state.core.ghost.deselect) - .compose(dropRepeats(equals)) - - const concentrationToggled$ = - sources.DOM.select('.concentration-options') - .events('click') - .map(function (ev) { ev.preventDefault(); return ev; }) - .map(ev => ev.ownerTarget.id) - .map(value => ({ concentration: value })) - - const protocolToggled$ = - sources.DOM.select('.protocol-options') - .events('click') - .map(function (ev) { ev.preventDefault(); return ev; }) - .map(ev => ev.ownerTarget.id) - .map(value => ({ protocol: value })) - - const typeToggled$ = - sources.DOM.select('.type-options') - .events('click') - .map(function (ev) { ev.preventDefault(); return ev; }) - .map(ev => ev.ownerTarget.id) - .map(value => ({ type: value })) - - const aDown$ = - sources.DOM.select('document') - .events('keydown') - .map(ev => ev.code) - .filter(code => code == "KeyA") - .mapTo(true) - .startWith(false) - - // A modifier stream - const aUp$ = - sources.DOM.select('document') - .events('keyup') - .map(ev => ev.code) - .filter(code => code == "KeyA") - .mapTo(false) - - const a$ = - xs.merge(aDown$, aUp$) - .compose(dropRepeats(equals)) - .remember() - - /** - * Reducers - */ - // Add the filter values from the settings (and originally from deployments.json) to the current values - const defaultReducer$ = xs.of(prevState => - ({...prevState, core: {...prevState.core, output: prevState.settings.values } }) - ) - - const inputReducer$ = input$.map(i => prevState => ({ ...prevState, core: { ...prevState.core, input: i } })) - - const toggleReducer$ = - xs.merge(concentrationToggled$, protocolToggled$, typeToggled$, toggledGhost$) - .compose(sampleCombine(a$)) - .map(([clickedFilter, a]) => prevState => { - // a is a modifier key. If it's not pressed it's the usual behaviour - if (a == false) { - // We want this function to work for 3 the different filters, so first get the appropriate one - const filterKey = head(keys(clickedFilter)) - const filterValue = prop(filterKey, clickedFilter) - // if already included, remove it from the list - // take into account that no filter means ALL values included - const currentArrayForFilterKey = - (prop(filterKey, prevState.core.output) == undefined) - ? prop(filterKey, prevState.settings.values) - : prop(filterKey, prevState.core.output) - const alreadyIncluded = currentArrayForFilterKey.includes(filterValue) - // does the value have to be removed from the list? - const newArrayForFilterKey = alreadyIncluded - ? currentArrayForFilterKey.filter(el => el != filterValue) - : currentArrayForFilterKey.concat(filterValue) // the value has to be added to the list - // When all options are _excluded_, reset to all options _included_ - // const cleanArrayForFilterKey = (newArrayForFilterKey.length == 4) ? [] : newArrayForFilterKey - const updatedState = assocPath(['core', 'output', filterKey], newArrayForFilterKey, prevState) - return updatedState - } else { - // If a is pressed during the click, toggle ALL values - const filterKey = head(keys(clickedFilter)) - // values currently selected - const currentValues = - (prop(filterKey, prevState.core.output) != undefined) - ? prop(filterKey, prevState.core.output) - : prop(filterKey, prevState.settings.values) - // possible values - const allValues = prop(filterKey, prevState.settings.values) - // possible - current values - const newValues = allValues.filter(v => !currentValues.includes(v)) - const updatedState = assocPath(['core', 'output', filterKey], newValues, prevState) - return updatedState - } - }) - - const outputReducer$ = - filter$ - .map(i => prevState => ({ ...prevState, core: { ...prevState.core, output: i } })) - - return { - log: xs.merge( - logger(state$, 'state$'), - ), - DOM: vdom$, - output: filter$, - onion: xs.merge( - defaultReducer$, - inputReducer$, - outputReducer$, - toggleReducer$ - ) - } + } } From d2df1af2a66166195faa6d48da1e8bc1a045b84c Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Wed, 15 Sep 2021 16:35:46 +0200 Subject: [PATCH 012/191] Refactored Filter component and dependencies --- src/js/components/CompoundForm.js | 6 - src/js/components/FetchFilters.js | 7 +- src/js/components/Filter.js | 755 ++++++++++++---------- src/js/components/Table.js | 997 ++++++++++++++++++------------ src/js/pages/compound.js | 312 +++++----- src/js/pages/correlation.js | 2 +- src/js/pages/disease.js | 313 +++++----- src/js/pages/statistics.js | 2 +- src/js/pages/target.js | 11 +- 9 files changed, 1372 insertions(+), 1033 deletions(-) diff --git a/src/js/components/CompoundForm.js b/src/js/components/CompoundForm.js index 1d952be5..9097ff98 100644 --- a/src/js/components/CompoundForm.js +++ b/src/js/components/CompoundForm.js @@ -1,15 +1,9 @@ -import sampleCombine from 'xstream/extra/sampleCombine' import isolate from '@cycle/isolate' import { i, p, div, br, label, input, code, table, tr, td, b, h2, button, textarea, a, ul, li, span } from '@cycle/dom'; -import { clone, equals } from 'ramda'; import xs from 'xstream'; -import { logThis, log } from '../utils/logger' -import { ENTER_KEYCODE } from '../utils/keycodes.js' import { CompoundCheck, checkLens } from './CompoundCheck' import { SampleSelection, sampleSelectionLens } from './SampleSelection' -import { mergeWith, merge } from 'ramda' import { SignatureGenerator, signatureLens } from './SignatureGenerator' -import { stateDebug } from '../utils/utils' import { loggerFactory } from '../utils/logger' function CompoundForm(sources) { diff --git a/src/js/components/FetchFilters.js b/src/js/components/FetchFilters.js index 75d6a386..a998ecd2 100644 --- a/src/js/components/FetchFilters.js +++ b/src/js/components/FetchFilters.js @@ -1,4 +1,5 @@ import xs from 'xstream' +import debounce from 'xstream/extra/debounce' function FetchFilters(sources) { @@ -14,8 +15,7 @@ function FetchFilters(sources) { send: {}, 'category': 'filters' }) - ) - .remember() + ) const response$$ = sources.HTTP .select('filters') @@ -23,8 +23,7 @@ function FetchFilters(sources) { // TODO: fall back to default filters set in config file const validResponse$ = response$$ .map(response$ => - response$ - .replaceError(error => xs.empty()) + response$.replaceError(_ => xs.empty()) ) .flatten() .map(result => result.body.result.data) diff --git a/src/js/components/Filter.js b/src/js/components/Filter.js index 911ed341..243b47b3 100644 --- a/src/js/components/Filter.js +++ b/src/js/components/Filter.js @@ -1,107 +1,328 @@ -import sampleCombine from 'xstream/extra/sampleCombine' -import isolate from '@cycle/isolate' -import { ul, li, form, i, p, div, br, label, input, code, table, tr, td, b, h2, h5, button, textarea, a, select, option, span } from '@cycle/dom'; -import { clone, merge, mergeAll } from 'ramda'; -import xs from 'xstream'; -import { ENTER_KEYCODE } from '../utils/keycodes.js' -import dropRepeats from 'xstream/extra/dropRepeats' -import { loggerFactory } from '../utils/logger' -import { difference, keys, head, prop, assocPath, equals } from 'ramda' -import { initSettings } from '../configuration' -import {FetchFilters} from './FetchFilters' +import sampleCombine from "xstream/extra/sampleCombine" +import { div, label, input, span } from "@cycle/dom" +import xs from "xstream" +import dropRepeats from "xstream/extra/dropRepeats" +import { loggerFactory } from "../utils/logger" +import { + difference, + keys, + head, + prop, + assocPath, + equals, + mergeAll, +} from "ramda" +import { FetchFilters } from "./FetchFilters" +import debounce from 'xstream/extra/debounce' // A typical Lens with one exception: // We allow the child state settings for filter to propagate to // the global state because the filter values are fetched in the child. export const filterLens = { - get: state => ({core: state.filter, settings: { filter: state.settings.filter, api: state.settings.api}} ), + get: (state) => ({ + core: state.filter, + settings: { filter: state.settings.filter, api: state.settings.api }, + }), set: (state, childState) => ({ ...state, filter: childState.core, settings: { ...state.settings, - filter: childState.settings.filter - } - }) + filter: childState.settings.filter, + }, + }), } -/** - * Provide a filter form. We inject (via input) a signature in order to hide the form when an empty signature is present. - * - * Please note that the filter state object contains the values for the filter that should be _included_. - * If the filter is empty, it means all values should be included (no filter) - * - * - input$: stream of signature updates - * - output$: to be consumed by components that require filter functionality, object with filter values. - */ -function Filter(sources) { - - const logger = loggerFactory('filter', sources.onion.state$, 'settings.filter.debug') - - const state$ = sources.onion.state$ - - const input$ = sources.input - - const filterValues = FetchFilters(sources) - - // When the component should not be shown, including empty signature - const isEmptyState = (state) => { - if (typeof state.core === 'undefined') { +// When the component should not be shown, including empty signature +const isEmptyState = (state) => { + if (typeof state.core === "undefined") { + return true + } else { + if (typeof state.core.input === "undefined") { return true } else { - if (typeof state.core.input === 'undefined') { + if (state.core.input === "") { return true } else { - if (state.core.input === '') { - return true - } else { - return false - } + return false } } } +} - const emptyState$ = state$ - .filter(input => isEmptyState(input)) +function intent(domSource$) { + // const expandAnyGhost$ = ghostChanges$.map(state => state.core.ghost.expand).startWith(false) - const modifiedState$ = xs.combine(input$, state$) - .filter(([_, state]) => !isEmptyState(state)) - .map(([i, state]) => ({ ...state, core: { ...state.core, input: i } })) - .compose(dropRepeats(equals)) + const showDoseUI$ = domSource$ + .select(".dose") + .events("click") + .fold((x, _) => ({ dose: !x.dose }), { dose: false }) + .startWith({ dose: false }) - // This for ghost mode, inject changes via external state updates... - const ghostChanges$ = modifiedState$ - .filter(state => typeof state.core.ghost !== 'undefined') - .compose(dropRepeats()) + const showDose$ = xs + .merge( + showDoseUI$ + // showAnyGhost$ + ) + .remember() - const expandAnyGhost$ = ghostChanges$.map(state => state.core.ghost.expand).startWith(false) + const showProtocolUI$ = domSource$ + .select(".protocol") + .events("click") + .fold((x, _) => ({ cell: !x.cell }), { cell: false }) + .startWith({ cell: false }) - const expandDoseUI$ = sources.DOM - .select('.dose') - .events('click') - .fold((x, y) => !x, false) - .startWith(false) + const showProtocol$ = xs + .merge( + showProtocolUI$ + // showAnyGhost$ + ) + .remember() - const expandDose$ = xs.merge(expandDoseUI$, expandAnyGhost$).remember() + const showTypeUI$ = domSource$ + .select(".type") + .events("click") + .fold((x, _) => ({ trtType: !x.trtType }), { trtType: false }) + .startWith({ trtType: false }) - const expandProtocolUI$ = sources.DOM - .select('.protocol') - .events('click') - .fold((x, _) => !x, false) - .startWith(false) + const showType$ = xs + .merge( + showTypeUI$ + // showAnyGhost$ + ) + .remember() - const expandProtocol$ = xs.merge(expandProtocolUI$, expandAnyGhost$).remember() + const filterAction$ = xs + .combine(showDose$, showProtocol$, showType$) + .map(mergeAll) - const expandTypeUI$ = sources.DOM - .select('.type') - .events('click') - .fold((x, _) => !x, false) + // Toggles for filter options + // const toggledGhost$ = + // ghostChanges$ + // .filter(state => typeof state.core.ghost.deselect !== 'undefined') + // .map(state => state.core.ghost.deselect) + // .compose(dropRepeats(equals)) + + const doseToggled$ = domSource$ + .select(".dose-options") + .events("click") + .map(function (ev) { + ev.preventDefault() + return ev + }) + .map((ev) => ev.ownerTarget.id) + .map((value) => ({ dose: value })) + + const protocolToggled$ = domSource$ + .select(".protocol-options") + .events("click") + .map(function (ev) { + ev.preventDefault() + return ev + }) + .map((ev) => ev.ownerTarget.id) + .map((value) => ({ cell: value })) + + const typeToggled$ = domSource$ + .select(".type-options") + .events("click") + .map(function (ev) { + ev.preventDefault() + return ev + }) + .map((ev) => ev.ownerTarget.id) + .map((value) => ({ trtType: value })) + + const modDown$ = domSource$ + .select("document") + .events("keydown") + .map((ev) => ev.code) + .filter((code) => code == "AltLeft") + .mapTo(true) .startWith(false) - const expandType$ = xs.merge(expandTypeUI$, expandAnyGhost$).remember() + // A modifier stream + const modUp$ = domSource$ + .select("document") + .events("keyup") + .map((ev) => ev.code) + .filter((code) => code == "AltLeft") + .mapTo(false) + + const modifier$ = xs + .merge(modDown$, modUp$) + .compose(dropRepeats(equals)) + .remember() + + const action$ = xs.merge( + doseToggled$, + protocolToggled$, + typeToggled$ + // toggledGhost$ + ) + + return { + filterValuesAction$: action$, + modifier$: modifier$, + filterAction$: filterAction$, + } +} + +function model( + possibleValues$, + input$, + filterValuesAction$, + modifier$, + filterAction$ +) { + + // Add the filter values from the settings (and originally from deployments.json) to the current values + const defaultReducer$ = xs.of((prevState) => ({ + ...prevState, + core: { + ...prevState.core, + output: {}, + filter_output: {}, + state: {dose: false, cell: false, trtType: false}, + }, + settings: { + filter: {values: {}} + } + })) + + // When the query for the current filter values returns we want to update + // the settings + const possibleValuesReducer$ = possibleValues$.map((fvs) => (prevState) => ({ + ...prevState, + settings: { + ...prevState.settings, + filter: { + ...prevState.settings.filter, + values: fvs, + }, + }, + core: { + ...prevState.core, + }, + }) + ) + + const inputReducer$ = input$.map((i) => (prevState) => ({ + ...prevState, + core: { ...prevState.core, input: i }, + })) + + const toggleReducer$ = filterValuesAction$ + .compose(sampleCombine(modifier$)) + .map(([clickedFilter, a]) => (prevState) => { + // a is a modifier key. If it's not pressed it's the usual behaviour + if (a == false) { + // We want this function to work for 3 the different filters, + // so first get the appropriate one + const filterKey = head(keys(clickedFilter)) + const filterValue = prop(filterKey, clickedFilter) + // if already included, remove it from the list + // take into account that no filter means ALL values included + const currentArrayForFilterKey = + prop(filterKey, prevState.core.output) == undefined + ? prop(filterKey, prevState.settings.filter.values) + : prop(filterKey, prevState.core.output) + const alreadyIncluded = currentArrayForFilterKey.includes(filterValue) + // does the value have to be removed from the list? + const newArrayForFilterKey = alreadyIncluded + ? currentArrayForFilterKey.filter((el) => el != filterValue) + : currentArrayForFilterKey.concat(filterValue) // the value has to be added to the list + const updatedState = assocPath( + ["core", "output", filterKey], + newArrayForFilterKey, + prevState + ) + return updatedState + } else { + // If a is pressed during the click, toggle ALL values + const filterKey = head(keys(clickedFilter)) + // values currently selected + const currentValues = + prop(filterKey, prevState.core.output) != undefined + ? prop(filterKey, prevState.core.output) + : prop(filterKey, prevState.settings.filter.values) + // possible values + const allValues = prop(filterKey, prevState.settings.filter.values) + // possible - current values + const newValues = allValues.filter((v) => !currentValues.includes(v)) + const updatedState = assocPath( + ["core", "output", filterKey], + newValues, + prevState + ) + return updatedState + } + }) + + // const outputReducer$ = filter$.map((i) => (prevState) => ({ + // ...prevState, + // core: { ...prevState.core, output: i }, + // })) + + const filterReducer$ = filterAction$.map((f) => (prevState) => ({ + ...prevState, + core: { ...prevState.core, state: f }, + })) + + // Push filter through as output field ONLY when filter fields are collapsed + // This is to avoid too frequent updates + // merge with the first state update in order to have at a value during initialization + // When the filter is not set, ALL values are present and we transform that into NO values + // We use the filter_output key for this, to keep it separate from the normal output key + const outputReducer$ = + filterAction$ + .map(_ => (prevState) => { + const filterKeys = keys(prevState.settings.filter.values) + var o = {} + // Mutable approach to transforming the values + filterKeys.forEach((key) => { + // If no values are present, ALL values are selected + const actual = + prop(key, prevState.core.output) == undefined + ? prop(key, prevState.settings.filter.values) + : prop(key, prevState.core.output) + if ( + difference(prop(key, prevState.settings.filter.values), actual).length != 0 + ) { + o[key] = prop(key, prevState.core.output) + } + }) + return { + ...prevState, + core: { ...prevState.core, filter_output: o }, + } + }) + + return xs.merge( + defaultReducer$, + inputReducer$, + possibleValuesReducer$, + filterReducer$, + toggleReducer$, + outputReducer$, + ) +} + +/** + * view + */ +function view(state$) { + + const emptyState$ = state$ + .filter((input) => isEmptyState(input)) + const emptyVdom$ = emptyState$.mapTo(div()).startWith(div()).remember() + + const modifiedState$ = state$ + .filter(state => !isEmptyState(state)) // Helper functions for options: all unset or all set - const noFilter = (selectedOptions, possibleOptions) => (selectedOptions.length == possibleOptions.length) + const noFilter = (selectedOptions, possibleOptions) => + selectedOptions.length == possibleOptions.length // const allFilter = (selectedOptions, possibleOptions) => (selectedOptions.length == 0) /** @@ -111,9 +332,9 @@ function Filter(sources) { */ const isSelectedProps = (option, selectedOptions) => { if (selectedOptions.includes(option)) { - return { props: { type: 'checkbox', checked: true, id: option } } + return { props: { type: "checkbox", checked: true, id: option } } } else { - return { props: { type: 'checkbox', checked: false, id: option } } + return { props: { type: "checkbox", checked: false, id: option } } } } @@ -123,13 +344,17 @@ function Filter(sources) { * @param {*} option The current option in the list * @param {*} selection Array with selection */ - const filterSwitch = (filter, option, selectedOptions) => div( - '.collection-item ' + '.' + filter + '-options', { props: { id: option }}, [ - label({ props: { id: option } }, [ - input(isSelectedProps(option, selectedOptions), ''), - span({ props: { id: option }, style: { padding : 2} }, [option]) - ]) - ]) + const filterSwitch = (filter, option, selectedOptions) => + div( + ".collection-item " + "." + filter + "-options", + { props: { id: option } }, + [ + label({ props: { id: option } }, [ + input(isSelectedProps(option, selectedOptions), ""), + span({ props: { id: option }, style: { padding: 2 } }, [option]), + ]), + ] + ) /** * A table of check boxes that is only shown if needed @@ -139,281 +364,161 @@ function Filter(sources) { */ const togglableFilter = (filter, toggle, possibleOptions, selectedOptions) => toggle - ? div('.col .s12', [ - div('.col.l6.s12', [ - div('.collection .selection', - possibleOptions - .filter((_,i) => i < possibleOptions.length/2) - .map(option => filterSwitch(filter, option, selectedOptions)) - )]), - div('.col.l6.s12', [ - div('.collection .selection', - possibleOptions - .filter((_,i) => i >= possibleOptions.length/2) - .map(option => filterSwitch(filter, option, selectedOptions)) - ) + ? div(".col .s12", [ + div(".col.l6.s12", [ + div( + ".collection .selection", + possibleOptions + .filter((_, i) => i < possibleOptions.length / 2) + .map((option) => filterSwitch(filter, option, selectedOptions)) + ), + ]), + div(".col.l6.s12", [ + div( + ".collection .selection", + possibleOptions + .filter((_, i) => i >= possibleOptions.length / 2) + .map((option) => filterSwitch(filter, option, selectedOptions)) + ), + ]), ]) - ]) - : div('.protocol .col .s10 .offset-s1', ['']) - - const emptyVdom$ = emptyState$.mapTo(div()) - - - const loadedVdom$ = - xs.combine( - modifiedState$, - expandDose$, - expandProtocol$, - expandType$, - filterValues.filters - ).map(([state, toggleDose, toggleProtocol, toggleType, fvs]) => { - - const possibleDoses = - (fvs.dose == undefined) ? ["Populating filter dialog..."] : fvs.dose - const possibleProtocols = - (fvs.cell == undefined) ? ["Populating filter dialog..."] : fvs.cell - const possibleTypes = - (fvs.trtType == undefined) ? ["Populating filter dialog..."] : fvs.trtType - - const selectedDoses = - (state.core.output.dose == undefined) + : div(".protocol .col .s10 .offset-s1", [""]) + + + const loadedVdom$ = modifiedState$.map((state) => { + const possibleDoses = state.settings.filter.values.dose || [ "Populating filter dialog...", ] + const possibleProtocols = state.settings.filter.values.cell || [ "Populating filter dialog...", ] + const possibleTypes = state.settings.filter.values.trtType || [ "Populating filter dialog...", ] + + const selectedDoses = + state.core.output.dose == undefined ? possibleDoses : state.core.output.dose - const selectedProtocols = - (state.core.output.cell == undefined) + const selectedProtocols = + state.core.output.cell == undefined ? possibleProtocols : state.core.output.cell - const selectedTypes = - (state.core.output.trtType == undefined) + const selectedTypes = + state.core.output.trtType == undefined ? possibleTypes : state.core.output.trtType - return div([ - // DEBUG -- debugging purposes, remove when no longer necessary !!! - // div('.col .s12', [ div('', [ code('', JSON.stringify(fvs)) ] ) ]), - // div('.col .s12', [ div('', [ code('', JSON.stringify(state.settings.filter.values)) ] ) ]), - // div('.col .s12', [ div('', [ code('', JSON.stringify(state.core.output)) ] ) ]), - div('.col .s12', [ - div('.chip .dose .col .s12', [ - span('.dose .blue-grey-text', [ - noFilter(selectedDoses, possibleDoses) - ? 'No Dose Filter' - : 'Doses: ' + selectedDoses.join(', ') - ]) + return div([ + // DEBUG -- debugging purposes, remove when no longer necessary !!! + // div('.col .s12', [ div('', [ code('', JSON.stringify(fvs)) ] ) ]), + // div('.col .s12', [ div('', [ code('', JSON.stringify(state.settings.filter.values)) ] ) ]), + // div('.col .s12', [ div('', [ code('', JSON.stringify(state.core.output)) ] ) ]), + div(".col .s12", [ + div(".chip .dose .col .s12", [ + span(".dose .blue-grey-text", [ + noFilter(selectedDoses, possibleDoses) + ? "No Dose Filter" + : "Doses: " + selectedDoses.join(", "), ]), - togglableFilter('dose', toggleDose, possibleDoses, selectedDoses) ]), - div('.col .s12', [ - div('.chip .protocol .col .s12', [ - span('.protocol .blue-grey-text', [ - noFilter(selectedProtocols, possibleProtocols) - ? 'No Protocol Filter' - : 'Protocols: ' + selectedProtocols.join(', ') - ]) + togglableFilter( + "dose", + state.core.state.dose, + possibleDoses, + selectedDoses + ), + ]), + div(".col .s12", [ + div(".chip .protocol .col .s12", [ + span(".protocol .blue-grey-text", [ + noFilter(selectedProtocols, possibleProtocols) + ? "No Protocol Filter" + : "Protocols: " + selectedProtocols.join(", "), ]), - togglableFilter('protocol', toggleProtocol, possibleProtocols, selectedProtocols) - ]), - div('.col .s12', [ - div('.chip .type .col .s12', [ - span('.type .blue-grey-text', [ - noFilter(selectedTypes, possibleTypes) - ? 'No Type Filter' - : 'Types: ' + selectedTypes.join(', ') - ]) + togglableFilter( + "protocol", + state.core.state.cell, + possibleProtocols, + selectedProtocols + ), + ]), + div(".col .s12", [ + div(".chip .type .col .s12", [ + span(".type .blue-grey-text", [ + noFilter(selectedTypes, possibleTypes) + ? "No Type Filter" + : "Types: " + selectedTypes.join(", "), ]), - togglableFilter('type', toggleType, possibleTypes, selectedTypes) - ]) - ]) - }) + ]), + togglableFilter( + "type", + state.core.state.trtType, + possibleTypes, + selectedTypes + ), + ]), + ]) + }) - const vdom$ = xs.merge( + return xs.merge( emptyVdom$, - loadedVdom$//.startWith(div()) + loadedVdom$ ) +} - /** - * A query should only be launched when all filter dialogs are closed (again). - * Unfortonately, redrawing the interface causes this to be triggered again. - */ - const expandAny$ = xs.combine(expandDose$, expandProtocol$, expandType$) - .compose(dropRepeats(equals)) - .filter(([doseExpanded, protocolExpanded, typeExpanded]) => !doseExpanded && !protocolExpanded && !typeExpanded) +/** + * Provide a filter form. We inject (via input) a signature in order to hide the form when an empty signature is present. + * + * Please note that the filter state object contains the values for the filter that should be _included_. + * If the filter is empty, it means all values should be included (no filter) + * + * - input$: stream of signature updates + * - output$: to be consumed by components that require filter functionality, object with filter values. + */ +function Filter(sources) { - // Push filter through as output field ONLY when filter fields are collapsed - // This is to avoid too frequent updates - // merge with the first state update in order to have at a value during initialization - // When the filter is not set, ALL values are present and we transform that into NO values - const filter$ = xs.merge( - // Necessary for the first query, when filters are not yet retrieved - state$.take(1).map(state => ({...state, core: {...state.core, output: state.settings.filter.values}})), - // Necessary triggering queries when filters are set - expandAny$.compose(sampleCombine(modifiedState$)).map(([_, state]) => state) + const logger = loggerFactory( + "filter", + sources.onion.state$, + "settings.filter.debug" ) - .map(state => { - const filterKeys = keys(state.settings.filter.values) - var o = {} - // Mutable approach to transforming the values - filterKeys - .forEach(key => { - // If no values are present, ALL values are selected - const actual = - (prop(key, state.core.output) == undefined) - ? prop(key, state.settings.filter.values) - : prop(key, state.core.output) - if (difference(prop(key, state.settings.filter.values), actual).length != 0) { - o[key] = prop(key, state.core.output) - } - }) - return o - }) - .remember() - // Toggles for filter options - const toggledGhost$ = - ghostChanges$ - .filter(state => typeof state.core.ghost.deselect !== 'undefined') - .map(state => state.core.ghost.deselect) - .compose(dropRepeats(equals)) + // The debounce is required, else it simply does not work + const state$ = sources.onion.state$.compose(debounce(100)) - const doseToggled$ = - sources.DOM.select('.dose-options') - .events('click') - .map(function (ev) { ev.preventDefault(); return ev; }) - .map(ev => ev.ownerTarget.id) - .map(value => ({dose: value})) - - const protocolToggled$ = - sources.DOM.select('.protocol-options') - .events('click') - .map(function (ev) { ev.preventDefault(); return ev; }) - .map(ev => ev.ownerTarget.id) - .map(value => ({ cell: value })) - - const typeToggled$ = - sources.DOM.select('.type-options') - .events('click') - .map(function (ev) { ev.preventDefault(); return ev; }) - .map(ev => ev.ownerTarget.id) - .map(value => ({ trtType: value })) - - const aDown$ = - sources.DOM.select('document') - .events('keydown') - .map(ev => ev.code) - .filter(code => code == "KeyA") - .mapTo(true) - .startWith(false) + const input$ = sources.input - // A modifier stream - const aUp$ = - sources.DOM.select('document') - .events('keyup') - .map(ev => ev.code) - .filter(code => code == "KeyA") - .mapTo(false) + const filterQuery = FetchFilters(sources) - const a$ = - xs.merge(aDown$, aUp$) - .compose(dropRepeats(equals)) - .remember() + // Ghost mode, inject changes via external state updates... + // const ghostChanges$ = modifiedState$ + // .filter((state) => typeof state.core.ghost !== "undefined") + // .compose(dropRepeats()); - /** - * Reducers - */ - // Add the filter values from the settings (and originally from deployments.json) to the current values - const defaultReducer$ = xs.of(prevState => - ({...prevState, core: {...prevState.core, output: {} } }) - ) + const actions = intent(sources.DOM) - // When the query for the current filter values returns we want to update - // the settings... - const filterOptionsReducer$ = - filterValues.filters - .map(fvs => prevState => { - return ({ - ...prevState, - settings: { - ...prevState.settings, - filter: { - ...prevState.settings.filter, - values: fvs - } - }, - core: { - ...prevState.core, - output: fvs - } - }) - } - ) + const vdom$ = view(state$) - const inputReducer$ = input$.map(i => prevState => ({ ...prevState, core: { ...prevState.core, input: i } })) - - const toggleReducer$ = - xs.merge( - doseToggled$, - protocolToggled$, - typeToggled$, - toggledGhost$) - .compose(sampleCombine(a$)) - .map(([clickedFilter, a]) => prevState => { - // a is a modifier key. If it's not pressed it's the usual behaviour - if (a == false) { - // We want this function to work for 3 the different filters, - // so first get the appropriate one - const filterKey = head(keys(clickedFilter)) - const filterValue = prop(filterKey, clickedFilter) - // if already included, remove it from the list - // take into account that no filter means ALL values included - const currentArrayForFilterKey = - (prop(filterKey, prevState.core.output) == undefined) - ? prop(filterKey, prevState.settings.filter.values) - : prop(filterKey, prevState.core.output) - const alreadyIncluded = currentArrayForFilterKey.includes(filterValue) - // does the value have to be removed from the list? - const newArrayForFilterKey = alreadyIncluded - ? currentArrayForFilterKey.filter(el => el != filterValue) - : currentArrayForFilterKey.concat(filterValue) // the value has to be added to the list - const updatedState = assocPath(['core', 'output', filterKey], newArrayForFilterKey, prevState) - return updatedState - } else { - // If a is pressed during the click, toggle ALL values - const filterKey = head(keys(clickedFilter)) - // values currently selected - const currentValues = - (prop(filterKey, prevState.core.output) != undefined) - ? prop(filterKey, prevState.core.output) - : prop(filterKey, prevState.settings.filter.values) - // possible values - const allValues = prop(filterKey, prevState.settings.filter.values) - // possible - current values - const newValues = allValues.filter(v => !currentValues.includes(v)) - const updatedState = assocPath(['core', 'output', filterKey], newValues, prevState) - return updatedState - } - }) + const reducers$ = model( + filterQuery.filters, + input$, + actions.filterValuesAction$, + actions.modifier$, + actions.filterAction$ + ) - const outputReducer$ = - filter$ - .map(i => prevState => ({ ...prevState, core: { ...prevState.core, output: i } })) + const outputTrigger$ = + state$ + .filter( + (state) => + !state.core.state.dose && !state.core.state.cell && !state.core.state.trtType + ) + .map(state => state.core.filter_output) + .compose(dropRepeats(equals)) return { - log: xs.merge( - logger(state$, 'state$'), - ), + log: xs.merge(logger(state$, "state$")), DOM: vdom$, - output: filter$, - HTTP: filterValues.HTTP, - onion: xs.merge( - defaultReducer$, - filterOptionsReducer$, - inputReducer$, - outputReducer$, - toggleReducer$ - ) + HTTP: filterQuery.HTTP, + onion: reducers$, + output: outputTrigger$ } - } export { Filter } diff --git a/src/js/components/Table.js b/src/js/components/Table.js index 8b05e668..3991e0c2 100644 --- a/src/js/components/Table.js +++ b/src/js/components/Table.js @@ -1,416 +1,633 @@ -import xs from 'xstream'; -import sampleCombine from 'xstream/extra/sampleCombine'; -import debounce from 'xstream/extra/debounce' -import { a, h, p, div, br, label, input, code, table, tr, td, b, h2, button, svg, h5, th, thead, tbody, i, span, ul, li } from '@cycle/dom'; -import { log } from '../utils/logger' -import { ENTER_KEYCODE } from '../utils/keycodes.js' -import { keys, values, filter, head, equals, map, prop, clone, omit, merge, intersection, difference } from 'ramda' +import xs from "xstream" +import sampleCombine from "xstream/extra/sampleCombine" +import { + a, + h, + p, + div, + br, + label, + input, + code, + table, + tr, + td, + b, + h2, + button, + svg, + h5, + th, + thead, + tbody, + i, + span, + ul, + li, +} from "@cycle/dom" +import { log } from "../utils/logger" +import { ENTER_KEYCODE } from "../utils/keycodes.js" +import { + keys, + values, + filter, + head, + equals, + map, + prop, + clone, + omit, + merge, + intersection, + difference, +} from "ramda" // import { tableContent, tableContentLens } from './tableContent/tableContent' -import isolate from '@cycle/isolate' -import dropRepeats from 'xstream/extra/dropRepeats' -import dropUntil from 'xstream/extra/dropUntil' -import { stateDebug } from '../utils/utils' -import { loggerFactory } from '../utils/logger' -import { convertToCSV } from '../utils/export' -import delay from 'xstream/extra/delay' +import isolate from "@cycle/isolate" +import dropRepeats from "xstream/extra/dropRepeats" +import { loggerFactory } from "../utils/logger" +import { convertToCSV } from "../utils/export" +import delay from "xstream/extra/delay" +import debounce from "xstream/extra/debounce" // Granular access to the settings // We _copy_ the results array to the root of this element's scope. // This makes it easier to apply fixed scope later in the process const headTableLens = { - get: state => ({ - core: state.headTable, - settings: { - table: state.settings.headTable, - api: state.settings.api, - common: state.settings.common, - sourire: state.settings.sourire, - filter: state.settings.filter - } - }), - set: (state, childState) => ({...state, headTable: childState.core, settings: {...state.settings, headTable: childState.settings.table } }) -}; + get: (state) => ({ + core: state.headTable, + settings: { + table: state.settings.headTable, + api: state.settings.api, + common: state.settings.common, + sourire: state.settings.sourire, + filter: state.settings.filter, + }, + }), + set: (state, childState) => ({ + ...state, + headTable: childState.core, + settings: { ...state.settings, headTable: childState.settings.table }, + }), +} // Granular access to the settings // We _copy_ the results array to the root of this element's scope. // This makes it easier to apply fixed scope later in the process const tailTableLens = { - get: state => ({ - core: state.tailTable, - settings: { - table: state.settings.tailTable, - api: state.settings.api, - common: state.settings.common, - sourire: state.settings.sourire, - filter: state.settings.filter - } - }), - set: (state, childState) => ({...state, tailTable: childState.core, settings: {...state.settings, tailTable: childState.settings.table } }) -}; + get: (state) => ({ + core: state.tailTable, + settings: { + table: state.settings.tailTable, + api: state.settings.api, + common: state.settings.common, + sourire: state.settings.sourire, + filter: state.settings.filter, + }, + }), + set: (state, childState) => ({ + ...state, + tailTable: childState.core, + settings: { ...state.settings, tailTable: childState.settings.table }, + }), +} // Granular access to the settings // We _copy_ the results array to the root of this element's scope. // This makes it easier to apply fixed scope later in the process const compoundContainerTableLens = { - get: state => ({ - core: state.compoundTable, - settings: { - table: state.settings.compoundTable, - api: state.settings.api, - common: state.settings.common, - sourire: state.settings.sourire, - filter: state.settings.filter - } - }), - set: (state, childState) => ({...state, compoundTable: childState.core, settings: {...state.settings, compoundTable: childState.settings.table } }) -}; - -function makeTable(tableComponent, tableLens, scope = 'scope1') { - - /** - * This is a general table container: a post query is sent to the endpoint (configured via settings). - * The resulting array is stored in the state and a dedicated component with onion scope is used to render the effective table. - * - * In other words, for what we use it, query can be either a signature or a (list of) known/predicted target(s). - * - * The lens that defines the rendering of the data is injected using the makeTable function. - */ - return function Table(sources) { - - const logger = loggerFactory('table', sources.onion.state$, 'settings.table.debug') - - const state$ = sources.onion.state$ - const domSource$ = sources.DOM; - const httpSource$ = sources.HTTP; - - // Input handling - const input$ = xs.merge( - sources.input, - // Ghost mode - state$.map(state => state.core.input).compose(dropRepeats(equals)) - ) - - const newInput$ = xs.combine( - input$, - state$ - ) - .map(([newInput, state]) => ({...state, core: {...state.core, input: newInput } })) - .compose(dropRepeats((x, y) => equals(x.core.input, y.core.input))) - - // When the component should not be shown, including empty query - const isEmptyState = (state) => { - if (typeof state.core === 'undefined') { - return true - } else { - if (typeof state.core.input === 'undefined') { - return true - } else { // XXX - if (state.core.input.query === '') { - return true - } else { - return false - } - } - } - } + get: (state) => ({ + core: state.compoundTable, + settings: { + table: state.settings.compoundTable, + api: state.settings.api, + common: state.settings.common, + sourire: state.settings.sourire, + filter: state.settings.filter, + }, + }), + set: (state, childState) => ({ + ...state, + compoundTable: childState.core, + settings: { ...state.settings, compoundTable: childState.settings.table }, + }), +} - const modifiedState$ = state$ - .filter(state => !isEmptyState(state)) - .compose(dropRepeats((x, y) => equals(x.core, y.core))) - - // Split off properties in separate stream to make life easier in simple subcomponents - // Be aware: this is required to be a memory stream!!! - const props$ = state$ - .filter(state => !isEmptyState(state)) - .map(state => state.settings) - .compose(dropRepeats(equals)) - .remember() - - const settings$ = state$ - .map(state => state.settings) - .compose(dropRepeats(equals)) // Avoid updates to vdom$ when no real change - - const expandOptions$ = state$ - .map(state => state.core.expandOptions) - .compose(dropRepeats(equals)) // Avoid updates to vdom$ when no real change - - const emptyState$ = state$ - .filter(state => isEmptyState(state)) - - const plus5$ = sources.DOM.select('.plus5').events('click').mapTo(5).startWith(0).fold((x, y) => x + y, 0) - const min5$ = sources.DOM.select('.min5').events('click').mapTo(5).startWith(0).fold((x, y) => x + y, 0) - const plus10$ = sources.DOM.select('.plus10').events('click').mapTo(10).startWith(0).fold((x, y) => x + y, 0) - const min10$ = sources.DOM.select('.min10').events('click').mapTo(10).startWith(0).fold((x, y) => x + y, 0) - - // ======================================================================== - - const triggerRequest$ = xs.combine( - newInput$, - plus5$, - min5$, - plus10$, - min10$ - ).map(([state, plus5, min5, plus10, min10]) => { - const tableType = state.settings.table.type - const cnt = parseInt(state.settings.table.count) + plus5 - min5 + plus10 - min10 - // Set a limit on the results depending on the type of table: - return (tableType == 'compoundTable') ? - ({...state, core: {...state.core, count: { limit: cnt } } }) : - (tableType == 'topTable') ? - ({...state, core: {...state.core, count: { head: cnt } } }) : - ({...state, core: {...state.core, count: { tail: cnt } } }) - }) - .compose(dropRepeats((x, y) => equals(x.core, y.core))) - .filter(state => state.core.input.query) - - const request$ = triggerRequest$ - .map(state => ({ - send: merge( - state.core.count, { - query: state.core.input.query, - version: 'v2', - // filter: (typeof state.core.input.filter !== 'undefined') ? state.core.input.filter : '', - filter: state.core.input.filter - } - ), - method: 'POST', - url: state.settings.api.url + '&classPath=com.dataintuitive.luciusapi.' + state.settings.table.apiClass, - category: 'table' - })) - - const response$$ = sources.HTTP - .select('table') - - const invalidResponse$ = response$$ - .map(response$ => - response$ - .filter(response => false) // ignore regular event - .replaceError(error => xs.of(error)) // emit error - ) - .flatten() - - const validResponse$ = response$$ - .map(response$ => - response$ - .replaceError(error => xs.empty()) - ) - .flatten() - - // ======================================================================== - - const data$ = validResponse$ - .map(result => result.body.result.data) - - // Convert to TSV and JSON - const csvData$ = data$ - .map(data => convertToCSV(data)) - .map(csv => "text/tsv;charset=utf-8," + encodeURIComponent(csv)) - const jsonData$ = data$ - .map(json => "text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(json))) - - const datas$ = xs.combine(data$, csvData$, jsonData$) - - // ======================================================================== - - // Table with samples/compounds -- depending on the tableComponent - const tableContent = isolate(tableComponent, { onion: tableLens, '*': scope })({...sources, props: props$ }); - - const chipStyle = { - style: { - fontWeight: 'lighter', - 'color': 'rgba(255, 255, 255, 0.5)', - 'background-color': 'rgba(0, 0, 0, 0.2)' - } +function makeTable(tableComponent, tableLens, scope = "scope1") { + /** + * This is a general table container: a post query is sent to the endpoint (configured via settings). + * The resulting array is stored in the state and a dedicated component with onion scope is used to render the effective table. + * + * In other words, for what we use it, query can be either a signature or a (list of) known/predicted target(s). + * + * The lens that defines the rendering of the data is injected using the makeTable function. + */ + return function Table(sources) { + const logger = loggerFactory( + "table", + sources.onion.state$, + "settings.table.debug" + ) + + const state$ = sources.onion.state$ + + // Input handling + const input$ = xs.merge( + sources.input + // Ghost mode + // state$.map(state => state.core.input).compose(dropRepeats(equals)) + ) + + const modifiedState$ = state$ + .filter((state) => !isEmptyState(state)) + .compose(dropRepeats((x, y) => equals(x.core.input, y.core.input))) + + const newInput$ = xs + .combine(input$, modifiedState$) + .map(([newInput, state]) => ({ + ...state, + core: { ...state.core, input: newInput }, + })) + .compose(dropRepeats((x, y) => equals(x.core.input, y.core.input))) + + // When the component should not be shown, including empty query + const isEmptyState = (state) => { + if (typeof state.core === "undefined") { + return true + } else { + if (typeof state.core.input === "undefined") { + return true + } else { + // XXX + if (state.core.input.query === "") { + return true + } else { + return false + } } + } + } - const filterDom$ = modifiedState$ - .compose(dropRepeats((x, y) => equals(x.core.input.filter, y.core.input.filter))) - .map(state => { - let filterKeys = keys(state.core.input.filter) - // Derive some relevant information and store it in an object - let filterDiffs = - map( - key => ({ - 'key': key, - 'selectedValues': prop(key, state.core.input.filter), - 'possibleValues': prop(key, state.settings.filter.values), - 'intersection': intersection(prop(key, state.core.input.filter), prop(key, state.settings.filter.values)), - 'difference': difference(prop(key, state.settings.filter.values), prop(key, state.core.input.filter)), - 'half': ((prop(key, state.settings.filter.values).length/2 - prop(key, state.core.input.filter).length ) < 0) - }), - filterKeys - ) - // Only show filters if something is filtered, so no filter if all or none of the options are selected ! - const nonEmptyFilters = - filter( - filter => !(filter.intersection.length == filter.possibleValues.length) && !(filter.selectedValues.length == 0), - filterDiffs - ) - .map(filter => ({ ...filter, values: filter.selectedValues })) - let divs = map(filter => - div('.chip', chipStyle, - (filter.half) - ? [ filter.key + 's excluded', ': ', filter.difference.join(', ') ] - : [filter.key + 's included', ': ', filter.values.join(', ') ] - ), nonEmptyFilters) - return divs - }).startWith([]) - - const smallBtnStyle = bgcolor => ({ - style: { - 'margin-bottom': '0px', - 'margin-top': '0px', - 'background-color': bgcolor, - 'color': 'white', - opacity: 0.8, - fontWeight: 'lighter', - fontSize: '16px', - 'vertical-align': 'middle' - } - }) - - // ======================================================================== - - const initVdom$ = emptyState$.mapTo(div()) - - const loadingVdom$ = request$.compose(sampleCombine(filterDom$, modifiedState$)) - .map(([ - r, - filterText, - state - ]) => div([ - div('.row .valign-wrapper', { style: { 'margin-bottom': '0px', 'padding-top': '5px', 'background-color': state.settings.table.color, opacity: 0.5 } }, [ - h5('.white-text .col .s5 .valign', state.settings.table.title), - div('.white-text .col .s7 .valign .right-align', filterText) - ]), - div('.progress ', { style: { margin: '2px 0px 2px 0px' } }, [div('.indeterminate')]) - ]), ).remember() - - const loadedVdom$ = - xs.combine(tableContent.DOM, expandOptions$, settings$, csvData$, jsonData$, filterDom$) - .map(([ - dom, - expandOptions, - settings, - csvData, - jsonData, - filterText, - ]) => - div([ - div('.pagebreak', []), - div('.row .valign-wrapper .switch', { style: { 'margin-bottom': '0px', 'padding-top': '5px', 'background-color': settings.table.color } }, [ - h5('.white-text .col', [ - settings.table.title, - span([' ']), - i('.material-icons .grey-text', { - style: { - fontSize: '16px', - 'background-color': settings.table.color, - opacity: 0.5, - } - }, 'add') - ]), - div('.white-text .col .s7 .valign .right-align', filterText) - ]), - div('.row .valign-wrapper', { style: { 'margin-bottom': '0px', 'padding-top': '0px', 'background-color': settings.table.color, opacity: 0.8 } }, [ - (expandOptions) ? - div([ - button('.btn-flat .waves-effect .waves-light', smallBtnStyle(settings.table.color), [ - a('', { style: { 'color': 'white' }, props: { href: 'data:' + csvData, download: 'table.tsv' } }, [ - span({ style: { 'vertical-align': 'top', fontSize: '8px' } }, 'tsv'), - i('.material-icons', 'file_download'), - ]) - ]), - button('.btn-flat .waves-effect .waves-light', smallBtnStyle(settings.table.color), [ - a('', { style: { 'color': 'white' }, props: { href: 'data:' + jsonData, download: 'table.json' } }, [ - span({ style: { 'vertical-align': 'top', fontSize: '8px' } }, 'json'), - i('.material-icons', 'file_download'), - ]) - ]), - button('.min10 .btn-flat .waves-effect .waves-light', smallBtnStyle(settings.table.color), [ - span({ style: { 'vertical-align': 'top', fontSize: '10px' } }, '-10'), - i('.material-icons', 'fast_rewind'), - ]), - button('.min5 .btn-flat .waves-effect .waves-light', smallBtnStyle(settings.table.color), [ - span({ style: { 'vertical-align': 'top', fontSize: '10px' } }, '-5'), - i('.material-icons', 'fast_rewind'), - ]), - button('.plus5 .btn-flat .waves-effect .waves-light', smallBtnStyle(settings.table.color), [ - span({ style: { 'vertical-align': 'top', fontSize: '10px' } }, '+5'), - i('.material-icons', 'fast_forward') - ]), - button('.plus10 .btn-flat .waves-effect .waves-light', smallBtnStyle(settings.table.color), [ - span({ style: { 'vertical-align': 'top', fontSize: '10px' } }, '+10'), - i('.material-icons', 'fast_forward') - ]) - ]) : - div() - ]), - div('.row', { style: { 'margin-bottom': '0px', 'margin-top': '0px' } }, [ - dom - ]), - ]) - ) - - const errorVdom$ = invalidResponse$.mapTo(div('.red .white-text', [p('An error occured !!!')])) - - const vdom$ = xs.merge( - initVdom$, - errorVdom$, - loadingVdom$, - loadedVdom$ - ) + // Split off properties in separate stream to make life easier in simple subcomponents + // Be aware: this is required to be a memory stream!!! + const props$ = state$ + .filter((state) => !isEmptyState(state)) + .map((state) => state.settings) + .compose(dropRepeats(equals)) + .remember() + + const settings$ = state$ + .map((state) => state.settings) + .compose(dropRepeats(equals)) // Avoid updates to vdom$ when no real change + + const expandOptions$ = modifiedState$ + .map((state) => state.core.expandOptions) + .compose(dropRepeats(equals)) // Avoid updates to vdom$ when no real change + + const emptyState$ = state$.filter((state) => isEmptyState(state)) + + const plus5$ = sources.DOM.select(".plus5") + .events("click") + .mapTo(5) + .startWith(0) + .fold((x, y) => x + y, 0) + const min5$ = sources.DOM.select(".min5") + .events("click") + .mapTo(5) + .startWith(0) + .fold((x, y) => x + y, 0) + const plus10$ = sources.DOM.select(".plus10") + .events("click") + .mapTo(10) + .startWith(0) + .fold((x, y) => x + y, 0) + const min10$ = sources.DOM.select(".min10") + .events("click") + .mapTo(10) + .startWith(0) + .fold((x, y) => x + y, 0) + + // ======================================================================== + + const triggerRequest$ = xs + .combine(newInput$, plus5$, min5$, plus10$, min10$) + .map(([state, plus5, min5, plus10, min10]) => { + const tableType = state.settings.table.type + const cnt = + parseInt(state.settings.table.count) + plus5 - min5 + plus10 - min10 + // Set a limit on the results depending on the type of table: + return tableType == "compoundTable" + ? { ...state, core: { ...state.core, count: { limit: cnt } } } + : tableType == "topTable" + ? { ...state, core: { ...state.core, count: { head: cnt } } } + : { ...state, core: { ...state.core, count: { tail: cnt } } } + }) + .compose(dropRepeats((x, y) => equals(x.core, y.core))) + .filter((state) => state.core.input.query) + + const request$ = triggerRequest$.map((state) => ({ + send: merge(state.core.count, { + query: state.core.input.query, + version: "v2", + // filter: (typeof state.core.input.filter !== 'undefined') ? state.core.input.filter : '', + filter: state.core.input.filter, + }), + method: "POST", + url: + state.settings.api.url + + "&classPath=com.dataintuitive.luciusapi." + + state.settings.table.apiClass, + category: "table", + })) + + const response$$ = sources.HTTP.select("table") + + const invalidResponse$ = response$$ + .map( + (response$) => + response$ + .filter((response) => false) // ignore regular event + .replaceError((error) => xs.of(error)) // emit error + ) + .flatten() + + const validResponse$ = response$$ + .map((response$) => response$.replaceError((error) => xs.empty())) + .flatten() + + // ======================================================================== + + const data$ = validResponse$.map((result) => result.body.result.data) + + // Convert to TSV and JSON + const csvData$ = data$ + .map((data) => convertToCSV(data)) + .map((csv) => "text/tsv;charset=utf-8," + encodeURIComponent(csv)) + const jsonData$ = data$.map( + (json) => + "text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(json)) + ) + + const datas$ = xs.combine(data$, csvData$, jsonData$) + + // ======================================================================== + + // Table with samples/compounds -- depending on the tableComponent + const tableContent = isolate(tableComponent, { + onion: tableLens, + "*": scope, + })({ ...sources, props: props$ }) + + const chipStyle = { + style: { + fontWeight: "lighter", + color: "rgba(255, 255, 255, 0.5)", + "background-color": "rgba(0, 0, 0, 0.2)", + }, + } - // ======================================================================== - - // Default Reducer - const defaultReducer$ = xs.of(prevState => - ({...prevState, core: {...prevState.core, input: { query: '', targets: '' } } }) - ) - // Add input to state - const inputReducer$ = input$.map(i => prevState => - // inputReducer - ({...prevState, core: {...prevState.core, input: i } }) - ) - // Add request body to state - const requestReducer$ = request$.map(req => prevState => - ({...prevState, core: {...prevState.core, request: req } }) - ) - // Data reducer - const dataReducer$ = data$.map(newData => prevState => - ({...prevState, core: {...prevState.core, data: newData } }) - ) - // Reducer for opening and closing option drawer - const switchReducer$ = sources.DOM.select('.switch') - .events('click') - .fold((x, y) => !x, false) - .map(yesorno => prevState => - ({...prevState, core: {...prevState.core, expandOptions: yesorno } }) - ) - - return { - DOM: vdom$, - HTTP: request$, - onion: xs.merge( - defaultReducer$, - inputReducer$, - requestReducer$, - dataReducer$, - switchReducer$ + const filterDom$ = modifiedState$ + .compose( + dropRepeats((x, y) => equals(x.core.input.filter, y.core.input.filter)) + ) + .map((state) => { + let filterKeys = keys(state.core.input.filter) + // Derive some relevant information and store it in an object + let filterDiffs = map( + (key) => ({ + key: key, + selectedValues: prop(key, state.core.input.filter), + possibleValues: prop(key, state.settings.filter.values), + intersection: intersection( + prop(key, state.core.input.filter), + prop(key, state.settings.filter.values) ), - log: xs.merge( - logger(modifiedState$, 'state$'), - // logger(loadingVdom$, 'loading...'), - // logger(loadedVdom$, 'loaded...') - // logger(newInput$, 'newInput$'), - // logger(request$, 'request$'), - // logger(validResponse$, 'validResponse$'), - // logger(invalidResponse$, 'invalidResponse$'), + difference: difference( + prop(key, state.settings.filter.values), + prop(key, state.core.input.filter) ), - }; - + half: + prop(key, state.settings.filter.values).length / 2 - + prop(key, state.core.input.filter).length < + 0, + }), + filterKeys + ) + // Only show filters if something is filtered, so no filter if all or none of the options are selected ! + const nonEmptyFilters = filter( + (filter) => + !(filter.intersection.length == filter.possibleValues.length) && + !(filter.selectedValues.length == 0), + filterDiffs + ).map((filter) => ({ ...filter, values: filter.selectedValues })) + let divs = map( + (filter) => + div( + ".chip", + chipStyle, + filter.half + ? [ + filter.key + "s excluded", + ": ", + filter.difference.join(", "), + ] + : [filter.key + "s included", ": ", filter.values.join(", ")] + ), + nonEmptyFilters + ) + return divs + }) + .startWith([]) + + const smallBtnStyle = (bgcolor) => ({ + style: { + "margin-bottom": "0px", + "margin-top": "0px", + "background-color": bgcolor, + color: "white", + opacity: 0.8, + fontWeight: "lighter", + fontSize: "16px", + "vertical-align": "middle", + }, + }) + + // ======================================================================== + + const initVdom$ = emptyState$.mapTo(div()) + + const loadingVdom$ = request$ + .compose(sampleCombine(filterDom$, modifiedState$)) + .map(([r, filterText, state]) => + div([ + div( + ".row .valign-wrapper", + { + style: { + "margin-bottom": "0px", + "padding-top": "5px", + "background-color": state.settings.table.color, + opacity: 0.5, + }, + }, + [ + h5(".white-text .col .s5 .valign", state.settings.table.title), + div(".white-text .col .s7 .valign .right-align", filterText), + ] + ), + div(".progress ", { style: { margin: "2px 0px 2px 0px" } }, [ + div(".indeterminate"), + ]), + ]) + ) + .remember() + + const loadedVdom$ = xs + .combine( + tableContent.DOM, + expandOptions$, + settings$, + csvData$, + jsonData$, + filterDom$ + ) + .map(([dom, expandOptions, settings, csvData, jsonData, filterText]) => + div([ + div(".pagebreak", []), + div( + ".row .valign-wrapper .switch", + { + style: { + "margin-bottom": "0px", + "padding-top": "5px", + "background-color": settings.table.color, + }, + }, + [ + h5(".white-text .col", [ + settings.table.title, + span([" "]), + i( + ".material-icons .grey-text", + { + style: { + fontSize: "16px", + "background-color": settings.table.color, + opacity: 0.5, + }, + }, + "add" + ), + ]), + div(".white-text .col .s7 .valign .right-align", filterText), + ] + ), + div( + ".row .valign-wrapper", + { + style: { + "margin-bottom": "0px", + "padding-top": "0px", + "background-color": settings.table.color, + opacity: 0.8, + }, + }, + [ + expandOptions + ? div([ + button( + ".btn-flat .waves-effect .waves-light", + smallBtnStyle(settings.table.color), + [ + a( + "", + { + style: { color: "white" }, + props: { + href: "data:" + csvData, + download: "table.tsv", + }, + }, + [ + span( + { + style: { + "vertical-align": "top", + fontSize: "8px", + }, + }, + "tsv" + ), + i(".material-icons", "file_download"), + ] + ), + ] + ), + button( + ".btn-flat .waves-effect .waves-light", + smallBtnStyle(settings.table.color), + [ + a( + "", + { + style: { color: "white" }, + props: { + href: "data:" + jsonData, + download: "table.json", + }, + }, + [ + span( + { + style: { + "vertical-align": "top", + fontSize: "8px", + }, + }, + "json" + ), + i(".material-icons", "file_download"), + ] + ), + ] + ), + button( + ".min10 .btn-flat .waves-effect .waves-light", + smallBtnStyle(settings.table.color), + [ + span( + { + style: { + "vertical-align": "top", + fontSize: "10px", + }, + }, + "-10" + ), + i(".material-icons", "fast_rewind"), + ] + ), + button( + ".min5 .btn-flat .waves-effect .waves-light", + smallBtnStyle(settings.table.color), + [ + span( + { + style: { + "vertical-align": "top", + fontSize: "10px", + }, + }, + "-5" + ), + i(".material-icons", "fast_rewind"), + ] + ), + button( + ".plus5 .btn-flat .waves-effect .waves-light", + smallBtnStyle(settings.table.color), + [ + span( + { + style: { + "vertical-align": "top", + fontSize: "10px", + }, + }, + "+5" + ), + i(".material-icons", "fast_forward"), + ] + ), + button( + ".plus10 .btn-flat .waves-effect .waves-light", + smallBtnStyle(settings.table.color), + [ + span( + { + style: { + "vertical-align": "top", + fontSize: "10px", + }, + }, + "+10" + ), + i(".material-icons", "fast_forward"), + ] + ), + ]) + : div(), + ] + ), + div( + ".row", + { style: { "margin-bottom": "0px", "margin-top": "0px" } }, + [dom] + ), + ]) + ) + + const errorVdom$ = invalidResponse$.mapTo( + div(".red .white-text", [p("An error occured !!!")]) + ) + + const vdom$ = xs.merge(initVdom$, errorVdom$, loadingVdom$.remember(), loadedVdom$) + + // ======================================================================== + + // Default Reducer + const defaultReducer$ = xs + .of(function defaultReducer(prevState) { + if (typeof prevState === "undefined") { + return ({ }) + } else { + return prevState + } + }) + + // Add input to state + const inputReducer$ = input$ + .map((i) => (prevState) => + // inputReducer + ({ ...prevState, core: { ...prevState.core, input: i } }) + ) + + // Add request body to state + const requestReducer$ = request$.map((req) => (prevState) => ({ + ...prevState, + core: { ...prevState.core, request: req }, + })) + + // Data reducer + const dataReducer$ = data$ + .map((newData) => (prevState) => ({ + ...prevState, + core: { ...prevState.core, data: newData }, + })) + + // Reducer for opening and closing option drawer + const switchReducer$ = sources.DOM.select(".switch") + .events("click") + .fold((x, y) => !x, false) + .map((yesorno) => (prevState) => ({ + ...prevState, + core: { ...prevState.core, expandOptions: yesorno }, + })) + + return { + DOM: vdom$, + HTTP: request$, + onion: xs.merge( + defaultReducer$, + inputReducer$, + requestReducer$, + dataReducer$, + switchReducer$ + ), + log: xs.merge( + logger(modifiedState$, "state$") + ), } - + } } export { makeTable, headTableLens, tailTableLens, compoundContainerTableLens } diff --git a/src/js/pages/compound.js b/src/js/pages/compound.js index 94dd47f7..f7dc84a2 100644 --- a/src/js/pages/compound.js +++ b/src/js/pages/compound.js @@ -1,172 +1,174 @@ -import { i, a, div, br, label, input, p, button, code, pre } from '@cycle/dom' -import xs from 'xstream' -import isolate from '@cycle/isolate' -import { mergeWith, merge } from 'ramda' -import { clone, equal, equals, mergeAll } from 'ramda' -import { CompoundForm } from '../components/CompoundForm' -import dropRepeats from 'xstream/extra/dropRepeats' -import { initSettings } from './settings' -import { makeTable, headTableLens, tailTableLens } from '../components/Table' -import { BinnedPlots, plotsLens } from '../components/BinnedPlots/BinnedPlots' -import { Filter, compoundFilterLens } from '../components/Filter' -import concat from 'xstream/extra/dropRepeats' -import { loggerFactory } from '../utils/logger' -import { SampleTable, sampleTableLens } from '../components/SampleTable/SampleTable' +import { div } from "@cycle/dom" +import xs from "xstream" +import isolate from "@cycle/isolate" +import { CompoundForm } from "../components/CompoundForm" +import { initSettings } from "../configuration.js" +import { makeTable, headTableLens, tailTableLens } from "../components/Table" +import { BinnedPlots, plotsLens } from "../components/BinnedPlots/BinnedPlots" +import { Filter, filterLens } from "../components/Filter" +import { loggerFactory } from "../utils/logger" +import { + SampleTable, + sampleTableLens, +} from "../components/SampleTable/SampleTable" // Support for ghost mode -import { scenario } from '../scenarios/compoundScenario' -import { runScenario } from '../utils/scenario' -import { SignatureGenerator } from '../components/SignatureGenerator'; +import { scenario } from "../scenarios/compoundScenario" +import { runScenario } from "../utils/scenario" export default function CompoundWorkflow(sources) { + const logger = loggerFactory( + "compound", + sources.onion.state$, + "settings.common.debug" + ) - const logger = loggerFactory('compound', sources.onion.state$, 'settings.common.debug') + const state$ = sources.onion.state$ - const state$ = sources.onion.state$ + // Scenario for ghost mode + const scenarioReducer$ = sources.onion.state$ + .take(1) + .filter((state) => state.settings.common.ghostMode) + .mapTo(runScenario(scenario).scenarioReducer$) + .flatten() + .startWith((prevState) => prevState) + const scenarioPopup$ = sources.onion.state$ + .take(1) + .filter((state) => state.settings.common.ghostMode) + .mapTo(runScenario(scenario).scenarioPopup$) + .flatten() + .startWith({ text: "Welcome to Compound Workflow", duration: 4000 }) - // Scenario for ghost mode - const scenarioReducer$ = - sources.onion.state$.take(1) - .filter(state => state.settings.common.ghostMode) - .mapTo(runScenario(scenario).scenarioReducer$) - .flatten() - .startWith(prevState => prevState) - const scenarioPopup$ = - sources.onion.state$.take(1) - .filter(state => state.settings.common.ghostMode) - .mapTo(runScenario(scenario).scenarioPopup$) - .flatten() - .startWith({ text: 'Welcome to Compound Workflow', duration: 4000 }) + const formLens = { + get: (state) => ({ + form: state.form, + settings: { + form: state.settings.form, + api: state.settings.api, + common: state.settings.common, + geneAnnotations: state.settings.geneAnnotations, + compoundAnnotations: state.settings.compoundAnnotations, + }, + }), + set: (state, childState) => ({ ...state, form: childState.form }), + } - const formLens = { - get: state => ({ - form: state.form, - settings: { - form: state.settings.form, - api: state.settings.api, - common: state.settings.common, - geneAnnotations: state.settings.geneAnnotations, - compoundAnnotations: state.settings.compoundAnnotations - } - }), - set: (state, childState) => ({...state, form: childState.form }) - }; - - const CompoundFormSink = isolate(CompoundForm, { onion: formLens })(sources) - const signature$ = CompoundFormSink.output + // Initialize if not yet done in parent (i.e. router) component (useful for testing) + const defaultReducer$ = xs.of((prevState) => { + // compound -- defaultReducer + if (typeof prevState === "undefined") { + return { + settings: initSettings, + } + } else { + return { + ...prevState, + settings: prevState.settings, + } + } + }) - // Initialize if not yet done in parent (i.e. router) component (useful for testing) - const defaultReducer$ = xs.of(prevState => { - // compound -- defaultReducer - if (typeof prevState === 'undefined') { - return ({ - settings: initSettings, - form: {}, - sim: {}, - hist: {}, - filter: {}, - headTable: {}, - tailTable: {}, - geneAnnotations: {} - }) - } else { - return ({ - ...prevState, - settings: prevState.settings, - form: {}, - sim: {}, - hist: {}, - filter: {}, - headTable: {}, - tailTable: {}, - geneAnnotations: {} - }) - } - }) + const CompoundFormSink = isolate(CompoundForm, { onion: formLens })(sources) + const signature$ = CompoundFormSink.output.remember() - // Filter Form - const filterForm = isolate(Filter, { onion: compoundFilterLens })({...sources, input: signature$ }) - const filter$ = filterForm.output.remember() + // Filter Form + const filterForm = isolate(Filter, { onion: filterLens })({ + ...sources, + input: signature$, + }) + const filter$ = filterForm.output.remember() - // Binned Plots Component - const binnedPlots = isolate(BinnedPlots, { onion: plotsLens }) - ({...sources, input: xs.combine(signature$, filter$).map(([s, f]) => ({ signature: s, filter: f })).remember() }); + // const filter$ = xs.of({}) + // Binned Plots Component + const binnedPlots = isolate(BinnedPlots, { onion: plotsLens })({ + ...sources, + input: xs + .combine(signature$, filter$) + .map(([s, f]) => ({ signature: s, filter: f })) + .remember(), + }) - // tables - const headTableContainer = makeTable(SampleTable, sampleTableLens) - const tailTableContainer = makeTable(SampleTable, sampleTableLens) + // tables + const headTableContainer = makeTable(SampleTable, sampleTableLens) + const tailTableContainer = makeTable(SampleTable, sampleTableLens) - // Join settings from api and sourire into props - const headTable = isolate(headTableContainer, { onion: headTableLens }) - ({...sources, input: xs.combine(signature$, filter$).map(([s, f]) => ({ query: s, filter: f })).remember() }); - const tailTable = isolate(tailTableContainer, { onion: tailTableLens }) - ({...sources, input: xs.combine(signature$, filter$).map(([s, f]) => ({ query: s, filter: f })).remember() }); + // Join settings from api and sourire into props + const headTable = isolate(headTableContainer, { onion: headTableLens })({ + ...sources, + input: xs + .combine(signature$, filter$) + .map(([s, f]) => ({ query: s, filter: f })) + .remember(), + }) + const tailTable = isolate(tailTableContainer, { onion: tailTableLens })({ + ...sources, + input: xs + .combine(signature$, filter$) + .map(([s, f]) => ({ query: s, filter: f })) + .remember(), + }) - const pageStyle = { - style: { - fontSize: '14px', - opacity: '0', - transition: 'opacity 1s', - delayed: { opacity: '1' }, - destroy: { opacity: '0' } - } - } + const pageStyle = { + style: { + fontSize: "14px", + opacity: "0", + transition: "opacity 1s", + delayed: { opacity: "1" }, + destroy: { opacity: "0" }, + }, + } - const vdom$ = xs.combine( - CompoundFormSink.DOM, - filterForm.DOM, - binnedPlots.DOM, - headTable.DOM, - tailTable.DOM, - ) - .map(([ - formDOM, - filter, - plots, - headTable, - tailTable - ]) => div('.row .compound', { style: { margin: '0px 0px 0px 0px' } }, [ - formDOM, - div('.col .s10 .offset-s1', pageStyle, [ - div('.row', [filter]), - div('.row', [ plots ]), - div('.col .s12', [headTable]), - div('.row', []), - div('.col .s12', [tailTable]), - div('.row', []) - ]) - ])) + const vdom$ = xs + .combine( + CompoundFormSink.DOM, + filterForm.DOM, + binnedPlots.DOM, + headTable.DOM, + tailTable.DOM + ) + .map(([formDOM, filter, plots, headTable, tailTable]) => + div(".row .compound", { style: { margin: "0px 0px 0px 0px" } }, [ + formDOM, + div(".col .s10 .offset-s1", pageStyle, [ + div(".row", [filter]), + div(".row", [plots]), + div(".col .s12", [headTable]), + div(".row", []), + div(".col .s12", [tailTable]), + div(".row", []), + ]), + ]) + ) - return { - log: xs.merge( - logger(state$, 'state$'), - CompoundFormSink.log, - filterForm.log, - binnedPlots.log, - headTable.log, - tailTable.log - ), - DOM: vdom$, - onion: xs.merge( - defaultReducer$, - CompoundFormSink.onion, - binnedPlots.onion, - filterForm.onion, - headTable.onion, - tailTable.onion, - scenarioReducer$ - ), - HTTP: xs.merge( - CompoundFormSink.HTTP, - binnedPlots.HTTP, - headTable.HTTP, - tailTable.HTTP - ), - vega: binnedPlots.vega, - popup: scenarioPopup$, - modal: xs.merge( - CompoundFormSink.modal - ), - ac: CompoundFormSink.ac - }; + return { + log: xs.merge( + logger(state$, "state$"), + CompoundFormSink.log, + filterForm.log, + binnedPlots.log, + headTable.log, + tailTable.log + ), + DOM: vdom$.startWith(div()), + onion: xs.merge( + defaultReducer$, + CompoundFormSink.onion, + binnedPlots.onion, + filterForm.onion, + headTable.onion, + tailTable.onion, + scenarioReducer$ + ), + HTTP: xs.merge( + CompoundFormSink.HTTP, + filterForm.HTTP, + binnedPlots.HTTP, + headTable.HTTP, + tailTable.HTTP + ), + vega: binnedPlots.vega, + popup: scenarioPopup$, + modal: xs.merge(CompoundFormSink.modal), + ac: CompoundFormSink.ac, + } } diff --git a/src/js/pages/correlation.js b/src/js/pages/correlation.js index 26c75252..dfb15a70 100644 --- a/src/js/pages/correlation.js +++ b/src/js/pages/correlation.js @@ -9,7 +9,7 @@ import dropRepeats from 'xstream/extra/dropRepeats' import { CorrelationForm, formLens } from '../components/CorrelationForm' import { CorrelationPlot, correlationPlotsLens } from '../components/BinnedPlots/CorrelationPlot' import { makeTable, headTableLens, tailTableLens } from '../components/Table' -import { initSettings } from './settings' +import { initSettings } from '../configuration.js' import { Filter, compoundFilterLens } from '../components/Filter' import { loggerFactory } from '../utils/logger' import { SampleTable, sampleTableLens } from '../components/SampleTable/SampleTable' diff --git a/src/js/pages/disease.js b/src/js/pages/disease.js index a416b290..28d3d299 100644 --- a/src/js/pages/disease.js +++ b/src/js/pages/disease.js @@ -1,155 +1,176 @@ -import { a, div, br, label, input, p, button, code, pre } from '@cycle/dom' -import xs from 'xstream' -import isolate from '@cycle/isolate' -import { mergeWith, merge } from 'ramda' -import { clone, equal, equals, mergeAll } from 'ramda'; -import dropRepeats from 'xstream/extra/dropRepeats' +import { div } from "@cycle/dom" +import xs from "xstream" +import isolate from "@cycle/isolate" // Components -import { SignatureForm, formLens } from '../components/SignatureForm' -import { BinnedPlots, plotsLens } from '../components/BinnedPlots/BinnedPlots' -import { makeTable, headTableLens, tailTableLens } from '../components/Table' -import { initSettings } from './settings' -import { Filter, compoundFilterLens } from '../components/Filter' -import { loggerFactory } from '../utils/logger' -import { SampleTable, sampleTableLens } from '../components/SampleTable/SampleTable' +import { SignatureForm, formLens } from "../components/SignatureForm" +import { BinnedPlots, plotsLens } from "../components/BinnedPlots/BinnedPlots" +import { makeTable, headTableLens, tailTableLens } from "../components/Table" +import { initSettings } from "../configuration.js" +import { Filter, filterLens } from "../components/Filter" +import { loggerFactory } from "../utils/logger" +import { + SampleTable, + sampleTableLens, +} from "../components/SampleTable/SampleTable" // Support for ghost mode -import { scenario } from '../scenarios/diseaseScenario' -import { runScenario } from '../utils/scenario' +import { scenario } from "../scenarios/diseaseScenario" +import { runScenario } from "../utils/scenario" function DiseaseWorkflow(sources) { - - const logger = loggerFactory('signature', sources.onion.state$, 'settings.common.debug') - - const state$ = sources.onion.state$ - - // Scenario for ghost mode - const scenarioReducer$ = - sources.onion.state$.take(1) - .filter(state => state.settings.common.ghostMode) - .mapTo(runScenario(scenario).scenarioReducer$) - .flatten() - .startWith(prevState => prevState) - const scenarioPopup$ = - sources.onion.state$.take(1) - .filter(state => state.settings.common.ghostMode) - .mapTo(runScenario(scenario).scenarioPopup$) - .flatten() - .startWith({ text: 'Welcome to Disease Workflow', duration: 4000 }) - - - /** - * Parse feedback from vega components. Not used yet... - */ - // const feedback$ = sources.vega.map(item => item).startWith(null).debug(); - // const feedback$ = domSource$.select('.SignatureCheck').events('click').mapTo('click !').startWith(null); - - const signatureForm = isolate(SignatureForm, { onion: formLens })(sources) - const signature$ = signatureForm.output - - // Filter Form - const filterForm = isolate(Filter, { onion: compoundFilterLens })({...sources, input: signature$ }) - const filter$ = filterForm.output.remember() - - // default Reducer, initialization - const defaultReducer$ = xs.of(prevState => { - // disease -- defaultReducer - if (typeof prevState === 'undefined') { - return ({ - settings: initSettings, - form: {}, - }) - } else { - return ({ - ...prevState, - settings: prevState.settings, - form: {}, - }) - } - }) - - // Binned Plots Component - const binnedPlots = isolate(BinnedPlots, { onion: plotsLens }) - ({...sources, input: xs.combine(signature$, filter$).map(([s, f]) => ({ signature: s, filter: f })).remember() }); - - // tables - const headTableContainer = makeTable(SampleTable, sampleTableLens) - const tailTableContainer = makeTable(SampleTable, sampleTableLens) - - // Join settings from api and sourire into props - const headTable = isolate(headTableContainer, { onion: headTableLens }) - ({...sources, input: xs.combine(signature$, filter$).map(([s, f]) => ({ query: s, filter: f })).remember() }); - const tailTable = isolate(tailTableContainer, { onion: tailTableLens }) - ({...sources, input: xs.combine(signature$, filter$).map(([s, f]) => ({ query: s, filter: f })).remember() }); - - const pageStyle = { - style: { - fontSize: '14px', - opacity: '0', - transition: 'opacity 1s', - delayed: { opacity: '1' }, - destroy: { opacity: '0' }, - } + const logger = loggerFactory( + "signature", + sources.onion.state$, + "settings.common.debug" + ) + + const state$ = sources.onion.state$ + + // Scenario for ghost mode + const scenarioReducer$ = sources.onion.state$ + .take(1) + .filter((state) => state.settings.common.ghostMode) + .mapTo(runScenario(scenario).scenarioReducer$) + .flatten() + .startWith((prevState) => prevState) + const scenarioPopup$ = sources.onion.state$ + .take(1) + .filter((state) => state.settings.common.ghostMode) + .mapTo(runScenario(scenario).scenarioPopup$) + .flatten() + .startWith({ text: "Welcome to Disease Workflow", duration: 4000 }) + + /** + * Parse feedback from vega components. Not used yet... + * + * const feedback$ = sources.vega.map(item => item).startWith(null).debug(); + * const feedback$ = domSource$.select('.SignatureCheck').events('click').mapTo('click !').startWith(null); + */ + + const signatureForm = isolate(SignatureForm, { onion: formLens })(sources) + const signature$ = signatureForm.output + + // default Reducer, initialization + const defaultReducer$ = xs.of((prevState) => { + // disease -- defaultReducer + if (typeof prevState === "undefined") { + return { + settings: initSettings, + } + } else { + return { + ...prevState, + settings: prevState.settings, + } } - - const vdom$ = xs.combine( - signatureForm.DOM, - filterForm.DOM, - binnedPlots.DOM, - headTable.DOM, - tailTable.DOM, - // feedback$ - ) - .map(([ - form, - filter, - plots, - headTable, - tailTable, - // feedback - ]) => - div('.row .disease', { style: { margin: '0px 0px 0px 0px' } }, [ - form, - div('.col .s10 .offset-s1', pageStyle, [ - div('.row', [filter]), - div('.row', [plots]), - div('.row', []), - div('.col .s12', [headTable]), - div('.row', []), - div('.col .s12', [tailTable]), - div('.row', []) - ]) - ]) - ); - - return { - log: xs.merge( - logger(state$, 'state$'), - binnedPlots.log, - signatureForm.log - ), - DOM: vdom$, - onion: xs.merge( - defaultReducer$, - signatureForm.onion, - filterForm.onion, - binnedPlots.onion, - headTable.onion, - tailTable.onion, - scenarioReducer$ - ), - vega: xs.merge( - binnedPlots.vega - ), - HTTP: xs.merge( - signatureForm.HTTP, - binnedPlots.HTTP, - headTable.HTTP, - tailTable.HTTP - ), - popup: scenarioPopup$ - }; + }) + + // Filter Form + const filterForm = isolate(Filter, { onion: filterLens })({ + ...sources, + input: signature$, + }) + const filter$ = filterForm.output.remember() + + // Binned Plots Component + const binnedPlots = isolate(BinnedPlots, { onion: plotsLens })({ + ...sources, + input: xs + .combine(signature$, filter$) + .map(([s, f]) => ({ signature: s, filter: f })) + .remember(), + }) + + // tables + const headTableContainer = makeTable(SampleTable, sampleTableLens) + const tailTableContainer = makeTable(SampleTable, sampleTableLens) + + // Join settings from api and sourire into props + const headTable = isolate(headTableContainer, { onion: headTableLens })({ + ...sources, + input: xs + .combine(signature$, filter$) + .map(([s, f]) => ({ query: s, filter: f })) + .remember(), + }) + const tailTable = isolate(tailTableContainer, { onion: tailTableLens })({ + ...sources, + input: xs + .combine(signature$, filter$) + .map(([s, f]) => ({ query: s, filter: f })) + .remember(), + }) + + const pageStyle = { + style: { + fontSize: "14px", + opacity: "0", + transition: "opacity 1s", + delayed: { opacity: "1" }, + destroy: { opacity: "0" }, + }, + } + + const vdom$ = xs + .combine( + signatureForm.DOM, + filterForm.DOM, + binnedPlots.DOM, + headTable.DOM, + tailTable.DOM + // feedback$ + ) + .map( + ([ + form, + filter, + plots, + headTable, + tailTable, + // feedback + ]) => + div(".row .disease", { style: { margin: "0px 0px 0px 0px" } }, [ + form, + div(".col .s10 .offset-s1", pageStyle, [ + div(".row", [filter]), + div(".row", [plots]), + div(".row", []), + div(".col .s12", [headTable]), + div(".row", []), + div(".col .s12", [tailTable]), + div(".row", []), + ]), + ]) + ) + + return { + log: xs.merge( + logger(state$, "state$"), + binnedPlots.log, + filterForm.log, + signatureForm.log + ), + DOM: vdom$, + onion: xs.merge( + defaultReducer$, + signatureForm.onion, + filterForm.onion, + binnedPlots.onion, + headTable.onion, + tailTable.onion, + scenarioReducer$ + ), + vega: xs.merge(binnedPlots.vega), + HTTP: xs.merge( + signatureForm.HTTP, + filterForm.HTTP, + binnedPlots.HTTP, + headTable.HTTP, + tailTable.HTTP + ), + popup: scenarioPopup$, + } } -export default DiseaseWorkflow; +export default DiseaseWorkflow diff --git a/src/js/pages/statistics.js b/src/js/pages/statistics.js index 7671189a..75d33e69 100644 --- a/src/js/pages/statistics.js +++ b/src/js/pages/statistics.js @@ -5,7 +5,7 @@ import { mergeWith, merge } from 'ramda' import { clone, equal, equals, mergeAll, prop, props } from 'ramda'; import dropRepeats from 'xstream/extra/dropRepeats' -import { initSettings } from './settings' +import { initSettings } from '../configuration.js' // Components import { Statistics } from '../components/Statistics' diff --git a/src/js/pages/target.js b/src/js/pages/target.js index 2146e8c8..62f3f94d 100644 --- a/src/js/pages/target.js +++ b/src/js/pages/target.js @@ -14,13 +14,13 @@ import { CompoundTable, compoundTableLens } from '../components/CompoundTable/Co import { Histogram, histLens } from '../components/Histogram/Histogram' import { pick, mix } from 'cycle-onionify'; -import { initSettings } from './settings' +import { initSettings } from '../configuration.js' import debounce from 'xstream/extra/debounce' import dropRepeats from 'xstream/extra/dropRepeats' import { loggerFactory } from '../utils/logger' import isolate from '@cycle/isolate' import { SignatureForm, formLens } from '../components/SignatureForm' -import { Filter, compoundFilterLens } from '../components/Filter' +import { Filter, filterLens } from '../components/Filter' // Support for ghost mode import { scenario } from '../scenarios/targetScenario' @@ -128,12 +128,12 @@ function TargetWorkflow(sources) { const signature$ = signatureForm.output // Filter Form - const filterForm = isolate(Filter, { onion: compoundFilterLens })({...sources, input: signature$ }) + const filterForm = isolate(Filter, { onion: filterLens })({...sources, input: signature$ }) const filter$ = filterForm.output // Histogram plot component const histogram = isolate(Histogram, { onion: histLens }) - ({...sources, input: xs.combine(signature$, filter$, target$.debug()).map(([s, f, t]) => ({ signature: s, filter: f, target: t })).remember() }); + ({...sources, input: xs.combine(signature$, filter$, target$).map(([s, f, t]) => ({ signature: s, filter: f, target: t })).remember() }); const pageStyle = { style: { @@ -197,7 +197,8 @@ function TargetWorkflow(sources) { scenarioReducer$ ), HTTP: xs.merge( - TargetFormSink.HTTP, + TargetFormSink.HTTP, + filterForm.HTTP, Table.HTTP, signatureForm.HTTP, histogram.HTTP From 9252c92ecdbda4409d3ef40ab7403b37b3f33f34 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Wed, 15 Sep 2021 16:36:36 +0200 Subject: [PATCH 013/191] Update index --- index.html | 3 +++ package.json | 12 ++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index acb861e7..eafbff84 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,9 @@ + + + diff --git a/package.json b/package.json index 85705f04..e5521b50 100644 --- a/package.json +++ b/package.json @@ -19,23 +19,25 @@ "build": "webpack", "pack": "build --dir", "dist": "build", + "test": "mocha src/test", "startelectron": "npm install && electron ." }, "author": "Toni Verbeiren", "license": "Apache v2", "devDependencies": { + "@babel/cli": "^7.15.4", + "@babel/core": "^7.15.5", + "@babel/preset-env": "^7.15.4", + "@babel/register": "^7.15.3", "ajv": "^6.10.2", "autoprefixer": "^6.7.2", - "babel-core": "^6.26.3", - "babel-loader": "^7.1.5", - "babel-preset-es2015": "^6.24.1", - "babel-preset-stage-0": "^6.24.1", "css-loader": "^0.26.4", "fibers": "^4.0.2", "file-loader": "^5.0.2", "jquery": "^3.3.1", "materialize-loader": "^3.0.1", "mini-css-extract-plugin": "^0.8.0", + "mocha": "^9.1.1", "sass": "^1.35.1", "sass-loader": "^11.0.1", "style-loader": "^1.0.1", @@ -52,6 +54,7 @@ "@cycle/isolate": "^3.4.0", "@cycle/run": "^4.4.0", "@cycle/storage": "^4.1.1", + "babel-loader": "^8.2.2", "cycle-onionify": "^3.3.0", "cycle-storageify": "^3.2.0", "cyclic-router": "^4.0.7", @@ -60,6 +63,7 @@ "node-gyp": "^6.0.1", "postcss-loader": "^1.3.3", "ramda": "^0.26.1", + "regenerator-runtime": "^0.13.9", "scss-loader": "0.0.1", "switch-path": "^1.2.0", "vega": "^5.9.0", From 23b1ee268bfbbaaeecc59dbf2d75bd09b39b2d1e Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Wed, 15 Sep 2021 19:33:22 +0200 Subject: [PATCH 014/191] Bump version to 5.0.0-alpha3 --- .babelrc | 3 --- babel.config.json | 3 +++ package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 .babelrc create mode 100644 babel.config.json diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 4897f9de..00000000 --- a/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["stage-0", "es2015"] -} diff --git a/babel.config.json b/babel.config.json new file mode 100644 index 00000000..1320b9a3 --- /dev/null +++ b/babel.config.json @@ -0,0 +1,3 @@ +{ + "presets": ["@babel/preset-env"] +} diff --git a/package.json b/package.json index e5521b50..cf5e628d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "LuciusWeb", - "version": "5.0.0-alpha2", + "version": "5.0.0-alpha3", "description": "Web interface for ComPass aka Lucius", "repository": { "type": "git", From 891611b53d2e8139b88fabaf5143a738b6ad0c9f Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Thu, 16 Sep 2021 00:04:16 +0200 Subject: [PATCH 015/191] Bump versions of cycle components --- package.json | 13 ++++++----- src/js/index.js | 49 +++++++++++++++++++---------------------- src/js/main.js | 8 ++++--- src/js/pages/disease.js | 2 ++ 4 files changed, 37 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index cf5e628d..7210c295 100644 --- a/package.json +++ b/package.json @@ -48,16 +48,16 @@ "webpack-material-design-icons": "^0.1.0" }, "dependencies": { - "@cycle/dom": "^20.4.0", - "@cycle/history": "^6.10.0", - "@cycle/http": "^14.9.0", - "@cycle/isolate": "^3.4.0", - "@cycle/run": "^4.4.0", + "@cycle/dom": "^23.0.0", + "@cycle/history": "^8.0.0", + "@cycle/http": "^15.4.0", + "@cycle/isolate": "^5.2.0", + "@cycle/run": "^5.5.0", "@cycle/storage": "^4.1.1", "babel-loader": "^8.2.2", "cycle-onionify": "^3.3.0", "cycle-storageify": "^3.2.0", - "cyclic-router": "^4.0.7", + "cyclic-router": "^6.0.0", "datalib": "^1.9.1", "materialize-css": "^1.0.0", "node-gyp": "^6.0.1", @@ -66,6 +66,7 @@ "regenerator-runtime": "^0.13.9", "scss-loader": "0.0.1", "switch-path": "^1.2.0", + "switch-path-ruta3": "^0.1.5", "vega": "^5.9.0", "vega-cli": "^5.9.0", "xstream": "^11.11.0" diff --git a/src/js/index.js b/src/js/index.js index 1345643c..8b8e8763 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -23,32 +23,27 @@ import initDeployments from '../../deployments.json' import { loggerFactory } from './utils/logger' export default function Index(sources) { - const { router } = sources; - - const logger = loggerFactory('index', sources.onion.state$, 'settings.common.debug') - - const state$ = sources.onion.state$ - - const match$ = router.define({ - '/': Home, - '/disease': DiseaseWorkflow, - '/compound': CompoundWorkflow, - '/target': TargetWorkflow, - '/statistics': StatisticsWorkflow, - '/settings': IsolatedSettings, - '/correlation': CorrelationWorkflow, - '/debug': Debug, - '/admin': IsolatedAdminSettings, - '*': Home - }) - .remember(); - - const page$ = match$.map(({ path, value }) => { - return value(Object.assign({}, sources, { - router: sources.router.path(path) - })) - }) - .remember() + const {router} = sources; + + const logger = loggerFactory('index', sources.onion.state$, 'settings.common.debug') + + const state$ = sources.onion.state$ + + const page$ = router.routedComponent({ + '/': Home, + '/disease': { + '/': DiseaseWorkflow, + // '/:id': id => sources => DiseaseWorkflow({props$: id, ...sources}) + }, + '/compound': CompoundWorkflow, + '/target': TargetWorkflow, + '/statistics': StatisticsWorkflow, + '/settings': IsolatedSettings, + '/correlation': CorrelationWorkflow, + '/debug': Debug, + '/admin': IsolatedAdminSettings, + '*': Home + })(sources) const makeLink = (path, label, options) => li([a(options, { props: { href: path } }, label)]); @@ -200,6 +195,8 @@ export default function Index(sources) { const history$ = sources.onion.state$.fold((acc, x) => acc.concat([x]), [{}]) + + return { log: xs.merge( // logger(page$, 'page$', '>> ', ' > ', ''), diff --git a/src/js/main.js b/src/js/main.js index c0242486..f993c1c8 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -7,7 +7,8 @@ import { makeDOMDriver } from '@cycle/dom'; import { makeHTTPDriver } from '@cycle/http'; import { makeHistoryDriver, makeServerHistoryDriver, makeHashHistoryDriver, captureClicks } from '@cycle/history' import storageDriver from '@cycle/storage'; -import { makeRouterDriver } from 'cyclic-router'; +// import { makeRouterDriver } from 'cyclic-router'; +import {routerify} from 'cyclic-router' import onionify from 'cycle-onionify'; import storageify from "cycle-storageify"; import delay from 'xstream/extra/delay' @@ -33,7 +34,8 @@ const drivers = { DOM: makeDOMDriver('#root'), vega: makeVegaDriver(), HTTP: makeHTTPDriver(), - router: makeRouterDriver(captureClicks(makeHistoryDriver()), switchPath), + // router: makeRouterDriver(captureClicks(makeHistoryDriver()), switchPath), + history: makeHistoryDriver(), preventDefault: preventDefaultDriver, alert: alertDriver, storage: storageDriver, @@ -45,5 +47,5 @@ const drivers = { deployments: () => xs.fromPromise(fetch('/deployments.json').then(m => m.json())) }; -let StatifiedMain = onionify(storageify(Index, { key: 'ComPass' })); +let StatifiedMain = onionify(storageify(routerify(Index, switchPath), { key: 'ComPass' })); run(StatifiedMain, drivers); diff --git a/src/js/pages/disease.js b/src/js/pages/disease.js index 28d3d299..ce6b4e0c 100644 --- a/src/js/pages/disease.js +++ b/src/js/pages/disease.js @@ -27,6 +27,8 @@ function DiseaseWorkflow(sources) { const state$ = sources.onion.state$ + console.log(sources) + // Scenario for ghost mode const scenarioReducer$ = sources.onion.state$ .take(1) From 66ef2b643b10261363b93c141daea925c94c1fd7 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Thu, 16 Sep 2021 09:42:47 +0200 Subject: [PATCH 016/191] Update some packages --- package.json | 2 +- src/js/pages/disease.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/package.json b/package.json index 7210c295..fa126bad 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "materialize-css": "^1.0.0", "node-gyp": "^6.0.1", "postcss-loader": "^1.3.3", - "ramda": "^0.26.1", + "ramda": "^0.27.1", "regenerator-runtime": "^0.13.9", "scss-loader": "0.0.1", "switch-path": "^1.2.0", diff --git a/src/js/pages/disease.js b/src/js/pages/disease.js index ce6b4e0c..28d3d299 100644 --- a/src/js/pages/disease.js +++ b/src/js/pages/disease.js @@ -27,8 +27,6 @@ function DiseaseWorkflow(sources) { const state$ = sources.onion.state$ - console.log(sources) - // Scenario for ghost mode const scenarioReducer$ = sources.onion.state$ .take(1) From b249abd6276a6251d653047ac6b2d1054a573cf5 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Thu, 16 Sep 2021 10:19:55 +0200 Subject: [PATCH 017/191] Correct routing --- src/js/index.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/js/index.js b/src/js/index.js index 8b8e8763..26e80fe1 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -31,10 +31,11 @@ export default function Index(sources) { const page$ = router.routedComponent({ '/': Home, - '/disease': { - '/': DiseaseWorkflow, - // '/:id': id => sources => DiseaseWorkflow({props$: id, ...sources}) - }, + '/disease' : DiseaseWorkflow, + // '/disease': { + // '/': DiseaseWorkflow, + // // '/:id': id => sources => DiseaseWorkflow({props$: id, ...sources}) + // }, '/compound': CompoundWorkflow, '/target': TargetWorkflow, '/statistics': StatisticsWorkflow, From 1c81984b2585613d0c60a8ca6643c52068019178 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Thu, 16 Sep 2021 20:09:53 +0200 Subject: [PATCH 018/191] More package cleanup and updates --- package.json | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index fa126bad..df62929b 100644 --- a/package.json +++ b/package.json @@ -29,10 +29,7 @@ "@babel/core": "^7.15.5", "@babel/preset-env": "^7.15.4", "@babel/register": "^7.15.3", - "ajv": "^6.10.2", - "autoprefixer": "^6.7.2", "css-loader": "^0.26.4", - "fibers": "^4.0.2", "file-loader": "^5.0.2", "jquery": "^3.3.1", "materialize-loader": "^3.0.1", @@ -60,15 +57,9 @@ "cyclic-router": "^6.0.0", "datalib": "^1.9.1", "materialize-css": "^1.0.0", - "node-gyp": "^6.0.1", - "postcss-loader": "^1.3.3", "ramda": "^0.27.1", - "regenerator-runtime": "^0.13.9", - "scss-loader": "0.0.1", "switch-path": "^1.2.0", - "switch-path-ruta3": "^0.1.5", - "vega": "^5.9.0", - "vega-cli": "^5.9.0", - "xstream": "^11.11.0" + "vega": "^5.20.2", + "xstream": "^11.14.0" } } From a662683d453fe38763276287296987287f09959d Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Fri, 17 Sep 2021 14:25:00 +0200 Subject: [PATCH 019/191] New default for compound wf --- deployments.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployments.json b/deployments.json index 34c17e9c..fd6fbdd9 100644 --- a/deployments.json +++ b/deployments.json @@ -20,7 +20,7 @@ }, "common": { "hourglass": { - "compound" : "BRD-A17655518", + "compound" : "BRD-K93645900", "signature" : "-WRONG HSPA1A DNAJB1 DDIT4 -TSEN2", "target" : "MELK" }, From 02271ece230a7543261d68498de048d34380ea24 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Fri, 17 Sep 2021 14:25:33 +0200 Subject: [PATCH 020/191] vega-parser added again --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index df62929b..c4eaeff8 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "ramda": "^0.27.1", "switch-path": "^1.2.0", "vega": "^5.20.2", + "vega-parser": "^6.1.3", "xstream": "^11.14.0" } } From 292df964c375b843e326b06ab0312c6b2c13a23e Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Fri, 17 Sep 2021 14:28:09 +0200 Subject: [PATCH 021/191] Support for trt_lig and ctl_vector --- src/js/components/SampleTable/SampleInfo.js | 88 +++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/js/components/SampleTable/SampleInfo.js b/src/js/components/SampleTable/SampleInfo.js index 818a6cf8..17380eff 100644 --- a/src/js/components/SampleTable/SampleInfo.js +++ b/src/js/components/SampleTable/SampleInfo.js @@ -69,6 +69,34 @@ export function SampleInfo(sources) { '' ]) ]), + trt_lig: + div('.row', {style: {fontWeight: 'small'}}, [ + div('.col .s1 .left-align', {style: {fontWeight: 'bold'}}, [zhangRounded]), + div('.col .s2', {style: {overflow: 'hidden', 'text-overflow': 'ellipsis'}}, [sample.id]), + div('.col .s1', [sample.cell]), + div('.col .s2', {style: blur}, [(sample.trt_id != "NA") ? sample.trt_id : '']), + div('.col .s3', {style: blur}, [sample.trt_name]), + div('.col .s1', {style: blur}, [sample.trt]), + div('.col .s2 .center-align', {style: blur}, [ + ((sample.trt_name != null && sample.trt_name != 'N/A') && zoom == false) ? + span({ style: { color: 'black', opacity: 0.4, "font-size": "clamp(16px, 5vw, 26px)", height: 50, display: "block", "font-family": 'Nova Mono', 'object-fit': 'contain', fontWeight: "bold" } }, [sample.trt_name]): + '' + ]) + ]), + ctl_vector: + div('.row', {style: {fontWeight: 'small'}}, [ + div('.col .s1 .left-align', {style: {fontWeight: 'bold'}}, [zhangRounded]), + div('.col .s2', {style: {overflow: 'hidden', 'text-overflow': 'ellipsis'}}, [sample.id]), + div('.col .s1', [sample.cell]), + div('.col .s2', {style: blur}, [(sample.trt_id != "NA") ? sample.trt_id : '']), + div('.col .s3', {style: blur}, [sample.trt_name]), + div('.col .s1', {style: blur}, [sample.trt]), + div('.col .s2 .center-align', {style: blur}, [ + ((sample.trt_name != null && sample.trt_name != 'N/A') && zoom == false) ? + span({ style: { color: 'black', opacity: 0.4, "font-size": "clamp(16px, 5vw, 26px)", height: 50, display: "block", "font-family": 'Nova Mono', 'object-fit': 'contain', fontWeight: "bold" } }, [sample.trt_name]): + '' + ]) + ]), _default: div('.row', {style: {fontWeight: 'small'}}, [ div('.col .s1 .left-align', {style: {fontWeight: 'bold'}}, [zhangRounded]), @@ -142,6 +170,66 @@ export function SampleInfo(sources) { .concat(_filters.map( x => p(pStyle, entrySmall(x.key, x.value)) )) ) ]), + trt_lig: + div([ + div('.row', [ + div('.col .s4 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ + p('.col .s12 .grey-text', hStyle, 'Sample Info:'), + p(pStyle, entry('Sample ID: ', sample.id)), + p(pStyle, entry('Cell: ', sample.cell)), + p(pStyle, entry('Dose: ', sample.dose)), + p(pStyle, entry('Time: ', sample.time)), + p(pStyle, entry('Year: ', sample.year)), + p(pStyle, entry('Plate: ', sample.plate)), + ]), + div('.col .s4 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ + p('.col .s12 .grey-text', hStyle, 'Treatment Info:'), + p(pStylewBlur, entry('Name: ', sample.trt_name)), + p(pStylewBlur, entry(safeModelToUi('id', props.common.modelTranslations) + ": ", sample.trt_id)), + p(pStyle, entry('Type: ', sample.trt)), + p('.s12', entry('Targets: ', sample.targets.join(', '))), + ]), + div('.col .s4 .l4', { style: merge(blur, { height: '100%', margin: '30px 0px 0px 0px' }) }, [ + ((sample.trt_name != null && sample.trt_name != 'N/A')) + ? div('.col .s12', {style: {color: 'black', opacity: 0.4, "font-size": "clamp(16px, 5vw, 50px)", "font-family": 'Nova Mono', 'object-fit': 'contain', fontWeight: "bold"}}, [sample.trt_name]) + : div() + ]) + ]), + div('.row', { style: { margin: '15px 0px 0px 0px' } }, + [p('.col .s12.grey-text', hStyle, 'Filter Info:')] + .concat(_filters.map( x => p(pStyle, entrySmall(x.key, x.value)) )) + ) + ]), + ctl_vector: + div([ + div('.row', [ + div('.col .s4 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ + p('.col .s12 .grey-text', hStyle, 'Sample Info:'), + p(pStyle, entry('Sample ID: ', sample.id)), + p(pStyle, entry('Cell: ', sample.cell)), + p(pStyle, entry('Dose: ', sample.dose)), + p(pStyle, entry('Time: ', sample.time)), + p(pStyle, entry('Year: ', sample.year)), + p(pStyle, entry('Plate: ', sample.plate)), + ]), + div('.col .s4 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ + p('.col .s12 .grey-text', hStyle, 'Treatment Info:'), + p(pStylewBlur, entry('Name: ', sample.trt_name)), + p(pStylewBlur, entry(safeModelToUi('id', props.common.modelTranslations) + ": ", sample.trt_id)), + p(pStyle, entry('Type: ', sample.trt)), + p('.s12', entry('Targets: ', sample.targets.join(', '))), + ]), + div('.col .s4 .l4', { style: merge(blur, { height: '100%', margin: '30px 0px 0px 0px' }) }, [ + ((sample.trt_name != null && sample.trt_name != 'N/A')) + ? div('.col .s12', {style: {color: 'black', opacity: 0.4, "font-size": "clamp(16px, 5vw, 50px)", "font-family": 'Nova Mono', 'object-fit': 'contain', fontWeight: "bold"}}, [sample.trt_name]) + : div() + ]) + ]), + div('.row', { style: { margin: '15px 0px 0px 0px' } }, + [p('.col .s12.grey-text', hStyle, 'Filter Info:')] + .concat(_filters.map( x => p(pStyle, entrySmall(x.key, x.value)) )) + ) + ]), _default: div('.row', {style: {fontWeight: 'small'}}, [ div('.col .s12', [ From cb07dfe7aaffcfccbac57cb21990b407f4a7f1e0 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Fri, 17 Sep 2021 14:32:56 +0200 Subject: [PATCH 022/191] Prettify SampleInfo --- src/js/components/SampleTable/SampleInfo.js | 690 +++++++++++++------- 1 file changed, 461 insertions(+), 229 deletions(-) diff --git a/src/js/components/SampleTable/SampleInfo.js b/src/js/components/SampleTable/SampleInfo.js index 17380eff..e1b8fdee 100644 --- a/src/js/components/SampleTable/SampleInfo.js +++ b/src/js/components/SampleTable/SampleInfo.js @@ -1,286 +1,518 @@ -import xs from 'xstream' -import { i, a, h, p, div, br, label, input, pre, code, table, tr, td, b, h2, button, svg, h1, th, thead, tbody, li, span, img, em } from '@cycle/dom' -import { merge } from 'ramda' -import { safeModelToUi } from '../../modelTranslations' +import xs from "xstream" +import { + p, + div, + li, + span, + img, +} from "@cycle/dom" +import { merge } from "ramda" +import { safeModelToUi } from "../../modelTranslations" export function SampleInfo(sources) { - const state$ = sources.onion.state$ const props$ = sources.props - const click$ = sources.DOM.select('.zoom').events('click').mapTo(1) + const click$ = sources.DOM.select(".zoom").events("click").mapTo(1) const zoomed$ = click$ .fold((x, y) => x + y, 0) - .map(count => (count % 2 == 0) ? false : true) + .map((count) => (count % 2 == 0 ? false : true)) function entry(key, value) { return [ - span('.col .s4 .grey-text.text-darken-1', { style: { fontWeight: 'lighter' } }, key), - span('.col .s8', { style : { overflow: 'hidden', 'text-overflow': 'ellipsis' }}, (value.length != 0) ? value : '') + span( + ".col .s4 .grey-text.text-darken-1", + { style: { fontWeight: "lighter" } }, + key + ), + span( + ".col .s8", + { style: { overflow: "hidden", "text-overflow": "ellipsis" } }, + value.length != 0 ? value : "" + ), ] } function entrySmall(key, value) { return [ - span('.col .s6 .l2', { style: { fontWeight: 'lighter' } }, key), - span('.col .s6 .l2', (value.length != 0) ? value : '') + span(".col .s6 .l2", { style: { fontWeight: "lighter" } }, key), + span(".col .s6 .l2", value.length != 0 ? value : ""), ] } const blur$ = props$ - .filter(props => props.common.blur != undefined) - .filter(props => props.common.blur) - .map(props => ({ filter: 'blur(' + props.common.amountBlur + 'px)' })) - .startWith({ filter: 'blur(0px)' }) + .filter((props) => props.common.blur != undefined) + .filter((props) => props.common.blur) + .map((props) => ({ filter: "blur(" + props.common.amountBlur + "px)" })) + .startWith({ filter: "blur(0px)" }) function sourireUrl(base, smiles) { - let url = base + encodeURIComponent(smiles).replace(/%20/g, '+') + let url = base + encodeURIComponent(smiles).replace(/%20/g, "+") return url } const row = (sample, props, blur, zoom) => { - let zhangRounded = (sample.zhang != null) ? parseFloat(sample.zhang).toFixed(3) : 'NA' + let zhangRounded = + sample.zhang != null ? parseFloat(sample.zhang).toFixed(3) : "NA" return { - trt_cp: - div('.row', {style: {fontWeight: 'small'}}, [ - div('.col .s1 .left-align', {style: {fontWeight: 'bold'}}, [zhangRounded]), - div('.col .s2', {style: {overflow: 'hidden', 'text-overflow': 'ellipsis'}}, [sample.id]), - div('.col .s1', [sample.cell]), - div('.col .s2', {style: blur}, [(sample.trt_id != "NA") ? sample.trt_id : '']), - div('.col .s3', {style: blur}, [sample.trt_name]), - div('.col .s1', {style: blur}, [sample.trt]), - div('.col .s2 .center-align', {style: blur}, [ - ((sample.smiles != null && sample.smiles != 'N/A' && sample.smiles != 'No Smiles') && zoom == false) ? - img({props: {src: sourireUrl(props.sourire.url, sample.smiles), height: 50, 'object-fit': 'contain'}}) : - '' - ]) + trt_cp: div(".row", { style: { fontWeight: "small" } }, [ + div(".col .s1 .left-align", { style: { fontWeight: "bold" } }, [ + zhangRounded, + ]), + div( + ".col .s2", + { style: { overflow: "hidden", "text-overflow": "ellipsis" } }, + [sample.id] + ), + div(".col .s1", [sample.cell]), + div(".col .s2", { style: blur }, [ + sample.trt_id != "NA" ? sample.trt_id : "", + ]), + div(".col .s3", { style: blur }, [sample.trt_name]), + div(".col .s1", { style: blur }, [sample.trt]), + div(".col .s2 .center-align", { style: blur }, [ + sample.smiles != null && + sample.smiles != "N/A" && + sample.smiles != "No Smiles" && + zoom == false + ? img({ + props: { + src: sourireUrl(props.sourire.url, sample.smiles), + height: 50, + "object-fit": "contain", + }, + }) + : "", + ]), + ]), + trt_sh: div(".row", { style: { fontWeight: "small" } }, [ + div(".col .s1 .left-align", { style: { fontWeight: "bold" } }, [ + zhangRounded, + ]), + div( + ".col .s2", + { style: { overflow: "hidden", "text-overflow": "ellipsis" } }, + [sample.id] + ), + div(".col .s1", [sample.cell]), + div(".col .s2", { style: blur }, [ + sample.trt_id != "NA" ? sample.trt_id : "", + ]), + div(".col .s3", { style: blur }, [sample.trt_name]), + div(".col .s1", { style: blur }, [sample.trt]), + div(".col .s2 .center-align", { style: blur }, [ + sample.trt_name != null && sample.trt_name != "N/A" && zoom == false + ? span( + { + style: { + color: "black", + opacity: 0.4, + "font-size": "clamp(16px, 5vw, 26px)", + height: 50, + display: "block", + "font-family": "Nova Mono", + "object-fit": "contain", + fontWeight: "bold", + }, + }, + [sample.trt_name] + ) + : "", + ]), + ]), + trt_lig: div(".row", { style: { fontWeight: "small" } }, [ + div(".col .s1 .left-align", { style: { fontWeight: "bold" } }, [ + zhangRounded, + ]), + div( + ".col .s2", + { style: { overflow: "hidden", "text-overflow": "ellipsis" } }, + [sample.id] + ), + div(".col .s1", [sample.cell]), + div(".col .s2", { style: blur }, [ + sample.trt_id != "NA" ? sample.trt_id : "", ]), - trt_sh: - div('.row', {style: {fontWeight: 'small'}}, [ - div('.col .s1 .left-align', {style: {fontWeight: 'bold'}}, [zhangRounded]), - div('.col .s2', {style: {overflow: 'hidden', 'text-overflow': 'ellipsis'}}, [sample.id]), - div('.col .s1', [sample.cell]), - div('.col .s2', {style: blur}, [(sample.trt_id != "NA") ? sample.trt_id : '']), - div('.col .s3', {style: blur}, [sample.trt_name]), - div('.col .s1', {style: blur}, [sample.trt]), - div('.col .s2 .center-align', {style: blur}, [ - ((sample.trt_name != null && sample.trt_name != 'N/A') && zoom == false) ? - span({ style: { color: 'black', opacity: 0.4, "font-size": "clamp(16px, 5vw, 26px)", height: 50, display: "block", "font-family": 'Nova Mono', 'object-fit': 'contain', fontWeight: "bold" } }, [sample.trt_name]): - '' - ]) + div(".col .s3", { style: blur }, [sample.trt_name]), + div(".col .s1", { style: blur }, [sample.trt]), + div(".col .s2 .center-align", { style: blur }, [ + sample.trt_name != null && sample.trt_name != "N/A" && zoom == false + ? span( + { + style: { + color: "black", + opacity: 0.4, + "font-size": "clamp(16px, 5vw, 26px)", + height: 50, + display: "block", + "font-family": "Nova Mono", + "object-fit": "contain", + fontWeight: "bold", + }, + }, + [sample.trt_name] + ) + : "", ]), - trt_lig: - div('.row', {style: {fontWeight: 'small'}}, [ - div('.col .s1 .left-align', {style: {fontWeight: 'bold'}}, [zhangRounded]), - div('.col .s2', {style: {overflow: 'hidden', 'text-overflow': 'ellipsis'}}, [sample.id]), - div('.col .s1', [sample.cell]), - div('.col .s2', {style: blur}, [(sample.trt_id != "NA") ? sample.trt_id : '']), - div('.col .s3', {style: blur}, [sample.trt_name]), - div('.col .s1', {style: blur}, [sample.trt]), - div('.col .s2 .center-align', {style: blur}, [ - ((sample.trt_name != null && sample.trt_name != 'N/A') && zoom == false) ? - span({ style: { color: 'black', opacity: 0.4, "font-size": "clamp(16px, 5vw, 26px)", height: 50, display: "block", "font-family": 'Nova Mono', 'object-fit': 'contain', fontWeight: "bold" } }, [sample.trt_name]): - '' - ]) + ]), + ctl_vector: div(".row", { style: { fontWeight: "small" } }, [ + div(".col .s1 .left-align", { style: { fontWeight: "bold" } }, [ + zhangRounded, ]), - ctl_vector: - div('.row', {style: {fontWeight: 'small'}}, [ - div('.col .s1 .left-align', {style: {fontWeight: 'bold'}}, [zhangRounded]), - div('.col .s2', {style: {overflow: 'hidden', 'text-overflow': 'ellipsis'}}, [sample.id]), - div('.col .s1', [sample.cell]), - div('.col .s2', {style: blur}, [(sample.trt_id != "NA") ? sample.trt_id : '']), - div('.col .s3', {style: blur}, [sample.trt_name]), - div('.col .s1', {style: blur}, [sample.trt]), - div('.col .s2 .center-align', {style: blur}, [ - ((sample.trt_name != null && sample.trt_name != 'N/A') && zoom == false) ? - span({ style: { color: 'black', opacity: 0.4, "font-size": "clamp(16px, 5vw, 26px)", height: 50, display: "block", "font-family": 'Nova Mono', 'object-fit': 'contain', fontWeight: "bold" } }, [sample.trt_name]): - '' - ]) + div( + ".col .s2", + { style: { overflow: "hidden", "text-overflow": "ellipsis" } }, + [sample.id] + ), + div(".col .s1", [sample.cell]), + div(".col .s2", { style: blur }, [ + sample.trt_id != "NA" ? sample.trt_id : "", ]), - _default: - div('.row', {style: {fontWeight: 'small'}}, [ - div('.col .s1 .left-align', {style: {fontWeight: 'bold'}}, [zhangRounded]), - div('.col .s2', {style: {overflow: 'hidden', 'text-overflow': 'ellipsis'}}, [sample.id]), - div('.col .s9', ["Treatment type not yet implemented"]), - ]) + div(".col .s3", { style: blur }, [sample.trt_name]), + div(".col .s1", { style: blur }, [sample.trt]), + div(".col .s2 .center-align", { style: blur }, [ + sample.trt_name != null && sample.trt_name != "N/A" && zoom == false + ? span( + { + style: { + color: "black", + opacity: 0.4, + "font-size": "clamp(16px, 5vw, 26px)", + height: 50, + display: "block", + "font-family": "Nova Mono", + "object-fit": "contain", + fontWeight: "bold", + }, + }, + [sample.trt_name] + ) + : "", + ]), + ]), + _default: div(".row", { style: { fontWeight: "small" } }, [ + div(".col .s1 .left-align", { style: { fontWeight: "bold" } }, [ + zhangRounded, + ]), + div( + ".col .s2", + { style: { overflow: "hidden", "text-overflow": "ellipsis" } }, + [sample.id] + ), + div(".col .s9", ["Treatment type not yet implemented"]), + ]), } } const rowDetail = (sample, props, blur) => { - let hStyle = { style: { margin: '0px', fontWeight: 'bold' } } - let pStyle = { style: { margin: '0px' } } - let pStylewBlur = { style: merge(blur, { margin: '0px' }) } - const _filters = (sample.filters != undefined) ? sample.filters : [] + let hStyle = { style: { margin: "0px", fontWeight: "bold" } } + let pStyle = { style: { margin: "0px" } } + let pStylewBlur = { style: merge(blur, { margin: "0px" }) } + const _filters = sample.filters != undefined ? sample.filters : [] return { - trt_cp: - div('.col .s12', [ - div('.col .s6 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ - p('.col .s12 .grey-text', hStyle, 'Sample Info:'), - p(pStyle, entry('Sample ID: ', sample.id)), - p(pStyle, entry('Cell: ', sample.cell)), - p(pStyle, entry('Dose: ', sample.dose)), - p(pStyle, entry('Time: ', sample.time)), - p(pStyle, entry('Year: ', sample.year)), - p(pStyle, entry('Plate: ', sample.plate)), - ]), - div('.col .s6 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ - p('.col .s12 .grey-text', hStyle, 'Treatment Info:'), - p(pStylewBlur, entry('Name: ', sample.trt_name)), - p(pStylewBlur, entry(safeModelToUi('id', props.common.modelTranslations) + ": ", sample.trt_id)), - p(pStyle, entry('Type: ', sample.trt)), - p('.s12', entry('Targets: ', sample.targets.join(', '))), + trt_cp: div(".col .s12", [ + div(".col .s6 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ + p(".col .s12 .grey-text", hStyle, "Sample Info:"), + p(pStyle, entry("Sample ID: ", sample.id)), + p(pStyle, entry("Cell: ", sample.cell)), + p(pStyle, entry("Dose: ", sample.dose)), + p(pStyle, entry("Time: ", sample.time)), + p(pStyle, entry("Year: ", sample.year)), + p(pStyle, entry("Plate: ", sample.plate)), + ]), + div(".col .s6 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ + p(".col .s12 .grey-text", hStyle, "Treatment Info:"), + p(pStylewBlur, entry("Name: ", sample.trt_name)), + p( + pStylewBlur, + entry( + safeModelToUi("id", props.common.modelTranslations) + ": ", + sample.trt_id + ) + ), + p(pStyle, entry("Type: ", sample.trt)), + p(".s12", entry("Targets: ", sample.targets.join(", "))), + ]), + div( + ".col .s12 .offset-s8 .l4", + { style: merge(blur, { margin: "20px 0px 0px 0px" }) }, + [ + sample.smiles != null && + sample.smiles != "N/A" && + sample.smiles != "No Smiles" + ? img(".col .s12 .valign", { + props: { src: sourireUrl(props.sourire.url, sample.smiles) }, + }) + : "", + ] + ), + div( + ".col .s12 .l12", + { style: { margin: "15px 0px 0px 0px" } }, + [p(".col .s12.grey-text", hStyle, "Filter Info:")].concat( + _filters.map((x) => p(pStyle, entrySmall(x.key, x.value))) + ) + ), + ]), + trt_sh: div([ + div(".row", [ + div(".col .s4 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ + p(".col .s12 .grey-text", hStyle, "Sample Info:"), + p(pStyle, entry("Sample ID: ", sample.id)), + p(pStyle, entry("Cell: ", sample.cell)), + p(pStyle, entry("Dose: ", sample.dose)), + p(pStyle, entry("Time: ", sample.time)), + p(pStyle, entry("Year: ", sample.year)), + p(pStyle, entry("Plate: ", sample.plate)), ]), - div('.col .s12 .offset-s8 .l4', { style: merge(blur, { margin: '20px 0px 0px 0px' }) }, [ - (sample.smiles != null && sample.smiles != 'N/A' && sample.smiles != 'No Smiles') ? - img('.col .s12 .valign', { props: { src: sourireUrl(props.sourire.url, sample.smiles) } }) : - '' + div(".col .s4 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ + p(".col .s12 .grey-text", hStyle, "Treatment Info:"), + p(pStylewBlur, entry("Name: ", sample.trt_name)), + p( + pStylewBlur, + entry( + safeModelToUi("id", props.common.modelTranslations) + ": ", + sample.trt_id + ) + ), + p(pStyle, entry("Type: ", sample.trt)), + p(".s12", entry("Targets: ", sample.targets.join(", "))), ]), - div('.col .s12 .l12', { style: { margin: '15px 0px 0px 0px' } }, - [p('.col .s12.grey-text', hStyle, 'Filter Info:')] - .concat(_filters.map( x => p(pStyle, entrySmall(x.key, x.value)) )) - ) + div( + ".col .s4 .l4", + { + style: merge(blur, { + height: "100%", + margin: "30px 0px 0px 0px", + }), + }, + [ + sample.trt_name != null && sample.trt_name != "N/A" + ? div( + ".col .s12", + { + style: { + color: "black", + opacity: 0.4, + "font-size": "clamp(16px, 5vw, 50px)", + "font-family": "Nova Mono", + "object-fit": "contain", + fontWeight: "bold", + }, + }, + [sample.trt_name] + ) + : div(), + ] + ), ]), - trt_sh: - div([ - div('.row', [ - div('.col .s4 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ - p('.col .s12 .grey-text', hStyle, 'Sample Info:'), - p(pStyle, entry('Sample ID: ', sample.id)), - p(pStyle, entry('Cell: ', sample.cell)), - p(pStyle, entry('Dose: ', sample.dose)), - p(pStyle, entry('Time: ', sample.time)), - p(pStyle, entry('Year: ', sample.year)), - p(pStyle, entry('Plate: ', sample.plate)), - ]), - div('.col .s4 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ - p('.col .s12 .grey-text', hStyle, 'Treatment Info:'), - p(pStylewBlur, entry('Name: ', sample.trt_name)), - p(pStylewBlur, entry(safeModelToUi('id', props.common.modelTranslations) + ": ", sample.trt_id)), - p(pStyle, entry('Type: ', sample.trt)), - p('.s12', entry('Targets: ', sample.targets.join(', '))), - ]), - div('.col .s4 .l4', { style: merge(blur, { height: '100%', margin: '30px 0px 0px 0px' }) }, [ - ((sample.trt_name != null && sample.trt_name != 'N/A')) - ? div('.col .s12', {style: {color: 'black', opacity: 0.4, "font-size": "clamp(16px, 5vw, 50px)", "font-family": 'Nova Mono', 'object-fit': 'contain', fontWeight: "bold"}}, [sample.trt_name]) - : div() - ]) - ]), - div('.row', { style: { margin: '15px 0px 0px 0px' } }, - [p('.col .s12.grey-text', hStyle, 'Filter Info:')] - .concat(_filters.map( x => p(pStyle, entrySmall(x.key, x.value)) )) + div( + ".row", + { style: { margin: "15px 0px 0px 0px" } }, + [p(".col .s12.grey-text", hStyle, "Filter Info:")].concat( + _filters.map((x) => p(pStyle, entrySmall(x.key, x.value))) ) - ]), - trt_lig: - div([ - div('.row', [ - div('.col .s4 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ - p('.col .s12 .grey-text', hStyle, 'Sample Info:'), - p(pStyle, entry('Sample ID: ', sample.id)), - p(pStyle, entry('Cell: ', sample.cell)), - p(pStyle, entry('Dose: ', sample.dose)), - p(pStyle, entry('Time: ', sample.time)), - p(pStyle, entry('Year: ', sample.year)), - p(pStyle, entry('Plate: ', sample.plate)), - ]), - div('.col .s4 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ - p('.col .s12 .grey-text', hStyle, 'Treatment Info:'), - p(pStylewBlur, entry('Name: ', sample.trt_name)), - p(pStylewBlur, entry(safeModelToUi('id', props.common.modelTranslations) + ": ", sample.trt_id)), - p(pStyle, entry('Type: ', sample.trt)), - p('.s12', entry('Targets: ', sample.targets.join(', '))), - ]), - div('.col .s4 .l4', { style: merge(blur, { height: '100%', margin: '30px 0px 0px 0px' }) }, [ - ((sample.trt_name != null && sample.trt_name != 'N/A')) - ? div('.col .s12', {style: {color: 'black', opacity: 0.4, "font-size": "clamp(16px, 5vw, 50px)", "font-family": 'Nova Mono', 'object-fit': 'contain', fontWeight: "bold"}}, [sample.trt_name]) - : div() - ]) + ), + ]), + trt_lig: div([ + div(".row", [ + div(".col .s4 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ + p(".col .s12 .grey-text", hStyle, "Sample Info:"), + p(pStyle, entry("Sample ID: ", sample.id)), + p(pStyle, entry("Cell: ", sample.cell)), + p(pStyle, entry("Dose: ", sample.dose)), + p(pStyle, entry("Time: ", sample.time)), + p(pStyle, entry("Year: ", sample.year)), + p(pStyle, entry("Plate: ", sample.plate)), ]), - div('.row', { style: { margin: '15px 0px 0px 0px' } }, - [p('.col .s12.grey-text', hStyle, 'Filter Info:')] - .concat(_filters.map( x => p(pStyle, entrySmall(x.key, x.value)) )) - ) - ]), - ctl_vector: - div([ - div('.row', [ - div('.col .s4 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ - p('.col .s12 .grey-text', hStyle, 'Sample Info:'), - p(pStyle, entry('Sample ID: ', sample.id)), - p(pStyle, entry('Cell: ', sample.cell)), - p(pStyle, entry('Dose: ', sample.dose)), - p(pStyle, entry('Time: ', sample.time)), - p(pStyle, entry('Year: ', sample.year)), - p(pStyle, entry('Plate: ', sample.plate)), - ]), - div('.col .s4 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ - p('.col .s12 .grey-text', hStyle, 'Treatment Info:'), - p(pStylewBlur, entry('Name: ', sample.trt_name)), - p(pStylewBlur, entry(safeModelToUi('id', props.common.modelTranslations) + ": ", sample.trt_id)), - p(pStyle, entry('Type: ', sample.trt)), - p('.s12', entry('Targets: ', sample.targets.join(', '))), - ]), - div('.col .s4 .l4', { style: merge(blur, { height: '100%', margin: '30px 0px 0px 0px' }) }, [ - ((sample.trt_name != null && sample.trt_name != 'N/A')) - ? div('.col .s12', {style: {color: 'black', opacity: 0.4, "font-size": "clamp(16px, 5vw, 50px)", "font-family": 'Nova Mono', 'object-fit': 'contain', fontWeight: "bold"}}, [sample.trt_name]) - : div() - ]) + div(".col .s4 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ + p(".col .s12 .grey-text", hStyle, "Treatment Info:"), + p(pStylewBlur, entry("Name: ", sample.trt_name)), + p( + pStylewBlur, + entry( + safeModelToUi("id", props.common.modelTranslations) + ": ", + sample.trt_id + ) + ), + p(pStyle, entry("Type: ", sample.trt)), + p(".s12", entry("Targets: ", sample.targets.join(", "))), ]), - div('.row', { style: { margin: '15px 0px 0px 0px' } }, - [p('.col .s12.grey-text', hStyle, 'Filter Info:')] - .concat(_filters.map( x => p(pStyle, entrySmall(x.key, x.value)) )) - ) + div( + ".col .s4 .l4", + { + style: merge(blur, { + height: "100%", + margin: "30px 0px 0px 0px", + }), + }, + [ + sample.trt_name != null && sample.trt_name != "N/A" + ? div( + ".col .s12", + { + style: { + color: "black", + opacity: 0.4, + "font-size": "clamp(16px, 5vw, 50px)", + "font-family": "Nova Mono", + "object-fit": "contain", + fontWeight: "bold", + }, + }, + [sample.trt_name] + ) + : div(), + ] + ), ]), - _default: - div('.row', {style: {fontWeight: 'small'}}, [ - div('.col .s12', [ - div('.col .s6 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ - p('.col .s12 .grey-text', hStyle, 'Sample Info:'), - p(pStyle, entry('Sample ID: ', sample.id)), - p(pStyle, entry('Cell: ', sample.cell)), - p(pStyle, entry('Dose: ', sample.dose)), - p(pStyle, entry('Time: ', sample.time)), - p(pStyle, entry('Year: ', sample.year)), - p(pStyle, entry('Plate: ', sample.plate)), - ]), - div('.col .s6 .l4', { style: { margin: '15px 0px 0px 0px' } }, [ - p('.col .s12 .grey-text', hStyle, 'Treatment Info:'), - p(pStylewBlur, entry('Name: ', sample.trt_name)), - p(pStylewBlur, entry(safeModelToUi('id', props.common.modelTranslations) + ": ", sample.trt_id)), - p(pStyle, entry('Type: ', sample.trt)), - p('.s12', entry('Targets: ', sample.targets.join(', '))), + div( + ".row", + { style: { margin: "15px 0px 0px 0px" } }, + [p(".col .s12.grey-text", hStyle, "Filter Info:")].concat( + _filters.map((x) => p(pStyle, entrySmall(x.key, x.value))) + ) + ), + ]), + ctl_vector: div([ + div(".row", [ + div(".col .s4 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ + p(".col .s12 .grey-text", hStyle, "Sample Info:"), + p(pStyle, entry("Sample ID: ", sample.id)), + p(pStyle, entry("Cell: ", sample.cell)), + p(pStyle, entry("Dose: ", sample.dose)), + p(pStyle, entry("Time: ", sample.time)), + p(pStyle, entry("Year: ", sample.year)), + p(pStyle, entry("Plate: ", sample.plate)), ]), - div('.col .s12 .offset-s8 .l4', { style: merge(blur, { margin: '20px 0px 0px 0px' }) }, [ - (sample.smiles != null && sample.smiles != 'N/A' && sample.smiles != 'No Smiles') ? - img('.col .s12 .valign', { props: { src: sourireUrl(props.sourire.url, sample.smiles) } }) : - '' + div(".col .s4 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ + p(".col .s12 .grey-text", hStyle, "Treatment Info:"), + p(pStylewBlur, entry("Name: ", sample.trt_name)), + p( + pStylewBlur, + entry( + safeModelToUi("id", props.common.modelTranslations) + ": ", + sample.trt_id + ) + ), + p(pStyle, entry("Type: ", sample.trt)), + p(".s12", entry("Targets: ", sample.targets.join(", "))), ]), - div('.col .s12 .l12', { style: { margin: '15px 0px 0px 0px' } }, - [p('.col .s12.grey-text', hStyle, 'Filter Info:')] - .concat(_filters.map( x => p(pStyle, entrySmall(x.key, x.value)) )) + div( + ".col .s4 .l4", + { + style: merge(blur, { + height: "100%", + margin: "30px 0px 0px 0px", + }), + }, + [ + sample.trt_name != null && sample.trt_name != "N/A" + ? div( + ".col .s12", + { + style: { + color: "black", + opacity: 0.4, + "font-size": "clamp(16px, 5vw, 50px)", + "font-family": "Nova Mono", + "object-fit": "contain", + fontWeight: "bold", + }, + }, + [sample.trt_name] + ) + : div(), + ] + ), + ]), + div( + ".row", + { style: { margin: "15px 0px 0px 0px" } }, + [p(".col .s12.grey-text", hStyle, "Filter Info:")].concat( + _filters.map((x) => p(pStyle, entrySmall(x.key, x.value))) ) - ]) - ]) - + ), + ]), + _default: div(".row", { style: { fontWeight: "small" } }, [ + div(".col .s12", [ + div(".col .s6 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ + p(".col .s12 .grey-text", hStyle, "Sample Info:"), + p(pStyle, entry("Sample ID: ", sample.id)), + p(pStyle, entry("Cell: ", sample.cell)), + p(pStyle, entry("Dose: ", sample.dose)), + p(pStyle, entry("Time: ", sample.time)), + p(pStyle, entry("Year: ", sample.year)), + p(pStyle, entry("Plate: ", sample.plate)), + ]), + div(".col .s6 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ + p(".col .s12 .grey-text", hStyle, "Treatment Info:"), + p(pStylewBlur, entry("Name: ", sample.trt_name)), + p( + pStylewBlur, + entry( + safeModelToUi("id", props.common.modelTranslations) + ": ", + sample.trt_id + ) + ), + p(pStyle, entry("Type: ", sample.trt)), + p(".s12", entry("Targets: ", sample.targets.join(", "))), + ]), + div( + ".col .s12 .offset-s8 .l4", + { style: merge(blur, { margin: "20px 0px 0px 0px" }) }, + [ + sample.smiles != null && + sample.smiles != "N/A" && + sample.smiles != "No Smiles" + ? img(".col .s12 .valign", { + props: { + src: sourireUrl(props.sourire.url, sample.smiles), + }, + }) + : "", + ] + ), + div( + ".col .s12 .l12", + { style: { margin: "15px 0px 0px 0px" } }, + [p(".col .s12.grey-text", hStyle, "Filter Info:")].concat( + _filters.map((x) => p(pStyle, entrySmall(x.key, x.value))) + ) + ), + ]), + ]), } } - const vdom$ = xs.combine(state$, zoomed$, props$, blur$) + const vdom$ = xs + .combine(state$, zoomed$, props$, blur$) .map(([sample, zoom, props, blur]) => { - let bgcolor = (sample.zhang >= 0) ? 'rgba(44,123,182, 0.08)' : 'rgba(215,25,28, 0.08)' - const updtProps = {...props, bgColor: bgcolor} + let bgcolor = + sample.zhang >= 0 ? "rgba(44,123,182, 0.08)" : "rgba(215,25,28, 0.08)" + const updtProps = { ...props, bgColor: bgcolor } const thisRow = row(sample, updtProps, blur, zoom) const thisRowDetail = rowDetail(sample, updtProps, blur) - return li('.collection-item .zoom', {style: {'background-color': bgcolor}}, [ - thisRow[sample.trt] ? thisRow[sample.trt] : thisRow["_default"], - (zoom) ? div('.row', [thisRowDetail[sample.trt] ? thisRowDetail[sample.trt] : thisRowDetail["_default"]]) : div() - ]) + return li( + ".collection-item .zoom", + { style: { "background-color": bgcolor } }, + [ + thisRow[sample.trt] ? thisRow[sample.trt] : thisRow["_default"], + zoom + ? div(".row", [ + thisRowDetail[sample.trt] + ? thisRowDetail[sample.trt] + : thisRowDetail["_default"], + ]) + : div(), + ] + ) }) - .startWith(li('.collection-itm .zoom', [p('Just one item!!!')])) + .startWith(li(".collection-itm .zoom", [p("Just one item!!!")])) return { - DOM: vdom$ - }; - + DOM: vdom$, + } } From 95ca1ce54cab20176b9625cbd344ed6d2d4f2df0 Mon Sep 17 00:00:00 2001 From: "Verbeiren, Toni [JRDBE Non-J&J]" Date: Wed, 22 Sep 2021 19:04:10 +0200 Subject: [PATCH 023/191] Update reducers in Filter.js --- src/js/components/Filter.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/js/components/Filter.js b/src/js/components/Filter.js index 243b47b3..18dcb026 100644 --- a/src/js/components/Filter.js +++ b/src/js/components/Filter.js @@ -168,7 +168,7 @@ function intent(domSource$) { } } -function model( +export function model( possibleValues$, input$, filterValuesAction$, @@ -180,7 +180,6 @@ function model( const defaultReducer$ = xs.of((prevState) => ({ ...prevState, core: { - ...prevState.core, output: {}, filter_output: {}, state: {dose: false, cell: false, trtType: false}, @@ -200,10 +199,7 @@ function model( ...prevState.settings.filter, values: fvs, }, - }, - core: { - ...prevState.core, - }, + } }) ) @@ -228,13 +224,15 @@ function model( ? prop(filterKey, prevState.settings.filter.values) : prop(filterKey, prevState.core.output) const alreadyIncluded = currentArrayForFilterKey.includes(filterValue) - // does the value have to be removed from the list? - const newArrayForFilterKey = alreadyIncluded - ? currentArrayForFilterKey.filter((el) => el != filterValue) - : currentArrayForFilterKey.concat(filterValue) // the value has to be added to the list + // does the value have to be removed from the list or added? + const newArrayForFilterKey = + alreadyIncluded + ? currentArrayForFilterKey.filter((el) => el != filterValue) + : currentArrayForFilterKey.concat(filterValue) // the value has to be added to the list + // add the updated array to the appropriate key but sort first const updatedState = assocPath( ["core", "output", filterKey], - newArrayForFilterKey, + newArrayForFilterKey.sort(), prevState ) return updatedState @@ -250,9 +248,10 @@ function model( const allValues = prop(filterKey, prevState.settings.filter.values) // possible - current values const newValues = allValues.filter((v) => !currentValues.includes(v)) + // add the updated array to the appropriate key but sort first const updatedState = assocPath( ["core", "output", filterKey], - newValues, + newValues.sort(), prevState ) return updatedState From 07da22323c4cd1151e5aab0a71b0926867afab7f Mon Sep 17 00:00:00 2001 From: "Verbeiren, Toni [JRDBE Non-J&J]" Date: Wed, 22 Sep 2021 19:09:27 +0200 Subject: [PATCH 024/191] Add tests for Filter.js --- .mocharc.yaml | 1 + src/test/FilterTest.js | 205 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 .mocharc.yaml create mode 100644 src/test/FilterTest.js diff --git a/.mocharc.yaml b/.mocharc.yaml new file mode 100644 index 00000000..15a5e8f1 --- /dev/null +++ b/.mocharc.yaml @@ -0,0 +1 @@ +require: '@babel/register' diff --git a/src/test/FilterTest.js b/src/test/FilterTest.js new file mode 100644 index 00000000..6dfca1cb --- /dev/null +++ b/src/test/FilterTest.js @@ -0,0 +1,205 @@ +import "mocha" +import * as assert from "assert" +import { model } from "../js/components/Filter.js" +import xs from "xstream" +import fromDiagram from "xstream/extra/fromDiagram" + +describe("defaultReducer", function () { + it("Generates a state if none exists", () => { + const state = null + const reducers$ = model( + xs.empty(), + xs.empty(), + xs.empty(), + xs.empty(), + xs.empty() + ) + + reducers$.addListener({ + next(f) { + const newState = f(state) + assert.deepStrictEqual(newState.core.output, {}) + assert.deepStrictEqual(newState.settings.filter, { values: {} }) + }, + error(e) { + done(e) + }, + complete() {}, + }) + }) + it("Generates a clean state if one exists", () => { + const state = { + core: ["bla", "blah"], + settings: { old: true }, + } + const reducers$ = model( + xs.empty(), + xs.empty(), + xs.empty(), + xs.empty(), + xs.empty() + ) + + reducers$.addListener({ + next(f) { + const newState = f(state) + assert.deepStrictEqual(newState.core.output, {}) + assert.deepStrictEqual(newState.settings.filter, { values: {} }) + }, + error(e) { + done(e) + }, + complete() {}, + }) + }) +}) + +describe("possibleValuesReducer", function () { + it("Updates the state with the possible values, leaves the rest intact", () => { + const state = { + core: { entry: "test" }, + settings: {}, + } + + const possibleValues = [ + { filter1: [1, 2, 3] }, + { filter2: ["a", "b", "c"] }, + ] + + const possibleValues$ = xs.of(possibleValues) + + const reducers$ = model( + possibleValues$, + xs.empty(), + xs.empty(), + xs.empty(), + xs.empty() + ) + + reducers$ + .drop(1) // avoid the defaultReducer + .addListener({ + next(f) { + const newState = f(state) + assert.deepStrictEqual(newState.core, state.core) + assert.deepStrictEqual(newState.settings.filter, { + values: possibleValues, + }) + }, + error(e) { + done(e) + }, + complete() {}, + }) + }) +}) + +describe("inputReducer", function () { + it("Updates the state when a new 'input' (signature) is provided", () => { + const state = { + core: { input: ["a", "b"] }, + settings: {}, + } + + const possibleValues = [ + { filter1: [1, 2, 3] }, + { filter2: ["a", "b", "c"] }, + ] + + const newInput = ["c", "d"] + + const input$ = xs.of(newInput) + + const reducers$ = model( + xs.empty(), + input$, + xs.empty(), + xs.empty(), + xs.empty() + ) + + reducers$ + .drop(1) // avoid the defaultReducer + .addListener({ + next(f) { + const newState = f(state) + assert.deepStrictEqual(newState.core.input, newInput) + }, + error(e) { + done(e) + }, + complete() {}, + }) + }) +}) + +describe("toggleReducer with and without modifier", function () { + it("Updates the state when a filter value is clicked", () => { + const possibleValues = { + dose: [1, 2, 3], + cell: ["cell1", "cell2", "cell3"], + trtType: ["a", "b", "c"], + } + const possibleValues$ = fromDiagram(`-x`).mapTo(possibleValues) + + const newInput = ["c", "d"] + const input$ = fromDiagram("-x").mapTo(newInput) + + const switchDose1 = { dose: 1 } + const switchCell = {cell: "cell1"} + + // Result of clicking on dose = 1 + const switchDose1$ = fromDiagram("--x").mapTo(switchDose1) + // Result of clicking on cell = cell1 + const action2$ = fromDiagram("---x").mapTo(switchCell) + // Result of clicking on dose = 1 again, this adds the filter value again + const action3$ = fromDiagram("----x").mapTo(switchDose1) + // Result of clicking on dose = 1 again, this time with modifier (see modifier$) + const action4$ = fromDiagram("-------x").mapTo(switchDose1) + const filterValuesAction$ = xs.merge( + switchDose1$, + action2$, + action3$, + action4$ + ) + + const modifierFalse$ = fromDiagram("x------").mapTo(false) + const modifierTrue$ = fromDiagram("------x").mapTo(true) + const modifier$ = xs.merge(modifierFalse$, modifierTrue$) + + const reducers$ = model( + possibleValues$, + input$, + filterValuesAction$, + modifier$, + xs.empty() + ) + + const state$ = reducers$.fold((state, reducer) => reducer(state), undefined) + + // The reducers should generate the following sequence for state.core.output + let expectedOutput = [ + {}, + {}, + {}, + { dose: [2, 3] }, + { dose: [2, 3], cell: ["cell2", "cell3"] }, + { dose: [1, 2, 3], cell: ["cell2", "cell3"] }, + { dose: [], cell: ["cell2", "cell3"] }, + ] + + state$ + .drop(1) // drop the first state as it is undefined + .addListener({ + next(state) { + assert.deepStrictEqual(state?.core?.output, expectedOutput.shift()) + }, + error(e) { + console.log(e) + }, + complete() { + console.log("done!") + }, + }) + }) +}) From bae5307404911047fdeae13f14f373399cf36ce6 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Thu, 30 Sep 2021 20:06:08 +0200 Subject: [PATCH 025/191] New genetic workflow (WIP) --- src/js/components/CompoundForm.js | 134 +++++----- src/js/components/TreatmentCheck.js | 300 +++++++++++++++++++++++ src/js/components/TreatmentForm.js | 79 ++++++ src/js/drivers/makeAutocompleteDriver.js | 2 - src/js/index.js | 5 +- src/js/main.scss | 6 + src/js/pages/genetic.js | 174 +++++++++++++ 7 files changed, 630 insertions(+), 70 deletions(-) create mode 100644 src/js/components/TreatmentCheck.js create mode 100644 src/js/components/TreatmentForm.js create mode 100644 src/js/pages/genetic.js diff --git a/src/js/components/CompoundForm.js b/src/js/components/CompoundForm.js index 9097ff98..60564507 100644 --- a/src/js/components/CompoundForm.js +++ b/src/js/components/CompoundForm.js @@ -1,79 +1,79 @@ -import isolate from '@cycle/isolate' -import { i, p, div, br, label, input, code, table, tr, td, b, h2, button, textarea, a, ul, li, span } from '@cycle/dom'; -import xs from 'xstream'; -import { CompoundCheck, checkLens } from './CompoundCheck' -import { SampleSelection, sampleSelectionLens } from './SampleSelection' -import { SignatureGenerator, signatureLens } from './SignatureGenerator' -import { loggerFactory } from '../utils/logger' +import isolate from "@cycle/isolate" +import { div } from "@cycle/dom" +import xs from "xstream" +import { CompoundCheck, checkLens } from "./CompoundCheck" +import { SampleSelection, sampleSelectionLens } from "./SampleSelection" +import { SignatureGenerator, signatureLens } from "./SignatureGenerator" +import { loggerFactory } from "../utils/logger" function CompoundForm(sources) { + const logger = loggerFactory( + "compoundForm", + sources.onion.state$, + "settings.debug" + ) - const logger = loggerFactory('compoundForm', sources.onion.state$, 'settings.debug') + const state$ = sources.onion.state$ - const state$ = sources.onion.state$ + const CompoundCheckSink = isolate(CompoundCheck, { onion: checkLens })( + sources + ) + const compoundQuery$ = CompoundCheckSink.output.remember() - const CompoundCheckSink = isolate(CompoundCheck, {onion: checkLens} )(sources) - const compoundQuery$ = CompoundCheckSink.output.remember() + const SampleSelectionSink = isolate(SampleSelection, { + onion: sampleSelectionLens, + })({ ...sources, input: compoundQuery$ }) + const sampleSelection$ = SampleSelectionSink.output.remember() - const SampleSelectionSink = isolate(SampleSelection, {onion: sampleSelectionLens})({...sources, input: compoundQuery$}) - const sampleSelection$ = SampleSelectionSink.output.remember() + const SignatureGeneratorSink = isolate(SignatureGenerator, { + onion: signatureLens, + })({ ...sources, input: sampleSelection$ }) + const signature$ = SignatureGeneratorSink.output.remember() - const SignatureGeneratorSink = isolate(SignatureGenerator, {onion: signatureLens})({...sources, input: sampleSelection$ }) - const signature$ = SignatureGeneratorSink.output.remember() + const vdom$ = xs + .combine( + CompoundCheckSink.DOM.startWith(div()), + SampleSelectionSink.DOM, + SignatureGeneratorSink.DOM + ) + .map(([formDom, selectionDOM, signatureDOM]) => + div([ + formDom, + selectionDOM, + div(".col .s10 .offset-s1", [ + div(".row", [div(".col .s12", [signatureDOM])]), + ]), + ]) + ) - const vdom$ = xs.combine( - CompoundCheckSink.DOM.startWith(div()), - SampleSelectionSink.DOM, - SignatureGeneratorSink.DOM, - ) - .map(([ - formDom, - selectionDOM, - signatureDOM, - ]) => - div([ - formDom, - selectionDOM, - div('.col .s10 .offset-s1', [ - div('.row', [ - div('.col .s12', [ - signatureDOM - ]) - ]) - ]) - ])) + const defaultReducer$ = xs.of((prevState) => { + // CompoundForm -- default Reducer + return { ...prevState, form: {}, sampleSelection: {}, signature: {} } + }) - const defaultReducer$ = xs.of(prevState => { - // CompoundForm -- default Reducer - return ({...prevState, form: {}, sampleSelection: {}, signature: {}}) - }) - - return { - log: xs.merge( - logger(state$, 'state$'), - CompoundCheckSink.log, - SampleSelectionSink.log, - SignatureGeneratorSink.log - ), - DOM: vdom$, - onion: xs.merge( - defaultReducer$, - CompoundCheckSink.onion, - SampleSelectionSink.onion, - SignatureGeneratorSink.onion - ), - HTTP: xs.merge( - CompoundCheckSink.HTTP, - SampleSelectionSink.HTTP, - SignatureGeneratorSink.HTTP - ), - output: signature$, - modal: xs.merge( - SignatureGeneratorSink.modal, - SampleSelectionSink.modal - ), - ac: CompoundCheckSink.ac - } + return { + log: xs.merge( + logger(state$, "state$"), + CompoundCheckSink.log, + SampleSelectionSink.log, + SignatureGeneratorSink.log + ), + DOM: vdom$, + onion: xs.merge( + defaultReducer$, + CompoundCheckSink.onion, + SampleSelectionSink.onion, + SignatureGeneratorSink.onion + ), + HTTP: xs.merge( + CompoundCheckSink.HTTP, + SampleSelectionSink.HTTP, + SignatureGeneratorSink.HTTP + ), + output: signature$, + modal: xs.merge(SignatureGeneratorSink.modal, SampleSelectionSink.modal), + ac: CompoundCheckSink.ac, + } } export { CompoundForm } diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js new file mode 100644 index 00000000..71c32b13 --- /dev/null +++ b/src/js/components/TreatmentCheck.js @@ -0,0 +1,300 @@ +import sampleCombine from "xstream/extra/sampleCombine" +import { i, div, input } from "@cycle/dom" +import { equals, mergeAll } from "ramda" +import xs from "xstream" +import dropRepeats from "xstream/extra/dropRepeats" +import debounce from "xstream/extra/debounce" +import { loggerFactory } from "../utils/logger" + +const checkLens = { + get: (state) => ({ + core: typeof state.form !== "undefined" ? state.form.check : {}, + settings: state.settings, + }), + set: (state, childState) => ({ + ...state, + form: { ...state.form, check: childState.core }, + }), +} + +/** + * Form for entering treatments with autocomplete. + * + * Input: Form input + * Output: treatment (string) + */ +function TreatmentCheck(sources) { + // States of autosuggestion field: + // - Less than N characters -> no query, no suggestions + // - N or more -> with every character a query is done (after 500ms). suggestions are shown + // - Clicking on a suggestion activates it in the search field and sets validated to true + // - At that point, the dropdown should dissapear!!! + // - The suggestions appear again whenever something changes in the input... + + const logger = loggerFactory( + "treatmentCheck", + sources.onion.state$, + "settings.form.debug" + ) + + const state$ = sources.onion.state$ + + const acInput$ = sources.ac + + const input$ = xs.merge( + sources.DOM.select(".treatmentQuery") + .events("input") + .map((ev) => ev.target.value) + .startWith(""), + // This for ghost mode, inject changes via external state updates... + state$ + .filter((state) => typeof state.core.ghostinput !== "undefined") + .map((state) => state.core.input) + .compose(dropRepeats()) + ) + + // When the component should not be shown, including empty signature + const isEmptyState = (state) => { + if (typeof state.core === "undefined") { + return true + } else { + if (typeof state.core.input === "undefined") { + return true + } else { + return false + } + } + } + + const emptyState$ = state$ + .filter((state) => isEmptyState(state)) + .compose(dropRepeats(equals)) + + // When the state is cycled because of an internal update + const modifiedState$ = state$ + .filter((state) => !isEmptyState(state)) + .compose(dropRepeats((x, y) => equals(x, y))) + .remember() + + // An update to the input$, join it with state$ + const newInput$ = xs + .combine(input$, modifiedState$) + .map(([newinput, state]) => ({ + ...state, + core: { ...state.core, input: newinput }, + })) + .compose(dropRepeats((x, y) => equals(x.core.input, y.core.input))) + + const triggerRequest$ = newInput$ + .filter((state) => state.core.input.length >= 1) + .filter((state) => state.core.showSuggestions) + .compose(debounce(200)) + + const request$ = triggerRequest$.map((state) => { + return { + url: + state.settings.api.url + + "&classPath=com.dataintuitive.luciusapi.treatments", + method: "POST", + send: { + version: "v2", + query: state.core.input, + like: "genetic" + }, + category: "treatments", + } + }) + + const response$ = sources.HTTP.select("treatments") + .map((response$) => response$.replaceError(() => xs.of([]))) + .flatten() + + const data$ = response$.map((res) => res.body.result.data).remember() + + const initVdom$ = emptyState$.mapTo(div()) + + const loadedVdom$ = modifiedState$.map((state) => { + const query = state.core.input + const validated = state.core.validated + return div([ + div( + ".row .green .darken-4 .white-text", + { style: { padding: "20px 10px 10px 10px" } }, + [ + div(".Default .waves-effect .col .s1 .center-align", [ + i( + ".large .center-align .material-icons .orange-text", + { style: { fontSize: "45px", fontColor: "gray" } }, + "search" + ), + ]), + div( + ".col .s10 .input-field", + { style: { margin: "0px 0px 0px 0px" } }, + [ + input( + ".treatmentQuery.col .s12 .autocomplete-input .white-text", + { + style: { fontSize: "20px" }, + props: { type: "text", value: query }, + value: query, + } + ), + ] + ), + validated + ? div(".treatmentCheck .waves-effect .col .s1 .center-align", [ + i( + ".large .material-icons", + { style: { fontSize: "45px", fontColor: "grey" } }, + ["play_arrow"] + ), + ]) + : div(".treatmentCheck .col .s1 .center-align", [ + i( + ".large .material-icons .orange-text", + { style: { fontSize: "45px", fontColor: "grey" } }, + "play_arrow" + ), + ]), + ] + ), + ]) + }) + + const vdom$ = xs.merge(initVdom$, loadedVdom$).startWith(div()) + + // Set a initial reducer, showing suggestions + const defaultReducer$ = xs.of((prevState) => { + // treatmentCheck -- defaultReducer$') + let newState = { + ...prevState, + core: { + ...prevState.core, + showSuggestions: true, + validated: false, + input: "", + data: [], + }, + } + return newState + }) + + // Reducer for showing suggestions again after an input event + const inputReducer$ = input$.map((value) => (prevState) => ({ + ...prevState, + core: { + ...prevState.core, + showSuggestions: true, + validated: false, + input: value, + }, + })) + + // Set a default signature for demo purposes + const setDefault$ = sources.DOM.select(".Default").events("click") + const setDefaultReducer$ = setDefault$ + .compose(sampleCombine(state$)) + .map(([_, state]) => (prevState) => ({ + ...prevState, + core: { + ...prevState.core, + showSuggestions: false, + validated: true, + input: state.settings.common.hourglass.treatment, + }, + })) + + // Add request body to state + const requestReducer$ = request$.map((req) => (prevState) => ({ + ...prevState, + core: { ...prevState.core, request: req }, + })) + + // Add data from API to state, update output key when relevant + const dataReducer$ = data$.map((newData) => (prevState) => ({ + ...prevState, + core: { ...prevState.core, data: newData }, + })) + + // Whenever more than 1 option is available per our treatment query, we list the options. + const ac$ = data$ + .filter((data) => data.length > 1) + .map((data) => ({ + el: ".treatmentQuery", + data: data, + render: function (data) { + return mergeAll( + data.map((d) => ({ [d.trtId + " - " + d.trtName + " (" + d.count + ")"]: null })) + ) + }, + strip: function (str) { + return str.split(" - ")[0]; + }, + })) + + + // When a suggestion is clicked, update the state so the query becomes this + const autocompleteReducer$ = xs + .merge( + // input from autocomplete (clicking an option) + acInput$, + // input from having one solution left in the autocomplete, extract the remaning target + ac$ + // Trigger an update when only one result is left so we can handle that in the AutoComplete driver + .filter((data) => data.length == 1) + .map((info) => info.data[0].trtId) + ) + .map((input) => (prevState) => { + const newInput = input + return { + ...prevState, + core: { + ...prevState.core, + input: newInput, + showSuggestions: false, + validated: true, + output: newInput, + }, + } + }) + + // GO!!! + const run$ = sources.DOM.select(".treatmentCheck").events("click") + + const query$ = xs + .merge( + run$, + // Ghost mode + sources.onion.state$ + .map((state) => state.core.ghostoutput) + .filter((ghost) => ghost) + .compose(dropRepeats()) + ) + .compose(sampleCombine(state$)) + .map(([_, state]) => state.core.input) + .remember() + + // const history$ = sources.onion.state$.fold((acc, x) => acc.concat([x]), [{}]) + + return { + log: xs.merge( + logger(state$, "state$") + // logger(history$, 'history$'), + ), + HTTP: request$, + DOM: vdom$, + onion: xs.merge( + defaultReducer$, + inputReducer$, + dataReducer$, + requestReducer$, + setDefaultReducer$, + autocompleteReducer$ + ), + output: query$, + ac: ac$ + } +} + +export { TreatmentCheck, checkLens } diff --git a/src/js/components/TreatmentForm.js b/src/js/components/TreatmentForm.js new file mode 100644 index 00000000..06847099 --- /dev/null +++ b/src/js/components/TreatmentForm.js @@ -0,0 +1,79 @@ +import isolate from "@cycle/isolate" +import { div } from "@cycle/dom" +import xs from "xstream" +import { TreatmentCheck, checkLens } from "./TreatmentCheck" +import { SampleSelection, sampleSelectionLens } from "./SampleSelection" +import { SignatureGenerator, signatureLens } from "./SignatureGenerator" +import { loggerFactory } from "../utils/logger" + +function TreatmentForm(sources) { + const logger = loggerFactory( + "treatmentForm", + sources.onion.state$, + "settings.debug" + ) + + const state$ = sources.onion.state$ + + const TreatmentCheckSink = isolate(TreatmentCheck, { onion: checkLens })( + sources + ) + const treatmentQuery$ = TreatmentCheckSink.output.remember() + + const SampleSelectionSink = isolate(SampleSelection, { + onion: sampleSelectionLens, + })({ ...sources, input: treatmentQuery$ }) + const sampleSelection$ = SampleSelectionSink.output.remember() + + const SignatureGeneratorSink = isolate(SignatureGenerator, { + onion: signatureLens, + })({ ...sources, input: sampleSelection$ }) + const signature$ = SignatureGeneratorSink.output.remember() + + const vdom$ = xs + .combine( + TreatmentCheckSink.DOM.startWith(div()), + SampleSelectionSink.DOM, + SignatureGeneratorSink.DOM + ) + .map(([formDom, selectionDOM, signatureDOM]) => + div([ + formDom, + selectionDOM, + div(".col .s10 .offset-s1", [ + div(".row", [div(".col .s12", [signatureDOM])]), + ]), + ]) + ) + + const defaultReducer$ = xs.of((prevState) => { + // treatmentForm -- default Reducer + return { ...prevState, form: {}, sampleSelection: {}, signature: {} } + }) + + return { + log: xs.merge( + logger(state$, "state$"), + TreatmentCheckSink.log, + SampleSelectionSink.log, + SignatureGeneratorSink.log + ), + DOM: vdom$, + onion: xs.merge( + defaultReducer$, + TreatmentCheckSink.onion, + SampleSelectionSink.onion, + SignatureGeneratorSink.onion + ), + HTTP: xs.merge( + TreatmentCheckSink.HTTP, + SampleSelectionSink.HTTP, + SignatureGeneratorSink.HTTP + ), + output: signature$, + modal: xs.merge(SignatureGeneratorSink.modal, SampleSelectionSink.modal), + ac: TreatmentCheckSink.ac, + } +} + +export { TreatmentForm } diff --git a/src/js/drivers/makeAutocompleteDriver.js b/src/js/drivers/makeAutocompleteDriver.js index 65ad4171..bd0e8c68 100644 --- a/src/js/drivers/makeAutocompleteDriver.js +++ b/src/js/drivers/makeAutocompleteDriver.js @@ -36,8 +36,6 @@ function makeAutocompleteDriver() { } }) ac.open() - // if (ac.isOpen || acInfo.data.length == 1) { ac.close() } - // else { ac.open() } } }, error: (m) => { diff --git a/src/js/index.js b/src/js/index.js index 26e80fe1..06890cda 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -7,6 +7,7 @@ import * as R from 'ramda' // Workflows import DiseaseWorkflow from './pages/disease' import CompoundWorkflow from './pages/compound' +import GeneticWorkflow from './pages/genetic' import TargetWorkflow from './pages/target' import CorrelationWorkflow from './pages/correlation' @@ -38,6 +39,7 @@ export default function Index(sources) { // }, '/compound': CompoundWorkflow, '/target': TargetWorkflow, + '/genetic': GeneticWorkflow, '/statistics': StatisticsWorkflow, '/settings': IsolatedSettings, '/correlation': CorrelationWorkflow, @@ -86,7 +88,8 @@ export default function Index(sources) { ), ul('.left .hide-on-med-and-down', [ makeLink('/compound', span(['Compound', ' ', compoundSVG]), '.orange-text'), - makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), + // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), + makeLink('/genetic', span(['Genetic', ' ', targetSVG]), '.red-text'), makeLink('/disease', span(['Disease', ' ', diseaseSVG]), '.pink-text'), makeLink('/settings', span(['Settings', ' ', settingsSVG]), '.grey-text'), // makeLink('/admin', span(['Admin']), '.blue-text'), diff --git a/src/js/main.scss b/src/js/main.scss index 03bfaedc..107b3a3d 100644 --- a/src/js/main.scss +++ b/src/js/main.scss @@ -17,6 +17,11 @@ main { @import 'materialize-css/sass/components/color-variables'; @import 'materialize-css/sass/components/color-classes'; +.genetic { + @extend .red; + @extend .lighten-5; +} + .compound { @extend .orange; @extend .lighten-5; @@ -52,6 +57,7 @@ a:hover #border { /* no background color for printing */ .compound, .disease, + .genetic, .target { background-color: white !important } diff --git a/src/js/pages/genetic.js b/src/js/pages/genetic.js new file mode 100644 index 00000000..e8987dea --- /dev/null +++ b/src/js/pages/genetic.js @@ -0,0 +1,174 @@ +import { div } from "@cycle/dom" +import xs from "xstream" +import isolate from "@cycle/isolate" +import { TreatmentForm} from "../components/TreatmentForm" +import { initSettings } from "../configuration.js" +import { makeTable, headTableLens, tailTableLens } from "../components/Table" +import { BinnedPlots, plotsLens } from "../components/BinnedPlots/BinnedPlots" +import { Filter, filterLens } from "../components/Filter" +import { loggerFactory } from "../utils/logger" +import { + SampleTable, + sampleTableLens, +} from "../components/SampleTable/SampleTable" + +// Support for ghost mode +import { scenario } from "../scenarios/compoundScenario" +import { runScenario } from "../utils/scenario" + +export default function GeneticWorkflow(sources) { + const logger = loggerFactory( + "genetic", + sources.onion.state$, + "settings.common.debug" + ) + + const state$ = sources.onion.state$ + + // Scenario for ghost mode + const scenarioReducer$ = sources.onion.state$ + .take(1) + .filter((state) => state.settings.common.ghostMode) + .mapTo(runScenario(scenario).scenarioReducer$) + .flatten() + .startWith((prevState) => prevState) + const scenarioPopup$ = sources.onion.state$ + .take(1) + .filter((state) => state.settings.common.ghostMode) + .mapTo(runScenario(scenario).scenarioPopup$) + .flatten() + .startWith({ text: "Welcome to Genetic Perturbation Workflow", duration: 4000 }) + + const formLens = { + get: (state) => ({ + form: state.form, + settings: { + form: state.settings.form, + api: state.settings.api, + common: state.settings.common, + geneAnnotations: state.settings.geneAnnotations, + compoundAnnotations: state.settings.compoundAnnotations, + }, + }), + set: (state, childState) => ({ ...state, form: childState.form }), + } + + // Initialize if not yet done in parent (i.e. router) component (useful for testing) + const defaultReducer$ = xs.of((prevState) => { + // genetic -- defaultReducer + if (typeof prevState === "undefined") { + return { + settings: initSettings, + } + } else { + return { + ...prevState, + settings: prevState.settings, + } + } + }) + + const TreatmentFormSink = isolate(TreatmentForm, { onion: formLens })(sources) + const signature$ = TreatmentFormSink.output.remember() + + // Filter Form + const filterForm = isolate(Filter, { onion: filterLens })({ + ...sources, + input: signature$, + }) + const filter$ = filterForm.output.remember() + + // const filter$ = xs.of({}) + // Binned Plots Component + const binnedPlots = isolate(BinnedPlots, { onion: plotsLens })({ + ...sources, + input: xs + .combine(signature$, filter$) + .map(([s, f]) => ({ signature: s, filter: f })) + .remember(), + }) + + // tables + const headTableContainer = makeTable(SampleTable, sampleTableLens) + const tailTableContainer = makeTable(SampleTable, sampleTableLens) + + // Join settings from api and sourire into props + const headTable = isolate(headTableContainer, { onion: headTableLens })({ + ...sources, + input: xs + .combine(signature$, filter$) + .map(([s, f]) => ({ query: s, filter: f })) + .remember(), + }) + const tailTable = isolate(tailTableContainer, { onion: tailTableLens })({ + ...sources, + input: xs + .combine(signature$, filter$) + .map(([s, f]) => ({ query: s, filter: f })) + .remember(), + }) + + const pageStyle = { + style: { + fontSize: "14px", + opacity: "0", + transition: "opacity 1s", + delayed: { opacity: "1" }, + destroy: { opacity: "0" }, + }, + } + + const vdom$ = xs + .combine( + TreatmentFormSink.DOM, + filterForm.DOM, + binnedPlots.DOM, + headTable.DOM, + tailTable.DOM + ) + .map(([formDOM, filter, plots, headTable, tailTable]) => + div(".row .genetic", { style: { margin: "0px 0px 0px 0px" } }, [ + formDOM, + div(".col .s10 .offset-s1", pageStyle, [ + div(".row", [filter]), + div(".row", [plots]), + div(".col .s12", [headTable]), + div(".row", []), + div(".col .s12", [tailTable]), + div(".row", []), + ]), + ]) + ) + + return { + log: xs.merge( + logger(state$, "state$"), + TreatmentFormSink.log, + filterForm.log, + binnedPlots.log, + headTable.log, + tailTable.log + ), + DOM: vdom$.startWith(div()), + onion: xs.merge( + defaultReducer$, + TreatmentFormSink.onion, + binnedPlots.onion, + filterForm.onion, + headTable.onion, + tailTable.onion, + scenarioReducer$ + ), + HTTP: xs.merge( + TreatmentFormSink.HTTP, + filterForm.HTTP, + binnedPlots.HTTP, + headTable.HTTP, + tailTable.HTTP + ), + vega: binnedPlots.vega, + popup: scenarioPopup$, + modal: xs.merge(TreatmentFormSink.modal), + ac: TreatmentFormSink.ac, + } +} From 067dc20daf17825a2e0de82578b1d2eaab995395 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Thu, 30 Sep 2021 20:09:32 +0200 Subject: [PATCH 026/191] Small layout fixes --- src/js/components/TreatmentCheck.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index 71c32b13..50acb10d 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -118,7 +118,7 @@ function TreatmentCheck(sources) { const validated = state.core.validated return div([ div( - ".row .green .darken-4 .white-text", + ".row .genetic .darken-4 .white-text", { style: { padding: "20px 10px 10px 10px" } }, [ div(".Default .waves-effect .col .s1 .center-align", [ From 4873e13c09d69edee4e11a79bd5c344b362bcd58 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Wed, 6 Oct 2021 10:05:21 +0200 Subject: [PATCH 027/191] Correct example value for genetic treatment --- deployments.json | 1 + src/js/components/TreatmentCheck.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/deployments.json b/deployments.json index fd6fbdd9..d6005d3b 100644 --- a/deployments.json +++ b/deployments.json @@ -22,6 +22,7 @@ "hourglass": { "compound" : "BRD-K93645900", "signature" : "-WRONG HSPA1A DNAJB1 DDIT4 -TSEN2", + "treatment" : "MELK", "target" : "MELK" }, "modelTranslations": [ diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index 50acb10d..094865ef 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -217,7 +217,7 @@ function TreatmentCheck(sources) { core: { ...prevState.core, data: newData }, })) - // Whenever more than 1 option is available per our treatment query, we list the options. + // Feed the autocomplete driver const ac$ = data$ .filter((data) => data.length > 1) .map((data) => ({ From 54fd5f714d93633dc7c84186bc4ec47cb45dfb8a Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Wed, 6 Oct 2021 10:05:59 +0200 Subject: [PATCH 028/191] Switch to ALT as modifier key --- src/js/components/SampleSelection.js | 592 ++++++++++++++------------- 1 file changed, 315 insertions(+), 277 deletions(-) diff --git a/src/js/components/SampleSelection.js b/src/js/components/SampleSelection.js index c8653d29..2c084b66 100644 --- a/src/js/components/SampleSelection.js +++ b/src/js/components/SampleSelection.js @@ -1,29 +1,43 @@ -import sampleCombine from 'xstream/extra/sampleCombine' -import isolate from '@cycle/isolate' -import { i, p, div, br, label, input, code, table, tr, td, b, h2, button, textarea, a, ul, li, span, th, thead, tbody } from '@cycle/dom'; -import { clone, equals, merge } from 'ramda'; -import xs from 'xstream'; -import { ENTER_KEYCODE } from '../utils/keycodes.js' -import dropRepeats from 'xstream/extra/dropRepeats' -import debounce from 'xstream/extra/debounce' -import delay from 'xstream/extra/delay' -import { loggerFactory } from '../utils/logger' -import { CompoundAnnotation } from '../components/CompoundAnnotation' -import { safeModelToUi } from '../modelTranslations' +import sampleCombine from "xstream/extra/sampleCombine" +import { + div, + label, + input, + table, + tr, + td, + button, + span, + th, + thead, + tbody, +} from "@cycle/dom" +import { clone, equals, merge } from "ramda" +import xs from "xstream" +import dropRepeats from "xstream/extra/dropRepeats" +import { loggerFactory } from "../utils/logger" +import { CompoundAnnotation } from "../components/CompoundAnnotation" +import { safeModelToUi } from "../modelTranslations" const emptyData = { - body: { - result: { - data: [] - } - } + body: { + result: { + data: [], + }, + }, } const sampleSelectionLens = { - get: state => ({ core: (typeof state.form !== 'undefined') ? state.form.sampleSelection : {}, settings: state.settings }), - // get: state => ({core: state.form.sampleSelection, settings: state.settings}), - set: (state, childState) => ({...state, form: {...state.form, sampleSelection: childState.core } }) -}; + get: (state) => ({ + core: typeof state.form !== "undefined" ? state.form.sampleSelection : {}, + settings: state.settings, + }), + // get: state => ({core: state.form.sampleSelection, settings: state.settings}), + set: (state, childState) => ({ + ...state, + form: { ...state.form, sampleSelection: childState.core }, + }), +} /** * Based on a (list of) compound(s), get the samples that correspond to it and allow users to select them. @@ -32,275 +46,299 @@ const sampleSelectionLens = { * output: list of samples (array) */ function SampleSelection(sources) { - - const compoundAnnotations = CompoundAnnotation(sources) - - const logger = loggerFactory('sampleSelection', sources.onion.state$, 'settings.form.debug') - - const state$ = sources.onion.state$ - - const input$ = sources.input - // .startWith("BRD-K28907958") // REMOVE ME !!! - - // When the component should not be shown, including empty signature - const isEmptyState = (state) => { - if (typeof state.core === 'undefined') { - return true + const compoundAnnotations = CompoundAnnotation(sources) + + const logger = loggerFactory( + "sampleSelection", + sources.onion.state$, + "settings.form.debug" + ) + + const state$ = sources.onion.state$ + + const input$ = sources.input + // .startWith("BRD-K28907958") // REMOVE ME !!! + + // When the component should not be shown, including empty signature + const isEmptyState = (state) => { + if (typeof state.core === "undefined") { + return true + } else { + if (typeof state.core.input === "undefined") { + return true + } else { + if (state.core.input == "") { + return true } else { - if (typeof state.core.input === 'undefined') { - return true - } else { - if (state.core.input == '') { - return true - } else { - return false - } - } + return false } + } } - - const emptyState$ = state$ - // .filter(state => state.core.input == null || state.core.input == '') - .filter(state => isEmptyState(state)) - .compose(dropRepeats((x, y) => equals(x, y))) - - // When the state is cycled because of an internal update - const modifiedState$ = state$ - // .filter(state => state.core.input != '') - .filter(state => !isEmptyState(state)) - .compose(dropRepeats((x, y) => equals(x, y))) - - const newInput$ = xs.combine( - input$, - state$ - ) - .map(([newinput, state]) => ({...state, core: {...state.core, input: newinput } })) - .compose(dropRepeats((x, y) => equals(x.core.input, y.core.input))) - - // When a new query is required - const updatedState$ = state$ - .compose(dropRepeats((x, y) => equals(x.core, y.core))) - - const request$ = newInput$ - .map(state => { - return { - url: state.settings.api.url + '&classPath=com.dataintuitive.luciusapi.compoundToSamples', - method: 'POST', - send: { - version: 'v2', - query: state.core.input, - pvalue: state.settings.common.pvalue - }, - 'category': 'samples' - } - }) - - const response$ = sources.HTTP - .select('samples') - .map((response$) => - response$.replaceError(() => xs.of(emptyData)) - ) - .flatten() - - const data$ = response$ - .map(res => res.body) - .map(json => json.result.data) - .remember() - - // Helper function for rendering the table, based on the state - const makeTable = (state, annotation, initialization) => { - const data = state.core.data - const blurStyle = (state.settings.common.blur) ? { style: { filter: 'blur(' + state.settings.common.amountBlur + 'px)' } } : {} - const selectedClass = (selected) => (selected) ? '.black-text' : '.grey-text .text-lighten-2' - let rows = data.map(entry => [ - td('.selection', { props: { id: entry.id } }, [ - label('', { props: { id: entry.id } }, [ - input('.grey', { props: { type: 'checkbox', checked: entry.use, id: entry.id } }, 'tt'), - span(['']) - ]) - ]), - td('.compoundPopup' + selectedClass(entry.use), blurStyle, entry.trt_id), - td(selectedClass(entry.use), blurStyle, - (entry.trt_name.length > 20) ? entry.trt_name.substring(0, 20) + '...' : entry.trt_name - ), - td(".left-align" + selectedClass(entry.use), - (entry.id.length > 30) ? entry.id.substring(0, 30) + '...' : entry.id - ), - td(selectedClass(entry.use), entry.cell), - td(selectedClass(entry.use), - (entry.dose.length > 6) ? entry.dose.substring(0, 6) + '...' : entry.dose - ), - td(selectedClass(entry.use), entry.batch), - td(selectedClass(entry.use), entry.year), - td(selectedClass(entry.use), entry.time), - td(selectedClass(entry.use), entry.significantGenes) - - ]); - const header = tr([ - th('Use?'), - th(safeModelToUi('id', state.settings.common.modelTranslations)), - th('Name'), - th('Sample'), - th('Protocol'), - th('Conc'), - th('Batch'), - th('Year'), - th('Time'), - th('Sign. Genes') - ]); - - let body = []; - rows.map(row => body.push(tr(row))); - const tableContent = [thead([header]), tbody(body)]; - - return ( - div([ - div('.row', [ - div('.col .s10 .offset-s1 .l10 .offset-l1', [table('.striped .centered', tableContent)]), - annotation, - div('.row .s6 .offset-s3', [ - (initialization) - ? span([]) - : button('.doSelect .btn .col .offset-s4 .s4 .orange .darken-2', 'Select') - ]), - ]) - ]) - ); + } + + const emptyState$ = state$ + // .filter(state => state.core.input == null || state.core.input == '') + .filter((state) => isEmptyState(state)) + .compose(dropRepeats((x, y) => equals(x, y))) + + // When the state is cycled because of an internal update + const modifiedState$ = state$ + // .filter(state => state.core.input != '') + .filter((state) => !isEmptyState(state)) + .compose(dropRepeats((x, y) => equals(x, y))) + + const newInput$ = xs + .combine(input$, state$) + .map(([newinput, state]) => ({ + ...state, + core: { ...state.core, input: newinput }, + })) + .compose(dropRepeats((x, y) => equals(x.core.input, y.core.input))) + + // When a new query is required + const updatedState$ = state$.compose( + dropRepeats((x, y) => equals(x.core, y.core)) + ) + + const request$ = newInput$.map((state) => { + return { + url: + state.settings.api.url + + "&classPath=com.dataintuitive.luciusapi.compoundToSamples", + method: "POST", + send: { + version: "v2", + query: state.core.input, + pvalue: state.settings.common.pvalue, + }, + category: "samples", } - - const initVdom$ = emptyState$.mapTo(div()) - - const loadingVdom$ = request$.compose(sampleCombine(state$)) - .map( ([_, state]) => - // Use the same makeTable function, pass a initialization=true parameter and a body DOM with preloading - makeTable( - state, - div('.col.s10.offset-s1.l10.offset-l1', [ - div('.progress.orange.lighten-3', { style: { margin: '2px 0px 2px 0px'} }, [ - div('.indeterminate', {style : { "background-color" : 'white' }}) - ]) - ]), - true - ) + }) + + const response$ = sources.HTTP.select("samples") + .map((response$) => response$.replaceError(() => xs.of(emptyData))) + .flatten() + + const data$ = response$ + .map((res) => res.body) + .map((json) => json.result.data) + .remember() + + // Helper function for rendering the table, based on the state + const makeTable = (state, annotation, initialization) => { + const data = state.core.data + const blurStyle = state.settings.common.blur + ? { + style: { filter: "blur(" + state.settings.common.amountBlur + "px)" }, + } + : {} + const selectedClass = (selected) => + selected ? ".black-text" : ".grey-text .text-lighten-2" + let rows = data.map((entry) => [ + td(".selection", { props: { id: entry.id } }, [ + label("", { props: { id: entry.id } }, [ + input( + ".grey", + { props: { type: "checkbox", checked: entry.use, id: entry.id } }, + "tt" + ), + span([""]), + ]), + ]), + td(".compoundPopup" + selectedClass(entry.use), blurStyle, entry.trt_id), + td( + selectedClass(entry.use), + blurStyle, + entry.trt_name.length > 20 + ? entry.trt_name.substring(0, 20) + "..." + : entry.trt_name + ), + td( + ".left-align" + selectedClass(entry.use), + entry.id.length > 30 ? entry.id.substring(0, 30) + "..." : entry.id + ), + td(selectedClass(entry.use), entry.cell), + td( + selectedClass(entry.use), + entry.dose.length > 6 ? entry.dose.substring(0, 6) + "..." : entry.dose + ), + td(selectedClass(entry.use), entry.batch), + td(selectedClass(entry.use), entry.year), + td(selectedClass(entry.use), entry.time), + td(selectedClass(entry.use), entry.significantGenes), + ]) + const header = tr([ + th("Use?"), + th(safeModelToUi("id", state.settings.common.modelTranslations)), + th("Name"), + th("Sample"), + th("Protocol"), + th("Conc"), + th("Batch"), + th("Year"), + th("Time"), + th("Sign. Genes"), + ]) + + let body = [] + rows.map((row) => body.push(tr(row))) + const tableContent = [thead([header]), tbody(body)] + + return div([ + div(".row", [ + div(".col .s10 .offset-s1 .l10 .offset-l1", [ + table(".striped .centered", tableContent), + ]), + annotation, + div(".row .s6 .offset-s3", [ + initialization + ? span([]) + : button( + ".doSelect .btn .col .offset-s4 .s4 .orange .darken-2", + "Select" + ), + ]), + ]), + ]) + } + + const initVdom$ = emptyState$.mapTo(div()) + + const loadingVdom$ = request$ + .compose(sampleCombine(state$)) + .map(([_, state]) => + // Use the same makeTable function, pass a initialization=true parameter and a body DOM with preloading + makeTable( + state, + div(".col.s10.offset-s1.l10.offset-l1", [ + div( + ".progress.orange.lighten-3", + { style: { margin: "2px 0px 2px 0px" } }, + [div(".indeterminate", { style: { "background-color": "white" } })] + ), + ]), + true ) - .remember() + ) + .remember() - const loadedVdom$ = xs.combine(modifiedState$, compoundAnnotations.DOM) - .map(([state, annotation]) => makeTable(state, annotation, false)) + const loadedVdom$ = xs + .combine(modifiedState$, compoundAnnotations.DOM) + .map(([state, annotation]) => makeTable(state, annotation, false)) - const vdom$ = xs.merge(initVdom$, loadingVdom$, loadedVdom$) + const vdom$ = xs.merge(initVdom$, loadingVdom$, loadedVdom$) - const dataReducer$ = data$.map(data => prevState => { - const newData = data.map(el => merge(el, { use: true })) + const dataReducer$ = data$.map((data) => (prevState) => { + const newData = data.map((el) => merge(el, { use: true })) + return { + ...prevState, + core: { + ...prevState.core, + data: newData, + output: newData.filter((x) => x.use).map((x) => x.id), + }, + } + }) + + const useClick$ = sources.DOM.select(".selection") + .events("click", { preventDefault: true }) + .map((ev) => ev.ownerTarget.id) + + const aDown$ = sources.DOM.select("document") + .events("keydown") + .map((ev) => ev.code) + .filter((code) => code == "AltLeft") + .mapTo(true) + .startWith(false) + + // A modifier stream + const aUp$ = sources.DOM.select("document") + .events("keyup") + .map((ev) => ev.code) + .filter((code) => code == "AltLeft") + .mapTo(false) + + const a$ = xs.merge(aDown$, aUp$).compose(dropRepeats(equals)) + + const selectReducer$ = useClick$ + .compose(sampleCombine(a$)) + .map(([id, a]) => (prevState) => { + // a = false is the usual behavior + if (!a) { + const newData = prevState.core.data.map((el) => { + // One sample object + var newEl = clone(el) + const switchUse = id === el.id + newEl.use = switchUse ? !el.use : el.use + // console.log(el) + // console.log(newEl) + return newEl + }) return { - ...prevState, - core: { - ...prevState.core, - data: newData, - output: newData.filter(x => x.use).map(x => x.id) - } + ...prevState, + core: { + ...prevState.core, + data: newData, + output: newData.filter((x) => x.use).map((x) => x.id), + }, } - }) - - const useClick$ = sources.DOM - .select('.selection') - .events('click', { preventDefault: true } ) - .map(ev => ev.ownerTarget.id) - - const aDown$ = - sources.DOM.select('document') - .events('keydown') - .map(ev => ev.code) - .filter(code => code == "KeyA") - .mapTo(true) - .startWith(false) - - // A modifier stream - const aUp$ = - sources.DOM.select('document') - .events('keyup') - .map(ev => ev.code) - .filter(code => code == "KeyA") - .mapTo(false) - - const a$ = - xs.merge(aDown$, aUp$) - .compose(dropRepeats(equals)) - - const selectReducer$ = - useClick$ - .compose(sampleCombine(a$)) - .map(([id, a]) => prevState => { - // a = false is the usual behavior - if (!a) { - const newData = prevState.core.data.map(el => { - // One sample object - var newEl = clone(el) - const switchUse = (id === el.id) - newEl.use = (switchUse) ? !el.use : el.use - // console.log(el) - // console.log(newEl) - return newEl - }) - return ({ - ...prevState, - core: { - ...prevState.core, - data: newData, - output: newData.filter(x => x.use).map(x => x.id) - } - }) - } else { - const newData = prevState.core.data.map(el => { - // One sample object - var newEl = clone(el) - newEl.use = !el.use - return newEl - }) - return ({ - ...prevState, - core: { - ...prevState.core, - data: newData, - output: newData.filter(x => x.use).map(x => x.id) - } - }) - + } else { + const newData = prevState.core.data.map((el) => { + // One sample object + var newEl = clone(el) + newEl.use = !el.use + return newEl + }) + return { + ...prevState, + core: { + ...prevState.core, + data: newData, + output: newData.filter((x) => x.use).map((x) => x.id), + }, } + } }) - const defaultReducer$ = xs.of(prevState => ({...prevState, core: { input: '', data: [] } })) - const inputReducer$ = input$.map(i => prevState => ({...prevState, core: {...prevState.core, input: i } })) - const requestReducer$ = request$.map(req => prevState => ({...prevState, core: {...prevState.core, request: req } })) - - const sampleSelection$ = - xs.merge( - sources.DOM.select('.doSelect').events('click'), - // Ghost mode - sources.onion.state$.map(state => state.core.ghostoutput).filter(ghost => ghost).compose(dropRepeats()) - ) - .compose(sampleCombine(state$)) - .map(([ev, state]) => state.core.output) - - return { - log: xs.merge( - logger(state$, 'state$'), - ), - DOM: vdom$, - HTTP: xs.merge( - request$, - compoundAnnotations.HTTP - ), - onion: xs.merge( - defaultReducer$, - inputReducer$, - requestReducer$, - dataReducer$, - selectReducer$ - ), - output: sampleSelection$, - modal: compoundAnnotations.modal - } + const defaultReducer$ = xs.of((prevState) => ({ + ...prevState, + core: { input: "", data: [] }, + })) + const inputReducer$ = input$.map((i) => (prevState) => ({ + ...prevState, + core: { ...prevState.core, input: i }, + })) + const requestReducer$ = request$.map((req) => (prevState) => ({ + ...prevState, + core: { ...prevState.core, request: req }, + })) + + const sampleSelection$ = xs + .merge( + sources.DOM.select(".doSelect").events("click"), + // Ghost mode + sources.onion.state$ + .map((state) => state.core.ghostoutput) + .filter((ghost) => ghost) + .compose(dropRepeats()) + ) + .compose(sampleCombine(state$)) + .map(([ev, state]) => state.core.output) + + return { + log: xs.merge(logger(state$, "state$")), + DOM: vdom$, + HTTP: xs.merge(request$, compoundAnnotations.HTTP), + onion: xs.merge( + defaultReducer$, + inputReducer$, + requestReducer$, + dataReducer$, + selectReducer$ + ), + output: sampleSelection$, + modal: compoundAnnotations.modal, + } } export { SampleSelection, sampleSelectionLens } From 5b77e4d37b83aa8c61acc75e7836a6ef358ef9b2 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Wed, 6 Oct 2021 10:07:22 +0200 Subject: [PATCH 029/191] Bump version to 5.0.0-alpha4 --- CHANGELOG.md | 15 +++++++++++++++ package.json | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..beb09794 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# CHANGELOG + +## Version 5.0.0-alpha4 + +## Functionality + +- A new 'Genetic' workflow is created useful for searching for genetic perturbations. +- Inverting the filter selection can be done using the `ALT` (Option on Mac) modifier key instead of the `a` key +- Filter values are populated dynamically based on the data available through the API + +## Other + +- The dependency stack has been cleaned and (partly) updated +- Cycle dependencies have been updated to the latest versions except for `@cycle/state`. + diff --git a/package.json b/package.json index c4eaeff8..6e513214 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "LuciusWeb", - "version": "5.0.0-alpha3", + "version": "5.0.0-alpha4", "description": "Web interface for ComPass aka Lucius", "repository": { "type": "git", From c1d81a2445ec8323013967c3b16eb1b75fb36412 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 18 Oct 2021 16:58:55 +0200 Subject: [PATCH 030/191] Add filter in TreatmentCheck to select between compound-like and genetic-like Change CompoundForm to use the adapted TreatmentCheck component instead of CompoundCheck Change TreatmentForm to use the adapted TreatmentCheck component --- src/js/components/CompoundForm.js | 6 +++--- src/js/components/TreatmentCheck.js | 14 ++++++++++---- src/js/components/TreatmentForm.js | 4 ++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/js/components/CompoundForm.js b/src/js/components/CompoundForm.js index 60564507..9c33180a 100644 --- a/src/js/components/CompoundForm.js +++ b/src/js/components/CompoundForm.js @@ -1,7 +1,7 @@ import isolate from "@cycle/isolate" import { div } from "@cycle/dom" import xs from "xstream" -import { CompoundCheck, checkLens } from "./CompoundCheck" +import { TreatmentCheck, checkLens, treatmentLikeFilter } from "./TreatmentCheck" import { SampleSelection, sampleSelectionLens } from "./SampleSelection" import { SignatureGenerator, signatureLens } from "./SignatureGenerator" import { loggerFactory } from "../utils/logger" @@ -15,8 +15,8 @@ function CompoundForm(sources) { const state$ = sources.onion.state$ - const CompoundCheckSink = isolate(CompoundCheck, { onion: checkLens })( - sources + const CompoundCheckSink = isolate(TreatmentCheck, { onion: checkLens })( + sources, treatmentLikeFilter.COMPOUND ) const compoundQuery$ = CompoundCheckSink.output.remember() diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index 094865ef..9e892139 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -17,13 +17,19 @@ const checkLens = { }), } +const treatmentLikeFilter = { + COMPOUND : "compound", + GENETIC : "genetic", + COMPOUND_AND_GENETIC : "compound genetic" +} + /** * Form for entering treatments with autocomplete. * * Input: Form input * Output: treatment (string) */ -function TreatmentCheck(sources) { +function TreatmentCheck(sources, likeFilter) { // States of autosuggestion field: // - Less than N characters -> no query, no suggestions // - N or more -> with every character a query is done (after 500ms). suggestions are shown @@ -89,7 +95,7 @@ function TreatmentCheck(sources) { .filter((state) => state.core.input.length >= 1) .filter((state) => state.core.showSuggestions) .compose(debounce(200)) - + const request$ = triggerRequest$.map((state) => { return { url: @@ -99,7 +105,7 @@ function TreatmentCheck(sources) { send: { version: "v2", query: state.core.input, - like: "genetic" + like: likeFilter }, category: "treatments", } @@ -297,4 +303,4 @@ function TreatmentCheck(sources) { } } -export { TreatmentCheck, checkLens } +export { TreatmentCheck, checkLens, treatmentLikeFilter } diff --git a/src/js/components/TreatmentForm.js b/src/js/components/TreatmentForm.js index 06847099..139ba71c 100644 --- a/src/js/components/TreatmentForm.js +++ b/src/js/components/TreatmentForm.js @@ -1,7 +1,7 @@ import isolate from "@cycle/isolate" import { div } from "@cycle/dom" import xs from "xstream" -import { TreatmentCheck, checkLens } from "./TreatmentCheck" +import { TreatmentCheck, checkLens, treatmentLikeFilter } from "./TreatmentCheck" import { SampleSelection, sampleSelectionLens } from "./SampleSelection" import { SignatureGenerator, signatureLens } from "./SignatureGenerator" import { loggerFactory } from "../utils/logger" @@ -16,7 +16,7 @@ function TreatmentForm(sources) { const state$ = sources.onion.state$ const TreatmentCheckSink = isolate(TreatmentCheck, { onion: checkLens })( - sources + sources, treatmentLikeFilter.GENETIC ) const treatmentQuery$ = TreatmentCheckSink.output.remember() From 46980cb7d88b2c4a35ff6e77cc9def7ef818951c Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 22 Oct 2021 10:55:05 +0200 Subject: [PATCH 031/191] Move treatmentLikeFilter into settings and move upstream to main compound or genetic pages CompoundCheck and CompoundForm are no longer used, and in case of CompoundForm there were only const naming differences CompoundCheck had small differences in code compared to TreatmentCheck but TreatmentCheck is just a newer version So removed CompoundCheck and CompoundForm --- src/js/components/CompoundCheck.js | 270 ---------------------------- src/js/components/CompoundForm.js | 79 -------- src/js/components/TreatmentCheck.js | 4 +- src/js/components/TreatmentForm.js | 4 +- src/js/pages/compound.js | 19 +- src/js/pages/genetic.js | 3 +- 6 files changed, 16 insertions(+), 363 deletions(-) delete mode 100644 src/js/components/CompoundCheck.js delete mode 100644 src/js/components/CompoundForm.js diff --git a/src/js/components/CompoundCheck.js b/src/js/components/CompoundCheck.js deleted file mode 100644 index b20a5447..00000000 --- a/src/js/components/CompoundCheck.js +++ /dev/null @@ -1,270 +0,0 @@ -import sampleCombine from 'xstream/extra/sampleCombine' -import { i, p, div, br, label, input, code, table, tr, td, b, h2, button, textarea, a, ul, li, span } from '@cycle/dom'; -import { clone, equals, mergeAll } from 'ramda'; -import xs from 'xstream'; -import { logThis, log } from '../utils/logger' -import { ENTER_KEYCODE } from '../utils/keycodes.js' -import dropRepeats from 'xstream/extra/dropRepeats' -import debounce from 'xstream/extra/debounce' -import { loggerFactory } from '../utils/logger' -import { titleCase } from '../utils/utils' - -const checkLens = { - get: state => ({ core: (typeof state.form !== 'undefined') ? state.form.check : {}, settings: state.settings }), - set: (state, childState) => ({ ...state, form: { ...state.form, check: childState.core } }) -} - -/** - * Form for entering compounds with autocomplete. - * - * Input: Form input - * Output: compound (string) - */ -function CompoundCheck(sources) { - // States of autosuggestion field: - // - Less than N characters -> no query, no suggestions - // - N or more -> with every character a query is done (after 500ms). suggestions are shown - // - Clicking on a suggestion activates it in the search field and sets validated to true - // - At that point, the dropdown should dissapear!!! - // - The suggestions appear again whenever something changes in the input... - - const logger = loggerFactory('compoundCheck', sources.onion.state$, 'settings.form.debug') - - const state$ = sources.onion.state$ - - const acInput$ = sources.ac - - const input$ = xs.merge( - sources.DOM - .select('.compoundQuery') - .events('input') - .map(ev => ev.target.value) - .startWith(''), - // This for ghost mode, inject changes via external state updates... - state$.filter(state => typeof state.core.ghostinput !== 'undefined').map(state => state.core.input).compose(dropRepeats()) - ) - - // When the component should not be shown, including empty signature - const isEmptyState = (state) => { - if (typeof state.core === 'undefined') { - return true - } else { - if (typeof state.core.input === 'undefined') { - return true - } else { - return false - } - } - } - - const emptyState$ = state$ - .filter(state => isEmptyState(state)) - .compose(dropRepeats(equals)) - - // When the state is cycled because of an internal update - const modifiedState$ = state$ - .filter(state => !isEmptyState(state)) - .compose(dropRepeats((x, y) => equals(x, y))) - .remember() - - // An update to the input$, join it with state$ - const newInput$ = xs.combine( - input$, - modifiedState$ - ) - .map(([newinput, state]) => ({ ...state, core: { ...state.core, input: newinput } })) - .compose(dropRepeats((x, y) => equals(x.core.input, y.core.input))) - - const triggerRequest$ = newInput$ - .filter(state => state.core.input.length >= 2) - .filter(state => state.core.showSuggestions) - .compose(debounce(500)) - - const request$ = triggerRequest$ - .map(state => { - return { - url: state.settings.api.url + '&classPath=com.dataintuitive.luciusapi.compounds', - method: 'POST', - send: { - version: 'v2', - query: state.core.input - }, - 'category': 'compounds' - } - }) - - const response$ = sources.HTTP - .select('compounds') - .map((response$) => - response$.replaceError(() => xs.of([])) - ) - .flatten() - - const data$ = response$ - .map(res => res.body.result.data) - .remember() - - const initVdom$ = emptyState$ - .mapTo(div()) - - const loadedVdom$ = modifiedState$ - .map(state => { - const query = state.core.input - const validated = state.core.validated - return div( - [ - div('.row .orange .darken-4 .white-text', { style: { padding: '20px 10px 10px 10px' } }, [ - div('.Default .waves-effect .col .s1 .center-align', [ - i('.large .center-align .material-icons .orange-text', { style: { fontSize: '45px', fontColor: 'gray' } }, 'search'), - ]), - div('.col .s10 .input-field', { style: { margin: '0px 0px 0px 0px' } }, [ - input('.compoundQuery.col .s12 .autocomplete-input .white-text', - { style: { fontSize: '20px' }, props: { type: 'text', value: query }, value: query }), - ]), - (validated) - ? div('.CompoundCheck .waves-effect .col .s1 .center-align', [ - i('.large .material-icons', { style: { fontSize: '45px', fontColor: 'grey' } }, ['play_arrow'])]) - : div('.CompoundCheck .col .s1 .center-align', [ - i('.large .material-icons .orange-text', { style: { fontSize: '45px', fontColor: 'grey' } }, 'play_arrow')]), - ]), - ]) - }) - - const vdom$ = xs.merge(initVdom$, loadedVdom$).startWith(div()) - - // Set a initial reducer, showing suggestions - const defaultReducer$ = xs.of(prevState => { - // CompoundCheck -- defaultReducer$') - let newState = { - ...prevState, - core: { - ...prevState.core, - showSuggestions: true, - validated: false, - input: '', - data: [], - } - } - return newState - }) - - // Reducer for showing suggestions again after an input event - const inputReducer$ = input$.map(value => prevState => - ({ - ...prevState, - core: { - ...prevState.core, - showSuggestions: true, - validated: false, - input: value, - } - }) - ) - - // Set a default signature for demo purposes - const setDefault$ = sources.DOM.select('.Default').events('click') - const setDefaultReducer$ = - setDefault$ - .compose(sampleCombine(state$)) - .map(([_, state]) => prevState => - ({ - ...prevState, core: { - ...prevState.core, - showSuggestions: false, - validated: true, - input: state.settings.common.hourglass.compound, - } - }) - ) - - // Add request body to state - const requestReducer$ = request$.map(req => prevState => - ({ ...prevState, core: { ...prevState.core, request: req } }) - ) - - // Add data from API to state, update output key when relevant - const dataReducer$ = data$.map(newData => prevState => - ({ ...prevState, core: { ...prevState.core, data: newData } }) - ) - - // Whenever more than 1 option is available per our compound query, we list the options. - const ac$ = data$ - .filter(data => data.length > 1) - .map(data => ({ - el: '.compoundQuery', - data: data, - render: function (data) { return mergeAll(data.map(d => ({ [d.compound_id + ' - ' + d.name]: null }))) }, - strip: function (str) { - return str; //.split(" - ")[0]; - } - })) - - // Trigger an update when only one result is left so we can handle that in the AutoComplete driver - const acOneSolution$ = data$ - .filter(data => data.length == 1) - .map(data => ({ - el: '.compoundQuery', - data: data, - render: function (data) { return mergeAll(data.map(d => ({ [d.compound_id + ' - ' + d.name]: null }))) }, - strip: function (str) { - return str; //.split(" - ")[0]; - } - })) - - // When a suggestion is clicked, update the state so the query becomes this - const autocompleteReducer$ = xs.merge( - // input from autocomplete (clicking an option) - acInput$, - // input from having one solution left in the autocomplete, extract the remaning target - acOneSolution$.map(info => info.data[0].compound_id) - ) - .map(input => prevState => { - const newInput = input - return ({ - ...prevState, core: { - ...prevState.core, - input: newInput, - showSuggestions: false, - validated: true, - output: newInput, - } - }) - }) - - // GO!!! - const run$ = sources.DOM - .select('.CompoundCheck') - .events('click') - - const query$ = xs.merge( - run$, - // Ghost mode - sources.onion.state$.map(state => state.core.ghostoutput).filter(ghost => ghost).compose(dropRepeats()) - ) - .compose(sampleCombine(state$)) - .map(([_, state]) => state.core.input) - .remember() - - // const history$ = sources.onion.state$.fold((acc, x) => acc.concat([x]), [{}]) - - return { - log: xs.merge( - logger(state$, 'state$') - // logger(history$, 'history$'), - ), - HTTP: request$, - DOM: vdom$, - onion: xs.merge( - defaultReducer$, - inputReducer$, - dataReducer$, - requestReducer$, - setDefaultReducer$, - autocompleteReducer$ - ), - output: query$, - ac: xs.merge(ac$, acOneSolution$) - }; -} - -export { CompoundCheck, checkLens } diff --git a/src/js/components/CompoundForm.js b/src/js/components/CompoundForm.js deleted file mode 100644 index 9c33180a..00000000 --- a/src/js/components/CompoundForm.js +++ /dev/null @@ -1,79 +0,0 @@ -import isolate from "@cycle/isolate" -import { div } from "@cycle/dom" -import xs from "xstream" -import { TreatmentCheck, checkLens, treatmentLikeFilter } from "./TreatmentCheck" -import { SampleSelection, sampleSelectionLens } from "./SampleSelection" -import { SignatureGenerator, signatureLens } from "./SignatureGenerator" -import { loggerFactory } from "../utils/logger" - -function CompoundForm(sources) { - const logger = loggerFactory( - "compoundForm", - sources.onion.state$, - "settings.debug" - ) - - const state$ = sources.onion.state$ - - const CompoundCheckSink = isolate(TreatmentCheck, { onion: checkLens })( - sources, treatmentLikeFilter.COMPOUND - ) - const compoundQuery$ = CompoundCheckSink.output.remember() - - const SampleSelectionSink = isolate(SampleSelection, { - onion: sampleSelectionLens, - })({ ...sources, input: compoundQuery$ }) - const sampleSelection$ = SampleSelectionSink.output.remember() - - const SignatureGeneratorSink = isolate(SignatureGenerator, { - onion: signatureLens, - })({ ...sources, input: sampleSelection$ }) - const signature$ = SignatureGeneratorSink.output.remember() - - const vdom$ = xs - .combine( - CompoundCheckSink.DOM.startWith(div()), - SampleSelectionSink.DOM, - SignatureGeneratorSink.DOM - ) - .map(([formDom, selectionDOM, signatureDOM]) => - div([ - formDom, - selectionDOM, - div(".col .s10 .offset-s1", [ - div(".row", [div(".col .s12", [signatureDOM])]), - ]), - ]) - ) - - const defaultReducer$ = xs.of((prevState) => { - // CompoundForm -- default Reducer - return { ...prevState, form: {}, sampleSelection: {}, signature: {} } - }) - - return { - log: xs.merge( - logger(state$, "state$"), - CompoundCheckSink.log, - SampleSelectionSink.log, - SignatureGeneratorSink.log - ), - DOM: vdom$, - onion: xs.merge( - defaultReducer$, - CompoundCheckSink.onion, - SampleSelectionSink.onion, - SignatureGeneratorSink.onion - ), - HTTP: xs.merge( - CompoundCheckSink.HTTP, - SampleSelectionSink.HTTP, - SignatureGeneratorSink.HTTP - ), - output: signature$, - modal: xs.merge(SignatureGeneratorSink.modal, SampleSelectionSink.modal), - ac: CompoundCheckSink.ac, - } -} - -export { CompoundForm } diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index 9e892139..3173869e 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -29,7 +29,7 @@ const treatmentLikeFilter = { * Input: Form input * Output: treatment (string) */ -function TreatmentCheck(sources, likeFilter) { +function TreatmentCheck(sources) { // States of autosuggestion field: // - Less than N characters -> no query, no suggestions // - N or more -> with every character a query is done (after 500ms). suggestions are shown @@ -105,7 +105,7 @@ function TreatmentCheck(sources, likeFilter) { send: { version: "v2", query: state.core.input, - like: likeFilter + like: state.settings.treatmentLike }, category: "treatments", } diff --git a/src/js/components/TreatmentForm.js b/src/js/components/TreatmentForm.js index 139ba71c..1d58bd70 100644 --- a/src/js/components/TreatmentForm.js +++ b/src/js/components/TreatmentForm.js @@ -16,7 +16,7 @@ function TreatmentForm(sources) { const state$ = sources.onion.state$ const TreatmentCheckSink = isolate(TreatmentCheck, { onion: checkLens })( - sources, treatmentLikeFilter.GENETIC + sources ) const treatmentQuery$ = TreatmentCheckSink.output.remember() @@ -76,4 +76,4 @@ function TreatmentForm(sources) { } } -export { TreatmentForm } +export { TreatmentForm, treatmentLikeFilter } diff --git a/src/js/pages/compound.js b/src/js/pages/compound.js index f7dc84a2..d3c7cb7d 100644 --- a/src/js/pages/compound.js +++ b/src/js/pages/compound.js @@ -1,7 +1,7 @@ import { div } from "@cycle/dom" import xs from "xstream" import isolate from "@cycle/isolate" -import { CompoundForm } from "../components/CompoundForm" +import { TreatmentForm, treatmentLikeFilter } from "../components/TreatmentForm" import { initSettings } from "../configuration.js" import { makeTable, headTableLens, tailTableLens } from "../components/Table" import { BinnedPlots, plotsLens } from "../components/BinnedPlots/BinnedPlots" @@ -48,6 +48,7 @@ export default function CompoundWorkflow(sources) { common: state.settings.common, geneAnnotations: state.settings.geneAnnotations, compoundAnnotations: state.settings.compoundAnnotations, + treatmentLike: treatmentLikeFilter.COMPOUND, }, }), set: (state, childState) => ({ ...state, form: childState.form }), @@ -68,8 +69,8 @@ export default function CompoundWorkflow(sources) { } }) - const CompoundFormSink = isolate(CompoundForm, { onion: formLens })(sources) - const signature$ = CompoundFormSink.output.remember() + const TreatmentFormSink = isolate(TreatmentForm, { onion: formLens })(sources) + const signature$ = TreatmentFormSink.output.remember() // Filter Form const filterForm = isolate(Filter, { onion: filterLens })({ @@ -120,7 +121,7 @@ export default function CompoundWorkflow(sources) { const vdom$ = xs .combine( - CompoundFormSink.DOM, + TreatmentFormSink.DOM, filterForm.DOM, binnedPlots.DOM, headTable.DOM, @@ -143,7 +144,7 @@ export default function CompoundWorkflow(sources) { return { log: xs.merge( logger(state$, "state$"), - CompoundFormSink.log, + TreatmentFormSink.log, filterForm.log, binnedPlots.log, headTable.log, @@ -152,7 +153,7 @@ export default function CompoundWorkflow(sources) { DOM: vdom$.startWith(div()), onion: xs.merge( defaultReducer$, - CompoundFormSink.onion, + TreatmentFormSink.onion, binnedPlots.onion, filterForm.onion, headTable.onion, @@ -160,7 +161,7 @@ export default function CompoundWorkflow(sources) { scenarioReducer$ ), HTTP: xs.merge( - CompoundFormSink.HTTP, + TreatmentFormSink.HTTP, filterForm.HTTP, binnedPlots.HTTP, headTable.HTTP, @@ -168,7 +169,7 @@ export default function CompoundWorkflow(sources) { ), vega: binnedPlots.vega, popup: scenarioPopup$, - modal: xs.merge(CompoundFormSink.modal), - ac: CompoundFormSink.ac, + modal: xs.merge(TreatmentFormSink.modal), + ac: TreatmentFormSink.ac, } } diff --git a/src/js/pages/genetic.js b/src/js/pages/genetic.js index e8987dea..6c309d1e 100644 --- a/src/js/pages/genetic.js +++ b/src/js/pages/genetic.js @@ -1,7 +1,7 @@ import { div } from "@cycle/dom" import xs from "xstream" import isolate from "@cycle/isolate" -import { TreatmentForm} from "../components/TreatmentForm" +import { TreatmentForm, treatmentLikeFilter } from "../components/TreatmentForm" import { initSettings } from "../configuration.js" import { makeTable, headTableLens, tailTableLens } from "../components/Table" import { BinnedPlots, plotsLens } from "../components/BinnedPlots/BinnedPlots" @@ -48,6 +48,7 @@ export default function GeneticWorkflow(sources) { common: state.settings.common, geneAnnotations: state.settings.geneAnnotations, compoundAnnotations: state.settings.compoundAnnotations, + treatmentLike: treatmentLikeFilter.GENETIC, }, }), set: (state, childState) => ({ ...state, form: childState.form }), From cd340cdd0a7931adcc627dbdd77da2fa487cb83d Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Tue, 9 Nov 2021 15:24:47 +0100 Subject: [PATCH 032/191] Delay updating the state from filterFetch to avoid race conditions --- src/js/components/FetchFilters.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/components/FetchFilters.js b/src/js/components/FetchFilters.js index a998ecd2..de81a806 100644 --- a/src/js/components/FetchFilters.js +++ b/src/js/components/FetchFilters.js @@ -1,5 +1,5 @@ import xs from 'xstream' -import debounce from 'xstream/extra/debounce' +import delay from 'xstream/extra/delay' function FetchFilters(sources) { @@ -31,7 +31,7 @@ function FetchFilters(sources) { return { // We don't initialize the stream here so we know exactly when the // information is available. - filters: validResponse$, + filters: validResponse$.compose(delay(2000)), HTTP: request$ } } From 551f2037cdad4f66e0895ecfbaaee893051e834d Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Tue, 9 Nov 2021 19:11:18 +0100 Subject: [PATCH 033/191] Fix Admin settings page and layout improvements --- src/js/components/Filter.js | 3 - src/js/pages/adminSettings.js | 747 ++++++++++++++++++---------------- src/js/pages/settings.js | 468 +++++++++++---------- 3 files changed, 646 insertions(+), 572 deletions(-) diff --git a/src/js/components/Filter.js b/src/js/components/Filter.js index 18dcb026..38cb3fed 100644 --- a/src/js/components/Filter.js +++ b/src/js/components/Filter.js @@ -184,9 +184,6 @@ export function model( filter_output: {}, state: {dose: false, cell: false, trtType: false}, }, - settings: { - filter: {values: {}} - } })) // When the query for the current filter values returns we want to update diff --git a/src/js/pages/adminSettings.js b/src/js/pages/adminSettings.js index bda73f89..c0b14d6b 100644 --- a/src/js/pages/adminSettings.js +++ b/src/js/pages/adminSettings.js @@ -1,393 +1,454 @@ -import { select, option, a, div, br, label, input, p, button, code, pre, h2, h4, i, h3, h5, span, ul, li } from '@cycle/dom' -import xs from 'xstream' -import isolate from '@cycle/isolate' -import { mergeWith, merge, props, keys, cond } from 'ramda' -import * as R from 'ramda' -import { pick, mix } from 'cycle-onionify' -import debounce from 'xstream/extra/debounce' -import dropRepeats from 'xstream/extra/dropRepeats' +import { + div, + label, + input, + button, + h4, + span, + ul, + li, +} from "@cycle/dom" +import xs from "xstream" +import isolate from "@cycle/isolate" +import { mergeWith, merge, props, keys, cond } from "ramda" +import * as R from "ramda" +import { pick, mix } from "cycle-onionify" +import debounce from "xstream/extra/debounce" +import dropRepeats from "xstream/extra/dropRepeats" export function IsolatedAdminSettings(sources) { - return isolate(AdminSettings, 'settings')(sources) + return isolate(AdminSettings, "settings")(sources) } export function AdminSettings(sources) { + const settings$ = sources.onion.state$ - const settings$ = sources.onion.state$ - - const settingsConfig$ = sources.deployments.map(deployments => ([ + const settingsConfig$ = sources.deployments.map((deployments) => [ + { + group: "deployment", + title: "Deployment", + settings: [ { - group: 'deployment', - title: 'Deployment', - settings: [{ - field: 'name', - type: 'select', - class: '.switch', - title: 'Deployment', - options: deployments.map(x => x.name), - props: { type: 'checkbox' } - } - ] + field: "name", + type: "select", + class: ".switch", + title: "Deployment", + options: deployments.map((x) => x.name), + props: { type: "checkbox" }, }, + ], + }, + { + group: "common", + title: "Common Settings", + settings: [ { - group: 'common', - title: 'Common Settings', - settings: [{ - field: 'version', - type: 'text', - class: '.input-field', - title: 'API Version', - props: { type: 'text' } - }, - { - field: 'debug', - type: 'checkbox', - class: '.switch', - title: 'Debug App?', - props: { type: 'checkbox' } - } - ] + field: "version", + type: "text", + class: ".input-field", + title: "API Version", + props: { type: "text" }, }, { - group: 'api', - title: 'API Settings', - settings: [{ - field: 'url', - class: '.input-field', - type: 'text', - title: 'LuciusAPI URL', - props: {} - }] + field: "debug", + type: "checkbox", + class: ".switch", + title: "Debug App?", + props: { type: "checkbox" }, }, + ], + }, + { + group: "api", + title: "API Settings", + settings: [ { - group: 'sourire', - title: 'Sourire Settings', - settings: [{ - field: 'url', - class: '.input-field', - type: 'text', - title: 'Sourire URL', - props: {} - }] + field: "url", + class: ".input-field", + type: "text", + title: "LuciusAPI URL", + props: {}, }, + ], + }, + { + group: "sourire", + title: "Sourire Settings", + settings: [ { - group: 'stats', - title: 'Statistics Settings', - settings: [{ - field: 'endpoint', - class: '.input-field', - type: 'text', - title: 'Statistics URL', - props: {} - }] + field: "url", + class: ".input-field", + type: "text", + title: "Sourire URL", + props: {}, }, + ], + }, + { + group: "stats", + title: "Statistics Settings", + settings: [ { - group: 'geneAnnotations', - title: 'Gene Annotation Settings', - settings: [{ - field: 'debug', - type: 'checkbox', - class: '.switch', - title: 'Debug component?', - props: { type: 'checkbox' } - }, { - field: 'url', - class: '.input-field', - type: 'text', - title: 'URL for Gene Annotations', - props: {} - }] + field: "endpoint", + class: ".input-field", + type: "text", + title: "Statistics URL", + props: {}, }, - { - group: 'compoundAnnotations', - title: 'Compound Annotation Settings', - settings: [ + ], + }, + { + group: "geneAnnotations", + title: "Gene Annotation Settings", + settings: [ { - field: 'version', - type: 'text', - class: '.input-field', - title: 'API Version', - props: { type: 'text' } + field: "debug", + type: "checkbox", + class: ".switch", + title: "Debug component?", + props: { type: "checkbox" }, }, { - field: 'debug', - type: 'checkbox', - class: '.switch', - title: 'Debug component?', - props: { type: 'checkbox' } + field: "url", + class: ".input-field", + type: "text", + title: "URL for Gene Annotations", + props: {}, }, + ], + }, + { + group: "compoundAnnotations", + title: "Compound Annotation Settings", + settings: [ { - field: 'url', - class: '.input-field', - type: 'text', - title: 'URL for Compound Annotations', - props: {} - }] - }, + field: "version", + type: "text", + class: ".input-field", + title: "API Version", + props: { type: "text" }, + }, { - group: 'compoundTable', - title: 'Compound Table Settings', - settings: [{ - field: 'debug', - type: 'checkbox', - class: '.switch', - title: 'Debug component?', - props: { type: 'checkbox' } - } - ] + field: "debug", + type: "checkbox", + class: ".switch", + title: "Debug component?", + props: { type: "checkbox" }, }, { - group: 'headTable', - title: 'Top Table Settings', - settings: [{ - field: 'debug', - type: 'checkbox', - class: '.switch', - title: 'Debug component?', - props: { type: 'checkbox' } - } - ] + field: "url", + class: ".input-field", + type: "text", + title: "URL for Compound Annotations", + props: {}, }, + ], + }, + { + group: "compoundTable", + title: "Compound Table Settings", + settings: [ { - group: 'tailTable', - title: 'Bottom Table Settings', - settings: [{ - field: 'debug', - type: 'checkbox', - class: '.switch', - title: 'Debug component?', - props: { type: 'checkbox' } - } - ] + field: "debug", + type: "checkbox", + class: ".switch", + title: "Debug component?", + props: { type: "checkbox" }, }, + ], + }, + { + group: "headTable", + title: "Top Table Settings", + settings: [ { - group: 'plots', - title: 'Combined (binned) plots', - settings: [{ - field: 'debug', - type: 'checkbox', - class: '.switch', - title: 'Debug component?', - props: { type: 'checkbox' } - } - ] + field: "debug", + type: "checkbox", + class: ".switch", + title: "Debug component?", + props: { type: "checkbox" }, }, + ], + }, + { + group: "tailTable", + title: "Bottom Table Settings", + settings: [ { - group: 'form', - title: 'Form Settings', - settings: [{ - field: 'debug', - type: 'checkbox', - class: '.switch', - title: 'Debug component?', - props: { type: 'checkbox' } - }] + field: "debug", + type: "checkbox", + class: ".switch", + title: "Debug component?", + props: { type: "checkbox" }, }, + ], + }, + { + group: "plots", + title: "Combined (binned) plots", + settings: [ { - group: 'filter', - title: 'Filter Settings', - settings: [{ - field: 'debug', - type: 'checkbox', - class: '.switch', - title: 'Debug component?', - props: { type: 'checkbox' } - }] - } - ])) - - // Depending on the type of config settings, render the appropriate vdom representation - function renderField(config, _state) { - if (config.type == 'checkbox') { - return ( - [ - label('.active', [ - input({ props: merge(config.props, { checked: _state }) }), - span('.lever') - ]) - ] - ) - } - if (config.type == 'text') { - return ( - [ - input({ props: merge(config.props, { value: _state }) }) - ] + field: "debug", + type: "checkbox", + class: ".switch", + title: "Debug component?", + props: { type: "checkbox" }, + }, + ], + }, + { + group: "form", + title: "Form Settings", + settings: [ + { + field: "debug", + type: "checkbox", + class: ".switch", + title: "Debug component?", + props: { type: "checkbox" }, + }, + ], + }, + { + group: "filter", + title: "Filter Settings", + settings: [ + { + field: "debug", + type: "checkbox", + class: ".switch", + title: "Debug component?", + props: { type: "checkbox" }, + }, + ], + }, + ]) + + // Depending on the type of config settings, render the appropriate vdom representation + function renderField(config, _state) { + if (config.type == "checkbox") { + return [ + label(".active", [ + input({ props: merge(config.props, { checked: _state }) }), + span(".lever"), + ]), + ] + } + if (config.type == "text") { + return [input({ props: merge(config.props, { value: _state }) })] + } + if (config.type == "select") { + const options = config.options + const selectedOption = (option) => + _state == option + ? ".grey.lighten-3.black-text" + : ".grey.lighten-3.grey-text.text-lighten-1" + const optionButtons = options.map((o) => + div( + ".col.selection" + selectedOption(o) + "." + o, + { style: { "border-style": "solid", margin: "2px" } }, + [ + label(selectedOption(o), [ + input("", { props: merge(config.props, { value: o }) }, ""), + o, + ]), + ] ) - } - if (config.type == 'select') { - const options = config.options - const selectedOption = (option) => (_state == option) ? '.grey.lighten-3.black-text' : '.grey.lighten-3.grey-text.text-lighten-1' - const optionButtons = options.map(o => - div( - '.col.selection'+selectedOption(o)+'.'+o, - { style : { 'border-style' : 'solid', 'margin' : '2px'}}, [ - label(selectedOption(o), [ - input('', { props : merge(config.props, { value: o}) }, '' ), - o - ]) - ] - ) - ) - return optionButtons - } - + ) + return optionButtons } + } + + const makeSetting = (config) => (sources) => { + const state$ = sources.onion.state$ + + const update$ = R.cond([ + [ + R.equals("checkbox"), + (_) => + sources.DOM.select("input") + .events("click") + .map((event) => event), + ], + [ + R.equals("text"), + (_) => sources.DOM.events("input").map((event) => event.target.value), + ], + [ + R.equals("select"), + (_) => + sources.DOM.select("input") + .events("click") + .map((event) => event.target.value), + ], + ])(config.type) + + const vdom$ = state$.map((state) => + li( + ".collection-item .row", + div(".valign-wrapper", [ + span(".col .l6 .s12 .truncate", [ + span(".flow-text", [config.title]), + span([" "]), + span(".grey-text .text-lighten-1 .right-align", [ + "(", + state.toString(), + ")", + ]), + ]), + + div(".col .s6 " + config.class, renderField(config, state)), + ]) + ) + ) + + const updateReducer$ = cond([ + [ + R.equals("checkbox"), + (_) => update$.map((_) => (prevState) => !prevState), + ], + [R.equals("text"), (_) => update$.map((update) => (_) => update)], + [ + R.equals("select"), + (_) => update$.map((update) => (_) => update), + ], + ])(config.type) - const makeSetting = (config) => (sources) => { - const state$ = sources.onion.state$ - - const update$ = R.cond([ - [R.equals('checkbox'), _ => sources.DOM.select('input').events('click').map(event => event) ], - [R.equals('text'), _ => sources.DOM.events('input').map(event => event.target.value) ], - [R.equals('select'), _ => sources.DOM.select('input').events('click').map(event => event.target.value)] - ])(config.type) - - const vdom$ = - state$.map(state => - li('.collection-item .row', - div('.valign-wrapper', [ - span('.col .l6 .s12 .truncate', [ - span('.flow-text', [config.title]), - span([' ']), - span('.grey-text .text-lighten-1 .right-align', ['(', state.toString(), ')']) - ]), - - div('.col .s6 ' + config.class, - renderField(config, state) - ) - - ]) - )) - - const updateReducer$ = cond([ - [R.equals('checkbox'), _ => update$.map(update => prevState => !prevState)], - [R.equals('text'), _ => update$.map(update => prevState => update)], - [R.equals('select'), _ => update$.map(update => prevState => update)], - ])(config.type) - - return { - DOM: vdom$, - onion: updateReducer$ - } - + return { + DOM: vdom$, + onion: updateReducer$, } + } + + const makeSettingsGroup = (settingsGroupObj) => (sources) => { + const group$ = sources.onion.state$ + const settingsArray = settingsGroupObj.settings + const title = settingsGroupObj.title + + const components$ = xs + .of(settingsArray) + .map((settings) => + settings.map((setting) => + isolate(makeSetting(setting), setting.field)(sources) + ) + ) + .remember() + + const vdom$ = components$ + .compose(pick("DOM")) + .compose(mix(xs.combine)) + .map((vdoms) => + ul( + ".collection .with-header", + [li(".collection-header .grey .lighten-2", [h4(title)])].concat(vdoms) + ) + ) + .remember() - const makeSettingsGroup = (settingsGroupObj) => (sources) => { - const group$ = sources.onion.state$ - const settingsArray = settingsGroupObj.settings - const title = settingsGroupObj.title - - const components$ = xs.of(settingsArray) - .map(settings => - settings.map(setting => - isolate(makeSetting(setting), setting.field)(sources) - ) - ) - .remember() - - const vdom$ = components$ - .compose(pick('DOM')) - .compose(mix(xs.combine)) - .map(vdoms => - ul('.collection .with-header', [ - li('.collection-header .grey .lighten-2', [ - h4(title) - ]) - ].concat(vdoms)) - ) - .remember() - - const reducer$ = components$ - .compose(pick('onion')) - .compose(mix(xs.merge)) + const reducer$ = components$.compose(pick("onion")).compose(mix(xs.merge)) - return { - onion: reducer$, - DOM: vdom$ - } + return { + onion: reducer$, + DOM: vdom$, } + } - const makeSettings = (settingsConf$, sources) => { - const settings$ = sources.onion.state$ + const makeSettings = (settingsConf$, sources) => { + const settings$ = sources.onion.state$ - const groups$ = settingsConf$ - .map(groups => - groups.map(group => - isolate(makeSettingsGroup(group), group.group)(sources) - ) - ) - .remember() + const groups$ = settingsConf$ + .map((groups) => + groups.map((group) => + isolate(makeSettingsGroup(group), group.group)(sources) + ) + ) + .remember() - const vdom$ = groups$ - .compose(pick('DOM')) - .compose(mix(xs.combine)) - .map(vdoms => - div('.col .l8 .offset-l2 .s12', vdoms) - ) - .remember() + const vdom$ = groups$ + .compose(pick("DOM")) + .compose(mix(xs.combine)) + .map((vdoms) => div(".col .l8 .offset-l2 .s12", vdoms)) + .remember() - const reducer$ = groups$ - .compose(pick('onion')) - .compose(mix(xs.merge)) - .remember() + const reducer$ = groups$ + .compose(pick("onion")) + .compose(mix(xs.merge)) + .remember() - return { - onion: reducer$, - DOM: vdom$ - } + return { + onion: reducer$, + DOM: vdom$, } - - const AdminSettings = makeSettings(settingsConfig$, sources) - - const vdom$ = xs.combine(settings$, AdminSettings.DOM) - .map(([_, topTableEntries]) => - div('.row .grey .lighten-3', { style: { margin: '0px 0px 0px 0px' } }, [ - div('.row .s12', ['']), - topTableEntries, - div('.row .s12', ['']), - button('.reset .col .s4 .offset-s4 .btn .grey', 'Reset to Default'), - div('.row .s12', ['']), - ]) - ).remember() - - // const apply$ = sources.DOM.select('.apply').events('click') - const reset$ = sources.DOM.select('.reset').events('click').remember() - - // Reset the storage by removing the ComPass key - const resetStorage$ = reset$.mapTo({ action: "removeItem", key : "ComPass"}) - - // The router does not reload the same page, so use the browser functionality for that... - const router$ = reset$.map(_ => location.reload()).mapTo('/admin').remember() - - // Deployment needs to be tackled globally, does not work in _isolation_ - const deploymentUpdated$ = settings$ - .compose(dropRepeats((x,y) => x.deployment.name == y.deployment.name)) - - // Just like in index.js: - // - fetch the appropriate deployment from deployments.js - // - overwrite the relevant entries (endpoints) of the various settings with the correct one - // - // TODOs: - // - align this with index.js - // - restructure deployments.js to an array of deployments rather than a hashmap - const deploymentReducer$ = xs.combine(deploymentUpdated$, sources.deployments) - .map(([settings, deployments]) => prevState => { - const desiredDeploymentName = settings.deployment.name - const desiredDeployment = R.head(deployments.filter(x => x.name == desiredDeploymentName)) - const updatedDeployment = R.mergeDeepRight(prevState.deployment, desiredDeployment) - const updatedSettings = R.merge(prevState, { deployment : updatedDeployment}) - const distributedAdminSettings = R.mergeDeepRight(updatedSettings, updatedSettings.deployment.services) - return distributedAdminSettings + } + + const AdminSettings = makeSettings(settingsConfig$, sources) + + const vdom$ = xs + .combine(settings$, AdminSettings.DOM) + .map(([_, topTableEntries]) => + div(".row .grey .lighten-3", { style: { margin: "0px 0px 0px 0px" } }, [ + div(".row .s12", [""]), + topTableEntries, + div(".row .s12", [""]), + button(".reset .col .s4 .offset-s4 .btn .grey", "Reset to Default"), + div(".row .s12", [""]), + ]) + ) + .remember() + + // const apply$ = sources.DOM.select('.apply').events('click') + const reset$ = sources.DOM.select(".reset").events("click").remember() + + // Reset the storage by removing the ComPass key + const resetStorage$ = reset$.mapTo({ action: "removeItem", key: "ComPass" }) + + // The router does not reload the same page, so use the browser functionality for that... + const router$ = reset$ + .map((_) => location.reload()) + .mapTo("/admin") + .remember() + + // Deployment needs to be tackled globally, does not work in _isolation_ + const deploymentUpdated$ = settings$.compose( + dropRepeats((x, y) => x.deployment.name == y.deployment.name) + ) + + // Just like in index.js: + // - fetch the appropriate deployment from deployments.js + // - overwrite the relevant entries (endpoints) of the various settings with the correct one + // + // TODOs: + // - align this with index.js + // - restructure deployments.js to an array of deployments rather than a hashmap + const deploymentReducer$ = xs + .combine(deploymentUpdated$, sources.deployments) + .map(([settings, deployments]) => (prevState) => { + const desiredDeploymentName = settings.deployment.name + const desiredDeployment = R.head( + deployments.filter((x) => x.name == desiredDeploymentName) + ) + const updatedDeployment = R.mergeDeepRight( + prevState.deployment, + desiredDeployment + ) + const updatedSettings = R.merge(prevState, { + deployment: updatedDeployment, + }) + const distributedAdminSettings = R.mergeDeepRight( + updatedSettings, + updatedSettings.deployment.services + ) + console.log(desiredDeploymentName) + return distributedAdminSettings }) - return { - DOM: vdom$, - onion: xs.merge( - deploymentReducer$.compose(debounce(50)), - AdminSettings.onion.compose(debounce(200)) - ), - router: router$, - storage: resetStorage$ - } + return { + DOM: vdom$, + onion: xs.merge( + deploymentReducer$.compose(debounce(50)), + AdminSettings.onion.compose(debounce(200)) + ), + router: router$, + storage: resetStorage$, + } } diff --git a/src/js/pages/settings.js b/src/js/pages/settings.js index 6233ba07..3846b2d3 100644 --- a/src/js/pages/settings.js +++ b/src/js/pages/settings.js @@ -1,268 +1,284 @@ -import { a, div, br, label, input, p, button, code, pre, h2, h4, i, h3, h5, span, ul, li } from '@cycle/dom' -import xs from 'xstream' -import isolate from '@cycle/isolate' -import { mergeWith, merge, mergeAll } from 'ramda' -import { clone } from 'ramda'; -import sampleCombine from 'xstream/extra/sampleCombine' -import { initSettings } from '../configuration.js' -import { pick, mix } from 'cycle-onionify'; -import debounce from 'xstream/extra/debounce' +import { + div, + label, + input, + button, + h4, + span, + ul, + li, +} from "@cycle/dom" +import xs from "xstream" +import isolate from "@cycle/isolate" +import { mergeWith, merge, mergeAll } from "ramda" +import { pick, mix } from "cycle-onionify" +import debounce from "xstream/extra/debounce" export function IsolatedSettings(sources) { - return isolate(Settings, 'settings')(sources) + return isolate(Settings, "settings")(sources) } export function Settings(sources) { + const settings$ = sources.onion.state$ - const settings$ = sources.onion.state$ - - const settingsConfig = [{ - group: 'common', - title: 'Common Settings', - settings: [ - // { - // field: 'ghostMode', - // type: 'checkbox', - // class: '.switch', - // title: 'Ghost Mode', - // props: { type: 'checkbox' } - // }, - { - field: 'pvalue', - class: 'text', - type: '.input-field', - title: 'p-value', - props: { type: 'text' } - }, - { - field: 'blur', - class: '.switch', - type: 'checkbox', - title: 'Blur?', - props: { type: 'checkbox' } - }, - { - field: 'amountBlur', - class: '.range-field', - type: 'range', - title: 'Amount blur?', - props: { type: 'range', min: 0, max: 10 } - } - ] + const settingsConfig = [ + { + group: "common", + title: "Common Settings", + settings: [ + { + field: 'ghostMode', + type: 'checkbox', + class: '.switch', + title: 'Ghost Mode', + props: { type: 'checkbox' } }, { - group: 'compoundTable', - title: 'Compound Table Settings', - settings: [ - { - field: 'count', - class: '.range-field', - type: 'range', - title: '# of entries in table', - props: { type: 'range', min: 0, max: 100 } - }, - ] + field: "pvalue", + class: "text", + type: ".input-field", + title: "p-value", + props: { type: "text" }, }, { - group: 'headTable', - title: 'Top Table Settings', - settings: [ - { - field: 'count', - class: '.range-field', - type: 'range', - title: '# of entries in table', - props: { type: 'range', min: 0, max: 20 } - }, - ] + field: "blur", + class: ".switch", + type: "checkbox", + title: "Blur?", + props: { type: "checkbox" }, }, { - group: 'tailTable', - title: 'Bottom Table Settings', - settings: [ - { - field: 'count', - class: '.range-field', - type: 'range', - title: '# of entries in table', - props: { type: 'range', min: 0, max: 20 } - }, - ] + field: "amountBlur", + class: ".range-field", + type: "range", + title: "Amount blur?", + props: { type: "range", min: 0, max: 10 }, }, + ], + }, + { + group: "compoundTable", + title: "Compound Table Settings", + settings: [ { - group: 'plots', - title: 'Combined (binned) plots', - settings: [ - { - field: 'bins', - class: '.range-field', - type: 'range', - title: '# of bins in Histogram and Y-direction', - props: { type: 'range', min: 5, max: 50 } - }, - { - field: 'binsX', - class: '.range-field', - type: 'range', - title: '# of bins in X direction', - props: { type: 'range', min: 5, max: 50 } - }, - ] - } - ] - - const makeSetting = (config) => (sources) => { - const state$ = sources.onion.state$ - - const update$ = (config.type == 'checkbox') ? - sources.DOM.select('input').events('click').map(event => event) : - sources.DOM.events('input').map(event => event.target.value) - - const vdom$ = - state$.map(state => - li('.collection-item .row', - div('.valign-wrapper', [ + field: "count", + class: ".range-field", + type: "range", + title: "# of entries in table", + props: { type: "range", min: 0, max: 100 }, + }, + ], + }, + { + group: "headTable", + title: "Top Table Settings", + settings: [ + { + field: "count", + class: ".range-field", + type: "range", + title: "# of entries in table", + props: { type: "range", min: 0, max: 20 }, + }, + ], + }, + { + group: "tailTable", + title: "Bottom Table Settings", + settings: [ + { + field: "count", + class: ".range-field", + type: "range", + title: "# of entries in table", + props: { type: "range", min: 0, max: 20 }, + }, + ], + }, + { + group: "plots", + title: "Combined (binned) plots", + settings: [ + { + field: "bins", + class: ".range-field", + type: "range", + title: "# of bins in Histogram and Y-direction", + props: { type: "range", min: 5, max: 50 }, + }, + { + field: "binsX", + class: ".range-field", + type: "range", + title: "# of bins in X direction", + props: { type: "range", min: 5, max: 50 }, + }, + ], + }, + ] - span('.col .l6 .s12 .truncate', [ - span('.flow-text', [config.title]), - span([' ']), - span('.grey-text .text-lighten-1 .right-align', ['(', state.toString(), ')']) - ]), + const makeSetting = (config) => (sources) => { + const state$ = sources.onion.state$ - div('.col .s6 ' + config.class, + const update$ = + config.type == "checkbox" + ? sources.DOM.select("input") + .events("click") + .map((event) => event) + : sources.DOM.events("input").map((event) => event.target.value) - (config.type == 'checkbox') - // Checkbox form - ? - [ - label('.active', [ - // config.title, - input({ props: merge(config.props, { checked: state }) }), - span('.lever'), - ]) - ] - // Range or input - : - [ - input({ props: merge(config.props, { value: state }) }), - // label(config.field), - ] - ) + const vdom$ = state$.map((state) => + li( + ".collection-item .row", + div(".valign-wrapper", [ + span(".col .l6 .s12 .truncate", [ + span(".flow-text", [config.title]), + span([" "]), + span(".grey-text .text-lighten-1 .right-align", [ + "(", + state.toString(), + ")", + ]), + ]), - ]) - )) + div( + ".col .s6 " + config.class, - const updateReducer$ = (config.type == 'checkbox') ? - update$.map(update => prevState => !prevState) : - update$.map(update => prevState => update) + config.type == "checkbox" + ? // Checkbox form + [ + label(".active", [ + // config.title, + input({ props: merge(config.props, { checked: state }) }), + span(".lever"), + ]), + ] + : // Range or input + [ + input({ props: merge(config.props, { value: state }) }), + // label(config.field), + ] + ), + ]) + ) + ) - return { - DOM: vdom$, - onion: updateReducer$ - } + const updateReducer$ = + config.type == "checkbox" + ? update$.map((_) => (prevState) => !prevState) + : update$.map((update) => (_) => update) + return { + DOM: vdom$, + onion: updateReducer$, } + } - const makeSettingsGroup = (settingsGroupObj) => (sources) => { - const group$ = sources.onion.state$ - const settingsArray = settingsGroupObj.settings - const title = settingsGroupObj.title + const makeSettingsGroup = (settingsGroupObj) => (sources) => { + const group$ = sources.onion.state$ + const settingsArray = settingsGroupObj.settings + const title = settingsGroupObj.title - const components$ = xs.of(settingsArray) - .map(settings => - settings.map(setting => - isolate(makeSetting(setting), setting.field)(sources) - ) - ) - .remember() + const components$ = xs + .of(settingsArray) + .map((settings) => + settings.map((setting) => + isolate(makeSetting(setting), setting.field)(sources) + ) + ) + .remember() - const vdom$ = components$ - .compose(pick('DOM')) - .compose(mix(xs.combine)) - .map(vdoms => - ul('.collection .with-header', [ - li('.collection-header .grey .lighten-2', [ - h4(title) - ]) - ].concat(vdoms)) - ) - .remember() + const vdom$ = components$ + .compose(pick("DOM")) + .compose(mix(xs.combine)) + .map((vdoms) => + ul( + ".collection .with-header", + [li(".collection-header .grey .lighten-2", [h4(title)])].concat(vdoms) + ) + ) + .remember() - const reducer$ = components$ - .compose(pick('onion')) - .compose(mix(xs.merge)) + const reducer$ = components$.compose(pick("onion")).compose(mix(xs.merge)) - return { - onion: reducer$, - DOM: vdom$ - } + return { + onion: reducer$, + DOM: vdom$, } + } - const makeSettings = (settingsObj) => (sources) => { - const settings$ = sources.onion.state$ + const makeSettings = (settingsObj) => (sources) => { + const settings$ = sources.onion.state$ - const groups$ = xs.of(settingsObj) - .map(groups => - groups.map(group => - isolate(makeSettingsGroup(group), group.group)(sources) - ) - ) - .remember() + const groups$ = xs + .of(settingsObj) + .map((groups) => + groups.map((group) => + isolate(makeSettingsGroup(group), group.group)(sources) + ) + ) + .remember() - const vdom$ = groups$ - .compose(pick('DOM')) - .compose(mix(xs.combine)) - .map(vdoms => - div('.col .l8 .offset-l2 .s12', vdoms) - ) - .remember() + const vdom$ = groups$ + .compose(pick("DOM")) + .compose(mix(xs.combine)) + .map((vdoms) => div(".col .l8 .offset-l2 .s12", vdoms)) + .remember() - const reducer$ = groups$ - .compose(pick('onion')) - .compose(mix(xs.merge)) - .remember() + const reducer$ = groups$ + .compose(pick("onion")) + .compose(mix(xs.merge)) + .remember() - return { - onion: reducer$, - DOM: vdom$ - } + return { + onion: reducer$, + DOM: vdom$, } + } - const Settings = makeSettings(settingsConfig)(sources) + const Settings = makeSettings(settingsConfig)(sources) - const vdom$ = xs.combine(settings$, Settings.DOM) - .map(([_, topTableEntries]) => - div('.row .grey .lighten-3', { style: { margin: '0px 0px 0px 0px' } }, [ - div('.row .s12', ['']), - topTableEntries, - div('.row .s12', ['']), - button('.reset .col .s2 .offset-s3 .btn .grey', 'Reset to Default'), - button('.admin .col .s2 .offset-s2 .btn .grey .lighten-2 .grey-text', 'Go to Admin Settings'), - div('.row .s12', ['']), - ]) - ).remember() + const vdom$ = xs + .combine(settings$, Settings.DOM) + .map(([_, topTableEntries]) => + div(".row .grey .lighten-3", { style: { margin: "0px 0px 0px 0px" } }, [ + div(".row .s12", [""]), + topTableEntries, + div(".row .s12", [""]), + button(".reset .col .s2 .offset-s3 .btn .grey", "Reset to Default"), + button( + ".admin .col .s2 .offset-s2 .btn .grey .lighten-2 .grey-text", + "Go to Admin Settings" + ), + div(".row .s12", [""]), + ]) + ) + .remember() - // When the reset button is pressed, we remove the ComPass key from the local storage - // and reload the page. The `defaultReducer$` in `index.js` handles taking care of - // the deployment scenario. - const reset$ = sources.DOM.select('.reset').events('click').remember() - // Reset the storage by removing the ComPass key - const resetStorage$ = reset$.mapTo({ action: "removeItem", key : "ComPass"}) + // When the reset button is pressed, we remove the ComPass key from the local storage + // and reload the page. The `defaultReducer$` in `index.js` handles taking care of + // the deployment scenario. + const reset$ = sources.DOM.select(".reset").events("click").remember() + // Reset the storage by removing the ComPass key + const resetStorage$ = reset$.mapTo({ action: "removeItem", key: "ComPass" }) - const admin$ = sources.DOM.select('.admin').events('click').remember() + const admin$ = sources.DOM.select(".admin").events("click").remember() - // The router does not reload the same page, so use the browser functionality for that... - const resetRouter$ = reset$.map(_ => location.reload()).mapTo('/settings').remember() - const adminRouter$ = admin$.mapTo('/admin').remember() + // The router does not reload the same page, so use the browser functionality for that... + const resetRouter$ = reset$ + .map((_) => location.reload()) + .mapTo("/settings") + .remember() + const adminRouter$ = admin$.mapTo("/admin").remember() - // This is an effect and should be moved to a driver... - // TODO - // const reload$ = xs.merge(resetRouter$, adminRouter$).compose(debounce(20)).map(_ => location.reload()) + // This is an effect and should be moved to a driver... + // TODO + // const reload$ = xs.merge(resetRouter$, adminRouter$).compose(debounce(20)).map(_ => location.reload()) - return { - DOM: vdom$, - onion: Settings.onion.compose(debounce(200)), - router: xs.merge(resetRouter$, adminRouter$), - storage: resetStorage$ - }; + return { + DOM: vdom$, + onion: Settings.onion.compose(debounce(200)), + router: xs.merge(resetRouter$, adminRouter$), + storage: resetStorage$, + } } From 26f368e6cfe0097ffc9fe9a402284fc82b0567dd Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Wed, 10 Nov 2021 15:42:47 +0100 Subject: [PATCH 034/191] Add some info the the (not finished) Admin page --- src/js/pages/admin.js | 142 ++++++++++++++++++++---------------------- 1 file changed, 69 insertions(+), 73 deletions(-) diff --git a/src/js/pages/admin.js b/src/js/pages/admin.js index d2ff8a5b..70d2ce57 100644 --- a/src/js/pages/admin.js +++ b/src/js/pages/admin.js @@ -1,90 +1,86 @@ -import xs from 'xstream'; -import { div, nav, a, h3, p, ul, li, h1, h2, i, footer, header, main, svg, g, path, img, map, area, use, span } from '@cycle/dom' -import { merge, prop, equals } from 'ramda'; +import xs from "xstream" +import { div, p } from "@cycle/dom" +import { merge, prop, equals } from "ramda" -import { Check } from '../components/Check' -import { IsolatedSettings } from './settings' - -import flattenSequentially from 'xstream/extra/flattenSequentially' -import { pick, mix } from 'cycle-onionify'; -import { initSettings } from '../configuration' -import debounce from 'xstream/extra/debounce' -import dropRepeats from 'xstream/extra/dropRepeats' +import { initSettings } from "../configuration" +import debounce from "xstream/extra/debounce" +import dropRepeats from "xstream/extra/dropRepeats" const defaultState = { - green : { - url: "http://localhost:8090" - }, - blue : { - url: "localhost:8081" - } + green: { + url: "http://localhost:8090", + }, + blue: { + url: "localhost:8081", + }, } +/** + * This component should become an API managemt page used to trigger initialization + * and other operational aspects of managing LuciusAPI. + */ function Admin(sources) { + const state$ = sources.onion.state$ + .compose(dropRepeats((x, y) => equals(x.core, y.core))) + .startWith({ core: defaultState, settings: initSettings }) + // .map(state => merge(state, state.settings.admin, state.settings.api)) - const state$ = sources.onion.state$ - .compose(dropRepeats((x,y) => equals(x.core, y.core))) - .startWith({ core: defaultState, settings: initSettings }) - // .map(state => merge(state, state.settings.admin, state.settings.api)) - - const request$ = state$.map(state => ({ - url: state.core.green.url + '/jobs', - method: 'GET', - send: {}, - 'category': 'status' - }) - ) + const request$ = state$.map((state) => ({ + url: state.core.green.url + "/jobs", + method: "GET", + send: {}, + category: "status", + })) - const response$$ = sources.HTTP - .select('status') + const response$$ = sources.HTTP.select("status") - const invalidResponse$ = response$$ - .map(response$ => - response$ - .filter(response => false) // ignore regular events - .replaceError(error => xs.of(error)) // emit error - ) - .flatten() + const invalidResponse$ = response$$ + .map( + (response$) => + response$ + .filter((response) => false) // ignore regular events + .replaceError((error) => xs.of(error)) // emit error + ) + .flatten() - /** - * Parse the successful results only. - * - * We add a little wait time (`debounce`) in order for the jobserver - * to be up-to-date with the actual jobs. Otherwize, we measure the - * wrong job times. - */ - const validResponse$ = response$$ - .map(response$ => - response$ - .replaceError(error => xs.empty()) - ) - .flatten() - .compose(debounce(500)) + /** + * Parse the successful results only. + * + * We add a little wait time (`debounce`) in order for the jobserver + * to be up-to-date with the actual jobs. Otherwize, we measure the + * wrong job times. + */ + const validResponse$ = response$$ + .map((response$) => response$.replaceError((error) => xs.empty())) + .flatten() + .compose(debounce(500)) - const vdom$ = state$.map( state => - div('.container', [ - div([p("Green - Alright, let's get rolling..." + state.extra)]), - div([p("Blue - Alright, let's get rolling...")]) - ])); + const vdom$ = state$.map((state) => + div(".container", [ + div([p("Green - Alright, let's get rolling..." + state.extra)]), + div([p("Blue - Alright, let's get rolling...")]), + ]) + ) - // This is needed in order to get the onion stream active! - const defaultReducer$ = xs.of(prevState => { - if (typeof prevState === 'undefined') { - return defaultState - } else { - return prevState - } - }); + // This is needed in order to get the onion stream active! + const defaultReducer$ = xs.of((prevState) => { + if (typeof prevState === "undefined") { + return defaultState + } else { + return prevState + } + }) - const responseReducer$ = validResponse$.map(response => prevState => ({...prevState, core: {...state.core, state: 1}})) + const responseReducer$ = validResponse$.map((response) => (prevState) => ({ + ...prevState, + core: { ...prevState.core, state: 1 }, + })) - return { - DOM: vdom$, - HTTP: request$, - onion: xs.merge(defaultReducer$, responseReducer$), - // alert: CheckSink.alert, - // popup: CheckSink.popup - }; + return { + DOM: vdom$, + HTTP: request$, + onion: xs.merge(defaultReducer$, responseReducer$), + } } export default Admin From 26451fb303d1588584638b24e06e77fd7514df5a Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Wed, 10 Nov 2021 19:01:11 +0100 Subject: [PATCH 035/191] Rename hourglass -> example and fix magnifying glass for treatments --- deployments.json | 6 +++--- src/js/components/SignatureForm.js | 2 +- src/js/components/TreatmentCheck.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deployments.json b/deployments.json index d6005d3b..81d23a9b 100644 --- a/deployments.json +++ b/deployments.json @@ -19,11 +19,11 @@ } }, "common": { - "hourglass": { + "example": { "compound" : "BRD-K93645900", "signature" : "-WRONG HSPA1A DNAJB1 DDIT4 -TSEN2", - "treatment" : "MELK", - "target" : "MELK" + "genetic" : "MELK", + "target": "MELK" }, "modelTranslations": [ { diff --git a/src/js/components/SignatureForm.js b/src/js/components/SignatureForm.js index 271b877f..663dd2ec 100644 --- a/src/js/components/SignatureForm.js +++ b/src/js/components/SignatureForm.js @@ -76,7 +76,7 @@ function SignatureForm(sources) { .compose(sampleCombine(state$)) .map(([_, state]) => prevState => { let newState = clone(prevState) - newState.form.query = state.settings.common.hourglass.signature //'ENSG00000012048 -WRONG HSPA1A DNAJB1 DDIT4 -TSEN2' + newState.form.query = state.settings.common.example.signature //'ENSG00000012048 -WRONG HSPA1A DNAJB1 DDIT4 -TSEN2' newState.form.validated = false return newState }) diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index 3173869e..c2f8b400 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -1,6 +1,6 @@ import sampleCombine from "xstream/extra/sampleCombine" import { i, div, input } from "@cycle/dom" -import { equals, mergeAll } from "ramda" +import { prop, equals, mergeAll } from "ramda" import xs from "xstream" import dropRepeats from "xstream/extra/dropRepeats" import debounce from "xstream/extra/debounce" @@ -207,7 +207,7 @@ function TreatmentCheck(sources) { ...prevState.core, showSuggestions: false, validated: true, - input: state.settings.common.hourglass.treatment, + input: prop(state.settings.treatmentLike, state.settings.common.example) }, })) From bbb0483973f1cc3efa25318dc10a19257d285a22 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 16 Nov 2021 18:03:41 +0100 Subject: [PATCH 036/191] Head table fixes * Fix expanding of options not working The clicking was only being processed when the input changed causing odd behaviour * Prevent user from setting 0 or negative amount of entries in a table additionally, prevent the internal state becoming negative if the user would keep pressing '-x', otherwise, when clicking '+x' the internal state would still be a negative amount and user needs to press an unknown amount of '+x' before changes are visible allow internal state to be 0, but always request at least 1 if user adds 5 again, new value should be 5 not 6 so we can't just limit the internal state to 1 --- src/js/components/Table.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/js/components/Table.js b/src/js/components/Table.js index 3991e0c2..d1164d89 100644 --- a/src/js/components/Table.js +++ b/src/js/components/Table.js @@ -40,6 +40,7 @@ import { merge, intersection, difference, + max, } from "ramda" // import { tableContent, tableContentLens } from './tableContent/tableContent' import isolate from "@cycle/isolate" @@ -179,7 +180,7 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { .map((state) => state.settings) .compose(dropRepeats(equals)) // Avoid updates to vdom$ when no real change - const expandOptions$ = modifiedState$ + const expandOptions$ = state$ .map((state) => state.core.expandOptions) .compose(dropRepeats(equals)) // Avoid updates to vdom$ when no real change @@ -189,31 +190,31 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { .events("click") .mapTo(5) .startWith(0) - .fold((x, y) => x + y, 0) const min5$ = sources.DOM.select(".min5") .events("click") - .mapTo(5) + .mapTo(-5) .startWith(0) - .fold((x, y) => x + y, 0) const plus10$ = sources.DOM.select(".plus10") .events("click") .mapTo(10) .startWith(0) - .fold((x, y) => x + y, 0) const min10$ = sources.DOM.select(".min10") .events("click") - .mapTo(10) + .mapTo(-10) .startWith(0) - .fold((x, y) => x + y, 0) + + const defaultAmountToDisplay$ = newInput$.map(state => parseInt(state.settings.table.count)) + const amountToDisplay$ = xs + .merge(defaultAmountToDisplay$, plus5$, min5$, plus10$, min10$) + .fold((x, y) => max(0, x + y), 0) // ======================================================================== const triggerRequest$ = xs - .combine(newInput$, plus5$, min5$, plus10$, min10$) - .map(([state, plus5, min5, plus10, min10]) => { + .combine(newInput$, amountToDisplay$) + .map(([state, amountToDisplay]) => { const tableType = state.settings.table.type - const cnt = - parseInt(state.settings.table.count) + plus5 - min5 + plus10 - min10 + const cnt = max(1, amountToDisplay) // Set a limit on the results depending on the type of table: return tableType == "compoundTable" ? { ...state, core: { ...state.core, count: { limit: cnt } } } From e4010eb93edee6c6c535f23b547dd699033865d4 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Wed, 17 Nov 2021 09:09:47 +0100 Subject: [PATCH 037/191] Rework ghost scenarios for compound and genetic worfklow --- deployments.json | 140 ++++++++++++----------- package.json | 2 +- src/js/components/Filter.js | 6 +- src/js/components/TreatmentCheck.js | 2 - src/js/configuration.js | 2 +- src/js/pages/compound.js | 13 +-- src/js/pages/genetic.js | 15 +-- src/js/scenarios/compoundScenario.js | 159 -------------------------- src/js/scenarios/treatmentScenario.js | 150 ++++++++++++++++++++++++ src/js/utils/scenario.js | 45 ++++---- 10 files changed, 264 insertions(+), 270 deletions(-) delete mode 100644 src/js/scenarios/compoundScenario.js create mode 100644 src/js/scenarios/treatmentScenario.js diff --git a/deployments.json b/deployments.json index 81d23a9b..1c96e89f 100644 --- a/deployments.json +++ b/deployments.json @@ -2,29 +2,37 @@ { "name": "default", "customizations": { - "wi": "https://github.com/data-intuitive/LuciusWeb123" - }, + "wi": "https://github.com/data-intuitive/LuciusWeb123" + }, "services": { "filter": { "values": { - "dose": [ - "sample_entry" - ], - "cell": [ - "sample_entry" - ], - "trtType": [ - "sample_entry" - ] + "dose": ["sample_entry"], + "cell": ["sample_entry"], + "trtType": ["sample_entry"] } }, "common": { "example": { - "compound" : "BRD-K93645900", - "signature" : "-WRONG HSPA1A DNAJB1 DDIT4 -TSEN2", - "genetic" : "MELK", + "compound": "BRD-K93645900", + "signature": "-WRONG HSPA1A DNAJB1 DDIT4 -TSEN2", + "genetic": "PARP2", "target": "MELK" }, + "ghost": { + "compound": { + "treatment": "BRD-K93645900", + "index": 0, + "sample": ["A375_6H_BRD_K93645900_10"], + "signature": "-CRCP" + }, + "genetic": { + "treatment": "PARP2", + "index": 0, + "sample": ["DER001_HCC515_96H_X1_F1B6_DUO52HI53LO:B03"], + "signature": "-COPS7A -KEAP1 -CCNB1 -HMG20B -ELOVL6 -DERA LPL -PLEKHJ1 -TPI1 ST3GAL5 -HAT1 -MLF2 -FOXG1 -STRAP -MRPL4 -EDN1 -PON2 KMT2A FRMD4A -C16orf58" + } + }, "modelTranslations": [ { "ui": "ID", @@ -38,63 +46,63 @@ } ] }, - "stats" : { + "stats": { "endpoint": "classPath=com.dataintuitive.luciusapi.statistics" }, - "api" : { + "api": { "url": "http://localhost:8090/jobs?context=luciusapi&appName=luciusapi&sync=true&timeout=30" }, "sourire": { "url": "http://localhost:9999/molecule/" }, - "geneAnnotations" : { - "url" : "http://localhost:8082/gene/symbol/" + "geneAnnotations": { + "url": "http://localhost:8082/gene/symbol/" }, - "compoundAnnotations" : { - "url" : "http://localhost:8082/ca/" + "compoundAnnotations": { + "url": "http://localhost:8082/ca/" } } }, { "name": "gsk", "customizations": { - "wi": "https://github.com/data-intuitive/LuciusWeb" - }, + "wi": "https://github.com/data-intuitive/LuciusWeb" + }, "services": { "filter": { "values": { "concentration": [ - "<= 4.0", - "(4.0, 4.5]", - "(4.5, 5.0]", - "(5.0, 5.5]", - "(5.5, 6.0]", - "(6.0, 6.5]", - "(6.5, 7.0]", - "(7.0, 7.5]", - "> 7.5", - "Other" - ], + "<= 4.0", + "(4.0, 4.5]", + "(4.5, 5.0]", + "(5.0, 5.5]", + "(5.5, 6.0]", + "(6.0, 6.5]", + "(6.5, 7.0]", + "(7.0, 7.5]", + "> 7.5", + "Other" + ], "protocol": [ - "endometrium", - "adipose", - "blood", - "skin", - "central nervous system", - "-666", - "bone", - "large intestine", - "prostate", - "haematopoietic and lymphoid tissue", - "breast", - "lung", - "muscle", - "large instestine", - "stomach", - "kidney", - "liver", - "ovary" - ], + "endometrium", + "adipose", + "blood", + "skin", + "central nervous system", + "-666", + "bone", + "large intestine", + "prostate", + "haematopoietic and lymphoid tissue", + "breast", + "lung", + "muscle", + "large instestine", + "stomach", + "kidney", + "liver", + "ovary" + ], "type": ["NA"] } }, @@ -112,28 +120,28 @@ } ] }, - "stats" : { + "stats": { "endpoint": "classPath=com.dataintuitive.luciusapi.statistics" }, - "api" : { + "api": { "url": "https://compass.data-intuitive.app:445/jobs?context=luciusapi&appName=luciusapi&sync=true&timeout=30" }, "sourire": { "url": "https://compass.data-intuitive.app/molecule/" }, - "geneAnnotations" : { - "url" : "https://compass.data-intuitive.app/gene/symbol/" + "geneAnnotations": { + "url": "https://compass.data-intuitive.app/gene/symbol/" }, - "compoundAnnotations" : { - "url" : "http://localhost:8082/ca/" + "compoundAnnotations": { + "url": "http://localhost:8082/ca/" } } }, { "name": "local-jnj", "customizations": { - "wi": "https://github.com/data-intuitive/LuciusCore" - }, + "wi": "https://github.com/data-intuitive/LuciusCore" + }, "services": { "filter": { "values": { @@ -156,20 +164,20 @@ } ] }, - "stats" : { + "stats": { "endpoint": "classPath=com.dataintuitive.luciusapi.statistics" }, - "api" : { + "api": { "url": "http://localhost:8090/jobs?context=luciusapi&appName=luciusapi&sync=true&timeout=30" }, "sourire": { "url": "http://localhost:9999/molecule/" }, - "geneAnnotations" : { - "url" : "http://localhost:8082/gene/symbol/" + "geneAnnotations": { + "url": "http://localhost:8082/gene/symbol/" }, - "compoundAnnotations" : { - "url" : "http://localhost:8082/drugbank/" + "compoundAnnotations": { + "url": "http://localhost:8082/drugbank/" } } } diff --git a/package.json b/package.json index 6e513214..22f3ae52 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "LuciusWeb", - "version": "5.0.0-alpha4", + "version": "5.0.0-alpha5", "description": "Web interface for ComPass aka Lucius", "repository": { "type": "git", diff --git a/src/js/components/Filter.js b/src/js/components/Filter.js index 38cb3fed..89b5f548 100644 --- a/src/js/components/Filter.js +++ b/src/js/components/Filter.js @@ -157,7 +157,7 @@ function intent(domSource$) { const action$ = xs.merge( doseToggled$, protocolToggled$, - typeToggled$ + typeToggled$, // toggledGhost$ ) @@ -485,7 +485,7 @@ function Filter(sources) { // Ghost mode, inject changes via external state updates... // const ghostChanges$ = modifiedState$ // .filter((state) => typeof state.core.ghost !== "undefined") - // .compose(dropRepeats()); + // .compose(dropRepeats()) const actions = intent(sources.DOM) @@ -513,7 +513,7 @@ function Filter(sources) { DOM: vdom$, HTTP: filterQuery.HTTP, onion: reducers$, - output: outputTrigger$ + output: outputTrigger$.debug('filter') } } diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index c2f8b400..a44a0aa1 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -281,8 +281,6 @@ function TreatmentCheck(sources) { .map(([_, state]) => state.core.input) .remember() - // const history$ = sources.onion.state$.fold((acc, x) => acc.concat([x]), [{}]) - return { log: xs.merge( logger(state$, "state$") diff --git a/src/js/configuration.js b/src/js/configuration.js index 374ba91b..48fbf2bc 100644 --- a/src/js/configuration.js +++ b/src/js/configuration.js @@ -1,5 +1,5 @@ export const initSettings = { - version: 4.001, + version: 5.1, deployment: { "name": "default", }, diff --git a/src/js/pages/compound.js b/src/js/pages/compound.js index d3c7cb7d..42431148 100644 --- a/src/js/pages/compound.js +++ b/src/js/pages/compound.js @@ -13,7 +13,7 @@ import { } from "../components/SampleTable/SampleTable" // Support for ghost mode -import { scenario } from "../scenarios/compoundScenario" +import { scenario } from "../scenarios/treatmentScenario" import { runScenario } from "../utils/scenario" export default function CompoundWorkflow(sources) { @@ -26,16 +26,13 @@ export default function CompoundWorkflow(sources) { const state$ = sources.onion.state$ // Scenario for ghost mode - const scenarioReducer$ = sources.onion.state$ + const scenarios$ = sources.onion.state$ .take(1) .filter((state) => state.settings.common.ghostMode) - .mapTo(runScenario(scenario).scenarioReducer$) + .map(state => runScenario(scenario(state.settings.common.ghost.compound))) + const scenarioReducer$ = scenarios$.map(s => s.scenarioReducer$) .flatten() - .startWith((prevState) => prevState) - const scenarioPopup$ = sources.onion.state$ - .take(1) - .filter((state) => state.settings.common.ghostMode) - .mapTo(runScenario(scenario).scenarioPopup$) + const scenarioPopup$ = scenarios$.map(s => s.scenarioPopup$) .flatten() .startWith({ text: "Welcome to Compound Workflow", duration: 4000 }) diff --git a/src/js/pages/genetic.js b/src/js/pages/genetic.js index 6c309d1e..67f3b413 100644 --- a/src/js/pages/genetic.js +++ b/src/js/pages/genetic.js @@ -13,7 +13,7 @@ import { } from "../components/SampleTable/SampleTable" // Support for ghost mode -import { scenario } from "../scenarios/compoundScenario" +import { scenario } from "../scenarios/treatmentScenario" import { runScenario } from "../utils/scenario" export default function GeneticWorkflow(sources) { @@ -26,18 +26,15 @@ export default function GeneticWorkflow(sources) { const state$ = sources.onion.state$ // Scenario for ghost mode - const scenarioReducer$ = sources.onion.state$ + const scenarios$ = sources.onion.state$ .take(1) .filter((state) => state.settings.common.ghostMode) - .mapTo(runScenario(scenario).scenarioReducer$) + .map(state => runScenario(scenario(state.settings.common.ghost.genetic))) + const scenarioReducer$ = scenarios$.map(s => s.scenarioReducer$) .flatten() - .startWith((prevState) => prevState) - const scenarioPopup$ = sources.onion.state$ - .take(1) - .filter((state) => state.settings.common.ghostMode) - .mapTo(runScenario(scenario).scenarioPopup$) + const scenarioPopup$ = scenarios$.map(s => s.scenarioPopup$) .flatten() - .startWith({ text: "Welcome to Genetic Perturbation Workflow", duration: 4000 }) + .startWith({ text: "Welcome to Genetic Workflow", duration: 4000 }) const formLens = { get: (state) => ({ diff --git a/src/js/scenarios/compoundScenario.js b/src/js/scenarios/compoundScenario.js deleted file mode 100644 index 1b5fca1e..00000000 --- a/src/js/scenarios/compoundScenario.js +++ /dev/null @@ -1,159 +0,0 @@ -const filterValues = { - concentration: ['0.1', '1', '10', '30'], - protocol: ['MCF7', 'PBMC'], - type: ['test', 'poscon'] - } - -export const scenario = [{ // Initiate ghost mode - delay: 100, - state: { form: { check: { ghostinput: true } } }, - }, - - { //Form - delay: 2000, - state: { form: { check: { input: "1", showSuggestions: true, validated: false } } }, - message: { - text: 'Please enter JNJ number or compound name', - duration: 4000 - } - }, - { - delay: 1200, - state: { form: { check: { input: "16", showSuggestions: true, validated: false } } }, - }, - { - delay: 2100, - state: { form: { check: { input: "169", showSuggestions: true, validated: false } } }, - message: { - text: '

CNS Example: sertraline or 16944811 a known SSRI (depression)

', - duration: 4000 - } - }, - { - delay: 1100, - state: { form: { check: { input: "1694", showSuggestions: true, validated: false } } }, - }, - { - delay: 800, - state: { form: { check: { input: "16944", showSuggestions: true, validated: false } } }, - }, - { - delay: 900, - state: { form: { check: { input: "169448", showSuggestions: true, validated: false } } }, - message: { - text: 'Select the desired compound from the drop down list of matches', - duration: 4000 - } - }, - { - delay: 1500, - state: { form: { check: { input: "16944811", showSuggestions: false, validated: true } } }, - }, - { - delay: 10, - state: { form: { check: { showSuggestions: false, validated: true } } }, - message: { - text: 'When ready, press play_arrow', - duration: 4000 - } - }, - - { // GO - delay: 1000, - state: { form: { check: { ghostoutput: true } } }, - }, - - // Sample Selection - { - delay: 500, - state: {}, - message: { - text: 'All samples that correspond to the selected compound are tabulated', - duration: 4000 - } - }, - { - delay: 3000, - state: { form: { sampleSelection: { data: { index: 0, value: { use: true } } } } }, - message: { - text: 'select or deselect the desired sample(s)', - duration: 4000 - } - }, - { - delay: 1000, - state: { form: { sampleSelection: { data: { index: 2, value: { use: false } } } } }, - }, - { - delay: 1000, - state: { form: { sampleSelection: { data: { index: 3, value: { use: false } } } } }, - }, - - { // Signature creation - delay: 100, - state: { form: { sampleSelection: { ghostoutput: true, output: ['GJA129_P03'] } } }, - message: { - text: 'Press Select', - duration: 4000 - } - }, - { - delay: 6000, - state: {}, - message: { - text: 'A signed ranked gene signature is generated across the samples', - duration: 6000 - } - }, - { - delay: 6000, - state: {}, - message: { - text: 'This gene signature can be copied and used in the target workflow', - duration: 6000 - } - }, - - { // Filter - delay: 7000, - state: { - filter: { - input: '-GOLT1B DDIT4 GPER -TNIP1 INSIG1 CLIC4 HMGCS1 HMOX1 AARS ELOVL6 -EGR1 -MAT2A FDFT1 -DDX42 PCK2 -MYCBP -RRP1B TSC22D3 CDK7 TIPARP -POLR1C -NFKBIA RGS2', - output: filterValues, - ghost: { expand: false } - }, - headTable: { input: { query: '-GOLT1B DDIT4 GPER -TNIP1 INSIG1 CLIC4 HMGCS1 HMOX1 AARS ELOVL6 -EGR1 -MAT2A FDFT1 -DDX42 PCK2 -MYCBP -RRP1B TSC22D3 CDK7 TIPARP -POLR1C -NFKBIA RGS2' } }, - tailTable: { input: { query: '-GOLT1B DDIT4 GPER -TNIP1 INSIG1 CLIC4 HMGCS1 HMOX1 AARS ELOVL6 -EGR1 -MAT2A FDFT1 -DDX42 PCK2 -MYCBP -RRP1B TSC22D3 CDK7 TIPARP -POLR1C -NFKBIA RGS2' } }, - }, - message: { - text: 'Set filter to MCF7', - duration: 7000 - } - }, - - { // Further info - delay: 3000, - state: {}, - message: { - text: 'The corresponding plots are generated for the entire compound database', - duration: 4000 - } - }, - { - delay: 3000, - state: {}, - message: { - text: 'Scroll down to assess the top correlated and anti-correlated compounds', - duration: 4000 - } - }, - { - delay: 3000, - state: {}, - message: { - text: 'Please note the fact that other anti-depressant compounds reveal high transcriptional correlation', - duration: 4000 - } - }, - -] diff --git a/src/js/scenarios/treatmentScenario.js b/src/js/scenarios/treatmentScenario.js new file mode 100644 index 00000000..a0cce206 --- /dev/null +++ b/src/js/scenarios/treatmentScenario.js @@ -0,0 +1,150 @@ +import {mergeDeepRight, concat} from 'ramda' + +/** + * A small utility function to simulate a user typing in the interface + */ +const typer = (text, json) => { + const l = text.length + const range = Array(l).fill().map((_, index) => index + 1) + const texts = range.map(i => text.substr(0,i)) + + return texts.map(substring => + mergeDeepRight( + mergeDeepRight( + json, + { delay: Math.floor(Math.random()*500) } + ), + {state: {form: {check: {input: substring}}}} + ) + ) +} + +/** + * Definition of the scenario + * Configuration is provided by means of `config` object: + * { + * treatment: <> + * index: <> + * sample: <> + * signature: <> + * } + */ +export const scenario = config => + Array.prototype.concat( + [ + { // Initiate ghost mode + delay: 100, + state: { form: { check: { ghostinput: true } } }, + }, + ], + typer( + config.treatment, + { // Form input + state: { form: { check: { input: config.treatment, showSuggestions: true, validated: false } } }, + } + ), + [ + { // Form input + delay: 1500, + state: { form: { check: { input: config.treatment, showSuggestions: false, validated: true } } }, + }, + + { // GO + delay: 1000, + state: { form: { check: { ghostoutput: true } } }, + }, + + { // Sample Selection + delay: 500, + state: {}, + message: { + text: 'All samples that correspond to the selected compound are tabulated', + duration: 4000 + } + }, + { + delay: 3000, + state: { form: { sampleSelection: { data: { index: config.index, value: { use: true } } } } }, + message: { + text: 'select or deselect the desired sample(s)', + duration: 4000 + } + }, + // { + // delay: 1000, + // state: { form: { sampleSelection: { data: { index: 2, value: { use: false } } } } }, + // }, + // { + // delay: 1000, + // state: { form: { sampleSelection: { data: { index: 3, value: { use: false } } } } }, + // }, + + { // Signature creation + delay: 500, + state: { form: { sampleSelection: { ghostoutput: true, output: config.sample } } }, + message: { + text: 'Press Select', + duration: 4000 + } + }, + { + delay: 4000, + state: {}, + message: { + text: 'A signed ranked gene signature is generated across the samples', + duration: 6000 + } + }, + { + delay: 4000, + state: {}, + message: { + text: 'This gene signature can be copied and used in the target workflow', + duration: 6000 + } + }, + + { // Filter + delay: 7000, + state: { + filter: { + input: config.signature, + output: { trtType: [ "trt_cp" ] }, + ghost: { expand: false } + }, + headTable: { input: { query: config.signature } }, + tailTable: { input: { query: config.signature } }, + }, + message: { + text: 'Set filter a filter', + duration: 7000 + } + }, + + // { // Further info + // delay: 3000, + // state: {}, + // message: { + // text: 'The corresponding plots are generated for the entire compound database', + // duration: 4000 + // } + // }, + // { + // delay: 3000, + // state: {}, + // message: { + // text: 'Scroll down to assess the top correlated and anti-correlated compounds', + // duration: 4000 + // } + // }, + // { + // delay: 3000, + // state: {}, + // message: { + // text: 'Please note the fact that other anti-depressant compounds reveal high transcriptional correlation', + // duration: 4000 + // } + // }, + + ] +) diff --git a/src/js/utils/scenario.js b/src/js/utils/scenario.js index 33b14951..922c957c 100644 --- a/src/js/utils/scenario.js +++ b/src/js/utils/scenario.js @@ -1,7 +1,7 @@ -import xs from 'xstream' -import { mergeDeepRight, mergeDeepWithKey} from 'ramda' -import delay from 'xstream/extra/delay' -import concat from 'xstream/extra/concat' +import xs from "xstream" +import { mergeDeepRight, mergeDeepWithKey } from "ramda" +import delay from "xstream/extra/delay" +import concat from "xstream/extra/concat" /** * We expect the following input for the right-hand side: @@ -9,9 +9,9 @@ import concat from 'xstream/extra/concat' * {index_in_array: {new object value}} */ const updateData = (k, l, r) => - k == 'data' - ? l.map((el, i) => (i == r.index) ? mergeDeepRight(el, r.value) : el) - : r + k == "data" + ? l.map((el, i) => (i == r.index ? mergeDeepRight(el, r.value) : el)) + : r /** * Helper function to run scenarios. @@ -20,23 +20,26 @@ const updateData = (k, l, r) => * Especially for Arrays, because we don't want to provide the full array in every step. */ export const runScenario = (scenario) => { + const scenarioStreamArray = scenario.map((step) => + xs.of(step).compose(delay(step.delay)) + ) - const scenarioStreamArray = scenario.map(step => xs.of(step).compose(delay(step.delay))) - // const scenarioStreamArray = scenario.map(step => xs.of(step).compose(delay(100))) - - const reducerStreamArray = scenarioStreamArray.map(step$ => - step$.map(step => prevState => mergeDeepWithKey(updateData, prevState, step.state)) - ) - const popupStreamArray = scenarioStreamArray.map(step$ => - step$.map(step => step.message) + const reducerStreamArray = scenarioStreamArray.map((step$) => + step$.map( + (step) => (prevState) => + mergeDeepWithKey(updateData, prevState, step.state) ) + ) + const popupStreamArray = scenarioStreamArray.map((step$) => + step$.map((step) => step.message) + ) - const reducer$ = concat(...reducerStreamArray) + const reducer$ = concat(...reducerStreamArray) - const popup$ = concat(...popupStreamArray) + const popup$ = concat(...popupStreamArray) - return { - scenarioReducer$: reducer$, - scenarioPopup$: popup$.debug() - } + return { + scenarioReducer$: reducer$, + scenarioPopup$: popup$, + } } From 7d2b1e34b80a4131568d58d566522b071a8fdb2c Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 17 Nov 2021 13:10:17 +0100 Subject: [PATCH 038/191] Dirty states (#66) * Adapt SignatureGenerator and SignatureForm to follow Model View Intent * Add test dirty flags to a few components and get it triggered by pressing alt key simply display an extra div with current dirty state as text * Move dirty flag to a separate 'ui' category in the state onion Capture not existing of the state.ui in the lenses Add very basic proof of concept dirty logic in the form that can be used to expand on * Add dirty state divs to topTables as placeholder for future work * Add dirty creator to Filter and TreatmentCheck, Show dirty ui in SampleSelection and tie it all together in genetic page UI dirty is still only displayed in a div Components becoming clean again when the change is reverted is still todo * Check if new input value matches old 'committed' value and if identical set dirty to false WIP but seems functional Code checks if the temporary value matches the current outputted value, if identical set dirty to false. However, the change of temporary value also triggers a new value on makeDirty$, which wins if no delay is added on the identical$ stream * Use debounce instead of delay to solve the race condition between identical$ and makeDirty$, use Ramda equals for array comparing Ramda has the needed functionality in equals to correctly compare arrays vs === which only checks if it is the same reference. * Greatly simplify logic for setting dirty states This logic should be water tight and is just plain simple, looks like win-win * Adapt dirty ui state in the lenses to be more robust Trade off is slightly less nice syntax * WIP new dirty wrapper Changes only in SampleSelection and SignatureGenerator so far, other components to be updated once format is more finalized or it is in a separate util library * Move dirty UI code into separate util library Reduces identical code over several files * Improve UI library to fall back to not dirty if state.ui.dirty is not defined Remove debug divs from the UI * Remove lingering .debug in dirtyUiStream it was added for debugging purposes but should have been removed before committing * Adapt dirty ui state in the lenses to be more robust * Use dirty logic for the filter component as used in other components Split of logic from reducer and use the same logic to create a temporary value stream so we can check changes * Rework genetic page to make ui reducer easier and copy to compound page * Fix table lenses for tail and compound tables tailTable was using headTable ui both lenses were using a less error proof syntax that wasn't replaced when headTable lens was changed * Keep plots and head tables as dirty when the signature is busy calculating Added busy flag in SignatureGenerator --- src/js/components/BinnedPlots/BinnedPlots.js | 22 ++- src/js/components/Filter.js | 56 +++--- src/js/components/SampleSelection.js | 17 +- src/js/components/SignatureForm.js | 151 ++++++++------- src/js/components/SignatureGenerator.js | 184 +++++++++++-------- src/js/components/Table.js | 7 +- src/js/components/TreatmentCheck.js | 7 +- src/js/pages/compound.js | 29 ++- src/js/pages/genetic.js | 29 ++- src/js/utils/ui.js | 66 +++++++ 10 files changed, 386 insertions(+), 182 deletions(-) create mode 100644 src/js/utils/ui.js diff --git a/src/js/components/BinnedPlots/BinnedPlots.js b/src/js/components/BinnedPlots/BinnedPlots.js index 0df9ad33..5ad8b358 100644 --- a/src/js/components/BinnedPlots/BinnedPlots.js +++ b/src/js/components/BinnedPlots/BinnedPlots.js @@ -9,13 +9,16 @@ import { similarityPlotVegaSpec } from './SimilarityPlotSpec.js' import { widthStream } from '../../utils/utils' import { loggerFactory } from '../../utils/logger' import { parse } from 'vega-parser' +import { dirtyWrapperStream } from "../../utils/ui" // const elementID = '#hist' // const component = 'histogram' // Granular access to the settings, only api and sim keys const plotsLens = { - get: state => ({ core: state.plots, settings: { plots: state.settings.plots, api: state.settings.api } }), + get: state => ({ core: state.plots, settings: { plots: state.settings.plots, api: state.settings.api }, + ui: (state.ui??{}).plots ?? {dirty: false}, // Get state.ui.prots in a safe way or else get a default + }), set: (state, childState) => ({...state, plots: childState.core }) }; @@ -241,14 +244,17 @@ function BinnedPlots(sources) { div('.red .white-text', [p('An error occured !!!')]), div('.red .white-text', [p('An error occured !!!')]))) + // Merge the streams, last event is shown... - const vdom$ = xs.merge( - initVdom$, - errorVdom$, - loadingVdom$, - emptyLoadedVdom$, - nonEmptyLoadedVdom$ - ) + // Wrap component vdom with an extra div that handles being dirty + const vdom$ = dirtyWrapperStream(state$, + xs.merge( + initVdom$, + errorVdom$, + loadingVdom$, + emptyLoadedVdom$, + nonEmptyLoadedVdom$ + ) ) // ======================================================================== diff --git a/src/js/components/Filter.js b/src/js/components/Filter.js index 89b5f548..df6acbaf 100644 --- a/src/js/components/Filter.js +++ b/src/js/components/Filter.js @@ -14,6 +14,7 @@ import { } from "ramda" import { FetchFilters } from "./FetchFilters" import debounce from 'xstream/extra/debounce' +import { dirtyUiReducer } from "../utils/ui" // A typical Lens with one exception: // We allow the child state settings for filter to propagate to @@ -173,7 +174,8 @@ export function model( input$, filterValuesAction$, modifier$, - filterAction$ + filterAction$, + state$, ) { // Add the filter values from the settings (and originally from deployments.json) to the current values @@ -270,29 +272,35 @@ export function model( // merge with the first state update in order to have at a value during initialization // When the filter is not set, ALL values are present and we transform that into NO values // We use the filter_output key for this, to keep it separate from the normal output key - const outputReducer$ = - filterAction$ - .map(_ => (prevState) => { - const filterKeys = keys(prevState.settings.filter.values) - var o = {} - // Mutable approach to transforming the values - filterKeys.forEach((key) => { - // If no values are present, ALL values are selected - const actual = - prop(key, prevState.core.output) == undefined - ? prop(key, prevState.settings.filter.values) - : prop(key, prevState.core.output) - if ( - difference(prop(key, prevState.settings.filter.values), actual).length != 0 - ) { - o[key] = prop(key, prevState.core.output) - } - }) - return { - ...prevState, - core: { ...prevState.core, filter_output: o }, + function minimizeFilterOutput(state) { + const filterKeys = keys(state.settings.filter.values) + var o = {} + // Mutable approach to transforming the values + filterKeys.forEach((key) => { + // If no values are present, ALL values are selected + const actual = + prop(key, state.core.output) == undefined + ? prop(key, state.settings.filter.values) + : prop(key, state.core.output) + if ( + difference(prop(key, state.settings.filter.values), actual).length != 0 + ) { + o[key] = prop(key, state.core.output) } + }) + return o + } + + const outputReducer$ = filterAction$.map(_ => (prevState) => ({ + ...prevState, + core: { ...prevState.core, filter_output: minimizeFilterOutput(prevState) }, + })) + + const minimizedCurrentOutput$ = state$.map((state) => minimizeFilterOutput(state)) + + // Logic and reducer stream that monitors if this component became dirty + const dirtyReducer$ = dirtyUiReducer(minimizedCurrentOutput$, state$.map(state => state.core.filter_output)) return xs.merge( defaultReducer$, @@ -301,6 +309,7 @@ export function model( filterReducer$, toggleReducer$, outputReducer$, + dirtyReducer$, ) } @@ -496,7 +505,8 @@ function Filter(sources) { input$, actions.filterValuesAction$, actions.modifier$, - actions.filterAction$ + actions.filterAction$, + state$, ) const outputTrigger$ = diff --git a/src/js/components/SampleSelection.js b/src/js/components/SampleSelection.js index 2c084b66..99fe6bc4 100644 --- a/src/js/components/SampleSelection.js +++ b/src/js/components/SampleSelection.js @@ -11,13 +11,16 @@ import { th, thead, tbody, + p, } from "@cycle/dom" import { clone, equals, merge } from "ramda" import xs from "xstream" import dropRepeats from "xstream/extra/dropRepeats" +import debounce from 'xstream/extra/debounce' import { loggerFactory } from "../utils/logger" import { CompoundAnnotation } from "../components/CompoundAnnotation" import { safeModelToUi } from "../modelTranslations" +import { dirtyUiReducer, dirtyWrapperStream } from "../utils/ui" const emptyData = { body: { @@ -31,11 +34,12 @@ const sampleSelectionLens = { get: (state) => ({ core: typeof state.form !== "undefined" ? state.form.sampleSelection : {}, settings: state.settings, + ui: (state.ui??{}).sampleSelection ?? {dirty: false}, // Get state.ui.sampleSelection in a safe way or else get a default }), // get: state => ({core: state.form.sampleSelection, settings: state.settings}), set: (state, childState) => ({ ...state, - form: { ...state.form, sampleSelection: childState.core }, + form: { ...state.form, sampleSelection: childState.core}, }), } @@ -226,7 +230,8 @@ function SampleSelection(sources) { .combine(modifiedState$, compoundAnnotations.DOM) .map(([state, annotation]) => makeTable(state, annotation, false)) - const vdom$ = xs.merge(initVdom$, loadingVdom$, loadedVdom$) + // Wrap component vdom with an extra div that handles being dirty + const vdom$ = dirtyWrapperStream( state$, xs.merge(initVdom$, loadingVdom$, loadedVdom$)) const dataReducer$ = data$.map((data) => (prevState) => { const newData = data.map((el) => merge(el, { use: true })) @@ -258,7 +263,7 @@ function SampleSelection(sources) { .filter((code) => code == "AltLeft") .mapTo(false) - const a$ = xs.merge(aDown$, aUp$).compose(dropRepeats(equals)) + const a$ = xs.merge(aDown$, aUp$).compose(dropRepeats(equals)).startWith(false) const selectReducer$ = useClick$ .compose(sampleCombine(a$)) @@ -325,6 +330,9 @@ function SampleSelection(sources) { .compose(sampleCombine(state$)) .map(([ev, state]) => state.core.output) + // Logic and reducer stream that monitors if this component became dirty + const dirtyReducer$ = dirtyUiReducer(sampleSelection$, state$.map(state => state.core.output)) + return { log: xs.merge(logger(state$, "state$")), DOM: vdom$, @@ -334,7 +342,8 @@ function SampleSelection(sources) { inputReducer$, requestReducer$, dataReducer$, - selectReducer$ + selectReducer$, + dirtyReducer$, ), output: sampleSelection$, modal: compoundAnnotations.modal, diff --git a/src/js/components/SignatureForm.js b/src/js/components/SignatureForm.js index 663dd2ec..99754ff3 100644 --- a/src/js/components/SignatureForm.js +++ b/src/js/components/SignatureForm.js @@ -13,62 +13,10 @@ import { loggerFactory } from '../utils/logger' const formLens = { get: state => ({ form: state.form, settings: { form: state.settings.form, api: state.settings.api, common: state.settings.common } }), set: (state, childState) => ({ ...state, form: childState.form }) -}; - -function SignatureForm(sources) { - - const logger = loggerFactory('signatureForm', sources.onion.state$, 'settings.form.debug') - - const state$ = sources.onion.state$ - const domSource$ = sources.DOM; - const props$ = sources.props; - - // Check Signature subcomponent, via isolation - const signatureCheckSink = isolate(SignatureCheck, { onion: checkLens })(sources) - const signatureCheckDom$ = signatureCheckSink.DOM; - const signatureCheckHTTP$ = signatureCheckSink.HTTP; - const signatureCheckReducer$ = signatureCheckSink.onion; - - // Valid query? - const validated$ = state$.map(state => state.form.validated) - - const vdom$ = xs.combine(state$, signatureCheckDom$, validated$) - .map( - ([state, checkdom, validated]) => { - const query = state.form.query - return div( - [ - div('.row .pink .darken-4 .white-text', { style: { padding: '20px 10px 10px 10px' } }, [ - // label('Query: '), - div('.Default .waves-effect .col .s1 .center-align', [ - i('.large .center-align .material-icons .pink-text', { style: { fontSize: '45px', fontColor: 'gray' } }, 'search'), - ]), - input('.Query .col s10 .white-text', { style: { fontSize: '20px' }, props: { type: 'text', value: query }, value: query }), - (validated) - ? div('.SignatureCheck .waves-effect .col .s1 .center-align', [ - i('.large .material-icons', { style: { fontSize: '45px', fontColor: 'grey' } }, ['play_arrow'])]) - : div('.SignatureCheck .col .s1 .center-align', [ - i('.large .material-icons .pink-text', { style: { fontSize: '45px', fontColor: 'grey' } }, 'play_arrow')]) - // ]) - ]), - div([ - (!validated || query == '') ? div([checkdom]) : div() - ]) - ]) - }); - - // Update in query, or simply ENTER - const newQuery$ = xs.merge( - domSource$.select('.Query').events('input').map(ev => ev.target.value), - // Ghost - state$.map(state => state.form.query).compose(dropRepeats()) - ) - - // Updated state is propagated and picked up by the necessary components - const click$ = domSource$.select('.SignatureCheck').events('click') - const enter$ = domSource$.select('.Query').events('keydown').filter(({ keyCode, ctrlKey }) => keyCode === ENTER_KEYCODE && ctrlKey === false); - const update$ = xs.merge(click$, enter$)//.debug(log) +} +function model(newQuery$, state$, sources, signatureCheckSink) { + // Set a default signature for demo purposes const setDefault$ = sources.DOM.select('.Default').events('click') const setDefaultReducer$ = @@ -102,7 +50,7 @@ function SignatureForm(sources) { }, }) } - }); + }) // Update the state when input changes const queryReducer$ = newQuery$.map(query => prevState => { @@ -132,10 +80,90 @@ function SignatureForm(sources) { // When update is clicked, update the query. Onionify does the rest const childReducer$ = signatureCheckSink.onion + return xs.merge( + defaultReducer$, + setDefaultReducer$, + queryReducer$, + invalidateReducer$, + childReducer$, + validateReducer$, + ) +} + +function view(state$, signatureCheckDom$) { + + // Valid query? + const validated$ = state$.map(state => state.form.validated) + + var result$ = xs.combine(state$, signatureCheckDom$, validated$) + .map( + ([state, checkdom, validated]) => { + const query = state.form.query + return div( + [ + div('.row .pink .darken-4 .white-text', { style: { padding: '20px 10px 10px 10px' } }, [ + // label('Query: '), + div('.Default .waves-effect .col .s1 .center-align', [ + i('.large .center-align .material-icons .pink-text', { style: { fontSize: '45px', fontColor: 'gray' } }, 'search'), + ]), + input('.Query .col s10 .white-text', { style: { fontSize: '20px' }, props: { type: 'text', value: query }, value: query }), + (validated) + ? div('.SignatureCheck .waves-effect .col .s1 .center-align', [ + i('.large .material-icons', { style: { fontSize: '45px', fontColor: 'grey' } }, ['play_arrow'])]) + : div('.SignatureCheck .col .s1 .center-align', [ + i('.large .material-icons .pink-text', { style: { fontSize: '45px', fontColor: 'grey' } }, 'play_arrow')]) + // ]) + ]), + div([ + (!validated || query == '') ? div([checkdom]) : div() + ]) + ]) + }) + return result$; +} + +function intent(domSource$) { + // Updated state is propagated and picked up by the necessary components + const click$ = domSource$.select('.SignatureCheck').events('click') + const enter$ = domSource$.select('.Query').events('keydown').filter(({ keyCode, ctrlKey }) => keyCode === ENTER_KEYCODE && ctrlKey === false); + const update$ = xs.merge(click$, enter$)//.debug(log) + + return { + update$: update$ + } +} + +function SignatureForm(sources) { + + const logger = loggerFactory('signatureForm', sources.onion.state$, 'settings.form.debug') + + const state$ = sources.onion.state$ + const domSource$ = sources.DOM; + const props$ = sources.props; + + // Check Signature subcomponent, via isolation + const signatureCheckSink = isolate(SignatureCheck, { onion: checkLens })(sources) + const signatureCheckDom$ = signatureCheckSink.DOM; + const signatureCheckHTTP$ = signatureCheckSink.HTTP; + const signatureCheckReducer$ = signatureCheckSink.onion; + + // Update in query, or simply ENTER + const newQuery$ = xs.merge( + domSource$.select('.Query').events('input').map(ev => ev.target.value), + // Ghost + state$.map(state => state.form.query).compose(dropRepeats()) + ) + + const reducers$ = model(newQuery$, state$, sources, signatureCheckSink) + + const vdom$ = view(state$, signatureCheckDom$) + + const actions$ = intent(domSource$) + // When GO clicked or enter -> send updated 'value' to sink // Maybe catch when no valid query? const query$ = xs.merge( - update$, + actions$.update$, // Ghost mode sources.onion.state$.map(state => state.form.ghost).filter(ghost => ghost).compose(dropRepeats()) ) @@ -149,14 +177,7 @@ function SignatureForm(sources) { signatureCheckSink.log ), DOM: vdom$, - onion: xs.merge( - defaultReducer$, - setDefaultReducer$, - queryReducer$, - invalidateReducer$, - childReducer$, - validateReducer$, - ), + onion: reducers$, HTTP: signatureCheckHTTP$, output: query$ }; diff --git a/src/js/components/SignatureGenerator.js b/src/js/components/SignatureGenerator.js index 54dac154..b24d4b33 100644 --- a/src/js/components/SignatureGenerator.js +++ b/src/js/components/SignatureGenerator.js @@ -11,6 +11,7 @@ import { loggerFactory } from '../utils/logger' import { stringify } from 'querystring'; import { GeneAnnotationQuery } from './GeneAnnotationQuery' import { absGene } from '../utils/utils' +import { busyUiReducer, dirtyWrapperStream } from "../utils/ui" const emptyData = { body: { @@ -21,20 +22,115 @@ const emptyData = { } const signatureLens = { - get: state => ({ core: state.form.signature, settings: state.settings }), + get: state => ({ core: state.form.signature, settings: state.settings, + ui: (state.ui??{}).signature ?? {dirty: false}, // Get state.ui.signature in a safe way or else get a default + }), set: (state, childState) => ({ ...state, form: { ...state.form, signature: childState.core } }) }; +function model(newInput$, request$, data$) { + + // Initialization + const defaultReducer$ = xs.of(prevState => ({ ...prevState, core: { input: '' } })) + // Add input to state + const inputReducer$ = newInput$.map(i => prevState => ({ ...prevState, core: { ...prevState.core, input: i } })) + // Add request body to state + const requestReducer$ = request$.map(req => prevState => ({ ...prevState, core: { ...prevState.core, request: req } })) + // Add data from API to state, update output key when relevant + const dataReducer$ = data$.map(newData => prevState => ({ ...prevState, core: { ...prevState.core, data: newData, output: newData.join(" ") } })) + // Logic and reducer stream that monitors if this component is busy + const busyReducer$ = busyUiReducer(newInput$, data$) + + return xs.merge( + defaultReducer$, + inputReducer$, + dataReducer$, + requestReducer$, + busyReducer$, + ) +} + +function view(state$, request$, response$, geneAnnotationQuery) { + + const validSignature$ = response$ + .map(r => r.body.result.join(" ")) + .filter(s => s != '') + + const invalidSignature$ = response$ + .map(r => r.body.result.join(" ")) + .filter(s => s == '') + + const signature$ = xs.merge(validSignature$, invalidSignature$).remember() + + const geneStyle = { + style: { + 'border-style': 'solid', + 'border-width': '1px', + 'margin': '2px 2px 2px 2px' + } + } + + const showGene = (thisGene) => + div('#' + absGene(thisGene) + '.col.orange.lighten-4.genePopup', geneStyle, [ + thisGene + ]) + + const validVdom$ = xs.combine(validSignature$, geneAnnotationQuery.DOM) + .map(([s, annotation]) => div('.card .orange .lighten-3', [ + div('.card-content .orange-text .text-darken-4', [ + span('.card-title', 'Signature:'), + div('.row', { style: { fontSize: "16px", fontStyle: 'bold' } }, + s.split(" ").map(gene => showGene(gene))), + annotation + ]) + ])) + .startWith(div('.card .orange .lighten-3', [])) + + const invalidVdom$ = invalidSignature$ + .map(_ => div('.card .orange .lighten-3', [ + div('.card-content .red-text .text-darken-1', [ + div('.row', { style: { fontSize: "16px", fontStyle: 'bold' } }, [ + p('.center', { style: { fontSize: "26px" } }, "The resulting signature is empty, please check the sample selection!") + ]) + ]) + ])) + .startWith(div('.card .orange .lighten-3', [])) + + const loadingVdom$ = request$.compose(sampleCombine(state$)) + .mapTo( + div('.card .orange .lighten-3', [ + div('.card-content .orange-text .text-darken-4', [ + span('.card-title', 'Signature:'), + div('.progress.orange.lighten-3.yellow-text', { style: { margin: '2px 0px 2px 0px'} }, [ + div('.indeterminate', {style : { "background-color" : 'orange' }}) + ]) + ]) + ])) + .startWith(div('.card .orange .lighten-3', [])) + .remember() + + // Wrap component vdom with an extra div that handles being dirty + const vdom$ = dirtyWrapperStream( state$, xs.merge(loadingVdom$, invalidVdom$, validVdom$) ) + + return { + vdom$: vdom$, + signature$: signature$ + } +} + +//function intent() {} + + /** * Generate a signature from a list of samples. - * + * * Input: List of samples (array) * Output: Signature (can be empty!) - * + * * Genes can be annotated (if Brutus is running). But the app is robust against Brutus not being online. - * + * * TODO: A lot of cleanup and rework is still required: - * + * * - Configure Brutus endpoint * - Isolate gene as a component */ @@ -79,94 +175,26 @@ function SignatureGenerator(sources) { ) .flatten() - const validSignature$ = response$ - .map(r => r.body.result.join(" ")) - .filter(s => s != '') - - const invalidSignature$ = response$ - .map(r => r.body.result.join(" ")) - .filter(s => s == '') - const data$ = response$ .map(r => r.body.result) - const geneStyle = { - style: { - 'border-style': 'solid', - 'border-width': '1px', - 'margin': '2px 2px 2px 2px' - } - } + const reducers$ = model(newInput$, request$, data$) - const showGene = (thisGene) => - div('#' + absGene(thisGene) + '.col.orange.lighten-4.genePopup', geneStyle, [ - thisGene - ]) + const views = view(state$, request$, response$, geneAnnotationQuery) - const validVdom$ = xs.combine(validSignature$, geneAnnotationQuery.DOM) - .map(([s, annotation]) => div('.card .orange .lighten-3', [ - div('.card-content .orange-text .text-darken-4', [ - span('.card-title', 'Signature:'), - div('.row', { style: { fontSize: "16px", fontStyle: 'bold' } }, - s.split(" ").map(gene => showGene(gene))), - annotation - ]) - ])) - .startWith(div('.card .orange .lighten-3', [])) - - const invalidVdom$ = invalidSignature$ - .map(_ => div('.card .orange .lighten-3', [ - div('.card-content .red-text .text-darken-1', [ - div('.row', { style: { fontSize: "16px", fontStyle: 'bold' } }, [ - p('.center', { style: { fontSize: "26px" } }, "The resulting signature is empty, please check the sample selection!") - ]) - ]) - ])) - .startWith(div('.card .orange .lighten-3', [])) - - const loadingVdom$ = request$.compose(sampleCombine(state$)) - .mapTo( - div('.card .orange .lighten-3', [ - div('.card-content .orange-text .text-darken-4', [ - span('.card-title', 'Signature:'), - div('.progress.orange.lighten-3.yellow-text', { style: { margin: '2px 0px 2px 0px'} }, [ - div('.indeterminate', {style : { "background-color" : 'orange' }}) - ]) - ]) - ])) - .startWith(div('.card .orange .lighten-3', [])) - .remember() - - const vdom$ = xs.merge(loadingVdom$, invalidVdom$, validVdom$) - - const signature$ = xs.merge(validSignature$, invalidSignature$).remember() - - // Initialization - const defaultReducer$ = xs.of(prevState => ({ ...prevState, core: { input: '' } })) - // Add input to state - const inputReducer$ = newInput$.map(i => prevState => ({ ...prevState, core: { ...prevState.core, input: i } })) - // Add request body to state - const requestReducer$ = request$.map(req => prevState => ({ ...prevState, core: { ...prevState.core, request: req } })) - // Add data from API to state, update output key when relevant - const dataReducer$ = data$.map(newData => prevState => ({ ...prevState, core: { ...prevState.core, data: newData, output: newData.join(" ") } })) return { log: xs.merge( logger(state$, 'state$'), geneAnnotationQuery.log ), - DOM: vdom$, - output: signature$, + DOM: views.vdom$, + output: views.signature$, HTTP: xs.merge( request$, geneAnnotationQuery.HTTP ), - onion: xs.merge( - defaultReducer$, - inputReducer$, - dataReducer$, - requestReducer$ - ), + onion: reducers$, modal: geneAnnotationQuery.modal } diff --git a/src/js/components/Table.js b/src/js/components/Table.js index d1164d89..f07616bb 100644 --- a/src/js/components/Table.js +++ b/src/js/components/Table.js @@ -49,6 +49,7 @@ import { loggerFactory } from "../utils/logger" import { convertToCSV } from "../utils/export" import delay from "xstream/extra/delay" import debounce from "xstream/extra/debounce" +import { dirtyWrapperStream } from "../utils/ui" // Granular access to the settings // We _copy_ the results array to the root of this element's scope. @@ -63,6 +64,7 @@ const headTableLens = { sourire: state.settings.sourire, filter: state.settings.filter, }, + ui: (state.ui??{}).headTable ?? {dirty: false}, // Get state.ui.headTable in a safe way or else get a default }), set: (state, childState) => ({ ...state, @@ -84,6 +86,7 @@ const tailTableLens = { sourire: state.settings.sourire, filter: state.settings.filter, }, + ui: (state.ui??{}).tailTable ?? {dirty: false}, // Get state.ui.tailTable in a safe way or else get a default }), set: (state, childState) => ({ ...state, @@ -105,6 +108,7 @@ const compoundContainerTableLens = { sourire: state.settings.sourire, filter: state.settings.filter, }, + ui: (state.ui??{}).compoundTable ?? {dirty: false}, // Get state.ui.compoundTable in a safe way or else get a default }), set: (state, childState) => ({ ...state, @@ -571,7 +575,8 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { div(".red .white-text", [p("An error occured !!!")]) ) - const vdom$ = xs.merge(initVdom$, errorVdom$, loadingVdom$.remember(), loadedVdom$) + // Wrap component vdom with an extra div that handles being dirty + const vdom$ = dirtyWrapperStream(state$, xs.merge(initVdom$, errorVdom$, loadingVdom$.remember(), loadedVdom$) ) // ======================================================================== diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index a44a0aa1..dea3b6b7 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -5,6 +5,7 @@ import xs from "xstream" import dropRepeats from "xstream/extra/dropRepeats" import debounce from "xstream/extra/debounce" import { loggerFactory } from "../utils/logger" +import { dirtyUiReducer } from "../utils/ui" const checkLens = { get: (state) => ({ @@ -281,6 +282,9 @@ function TreatmentCheck(sources) { .map(([_, state]) => state.core.input) .remember() + // Logic and reducer stream that monitors if this component became dirty + const dirtyReducer$ = dirtyUiReducer(query$, state$.map(state => state.core.input)) + return { log: xs.merge( logger(state$, "state$") @@ -294,7 +298,8 @@ function TreatmentCheck(sources) { dataReducer$, requestReducer$, setDefaultReducer$, - autocompleteReducer$ + autocompleteReducer$, + dirtyReducer$, ), output: query$, ac: ac$ diff --git a/src/js/pages/compound.js b/src/js/pages/compound.js index 42431148..9f04b343 100644 --- a/src/js/pages/compound.js +++ b/src/js/pages/compound.js @@ -16,6 +16,9 @@ import { import { scenario } from "../scenarios/treatmentScenario" import { runScenario } from "../utils/scenario" +import dropRepeats from "xstream/extra/dropRepeats" +import { equals } from "ramda" + export default function CompoundWorkflow(sources) { const logger = loggerFactory( "compound", @@ -47,6 +50,7 @@ export default function CompoundWorkflow(sources) { compoundAnnotations: state.settings.compoundAnnotations, treatmentLike: treatmentLikeFilter.COMPOUND, }, + ui: (state.ui ?? {} ).form ?? {}, }), set: (state, childState) => ({ ...state, form: childState.form }), } @@ -66,6 +70,28 @@ export default function CompoundWorkflow(sources) { } }) + // Use dropRepeats else the stream gets in an infinite loop + const uiReducer$ = state$.compose(dropRepeats(equals)) + .map(state => + prevState => { + const dirtyCheck = state.form.check.dirty + const dirtySampleSelection = state.form.sampleSelection.dirty + const busySignature = state.form.signature.busy + const dirtyFilter = state.filter.dirty + return ({...prevState, + ui: { + form: { + sampleSelection: {dirty: dirtyCheck }, + signature: {dirty: dirtyCheck || dirtySampleSelection }, + }, + headTable: {dirty: dirtyCheck || dirtySampleSelection || busySignature || dirtyFilter }, + tailTable: {dirty: dirtyCheck || dirtySampleSelection || busySignature || dirtyFilter }, + plots: {dirty: dirtyCheck || dirtySampleSelection || busySignature || dirtyFilter }, + }, + }) + } + ) + const TreatmentFormSink = isolate(TreatmentForm, { onion: formLens })(sources) const signature$ = TreatmentFormSink.output.remember() @@ -155,7 +181,8 @@ export default function CompoundWorkflow(sources) { filterForm.onion, headTable.onion, tailTable.onion, - scenarioReducer$ + scenarioReducer$, + uiReducer$ ), HTTP: xs.merge( TreatmentFormSink.HTTP, diff --git a/src/js/pages/genetic.js b/src/js/pages/genetic.js index 67f3b413..14e138bc 100644 --- a/src/js/pages/genetic.js +++ b/src/js/pages/genetic.js @@ -16,6 +16,9 @@ import { import { scenario } from "../scenarios/treatmentScenario" import { runScenario } from "../utils/scenario" +import dropRepeats from "xstream/extra/dropRepeats" +import { equals } from "ramda" + export default function GeneticWorkflow(sources) { const logger = loggerFactory( "genetic", @@ -47,6 +50,7 @@ export default function GeneticWorkflow(sources) { compoundAnnotations: state.settings.compoundAnnotations, treatmentLike: treatmentLikeFilter.GENETIC, }, + ui: (state.ui ?? {} ).form ?? {}, }), set: (state, childState) => ({ ...state, form: childState.form }), } @@ -66,6 +70,28 @@ export default function GeneticWorkflow(sources) { } }) + // Use dropRepeats else the stream gets in an infinite loop + const uiReducer$ = state$.compose(dropRepeats(equals)) + .map(state => + prevState => { + const dirtyCheck = state.form.check.dirty + const dirtySampleSelection = state.form.sampleSelection.dirty + const busySignature = state.form.signature.busy + const dirtyFilter = state.filter.dirty + return ({...prevState, + ui: { + form: { + sampleSelection: {dirty: dirtyCheck }, + signature: {dirty: dirtyCheck || dirtySampleSelection }, + }, + headTable: {dirty: dirtyCheck || dirtySampleSelection || busySignature || dirtyFilter }, + tailTable: {dirty: dirtyCheck || dirtySampleSelection || busySignature || dirtyFilter }, + plots: {dirty: dirtyCheck || dirtySampleSelection || busySignature || dirtyFilter }, + }, + }) + } + ) + const TreatmentFormSink = isolate(TreatmentForm, { onion: formLens })(sources) const signature$ = TreatmentFormSink.output.remember() @@ -155,7 +181,8 @@ export default function GeneticWorkflow(sources) { filterForm.onion, headTable.onion, tailTable.onion, - scenarioReducer$ + scenarioReducer$, + uiReducer$, ), HTTP: xs.merge( TreatmentFormSink.HTTP, diff --git a/src/js/utils/ui.js b/src/js/utils/ui.js new file mode 100644 index 00000000..360103c5 --- /dev/null +++ b/src/js/utils/ui.js @@ -0,0 +1,66 @@ +import { equals } from "ramda" +import xs from "xstream" +import dropRepeats from "xstream/extra/dropRepeats" +import debounce from 'xstream/extra/debounce' + +import { div, p } from "@cycle/dom" + +// Check committed output of a component with the intermediate value. +// If these are not the same then it suggests that the user made a change, hence the state is dirty. +function dirtyUiStream(output$, current$) { + return xs.combine(output$, current$) + .map(([output, current]) => !equals(output, current)) + .compose(debounce(10)) + .compose(dropRepeats(equals)) + .startWith(false) +} + +// Reducer dedicated to outputting the dirty state of a component into the component onion +export function dirtyUiReducer(output$, current$) { + + const dirty$ = dirtyUiStream(output$, current$) + + return dirty$.map((dirty) => (prevState) => ({ + ...prevState, + core: {...prevState.core, dirty: dirty }, + })) +} + +// Reducer dedicated to outputting the busy state of a component into the component onion +export function busyUiReducer(start$, finished$) { + + const busy$ = xs.merge(start$.mapTo(true), finished$.mapTo(false)) + + return busy$.map((busy) => (prevState) => ({ + ...prevState, + core: {...prevState.core, busy: busy }, + })) +} + +// Provide wrapper that encapsulates the inner portion with an extra div that sets opacity +// Supports setting 'debugName' which adds an extra div with text in it to display the current dirty state on the vdom +function dirtyWrapper(dirty, inner, debugName) { + const withDebug = ( + div({ style: { opacity: dirty ? 0.2 : 1.0 } },[ + div('.card .orange .lighten-3', [ p('.center', debugName + " dirty: " + dirty) ]), + inner + ]) + ) + + const withoutDebug = ( + div({ style: { opacity: dirty ? 0.2 : 1.0 } },[ + inner + ]) + ) + + return (debugName === undefined) ? withoutDebug : withDebug +} + +// Provide wrapper that encapsulates the inner portion with an extra div that sets opacity +// Assumes state$ to be the default component onion which includes _.ui.dirty +// inner$ is a stream with the regular component UI +// Supports setting 'debugName' which adds an extra div with text in it to display the current dirty state on the vdom +export function dirtyWrapperStream(state$, inner$, debugName) { + return xs.combine(state$, inner$) + .map(([state, inner]) => dirtyWrapper( (state.ui ?? {}).dirty ?? false, inner, debugName)) +} From e137c16cc7e896c51e8fde8213ac5f11950da64c Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Thu, 18 Nov 2021 10:04:46 +0100 Subject: [PATCH 039/191] Improve sampleSelection table (include significante genes) --- src/js/components/SampleSelection.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/js/components/SampleSelection.js b/src/js/components/SampleSelection.js index 99fe6bc4..442736d1 100644 --- a/src/js/components/SampleSelection.js +++ b/src/js/components/SampleSelection.js @@ -159,15 +159,15 @@ function SampleSelection(sources) { ), td( ".left-align" + selectedClass(entry.use), - entry.id.length > 30 ? entry.id.substring(0, 30) + "..." : entry.id + entry.id.length > 30 ? entry.id.substring(0, 40) + "..." : entry.id ), td(selectedClass(entry.use), entry.cell), td( selectedClass(entry.use), entry.dose.length > 6 ? entry.dose.substring(0, 6) + "..." : entry.dose ), - td(selectedClass(entry.use), entry.batch), - td(selectedClass(entry.use), entry.year), + // td(selectedClass(entry.use), entry.batch), + // td(selectedClass(entry.use), entry.year), td(selectedClass(entry.use), entry.time), td(selectedClass(entry.use), entry.significantGenes), ]) @@ -176,10 +176,10 @@ function SampleSelection(sources) { th(safeModelToUi("id", state.settings.common.modelTranslations)), th("Name"), th("Sample"), - th("Protocol"), - th("Conc"), - th("Batch"), - th("Year"), + th("Cell"), + th("Dose"), + // th("Batch"), + // th("Year"), th("Time"), th("Sign. Genes"), ]) From 9bf985181b0451de2f33a49c2872430a61a5c288 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 22 Nov 2021 22:46:46 +0100 Subject: [PATCH 040/191] Use renamed API call compoundToSamples -> treatmentToPerturbations (#67) --- src/js/components/SampleSelection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/components/SampleSelection.js b/src/js/components/SampleSelection.js index 442736d1..e2057d4b 100644 --- a/src/js/components/SampleSelection.js +++ b/src/js/components/SampleSelection.js @@ -108,7 +108,7 @@ function SampleSelection(sources) { return { url: state.settings.api.url + - "&classPath=com.dataintuitive.luciusapi.compoundToSamples", + "&classPath=com.dataintuitive.luciusapi.treatmentToPerturbations", method: "POST", send: { version: "v2", From 8f2b80513d2bfc3629593936654862e2972e5676 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Thu, 25 Nov 2021 10:20:20 +0100 Subject: [PATCH 041/191] Add plot names and axis names & tags (#70) Done for both Histograms and Similarity plots --- .../components/BinnedPlots/HistogramSpec.js | 19 +++++++++---- .../BinnedPlots/SimilarityPlotSpec.js | 28 +++++++++++++++---- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/js/components/BinnedPlots/HistogramSpec.js b/src/js/components/BinnedPlots/HistogramSpec.js index cf0866fc..c3840eeb 100644 --- a/src/js/components/BinnedPlots/HistogramSpec.js +++ b/src/js/components/BinnedPlots/HistogramSpec.js @@ -109,9 +109,11 @@ export const histogramVegaSpec = (data) => ({ "axes": [{ "orient": "bottom", "scale": "xscale", - "ticks": false, - "labels": false, - "domain": false + "ticks": true, + "labels": true, + "domain": false, + "title": "Bin size", + "titleColor": "grey" }, { "orient": "right", @@ -126,7 +128,9 @@ export const histogramVegaSpec = (data) => ({ "fill": { "value": "grey" } } } - } + }, + "title": "Zhang Score", + "titleColor": "grey" } ], "marks": [{ @@ -265,5 +269,10 @@ export const histogramVegaSpec = (data) => ({ // } // } // } - ] + ], + "title": { + "text": "Similarity Histogram", + "anchor": "middle", + "color": "grey" + } }) \ No newline at end of file diff --git a/src/js/components/BinnedPlots/SimilarityPlotSpec.js b/src/js/components/BinnedPlots/SimilarityPlotSpec.js index b122744e..1d2dc4e7 100644 --- a/src/js/components/BinnedPlots/SimilarityPlotSpec.js +++ b/src/js/components/BinnedPlots/SimilarityPlotSpec.js @@ -57,8 +57,17 @@ export const similarityPlotVegaSpec = (data) => ({ "orient": "bottom", "scale": "xscale", "domain": false, - "ticks": false, - "labels": false + "ticks": true, + "labels": true, + "encode": { + "labels": { + "update": { + "fill": { "value": "grey" } + } + } + }, + "title": "Bin Index", + "titleColor": "grey" }, { "orient": "left", @@ -73,7 +82,9 @@ export const similarityPlotVegaSpec = (data) => ({ "fill": { "value": "grey" } } } - } + }, + "title": "Zhang Score", + "titleColor": "grey" } ], @@ -85,7 +96,7 @@ export const similarityPlotVegaSpec = (data) => ({ "shape": { "value": "circle" }, "size": { "scale": "sizeScale", "field": "count" }, "x": { "scale": "xscale", "field": "x" }, - "y": { "scale": "yscale2", "field": "avg" }, + "y": { "scale": "yscale2", "field": "avg" } }, "update": { "fill": { "scale": "color", "field": "avg" }, @@ -96,8 +107,13 @@ export const similarityPlotVegaSpec = (data) => ({ "hover": { "fill": { "value": "white" }, "fillOpacity": { "value": 0.5 }, - "stroke": { "value": "grey" }, + "stroke": { "value": "grey" } } } - }] + }], + "title": { + "text": "Similarity Plot", + "anchor": "middle", + "color": "grey" + } }) \ No newline at end of file From d92eafbb6b490dbd76ede7488ddf17e2813f768f Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 25 Nov 2021 10:54:37 +0100 Subject: [PATCH 042/191] Page identification (#69) * Identify current page and highlight in the menu * Change active page style to bottom border instead of background * Move nav bar underline closer --- src/js/index.js | 7 ++++++- src/js/main.scss | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/js/index.js b/src/js/index.js index 06890cda..3d060675 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -48,7 +48,12 @@ export default function Index(sources) { '*': Home })(sources) - const makeLink = (path, label, options) => li([a(options, { props: { href: path } }, label)]); + const makeLink = (path, label, options) => { + const currentPage = window.location.href + const highlight = currentPage.endsWith(path) + + return li(highlight ? ".active" : "", [a(options, { props: { href: path } }, label)]) + } // TODO: Add a visual reference for ghost mode // const ghost$ = state$ diff --git a/src/js/main.scss b/src/js/main.scss index 107b3a3d..50633cdc 100644 --- a/src/js/main.scss +++ b/src/js/main.scss @@ -42,6 +42,11 @@ main { @extend .lighten-5; } +nav ul li.active span { + border-bottom-width: medium; + border-bottom-style: solid; + padding-bottom: 0.2em; +} /* home svg styling and hover */ From b02637f1404b2864c25575373198d234605c93ea Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Thu, 25 Nov 2021 11:22:14 +0100 Subject: [PATCH 043/191] Head table improvements and tweaks (#71) * Add header to top and bottom tables Using same style as table entries but with bold letters Mimics header in the sample selection table * Use images for treatment types in the head tables Scaling of images tested on Chrome and Firefox. * Fix bug where amount of lines in head table could keep increasing In some cases the settings can get updated which resulted in the head table amount increasing with each refresh Split off the default value from the accumulator and merge it after accumulation * Attempt at including the treatment type images into the bundle.js * Fix logic for table row increasing/decreasing again When making previous changes forgot that it prevented the user to go less than the default value. Added comments so that the next time we touch it it gives us a reminder * Tweak table content grid for different screen sizes * Don't allow lines to be split as it messes up key value pairs * remove 'text visualization' on medium or small screens also means we don't have to push/pull order the columns anymore so put them in the correct order straight away * Fix exporting of TSVs Convert filters array of objects to an array of strings, which can be serialized * Change options expand button when expanded Made icon slightly larger as it was very small and less clear than the + sign * Move table row data to separate lines on small or medium screens additional data rows have their own headers some extra separation of data and style in rows * Give visualization bit more space on small & medium screens * Also truncate treatment ID * Rework small/medium/large content flow with separate divs that enabled or disabled This way it is possible to use vertical alignment Also add a bit extra padding on small/medium screens - which was also difficult to organize in the previous way * Add JSDoc comments to some of the table component functions add 'out' to the .gitignore so that the output of JSDoc doesn't get picked up by git * Reduce distance between header text and the bottom border in table header --- .gitignore | 1 + images/treatmentTypes/CTL_UNTRT.CNS.png | Bin 0 -> 8330 bytes images/treatmentTypes/CTL_UNTRT.png | Bin 0 -> 5844 bytes images/treatmentTypes/CTL_VECTOR.CNS.png | Bin 0 -> 9985 bytes images/treatmentTypes/CTL_VECTOR.png | Bin 0 -> 8083 bytes images/treatmentTypes/CTL_VEHICLE.CNS.png | Bin 0 -> 8553 bytes images/treatmentTypes/CTL_VEHICLE.png | Bin 0 -> 6415 bytes images/treatmentTypes/TRT_CP.png | Bin 0 -> 4981 bytes images/treatmentTypes/TRT_LIG.png | Bin 0 -> 4645 bytes images/treatmentTypes/TRT_OE.MUT.png | Bin 0 -> 6278 bytes images/treatmentTypes/TRT_OE.png | Bin 0 -> 4826 bytes images/treatmentTypes/TRT_SH.CGS.png | Bin 0 -> 7484 bytes images/treatmentTypes/TRT_SH.png | Bin 0 -> 4890 bytes images/treatmentTypes/TRT_XPR.png | Bin 0 -> 5706 bytes src/js/components/SampleTable/SampleInfo.js | 645 +++++++++---------- src/js/components/SampleTable/SampleTable.js | 37 +- src/js/components/Table.js | 9 +- src/js/main.scss | 7 + src/js/utils/export.js | 23 + 19 files changed, 362 insertions(+), 360 deletions(-) create mode 100644 images/treatmentTypes/CTL_UNTRT.CNS.png create mode 100644 images/treatmentTypes/CTL_UNTRT.png create mode 100644 images/treatmentTypes/CTL_VECTOR.CNS.png create mode 100644 images/treatmentTypes/CTL_VECTOR.png create mode 100644 images/treatmentTypes/CTL_VEHICLE.CNS.png create mode 100644 images/treatmentTypes/CTL_VEHICLE.png create mode 100644 images/treatmentTypes/TRT_CP.png create mode 100644 images/treatmentTypes/TRT_LIG.png create mode 100644 images/treatmentTypes/TRT_OE.MUT.png create mode 100644 images/treatmentTypes/TRT_OE.png create mode 100644 images/treatmentTypes/TRT_SH.CGS.png create mode 100644 images/treatmentTypes/TRT_SH.png create mode 100644 images/treatmentTypes/TRT_XPR.png diff --git a/.gitignore b/.gitignore index b055722a..d4f28ec4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ specs .cache/ etc/gsk etc/jnj +out \ No newline at end of file diff --git a/images/treatmentTypes/CTL_UNTRT.CNS.png b/images/treatmentTypes/CTL_UNTRT.CNS.png new file mode 100644 index 0000000000000000000000000000000000000000..e8d23a20a74cd9c37dde01c8adce3a40024758cd GIT binary patch literal 8330 zcmdUV_aoKcAOCHLu5KD`0}+*(mDN>Nl)Wz3%r3h2mc7DTGAgn+_qx}($cT&(%Ff=( z-YZ;OuJ5^hzJI{?*Y6L>Ij`p#&vPEnaUM_M8fx+|C<7D%fxr|M+=eL1$!Nb$S(|{p)1Iqs-I71U$-R1dx^$Z@ zC}^8a!{i%&F87(k+_X_-WXU9rb>tt>ik^8)i}@#2I|~;F{=AZwaf4!<=sbf=#jy2i zb$@&i@)T8U`8FFZEqC!(PVHCIog0o_;tLNwW-3y-blNs6M?P3s*(3#*U+CZGIu zRy*8o=Z{=l&R==PVDZdgN&{^5QF#{{H}Q&8Rl=Pxs5_)8+R`D=qT(K$M>aH*=>q~v z%?uVDYQ%geHD$YO-fo^>rpr1{Y{mt|gD-(Ih@lA?h_lyFEhuI17APbX<-0a5p0?=7 z(PLo(V~s{$zSGBNh+}izty1K9D!@?r>t8ct+c+VRfa5e>g-Dw(^@|O{cVfDDm(QGn zKr-}%`|H09pDKUV(0{AcMCmpa1j2a}nn(Lqgsz;kp{%ajKG5*q%OA`@bm#G`j)q~j zkLCMwS{}oGRf3jYFFYvM!iav~U&9$jRF7JY0=mxMv=F_b1SW55M6@L6f}U&gyD#qG zDC+WB@o_;Q(j&d&eMxB-*NB`6)wC%LBhmC={?#?-CUoWXXKvn{Q>p4ooM0;fPGNhD z6UrSP%9Y%h^)sTS%U~mrQ+Cz;J|5gyDqWTHxuCr$_tb+2ONCOIK;$w`AbCMojB-O< zUkwlDCT%LsKasb(h+Byp@3~)a`XgvlfrJJmq%Y1s@UyZfl@{?z_DPFA=#&m*ucGSW zb3?TDr+%pp6arlV%z6*OaiXp2C(#0`E9Tg`>pBTCpS#3rdDMjBX2OytkJZ-pD=P}U zJ4^Pm=f5Sf_mL0lsa6CYX7siO3ReI-Gka z!L!hfG7_Kueljl9=QG%A?=DFa>pS``?&5QD-AJ;fis;*<^2N$Bwv$iK*Uf0M{>tEF zXLOCZ`_U19xFmqS#wYG^*5jL1X+B*D%=#rVU9V$5{Wr_n>YnpvsUf`?LU_`n`s}G& zd$r2b)_g$2sw86*BQgFVmkrx3EvyT~w&&z6tL6L{$7yuBAJ+Bj)58g_2+L$H^@(Y1 zV{nNY!pbg%y>u|l?tq(!QkiHv9qJ*!iD);@>zRA{=avLq_qfbM3Tl{POZ+rniO*eB z3X48*$Ko3kBg8z)?E36o9Ct5HsdKqrtnn&C{B0|-)b4z$Pt3qUP-3dWozX?zhhh`y z^RKR1&#GRxqFOw5@O>evPcqAC1+(71cdZ|wCbbVamNYV6V7eSdGW_~mv58pqWT~Xd zB!QQ%mWhE|!eF-S1XtKKv=_$M@!e#6e;|m{qGK;^WA?~5WZbl;v-LMR`*RTP30Fi6 zID*7&JSIXY-EUaK?-hFB9Nyh}U$P_9jwLxX|C1|&QgBq=kmY2JpT0a?JJYrO2wo!% zaPZba#<2)TFgF0F*EHWA6{YWEw8+K*i{LSJkLQjzYa#<7P6m{!%>x1yUXU>Vye_>g z@N+jT^(y`#hjHDjy0!35>}d$(dzTn;>axlN(mJwmVW#G56VZIa^`xyvv=v-ufE#<2 z_rjwGJQxvs#R2i^@2}=h-;17qgIqcbfhd$mT_%|dl=uEu)%PSh7qtsk1br8hZmPH# zR|=zpv5{@ofjWO|P!iw^a*fI1;naD|y0wo@*j7`KfBix(Z+IESUA}jL<`_5kk6NQo z2ryXf@wX(*@EF)yK-MNz-_Hu>nZ7QB(9Yi85V3jk8jz=N)|EXFrS5Cn8i8xs<{BVYoZ)oc5I{O|Vu6Yre;Xjk+}DV;phQ0CU2CO7(TZl@r0ZfC2Q z?NdoGrIK3iE?f)z5*zZ9!zB#>%`{MHB^Xz<2^0FXIInF!w9s{maYuM6lfYYt%!7^zo> z3I9`Xx3@y!#)ML}a(M$0s^zz)-q}F`VHiLH6Z2jLx7TEuF%kje-cCtB! zLMQzuCv-Mk!9oG2bJ!BBANrpdvb>xq8aU8a1shV5wcZmvCe1`*wD#2Ds7<=+=&iYKJhm$nBt9j|MOEGs1m?;A(*kHd9anA2ZF~tzi)~5u5Vu?x zUdY!7tMIj*JopK9@$uh_4Mb+7Uf6-d*FH%L0C`P?mjWX84<#nOzN-|~PU2#p$*|fq z8L7wHQv7dsDl~ox)NjQPPxvSo~23reBb{f{pJD?@7xE#zYJH9uh_1GhKVf zV*n?v*H)Qc?SIps1F(hg6e#*O3#*V;NGbfFnIn&1NC4o>aJ{7R4_ZMWIzbBlX%GOX zq2w9GryD?Ug6Q0)$bEyJrC?Ss(3BGQc>cc_6k?nLGNm4)+I%&^V?M;Al}b9olugve zNze>KAaGEf)gQCR=#sKG%5yhIT%$~(dPOI2SrtNC-K|KKQX!~&6aes#gU`PF1p>bE zuny8f#JzpYLHA%b=6Kil))aNLgTdMKFb-XX#VGR>0`k%1Tpqw7#(CONOK{y*%WdgD zwdikFyla@f4A_+i(NcmDo)z zOHwx(v%FY$WVS4IA*Iav$MiFc4p-XI)3?9B%(o}Hk&{_Gn^*S_Os*ibT@0*!r-pQg z=8i^OHb|j~j@>F#c8tt=c1bKoCS`jenhAB&LzQkv5#n271e3un-_3o@(@oFULEU29 ztrz{xF4ophd7gP(N(w*}V{nEJKXTn`+Cv(9H~s6!FOcSk`GS#F6$GEwRq})hEqP2) z*}wN{Q=Lcn&4V@N&VUlD-9ED}`}cf;tN*#1lPAinz{sqzkA_2U5fdX(%a8%?%X%9YZ*J>Y`KuJ*r6 zT(~vU?bmNFI#K`NeVem70NnRyw+P~9jc9bw0h5qN(qH(~o&X8q7ZTQgE-Z}C%f>aS z_#UQ@qUY2b6bFVtcpP6N-(EWokJ@^3QuXUXvIwR!(@l3_ zeu70RK52-(fnK`6Hs86-Zi4Z^H%+pjSNC``v>{tt;j74T_9Nle&e_WkR8GlcGevof zqKC=T^7cRV8~V2F$bUuqDm*I@J)1Jvz}94b#7@$dzP~IKrFE@S6n-@{nfI@*>q=g3 zxix;q`B%3+jjs{h*(cI)peRB3@y?A1{KT2G^r>vRQ%-CNr~iwaTopFCkMk^*8C&-i z+$LS<3e!?ha`rR#h~B+8b0SfjxR?I8~iT<$Od}bg}R| zZo7im>_H0^OH#)3AH(L8u4T}P^p@?11YKVr9!#H=jq7cX?`#)uf##^TPYHCY7w{}T ze3fTxB7V-`uBiFCu+Vl3R~;#OT2}fw@B-#ymXkxY>pt%v^S=;I-o@aS>9ySYzV}o) zF_0w&{e)3wr#PPA8kJ*CYtavghG4~XHym|#)q&r;o>JpDn%WavL4L6;-^8~q+$lW%IZRFHWUwDvk%yBxgUi>IPwnn* zkO%d15)}Uk+nQ(_Qjqfm4nr9~#yh`Kpk2aoGj6M^`8tjGO zDPms7-3l)~dU#b8^&`jh)DK}oPm7(@a7JH=TqPtyPDph5PW*Bw>UE87LU#mPS4u)(|Nyw z`#I8>5Ze2n-{O3EtT?xyS+~)?6a)*QpgNU(%F5m>>yKP=CBKYWkVAE~GyQ7Tv6ax$ zZo;6iRbNl~&iaBs>{Yr6Wfg_*)ao_M-g-ut&*if2W5x#NR4x;Wt-HXh^ zSjAM@tXu2g$w&|P-obRH5#MjbT1=%Qs?3pcgf#Z?(<6tv^5Ipua0I?~0qH?ULQ&$483GrOtN+ z4#Z{f`K>;>utW`jes}%%@-2N-6bJ?9IM_pW*cl7j%I!zA*29{i@QY=4LkjS?+dzvvCDu za5eB|BCN{4z0g1(pgQUQo65u{F9{PdsU56KKYFk_)plI0cjq-#5io^(dYj==;3^pP#jTr30$lRalwVE7Vb0$Jm9 z(eOr{S(XvcZ*3LNe$JESA~KCn)*U`ylk=4B=@5bC6BVFx8_yW%bP4!sb3f(8z>zTGlxy$~f)&|+timgfS z;)OHb&jy~$4fL}y81!Gt4rK^#BDTPC8Srl=E)u2;uUY-rBgR1s zxkj9b3d=KxEU&-1bJTn1$DY@xc999J<*!@m zFjRd6Hpb(Lit5Om{=1%LHhDMU+Wpt}FnKto(XxH2YvK(C=vDO^R=*Hyqc2aq&xS=J z6QTmd8xL+89SV1zip%tY3?i{9RpzK$4!Ev-B}O9=1zJ(z)x1CQPjC}TzBYM2IptEn zY?NjG#u0d(DiYet-%h=;6^VOBxsg8Xi3;BNwx5@f_N4*F?8ZeKA!e7!MUhn+Qe4xZ z#({#j|1_1holx!{(({@CJo+)(@L`*VBqSeqr;Dl4Ikdq3R;!L2RUV;;XWv8H=_ixu zs!I!3R}5R+uii{5yE*r&VdRp!u$WHYCGD+3^3cz&lo0_hKM=9g@ds1d0G0fDb2o_D zHVE|7iY0Pf9rbVS=ebzjms6!alezuroEcs`2QGFZb8EqPUQBNQFdQ$u^NWjyxKYin zp$}2Plbb=ZAb0UvAVB3jDGtJNhl%>gZ@n?< zavClWTzwaVY=DiCcKEw9eYDOUho?FT&!-7cQv53xNWNpmP0u!vlIq`VJdcrX(~e?EQV1qRF)WfKA8rV!jj`Bo zN+jW#=8O-xMD3)RnJ&(L!A9uJQ}ay0ueN!-tiytZC+bPyEqsQ+oT^|bE-OHFpLmd_ zV{*ou_-NqmIqK`k?_33Kf*_V(u~DyjeRAD@M?x#g`_QMx9!uzKszX$a(G@gB{&Vims~Gol`i$>+9W z33h+WQLHNQv-dsbdeiUfYy7z0Kwmum-YET$Ay4eX-4u#H)|3JSQ5H3yZTV}xxS7qv zTK&pQlJM%!JiJ@(u_0*@MN=l`Lz{qK@ggT9CL#k*uimiZ>?VKzgfmN|!350Neu>S{vA2f1 zWA-9D!&$!gq;0S`@uz~5MdHL2?h z$v=J*NJR0Y{X(Nbrn2Ig?)-6!b*bNiF#| z$NXn7C}g&<_q+YV+?mC5uw<6ey^@XVGu5t>;MMIjWpl&-ZK7iu0G_U8(!M zPV1#!;v@*}Qz2Y}-)>7pI7)c|XCTggNu@k%sHIB<_|MHW{W%)Gei_#w-A+Lt&czR> zhhLr?uN6>*c&XYHSv_Zc#`0QBYFkN%5hLj$e?K~1vGqk;)`-s8hgUWIvjY6V+v1_% z2Mrg^tZr31x+tHuqxwn!Q!8*ON@pt{2(AZ(m|wiAdej`LUl~Gm+0gcwAa!6cO_kG~ zY*=XLGKwbUnSL=N<&E!{sh=tka?(ll0931fwqQ~dv()Iu9ZCM>m$Et zJMn8pd}5ZUgFsb9pZ_d@Z|i?vdD0%7#k&3^r@4OI_zhs#Hi`yUv91j!WJph6~Gp$lzPH zn9s)^hhvn^M&}1D$N3*c>ydKLpSWc6-)YbN#I;G)Fwh=@toDeGcyP=v;^JT-g zg523;>o%>*7pidNNyVQctcCZw(UPM7hm|kO<(Y_P=v?y!aPOs zo^M8#aF?RRTN1rS`sg7CNQH5YD)F*XoSK>mL(wn4Y-$Ecn$+ufvG_pUXwkT!o32ScPrMb|{bhBjcZM^JrV`HR9<#)!cb5w{mu-*=evBMb>yi|RY$fRj$Vx+?M^ zLptfFVHJTt)qBm)`2{ghEt=rOlZ}mJdcn(iHHw22 zk@RXN$y*6R)DIp~_h&C$+ItLLve@~YBx$;`yr}{#d|91XI6V9A?rgObe@-Y@8J!r{ zYU|B5YNQG(53uPk?3~k->ux3CS#9jS9+zkKztBcpI&pIFaxUBut%yEU8oqeW%y#&b z-B>MuQoI$f+cT5xI0u&OAxR+qEBsP`UMZj1 zyvWeMhaUc8Gf`TB+Qe*D4wP!IT2rthEd0~wHDv|>a_U-Rm(H0T+t;S$B4eevTa5O{ zwO1W?0<$>?99CA*97E#7TuB~SQ(I!zx_V8(A@(PoII6Rl3K;@rwn(ldw$J6DWUtxq zzgcn@rIXIwnKyCklXC%lLziDbDvS|-=kd;@B;Y+fx~lRZS=b3Eco3#KysVbIVeQQ} zwlHJ8zL>FOky1z~YnLvn%7!Y2O-^m7m^=g=(_P8cYsqB3bDiS#*(c17GAJwxx%(%5 z11(Z$xEI?dXbi$`+kfnB9-{A=rH|;JDgBYntVBPiop>wj&jDz-`@1vB6rMEzJ}A)o z|3*+|m{z~n0xV}h4p%~jGli&+5&pl7I#pk43Wd?UTk!sW%x3XEs9!-uUp@9zQ!czF zRqiBC5zk1SoaVq&SWyL~F;LfvG9CSa1}*6)DQsN-*!;VlTpz?6T-B7G6H0%jD_DU7$1p@mkD8@RmoWPBbf;_#M2Ar_(es z!gi`JIE#XhJ+S2YvVOJo<)LbZHsUMRe)fM`h323$MGlUXjNw`?S=QpcH$MNmq17+~ z21uu2IzTx%p*wosP*7yOlzj7kC{Rv1&AwYKHt9(a4slsc>g3kt%q zrcfwr8>%p5U#h7!4HnGvqtGz3-&HJup^>7;(WuuIB~{k^Z5JG<5DT}w`rNOW0_wv7 zr8&xxgY>06Tq@DQIBZ?D+K|NYDgOvCHIdzI7B?;}!HAFE6u!(;EG6c?&U%Eo9@ z#I?%)_XW#g`VN83qodSz%+W5iQOel~%1O$Gumd%5=j literal 0 HcmV?d00001 diff --git a/images/treatmentTypes/CTL_UNTRT.png b/images/treatmentTypes/CTL_UNTRT.png new file mode 100644 index 0000000000000000000000000000000000000000..d2c04ebcc1d85879c2f39b19040be1fbbe998f1f GIT binary patch literal 5844 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 UQM&#CZE8s8j^XVxb%&?_2eg#->Hq)$ literal 0 HcmV?d00001 diff --git a/images/treatmentTypes/CTL_VECTOR.CNS.png b/images/treatmentTypes/CTL_VECTOR.CNS.png new file mode 100644 index 0000000000000000000000000000000000000000..91d8e0f3983551f0e69af6688fa966345eccad43 GIT binary patch literal 9985 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 KQlk9)-Twg1j*4&q literal 0 HcmV?d00001 diff --git a/images/treatmentTypes/CTL_VEHICLE.CNS.png b/images/treatmentTypes/CTL_VEHICLE.CNS.png new file mode 100644 index 0000000000000000000000000000000000000000..3ac3a11af58b56762efd5ad3340d5ec83773ae46 GIT binary patch literal 8553 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 bx=xfXFS7O|wetx9v=o|ZdaC6rFFyPa2P&Gk literal 0 HcmV?d00001 diff --git a/images/treatmentTypes/CTL_VEHICLE.png b/images/treatmentTypes/CTL_VEHICLE.png new file mode 100644 index 0000000000000000000000000000000000000000..0dd8ed5c8055b493bbe0d4a52bfe4a1ceb5178ef GIT binary patch literal 6415 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 literal 0 HcmV?d00001 diff --git a/images/treatmentTypes/TRT_CP.png b/images/treatmentTypes/TRT_CP.png new file mode 100644 index 0000000000000000000000000000000000000000..58428adc43e3fc33d63b98f3c98bc2c50448afc0 GIT binary patch literal 4981 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 literal 0 HcmV?d00001 diff --git a/images/treatmentTypes/TRT_LIG.png b/images/treatmentTypes/TRT_LIG.png new file mode 100644 index 0000000000000000000000000000000000000000..7f7096beeaebb48fcf940e6932c9d43cf382cf56 GIT binary patch literal 4645 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 literal 0 HcmV?d00001 diff --git a/images/treatmentTypes/TRT_SH.png b/images/treatmentTypes/TRT_SH.png new file mode 100644 index 0000000000000000000000000000000000000000..cc29d3546fd94932d512a526f3b16a164d040792 GIT binary patch literal 4890 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 LuUV>Y^Z0)NSV8f3{XN4 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 literal 0 HcmV?d00001 diff --git a/src/js/components/SampleTable/SampleInfo.js b/src/js/components/SampleTable/SampleInfo.js index e1b8fdee..2cd77464 100644 --- a/src/js/components/SampleTable/SampleInfo.js +++ b/src/js/components/SampleTable/SampleInfo.js @@ -9,6 +9,56 @@ import { import { merge } from "ramda" import { safeModelToUi } from "../../modelTranslations" +import img_trt_cp from "/images/treatmentTypes/TRT_CP.png" +import img_trt_lig from "/images/treatmentTypes/TRT_LIG.png" +import img_trt_sh from "/images/treatmentTypes/TRT_SH.png" +import img_trt_sh_cgs from "/images/treatmentTypes/TRT_SH.CGS.png" +import img_trt_oe from "/images/treatmentTypes/TRT_OE.png" +import img_trt_oe_mut from "/images/treatmentTypes/TRT_OE.MUT.png" +import img_trt_xpr from "/images/treatmentTypes/TRT_XPR.png" +import img_ctl_vehicle from "/images/treatmentTypes/CTL_VEHICLE.png" +import img_ctl_vector from "/images/treatmentTypes/CTL_VECTOR.png" +//import img_trt_sh_css from "/images/treatmentTypes/TRT_SH.CSS.png" // MISSING! +import img_ctl_vehicle_cns from "/images/treatmentTypes/CTL_VEHICLE.CNS.png" +import img_ctl_vector_cns from "/images/treatmentTypes/CTL_VECTOR.png" +import img_ctl_untrt_cns from "/images/treatmentTypes/CTL_UNTRT.CNS.png" +import img_ctl_untrt from "/images/treatmentTypes/CTL_UNTRT.png" + +/** + * Create a header matching the data order of sample rows displayed in case of large displays + * header hides itself in case of small and medium screens + * @function SampleInfoHeader + * @param {string} bgcolor color of the table background + * @param {string} color color of the table foreground + * @returns VNode li element with header data + */ + +export function SampleInfoHeader(bgcolor, color) { + return li( + ".collection-item .hide-on-med-and-down .zoom", + { style: { backgroundColor: bgcolor, borderBottom: "2px solid " + color} }, + [ + div(".row", { style: { fontWeight: "small", marginBottom: "5px" } }, [ + div(".col .s1 .left-align", { style: { fontWeight: "bold" } }, ["Zhang Score"]), + div(".col .s2", { style: { fontWeight: "bold" } }, ["Sample ID"]), + div(".col .s1", { style: { fontWeight: "bold" } }, ["Cell"]), + div(".col .s2", { style: { fontWeight: "bold" } }, ["Treatment ID"]), + div(".col .s3", { style: { fontWeight: "bold" } }, ["Treatment Name"]), + div(".col .s1", { style: { fontWeight: "bold" } }, ["Treatment Type"]), + div(".col .s2 .center-align", { style: { fontWeight: "bold" } }, ["Visualization"]), + ]) + ]) +} + +/** + * Create a single table row displaying sample information + * Depending on the width displays basic information in one line or multiple lines + * When clicked, adds more information in a separate div + * @function SampleInfo + * @param {stream} sources.onion.state$ stream of sample data to be displayed + * @param sources.DOM user click events + * @param sources.props semi-static settings + */ export function SampleInfo(sources) { const state$ = sources.onion.state$ const props$ = sources.props @@ -22,7 +72,7 @@ export function SampleInfo(sources) { return [ span( ".col .s4 .grey-text.text-darken-1", - { style: { fontWeight: "lighter" } }, + { style: { fontWeight: "lighter", whiteSpace: "nowrap"} }, key ), span( @@ -35,8 +85,8 @@ export function SampleInfo(sources) { function entrySmall(key, value) { return [ - span(".col .s6 .l2", { style: { fontWeight: "lighter" } }, key), - span(".col .s6 .l2", value.length != 0 ? value : ""), + span(".col .s4 .m2", { style: { fontWeight: "lighter" } }, key), + span(".col .s8 .m4", value.length != 0 ? value : ""), ] } @@ -51,144 +101,182 @@ export function SampleInfo(sources) { return url } + + /** + * Constant lambda function to create a data row for a sample + * Uses materialize.css grid features to display basic sample data in a row + * Depending on the width of the screen the content is either in one single line + * or details get spread into multiple lines + * @function row + * @param {object} sample the data to be displayed + * @param {object} props semi-static settings for ie. sourire url or background colors + * @param {style} blur component style to contain blur settings + * @param {boolean} zoom boolean to alter component content depending if the rowDetails are expanded or not + * @return {object} object with members for each treatment type, each has wrapping div with vdom elements + */ const row = (sample, props, blur, zoom) => { let zhangRounded = sample.zhang != null ? parseFloat(sample.zhang).toFixed(3) : "NA" - return { - trt_cp: div(".row", { style: { fontWeight: "small" } }, [ - div(".col .s1 .left-align", { style: { fontWeight: "bold" } }, [ - zhangRounded, - ]), - div( - ".col .s2", - { style: { overflow: "hidden", "text-overflow": "ellipsis" } }, - [sample.id] - ), - div(".col .s1", [sample.cell]), - div(".col .s2", { style: blur }, [ - sample.trt_id != "NA" ? sample.trt_id : "", - ]), - div(".col .s3", { style: blur }, [sample.trt_name]), - div(".col .s1", { style: blur }, [sample.trt]), - div(".col .s2 .center-align", { style: blur }, [ - sample.smiles != null && - sample.smiles != "N/A" && - sample.smiles != "No Smiles" && - zoom == false - ? img({ - props: { - src: sourireUrl(props.sourire.url, sample.smiles), + + const imgForTrt= (trt) => { + let knownTrt = { + "trt_cp": img(".trt_img", { props: { alt: trt, src: img_trt_cp }}), + "trt_lig": img(".trt_img", { props: { alt: trt, src: img_trt_lig }}), + "trt_sh": img(".trt_img", { props: { alt: trt, src: img_trt_sh }}), + "trt_sh.cgs": img(".trt_img", { props: { alt: trt, src: img_trt_sh_cgs }}), + "trt_oe": img(".trt_img", { props: { alt: trt, src: img_trt_oe }}), + "trt_oe.mut": img(".trt_img", { props: { alt: trt, src: img_trt_oe_mut }}), + "trt_xpr": img(".trt_img", { props: { alt: trt, src: img_trt_xpr }}), + "ctl_vehicle": img(".trt_img", { props: { alt: trt, src: img_ctl_vehicle }}), + "ctl_vector": img(".trt_img", { props: { alt: trt, src: img_ctl_vector }}), + //"trt_sh.css": img(".trt_img", { props: { alt: trt, src: img_trt_sh_css }}), // MISSING! + "ctl_vehicle.cns": img(".trt_img", { props: { alt: trt, src: img_ctl_vehicle_cns }}), + "ctl_vector.cns": img(".trt_img", { props: { alt: trt, src: img_ctl_vector_cns }}), + "ctl_untrt.cns": img(".trt_img", { props: { alt: trt, src: img_ctl_untrt_cns }}), + "ctl_untrt": img(".trt_img", { props: { alt: trt, src: img_ctl_untrt }}), + "_default": p([trt]) + } + + return knownTrt[trt] ?? knownTrt["_default"] + } + + const imgForTrtPart = [ imgForTrt(sample.trt) ] + + const visualizeTextPart = [ + sample.trt_name != null && sample.trt_name != "N/A" && zoom == false + ? span( + { + style: { + color: "black", + opacity: 0.4, + "font-size": "clamp(16px, 5vw, 26px)", height: 50, + display: "block", + "font-family": "Nova Mono", "object-fit": "contain", + fontWeight: "bold", }, - }) - : "", + }, + [sample.trt_name] + ) + : "", + ] + + const visualizeSmilesPart = [ + sample.smiles != null && + sample.smiles != "N/A" && + sample.smiles != "No Smiles" && + zoom == false + ? img({ + props: { + src: sourireUrl(props.sourire.url, sample.smiles), + height: 50, + "object-fit": "contain", + }, + }) + : "", + ] + + return { + trt_cp: div(".row", { style: { fontWeight: "small" } }, [ + div(".valign-wrapper", [ + div(".col .s2 .l1 .left-align", { style: { fontWeight: "bold" } }, [ + zhangRounded, + ]), + + div(".col .l2 .hide-on-med-and-down .truncate", [sample.id]), + div(".col .l1 .hide-on-med-and-down", [sample.cell]), + div(".col .l2 .hide-on-med-and-down .truncate", { style: blur }, [ sample.trt_id != "NA" ? sample.trt_id : "" ]), + div(".col .l3 .hide-on-med-and-down", { style: blur }, [sample.trt_name]), + + div(".col .s2 .offset-s5 .l1", { style: blur }, imgForTrtPart), + div(".col .s3 .l2 .center-align", { style: blur }, visualizeSmilesPart), ]), + div(".hide-on-large-only", {style: {paddingTop: "10px"}}, [ + div(".col .s4 .m3 .offset-m1", {style: {whiteSpace: "nowrap"}}, ["Sample ID"]), + div(".col .s8 .truncate", [sample.id]), + div(".col .s4 .m3 .offset-m1", ["Cell"]), + div(".col .s8", [sample.cell]), + div(".col .s4 .m3 .offset-m1", {style: {whiteSpace: "nowrap"}}, ["Treatment ID"]), + div(".col .s8 .truncate", { style: blur }, [ sample.trt_id != "NA" ? sample.trt_id : "" ]), + div(".col .s4 .m3 .offset-m1", {style: {whiteSpace: "nowrap"}}, ["Treatment Name"]), + div(".col .s8", { style: blur }, [sample.trt_name]) + ]) ]), trt_sh: div(".row", { style: { fontWeight: "small" } }, [ - div(".col .s1 .left-align", { style: { fontWeight: "bold" } }, [ - zhangRounded, - ]), - div( - ".col .s2", - { style: { overflow: "hidden", "text-overflow": "ellipsis" } }, - [sample.id] - ), - div(".col .s1", [sample.cell]), - div(".col .s2", { style: blur }, [ - sample.trt_id != "NA" ? sample.trt_id : "", - ]), - div(".col .s3", { style: blur }, [sample.trt_name]), - div(".col .s1", { style: blur }, [sample.trt]), - div(".col .s2 .center-align", { style: blur }, [ - sample.trt_name != null && sample.trt_name != "N/A" && zoom == false - ? span( - { - style: { - color: "black", - opacity: 0.4, - "font-size": "clamp(16px, 5vw, 26px)", - height: 50, - display: "block", - "font-family": "Nova Mono", - "object-fit": "contain", - fontWeight: "bold", - }, - }, - [sample.trt_name] - ) - : "", + div(".valign-wrapper", [ + div(".col .s2 .l1 .left-align", { style: { fontWeight: "bold" } }, [ + zhangRounded, + ]), + + div(".col .l2 .hide-on-med-and-down .truncate", [sample.id]), + div(".col .l1 .hide-on-med-and-down", [sample.cell]), + div(".col .l2 .hide-on-med-and-down .truncate", { style: blur }, [ sample.trt_id != "NA" ? sample.trt_id : "" ]), + div(".col .l3 .hide-on-med-and-down", { style: blur }, [sample.trt_name]), + + div(".col .s2 .offset-s5 .l1", { style: blur }, imgForTrtPart), + div(".col .s3 .l2 .center-align", { style: blur }, visualizeTextPart), ]), + div(".hide-on-large-only", {style: {paddingTop: "10px"}}, [ + div(".col .s4 .m3 .offset-m1", {style: {whiteSpace: "nowrap"}}, ["Sample ID"]), + div(".col .s8 .truncate", [sample.id]), + div(".col .s4 .m3 .offset-m1", ["Cell"]), + div(".col .s8", [sample.cell]), + div(".col .s4 .m3 .offset-m1", {style: {whiteSpace: "nowrap"}}, ["Treatment ID"]), + div(".col .s8 .truncate", { style: blur }, [ sample.trt_id != "NA" ? sample.trt_id : "" ]), + div(".col .s4 .m3 .offset-m1", {style: {whiteSpace: "nowrap"}}, ["Treatment Name"]), + div(".col .s8", { style: blur }, [sample.trt_name]) + ]) ]), trt_lig: div(".row", { style: { fontWeight: "small" } }, [ - div(".col .s1 .left-align", { style: { fontWeight: "bold" } }, [ - zhangRounded, - ]), - div( - ".col .s2", - { style: { overflow: "hidden", "text-overflow": "ellipsis" } }, - [sample.id] - ), - div(".col .s1", [sample.cell]), - div(".col .s2", { style: blur }, [ - sample.trt_id != "NA" ? sample.trt_id : "", - ]), - div(".col .s3", { style: blur }, [sample.trt_name]), - div(".col .s1", { style: blur }, [sample.trt]), - div(".col .s2 .center-align", { style: blur }, [ - sample.trt_name != null && sample.trt_name != "N/A" && zoom == false - ? span( - { - style: { - color: "black", - opacity: 0.4, - "font-size": "clamp(16px, 5vw, 26px)", - height: 50, - display: "block", - "font-family": "Nova Mono", - "object-fit": "contain", - fontWeight: "bold", - }, - }, - [sample.trt_name] - ) - : "", + div(".valign-wrapper", [ + div(".col .s2 .l1 .left-align", { style: { fontWeight: "bold" } }, [ + zhangRounded, + ]), + + div(".col .l2 .hide-on-med-and-down .truncate", [sample.id]), + div(".col .l1 .hide-on-med-and-down", [sample.cell]), + div(".col .l2 .hide-on-med-and-down .truncate", { style: blur }, [ sample.trt_id != "NA" ? sample.trt_id : "" ]), + div(".col .l3 .hide-on-med-and-down", { style: blur }, [sample.trt_name]), + + div(".col .s2 .offset-s5 .l1", { style: blur }, imgForTrtPart), + div(".col .s3 .l2 .center-align", { style: blur }, visualizeTextPart), ]), + div(".hide-on-large-only", {style: {paddingTop: "10px"}}, [ + div(".col .s4 .m3 .offset-m1", {style: {whiteSpace: "nowrap"}}, ["Sample ID"]), + div(".col .s8 .truncate", [sample.id]), + div(".col .s4 .m3 .offset-m1", ["Cell"]), + div(".col .s8", [sample.cell]), + div(".col .s4 .m3 .offset-m1", {style: {whiteSpace: "nowrap"}}, ["Treatment ID"]), + div(".col .s8 .truncate", { style: blur }, [ sample.trt_id != "NA" ? sample.trt_id : "" ]), + div(".col .s4 .m3 .offset-m1", {style: {whiteSpace: "nowrap"}}, ["Treatment Name"]), + div(".col .s8", { style: blur }, [sample.trt_name]) + ]) ]), ctl_vector: div(".row", { style: { fontWeight: "small" } }, [ - div(".col .s1 .left-align", { style: { fontWeight: "bold" } }, [ - zhangRounded, - ]), - div( - ".col .s2", - { style: { overflow: "hidden", "text-overflow": "ellipsis" } }, - [sample.id] - ), - div(".col .s1", [sample.cell]), - div(".col .s2", { style: blur }, [ - sample.trt_id != "NA" ? sample.trt_id : "", - ]), - div(".col .s3", { style: blur }, [sample.trt_name]), - div(".col .s1", { style: blur }, [sample.trt]), - div(".col .s2 .center-align", { style: blur }, [ - sample.trt_name != null && sample.trt_name != "N/A" && zoom == false - ? span( - { - style: { - color: "black", - opacity: 0.4, - "font-size": "clamp(16px, 5vw, 26px)", - height: 50, - display: "block", - "font-family": "Nova Mono", - "object-fit": "contain", - fontWeight: "bold", - }, - }, - [sample.trt_name] - ) - : "", + div(".valign-wrapper", [ + div(".col .s2 .l1 .left-align", { style: { fontWeight: "bold" } }, [ + zhangRounded, + ]), + + div(".col .l2 .hide-on-med-and-down .truncate", [sample.id]), + div(".col .l1 .hide-on-med-and-down", [sample.cell]), + div(".col .l2 .hide-on-med-and-down .truncate", { style: blur }, [ sample.trt_id != "NA" ? sample.trt_id : "" ]), + div(".col .l3 .hide-on-med-and-down", { style: blur }, [sample.trt_name]), + + div(".col .s2 .offset-s5 .l1", { style: blur }, imgForTrtPart), + div(".col .s3 .l2 .center-align", { style: blur }, visualizeTextPart), ]), + div(".hide-on-large-only", {style: {paddingTop: "10px"}}, [ + div(".col .s4 .m3 .offset-m1", {style: {whiteSpace: "nowrap"}}, ["Sample ID"]), + div(".col .s8 .truncate", [sample.id]), + div(".col .s4 .m3 .offset-m1", ["Cell"]), + div(".col .s8", [sample.cell]), + div(".col .s4 .m3 .offset-m1", {style: {whiteSpace: "nowrap"}}, ["Treatment ID"]), + div(".col .s8 .truncate", { style: blur }, [ sample.trt_id != "NA" ? sample.trt_id : "" ]), + div(".col .s4 .m3 .offset-m1", {style: {whiteSpace: "nowrap"}}, ["Treatment Name"]), + div(".col .s8", { style: blur }, [sample.trt_name]) + ]) ]), _default: div(".row", { style: { fontWeight: "small" } }, [ div(".col .s1 .left-align", { style: { fontWeight: "bold" } }, [ @@ -204,47 +292,84 @@ export function SampleInfo(sources) { } } + /** + * Constant lambda function to create a data row details for a sample + * @function rowDetails + * @param {object} sample the data to be displayed + * @param {object} props static settings for ie. sourire url or background colors + * @param {style} blur component style to contain blur settings + * @return {object} object with members for each treatment type, each has wrapping div with vdom elements + */ const rowDetail = (sample, props, blur) => { let hStyle = { style: { margin: "0px", fontWeight: "bold" } } let pStyle = { style: { margin: "0px" } } let pStylewBlur = { style: merge(blur, { margin: "0px" }) } const _filters = sample.filters != undefined ? sample.filters : [] + + const samplePart = + [ + p(".col .s12 .grey-text", hStyle, "Sample Info:"), + p(pStyle, entry("Sample ID: ", sample.id)), + p(pStyle, entry("Cell: ", sample.cell)), + p(pStyle, entry("Dose: ", sample.dose)), + p(pStyle, entry("Time: ", sample.time)), + p(pStyle, entry("Year: ", sample.year)), + p(pStyle, entry("Plate: ", sample.plate)), + ] + + const treatmentPart = + [ + p(".col .s12 .grey-text", hStyle, "Treatment Info:"), + p(pStylewBlur, entry("Name: ", sample.trt_name)), + p( + pStylewBlur, + entry( + safeModelToUi("id", props.common.modelTranslations) + ": ", + sample.trt_id + ) + ), + p(pStyle, entry("Type: ", sample.trt)), + p(".s12", entry("Targets: ", sample.targets.join(", "))), + ] + + const visualizeTextPart = + [ + sample.trt_name != null && sample.trt_name != "N/A" + ? div( + ".col .s12", + { + style: { + color: "black", + opacity: 0.4, + "font-size": "clamp(16px, 5vw, 26px)", + "font-family": "Nova Mono", + "object-fit": "contain", + fontWeight: "bold", + }, + }, + [sample.trt_name] + ) + : div(), + ] + + const visualizeSmilesPart = + [ + sample.smiles != null && + sample.smiles != "N/A" && + sample.smiles != "No Smiles" + ? img(".col .s12 .valign", { + props: { src: sourireUrl(props.sourire.url, sample.smiles) }, + }) + : "", + ] + return { trt_cp: div(".col .s12", [ - div(".col .s6 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ - p(".col .s12 .grey-text", hStyle, "Sample Info:"), - p(pStyle, entry("Sample ID: ", sample.id)), - p(pStyle, entry("Cell: ", sample.cell)), - p(pStyle, entry("Dose: ", sample.dose)), - p(pStyle, entry("Time: ", sample.time)), - p(pStyle, entry("Year: ", sample.year)), - p(pStyle, entry("Plate: ", sample.plate)), - ]), - div(".col .s6 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ - p(".col .s12 .grey-text", hStyle, "Treatment Info:"), - p(pStylewBlur, entry("Name: ", sample.trt_name)), - p( - pStylewBlur, - entry( - safeModelToUi("id", props.common.modelTranslations) + ": ", - sample.trt_id - ) - ), - p(pStyle, entry("Type: ", sample.trt)), - p(".s12", entry("Targets: ", sample.targets.join(", "))), - ]), - div( - ".col .s12 .offset-s8 .l4", - { style: merge(blur, { margin: "20px 0px 0px 0px" }) }, - [ - sample.smiles != null && - sample.smiles != "N/A" && - sample.smiles != "No Smiles" - ? img(".col .s12 .valign", { - props: { src: sourireUrl(props.sourire.url, sample.smiles) }, - }) - : "", - ] + div(".col .s12 .m6 .l4", { style: { margin: "15px 0px 0px 0px" } }, samplePart), + div(".col .s12 .m6 .l4", { style: { margin: "15px 0px 0px 0px" } }, treatmentPart), + div(".col .s12 .offset-s8 .offset-m8 .l4", + {style: merge(blur, { margin: "20px 0px 0px 0px" }) }, + visualizeSmilesPart ), div( ".col .s12 .l12", @@ -256,55 +381,12 @@ export function SampleInfo(sources) { ]), trt_sh: div([ div(".row", [ - div(".col .s4 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ - p(".col .s12 .grey-text", hStyle, "Sample Info:"), - p(pStyle, entry("Sample ID: ", sample.id)), - p(pStyle, entry("Cell: ", sample.cell)), - p(pStyle, entry("Dose: ", sample.dose)), - p(pStyle, entry("Time: ", sample.time)), - p(pStyle, entry("Year: ", sample.year)), - p(pStyle, entry("Plate: ", sample.plate)), - ]), - div(".col .s4 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ - p(".col .s12 .grey-text", hStyle, "Treatment Info:"), - p(pStylewBlur, entry("Name: ", sample.trt_name)), - p( - pStylewBlur, - entry( - safeModelToUi("id", props.common.modelTranslations) + ": ", - sample.trt_id - ) - ), - p(pStyle, entry("Type: ", sample.trt)), - p(".s12", entry("Targets: ", sample.targets.join(", "))), - ]), - div( - ".col .s4 .l4", - { - style: merge(blur, { - height: "100%", - margin: "30px 0px 0px 0px", - }), - }, - [ - sample.trt_name != null && sample.trt_name != "N/A" - ? div( - ".col .s12", - { - style: { - color: "black", - opacity: 0.4, - "font-size": "clamp(16px, 5vw, 50px)", - "font-family": "Nova Mono", - "object-fit": "contain", - fontWeight: "bold", - }, - }, - [sample.trt_name] - ) - : div(), - ] - ), + div(".col .s12 .m6 .l4", { style: { margin: "15px 0px 0px 0px" } }, samplePart), + div(".col .s12 .m6 .l4", { style: { margin: "15px 0px 0px 0px" } }, treatmentPart), + div(".col .s12 .m12 .l2 .push-l2 .hide-on-med-and-down .center-align", + { style: merge(blur, { height: "100%", "margin-top": "30px"}) }, + visualizeTextPart + ), ]), div( ".row", @@ -316,55 +398,12 @@ export function SampleInfo(sources) { ]), trt_lig: div([ div(".row", [ - div(".col .s4 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ - p(".col .s12 .grey-text", hStyle, "Sample Info:"), - p(pStyle, entry("Sample ID: ", sample.id)), - p(pStyle, entry("Cell: ", sample.cell)), - p(pStyle, entry("Dose: ", sample.dose)), - p(pStyle, entry("Time: ", sample.time)), - p(pStyle, entry("Year: ", sample.year)), - p(pStyle, entry("Plate: ", sample.plate)), - ]), - div(".col .s4 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ - p(".col .s12 .grey-text", hStyle, "Treatment Info:"), - p(pStylewBlur, entry("Name: ", sample.trt_name)), - p( - pStylewBlur, - entry( - safeModelToUi("id", props.common.modelTranslations) + ": ", - sample.trt_id - ) - ), - p(pStyle, entry("Type: ", sample.trt)), - p(".s12", entry("Targets: ", sample.targets.join(", "))), - ]), - div( - ".col .s4 .l4", - { - style: merge(blur, { - height: "100%", - margin: "30px 0px 0px 0px", - }), - }, - [ - sample.trt_name != null && sample.trt_name != "N/A" - ? div( - ".col .s12", - { - style: { - color: "black", - opacity: 0.4, - "font-size": "clamp(16px, 5vw, 50px)", - "font-family": "Nova Mono", - "object-fit": "contain", - fontWeight: "bold", - }, - }, - [sample.trt_name] - ) - : div(), - ] - ), + div(".col .s12 .m6 .l4", { style: { margin: "15px 0px 0px 0px" } }, samplePart), + div(".col .s12 .m6 .l4", { style: { margin: "15px 0px 0px 0px" } }, treatmentPart), + div(".col .s12 .m12 .l2 .push-l2 .hide-on-med-and-down .center-align", + { style: merge(blur, { height: "100%", "margin-top": "30px"}) }, + visualizeTextPart + ), ]), div( ".row", @@ -376,54 +415,11 @@ export function SampleInfo(sources) { ]), ctl_vector: div([ div(".row", [ - div(".col .s4 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ - p(".col .s12 .grey-text", hStyle, "Sample Info:"), - p(pStyle, entry("Sample ID: ", sample.id)), - p(pStyle, entry("Cell: ", sample.cell)), - p(pStyle, entry("Dose: ", sample.dose)), - p(pStyle, entry("Time: ", sample.time)), - p(pStyle, entry("Year: ", sample.year)), - p(pStyle, entry("Plate: ", sample.plate)), - ]), - div(".col .s4 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ - p(".col .s12 .grey-text", hStyle, "Treatment Info:"), - p(pStylewBlur, entry("Name: ", sample.trt_name)), - p( - pStylewBlur, - entry( - safeModelToUi("id", props.common.modelTranslations) + ": ", - sample.trt_id - ) - ), - p(pStyle, entry("Type: ", sample.trt)), - p(".s12", entry("Targets: ", sample.targets.join(", "))), - ]), - div( - ".col .s4 .l4", - { - style: merge(blur, { - height: "100%", - margin: "30px 0px 0px 0px", - }), - }, - [ - sample.trt_name != null && sample.trt_name != "N/A" - ? div( - ".col .s12", - { - style: { - color: "black", - opacity: 0.4, - "font-size": "clamp(16px, 5vw, 50px)", - "font-family": "Nova Mono", - "object-fit": "contain", - fontWeight: "bold", - }, - }, - [sample.trt_name] - ) - : div(), - ] + div(".col .s12 .m6 .l4", { style: { margin: "15px 0px 0px 0px" } }, samplePart), + div(".col .s12 .m6 .l4", { style: { margin: "15px 0px 0px 0px" } }, treatmentPart), + div(".col .s12 .m12 .l2 .push-l2 .hide-on-med-and-down .center-align", + { style: merge(blur, { height: "100%", "margin-top": "30px"}) }, + visualizeTextPart ), ]), div( @@ -436,42 +432,11 @@ export function SampleInfo(sources) { ]), _default: div(".row", { style: { fontWeight: "small" } }, [ div(".col .s12", [ - div(".col .s6 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ - p(".col .s12 .grey-text", hStyle, "Sample Info:"), - p(pStyle, entry("Sample ID: ", sample.id)), - p(pStyle, entry("Cell: ", sample.cell)), - p(pStyle, entry("Dose: ", sample.dose)), - p(pStyle, entry("Time: ", sample.time)), - p(pStyle, entry("Year: ", sample.year)), - p(pStyle, entry("Plate: ", sample.plate)), - ]), - div(".col .s6 .l4", { style: { margin: "15px 0px 0px 0px" } }, [ - p(".col .s12 .grey-text", hStyle, "Treatment Info:"), - p(pStylewBlur, entry("Name: ", sample.trt_name)), - p( - pStylewBlur, - entry( - safeModelToUi("id", props.common.modelTranslations) + ": ", - sample.trt_id - ) - ), - p(pStyle, entry("Type: ", sample.trt)), - p(".s12", entry("Targets: ", sample.targets.join(", "))), - ]), - div( - ".col .s12 .offset-s8 .l4", + div(".col .s12 .m6 .l4", { style: { margin: "15px 0px 0px 0px" } }, samplePart), + div(".col .s12 .m6 .l4", { style: { margin: "15px 0px 0px 0px" } }, treatmentPart), + div(".col .s12 .offset-s8 .offset-m8 .l4", { style: merge(blur, { margin: "20px 0px 0px 0px" }) }, - [ - sample.smiles != null && - sample.smiles != "N/A" && - sample.smiles != "No Smiles" - ? img(".col .s12 .valign", { - props: { - src: sourireUrl(props.sourire.url, sample.smiles), - }, - }) - : "", - ] + visualizeSmilesPart ), div( ".col .s12 .l12", diff --git a/src/js/components/SampleTable/SampleTable.js b/src/js/components/SampleTable/SampleTable.js index e600c0b2..cc763317 100644 --- a/src/js/components/SampleTable/SampleTable.js +++ b/src/js/components/SampleTable/SampleTable.js @@ -2,7 +2,7 @@ import xs from 'xstream'; import { ul, li } from '@cycle/dom'; import { pick, mix } from 'cycle-onionify'; import isolate from '@cycle/isolate' -import { SampleInfo } from './SampleInfo' +import { SampleInfoHeader, SampleInfo } from './SampleInfo' const sampleTableLens = { get: state => state.core.data, @@ -12,6 +12,7 @@ const sampleTableLens = { function SampleTable(sources) { const array$ = sources.onion.state$ + const props$ = sources.props const childrenSinks$ = array$.map(array => { return array.map((_, index) => isolate(SampleInfo, index)(sources)) @@ -19,23 +20,23 @@ function SampleTable(sources) { const listStyle = {style : {'margin-top' : '0px', 'margin-bottom':'0px'}} - const vdom$ = - childrenSinks$ - .compose(pick('DOM')) - .compose(mix(xs.combine)) - .map(itemVNodes => - ul( - '.collection', - listStyle, - [].concat(itemVNodes) - ) - ) - .startWith( - ul( - '.collection', - [ li('.collection-item .center-align .grey-text','no query yet...') ] - ) - ) + const sampleInfoHeader$ = props$.map(props => SampleInfoHeader(props.table.bgcolor, props.table.color)) + + const composedChildrenSinks$ = childrenSinks$.compose(pick('DOM')).compose(mix(xs.combine)) + const vdom$ = xs.combine(sampleInfoHeader$, composedChildrenSinks$) + .map(([header, itemVNodes]) => + ul( + '.collection', + listStyle, + [].concat(header).concat(itemVNodes) + ) + ) + .startWith( + ul( + '.collection', + [ li('.collection-item .center-align .grey-text','no query yet...') ] + ) + ) return { DOM: vdom$, diff --git a/src/js/components/Table.js b/src/js/components/Table.js index f07616bb..4f8ad3f5 100644 --- a/src/js/components/Table.js +++ b/src/js/components/Table.js @@ -207,7 +207,12 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { .mapTo(-10) .startWith(0) + // Only take the difference of the default value compared to old value + // 1. Prevent state changes adding the default value in the accumulator + // 2. Needs to be in the accumulator otherwise we can't reduce the amount of lines less than the default setting + // 3. By folding & limiting the value here we prevent (hidden) negative numbers that the user would have to increase before seeing changes again const defaultAmountToDisplay$ = newInput$.map(state => parseInt(state.settings.table.count)) + .fold((acc, newValue) => newValue - acc, 0) const amountToDisplay$ = xs .merge(defaultAmountToDisplay$, plus5$, min5$, plus10$, min10$) .fold((x, y) => max(0, x + y), 0) @@ -415,12 +420,12 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { ".material-icons .grey-text", { style: { - fontSize: "16px", + fontSize: "20px", "background-color": settings.table.color, opacity: 0.5, }, }, - "add" + expandOptions ? "arrow_drop_up" : "arrow_drop_down" ), ]), div(".white-text .col .s7 .valign .right-align", filterText), diff --git a/src/js/main.scss b/src/js/main.scss index 50633cdc..f699b8f2 100644 --- a/src/js/main.scss +++ b/src/js/main.scss @@ -42,6 +42,13 @@ main { @extend .lighten-5; } +img.trt_img { + width: 100%; + width: -moz-available; /* WebKit-based browsers will ignore this. */ + width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */ + width: fill-available; +} + nav ul li.active span { border-bottom-width: medium; border-bottom-style: solid; diff --git a/src/js/utils/export.js b/src/js/utils/export.js index 2e120960..2940aebc 100644 --- a/src/js/utils/export.js +++ b/src/js/utils/export.js @@ -8,6 +8,29 @@ const convertToSafeString = (s) => { } function convertToCSV(objArray) { + + // input is array of objects with 'key' and 'value' members + // returning array of strings in format 'key:value'. Javascript handles serializing the arrays for us. + const filterStringify = (filters) => + { + return filters + .map((f) => { + return convertToSafeString(f.key) + ':' + convertToSafeString(f.value) + }) + } + + // only change filters if the member exists + const data = objArray.map( + (d) => (d.filters !== undefined ? + {...d, filters: filterStringify(d.filters)} : + d + ) + ) + + return convertToCSV_internal(data) +} + +function convertToCSV_internal(objArray) { const header = keys(objArray[0]) const data = objArray.map(obj => values(obj)) From e3940f77f62e8d2b795be289154c84d767e2c500 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Thu, 25 Nov 2021 11:38:58 +0100 Subject: [PATCH 044/191] Add Docker build action --- .github/workflows/docker-image.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/docker-image.yml diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 00000000..950f4ca6 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,15 @@ +name: Docker Image CI + +on: + push: + branches: [ "*" ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Build the Docker image + run: docker build . --file Dockerfile --tag LuciusWeb:$(date +%s) From e5865b1640122f0c5c69e91929b90ebc8a76053c Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Thu, 25 Nov 2021 11:40:18 +0100 Subject: [PATCH 045/191] Correct Docker tag --- .github/workflows/docker-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 950f4ca6..32a9ca9e 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -12,4 +12,4 @@ jobs: steps: - uses: actions/checkout@v2 - name: Build the Docker image - run: docker build . --file Dockerfile --tag LuciusWeb:$(date +%s) + run: docker build . --file Dockerfile --tag luciusweb:$(date +%s) From 0938ce211f4b67c22406dd3e815a85313cb2faf8 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 30 Nov 2021 16:41:10 +0100 Subject: [PATCH 046/191] Add documentation to Filters component Add module info in SampleInfo to help JSDoc and add comments to SampleTable also tweak @returns comments a bit in SampleInfo --- src/js/components/Filter.js | 88 +++++++++++++++++--- src/js/components/SampleTable/SampleInfo.js | 8 +- src/js/components/SampleTable/SampleTable.js | 30 +++++++ 3 files changed, 114 insertions(+), 12 deletions(-) diff --git a/src/js/components/Filter.js b/src/js/components/Filter.js index df6acbaf..87c99958 100644 --- a/src/js/components/Filter.js +++ b/src/js/components/Filter.js @@ -16,9 +16,17 @@ import { FetchFilters } from "./FetchFilters" import debounce from 'xstream/extra/debounce' import { dirtyUiReducer } from "../utils/ui" -// A typical Lens with one exception: -// We allow the child state settings for filter to propagate to -// the global state because the filter values are fetched in the child. +/** + * @module components/Filter + */ + +/** + * A typical Lens with one exception: + * We allow the child state settings for filter to propagate to + * the global state because the filter values are fetched in the child. + * @const filterLens + * @type {Lens} + */ export const filterLens = { get: (state) => ({ core: state.filter, @@ -34,7 +42,12 @@ export const filterLens = { }), } -// When the component should not be shown, including empty signature +/** + * When the component should not be shown, including empty signature + * @function isEmptyState + * @param {String} state.core.input input value from previous component to signal if this filter should be displayed empty or not + * @returns {boolean} + */ const isEmptyState = (state) => { if (typeof state.core === "undefined") { return true @@ -51,6 +64,15 @@ const isEmptyState = (state) => { } } +/** + * Filter intent, convert events on the dom to actions + * @function intent + * @param {Stream} domSource$ events from the dom + * @returns {Stream} object with: + * - filterValuesAction$ stream of object where key is filter group (top level) and value is which option is being clicked/modified + * - modifier$: stream of boolean of modifier key being pressed or not + * - filterAction$: stream of object where key is filter group (top level) and value is boolean of the group being clicked open or not + */ function intent(domSource$) { // const expandAnyGhost$ = ghostChanges$.map(state => state.core.ghost.expand).startWith(false) @@ -169,7 +191,18 @@ function intent(domSource$) { } } -export function model( +/** + * Filters model, control state changes according to actions + * @function model + * @param {Stream} possibleValues$ object with 'key': 'array of strings' + * @param {Stream} input$ signature string, used to pass to view for it to check if there is any input at all + * @param {Stream} filterValuesAction$ object where key is filter group (top level; dose, protocol, type) and value is which option is being clicked/modified + * @param {Stream} modifier$ boolean of modifier key being pressed or not + * @param {Stream} filterAction$ object where key is filter group (top level; dose, protocol, type) and value is boolean of the group being clicked open or not + * @param {Stream} state$ readback of full state object used for comparing committed state vs current state, if not identical means ui is dirty + * @returns {Stream} reducers + */ +function model( possibleValues$, input$, filterValuesAction$, @@ -178,7 +211,11 @@ export function model( state$, ) { - // Add the filter values from the settings (and originally from deployments.json) to the current values + /** + * Add the filter values from the settings (and originally from deployments.json) to the current values + * @const model/defaultReducer$ + * @type {Reducer} + */ const defaultReducer$ = xs.of((prevState) => ({ ...prevState, core: { @@ -188,8 +225,11 @@ export function model( }, })) - // When the query for the current filter values returns we want to update - // the settings + /** + * When the query for the current filter values returns we want to update the settings + * @const model/possibleValuesReducer$ + * @type {Reducer} + */ const possibleValuesReducer$ = possibleValues$.map((fvs) => (prevState) => ({ ...prevState, settings: { @@ -202,11 +242,21 @@ export function model( }) ) + /** + * Store input so view can access it to see if the content is empty or not + * @const model/inputReducer$ + * @type {Reducer} + */ const inputReducer$ = input$.map((i) => (prevState) => ({ ...prevState, core: { ...prevState.core, input: i }, })) + /** + * Handle toggling of filters to a state of which filter is currently selected or not + * @const model/toggleReducer$ + * @type {Reducer} + */ const toggleReducer$ = filterValuesAction$ .compose(sampleCombine(modifier$)) .map(([clickedFilter, a]) => (prevState) => { @@ -292,6 +342,11 @@ export function model( return o } + /** + * Output reducer that only outputs to 'filter_output' when changes happened to the opening or closing of the filter top levels + * @const model/outputReducer$ + * @type {Reducer} + */ const outputReducer$ = filterAction$.map(_ => (prevState) => ({ ...prevState, core: { ...prevState.core, filter_output: minimizeFilterOutput(prevState) }, @@ -314,7 +369,10 @@ export function model( } /** - * view + * Filters view, display the component on the vdom + * @function view + * @param {Stream} state$ full state onion + * @returns {VNodes} VNodes object of either emptyVdom$ or loadedVdom$ */ function view(state$) { @@ -475,6 +533,16 @@ function view(state$) { * * - input$: stream of signature updates * - output$: to be consumed by components that require filter functionality, object with filter values. + * + * @function Filter + * @param {*} sources + * - onion.state$: default onion atom + * - input$: signature used as trigger for empty or not empty + * @returns - log: logger stream, + * - DOM: vdom stream, + * - HTTP: HTTP stream, + * - onion: reducers stream, + * - output: minimized filter selection */ function Filter(sources) { @@ -523,7 +591,7 @@ function Filter(sources) { DOM: vdom$, HTTP: filterQuery.HTTP, onion: reducers$, - output: outputTrigger$.debug('filter') + output: outputTrigger$ } } diff --git a/src/js/components/SampleTable/SampleInfo.js b/src/js/components/SampleTable/SampleInfo.js index 2cd77464..c2daff8c 100644 --- a/src/js/components/SampleTable/SampleInfo.js +++ b/src/js/components/SampleTable/SampleInfo.js @@ -24,15 +24,18 @@ import img_ctl_vector_cns from "/images/treatmentTypes/CTL_VECTOR.png" import img_ctl_untrt_cns from "/images/treatmentTypes/CTL_UNTRT.CNS.png" import img_ctl_untrt from "/images/treatmentTypes/CTL_UNTRT.png" +/** + * @module components/SampleTable/SampleInfo + */ + /** * Create a header matching the data order of sample rows displayed in case of large displays * header hides itself in case of small and medium screens * @function SampleInfoHeader * @param {string} bgcolor color of the table background * @param {string} color color of the table foreground - * @returns VNode li element with header data + * @returns {VNode} li element with header data */ - export function SampleInfoHeader(bgcolor, color) { return li( ".collection-item .hide-on-med-and-down .zoom", @@ -58,6 +61,7 @@ export function SampleInfoHeader(bgcolor, color) { * @param {stream} sources.onion.state$ stream of sample data to be displayed * @param sources.DOM user click events * @param sources.props semi-static settings + * @returns {object} - DOM: VNode stream containing sample information */ export function SampleInfo(sources) { const state$ = sources.onion.state$ diff --git a/src/js/components/SampleTable/SampleTable.js b/src/js/components/SampleTable/SampleTable.js index cc763317..a3dab3f2 100644 --- a/src/js/components/SampleTable/SampleTable.js +++ b/src/js/components/SampleTable/SampleTable.js @@ -4,25 +4,55 @@ import { pick, mix } from 'cycle-onionify'; import isolate from '@cycle/isolate' import { SampleInfoHeader, SampleInfo } from './SampleInfo' +/** + * @module components/SampleTable/SampleTable + */ + +/** + * Minimalistic lens that passes state.core.data as the state + * @const samplTableLens + */ const sampleTableLens = { get: state => state.core.data, set: (state, _) => state } +/** + * Straight forward displaying of input data to vdom with header + * Requires all data to be present and ready to be displayed + * @function SampleTable + * @param {*} sources + * - onion.state$: default onion atom containing the input data + * - DOM: user click events + * - props: settings for e.g. background and foreground colors + * @returns {object} - DOM: vdom stream + */ function SampleTable(sources) { const array$ = sources.onion.state$ const props$ = sources.props + // isolate each line so that it separates clicks const childrenSinks$ = array$.map(array => { return array.map((_, index) => isolate(SampleInfo, index)(sources)) }); const listStyle = {style : {'margin-top' : '0px', 'margin-bottom':'0px'}} + /** + * Get header line, only variable input is background and foreground colors, so map from props$ + * @const sampleInfoHeaers$ + * @type {Stream} + */ const sampleInfoHeader$ = props$.map(props => SampleInfoHeader(props.table.bgcolor, props.table.color)) + /** + * Get all 'DOM' streams from all lines and convert to single stream of array of VNode + * @const SampleTable/composedChildrenSinks$ + * @type {Stream} + */ const composedChildrenSinks$ = childrenSinks$.compose(pick('DOM')).compose(mix(xs.combine)) + const vdom$ = xs.combine(sampleInfoHeader$, composedChildrenSinks$) .map(([header, itemVNodes]) => ul( From dd48798ed2e09db3986352ae1b1fd3947f29c893 Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Thu, 2 Dec 2021 13:57:45 +0100 Subject: [PATCH 047/191] Menu reduces to hamburger for small screen (#73) --- index.html | 1 + navbar.js | 11 +++++++++++ src/js/index.js | 44 +++++++++++++++++++++++++++++--------------- 3 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 navbar.js diff --git a/index.html b/index.html index eafbff84..7bfc9bba 100644 --- a/index.html +++ b/index.html @@ -17,6 +17,7 @@
+ diff --git a/navbar.js b/navbar.js new file mode 100644 index 00000000..3cc0fb80 --- /dev/null +++ b/navbar.js @@ -0,0 +1,11 @@ + +// document.addEventListener('DOMContentLoaded', function() { +// var elems = document.querySelectorAll('.sidenav'); +// var instances = M.Sidenav.init(elems, options); +// }); + + // Or with jQuery + + $(document).ready(function(){ + $('.sidenav').sidenav(); + }); \ No newline at end of file diff --git a/src/js/index.js b/src/js/index.js index 3d060675..ab07679e 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -23,6 +23,8 @@ import { initSettings } from './configuration.js' import initDeployments from '../../deployments.json' import { loggerFactory } from './utils/logger' +import { navbarModule } from "../../navbar.js"; + export default function Index(sources) { const {router} = sources; @@ -85,23 +87,35 @@ export default function Index(sources) { ]) ]) - const nav$ = xs.of(header([nav('#navigation .grey .darken-4', [ - div('.nav-wrapper', [ - a('.brand-logo .right .grey-text', { props: { href: "/" } }, - div({ style: { width: '140px' } }, logoSVG), - // span('.gradient', 'ComPass') - ), - ul('.left .hide-on-med-and-down', [ - makeLink('/compound', span(['Compound', ' ', compoundSVG]), '.orange-text'), - // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), - makeLink('/genetic', span(['Genetic', ' ', targetSVG]), '.red-text'), - makeLink('/disease', span(['Disease', ' ', diseaseSVG]), '.pink-text'), - makeLink('/settings', span(['Settings', ' ', settingsSVG]), '.grey-text'), - // makeLink('/admin', span(['Admin']), '.blue-text'), - makeLink('/correlation', span('.grey-text .text-darken-3','', ["v", VERSION]), ''), + const nav$ = xs.of(header([ + nav('#navigation .grey .darken-4', [ + div('.nav-wrapper', [ + a('.brand-logo .right .grey-text', { props: { href: "/" } }, + div({ style: { width: '140px' } }, logoSVG), + // span('.gradient', 'ComPass') + ), + a('.sidenav-trigger', { props: { href: '#' }, attrs: {'data-target': 'mobile-demo' }}, i('.material-icons', 'menu')), + ul('.left .hide-on-med-and-down', [ + makeLink('/compound', span(['Compound', ' ', compoundSVG]), '.orange-text'), + // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), + makeLink('/genetic', span(['Genetic', ' ', targetSVG]), '.red-text'), + makeLink('/disease', span(['Disease', ' ', diseaseSVG]), '.pink-text'), + makeLink('/settings', span(['Settings', ' ', settingsSVG]), '.grey-text'), + // makeLink('/admin', span(['Admin']), '.blue-text'), + makeLink('/correlation', span('.grey-text .text-darken-3','', ["v", VERSION]), ''), + ]) ]) + ]), + ul(".sidenav", {props: {id: 'mobile-demo'}}, [ + makeLink('/compound', span(['Compound', ' ', compoundSVG]), '.orange-text'), + // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), + makeLink('/genetic', span(['Genetic', ' ', targetSVG]), '.red-text'), + makeLink('/disease', span(['Disease', ' ', diseaseSVG]), '.pink-text'), + makeLink('/settings', span(['Settings', ' ', settingsSVG]), '.grey-text'), + // makeLink('/admin', span(['Admin']), '.blue-text'), + makeLink('/correlation', span('.grey-text .text-darken-3','', ["v", VERSION]), ''), ]) - ])])); + ])); // We combine with state in order to read the customizations // This works because the defaultReducer runs before anything else From 45197d19bd22e078da0f567cd8dc9b3da4992165 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 29 Nov 2021 16:23:32 +0100 Subject: [PATCH 048/191] Add github workflow for CI Pretty default configuration Fix yaml syntax --- .github/workflows/test.yaml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/test.yaml diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 00000000..8dabd293 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,23 @@ +name: Node.js CI + +on: + push: + branches: [ "*" ] + pull_request: + branches: [ main, develop ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js + uses: actions/setup-node@v1 + with: + node-version: '14.x' + - name: Install dependencies + run: npm install + - run: npm run build --if-present + - run: npm test \ No newline at end of file From db6c3511bab3ce0eaeae8c7d32aca26d818409fd Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 3 Dec 2021 08:49:27 +0100 Subject: [PATCH 049/191] Rework filter dirty state (#78) * Fix filter tests Update for changes made in code since the tests were added Fix UI that had .startWith(false) as it will act as a default reducer too * Rework filter UI dirty reducer logic Previously state$ needed to loop back into model as filter_output was used to pass into the dirtyUI reducer Instead use a custom dirty reducer instead of one in ui.js This new reducer listens filterActions$ and filterValuesActions$ (opening/closing of filters and filter value changes) Also now moved filtering of filter_output updates only when the filters are closed to the model * Change Filter tests again now that state$ is no longer a parameter in model * Fix test and add new basic test that checks dirty flag Added dirty flag in default reducer Seems output and dirty flags update state individually so tests get double outputs --- src/js/components/Filter.js | 44 +++++++++-------- src/js/utils/ui.js | 2 +- src/test/FilterTest.js | 97 ++++++++++++++++++++++++++++++++++--- 3 files changed, 115 insertions(+), 28 deletions(-) diff --git a/src/js/components/Filter.js b/src/js/components/Filter.js index 87c99958..73b32146 100644 --- a/src/js/components/Filter.js +++ b/src/js/components/Filter.js @@ -14,7 +14,6 @@ import { } from "ramda" import { FetchFilters } from "./FetchFilters" import debounce from 'xstream/extra/debounce' -import { dirtyUiReducer } from "../utils/ui" /** * @module components/Filter @@ -193,6 +192,7 @@ function intent(domSource$) { /** * Filters model, control state changes according to actions + * set export for unit tests * @function model * @param {Stream} possibleValues$ object with 'key': 'array of strings' * @param {Stream} input$ signature string, used to pass to view for it to check if there is any input at all @@ -202,13 +202,12 @@ function intent(domSource$) { * @param {Stream} state$ readback of full state object used for comparing committed state vs current state, if not identical means ui is dirty * @returns {Stream} reducers */ -function model( +export function model( possibleValues$, input$, filterValuesAction$, modifier$, filterAction$, - state$, ) { /** @@ -222,6 +221,7 @@ function model( output: {}, filter_output: {}, state: {dose: false, cell: false, trtType: false}, + dirty: false, }, })) @@ -343,19 +343,33 @@ function model( } /** - * Output reducer that only outputs to 'filter_output' when changes happened to the opening or closing of the filter top levels + * Output reducer that only outputs the minimized state to 'filter_output' when all top level menus are closed * @const model/outputReducer$ * @type {Reducer} */ - const outputReducer$ = filterAction$.map(_ => (prevState) => ({ + const outputReducer$ = filterAction$ + .filter((state) => !state.dose && !state.cell && !state.trtType) + .map(_ => (prevState) => ({ ...prevState, core: { ...prevState.core, filter_output: minimizeFilterOutput(prevState) }, })) - const minimizedCurrentOutput$ = state$.map((state) => minimizeFilterOutput(state)) - - // Logic and reducer stream that monitors if this component became dirty - const dirtyReducer$ = dirtyUiReducer(minimizedCurrentOutput$, state$.map(state => state.core.filter_output)) + /** + * Dirty state reducer, custom version than in ui.js as more logic is required and otherwise need to loop state$ back into model + * Uses value change or opening/closing of the filters and compares current state with committed state + * + * @const model/dirtyReducer$ + * @type {Reducer} + */ + const dirtyReducer$ = xs.merge(filterValuesAction$, filterAction$).map(_ => (prevState) => { + const minimizedCurrentOutput = minimizeFilterOutput(prevState) + const dirty = !equals(minimizedCurrentOutput, prevState.core.filter_output) + + return { + ...prevState, + core: {...prevState.core, dirty: dirty} + } + }) return xs.merge( defaultReducer$, @@ -559,11 +573,6 @@ function Filter(sources) { const filterQuery = FetchFilters(sources) - // Ghost mode, inject changes via external state updates... - // const ghostChanges$ = modifiedState$ - // .filter((state) => typeof state.core.ghost !== "undefined") - // .compose(dropRepeats()) - const actions = intent(sources.DOM) const vdom$ = view(state$) @@ -574,15 +583,10 @@ function Filter(sources) { actions.filterValuesAction$, actions.modifier$, actions.filterAction$, - state$, ) - const outputTrigger$ = + const outputTrigger$ = state$ - .filter( - (state) => - !state.core.state.dose && !state.core.state.cell && !state.core.state.trtType - ) .map(state => state.core.filter_output) .compose(dropRepeats(equals)) diff --git a/src/js/utils/ui.js b/src/js/utils/ui.js index 360103c5..09cd21d7 100644 --- a/src/js/utils/ui.js +++ b/src/js/utils/ui.js @@ -12,7 +12,7 @@ function dirtyUiStream(output$, current$) { .map(([output, current]) => !equals(output, current)) .compose(debounce(10)) .compose(dropRepeats(equals)) - .startWith(false) + //.startWith(false) } // Reducer dedicated to outputting the dirty state of a component into the component onion diff --git a/src/test/FilterTest.js b/src/test/FilterTest.js index 6dfca1cb..f090f9b1 100644 --- a/src/test/FilterTest.js +++ b/src/test/FilterTest.js @@ -19,7 +19,8 @@ describe("defaultReducer", function () { next(f) { const newState = f(state) assert.deepStrictEqual(newState.core.output, {}) - assert.deepStrictEqual(newState.settings.filter, { values: {} }) + assert.deepStrictEqual(newState.core.filter_output, {}) + assert.deepStrictEqual(newState.core.state, {dose: false, cell: false, trtType: false}) }, error(e) { done(e) @@ -44,7 +45,6 @@ describe("defaultReducer", function () { next(f) { const newState = f(state) assert.deepStrictEqual(newState.core.output, {}) - assert.deepStrictEqual(newState.settings.filter, { values: {} }) }, error(e) { done(e) @@ -57,7 +57,7 @@ describe("defaultReducer", function () { describe("possibleValuesReducer", function () { it("Updates the state with the possible values, leaves the rest intact", () => { const state = { - core: { entry: "test" }, + core: { entry: "test", dirty: false }, settings: {}, } @@ -82,9 +82,6 @@ describe("possibleValuesReducer", function () { next(f) { const newState = f(state) assert.deepStrictEqual(newState.core, state.core) - assert.deepStrictEqual(newState.settings.filter, { - values: possibleValues, - }) }, error(e) { done(e) @@ -175,16 +172,23 @@ describe("toggleReducer with and without modifier", function () { xs.empty() ) - const state$ = reducers$.fold((state, reducer) => reducer(state), undefined) + // Predefine filters in settings as possible filters will be updated there + const defaultFilter = {settings: {filter: {}}} + const state$ = reducers$.fold((state, reducer) => reducer(state), defaultFilter) // The reducers should generate the following sequence for state.core.output + // Values are duplicated as output & dirty reducers are changing let expectedOutput = [ {}, {}, {}, { dose: [2, 3] }, + { dose: [2, 3] }, + { dose: [2, 3], cell: ["cell2", "cell3"] }, { dose: [2, 3], cell: ["cell2", "cell3"] }, { dose: [1, 2, 3], cell: ["cell2", "cell3"] }, + { dose: [1, 2, 3], cell: ["cell2", "cell3"] }, + { dose: [], cell: ["cell2", "cell3"] }, { dose: [], cell: ["cell2", "cell3"] }, ] @@ -203,3 +207,82 @@ describe("toggleReducer with and without modifier", function () { }) }) }) + +describe("uiReducer", function () { + it("Becomes dirty when a filter value is clicked and becomes clean when the filter is clicked again", () => { + const possibleValues = { + dose: [1, 2, 3], + cell: ["cell1", "cell2", "cell3"], + trtType: ["a", "b", "c"], + } + const possibleValues$ = fromDiagram(`-x`).mapTo(possibleValues) + + const newInput = ["c", "d"] + const input$ = fromDiagram("-x").mapTo(newInput) + + const switchDose1 = { dose: 1 } + + // Result of clicking on dose = 1 + const switchDose1$ = fromDiagram("---x").mapTo(switchDose1) + // Result of clicking on dose = 1 again, this adds the filter value again + const action1$ = fromDiagram("----x").mapTo(switchDose1) + const action2$ = fromDiagram("-----x").mapTo(switchDose1) + const filterValuesAction$ = xs.merge( + switchDose1$, + action1$, + action2$ + ) + + const modifierFalse$ = xs.of(false) + + const openFilter = { + dose: true, + cell: false, + trtType: false, + } + const openFilter$ = fromDiagram("--x").mapTo(openFilter) + + const reducers$ = model( + possibleValues$, + input$, + filterValuesAction$, + modifierFalse$, + openFilter$ + ) + + // Predefine filters in settings as possible filters will be updated there + const defaultFilter = {settings: {filter: {}}} + const state$ = reducers$.fold((state, reducer) => reducer(state), defaultFilter) + + // The reducers should generate the following sequence for state.core.dirty + // Values are duplicated as output & dirty reducers are changing + let expectedOutput = [ + false, + false, + false, + false, + false, + false, + true, + true, + false, + false, + true, + true, + ] + + state$ + .drop(1) // drop the first state as it is undefined + .addListener({ + next(state) { + assert.deepStrictEqual(state?.core?.dirty, expectedOutput.shift()) + }, + error(e) { + console.log(e) + }, + complete() { + console.log("done!") + }, + }) + }) +}) From 4350b164cf3fc7c2afdbf1afc99a89c976179565 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 3 Dec 2021 12:15:09 +0100 Subject: [PATCH 050/191] Plot position configurable and create generic treatment workflow for compound, genetic, ligand (#72) * Make the binned plots location configurable Can be "before tables", "after tables", "no" Only genetic page changed yet. compound is to-do once change is +- accepted * Create generic treatment workflow page to wrap genetic and compound workflows in a common page This way the compound now also has the plot position settings implemented There was a huge commonality and in the prospect of having such a third workflow coming it makes sense to not keep duplicating code and having to copy changes between different pages * Fix typo in driver name of genetic wrapper * Add a ghost mode scenario selector in the page wrappers different pages need different ghost modes, so need to add selectors for those too * Add documentation to genericTreatment * Add documentation in the generic treatment form * Bump config version number as there is a new value * Copy changes from genericTreatment into disease workflow plots in the disease workflow now also has the option to change the plots to above tables, below tables, or no Co-authored-by: Hendrik Cannoodt <--get> --- src/js/configuration.js | 3 +- src/js/index.js | 2 + src/js/main.scss | 5 + src/js/pages/compound.js | 205 ++------------------ src/js/pages/disease.js | 86 ++++++++- src/js/pages/genericTreatment.js | 317 +++++++++++++++++++++++++++++++ src/js/pages/genetic.js | 203 ++------------------ src/js/pages/settings.js | 63 ++++-- 8 files changed, 474 insertions(+), 410 deletions(-) create mode 100644 src/js/pages/genericTreatment.js diff --git a/src/js/configuration.js b/src/js/configuration.js index 48fbf2bc..6a891519 100644 --- a/src/js/configuration.js +++ b/src/js/configuration.js @@ -1,5 +1,5 @@ export const initSettings = { - version: 5.1, + version: 5.2, deployment: { "name": "default", }, @@ -58,6 +58,7 @@ export const initSettings = { }, plots: { debug: false, + displayPlots: 'before tables', bins: 40, binsX: 20 }, diff --git a/src/js/index.js b/src/js/index.js index ab07679e..d27e5880 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -8,6 +8,7 @@ import * as R from 'ramda' import DiseaseWorkflow from './pages/disease' import CompoundWorkflow from './pages/compound' import GeneticWorkflow from './pages/genetic' +import GenericTreatmentWorkflow from './pages/genericTreatment' import TargetWorkflow from './pages/target' import CorrelationWorkflow from './pages/correlation' @@ -42,6 +43,7 @@ export default function Index(sources) { '/compound': CompoundWorkflow, '/target': TargetWorkflow, '/genetic': GeneticWorkflow, + '/generic': GenericTreatmentWorkflow, '/statistics': StatisticsWorkflow, '/settings': IsolatedSettings, '/correlation': CorrelationWorkflow, diff --git a/src/js/main.scss b/src/js/main.scss index f699b8f2..d9fd52ab 100644 --- a/src/js/main.scss +++ b/src/js/main.scss @@ -42,6 +42,11 @@ main { @extend .lighten-5; } +.generic { + @extend .green; + @extend .lighten-5; +} + img.trt_img { width: 100%; width: -moz-available; /* WebKit-based browsers will ignore this. */ diff --git a/src/js/pages/compound.js b/src/js/pages/compound.js index 9f04b343..c75c3be8 100644 --- a/src/js/pages/compound.js +++ b/src/js/pages/compound.js @@ -1,199 +1,18 @@ -import { div } from "@cycle/dom" -import xs from "xstream" -import isolate from "@cycle/isolate" -import { TreatmentForm, treatmentLikeFilter } from "../components/TreatmentForm" -import { initSettings } from "../configuration.js" -import { makeTable, headTableLens, tailTableLens } from "../components/Table" -import { BinnedPlots, plotsLens } from "../components/BinnedPlots/BinnedPlots" -import { Filter, filterLens } from "../components/Filter" -import { loggerFactory } from "../utils/logger" -import { - SampleTable, - sampleTableLens, -} from "../components/SampleTable/SampleTable" - -// Support for ghost mode -import { scenario } from "../scenarios/treatmentScenario" -import { runScenario } from "../utils/scenario" - -import dropRepeats from "xstream/extra/dropRepeats" -import { equals } from "ramda" +import { treatmentLikeFilter } from "../components/TreatmentForm" +import GenericTreatmentWorkflow from "./genericTreatment" export default function CompoundWorkflow(sources) { - const logger = loggerFactory( - "compound", - sources.onion.state$, - "settings.common.debug" - ) - - const state$ = sources.onion.state$ - - // Scenario for ghost mode - const scenarios$ = sources.onion.state$ - .take(1) - .filter((state) => state.settings.common.ghostMode) - .map(state => runScenario(scenario(state.settings.common.ghost.compound))) - const scenarioReducer$ = scenarios$.map(s => s.scenarioReducer$) - .flatten() - const scenarioPopup$ = scenarios$.map(s => s.scenarioPopup$) - .flatten() - .startWith({ text: "Welcome to Compound Workflow", duration: 4000 }) - - const formLens = { - get: (state) => ({ - form: state.form, - settings: { - form: state.settings.form, - api: state.settings.api, - common: state.settings.common, - geneAnnotations: state.settings.geneAnnotations, - compoundAnnotations: state.settings.compoundAnnotations, - treatmentLike: treatmentLikeFilter.COMPOUND, - }, - ui: (state.ui ?? {} ).form ?? {}, - }), - set: (state, childState) => ({ ...state, form: childState.form }), - } - - // Initialize if not yet done in parent (i.e. router) component (useful for testing) - const defaultReducer$ = xs.of((prevState) => { - // compound -- defaultReducer - if (typeof prevState === "undefined") { - return { - settings: initSettings, - } - } else { - return { - ...prevState, - settings: prevState.settings, - } - } - }) - - // Use dropRepeats else the stream gets in an infinite loop - const uiReducer$ = state$.compose(dropRepeats(equals)) - .map(state => - prevState => { - const dirtyCheck = state.form.check.dirty - const dirtySampleSelection = state.form.sampleSelection.dirty - const busySignature = state.form.signature.busy - const dirtyFilter = state.filter.dirty - return ({...prevState, - ui: { - form: { - sampleSelection: {dirty: dirtyCheck }, - signature: {dirty: dirtyCheck || dirtySampleSelection }, - }, - headTable: {dirty: dirtyCheck || dirtySampleSelection || busySignature || dirtyFilter }, - tailTable: {dirty: dirtyCheck || dirtySampleSelection || busySignature || dirtyFilter }, - plots: {dirty: dirtyCheck || dirtySampleSelection || busySignature || dirtyFilter }, - }, - }) - } - ) - - const TreatmentFormSink = isolate(TreatmentForm, { onion: formLens })(sources) - const signature$ = TreatmentFormSink.output.remember() - // Filter Form - const filterForm = isolate(Filter, { onion: filterLens })({ + const enhancedSources = { ...sources, - input: signature$, - }) - const filter$ = filterForm.output.remember() - - // const filter$ = xs.of({}) - // Binned Plots Component - const binnedPlots = isolate(BinnedPlots, { onion: plotsLens })({ - ...sources, - input: xs - .combine(signature$, filter$) - .map(([s, f]) => ({ signature: s, filter: f })) - .remember(), - }) - - // tables - const headTableContainer = makeTable(SampleTable, sampleTableLens) - const tailTableContainer = makeTable(SampleTable, sampleTableLens) - - // Join settings from api and sourire into props - const headTable = isolate(headTableContainer, { onion: headTableLens })({ - ...sources, - input: xs - .combine(signature$, filter$) - .map(([s, f]) => ({ query: s, filter: f })) - .remember(), - }) - const tailTable = isolate(tailTableContainer, { onion: tailTableLens })({ - ...sources, - input: xs - .combine(signature$, filter$) - .map(([s, f]) => ({ query: s, filter: f })) - .remember(), - }) - - const pageStyle = { - style: { - fontSize: "14px", - opacity: "0", - transition: "opacity 1s", - delayed: { opacity: "1" }, - destroy: { opacity: "0" }, - }, + workflow: { + treatmentType: treatmentLikeFilter.COMPOUND, + welcomeText: "Welcome to Compound Workflow", + mainDivClass: ".row .compound", + loggerName: "compound", + ghostModeScenarioSelector: ((state) => state.settings.common.ghost.compound), + }, } - const vdom$ = xs - .combine( - TreatmentFormSink.DOM, - filterForm.DOM, - binnedPlots.DOM, - headTable.DOM, - tailTable.DOM - ) - .map(([formDOM, filter, plots, headTable, tailTable]) => - div(".row .compound", { style: { margin: "0px 0px 0px 0px" } }, [ - formDOM, - div(".col .s10 .offset-s1", pageStyle, [ - div(".row", [filter]), - div(".row", [plots]), - div(".col .s12", [headTable]), - div(".row", []), - div(".col .s12", [tailTable]), - div(".row", []), - ]), - ]) - ) - - return { - log: xs.merge( - logger(state$, "state$"), - TreatmentFormSink.log, - filterForm.log, - binnedPlots.log, - headTable.log, - tailTable.log - ), - DOM: vdom$.startWith(div()), - onion: xs.merge( - defaultReducer$, - TreatmentFormSink.onion, - binnedPlots.onion, - filterForm.onion, - headTable.onion, - tailTable.onion, - scenarioReducer$, - uiReducer$ - ), - HTTP: xs.merge( - TreatmentFormSink.HTTP, - filterForm.HTTP, - binnedPlots.HTTP, - headTable.HTTP, - tailTable.HTTP - ), - vega: binnedPlots.vega, - popup: scenarioPopup$, - modal: xs.merge(TreatmentFormSink.modal), - ac: TreatmentFormSink.ac, - } -} + return GenericTreatmentWorkflow(enhancedSources) +} \ No newline at end of file diff --git a/src/js/pages/disease.js b/src/js/pages/disease.js index 28d3d299..35bc06f6 100644 --- a/src/js/pages/disease.js +++ b/src/js/pages/disease.js @@ -1,6 +1,8 @@ import { div } from "@cycle/dom" import xs from "xstream" import isolate from "@cycle/isolate" +import dropRepeats from "xstream/extra/dropRepeats" +import { equals } from "ramda" // Components import { SignatureForm, formLens } from "../components/SignatureForm" @@ -71,22 +73,74 @@ function DiseaseWorkflow(sources) { ...sources, input: signature$, }) + + /** + * Memory stream from filter output + * @const filter$ + * @type {MemoryStream} + */ const filter$ = filterForm.output.remember() - // Binned Plots Component + /** + * Setting of how to display the binned plots. Can be "before tables", "after tables", "no". + * Pull setting into a separate stream to aid function in vdom combining + * @const displayPlots$ + * @type {MemoryStream} + */ + const displayPlots$ = state$ + .map((state) => state.settings.plots.displayPlots) + .startWith("") + .compose(dropRepeats(equals)) + .remember() + + /** + * Isolated BinnedPlots component, containing 2 plots (similarity and histogram) + * + * Takes input data from 'input' + * + * Filter outputs if displayPlots$ is 'no' to prevent e.g. API calls when the graphs won't be displayed + * Combine signature$ and filter$ into an object for the input stream + * @const binnedPlots + * @type {Isolated(Component)} + */ const binnedPlots = isolate(BinnedPlots, { onion: plotsLens })({ ...sources, input: xs - .combine(signature$, filter$) - .map(([s, f]) => ({ signature: s, filter: f })) + .combine(signature$, filter$, displayPlots$) + .filter(([s, f, d]) => d !== "no") + .map(([s, f, _]) => ({ signature: s, filter: f })) .remember(), }) - // tables + /** + * Wrap a table with a table name and an option bar with tsv, json, and amount of lines modifier buttons + * Generic table for now, later will be refined to head table during isolation + * + * Takes input data from 'input' + * @const headTableContainer + * @type {Component} + */ const headTableContainer = makeTable(SampleTable, sampleTableLens) + + /** + * Wrap a table with a table name and an option bar with tsv, json, and amount of lines modifier buttons + * Generic table for now, later will be refined to tail table during isolation + * + * Takes input data from 'input' + * @const tailTableContainer + * @type {Component} + */ const tailTableContainer = makeTable(SampleTable, sampleTableLens) - // Join settings from api and sourire into props + /** + * Isolated table for top entries + * Add signature$ and filter$ into input stream + * + * Takes input data from 'input' + * Combine signature$ and filter$ into an object for the input stream + * @const headTable + * @type {Isolated(Component)} + */ const headTable = isolate(headTableContainer, { onion: headTableLens })({ ...sources, input: xs @@ -94,6 +148,16 @@ function DiseaseWorkflow(sources) { .map(([s, f]) => ({ query: s, filter: f })) .remember(), }) + + /** + * Isolated table for bottom entries + * Add signature$ and filter$ into input stream + * + * Takes input data from 'input' + * Combine signature$ and filter$ into an object for the input stream + * @const headTable + * @type {Isolated(Component)} + */ const tailTable = isolate(tailTableContainer, { onion: tailTableLens })({ ...sources, input: xs @@ -102,6 +166,11 @@ function DiseaseWorkflow(sources) { .remember(), }) + /** + * Style object used in div capsulating filter, displayPlots and tables + * @const pageStyle + * @type {object} + */ const pageStyle = { style: { fontSize: "14px", @@ -118,7 +187,8 @@ function DiseaseWorkflow(sources) { filterForm.DOM, binnedPlots.DOM, headTable.DOM, - tailTable.DOM + tailTable.DOM, + displayPlots$, // feedback$ ) .map( @@ -128,18 +198,20 @@ function DiseaseWorkflow(sources) { plots, headTable, tailTable, + displayPlots, // feedback ]) => div(".row .disease", { style: { margin: "0px 0px 0px 0px" } }, [ form, div(".col .s10 .offset-s1", pageStyle, [ div(".row", [filter]), - div(".row", [plots]), + div(".row", [displayPlots === "before tables" ? plots : div()]), div(".row", []), div(".col .s12", [headTable]), div(".row", []), div(".col .s12", [tailTable]), div(".row", []), + div(".row", [displayPlots === "after tables" ? plots : div()]), ]), ]) ) diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js new file mode 100644 index 00000000..2ffbe8d0 --- /dev/null +++ b/src/js/pages/genericTreatment.js @@ -0,0 +1,317 @@ +import { div } from "@cycle/dom" +import xs from "xstream" +import isolate from "@cycle/isolate" +import { TreatmentForm, treatmentLikeFilter } from "../components/TreatmentForm" +import { initSettings } from "../configuration.js" +import { makeTable, headTableLens, tailTableLens } from "../components/Table" +import { BinnedPlots, plotsLens } from "../components/BinnedPlots/BinnedPlots" +import { Filter, filterLens } from "../components/Filter" +import { loggerFactory } from "../utils/logger" +import { + SampleTable, + sampleTableLens, +} from "../components/SampleTable/SampleTable" + +// Support for ghost mode +import { scenario } from "../scenarios/treatmentScenario" +import { runScenario } from "../utils/scenario" + +import dropRepeats from "xstream/extra/dropRepeats" +import { equals } from "ramda" + + +/** + * @module pages/GenericTreatmentWorkflow + */ + +export default function GenericTreatmentWorkflow(sources) { + + // configuration of the generic treatment workflow, should be set by the calling page but provide defaults + const workflow = (sources??{}).workflow ?? {} + const workflowWelcomeText = workflow.welcomeText ?? "This is the generic treatment workflow template" + const workflowMainDivClass = workflow.mainDivClass ?? ".row .generic" + const workflowTreatmentType = workflow.treatmentType ?? treatmentLikeFilter.COMPOUND_AND_GENETIC + const workflowLoggerName = workflow.loggerName ?? "generic" + const workflowGhostModeScenarioSelector = workflow.ghostModeScenarioSelector ?? ((state) => state.settings.common.ghost.genetic) + + const logger = loggerFactory( + workflowLoggerName, + sources.onion.state$, + "settings.common.debug" + ) + + const state$ = sources.onion.state$ + + // Scenario for ghost mode + const scenarios$ = sources.onion.state$ + .take(1) + .filter((state) => state.settings.common.ghostMode) + .map(state => runScenario(scenario( workflowGhostModeScenarioSelector(state) ))) + const scenarioReducer$ = scenarios$.map(s => s.scenarioReducer$) + .flatten() + const scenarioPopup$ = scenarios$.map(s => s.scenarioPopup$) + .flatten() + .startWith({ text: workflowWelcomeText, duration: 4000 }) + + /** + * Lens to pass data from top level to TreatmentForm + * @const formLens + * @type {Lens} + */ + const formLens = { + get: (state) => ({ + form: state.form, + settings: { + form: state.settings.form, + api: state.settings.api, + common: state.settings.common, + geneAnnotations: state.settings.geneAnnotations, + compoundAnnotations: state.settings.compoundAnnotations, + treatmentLike: workflowTreatmentType, + }, + ui: (state.ui ?? {} ).form ?? {}, + }), + set: (state, childState) => ({ ...state, form: childState.form }), + } + + /** + * Default reducer; initialize if not yet done in parent (i.e. router) component (useful for testing) + * @const defaultReducer$ + * @type {Reducer} + */ + const defaultReducer$ = xs.of((prevState) => { + // defaultReducer + if (typeof prevState === "undefined") { + return { + settings: initSettings, + } + } else { + return { + ...prevState, + settings: prevState.settings, + } + } + }) + + /** + * UI dirty logic, checks TreatmentCheck, SampleSelection, SignatureGenerator and Filter components if they are dirty or busy + * If components are dirty/busy, enable UI dirty overlay in subsequent components. + * + * Use dropRepeats else the stream gets in an infinite loop + * @const uiReducer$ + * @type {Reducer} + */ + const uiReducer$ = state$.compose(dropRepeats(equals)) + .map(state => + prevState => { + const dirtyCheck = state.form.check.dirty + const dirtySampleSelection = state.form.sampleSelection.dirty + const busySignature = state.form.signature.busy + const dirtyFilter = state.filter.dirty + return ({...prevState, + ui: { + form: { + sampleSelection: {dirty: dirtyCheck }, + signature: {dirty: dirtyCheck || dirtySampleSelection }, + }, + headTable: {dirty: dirtyCheck || dirtySampleSelection || busySignature || dirtyFilter }, + tailTable: {dirty: dirtyCheck || dirtySampleSelection || busySignature || dirtyFilter }, + plots: {dirty: dirtyCheck || dirtySampleSelection || busySignature || dirtyFilter }, + }, + }) + } + ) + + /** + * Isolated TreatmentForm component/form, which contains TreatmentCheck, SampleSelection and SignatureGenerator + * + * Outputs data to 'output' + * @const TreatmentFormSink + * @type {Isolated(Component)} + */ + const TreatmentFormSink = isolate(TreatmentForm, { onion: formLens })(sources) + + /** + * Memory stream from TreatmentFormSink output + * @const signature$ + * @type {MemoryStream} + */ + const signature$ = TreatmentFormSink.output.remember() + + /** + * Isolated Filter component + * + * Takes input data from 'input' + * Outputs data to 'output' + * @const filterForm + * @type {Isolated(Component)} + */ + const filterForm = isolate(Filter, { onion: filterLens })({ + ...sources, + input: signature$, + }) + + /** + * Memory stream from filter output + * @const filter$ + * @type {MemoryStream} + */ + const filter$ = filterForm.output.remember() + + /** + * Setting of how to display the binned plots. Can be "before tables", "after tables", "no". + * Pull setting into a separate stream to aid function in vdom combining + * @const displayPlots$ + * @type {MemoryStream} + */ + const displayPlots$ = state$ + .map((state) => state.settings.plots.displayPlots) + .startWith("") + .compose(dropRepeats(equals)) + .remember() + + /** + * Isolated BinnedPlots component, containing 2 plots (similarity and histogram) + * + * Takes input data from 'input' + * + * Filter outputs if displayPlots$ is 'no' to prevent e.g. API calls when the graphs won't be displayed + * Combine signature$ and filter$ into an object for the input stream + * @const binnedPlots + * @type {Isolated(Component)} + */ + const binnedPlots = isolate(BinnedPlots, { onion: plotsLens })({ + ...sources, + input: xs + .combine(signature$, filter$, displayPlots$) + .filter(([s, f, d]) => d !== "no") + .map(([s, f, _]) => ({ signature: s, filter: f })) + .remember(), + }) + + /** + * Wrap a table with a table name and an option bar with tsv, json, and amount of lines modifier buttons + * Generic table for now, later will be refined to head table during isolation + * + * Takes input data from 'input' + * @const headTableContainer + * @type {Component} + */ + const headTableContainer = makeTable(SampleTable, sampleTableLens) + + /** + * Wrap a table with a table name and an option bar with tsv, json, and amount of lines modifier buttons + * Generic table for now, later will be refined to tail table during isolation + * + * Takes input data from 'input' + * @const tailTableContainer + * @type {Component} + */ + const tailTableContainer = makeTable(SampleTable, sampleTableLens) + + /** + * Isolated table for top entries + * Add signature$ and filter$ into input stream + * + * Takes input data from 'input' + * Combine signature$ and filter$ into an object for the input stream + * @const headTable + * @type {Isolated(Component)} + */ + const headTable = isolate(headTableContainer, { onion: headTableLens })({ + ...sources, + input: xs + .combine(signature$, filter$) + .map(([s, f]) => ({ query: s, filter: f })) + .remember(), + }) + + /** + * Isolated table for bottom entries + * Add signature$ and filter$ into input stream + * + * Takes input data from 'input' + * Combine signature$ and filter$ into an object for the input stream + * @const headTable + * @type {Isolated(Component)} + */ + const tailTable = isolate(tailTableContainer, { onion: tailTableLens })({ + ...sources, + input: xs + .combine(signature$, filter$) + .map(([s, f]) => ({ query: s, filter: f })) + .remember(), + }) + + /** + * Style object used in div capsulating filter, displayPlots and tables + * @const pageStyle + * @type {object} + */ + const pageStyle = { + style: { + fontSize: "14px", + opacity: "0", + transition: "opacity 1s", + delayed: { opacity: "1" }, + destroy: { opacity: "0" }, + }, + } + + const vdom$ = xs + .combine( + TreatmentFormSink.DOM, + filterForm.DOM, + binnedPlots.DOM, + headTable.DOM, + tailTable.DOM, + displayPlots$, + ) + .map(([formDOM, filter, plots, headTable, tailTable, displayPlots]) => + div(workflowMainDivClass /* something like ".row .genetic" */ , { style: { margin: "0px 0px 0px 0px" } }, [ + formDOM, + div(".col .s10 .offset-s1", pageStyle, [ + div(".row", [filter]), + div(".row", [displayPlots === "before tables" ? plots : div()]), + div(".col .s12", [headTable]), + div(".row", []), + div(".col .s12", [tailTable]), + div(".row", []), + div(".row", [displayPlots === "after tables" ? plots : div()]), + ]), + ]) + ) + + return { + log: xs.merge( + logger(state$, "state$"), + TreatmentFormSink.log, + filterForm.log, + binnedPlots.log, + headTable.log, + tailTable.log + ), + DOM: vdom$.startWith(div()), + onion: xs.merge( + defaultReducer$, + TreatmentFormSink.onion, + binnedPlots.onion, + filterForm.onion, + headTable.onion, + tailTable.onion, + scenarioReducer$, + uiReducer$, + ), + HTTP: xs.merge( + TreatmentFormSink.HTTP, + filterForm.HTTP, + binnedPlots.HTTP, + headTable.HTTP, + tailTable.HTTP + ), + vega: binnedPlots.vega, + popup: scenarioPopup$, + modal: xs.merge(TreatmentFormSink.modal), + ac: TreatmentFormSink.ac, + } +} diff --git a/src/js/pages/genetic.js b/src/js/pages/genetic.js index 14e138bc..b40b14c1 100644 --- a/src/js/pages/genetic.js +++ b/src/js/pages/genetic.js @@ -1,199 +1,18 @@ -import { div } from "@cycle/dom" -import xs from "xstream" -import isolate from "@cycle/isolate" -import { TreatmentForm, treatmentLikeFilter } from "../components/TreatmentForm" -import { initSettings } from "../configuration.js" -import { makeTable, headTableLens, tailTableLens } from "../components/Table" -import { BinnedPlots, plotsLens } from "../components/BinnedPlots/BinnedPlots" -import { Filter, filterLens } from "../components/Filter" -import { loggerFactory } from "../utils/logger" -import { - SampleTable, - sampleTableLens, -} from "../components/SampleTable/SampleTable" - -// Support for ghost mode -import { scenario } from "../scenarios/treatmentScenario" -import { runScenario } from "../utils/scenario" - -import dropRepeats from "xstream/extra/dropRepeats" -import { equals } from "ramda" +import { treatmentLikeFilter } from "../components/TreatmentForm" +import GenericTreatmentWorkflow from "./genericTreatment" export default function GeneticWorkflow(sources) { - const logger = loggerFactory( - "genetic", - sources.onion.state$, - "settings.common.debug" - ) - - const state$ = sources.onion.state$ - - // Scenario for ghost mode - const scenarios$ = sources.onion.state$ - .take(1) - .filter((state) => state.settings.common.ghostMode) - .map(state => runScenario(scenario(state.settings.common.ghost.genetic))) - const scenarioReducer$ = scenarios$.map(s => s.scenarioReducer$) - .flatten() - const scenarioPopup$ = scenarios$.map(s => s.scenarioPopup$) - .flatten() - .startWith({ text: "Welcome to Genetic Workflow", duration: 4000 }) - - const formLens = { - get: (state) => ({ - form: state.form, - settings: { - form: state.settings.form, - api: state.settings.api, - common: state.settings.common, - geneAnnotations: state.settings.geneAnnotations, - compoundAnnotations: state.settings.compoundAnnotations, - treatmentLike: treatmentLikeFilter.GENETIC, - }, - ui: (state.ui ?? {} ).form ?? {}, - }), - set: (state, childState) => ({ ...state, form: childState.form }), - } - - // Initialize if not yet done in parent (i.e. router) component (useful for testing) - const defaultReducer$ = xs.of((prevState) => { - // genetic -- defaultReducer - if (typeof prevState === "undefined") { - return { - settings: initSettings, - } - } else { - return { - ...prevState, - settings: prevState.settings, - } - } - }) - - // Use dropRepeats else the stream gets in an infinite loop - const uiReducer$ = state$.compose(dropRepeats(equals)) - .map(state => - prevState => { - const dirtyCheck = state.form.check.dirty - const dirtySampleSelection = state.form.sampleSelection.dirty - const busySignature = state.form.signature.busy - const dirtyFilter = state.filter.dirty - return ({...prevState, - ui: { - form: { - sampleSelection: {dirty: dirtyCheck }, - signature: {dirty: dirtyCheck || dirtySampleSelection }, - }, - headTable: {dirty: dirtyCheck || dirtySampleSelection || busySignature || dirtyFilter }, - tailTable: {dirty: dirtyCheck || dirtySampleSelection || busySignature || dirtyFilter }, - plots: {dirty: dirtyCheck || dirtySampleSelection || busySignature || dirtyFilter }, - }, - }) - } - ) - - const TreatmentFormSink = isolate(TreatmentForm, { onion: formLens })(sources) - const signature$ = TreatmentFormSink.output.remember() - // Filter Form - const filterForm = isolate(Filter, { onion: filterLens })({ + const enhancedSources = { ...sources, - input: signature$, - }) - const filter$ = filterForm.output.remember() - - // const filter$ = xs.of({}) - // Binned Plots Component - const binnedPlots = isolate(BinnedPlots, { onion: plotsLens })({ - ...sources, - input: xs - .combine(signature$, filter$) - .map(([s, f]) => ({ signature: s, filter: f })) - .remember(), - }) - - // tables - const headTableContainer = makeTable(SampleTable, sampleTableLens) - const tailTableContainer = makeTable(SampleTable, sampleTableLens) - - // Join settings from api and sourire into props - const headTable = isolate(headTableContainer, { onion: headTableLens })({ - ...sources, - input: xs - .combine(signature$, filter$) - .map(([s, f]) => ({ query: s, filter: f })) - .remember(), - }) - const tailTable = isolate(tailTableContainer, { onion: tailTableLens })({ - ...sources, - input: xs - .combine(signature$, filter$) - .map(([s, f]) => ({ query: s, filter: f })) - .remember(), - }) - - const pageStyle = { - style: { - fontSize: "14px", - opacity: "0", - transition: "opacity 1s", - delayed: { opacity: "1" }, - destroy: { opacity: "0" }, - }, + workflow: { + treatmentType: treatmentLikeFilter.GENETIC, + welcomeText: "Welcome to Genetic Workflow", + mainDivClass: ".row .genetic", + loggerName: "genetic", + ghostModeScenarioSelector: ((state) => state.settings.common.ghost.genetic), + }, } - const vdom$ = xs - .combine( - TreatmentFormSink.DOM, - filterForm.DOM, - binnedPlots.DOM, - headTable.DOM, - tailTable.DOM - ) - .map(([formDOM, filter, plots, headTable, tailTable]) => - div(".row .genetic", { style: { margin: "0px 0px 0px 0px" } }, [ - formDOM, - div(".col .s10 .offset-s1", pageStyle, [ - div(".row", [filter]), - div(".row", [plots]), - div(".col .s12", [headTable]), - div(".row", []), - div(".col .s12", [tailTable]), - div(".row", []), - ]), - ]) - ) - - return { - log: xs.merge( - logger(state$, "state$"), - TreatmentFormSink.log, - filterForm.log, - binnedPlots.log, - headTable.log, - tailTable.log - ), - DOM: vdom$.startWith(div()), - onion: xs.merge( - defaultReducer$, - TreatmentFormSink.onion, - binnedPlots.onion, - filterForm.onion, - headTable.onion, - tailTable.onion, - scenarioReducer$, - uiReducer$, - ), - HTTP: xs.merge( - TreatmentFormSink.HTTP, - filterForm.HTTP, - binnedPlots.HTTP, - headTable.HTTP, - tailTable.HTTP - ), - vega: binnedPlots.vega, - popup: scenarioPopup$, - modal: xs.merge(TreatmentFormSink.modal), - ac: TreatmentFormSink.ac, - } + return GenericTreatmentWorkflow(enhancedSources) } diff --git a/src/js/pages/settings.js b/src/js/pages/settings.js index 3846b2d3..9481c25b 100644 --- a/src/js/pages/settings.js +++ b/src/js/pages/settings.js @@ -35,8 +35,8 @@ export function Settings(sources) { }, { field: "pvalue", - class: "text", - type: ".input-field", + class: ".input-field", + type: "text", title: "p-value", props: { type: "text" }, }, @@ -99,6 +99,14 @@ export function Settings(sources) { group: "plots", title: "Combined (binned) plots", settings: [ + { + field: "displayPlots", + type: "select", + class: ".switch", + title: "Display plots?", + options: ["before tables", "after tables", "no"], + props: { type: "checkbox" }, + }, { field: "bins", class: ".range-field", @@ -127,6 +135,41 @@ export function Settings(sources) { .map((event) => event) : sources.DOM.events("input").map((event) => event.target.value) + function renderField(config, _state) { + if (config.type == "checkbox") { + return [ + label(".active", [ + input({ props: merge(config.props, { checked: _state }) }), + span(".lever"), + ]), + ] + } + if (config.type == "text" || config.type == "range") { + return [input({ props: merge(config.props, { value: _state }) })] + } + if (config.type == "select") { + const options = config.options + const selectedOption = (option) => + _state == option + ? ".grey.lighten-3.black-text" + : ".grey.lighten-3.grey-text.text-lighten-1" + const optionButtons = options.map((o) => + div( + ".col.selection" + selectedOption(o) + "." + o, + { style: { "border-style": "solid", margin: "2px" } }, + [ + label(selectedOption(o), [ + input("", { props: merge(config.props, { value: o }) }, ""), + o, + ]), + ] + ) + ) + return optionButtons + } + } + + const vdom$ = state$.map((state) => li( ".collection-item .row", @@ -143,21 +186,7 @@ export function Settings(sources) { div( ".col .s6 " + config.class, - - config.type == "checkbox" - ? // Checkbox form - [ - label(".active", [ - // config.title, - input({ props: merge(config.props, { checked: state }) }), - span(".lever"), - ]), - ] - : // Range or input - [ - input({ props: merge(config.props, { value: state }) }), - // label(config.field), - ] + renderField(config, state) ), ]) ) From 340c6d8caa11be3958efc27938e0b2130f0fcf1f Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 3 Dec 2021 12:16:21 +0100 Subject: [PATCH 051/191] SampleTable defaultAmountToDisplay$ accumulator is still wrong (#77) * Update default value logic to not just use fold but actually use last value to take difference Still use fold though to have access to the old value but instead put old and new value in array * Use pairwise instead of fold to create an array of old and new value pairwise is built in logic that does exactly what was needed, just needed to add .startWith(0) to get a result from the first input value --- src/js/components/Table.js | 41 +++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/src/js/components/Table.js b/src/js/components/Table.js index 4f8ad3f5..e9bbc1be 100644 --- a/src/js/components/Table.js +++ b/src/js/components/Table.js @@ -49,6 +49,7 @@ import { loggerFactory } from "../utils/logger" import { convertToCSV } from "../utils/export" import delay from "xstream/extra/delay" import debounce from "xstream/extra/debounce" +import pairwise from "xstream/extra/pairwise" import { dirtyWrapperStream } from "../utils/ui" // Granular access to the settings @@ -207,12 +208,42 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { .mapTo(-10) .startWith(0) - // Only take the difference of the default value compared to old value - // 1. Prevent state changes adding the default value in the accumulator - // 2. Needs to be in the accumulator otherwise we can't reduce the amount of lines less than the default setting - // 3. By folding & limiting the value here we prevent (hidden) negative numbers that the user would have to increase before seeing changes again + /** + * Only take the difference of the default value compared to old value + * + * Prevent state changes adding the default value in the accumulator. + * Needs to have previous and new value and take difference. Simple accumulator with fold would cycle between zero and new value. + * + * Desired behaviour + * acc newInput output + * initial 0 5 5 + * first update 5 5 0 + * second update 5 5 0 + * + * Highlight why .fold((acc, newValue) => newValue - acc, 0) doesn't work: + * acc newInput output + * initial 0 5 5 + * first update 5 5 0 + * second update 0 5 5 + * + * pairwise gives us previous and new value but need to make sure that if we only receive 1 value we do get an output, so use .startWith(0) + * + * @const defaultAmountToDisplay$ + * @type {Stream} + */ const defaultAmountToDisplay$ = newInput$.map(state => parseInt(state.settings.table.count)) - .fold((acc, newValue) => newValue - acc, 0) + .startWith(0) + .compose(pairwise) + .map((v) => (v[1] - v[0])) + + /** + * Merge all + and - buttons with default value + * Default value needs to be in the accumulator otherwise we can't reduce the amount of lines less than the default setting + * By folding & limiting the value here we prevent (hidden) negative numbers that the user would have to increase before seeing changes again + * + * @const amountToDisplay + * @type {Stream} + */ const amountToDisplay$ = xs .merge(defaultAmountToDisplay$, plus5$, min5$, plus10$, min10$) .fold((x, y) => max(0, x + y), 0) From e2fb548e025e9f0c1e85ae0f97809361036073d0 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 3 Dec 2021 12:16:50 +0100 Subject: [PATCH 052/191] Dependency update (#79) * update css-loader seems to work out of the box 50 -> 45 vulnerabilities * update webpack-dev-server small update in webpack.config.js because of api changes tested in standard live tests and also by building docker container 45 -> 39 vulnerabilities * update jquery tested in standard live tests and also by building docker container 32 vulnerabilities left * Update datalib and push resolved package lodash to latest version 17 vulnerabilities remaining --- package-lock.json | 16144 +++++++++++++++++--------------------------- package.json | 8 +- webpack.config.js | 6 +- 3 files changed, 6048 insertions(+), 10110 deletions(-) diff --git a/package-lock.json b/package-lock.json index c7805130..6bbcfc83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,2505 +1,2473 @@ { "name": "LuciusWeb", - "version": "3.2.1", + "version": "5.0.0-alpha5", "lockfileVersion": 1, "requires": true, "dependencies": { - "7zip-bin": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-4.0.2.tgz", - "integrity": "sha512-XtGk+IF57pr852UK1AhQJXqmm1WmSgS5uISL+LPs0z/iAxXouMvdlLJrHPeukP6gd7yR2rDTMSMkHNODgwIq7A==", - "dev": true - }, - "@cycle/dom": { - "version": "20.4.0", - "resolved": "https://registry.npmjs.org/@cycle/dom/-/dom-20.4.0.tgz", - "integrity": "sha512-67z3FLe1MZhIVdI/HWS+7WlNGWEWsPaF36bW1u96D35dIsyLBirgggswk2Za1uoXTqt5bs6s4Wvkh8F+4RLD0w==", - "requires": { - "@cycle/run": "*", - "es6-map": "^0.1.4", - "snabbdom": "0.7.0", - "snabbdom-selector": "^3.0.0", - "xstream": "*" - } - }, - "@cycle/history": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@cycle/history/-/history-6.10.0.tgz", - "integrity": "sha512-YLkGGtL9dmWE+0nUSLIkarNjVw8IDnV1IFN9H8Jdjr7vj+YPvveCD0cs9gDyQ+ob9Jq8hBks55/VnyuQCE9SDg==", - "requires": { - "@cycle/run": "*", - "@types/history": "4.6.x", - "history": "4.7.x", - "xstream": "*" - } - }, - "@cycle/http": { - "version": "14.9.0", - "resolved": "https://registry.npmjs.org/@cycle/http/-/http-14.9.0.tgz", - "integrity": "sha512-wOLMflfsszUXej5wSNxo06VgFqeWZ/GQFz/OzcrHv4r3oXvBsFh9KyyvIV6kOLpaZ+8bkOwBWbpGbJZWK00NFQ==", - "requires": { - "@cycle/run": "*", - "@types/superagent": "3.5.6", - "superagent": "3.8.2", - "xstream": "*" - } - }, - "@cycle/isolate": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@cycle/isolate/-/isolate-3.4.0.tgz", - "integrity": "sha512-mOAlwLeTr6wTdHwKWAfaFeuKeD540kKcJlLVKsqLhbfLp6orF1B3CzMfFNlmqNY30t6o6TORCFfV+0EATK9Y7Q==", - "requires": { - "@cycle/run": "^4.4.0", - "xstream": "^11.7.0" - } - }, - "@cycle/run": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@cycle/run/-/run-4.4.0.tgz", - "integrity": "sha512-vVxnTqKKlgasE+we9X2z3og8z5KouO2RMiIgHWkVek+NomsdaeZwfvbutqzm3VToEImaz0DE2Iln9AxtCOVjpQ==", - "requires": { - "quicktask": "1.1.0", - "xstream": "10.x || 11.x" - } - }, - "@cycle/storage": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@cycle/storage/-/storage-4.1.1.tgz", - "integrity": "sha512-Bw0mn2r4/DzoVjcP11Z35DMt92UUn6b2YteHKW4ph3koyiGWlspxNRVL/fOUwMd/VcrvSTcrtxPEt6uDxEpZXQ==", + "@babel/cli": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.16.0.tgz", + "integrity": "sha512-WLrM42vKX/4atIoQB+eb0ovUof53UUvecb4qGjU2PDDWRiZr50ZpiV8NpcLo7iSxeGYrRG0Mqembsa+UrTAV6Q==", + "dev": true, "requires": { - "@cycle/run": "^3.1.0" + "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3", + "chokidar": "^3.4.0", + "commander": "^4.0.1", + "convert-source-map": "^1.1.0", + "fs-readdir-recursive": "^1.1.0", + "glob": "^7.0.0", + "make-dir": "^2.1.0", + "slash": "^2.0.0", + "source-map": "^0.5.0" }, "dependencies": { - "@cycle/run": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@cycle/run/-/run-3.4.0.tgz", - "integrity": "sha512-YUZyPu0nC4YDC31mLH5PGxbMoPEH5dNEV+nmgt34GgGgJ0ykDd4PrY7/ph5MAEpQE6rOfov0VN44qQRs6beQow==", + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, "requires": { - "xstream": "10.x || 11.x" + "pify": "^4.0.1", + "semver": "^5.6.0" } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true } } }, - "@types/history": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@types/history/-/history-4.6.2.tgz", - "integrity": "sha512-eVAb52MJ4lfPLiO9VvTgv8KaZDEIqCwhv+lXOMLlt4C1YHTShgmMULEg0RrCbnqfYd6QKfHsMp0MiX0vWISpSw==" - }, - "@types/node": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.1.2.tgz", - "integrity": "sha512-bjk1RIeZBCe/WukrFToIVegOf91Pebr8cXYBwLBIsfiGWVQ+ifwWsT59H3RxrWzWrzd1l/Amk1/ioY5Fq3/bpA==" - }, - "@types/superagent": { - "version": "3.5.6", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.5.6.tgz", - "integrity": "sha512-yGiVkRbB1qtIkRCpEJIxlHazBoILmu33xbbu4IiwxTJjwDi/EudiPYAD7QwWe035jkE40yQgTVXZsAePFtleww==", + "@babel/code-frame": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.0.tgz", + "integrity": "sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA==", + "dev": true, "requires": { - "@types/node": "*" + "@babel/highlight": "^7.16.0" } }, - "abbrev": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", - "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=" + "@babel/compat-data": { + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.4.tgz", + "integrity": "sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q==", + "dev": true }, - "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "@babel/core": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.0.tgz", + "integrity": "sha512-mYZEvshBRHGsIAiyH5PzCFTCfbWfoYbO/jcSdXQSUQu1/pW0xDZAUP7KEc32heqWTAfAHhV9j1vH8Sav7l+JNQ==", "dev": true, "requires": { - "mime-types": "~2.1.18", - "negotiator": "0.6.1" + "@babel/code-frame": "^7.16.0", + "@babel/generator": "^7.16.0", + "@babel/helper-compilation-targets": "^7.16.0", + "@babel/helper-module-transforms": "^7.16.0", + "@babel/helpers": "^7.16.0", + "@babel/parser": "^7.16.0", + "@babel/template": "^7.16.0", + "@babel/traverse": "^7.16.0", + "@babel/types": "^7.16.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" }, "dependencies": { - "mime-db": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", - "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==", - "dev": true + "convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } }, - "mime-types": { - "version": "2.1.19", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", - "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { - "mime-db": "~1.35.0" + "ms": "2.1.2" } + }, + "json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true } } }, - "acorn-dynamic-import": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz", - "integrity": "sha1-x1K9IQvvZ5UBtsbLf8hPj0cVjMQ=", + "@babel/generator": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.0.tgz", + "integrity": "sha512-RR8hUCfRQn9j9RPKEVXo9LiwoxLPYn6hNZlvUOR8tSnaxlD0p0+la00ZP9/SnRt6HchKr+X0fO2r8vrETiJGew==", "dev": true, "requires": { - "acorn": "^4.0.3" + "@babel/types": "^7.16.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" }, "dependencies": { - "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=", + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true } } }, - "ajv": { - "version": "4.11.5", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.5.tgz", - "integrity": "sha1-tu50ZXuZOgHc5Et5RNVvSFgo1b0=", + "@babel/helper-annotate-as-pure": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.0.tgz", + "integrity": "sha512-ItmYF9vR4zA8cByDocY05o0LGUkp1zhbTQOH1NFyl5xXEqlTJQCEJjieriw+aFpxo16swMxUnUiKS7a/r4vtHg==", + "dev": true, "requires": { - "co": "^4.6.0", - "json-stable-stringify": "^1.0.1" + "@babel/types": "^7.16.0" } }, - "ajv-keywords": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", - "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", - "dev": true - }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.0.tgz", + "integrity": "sha512-9KuleLT0e77wFUku6TUkqZzCEymBdtuQQ27MhEKzf9UOOJu3cYj98kyaDAzxpC7lV6DGiZFuC8XqDsq8/Kl6aQ==", "dev": true, "requires": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" + "@babel/helper-explode-assignable-expression": "^7.16.0", + "@babel/types": "^7.16.0" } }, - "alphanum-sort": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", - "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", - "dev": true - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, - "ansi-align": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", - "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "@babel/helper-compilation-targets": { + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.3.tgz", + "integrity": "sha512-vKsoSQAyBmxS35JUOOt+07cLc6Nk/2ljLIHwmq2/NM6hdioUaqEXq/S+nXvbvXbZkNDlWOymPanJGOc4CBjSJA==", "dev": true, "requires": { - "string-width": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } + "@babel/compat-data": "^7.16.0", + "@babel/helper-validator-option": "^7.14.5", + "browserslist": "^4.17.5", + "semver": "^6.3.0" } }, - "ansi-html": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", - "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "anymatch": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.0.tgz", - "integrity": "sha1-o+Uvo5FoyCX/V7AkgSbOWo/5VQc=", + "@babel/helper-create-class-features-plugin": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.0.tgz", + "integrity": "sha512-XLwWvqEaq19zFlF5PTgOod4bUA+XbkR4WLQBct1bkzmxJGB0ZEJaoKF4c8cgH9oBtCDuYJ8BP5NB9uFiEgO5QA==", "dev": true, "requires": { - "arrify": "^1.0.0", - "micromatch": "^2.1.5" + "@babel/helper-annotate-as-pure": "^7.16.0", + "@babel/helper-function-name": "^7.16.0", + "@babel/helper-member-expression-to-functions": "^7.16.0", + "@babel/helper-optimise-call-expression": "^7.16.0", + "@babel/helper-replace-supers": "^7.16.0", + "@babel/helper-split-export-declaration": "^7.16.0" } }, - "app-builder-bin": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-2.1.2.tgz", - "integrity": "sha512-PZJspzAqB0+z60OalXChP9I05BzODd/ffDz6RvTmDG3qclr7YrnpqzvPF+T7vGVtk2nN7syuveTQROJfXcB8xA==", - "dev": true - }, - "app-builder-lib": { - "version": "20.28.1", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-20.28.1.tgz", - "integrity": "sha512-OjPTarC27/P3312dNu8N6k2X1r6QGr/q243+bM+DnXddZ6qZQQDsxJz5ONW8b1chRErTUZDRaKQ8RdAYjUIbxw==", + "@babel/helper-create-regexp-features-plugin": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.16.0.tgz", + "integrity": "sha512-3DyG0zAFAZKcOp7aVr33ddwkxJ0Z0Jr5V99y3I690eYLpukJsJvAbzTy1ewoCqsML8SbIrjH14Jc/nSQ4TvNPA==", "dev": true, "requires": { - "7zip-bin": "~4.0.2", - "app-builder-bin": "2.1.2", - "async-exit-hook": "^2.0.1", - "bluebird-lst": "^1.0.5", - "builder-util": "6.1.1", - "builder-util-runtime": "4.4.1", - "chromium-pickle-js": "^0.2.0", - "debug": "^3.1.0", - "ejs": "^2.6.1", - "electron-osx-sign": "0.4.10", - "electron-publish": "20.28.0", - "fs-extra-p": "^4.6.1", - "hosted-git-info": "^2.7.1", - "is-ci": "^1.1.0", - "isbinaryfile": "^3.0.3", - "js-yaml": "^3.12.0", - "lazy-val": "^1.0.3", - "minimatch": "^3.0.4", - "normalize-package-data": "^2.4.0", - "plist": "^3.0.1", - "read-config-file": "3.1.2", - "sanitize-filename": "^1.6.1", - "semver": "^5.5.0", - "temp-file": "^3.1.3" + "@babel/helper-annotate-as-pure": "^7.16.0", + "regexpu-core": "^4.7.1" }, "dependencies": { - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "regexpu-core": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.8.0.tgz", + "integrity": "sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg==", "dev": true, "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^9.0.0", + "regjsgen": "^0.5.2", + "regjsparser": "^0.7.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.0.0" } - }, + } + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.0.tgz", + "integrity": "sha512-7hfT8lUljl/tM3h+izTX/pO3W3frz2ok6Pk+gzys8iJqDfZrZy2pXjRTZAvG2YmfHun1X4q8/UZRLatMfqc5Tg==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.13.0", + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/traverse": "^7.13.0", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "dependencies": { "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", - "dev": true - }, - "js-yaml": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", - "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { - "brace-expansion": "^1.1.7" + "ms": "2.1.2" } }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "semver": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", - "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } }, - "aproba": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.2.tgz", - "integrity": "sha512-ZpYajIfO0j2cOFTO955KUMIKNmj6zhX8kVztMAxFsDaMwz+9Z9SV0uou2pC9HJqcfpffOsjnbrDMvkNy+9RXPw==" - }, - "are-we-there-yet": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "@babel/helper-explode-assignable-expression": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.0.tgz", + "integrity": "sha512-Hk2SLxC9ZbcOhLpg/yMznzJ11W++lg5GMbxt1ev6TXUiJB0N42KPC+7w8a+eWGuqDnUYuwStJoZHM7RgmIOaGQ==", + "dev": true, "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" + "@babel/types": "^7.16.0" } }, - "argparse": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", - "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "@babel/helper-function-name": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.0.tgz", + "integrity": "sha512-BZh4mEk1xi2h4HFjWUXRQX5AEx4rvaZxHgax9gcjdLWdkjsY7MKt5p0otjsg5noXw+pB+clMCjw+aEVYADMjog==", + "dev": true, "requires": { - "sprintf-js": "~1.0.2" + "@babel/helper-get-function-arity": "^7.16.0", + "@babel/template": "^7.16.0", + "@babel/types": "^7.16.0" } }, - "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "@babel/helper-get-function-arity": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.0.tgz", + "integrity": "sha512-ASCquNcywC1NkYh/z7Cgp3w31YW8aojjYIlNg4VeJiHkqyP4AzIvr4qx7pYDb4/s8YcsZWqqOSxgkvjUz1kpDQ==", "dev": true, "requires": { - "arr-flatten": "^1.0.1" + "@babel/types": "^7.16.0" } }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" + "@babel/helper-hoist-variables": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.0.tgz", + "integrity": "sha512-1AZlpazjUR0EQZQv3sgRNfM9mEVWPK3M6vlalczA+EECcPz3XPh6VplbErL5UoMpChhSck5wAJHthlj1bYpcmg==", + "dev": true, + "requires": { + "@babel/types": "^7.16.0" + } }, - "array-flatten": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.1.tgz", - "integrity": "sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=", - "dev": true + "@babel/helper-member-expression-to-functions": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.0.tgz", + "integrity": "sha512-bsjlBFPuWT6IWhl28EdrQ+gTvSvj5tqVP5Xeftp07SEuz5pLnsXZuDkDD3Rfcxy0IsHmbZ+7B2/9SHzxO0T+sQ==", + "dev": true, + "requires": { + "@babel/types": "^7.16.0" + } }, - "array-includes": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", - "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", + "@babel/helper-module-imports": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz", + "integrity": "sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.7.0" + "@babel/types": "^7.16.0" } }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "@babel/helper-module-transforms": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.0.tgz", + "integrity": "sha512-My4cr9ATcaBbmaEa8M0dZNA74cfI6gitvUAskgDtAFmAqyFKDSHQo5YstxPbN+lzHl2D9l/YOEFqb2mtUh4gfA==", "dev": true, "requires": { - "array-uniq": "^1.0.1" + "@babel/helper-module-imports": "^7.16.0", + "@babel/helper-replace-supers": "^7.16.0", + "@babel/helper-simple-access": "^7.16.0", + "@babel/helper-split-export-declaration": "^7.16.0", + "@babel/helper-validator-identifier": "^7.15.7", + "@babel/template": "^7.16.0", + "@babel/traverse": "^7.16.0", + "@babel/types": "^7.16.0" } }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true + "@babel/helper-optimise-call-expression": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.0.tgz", + "integrity": "sha512-SuI467Gi2V8fkofm2JPnZzB/SUuXoJA5zXe/xzyPP2M04686RzFKFHPK6HDVN6JvWBIEW8tt9hPR7fXdn2Lgpw==", + "dev": true, + "requires": { + "@babel/types": "^7.16.0" + } }, - "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "@babel/helper-plugin-utils": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", "dev": true }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true + "@babel/helper-remap-async-to-generator": { + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.4.tgz", + "integrity": "sha512-vGERmmhR+s7eH5Y/cp8PCVzj4XEjerq8jooMfxFdA5xVtAk9Sh4AQsrWgiErUEBjtGrBtOFKDUcWQFW4/dFwMA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.0", + "@babel/helper-wrap-function": "^7.16.0", + "@babel/types": "^7.16.0" + } }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + "@babel/helper-replace-supers": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.0.tgz", + "integrity": "sha512-TQxuQfSCdoha7cpRNJvfaYxxxzmbxXw/+6cS7V02eeDYyhxderSoMVALvwupA54/pZcOTtVeJ0xccp1nGWladA==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.16.0", + "@babel/helper-optimise-call-expression": "^7.16.0", + "@babel/traverse": "^7.16.0", + "@babel/types": "^7.16.0" + } }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + "@babel/helper-simple-access": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.0.tgz", + "integrity": "sha512-o1rjBT/gppAqKsYfUdfHq5Rk03lMQrkPHG1OWzHWpLgVXRH4HnMM9Et9CVdIqwkCQlobnGHEJMsgWP/jE1zUiw==", + "dev": true, + "requires": { + "@babel/types": "^7.16.0" + } }, - "asn1.js": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.1.tgz", - "integrity": "sha1-SLokC0WpKA6UdImQull9IWYX/UA=", + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz", + "integrity": "sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw==", "dev": true, "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" + "@babel/types": "^7.16.0" } }, - "assert": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", - "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "@babel/helper-split-export-declaration": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.0.tgz", + "integrity": "sha512-0YMMRpuDFNGTHNRiiqJX19GjNXA4H0E8jZ2ibccfSxaCogbm3am5WN/2nQNj0YnQwGWM1J06GOcQ2qnh3+0paw==", "dev": true, "requires": { - "util": "0.10.3" + "@babel/types": "^7.16.0" } }, - "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + "@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "@babel/helper-validator-option": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", + "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", "dev": true }, - "async": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", - "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", - "dev": true, - "requires": { - "lodash": "^4.14.0" - } - }, - "async-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", - "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", - "dev": true - }, - "async-exit-hook": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", - "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", - "dev": true - }, - "async-foreach": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", - "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "atob": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/atob/-/atob-1.1.3.tgz", - "integrity": "sha1-lfE2KbEsOlGl0hWr3OKqnzL4B3M=", - "dev": true - }, - "autoprefixer": { - "version": "6.7.7", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.7.7.tgz", - "integrity": "sha1-Hb0cg1ZY41zj+ZhAmdsAWFx4IBQ=", + "@babel/helper-wrap-function": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.16.0.tgz", + "integrity": "sha512-VVMGzYY3vkWgCJML+qVLvGIam902mJW0FvT7Avj1zEe0Gn7D93aWdLblYARTxEw+6DhZmtzhBM2zv0ekE5zg1g==", "dev": true, "requires": { - "browserslist": "^1.7.6", - "caniuse-db": "^1.0.30000634", - "normalize-range": "^0.1.2", - "num2fraction": "^1.2.2", - "postcss": "^5.2.16", - "postcss-value-parser": "^3.2.3" - } - }, - "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" - }, - "aws4": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" - }, - "babel-code-frame": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", - "integrity": "sha1-AnYgvuVnqIwyVhV05/0IAdMxGOQ=", - "dev": true, - "requires": { - "chalk": "^1.1.0", - "esutils": "^2.0.2", - "js-tokens": "^3.0.0" - } - }, - "babel-core": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", - "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", - "dev": true, - "requires": { - "babel-code-frame": "^6.26.0", - "babel-generator": "^6.26.0", - "babel-helpers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-register": "^6.26.0", - "babel-runtime": "^6.26.0", - "babel-template": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "convert-source-map": "^1.5.1", - "debug": "^2.6.9", - "json5": "^0.5.1", - "lodash": "^4.17.4", - "minimatch": "^3.0.4", - "path-is-absolute": "^1.0.1", - "private": "^0.1.8", - "slash": "^1.0.0", - "source-map": "^0.5.7" - }, - "dependencies": { - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" - } - }, - "babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "dev": true, - "requires": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - } - }, - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "convert-source-map": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", - "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } + "@babel/helper-function-name": "^7.16.0", + "@babel/template": "^7.16.0", + "@babel/traverse": "^7.16.0", + "@babel/types": "^7.16.0" } }, - "babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "@babel/helpers": { + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.16.3.tgz", + "integrity": "sha512-Xn8IhDlBPhvYTvgewPKawhADichOsbkZuzN7qz2BusOM0brChsyXMDJvldWaYMMUNiCQdQzNEioXTp3sC8Nt8w==", "dev": true, "requires": { - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "detect-indent": "^4.0.0", - "jsesc": "^1.3.0", - "lodash": "^4.17.4", - "source-map": "^0.5.7", - "trim-right": "^1.0.1" - }, - "dependencies": { - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - } - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } + "@babel/template": "^7.16.0", + "@babel/traverse": "^7.16.3", + "@babel/types": "^7.16.0" } }, - "babel-helper-bindify-decorators": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz", - "integrity": "sha1-FMGeXxQte0fxmlJDHlKxzLxAozA=", + "@babel/highlight": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", + "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "@babel/helper-validator-identifier": "^7.15.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" } }, - "babel-helper-builder-binary-assignment-operator-visitor": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz", - "integrity": "sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=", - "dev": true, - "requires": { - "babel-helper-explode-assignable-expression": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" - } + "@babel/parser": { + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.4.tgz", + "integrity": "sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng==", + "dev": true }, - "babel-helper-call-delegate": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", - "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.16.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.2.tgz", + "integrity": "sha512-h37CvpLSf8gb2lIJ2CgC3t+EjFbi0t8qS7LCS1xcJIlEXE4czlofwaW7W1HA8zpgOCzI9C1nmoqNR1zWkk0pQg==", "dev": true, "requires": { - "babel-helper-hoist-variables": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-helper-define-map": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.24.1.tgz", - "integrity": "sha1-epdH8ljYlH0y1RX2qhx70CIEoIA=", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.0.tgz", + "integrity": "sha512-4tcFwwicpWTrpl9qjf7UsoosaArgImF85AxqCRZlgc3IQDvkUHjJpruXAL58Wmj+T6fypWTC/BakfEkwIL/pwA==", "dev": true, "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1", - "lodash": "^4.2.0" + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", + "@babel/plugin-proposal-optional-chaining": "^7.16.0" } }, - "babel-helper-explode-assignable-expression": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz", - "integrity": "sha1-8luCz33BBDPFX3BZLVdGQArCLKo=", + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.4.tgz", + "integrity": "sha512-/CUekqaAaZCQHleSK/9HajvcD/zdnJiKRiuUFq8ITE+0HsPzquf53cpFiqAwl/UfmJbR6n5uGPQSPdrmKOvHHg==", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-remap-async-to-generator": "^7.16.4", + "@babel/plugin-syntax-async-generators": "^7.8.4" } }, - "babel-helper-explode-class": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz", - "integrity": "sha1-fcKjkQ3uAHBW4eMdZAztPVTqqes=", + "@babel/plugin-proposal-class-properties": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.0.tgz", + "integrity": "sha512-mCF3HcuZSY9Fcx56Lbn+CGdT44ioBMMvjNVldpKtj8tpniETdLjnxdHI1+sDWXIM1nNt+EanJOZ3IG9lzVjs7A==", "dev": true, "requires": { - "babel-helper-bindify-decorators": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "@babel/helper-create-class-features-plugin": "^7.16.0", + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-helper-function-name": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", - "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "@babel/plugin-proposal-class-static-block": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.16.0.tgz", + "integrity": "sha512-mAy3sdcY9sKAkf3lQbDiv3olOfiLqI51c9DR9b19uMoR2Z6r5pmGl7dfNFqEvqOyqbf1ta4lknK4gc5PJn3mfA==", "dev": true, "requires": { - "babel-helper-get-function-arity": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "@babel/helper-create-class-features-plugin": "^7.16.0", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" } }, - "babel-helper-get-function-arity": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", - "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "@babel/plugin-proposal-dynamic-import": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.0.tgz", + "integrity": "sha512-QGSA6ExWk95jFQgwz5GQ2Dr95cf7eI7TKutIXXTb7B1gCLTCz5hTjFTQGfLFBBiC5WSNi7udNwWsqbbMh1c4yQ==", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" } }, - "babel-helper-hoist-variables": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", - "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.0.tgz", + "integrity": "sha512-CjI4nxM/D+5wCnhD11MHB1AwRSAYeDT+h8gCdcVJZ/OK7+wRzFsf7PFPWVpVpNRkHMmMkQWAHpTq+15IXQ1diA==", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" } }, - "babel-helper-optimise-call-expression": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", - "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", + "@babel/plugin-proposal-json-strings": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.0.tgz", + "integrity": "sha512-kouIPuiv8mSi5JkEhzApg5Gn6hFyKPnlkO0a9YSzqRurH8wYzSlf6RJdzluAsbqecdW5pBvDJDfyDIUR/vLxvg==", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" } }, - "babel-helper-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.24.1.tgz", - "integrity": "sha1-024i+rEAjXnYhkjjIRaGgShFbOg=", + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.0.tgz", + "integrity": "sha512-pbW0fE30sVTYXXm9lpVQQ/Vc+iTeQKiXlaNRZPPN2A2VdlWyAtsUrsQ3xydSlDW00TFMK7a8m3cDTkBF5WnV3Q==", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1", - "lodash": "^4.2.0" + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" } }, - "babel-helper-remap-async-to-generator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz", - "integrity": "sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=", + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.0.tgz", + "integrity": "sha512-3bnHA8CAFm7cG93v8loghDYyQ8r97Qydf63BeYiGgYbjKKB/XP53W15wfRC7dvKfoiJ34f6Rbyyx2btExc8XsQ==", "dev": true, "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" } }, - "babel-helper-replace-supers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", - "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", + "@babel/plugin-proposal-numeric-separator": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.0.tgz", + "integrity": "sha512-FAhE2I6mjispy+vwwd6xWPyEx3NYFS13pikDBWUAFGZvq6POGs5eNchw8+1CYoEgBl9n11I3NkzD7ghn25PQ9Q==", "dev": true, "requires": { - "babel-helper-optimise-call-expression": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" } }, - "babel-helpers": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", - "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.16.0.tgz", + "integrity": "sha512-LU/+jp89efe5HuWJLmMmFG0+xbz+I2rSI7iLc1AlaeSMDMOGzWlc5yJrMN1d04osXN4sSfpo4O+azkBNBes0jg==", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" + "@babel/compat-data": "^7.16.0", + "@babel/helper-compilation-targets": "^7.16.0", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.16.0" } }, - "babel-loader": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-7.1.5.tgz", - "integrity": "sha512-iCHfbieL5d1LfOQeeVJEUyD9rTwBcP/fcEbRCfempxTDuqrKpu0AZjLAQHEQa3Yqyj9ORKe2iHfoj4rHLf7xpw==", + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.0.tgz", + "integrity": "sha512-kicDo0A/5J0nrsCPbn89mTG3Bm4XgYi0CZtvex9Oyw7gGZE3HXGD0zpQNH+mo+tEfbo8wbmMvJftOwpmPy7aVw==", "dev": true, "requires": { - "find-cache-dir": "^1.0.0", - "loader-utils": "^1.0.2", - "mkdirp": "^0.5.1" + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" } }, - "babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "@babel/plugin-proposal-optional-chaining": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.0.tgz", + "integrity": "sha512-Y4rFpkZODfHrVo70Uaj6cC1JJOt3Pp0MdWSwIKtb8z1/lsjl9AmnB7ErRFV+QNGIfcY1Eruc2UMx5KaRnXjMyg==", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" } }, - "babel-plugin-check-es2015-constants": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", - "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", + "@babel/plugin-proposal-private-methods": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.0.tgz", + "integrity": "sha512-IvHmcTHDFztQGnn6aWq4t12QaBXTKr1whF/dgp9kz84X6GUcwq9utj7z2wFCUfeOup/QKnOlt2k0zxkGFx9ubg==", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "@babel/helper-create-class-features-plugin": "^7.16.0", + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-plugin-syntax-async-functions": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", - "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", - "dev": true - }, - "babel-plugin-syntax-async-generators": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz", - "integrity": "sha1-a8lj67FuzLrmuStZbrfzXDQqi5o=", - "dev": true - }, - "babel-plugin-syntax-class-constructor-call": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz", - "integrity": "sha1-nLnTn+Q8hgC+yBRkVt3L1OGnZBY=", - "dev": true - }, - "babel-plugin-syntax-class-properties": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", - "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=", - "dev": true - }, - "babel-plugin-syntax-decorators": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz", - "integrity": "sha1-MSVjtNvePMgGzuPkFszurd0RrAs=", - "dev": true - }, - "babel-plugin-syntax-do-expressions": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz", - "integrity": "sha1-V0d1YTmqJtOQ0JQQsDdEugfkeW0=", - "dev": true - }, - "babel-plugin-syntax-dynamic-import": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz", - "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=", - "dev": true - }, - "babel-plugin-syntax-exponentiation-operator": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz", - "integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=", - "dev": true - }, - "babel-plugin-syntax-export-extensions": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz", - "integrity": "sha1-cKFITw+QiaToStRLrDU8lbmxJyE=", - "dev": true - }, - "babel-plugin-syntax-function-bind": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz", - "integrity": "sha1-SMSV8Xe98xqYHnMvVa3AvdJgH0Y=", - "dev": true - }, - "babel-plugin-syntax-object-rest-spread": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", - "integrity": "sha1-/WU28rzhODb/o6VFjEkDpZe7O/U=", - "dev": true - }, - "babel-plugin-syntax-trailing-function-commas": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz", - "integrity": "sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=", - "dev": true - }, - "babel-plugin-transform-async-generator-functions": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz", - "integrity": "sha1-8FiQAUX9PpkHpt3yjaWfIVJYpds=", + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.0.tgz", + "integrity": "sha512-3jQUr/HBbMVZmi72LpjQwlZ55i1queL8KcDTQEkAHihttJnAPrcvG9ZNXIfsd2ugpizZo595egYV6xy+pv4Ofw==", "dev": true, "requires": { - "babel-helper-remap-async-to-generator": "^6.24.1", - "babel-plugin-syntax-async-generators": "^6.5.0", - "babel-runtime": "^6.22.0" + "@babel/helper-annotate-as-pure": "^7.16.0", + "@babel/helper-create-class-features-plugin": "^7.16.0", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" } }, - "babel-plugin-transform-async-to-generator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz", - "integrity": "sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=", + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.0.tgz", + "integrity": "sha512-ti7IdM54NXv29cA4+bNNKEMS4jLMCbJgl+Drv+FgYy0erJLAxNAIXcNjNjrRZEcWq0xJHsNVwQezskMFpF8N9g==", "dev": true, "requires": { - "babel-helper-remap-async-to-generator": "^6.24.1", - "babel-plugin-syntax-async-functions": "^6.8.0", - "babel-runtime": "^6.22.0" + "@babel/helper-create-regexp-features-plugin": "^7.16.0", + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-plugin-transform-class-constructor-call": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz", - "integrity": "sha1-gNwoVQWsBn3LjWxl4vbxGrd2Xvk=", + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, "requires": { - "babel-plugin-syntax-class-constructor-call": "^6.18.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" + "@babel/helper-plugin-utils": "^7.8.0" } }, - "babel-plugin-transform-class-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", - "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-plugin-syntax-class-properties": "^6.8.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" + "@babel/helper-plugin-utils": "^7.12.13" } }, - "babel-plugin-transform-decorators": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz", - "integrity": "sha1-eIAT2PjGtSIr33s0Q5Df13Vp4k0=", + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dev": true, "requires": { - "babel-helper-explode-class": "^6.24.1", - "babel-plugin-syntax-decorators": "^6.13.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-types": "^6.24.1" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-plugin-transform-do-expressions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.22.0.tgz", - "integrity": "sha1-KMyvkoEtlJws0SgfaQyP3EaK6bs=", + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", "dev": true, "requires": { - "babel-plugin-syntax-do-expressions": "^6.8.0", - "babel-runtime": "^6.22.0" + "@babel/helper-plugin-utils": "^7.8.0" } }, - "babel-plugin-transform-es2015-arrow-functions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", - "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "@babel/helper-plugin-utils": "^7.8.3" } }, - "babel-plugin-transform-es2015-block-scoped-functions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", - "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "@babel/helper-plugin-utils": "^7.8.0" } }, - "babel-plugin-transform-es2015-block-scoping": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.24.1.tgz", - "integrity": "sha1-dsKV3DpHQbFmWt/TFnIV3P8ypXY=", + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1", - "lodash": "^4.2.0" + "@babel/helper-plugin-utils": "^7.10.4" } }, - "babel-plugin-transform-es2015-classes": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", - "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, "requires": { - "babel-helper-define-map": "^6.24.1", - "babel-helper-function-name": "^6.24.1", - "babel-helper-optimise-call-expression": "^6.24.1", - "babel-helper-replace-supers": "^6.24.1", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "@babel/helper-plugin-utils": "^7.8.0" } }, - "babel-plugin-transform-es2015-computed-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", - "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" + "@babel/helper-plugin-utils": "^7.10.4" } }, - "babel-plugin-transform-es2015-destructuring": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", - "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "@babel/helper-plugin-utils": "^7.8.0" } }, - "babel-plugin-transform-es2015-duplicate-keys": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", - "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "@babel/helper-plugin-utils": "^7.8.0" } }, - "babel-plugin-transform-es2015-for-of": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", - "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "@babel/helper-plugin-utils": "^7.8.0" } }, - "babel-plugin-transform-es2015-function-name": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", - "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "dev": true, "requires": { - "babel-helper-function-name": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-plugin-transform-es2015-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", - "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-plugin-transform-es2015-modules-amd": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", - "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", + "@babel/plugin-transform-arrow-functions": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.0.tgz", + "integrity": "sha512-vIFb5250Rbh7roWARvCLvIJ/PtAU5Lhv7BtZ1u24COwpI9Ypjsh+bZcKk6rlIyalK+r0jOc1XQ8I4ovNxNrWrA==", "dev": true, "requires": { - "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-plugin-transform-es2015-modules-commonjs": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.1.tgz", - "integrity": "sha1-0+MQtA72ZKNmIiAAl8bUQCmPK/4=", + "@babel/plugin-transform-async-to-generator": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.0.tgz", + "integrity": "sha512-PbIr7G9kR8tdH6g8Wouir5uVjklETk91GMVSUq+VaOgiinbCkBP6Q7NN/suM/QutZkMJMvcyAriogcYAdhg8Gw==", "dev": true, "requires": { - "babel-plugin-transform-strict-mode": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-types": "^6.24.1" + "@babel/helper-module-imports": "^7.16.0", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-remap-async-to-generator": "^7.16.0" } }, - "babel-plugin-transform-es2015-modules-systemjs": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", - "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.0.tgz", + "integrity": "sha512-V14As3haUOP4ZWrLJ3VVx5rCnrYhMSHN/jX7z6FAt5hjRkLsb0snPCmJwSOML5oxkKO4FNoNv7V5hw/y2bjuvg==", "dev": true, "requires": { - "babel-helper-hoist-variables": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-plugin-transform-es2015-modules-umd": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", - "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", + "@babel/plugin-transform-block-scoping": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.0.tgz", + "integrity": "sha512-27n3l67/R3UrXfizlvHGuTwsRIFyce3D/6a37GRxn28iyTPvNXaW4XvznexRh1zUNLPjbLL22Id0XQElV94ruw==", "dev": true, "requires": { - "babel-plugin-transform-es2015-modules-amd": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-plugin-transform-es2015-object-super": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", - "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", + "@babel/plugin-transform-classes": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.0.tgz", + "integrity": "sha512-HUxMvy6GtAdd+GKBNYDWCIA776byUQH8zjnfjxwT1P1ARv/wFu8eBDpmXQcLS/IwRtrxIReGiplOwMeyO7nsDQ==", "dev": true, "requires": { - "babel-helper-replace-supers": "^6.24.1", - "babel-runtime": "^6.22.0" + "@babel/helper-annotate-as-pure": "^7.16.0", + "@babel/helper-function-name": "^7.16.0", + "@babel/helper-optimise-call-expression": "^7.16.0", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-replace-supers": "^7.16.0", + "@babel/helper-split-export-declaration": "^7.16.0", + "globals": "^11.1.0" + }, + "dependencies": { + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + } } }, - "babel-plugin-transform-es2015-parameters": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", - "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", + "@babel/plugin-transform-computed-properties": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.0.tgz", + "integrity": "sha512-63l1dRXday6S8V3WFY5mXJwcRAnPYxvFfTlt67bwV1rTyVTM5zrp0DBBb13Kl7+ehkCVwIZPumPpFP/4u70+Tw==", "dev": true, "requires": { - "babel-helper-call-delegate": "^6.24.1", - "babel-helper-get-function-arity": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-template": "^6.24.1", - "babel-traverse": "^6.24.1", - "babel-types": "^6.24.1" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-plugin-transform-es2015-shorthand-properties": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", - "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", + "@babel/plugin-transform-destructuring": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.16.0.tgz", + "integrity": "sha512-Q7tBUwjxLTsHEoqktemHBMtb3NYwyJPTJdM+wDwb0g8PZ3kQUIzNvwD5lPaqW/p54TXBc/MXZu9Jr7tbUEUM8Q==", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-plugin-transform-es2015-spread": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", - "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", + "@babel/plugin-transform-dotall-regex": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.0.tgz", + "integrity": "sha512-FXlDZfQeLILfJlC6I1qyEwcHK5UpRCFkaoVyA1nk9A1L1Yu583YO4un2KsLBsu3IJb4CUbctZks8tD9xPQubLw==", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "@babel/helper-create-regexp-features-plugin": "^7.16.0", + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-plugin-transform-es2015-sticky-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", - "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", + "@babel/plugin-transform-duplicate-keys": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.0.tgz", + "integrity": "sha512-LIe2kcHKAZOJDNxujvmp6z3mfN6V9lJxubU4fJIGoQCkKe3Ec2OcbdlYP+vW++4MpxwG0d1wSDOJtQW5kLnkZQ==", "dev": true, "requires": { - "babel-helper-regex": "^6.24.1", - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-plugin-transform-es2015-template-literals": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", - "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.0.tgz", + "integrity": "sha512-OwYEvzFI38hXklsrbNivzpO3fh87skzx8Pnqi4LoSYeav0xHlueSoCJrSgTPfnbyzopo5b3YVAJkFIcUpK2wsw==", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.16.0", + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-plugin-transform-es2015-typeof-symbol": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", - "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", + "@babel/plugin-transform-for-of": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.0.tgz", + "integrity": "sha512-5QKUw2kO+GVmKr2wMYSATCTTnHyscl6sxFRAY+rvN7h7WB0lcG0o4NoV6ZQU32OZGVsYUsfLGgPQpDFdkfjlJQ==", "dev": true, "requires": { - "babel-runtime": "^6.22.0" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-plugin-transform-es2015-unicode-regex": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", - "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", + "@babel/plugin-transform-function-name": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.0.tgz", + "integrity": "sha512-lBzMle9jcOXtSOXUpc7tvvTpENu/NuekNJVova5lCCWCV9/U1ho2HH2y0p6mBg8fPm/syEAbfaaemYGOHCY3mg==", "dev": true, "requires": { - "babel-helper-regex": "^6.24.1", - "babel-runtime": "^6.22.0", - "regexpu-core": "^2.0.0" + "@babel/helper-function-name": "^7.16.0", + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-plugin-transform-exponentiation-operator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz", - "integrity": "sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=", + "@babel/plugin-transform-literals": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.0.tgz", + "integrity": "sha512-gQDlsSF1iv9RU04clgXqRjrPyyoJMTclFt3K1cjLmTKikc0s/6vE3hlDeEVC71wLTRu72Fq7650kABrdTc2wMQ==", "dev": true, "requires": { - "babel-helper-builder-binary-assignment-operator-visitor": "^6.24.1", - "babel-plugin-syntax-exponentiation-operator": "^6.8.0", - "babel-runtime": "^6.22.0" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-plugin-transform-export-extensions": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz", - "integrity": "sha1-U3OLR+deghhYnuqUbLvTkQm75lM=", + "@babel/plugin-transform-member-expression-literals": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.0.tgz", + "integrity": "sha512-WRpw5HL4Jhnxw8QARzRvwojp9MIE7Tdk3ez6vRyUk1MwgjJN0aNpRoXainLR5SgxmoXx/vsXGZ6OthP6t/RbUg==", "dev": true, "requires": { - "babel-plugin-syntax-export-extensions": "^6.8.0", - "babel-runtime": "^6.22.0" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-plugin-transform-function-bind": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz", - "integrity": "sha1-xvuOlqwpajELjPjqQBRiQH3fapc=", + "@babel/plugin-transform-modules-amd": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.0.tgz", + "integrity": "sha512-rWFhWbCJ9Wdmzln1NmSCqn7P0RAD+ogXG/bd9Kg5c7PKWkJtkiXmYsMBeXjDlzHpVTJ4I/hnjs45zX4dEv81xw==", "dev": true, "requires": { - "babel-plugin-syntax-function-bind": "^6.8.0", - "babel-runtime": "^6.22.0" + "@babel/helper-module-transforms": "^7.16.0", + "@babel/helper-plugin-utils": "^7.14.5", + "babel-plugin-dynamic-import-node": "^2.3.3" } }, - "babel-plugin-transform-object-rest-spread": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz", - "integrity": "sha1-h11ryb52HFiirj/u5dxIldjH+SE=", + "@babel/plugin-transform-modules-commonjs": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.0.tgz", + "integrity": "sha512-Dzi+NWqyEotgzk/sb7kgQPJQf7AJkQBWsVp1N6JWc1lBVo0vkElUnGdr1PzUBmfsCCN5OOFya3RtpeHk15oLKQ==", "dev": true, "requires": { - "babel-plugin-syntax-object-rest-spread": "^6.8.0", - "babel-runtime": "^6.22.0" + "@babel/helper-module-transforms": "^7.16.0", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-simple-access": "^7.16.0", + "babel-plugin-dynamic-import-node": "^2.3.3" } }, - "babel-plugin-transform-regenerator": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.24.1.tgz", - "integrity": "sha1-uNowWtQ8PJm0hI5P5AN7dw0jxBg=", + "@babel/plugin-transform-modules-systemjs": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.16.0.tgz", + "integrity": "sha512-yuGBaHS3lF1m/5R+6fjIke64ii5luRUg97N2wr+z1sF0V+sNSXPxXDdEEL/iYLszsN5VKxVB1IPfEqhzVpiqvg==", "dev": true, "requires": { - "regenerator-transform": "0.9.11" + "@babel/helper-hoist-variables": "^7.16.0", + "@babel/helper-module-transforms": "^7.16.0", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-validator-identifier": "^7.15.7", + "babel-plugin-dynamic-import-node": "^2.3.3" } }, - "babel-plugin-transform-strict-mode": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", - "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "@babel/plugin-transform-modules-umd": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.0.tgz", + "integrity": "sha512-nx4f6no57himWiHhxDM5pjwhae5vLpTK2zCnDH8+wNLJy0TVER/LJRHl2bkt6w9Aad2sPD5iNNoUpY3X9sTGDg==", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "babel-types": "^6.24.1" + "@babel/helper-module-transforms": "^7.16.0", + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-preset-es2015": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", - "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=", + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.0.tgz", + "integrity": "sha512-LogN88uO+7EhxWc8WZuQ8vxdSyVGxhkh8WTC3tzlT8LccMuQdA81e9SGV6zY7kY2LjDhhDOFdQVxdGwPyBCnvg==", "dev": true, "requires": { - "babel-plugin-check-es2015-constants": "^6.22.0", - "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", - "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", - "babel-plugin-transform-es2015-block-scoping": "^6.24.1", - "babel-plugin-transform-es2015-classes": "^6.24.1", - "babel-plugin-transform-es2015-computed-properties": "^6.24.1", - "babel-plugin-transform-es2015-destructuring": "^6.22.0", - "babel-plugin-transform-es2015-duplicate-keys": "^6.24.1", - "babel-plugin-transform-es2015-for-of": "^6.22.0", - "babel-plugin-transform-es2015-function-name": "^6.24.1", - "babel-plugin-transform-es2015-literals": "^6.22.0", - "babel-plugin-transform-es2015-modules-amd": "^6.24.1", - "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", - "babel-plugin-transform-es2015-modules-systemjs": "^6.24.1", - "babel-plugin-transform-es2015-modules-umd": "^6.24.1", - "babel-plugin-transform-es2015-object-super": "^6.24.1", - "babel-plugin-transform-es2015-parameters": "^6.24.1", - "babel-plugin-transform-es2015-shorthand-properties": "^6.24.1", - "babel-plugin-transform-es2015-spread": "^6.22.0", - "babel-plugin-transform-es2015-sticky-regex": "^6.24.1", - "babel-plugin-transform-es2015-template-literals": "^6.22.0", - "babel-plugin-transform-es2015-typeof-symbol": "^6.22.0", - "babel-plugin-transform-es2015-unicode-regex": "^6.24.1", - "babel-plugin-transform-regenerator": "^6.24.1" + "@babel/helper-create-regexp-features-plugin": "^7.16.0" } }, - "babel-preset-stage-0": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-0/-/babel-preset-stage-0-6.24.1.tgz", - "integrity": "sha1-VkLRUEL5E4TX5a+LyIsduVsDnmo=", + "@babel/plugin-transform-new-target": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.0.tgz", + "integrity": "sha512-fhjrDEYv2DBsGN/P6rlqakwRwIp7rBGLPbrKxwh7oVt5NNkIhZVOY2GRV+ULLsQri1bDqwDWnU3vhlmx5B2aCw==", "dev": true, "requires": { - "babel-plugin-transform-do-expressions": "^6.22.0", - "babel-plugin-transform-function-bind": "^6.22.0", - "babel-preset-stage-1": "^6.24.1" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-preset-stage-1": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz", - "integrity": "sha1-dpLNfc1oSZB+auSgqFWJz7niv7A=", + "@babel/plugin-transform-object-super": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.0.tgz", + "integrity": "sha512-fds+puedQHn4cPLshoHcR1DTMN0q1V9ou0mUjm8whx9pGcNvDrVVrgw+KJzzCaiTdaYhldtrUps8DWVMgrSEyg==", "dev": true, "requires": { - "babel-plugin-transform-class-constructor-call": "^6.24.1", - "babel-plugin-transform-export-extensions": "^6.22.0", - "babel-preset-stage-2": "^6.24.1" + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-replace-supers": "^7.16.0" } }, - "babel-preset-stage-2": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz", - "integrity": "sha1-2eKWD7PXEYfw5k7sYrwHdnIZvcE=", + "@babel/plugin-transform-parameters": { + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.3.tgz", + "integrity": "sha512-3MaDpJrOXT1MZ/WCmkOFo7EtmVVC8H4EUZVrHvFOsmwkk4lOjQj8rzv8JKUZV4YoQKeoIgk07GO+acPU9IMu/w==", "dev": true, "requires": { - "babel-plugin-syntax-dynamic-import": "^6.18.0", - "babel-plugin-transform-class-properties": "^6.24.1", - "babel-plugin-transform-decorators": "^6.24.1", - "babel-preset-stage-3": "^6.24.1" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-preset-stage-3": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz", - "integrity": "sha1-g2raCp56f6N8sTj7kyb4eTSkg5U=", + "@babel/plugin-transform-property-literals": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.0.tgz", + "integrity": "sha512-XLldD4V8+pOqX2hwfWhgwXzGdnDOThxaNTgqagOcpBgIxbUvpgU2FMvo5E1RyHbk756WYgdbS0T8y0Cj9FKkWQ==", "dev": true, "requires": { - "babel-plugin-syntax-trailing-function-commas": "^6.22.0", - "babel-plugin-transform-async-generator-functions": "^6.24.1", - "babel-plugin-transform-async-to-generator": "^6.24.1", - "babel-plugin-transform-exponentiation-operator": "^6.24.1", - "babel-plugin-transform-object-rest-spread": "^6.22.0" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-register": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", - "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "@babel/plugin-transform-regenerator": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.0.tgz", + "integrity": "sha512-JAvGxgKuwS2PihiSFaDrp94XOzzTUeDeOQlcKzVAyaPap7BnZXK/lvMDiubkPTdotPKOIZq9xWXWnggUMYiExg==", "dev": true, "requires": { - "babel-core": "^6.26.0", - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "home-or-tmp": "^2.0.0", - "lodash": "^4.17.4", - "mkdirp": "^0.5.1", - "source-map-support": "^0.4.15" + "regenerator-transform": "^0.14.2" }, "dependencies": { - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "regenerator-transform": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", + "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", "dev": true, "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" + "@babel/runtime": "^7.8.4" } - }, - "core-js": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.6.tgz", - "integrity": "sha512-lQUVfQi0aLix2xpyjrrJEvfuYCqPc/HwmTKsC/VNf8q0zsjX7SQZtp4+oRONN5Tsur9GDETPjj+Ub2iDiGZfSQ==", - "dev": true - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true } } }, - "babel-runtime": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.23.0.tgz", - "integrity": "sha1-CpSJ8UTecO+zzkMArM2zKeL8VDs=", + "@babel/plugin-transform-reserved-words": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.0.tgz", + "integrity": "sha512-Dgs8NNCehHSvXdhEhln8u/TtJxfVwGYCgP2OOr5Z3Ar+B+zXicEOKNTyc+eca2cuEOMtjW6m9P9ijOt8QdqWkg==", "dev": true, "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.10.0" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-template": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.25.0.tgz", - "integrity": "sha1-ZlJBFmt8KqTGGdceGSlpVSsQwHE=", + "@babel/plugin-transform-shorthand-properties": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.0.tgz", + "integrity": "sha512-iVb1mTcD8fuhSv3k99+5tlXu5N0v8/DPm2mO3WACLG6al1CGZH7v09HJyUb1TtYl/Z+KrM6pHSIJdZxP5A+xow==", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "babel-traverse": "^6.25.0", - "babel-types": "^6.25.0", - "babylon": "^6.17.2", - "lodash": "^4.2.0" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babel-traverse": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.25.0.tgz", - "integrity": "sha1-IldJfi/NGbie3BPEyROB+VEklvE=", + "@babel/plugin-transform-spread": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.0.tgz", + "integrity": "sha512-Ao4MSYRaLAQczZVp9/7E7QHsCuK92yHRrmVNRe/SlEJjhzivq0BSn8mEraimL8wizHZ3fuaHxKH0iwzI13GyGg==", "dev": true, "requires": { - "babel-code-frame": "^6.22.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.22.0", - "babel-types": "^6.25.0", - "babylon": "^6.17.2", - "debug": "^2.2.0", - "globals": "^9.0.0", - "invariant": "^2.2.0", - "lodash": "^4.2.0" + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0" } }, - "babel-types": { - "version": "6.25.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.25.0.tgz", - "integrity": "sha1-cK+ySNVmDl0Y+BHZHIMDtUE0oY4=", + "@babel/plugin-transform-sticky-regex": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.0.tgz", + "integrity": "sha512-/ntT2NljR9foobKk4E/YyOSwcGUXtYWv5tinMK/3RkypyNBNdhHUaq6Orw5DWq9ZcNlS03BIlEALFeQgeVAo4Q==", "dev": true, "requires": { - "babel-runtime": "^6.22.0", - "esutils": "^2.0.2", - "lodash": "^4.2.0", - "to-fast-properties": "^1.0.1" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "babylon": { - "version": "6.17.4", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.17.4.tgz", - "integrity": "sha512-kChlV+0SXkjE0vUn9OZ7pBMWRFd8uq3mZe8x1K6jhuNcAFAtEnjchFAqB+dYEXKyd+JpT6eppRR78QAr5gTsUw==", - "dev": true + "@babel/plugin-transform-template-literals": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.0.tgz", + "integrity": "sha512-Rd4Ic89hA/f7xUSJQk5PnC+4so50vBoBfxjdQAdvngwidM8jYIBVxBZ/sARxD4e0yMXRbJVDrYf7dyRtIIKT6Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.0.tgz", + "integrity": "sha512-++V2L8Bdf4vcaHi2raILnptTBjGEFxn5315YU+e8+EqXIucA+q349qWngCLpUYqqv233suJ6NOienIVUpS9cqg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.0.tgz", + "integrity": "sha512-VFi4dhgJM7Bpk8lRc5CMaRGlKZ29W9C3geZjt9beuzSUrlJxsNwX7ReLwaL6WEvsOf2EQkyIJEPtF8EXjB/g2A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.0.tgz", + "integrity": "sha512-jHLK4LxhHjvCeZDWyA9c+P9XH1sOxRd1RO9xMtDVRAOND/PczPqizEtVdx4TQF/wyPaewqpT+tgQFYMnN/P94A==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.16.0", + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/preset-env": { + "version": "7.16.4", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.16.4.tgz", + "integrity": "sha512-v0QtNd81v/xKj4gNKeuAerQ/azeNn/G1B1qMLeXOcV8+4TWlD2j3NV1u8q29SDFBXx/NBq5kyEAO+0mpRgacjA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.16.4", + "@babel/helper-compilation-targets": "^7.16.3", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-validator-option": "^7.14.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.16.2", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.16.0", + "@babel/plugin-proposal-async-generator-functions": "^7.16.4", + "@babel/plugin-proposal-class-properties": "^7.16.0", + "@babel/plugin-proposal-class-static-block": "^7.16.0", + "@babel/plugin-proposal-dynamic-import": "^7.16.0", + "@babel/plugin-proposal-export-namespace-from": "^7.16.0", + "@babel/plugin-proposal-json-strings": "^7.16.0", + "@babel/plugin-proposal-logical-assignment-operators": "^7.16.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", + "@babel/plugin-proposal-numeric-separator": "^7.16.0", + "@babel/plugin-proposal-object-rest-spread": "^7.16.0", + "@babel/plugin-proposal-optional-catch-binding": "^7.16.0", + "@babel/plugin-proposal-optional-chaining": "^7.16.0", + "@babel/plugin-proposal-private-methods": "^7.16.0", + "@babel/plugin-proposal-private-property-in-object": "^7.16.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.16.0", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.16.0", + "@babel/plugin-transform-async-to-generator": "^7.16.0", + "@babel/plugin-transform-block-scoped-functions": "^7.16.0", + "@babel/plugin-transform-block-scoping": "^7.16.0", + "@babel/plugin-transform-classes": "^7.16.0", + "@babel/plugin-transform-computed-properties": "^7.16.0", + "@babel/plugin-transform-destructuring": "^7.16.0", + "@babel/plugin-transform-dotall-regex": "^7.16.0", + "@babel/plugin-transform-duplicate-keys": "^7.16.0", + "@babel/plugin-transform-exponentiation-operator": "^7.16.0", + "@babel/plugin-transform-for-of": "^7.16.0", + "@babel/plugin-transform-function-name": "^7.16.0", + "@babel/plugin-transform-literals": "^7.16.0", + "@babel/plugin-transform-member-expression-literals": "^7.16.0", + "@babel/plugin-transform-modules-amd": "^7.16.0", + "@babel/plugin-transform-modules-commonjs": "^7.16.0", + "@babel/plugin-transform-modules-systemjs": "^7.16.0", + "@babel/plugin-transform-modules-umd": "^7.16.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.16.0", + "@babel/plugin-transform-new-target": "^7.16.0", + "@babel/plugin-transform-object-super": "^7.16.0", + "@babel/plugin-transform-parameters": "^7.16.3", + "@babel/plugin-transform-property-literals": "^7.16.0", + "@babel/plugin-transform-regenerator": "^7.16.0", + "@babel/plugin-transform-reserved-words": "^7.16.0", + "@babel/plugin-transform-shorthand-properties": "^7.16.0", + "@babel/plugin-transform-spread": "^7.16.0", + "@babel/plugin-transform-sticky-regex": "^7.16.0", + "@babel/plugin-transform-template-literals": "^7.16.0", + "@babel/plugin-transform-typeof-symbol": "^7.16.0", + "@babel/plugin-transform-unicode-escapes": "^7.16.0", + "@babel/plugin-transform-unicode-regex": "^7.16.0", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.16.0", + "babel-plugin-polyfill-corejs2": "^0.3.0", + "babel-plugin-polyfill-corejs3": "^0.4.0", + "babel-plugin-polyfill-regenerator": "^0.3.0", + "core-js-compat": "^3.19.1", + "semver": "^6.3.0" + } + }, + "@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } }, - "balanced-match": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", - "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" + "@babel/register": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.16.0.tgz", + "integrity": "sha512-lzl4yfs0zVXnooeLE0AAfYaT7F3SPA8yB2Bj4W1BiZwLbMS3MZH35ZvCWSRHvneUugwuM+Wsnrj7h0F7UmU3NQ==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.0", + "source-map-support": "^0.5.16" }, "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", "dev": true, "requires": { - "is-descriptor": "^1.0.0" + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" } }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, "requires": { - "kind-of": "^6.0.0" + "locate-path": "^3.0.0" } }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dev": true, "requires": { - "kind-of": "^6.0.0" + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" } }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "dev": true, "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "pify": "^4.0.1", + "semver": "^5.6.0" } }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } } } }, - "base64-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", - "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==", - "dev": true - }, - "batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "big.js": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.1.3.tgz", - "integrity": "sha1-TK2iGTZS6zyp7I5VyQFWacmAaXg=" - }, - "binary-extensions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.9.0.tgz", - "integrity": "sha1-ZlBsFs5vTWkopbPNajPKQelB43s=", - "dev": true - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "@babel/runtime": { + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz", + "integrity": "sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ==", + "dev": true, "requires": { - "inherits": "~2.0.0" + "regenerator-runtime": "^0.13.4" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "dev": true + } } }, - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", - "dev": true - }, - "bluebird-lst": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.5.tgz", - "integrity": "sha512-Ey0bDNys5qpYPhZ/oQ9vOEvD0TYQDTILMXWP2iGfvMg7rSDde+oV4aQQgqRH+CvBFNz2BSDQnPGMUl6LKBUUQA==", + "@babel/template": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.0.tgz", + "integrity": "sha512-MnZdpFD/ZdYhXwiunMqqgyZyucaYsbL0IrjoGjaVhGilz+x8YB++kRfygSOIj1yOtWKPlx7NBp+9I1RQSgsd5A==", "dev": true, "requires": { - "bluebird": "^3.5.1" + "@babel/code-frame": "^7.16.0", + "@babel/parser": "^7.16.0", + "@babel/types": "^7.16.0" } }, - "bn.js": { - "version": "4.11.7", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.7.tgz", - "integrity": "sha512-LxFiV5mefv0ley0SzqkOPR1bC4EbpPx8LkOz5vMe/Yi15t5hzwgO/G+tc7wOtL4PZTYjwHu8JnEiSLumuSjSfA==", - "dev": true - }, - "body-parser": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", - "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "@babel/traverse": { + "version": "7.16.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.3.tgz", + "integrity": "sha512-eolumr1vVMjqevCpwVO99yN/LoGL0EyHiLO5I043aYQvwOJ9eR5UsZSClHVCzfhBduMAsSzgA/6AyqPjNayJag==", "dev": true, "requires": { - "bytes": "3.0.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.1", - "http-errors": "~1.6.2", - "iconv-lite": "0.4.19", - "on-finished": "~2.3.0", - "qs": "6.5.1", - "raw-body": "2.3.2", - "type-is": "~1.6.15" + "@babel/code-frame": "^7.16.0", + "@babel/generator": "^7.16.0", + "@babel/helper-function-name": "^7.16.0", + "@babel/helper-hoist-variables": "^7.16.0", + "@babel/helper-split-export-declaration": "^7.16.0", + "@babel/parser": "^7.16.3", + "@babel/types": "^7.16.0", + "debug": "^4.1.0", + "globals": "^11.1.0" }, "dependencies": { "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "2.1.2" } }, - "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } }, - "bonjour": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", - "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "@babel/types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.0.tgz", + "integrity": "sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg==", "dev": true, "requires": { - "array-flatten": "^2.1.0", - "deep-equal": "^1.0.1", - "dns-equal": "^1.0.0", - "dns-txt": "^2.0.2", - "multicast-dns": "^6.0.1", - "multicast-dns-service-types": "^1.1.0" + "@babel/helper-validator-identifier": "^7.15.7", + "to-fast-properties": "^2.0.0" + }, + "dependencies": { + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + } } }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "@cycle/dom": { + "version": "23.0.0", + "resolved": "https://registry.npmjs.org/@cycle/dom/-/dom-23.0.0.tgz", + "integrity": "sha512-cbwEJALcjvn8f7i7PjG9Z48yX8un44QXhu/0g3oe6vbAahO59maxL/QwnWACO8Y38FjIurhAOpqEeJzfaYKyjw==", "requires": { - "hoek": "2.x.x" + "@cycle/run": "^5.2.0", + "snabbdom": "^3.0.0", + "snabbdom-selector": "^5.0.0", + "xstream": "*" } }, - "bootstrap-sass": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/bootstrap-sass/-/bootstrap-sass-3.3.7.tgz", - "integrity": "sha1-ZZbHq0D2Y3OTMjqwvIDQZPxjBJg=", - "dev": true + "@cycle/history": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@cycle/history/-/history-8.0.0.tgz", + "integrity": "sha512-yUZRvZ/8K3bgckXQwczzNWUxCtmEJvTISNKch7yvFlnzPpaudB4N/Phi+WC37nFdI6GXmnjNzbiz0b/Kb/w0ag==", + "requires": { + "@cycle/run": "^5.2.0", + "@types/history": "^4.7.2", + "history": "4.7.x", + "xstream": "*" + } }, - "boxen": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", - "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", - "dev": true, + "@cycle/http": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@cycle/http/-/http-15.4.0.tgz", + "integrity": "sha512-cTx3d//qmqLCrwwuHNXOizF2+p0Spa9AGUWrkKf2/ykqgEclhGwAsjiOEYn2YHacBKxCBUNW7rJR2+C+a7Cipg==", + "requires": { + "@cycle/run": "^5.2.0", + "@types/superagent": "3.8.4", + "most": "^1.7.3", + "rxjs": "^6.3.3", + "superagent": "^3.8.3", + "xstream": "*" + } + }, + "@cycle/isolate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@cycle/isolate/-/isolate-5.2.0.tgz", + "integrity": "sha512-BLyyd/EceQnuGaUVSBotdZkBEUeJq1zMbdK7LPdc7WMiJbinkduHue+rOe3BGbK3Ba7LGt7kjRBEfzLke9FGkg==", + "requires": { + "@cycle/run": "^5.2.0", + "xstream": "^11.7.0" + } + }, + "@cycle/run": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@cycle/run/-/run-5.5.0.tgz", + "integrity": "sha512-RZ7Zfai1zfxa0ZP2I4Pu72UgZrRyG3PnivFCe/yYfQBcFHSlUy1biwUTnPB4+k/SE6/cOMTiFqwqu/Y3M+fh1g==", + "requires": { + "quicktask": "1.1.0", + "xstream": "10.x || 11.x" + } + }, + "@cycle/storage": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cycle/storage/-/storage-4.1.1.tgz", + "integrity": "sha512-Bw0mn2r4/DzoVjcP11Z35DMt92UUn6b2YteHKW4ph3koyiGWlspxNRVL/fOUwMd/VcrvSTcrtxPEt6uDxEpZXQ==", "requires": { - "ansi-align": "^2.0.0", - "camelcase": "^4.0.0", - "chalk": "^2.0.1", - "cli-boxes": "^1.0.0", - "string-width": "^2.0.0", - "term-size": "^1.2.0", - "widest-line": "^2.0.0" + "@cycle/run": "^3.1.0" }, "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, + "@cycle/run": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@cycle/run/-/run-3.4.0.tgz", + "integrity": "sha512-YUZyPu0nC4YDC31mLH5PGxbMoPEH5dNEV+nmgt34GgGgJ0ykDd4PrY7/ph5MAEpQE6rOfov0VN44qQRs6beQow==", "requires": { - "has-flag": "^3.0.0" + "xstream": "10.x || 11.x" } } } }, - "brace-expansion": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", - "integrity": "sha1-cZfX6qm4fmSDkOph/GbIRCdCDfk=", + "@discoveryjs/json-ext": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz", + "integrity": "sha512-6nFkfkmSeV/rqSaS4oWHgmpnYw194f6hmWF5is6b0J1naJZoiD0NTc9AiUwPHvWsowkjuHErCZT1wa0jg+BLIA==", + "dev": true + }, + "@most/multicast": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@most/multicast/-/multicast-1.3.0.tgz", + "integrity": "sha512-DWH8AShgp5bXn+auGzf5tzPxvpmEvQJd0CNsApOci1LDF4eAEcnw4HQOr2Jaa+L92NbDYFKBSXxll+i7r1ikvw==", "requires": { - "balanced-match": "^0.4.1", - "concat-map": "0.0.1" + "@most/prelude": "^1.4.0" } }, - "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "@most/prelude": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@most/prelude/-/prelude-1.8.0.tgz", + "integrity": "sha512-t1CcURpZzfmBA6fEWwqmCqeNzWAj1w2WqEmCk/2yXMe/p8Ut000wFmVKMy8A1Rl9VVxZEZ5nBHd/pU0dR4bv/w==" + }, + "@nicolo-ribaudo/chokidar-2": { + "version": "2.1.8-no-fsevents.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", + "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==", + "dev": true, + "optional": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "requires": { - "expand-range": "^1.8.1", - "preserve": "^0.2.0", - "repeat-element": "^1.1.2" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" } }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true }, - "browserify-aes": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.0.6.tgz", - "integrity": "sha1-Xncl297x/Vkw1OurSFZ85FHEigo=", + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "requires": { - "buffer-xor": "^1.0.2", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.0", - "inherits": "^2.0.1" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" } }, - "browserify-cipher": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz", - "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=", + "@types/concat-stream": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@types/concat-stream/-/concat-stream-1.6.1.tgz", + "integrity": "sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==", + "requires": { + "@types/node": "*" + } + }, + "@types/cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==" + }, + "@types/eslint": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.0.tgz", + "integrity": "sha512-74hbvsnc+7TEDa1z5YLSe4/q8hGYB3USNvCuzHUJrjPV6hXaq8IXcngCrHkuvFt0+8rFz7xYXrHgNayIX0UZvQ==", "dev": true, "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" + "@types/estree": "*", + "@types/json-schema": "*" } }, - "browserify-des": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz", - "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=", + "@types/eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g==", "dev": true, "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1" + "@types/eslint": "*", + "@types/estree": "*" } }, - "browserify-rsa": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "@types/estree": { + "version": "0.0.50", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", + "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==" + }, + "@types/form-data": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-0.0.33.tgz", + "integrity": "sha1-yayFsqX9GENbjIXZ7LUObWyJP/g=", + "requires": { + "@types/node": "*" + } + }, + "@types/history": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz", + "integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==" + }, + "@types/http-proxy": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.7.tgz", + "integrity": "sha512-9hdj6iXH64tHSLTY+Vt2eYOGzSogC+JQ2H7bdPWkuh7KXP5qLllWx++t+K9Wk556c3dkDdPws/SpMRi0sdCT1w==", "dev": true, "requires": { - "bn.js": "^4.1.0", - "randombytes": "^2.0.1" + "@types/node": "*" } }, - "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "@types/json-schema": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==" + }, + "@types/node": { + "version": "16.11.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.10.tgz", + "integrity": "sha512-3aRnHa1KlOEEhJ6+CvyHKK5vE9BcLGjtUpwvqYLRvYNQKMfabu3BwfJaA/SLW8dxe28LsNDjtHwePTuzn3gmOA==" + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "@types/retry": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", + "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", + "dev": true + }, + "@types/superagent": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.8.4.tgz", + "integrity": "sha512-Dnh0Iw6NO55z1beXvlsvUrfk4cd9eL2nuTmUk+rAhSVCk10PGGFbqCCTwbau9D0d2W3DITiXl4z8VCqppGkMPQ==", + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, + "@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", "dev": true, "requires": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" } }, - "browserify-zlib": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", - "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "dev": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", "dev": true, "requires": { - "pako": "~0.2.0" + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" } }, - "browserslist": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", - "integrity": "sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk=", + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", "dev": true, "requires": { - "caniuse-db": "^1.0.30000639", - "electron-to-chromium": "^1.2.7" + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" } }, - "buffer": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", "dev": true, "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" + "@xtuc/ieee754": "^1.2.0" } }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", "dev": true, "requires": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" + "@xtuc/long": "4.2.2" } }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", "dev": true }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "@webpack-cli/configtest": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.0.tgz", + "integrity": "sha512-ttOkEkoalEHa7RaFYpM0ErK1xc4twg3Am9hfHhL7MVqlHebnkYd2wuI/ZqTDj0cVzZho6PdinY0phFZV3O0Mzg==", "dev": true }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + "@webpack-cli/info": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.4.0.tgz", + "integrity": "sha512-F6b+Man0rwE4n0409FyAJHStYA5OIZERxmnUfLVwv0mc0V1wLad3V7jqRlMkgKBeAq07jUvglacNaa6g9lOpuw==", + "dev": true, + "requires": { + "envinfo": "^7.7.3" + } }, - "buffer-indexof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", - "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "@webpack-cli/serve": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.0.tgz", + "integrity": "sha512-ZkVeqEmRpBV2GHvjjUZqEai2PpUbuq8Bqd//vEYsp63J8WyexI8ppCqVS3Zs0QADf6aWuPdU+0XsPI647PVlQA==", "dev": true }, - "buffer-shims": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", - "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, - "builder-util": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-6.1.1.tgz", - "integrity": "sha512-n+ah8X8H+DU1YPQHCW9ayLb2g8+KENtRfPtIei0UiqP7p+pURKzL3/sMsxmu4S7mbGQBHV8R6PMu/axBjxy+Ow==", + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", "dev": true, "requires": { - "7zip-bin": "~4.0.2", - "app-builder-bin": "2.1.2", - "bluebird-lst": "^1.0.5", - "builder-util-runtime": "^4.4.1", - "chalk": "^2.4.1", - "debug": "^3.1.0", - "fs-extra-p": "^4.6.1", - "is-ci": "^1.1.0", - "js-yaml": "^3.12.0", - "lazy-val": "^1.0.3", - "semver": "^5.5.0", - "source-map-support": "^0.5.8", - "stat-mode": "^0.2.2", - "temp-file": "^3.1.3" + "mime-types": "~2.1.24", + "negotiator": "0.6.2" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } + "mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "dev": true }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "mime-types": { + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "dev": true, "requires": { - "ms": "2.0.0" + "mime-db": "1.51.0" } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + } + } + }, + "acorn": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", + "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", + "dev": true + }, + "acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "dependencies": { + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true + } + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "dependencies": { + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "js-yaml": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", - "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "semver": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", - "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-support": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.8.tgz", - "integrity": "sha512-WqAEWPdb78u25RfKzOF0swBpY0dKrNdjc4GvLwm7ScX/o9bj8Eh/YL8mcMhBHYDGl87UkkSXDOFnW4G7GhWhGg==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" } } }, - "builder-util-runtime": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.4.1.tgz", - "integrity": "sha512-8L2pbL6D3VdI1f8OMknlZJpw0c7KK15BRz3cY77AOUElc4XlCv2UhVV01jJM7+6Lx7henaQh80ALULp64eFYAQ==", + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true + }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "requires": { - "bluebird-lst": "^1.0.5", - "debug": "^3.1.0", - "fs-extra-p": "^4.6.1", - "sax": "^1.2.4" + "ajv": "^8.0.0" }, "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "ajv": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", "dev": true, "requires": { - "ms": "2.0.0" + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true } } }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" + "ajv-keywords": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", + "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", + "dev": true }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", "dev": true }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", "dev": true }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } + "ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true }, - "camelcase": { + "ansi-regex": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" + "color-convert": "^1.9.0" } }, - "caniuse-api": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-1.6.1.tgz", - "integrity": "sha1-tTTnxzTE+B7F++isoq0kNUuWLGw=", + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "dev": true, "requires": { - "browserslist": "^1.3.6", - "caniuse-db": "^1.0.30000529", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" } }, - "caniuse-db": { - "version": "1.0.30000706", - "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000706.tgz", - "integrity": "sha1-4rXwRgVzy8yIoJhfXM7QjxYXxvU=", + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true }, - "canvas": { - "version": "1.6.11", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-1.6.11.tgz", - "integrity": "sha512-ElVw5Uk8PReGpzXfDg6PDa+wntnZLGWWfdSHI0Pc8GyXiFbW13drSTzWU6C4E5QylHe+FnLqI7ngMRlp3eGZIQ==", - "optional": true, + "are-we-there-yet": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", + "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", + "dev": true, "requires": { - "nan": "^2.10.0" - }, - "dependencies": { - "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", - "optional": true - } + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" } }, - "canvas-prebuilt": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/canvas-prebuilt/-/canvas-prebuilt-1.6.0.tgz", - "integrity": "sha1-+N2avoH9whA6Odg2LfMhnW2D94g=", - "optional": true, - "requires": { - "node-pre-gyp": "^0.6.29" - } + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, - "capture-stack-trace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", - "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", "dev": true }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "dev": true, - "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" - } + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "safer-buffer": "~2.1.0" } }, - "chokidar": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", "dev": true, "requires": { - "anymatch": "^1.3.0", - "async-each": "^1.0.0", - "fsevents": "^1.0.0", - "glob-parent": "^2.0.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^2.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0" + "lodash": "^4.17.14" } }, - "chromium-pickle-js": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha1-BKEGZywYsIWrd02YPfo+oTjyIgU=", + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", "dev": true }, - "ci-info": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.3.1.tgz", - "integrity": "sha512-l4wK/SFEN8VVTQ9RO1I5yzIL2vw1w6My29qA6Gwaec80QeHxfXbruuUWqn1knyMoJn/X5kav3zVY1TlRHSKeIA==", - "dev": true + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, - "clap": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.0.tgz", - "integrity": "sha1-WckP4+E3EEdG/xlGmiemNP9oyFc=", - "dev": true, - "requires": { - "chalk": "^1.1.3" - } + "aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" + "babel-loader": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz", + "integrity": "sha512-n4Zeta8NC3QAsuyiizu0GkmRcQ6clkV9WFUnUf1iXP//IeSKbWjofW3UHyZVwlOB4y039YQKefawyTn64Zwbuw==", + "requires": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^1.4.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" }, "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "requires": { - "is-descriptor": "^0.1.0" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } } } }, - "cli-boxes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", - "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", - "dev": true - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dev": true, "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" + "object.assign": "^4.1.0" } }, - "clone": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz", - "integrity": "sha1-Jgt6meux7f4kdTgXX3gyQ8sZ0Uk=", - "dev": true - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "coa": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.4.tgz", - "integrity": "sha1-qe8VNmDWqGqL3sAomlxoTSF0Mv0=", + "babel-plugin-polyfill-corejs2": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.0.tgz", + "integrity": "sha512-wMDoBJ6uG4u4PNFh72Ty6t3EgfA91puCuAwKIazbQlci+ENb/UU9A3xG5lutjUIiXCIn1CY5L15r9LimiJyrSA==", "dev": true, "requires": { - "q": "^1.1.2" + "@babel/compat-data": "^7.13.11", + "@babel/helper-define-polyfill-provider": "^0.3.0", + "semver": "^6.1.1" } }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "babel-plugin-polyfill-corejs3": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.4.0.tgz", + "integrity": "sha512-YxFreYwUfglYKdLUGvIF2nJEsGwj+RhWSX/ije3D2vQPOXuyMLMtg/cCGMDpOA7Nd+MwlNdnGODbd2EwUZPlsw==", "dev": true, "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" + "@babel/helper-define-polyfill-provider": "^0.3.0", + "core-js-compat": "^3.18.0" } }, - "color": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz", - "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=", + "babel-plugin-polyfill-regenerator": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.0.tgz", + "integrity": "sha512-dhAPTDLGoMW5/84wkgwiLRwMnio2i1fUe53EuvtKMv0pn2p3S8OCoV1xAzfJPl0KOX7IB89s2ib85vbYiea3jg==", "dev": true, "requires": { - "clone": "^1.0.2", - "color-convert": "^1.3.0", - "color-string": "^0.3.0" + "@babel/helper-define-polyfill-provider": "^0.3.0" } }, - "color-convert": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", - "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", - "dev": true, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", "requires": { - "color-name": "^1.1.1" + "tweetnacl": "^0.14.3" } }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, - "color-string": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz", - "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=", + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", "dev": true, "requires": { - "color-name": "^1.0.0" + "inherits": "~2.0.0" } }, - "colormin": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colormin/-/colormin-1.1.2.tgz", - "integrity": "sha1-6i90IKcrlogaOKrlnsEkpvcpgTM=", + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", "dev": true, "requires": { - "color": "^0.11.0", - "css-color-names": "0.0.4", - "has": "^1.0.1" - } - }, - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", - "dev": true + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + } + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserslist": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.18.1.tgz", + "integrity": "sha512-8ScCzdpPwR2wQh8IT82CA2VgDwjHyqMovPBZSNH54+tm4Jk2pCuv90gmAdH6J84OCRWi0b4gMe6O6XPXuJnjgQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001280", + "electron-to-chromium": "^1.3.896", + "escalade": "^3.1.1", + "node-releases": "^2.0.1", + "picocolors": "^1.0.0" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "buffer-shims": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "dependencies": { + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + } + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + } + } + }, + "caniuse-lite": { + "version": "1.0.30001283", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001283.tgz", + "integrity": "sha512-9RoKo841j1GQFSJz/nCXOj0sD7tHBtlowjYlrqIUS812x9/emfBLBt6IyMz1zIaYc/eRL8Cs6HPUVi2Hzq4sIg==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.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" + } + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "colorette": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", + "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", + "dev": true }, "combined-stream": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "requires": { "delayed-stream": "~1.0.0" } @@ -2512,48 +2480,41 @@ "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "compare-version": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha1-AWLsLZNR9d3VmpICy6k1NmpyUIA=", - "dev": true + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" }, "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, "compressible": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.14.tgz", - "integrity": "sha1-MmxfUH+7BV9UEWeCuWmoG2einac=", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "dev": true, "requires": { - "mime-db": ">= 1.34.0 < 2" + "mime-db": ">= 1.43.0 < 2" }, "dependencies": { "mime-db": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", - "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==", + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", "dev": true } } }, "compression": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.3.tgz", - "integrity": "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", "dev": true, "requires": { "accepts": "~1.3.5", "bytes": "3.0.0", - "compressible": "~2.0.14", + "compressible": "~2.0.16", "debug": "2.6.9", - "on-headers": "~1.0.1", + "on-headers": "~1.0.2", "safe-buffer": "5.1.2", "vary": "~1.1.2" }, @@ -2584,7 +2545,8 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "concat-stream": { "version": "1.6.2", @@ -2597,51 +2559,34 @@ "typedarray": "^0.0.6" } }, - "configstore": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", - "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", - "dev": true, - "requires": { - "dot-prop": "^4.1.0", - "graceful-fs": "^4.1.2", - "make-dir": "^1.0.0", - "unique-string": "^1.0.0", - "write-file-atomic": "^2.0.0", - "xdg-basedir": "^3.0.0" - } - }, "connect-history-api-fallback": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", - "integrity": "sha1-sGhzk0vF40T+9hGhlqb6rgruAVo=", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", "dev": true }, - "console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "dev": true, - "requires": { - "date-now": "^0.1.4" - } - }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true }, "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", - "dev": true + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } }, "content-type": { "version": "1.0.4", @@ -2656,9 +2601,9 @@ "dev": true }, "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", "dev": true }, "cookie-signature": { @@ -2668,276 +2613,86 @@ "dev": true }, "cookiejar": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.1.tgz", - "integrity": "sha1-Qa1XsbVVlR7BcUEqgZQrHoIA00o=" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz", + "integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==" }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-js": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", - "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=", - "dev": true + "core-js-compat": { + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.19.1.tgz", + "integrity": "sha512-Q/VJ7jAF/y68+aUsQJ/afPOewdsGkDtcMb40J8MbuWKlK3Y+wtHq8bTHKPj2WKWLIqmS5JhHs4CzHtz6pT2W6g==", + "dev": true, + "requires": { + "browserslist": "^4.17.6", + "semver": "7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, - "cosmiconfig": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-2.1.1.tgz", - "integrity": "sha1-gX8sIDk0eh6b99CQwJI+U/dJyoI=", - "requires": { - "js-yaml": "^3.4.3", - "minimist": "^1.2.0", - "object-assign": "^4.1.0", - "os-homedir": "^1.0.1", - "parse-json": "^2.2.0", - "require-from-string": "^1.1.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "create-ecdh": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", - "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.0.0" - } - }, - "create-error-class": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", - "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", - "dev": true, - "requires": { - "capture-stack-trace": "^1.0.0" - } - }, - "create-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", - "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz", - "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=", - "dev": true, - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "requires": { - "boom": "2.x.x" - } - }, - "crypto-browserify": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.11.1.tgz", - "integrity": "sha512-Na7ZlwCOqoaW5RwUK1WpXws2kv8mNhWdTlzob0UXulk6G9BDbyiJaGTYBIX61Ozn9l1EPPJpICZb4DaOpT9NlQ==", - "dev": true, - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "crypto-random-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", - "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", - "dev": true - }, - "css": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css/-/css-2.2.1.tgz", - "integrity": "sha1-c6TIHehdtmTU7mdPfUcIXjstVdw=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "source-map": "^0.1.38", - "source-map-resolve": "^0.3.0", - "urix": "^0.1.0" + "css-loader": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.5.1.tgz", + "integrity": "sha512-gEy2w9AnJNnD9Kuo4XAP9VflW/ujKoS9c/syO+uWMlm5igc7LysKzPXaDoR2vroROkSwsTS2tGr1yGGEbZOYZQ==", + "dev": true, + "requires": { + "icss-utils": "^5.1.0", + "postcss": "^8.2.15", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.1.0", + "semver": "^7.3.5" }, "dependencies": { - "source-map": { - "version": "0.1.43", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", - "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "requires": { - "amdefine": ">=0.0.4" + "yallist": "^4.0.0" } - } - } - }, - "css-color-names": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", - "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", - "dev": true - }, - "css-loader": { - "version": "0.26.4", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.26.4.tgz", - "integrity": "sha1-th6eMNuUMD5v/IkvEOzQmtAlof0=", - "dev": true, - "requires": { - "babel-code-frame": "^6.11.0", - "css-selector-tokenizer": "^0.7.0", - "cssnano": ">=2.6.1 <4", - "loader-utils": "^1.0.2", - "lodash.camelcase": "^4.3.0", - "object-assign": "^4.0.1", - "postcss": "^5.0.6", - "postcss-modules-extract-imports": "^1.0.0", - "postcss-modules-local-by-default": "^1.0.1", - "postcss-modules-scope": "^1.0.0", - "postcss-modules-values": "^1.1.0", - "source-list-map": "^0.1.7" - }, - "dependencies": { - "source-list-map": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-0.1.8.tgz", - "integrity": "sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY=", - "dev": true - } - } - }, - "css-selector-tokenizer": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz", - "integrity": "sha1-5piEdK6MlTR3v15+/s/OzNnPTIY=", - "dev": true, - "requires": { - "cssesc": "^0.1.0", - "fastparse": "^1.1.1", - "regexpu-core": "^1.0.0" - }, - "dependencies": { - "regexpu-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", - "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { - "regenerate": "^1.2.1", - "regjsgen": "^0.2.0", - "regjsparser": "^0.1.4" + "lru-cache": "^6.0.0" } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } }, "cssesc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz", - "integrity": "sha1-yBSQPkViM3GgR3tAEJqq++6t27Q=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true }, - "cssnano": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-3.10.0.tgz", - "integrity": "sha1-Tzj2zqK5sX+gFJDyPx3GjqZcHDg=", - "dev": true, - "requires": { - "autoprefixer": "^6.3.1", - "decamelize": "^1.1.2", - "defined": "^1.0.0", - "has": "^1.0.1", - "object-assign": "^4.0.1", - "postcss": "^5.0.14", - "postcss-calc": "^5.2.0", - "postcss-colormin": "^2.1.8", - "postcss-convert-values": "^2.3.4", - "postcss-discard-comments": "^2.0.4", - "postcss-discard-duplicates": "^2.0.1", - "postcss-discard-empty": "^2.0.1", - "postcss-discard-overridden": "^0.1.1", - "postcss-discard-unused": "^2.2.1", - "postcss-filter-plugins": "^2.0.0", - "postcss-merge-idents": "^2.1.5", - "postcss-merge-longhand": "^2.0.1", - "postcss-merge-rules": "^2.0.3", - "postcss-minify-font-values": "^1.0.2", - "postcss-minify-gradients": "^1.0.1", - "postcss-minify-params": "^1.0.4", - "postcss-minify-selectors": "^2.0.4", - "postcss-normalize-charset": "^1.1.0", - "postcss-normalize-url": "^3.0.7", - "postcss-ordered-values": "^2.1.0", - "postcss-reduce-idents": "^2.2.2", - "postcss-reduce-initial": "^1.0.0", - "postcss-reduce-transforms": "^1.0.3", - "postcss-svgo": "^2.1.1", - "postcss-unique-selectors": "^2.0.2", - "postcss-value-parser": "^3.2.3", - "postcss-zindex": "^2.0.1" - } - }, - "csso": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/csso/-/csso-2.3.2.tgz", - "integrity": "sha1-3dUsWHAz9J6Utx/FVWnyUuj/X4U=", - "dev": true, - "requires": { - "clap": "^1.0.9", - "source-map": "^0.5.3" - } - }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, "requires": { "array-find-index": "^1.0.1" } @@ -2969,60 +2724,38 @@ } }, "cyclic-router": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/cyclic-router/-/cyclic-router-4.0.7.tgz", - "integrity": "sha1-7mHNvaDcH3xi9iobiKPUSnqn+kU=", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cyclic-router/-/cyclic-router-6.0.0.tgz", + "integrity": "sha512-Gv+ZvHlAGGkqiaQvEry1qp6bgEBsXst/F188FDS0siSwQ26iQgrd1y4BAz0TeRQE0/gxCZB6GfTZvp/pFcmxgA==", "requires": { - "@cycle/history": "^6.3.0", - "@cycle/run": "^3.1.0", - "history": "^4.6.3" - }, - "dependencies": { - "@cycle/run": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@cycle/run/-/run-3.4.0.tgz", - "integrity": "sha512-YUZyPu0nC4YDC31mLH5PGxbMoPEH5dNEV+nmgt34GgGgJ0ykDd4PrY7/ph5MAEpQE6rOfov0VN44qQRs6beQow==", - "requires": { - "xstream": "10.x || 11.x" - } - } + "@cycle/run": "*" } }, - "d": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", "requires": { - "es5-ext": "^0.10.9" + "internmap": "^1.0.0" } }, - "d3-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.1.tgz", - "integrity": "sha512-CyINJQ0SOUHojDdFDH4JEM0552vCR1utGyLHegJHyYH0JyCpSeTPxi4OBqHMA2jJZq4NH782LtaJWBImqI/HBw==" - }, - "d3-collection": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.4.tgz", - "integrity": "sha1-NC39EoN8kJdPM/HMCnha6lcNzcI=" - }, "d3-color": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.2.0.tgz", - "integrity": "sha512-dmL9Zr/v39aSSMnLOTd58in2RbregCg4UtGyUArvEKTTN6S3HKEy+ziBWVYo9PTzRyVW+pUBHUtRKz0HYX+SQg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz", + "integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==" }, - "d3-contour": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.2.0.tgz", - "integrity": "sha512-nDzZ2KDnrgTrhMjV8TH0RNrljk6uPNAGkG/v/1SKNVvJa2JU8szjh7o2ZYTX8yufA2oCI5HyeMqbzwiB+oDoIA==", + "d3-delaunay": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-5.3.0.tgz", + "integrity": "sha512-amALSrOllWVLaHTnDLHwMIiz0d1bBu9gZXd1FiLfXf8sHcX9jrcj81TVZOqD4UX7MgBZZ07c8GxzEgBpJqc74w==", "requires": { - "d3-array": "^1.1.1" + "delaunator": "4" } }, "d3-dispatch": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.3.tgz", - "integrity": "sha1-RuFJHqqbWMNY/OW+TovtYm54cfg=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz", + "integrity": "sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA==" }, "d3-dsv": { "version": "0.1.14", @@ -3030,14 +2763,13 @@ "integrity": "sha1-mDPNYaWj6B4DJjoc54903lah27g=" }, "d3-force": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.1.0.tgz", - "integrity": "sha512-2HVQz3/VCQs0QeRNZTYb7GxoUCeb6bOzMp/cGcLa87awY9ZsPvXOGeZm0iaGBjXic6I1ysKwMn+g+5jSAdzwcg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-2.1.1.tgz", + "integrity": "sha512-nAuHEzBqMvpFVMf9OX75d00OxvOXdxY+xECIXjW6Gv8BRrXu6gAWbv/9XKrvfJ5i5DCokDW7RYE50LRoK092ew==", "requires": { - "d3-collection": "1", - "d3-dispatch": "1", - "d3-quadtree": "1", - "d3-timer": "1" + "d3-dispatch": "1 - 2", + "d3-quadtree": "1 - 2", + "d3-timer": "1 - 2" } }, "d3-format": { @@ -3046,115 +2778,88 @@ "integrity": "sha1-qnWcHlquX6javJq3gZxQL8a1aHU=" }, "d3-geo": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.10.0.tgz", - "integrity": "sha512-VK/buVGgexthTTqGRNXQ/LSo3EbOFu4p2Pjud5drSIaEnOaF2moc8A3P7WEljEO1JEBEwbpAJjFWMuJiUtoBcw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-2.0.2.tgz", + "integrity": "sha512-8pM1WGMLGFuhq9S+FpPURxic+gKzjluCD/CHTuUF3mXMeiCo0i6R0tO1s4+GArRFde96SLcW/kOFRjoAosPsFA==", + "requires": { + "d3-array": "^2.5.0" + } + }, + "d3-geo-projection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-3.0.0.tgz", + "integrity": "sha512-1JE+filVbkEX2bT25dJdQ05iA4QHvUwev6o0nIQHOSrNlHCAKfVss/U10vEM3pA4j5v7uQoFdQ4KLbx9BlEbWA==", "requires": { - "d3-array": "1" + "commander": "2", + "d3-array": "1 - 2", + "d3-geo": "1.12.0 - 2", + "resolve": "^1.1.10" } }, "d3-hierarchy": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.6.tgz", - "integrity": "sha512-nn4bhBnwWnMSoZgkBXD7vRyZ0xVUsNMQRKytWYHhP1I4qHw+qzApCTgSQTZqMdf4XXZbTMqA59hFusga+THA/g==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz", + "integrity": "sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw==" }, "d3-interpolate": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.2.0.tgz", - "integrity": "sha512-zLvTk8CREPFfc/2XglPQriAsXkzoRDAyBzndtKJWrZmHw7kmOWHNS11e40kPTd/oGk8P5mFJW5uBbcFQ+ybxyA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz", + "integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==", "requires": { - "d3-color": "1" + "d3-color": "1 - 2" } }, "d3-path": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.5.tgz", - "integrity": "sha1-JB6xhJvZ6egCHA0KeZ+KDo5EF2Q=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-2.0.0.tgz", + "integrity": "sha512-ZwZQxKhBnv9yHaiWd6ZU4x5BtCQ7pXszEV9CU6kRgwIQVQGLMv1oiL4M+MK/n79sYzsj+gcgpPQSctJUsLN7fA==" }, "d3-quadtree": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.3.tgz", - "integrity": "sha1-rHmH4+I/6AWpkPKOG1DTj8uCJDg=" - }, - "d3-request": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-request/-/d3-request-1.0.6.tgz", - "integrity": "sha512-FJj8ySY6GYuAJHZMaCQ83xEYE4KbkPkmxZ3Hu6zA1xxG2GD+z6P+Lyp+zjdsHf0xEbp2xcluDI50rCS855EQ6w==", - "requires": { - "d3-collection": "1", - "d3-dispatch": "1", - "d3-dsv": "1", - "xmlhttprequest": "1" - }, - "dependencies": { - "d3-dsv": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.0.8.tgz", - "integrity": "sha512-IVCJpQ+YGe3qu6odkPQI0KPqfxkhbP/oM1XhhE/DFiYmcXKfCRub4KXyiuehV1d4drjWVXHUWx4gHqhdZb6n/A==", - "requires": { - "commander": "2", - "iconv-lite": "0.4", - "rw": "1" - } - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } - } + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-2.0.0.tgz", + "integrity": "sha512-b0Ed2t1UUalJpc3qXzKi+cPGxeXRr4KU9YSlocN74aTzp6R/Ud43t79yLLqxHRWZfsvWXmbDWPpoENK1K539xw==" }, "d3-scale": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.0.0.tgz", - "integrity": "sha512-Sa2Ny6CoJT7x6dozxPnvUQT61epGWsgppFvnNl8eJEzfJBG0iDBBTJAtz2JKem7Mb+NevnaZiDiIDHsuWkv6vg==", - "requires": { - "d3-array": "^1.2.0", - "d3-collection": "1", - "d3-format": "1", - "d3-interpolate": "1", - "d3-time": "1", - "d3-time-format": "2" + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.3.0.tgz", + "integrity": "sha512-1JGp44NQCt5d1g+Yy+GeOnZP7xHo0ii8zsQp6PGzd+C1/dl0KGsp9A7Mxwp+1D1o4unbTTxVdU/ZOIEBoeZPbQ==", + "requires": { + "d3-array": "^2.3.0", + "d3-format": "1 - 2", + "d3-interpolate": "1.2.0 - 2", + "d3-time": "^2.1.1", + "d3-time-format": "2 - 3" }, "dependencies": { "d3-format": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.0.tgz", - "integrity": "sha512-ycfLEIzHVZC3rOvuBOKVyQXSiUyCDjeAPIj9n/wugrr+s5AcTQC2Bz6aKkubG7rQaQF0SGW/OV4UEJB9nfioFg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz", + "integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==" }, "d3-time": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.8.tgz", - "integrity": "sha512-YRZkNhphZh3KcnBfitvF3c6E0JOFGikHZ4YqD+Lzv83ZHn1/u6yGenRU1m+KAk9J1GnZMnKcrtfvSktlA1DXNQ==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz", + "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==", + "requires": { + "d3-array": "2" + } }, "d3-time-format": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.1.tgz", - "integrity": "sha512-8kAkymq2WMfzW7e+s/IUNAtN/y3gZXGRrdGfo6R8NKPAA85UBTxZg5E61bR6nLwjPjj4d3zywSQe1CkYLPFyrw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", + "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", "requires": { - "d3-time": "1" + "d3-time": "1 - 2" } } } }, - "d3-scale-chromatic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.3.0.tgz", - "integrity": "sha512-YwMbiaW2bStWvQFByK8hA6hk7ToWflspIo2TRukCqERd8isiafEMBXmwfh8c7/0Z94mVvIzIveRLVC6RAjhgeA==", - "requires": { - "d3-color": "1", - "d3-interpolate": "1" - } - }, "d3-shape": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.2.0.tgz", - "integrity": "sha1-RdAVOPBkuv0F6j1tLLdI/YxB93c=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz", + "integrity": "sha512-PnjUqfM2PpskbSLTJvAzp2Wv4CZsnAgTfcVRTwW03QR3MkXF8Uo7B1y/lWkAsmbKwuecto++4NlsYcvYpXpTHA==", "requires": { - "d3-path": "1" + "d3-path": "1 - 2" } }, "d3-time": { @@ -3171,14 +2876,9 @@ } }, "d3-timer": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.7.tgz", - "integrity": "sha512-vMZXR88XujmG/L5oB96NNKH5lCWwiLM/S2HyyAQLcjWJCloK5shxta4CwOFYLZoY3AWX73v8Lgv4cCAdWtRmOA==" - }, - "d3-voronoi": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.2.tgz", - "integrity": "sha1-Fodmfo8TotFYyAwUgMWinLDYlzw=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz", + "integrity": "sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA==" }, "dashdash": { "version": "1.14.1", @@ -3196,153 +2896,204 @@ } }, "datalib": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/datalib/-/datalib-1.9.1.tgz", - "integrity": "sha512-E1F/V6LkSw2RZ3A7g/uiVPRD5oE5NjWXgZLU0aBmgIWbGLvYdsgrODfYJJmFcZE1bWr9SOO1UzfJ9pj+g+t2ng==", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/datalib/-/datalib-1.9.3.tgz", + "integrity": "sha512-9rcwGd3zhvmJChyLzL5jjZ6UEtWO0SKa9Ycy6RVoQxSW43TSOBRbizj/Zn8UonfpBjCikHEQrJyE72Xw5eCY5A==", "requires": { "d3-dsv": "0.1", "d3-format": "0.4", "d3-time": "0.1", "d3-time-format": "0.2", "request": "^2.67.0", - "sync-request": "^2.1.0", + "sync-request": "^6.0.0", "topojson-client": "^3.0.0" } }, - "date-now": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", - "dev": true - }, "debug": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.3.tgz", - "integrity": "sha1-D364wwll7AjHKsz6ATDIt5mEFB0=", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "requires": { - "ms": "0.7.2" + "ms": "^2.1.1" } }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", - "dev": true - }, - "deep-extend": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", - "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==" - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", "dev": true, "requires": { - "object-keys": "^1.0.12" + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" }, "dependencies": { "object-keys": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true } } }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", "dev": true, "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" + "execa": "^5.0.0" }, "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { - "kind-of": "^6.0.0" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "requires": { - "kind-of": "^6.0.0" + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" } }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "path-key": "^3.0.0" } }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } } } }, - "defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "dev": true }, - "del": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", - "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", - "dev": true, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "requires": { - "globby": "^6.1.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "p-map": "^1.1.1", - "pify": "^3.0.0", - "rimraf": "^2.2.8" + "object-keys": "^1.0.12" }, "dependencies": { - "pify": { + "object-keys": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", + "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==" + } + } + }, + "del": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", + "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", + "dev": true, + "requires": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true } } }, + "delaunator": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz", + "integrity": "sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag==" + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3351,7 +3102,8 @@ "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true }, "depd": { "version": "1.1.2", @@ -3359,85 +3111,38 @@ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", "dev": true }, - "des.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", - "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, "destroy": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", "dev": true }, - "detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "optional": true - }, "detect-node": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.3.tgz", - "integrity": "sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, - "diffie-hellman": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", - "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=", + "diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "dmg-builder": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-5.3.0.tgz", - "integrity": "sha512-vzjrc7UmPQ+rb4tH8wbQdMq6Fu9M5chFndzhK2831xIpRsRlNlGEIWMiFRZ/MlboVL0vWxG0/2JCd2YMAevEpA==", - "dev": true, - "requires": { - "app-builder-lib": "~20.28.0", - "bluebird-lst": "^1.0.5", - "builder-util": "~6.1.0", - "fs-extra-p": "^4.6.1", - "iconv-lite": "^0.4.23", - "js-yaml": "^3.12.0", - "parse-color": "^1.0.0", - "sanitize-filename": "^1.6.1" + "path-type": "^4.0.0" }, "dependencies": { - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true - }, - "js-yaml": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", - "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } } } }, @@ -3448,9 +3153,9 @@ "dev": true }, "dns-packet": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", - "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", + "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", "dev": true, "requires": { "ip": "^1.1.0", @@ -3466,313 +3171,192 @@ "buffer-indexof": "^1.0.0" } }, - "domain-browser": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", - "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=", - "dev": true - }, - "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", - "dev": true, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", "requires": { - "is-obj": "^1.0.0" + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" } }, - "dotenv": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.0.0.tgz", - "integrity": "sha512-FlWbnhgjtwD+uNLUGHbMykMOYQaTivdHEmYwAKFjn6GKe/CqY0fNae93ZHTd20snh9ZLr8mTzIL9m0APQ1pjQg==", + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "dev": true }, - "dotenv-expand": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-4.2.0.tgz", - "integrity": "sha1-3vHxyl1gWdJKdm5YeULCEQbOEnU=", + "electron-to-chromium": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.1.tgz", + "integrity": "sha512-9ldvb6QMHiDpUNF1iSwBTiTT0qXEN+xIO5WlCJrC5gt0z74ofOiqR698vaJqYWnri0XZiF0YmnrFmGq/EmpGAA==", "dev": true }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "enhanced-resolve": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz", + "integrity": "sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA==", + "dev": true, "requires": { - "jsbn": "~0.1.0" + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" } }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", "dev": true }, - "ejs": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz", - "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==", + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true }, - "electron": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/electron/-/electron-2.0.7.tgz", - "integrity": "sha512-MRrDE6mrp+ZrIBpZM27pxbO2yEDKYfkmc6Ll79BtedMNEZsY4+oblupeDJL6RM6meUIp82KMo63W7fP65Tb89Q==", + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, "requires": { - "@types/node": "^8.0.24", - "electron-download": "^3.0.1", - "extract-zip": "^1.0.3" - }, - "dependencies": { - "@types/node": { - "version": "8.10.26", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.26.tgz", - "integrity": "sha512-opk6bLLErLSwyVVJeSH5Ek7ZWOBSsN0JrvXTNVGLXLAXKB9xlTYajrplR44xVyMrmbut94H6uJ9jqzM/12jxkA==" - } + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" } }, - "electron-builder": { - "version": "20.28.1", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-20.28.1.tgz", - "integrity": "sha512-OKj107B2fV0ftOFOjQLyuKl6n+R8KJOGgGUrHaW4EI8bwqycTq67bgCc0xwPruHBWDX/Kg3tMYBRLbjUNw+6Qw==", - "dev": true, - "requires": { - "app-builder-lib": "20.28.1", - "bluebird-lst": "^1.0.5", - "builder-util": "6.1.1", - "builder-util-runtime": "4.4.1", - "chalk": "^2.4.1", - "dmg-builder": "5.3.0", - "fs-extra-p": "^4.6.1", - "is-ci": "^1.1.0", - "lazy-val": "^1.0.3", - "read-config-file": "3.1.2", - "sanitize-filename": "^1.6.1", - "update-notifier": "^2.5.0", - "yargs": "^12.0.1" + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" }, "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - } - }, - "decamelize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz", - "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==", - "dev": true, - "requires": { - "xregexp": "4.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "os-locale": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", - "dev": true, - "requires": { - "execa": "^0.7.0", - "lcid": "^1.0.0", - "mem": "^1.1.0" - } - }, - "p-limit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz", - "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true - }, - "yargs": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.1.tgz", - "integrity": "sha512-B0vRAp1hRX4jgIOWFtjfNjd9OA9RWYZ6tqGA9/I/IrTMsxmKvtWy+ersM+jzpQqbC3YfLzeABPdeTgcJ9eu1qQ==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^2.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^2.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^10.1.0" - } } } }, - "electron-download": { + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "events": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-3.3.0.tgz", - "integrity": "sha1-LP1U1pZsAZxNSa1l++Zcyc3vaMg=", - "requires": { - "debug": "^2.2.0", - "fs-extra": "^0.30.0", - "home-path": "^1.0.1", - "minimist": "^1.2.0", - "nugget": "^2.0.0", - "path-exists": "^2.1.0", - "rc": "^1.1.2", - "semver": "^5.3.0", - "sumchecker": "^1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true }, - "electron-osx-sign": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.10.tgz", - "integrity": "sha1-vk87ibKnWh3F8eckkIGrKSnKOiY=", + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", "dev": true, "requires": { - "bluebird": "^3.5.0", - "compare-version": "^0.1.2", - "debug": "^2.6.8", - "isbinaryfile": "^3.0.2", - "minimist": "^1.2.0", - "plist": "^2.1.0" + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" }, "dependencies": { - "base64-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz", - "integrity": "sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE=", + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", "dev": true }, "debug": { @@ -3784,2161 +3368,680 @@ "ms": "2.0.0" } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, - "plist": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-2.1.0.tgz", - "integrity": "sha1-V8zbeggh3yGDEhejytVOPhRqECU=", - "dev": true, - "requires": { - "base64-js": "1.2.0", - "xmlbuilder": "8.2.2", - "xmldom": "0.1.x" - } - } - } - }, - "electron-publish": { - "version": "20.28.0", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-20.28.0.tgz", - "integrity": "sha512-ZGwzXyWuEGIvaCCGD0tebhjYGf7lxjdmkFAW3oFjRXOBXsBl91elOzOwfRSs/7zUE9mvvE0MnyJeBlqO7SAUvA==", - "dev": true, - "requires": { - "bluebird-lst": "^1.0.5", - "builder-util": "~6.1.0", - "builder-util-runtime": "^4.4.1", - "chalk": "^2.4.1", - "fs-extra-p": "^4.6.1", - "lazy-val": "^1.0.3", - "mime": "^2.3.1" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", "dev": true }, - "mime": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz", - "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==", + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, - "electron-to-chromium": { - "version": "1.3.16", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.16.tgz", - "integrity": "sha1-0OAmc1dUdwkBrjAaIWZMukXZL30=", + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", + "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=" + }, + "fast-deep-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", + "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=", "dev": true }, - "elliptic": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", - "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "fast-glob": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", + "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", "dev": true, "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" } }, - "emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=" + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "fastest-levenshtein": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", + "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", "dev": true }, - "enhanced-resolve": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz", - "integrity": "sha1-BCHjOf1xQZs9oT0Smzl5BAIwR24=", + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.4.0", - "object-assign": "^4.0.1", - "tapable": "^0.2.7" + "reusify": "^1.0.4" } }, - "errno": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz", - "integrity": "sha1-uJbiOp5ei6M4cfyZar02NfyaHH0=", + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", "dev": true, "requires": { - "prr": "~0.0.0" - } - }, - "error-ex": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", - "requires": { - "is-arrayish": "^0.2.1" + "websocket-driver": ">=0.5.1" } }, - "es-abstract": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", - "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", + "file-loader": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-5.1.0.tgz", + "integrity": "sha512-u/VkLGskw3Ue59nyOwUwXI/6nuBCo7KBkniB/l7ICwr/7cPNGsL1WCXUp3GB0qgOOKU1TiP49bv4DZF/LJqprg==", "dev": true, "requires": { - "es-to-primitive": "^1.1.1", - "function-bind": "^1.1.1", - "has": "^1.0.1", - "is-callable": "^1.1.3", - "is-regex": "^1.0.4" + "loader-utils": "^1.4.0", + "schema-utils": "^2.5.0" }, "dependencies": { - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } } } }, - "es-to-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", - "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, "requires": { - "is-callable": "^1.1.1", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.1" + "to-regex-range": "^5.0.1" } }, - "es5-ext": { - "version": "0.10.42", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.42.tgz", - "integrity": "sha512-AJxO1rmPe1bDEfSR6TJ/FgMFYuTBhR5R57KW58iCkYACMyFbrkqVyzXSurYoScDGvgyMpk7uRF/lPUPPTmsRSA==", + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.1", - "next-tick": "1" + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" } }, - "es6-map": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", - "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-set": "~0.1.5", - "es6-symbol": "~3.1.1", - "event-emitter": "~0.3.5" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" } }, - "es6-promise": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", - "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==" + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "follow-redirects": { + "version": "1.14.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz", + "integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==", + "dev": true }, - "es6-set": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", - "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-symbol": "3.1.1", - "event-emitter": "~0.3.5" - } + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", "requires": { - "d": "1", - "es5-ext": "~0.10.14" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "dependencies": { + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + } } }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=" + "formidable": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz", + "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==" }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", "dev": true }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } + "fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "dev": true }, - "eventemitter3": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", - "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==", + "fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", "dev": true }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "eventsource": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-0.1.6.tgz", - "integrity": "sha1-Cs7ehJ7X3RzMMsgRuxG5RNTykjI=", + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, - "requires": { - "original": ">=0.0.5" - } + "optional": true }, - "evp_bytestokey": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.0.tgz", - "integrity": "sha1-SXtmrZ/vZc18CKYYCCS6FHa2blM=", + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", "dev": true, "requires": { - "create-hash": "^1.1.1" + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" } }, - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" } }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", "dev": true, "requires": { - "is-posix-bracket": "^0.1.0" + "globule": "^1.0.0" } }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "dev": true, - "requires": { - "fill-range": "^2.1.0" - } + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true }, - "express": { - "version": "4.16.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", - "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", - "dev": true, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", "requires": { - "accepts": "~1.3.5", - "array-flatten": "1.1.1", - "body-parser": "1.18.2", - "content-disposition": "0.5.2", - "content-type": "~1.0.4", - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.1.1", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.3", - "qs": "6.5.1", - "range-parser": "~1.2.0", - "safe-buffer": "5.1.1", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "~1.4.0", - "type-is": "~1.6.16", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" }, "dependencies": { - "array-flatten": { + "function-bind": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "requires": { - "ms": "2.0.0" + "function-bind": "^1.1.1" } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", - "dev": true - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true } } }, - "extend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz", - "integrity": "sha1-WkdDU7nzNT3dgXbf03uRyDpG8dQ=" + "get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=" }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" + "assert-plus": "^1.0.0" }, "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" } } }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dev": true, "requires": { - "is-extglob": "^1.0.0" + "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" } }, - "extract-text-webpack-plugin": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/extract-text-webpack-plugin/-/extract-text-webpack-plugin-2.1.2.tgz", - "integrity": "sha1-dW7076gVXDaBgz+8NNpTuUF0bWw=", + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "requires": { - "async": "^2.1.2", - "loader-utils": "^1.0.2", - "schema-utils": "^0.3.0", - "webpack-sources": "^1.0.1" + "is-glob": "^4.0.1" } }, - "extract-zip": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz", - "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "globalthis": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.2.tgz", + "integrity": "sha512-ZQnSFO1la8P7auIOQECnm0sSuoMeaSq0EEdXMBFF2QJO4uNcwbyhSgG3MruWNbFTqCLmxVwGOl7LZ9kASvHdeQ==", "requires": { - "concat-stream": "1.6.2", - "debug": "2.6.9", - "mkdirp": "0.5.1", - "yauzl": "2.4.1" + "define-properties": "^1.1.3" + } + }, + "globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" }, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + } + } + }, + "globule": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "dev": true, + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + }, + "dependencies": { + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, "requires": { - "ms": "2.0.0" + "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" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true } } }, - "extsprintf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", - "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=" - }, - "fast-deep-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=", + "graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", "dev": true }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, - "fastparse": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz", - "integrity": "sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg=", + "handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", "dev": true }, - "faye-websocket": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", - "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, - "fd-slicer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", - "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "requires": { - "pend": "~1.2.0" + "ajv": "^6.12.3", + "har-schema": "^2.0.0" } }, - "file-loader": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-0.10.1.tgz", - "integrity": "sha1-gVA0EZiR/GRB+1pkwRvJPCLd2EI=", + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, "requires": { - "loader-utils": "^1.0.2" + "ansi-regex": "^2.0.0" } }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "fill-range": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", - "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", - "dev": true, - "requires": { - "is-number": "^2.1.0", - "isobject": "^2.0.0", - "randomatic": "^1.1.3", - "repeat-element": "^1.1.2", - "repeat-string": "^1.5.2" - } - }, - "finalhandler": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.4.0", - "unpipe": "~1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, - "find-cache-dir": { + "has-tostringtag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", - "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", "dev": true, "requires": { - "commondir": "^1.0.1", - "make-dir": "^1.0.0", - "pkg-dir": "^2.0.0" + "has-symbols": "^1.0.2" } }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true }, - "flatten": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz", - "integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=", + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, - "follow-redirects": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.5.tgz", - "integrity": "sha512-GHjtHDlY/ehslqv0Gr5N0PUJppgg/q0rOBvX0na1s7y1A3LWxPqCYU76s3Z1bM4+UZB4QF0usaXLT5wFpof5PA==", - "dev": true, + "history": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz", + "integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==", "requires": { - "debug": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } + "invariant": "^2.2.1", + "loose-envify": "^1.2.0", + "resolve-pathname": "^2.2.0", + "value-equal": "^0.4.0", + "warning": "^3.0.0" } }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", "dev": true, "requires": { - "for-in": "^1.0.1" + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" } }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { + "html-entities": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", - "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz", + "integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==", + "dev": true + }, + "http-basic": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-8.1.3.tgz", + "integrity": "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==", "requires": { - "asynckit": "^0.4.0", - "combined-stream": "1.0.6", - "mime-types": "^2.1.12" - }, - "dependencies": { - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "requires": { - "delayed-stream": "~1.0.0" - } - } + "caseless": "^0.12.0", + "concat-stream": "^1.6.2", + "http-response-object": "^3.0.1", + "parse-cache-control": "^1.0.1" } }, - "formidable": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", - "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==" - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", "dev": true }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", "dev": true, "requires": { - "map-cache": "^0.2.2" + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" } }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "http-parser-js": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", + "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==", "dev": true }, - "fs-extra": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.30.0.tgz", - "integrity": "sha1-8jP/zAjU2n1DLapEl3aYnbHfk/A=", + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^2.1.0", - "klaw": "^1.0.0", - "path-is-absolute": "^1.0.0", - "rimraf": "^2.2.8" + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" } }, - "fs-extra-p": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-4.6.1.tgz", - "integrity": "sha512-IsTMbUS0svZKZTvqF4vDS9c/L7Mw9n8nZQWWeSzAGacOSe+8CzowhUN0tdZEZFIJNP5HC7L9j3MMikz/G4hDeQ==", + "http-proxy-middleware": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.1.tgz", + "integrity": "sha512-cfaXRVoZxSed/BmkA7SwBVNI9Kj7HFltaE5rqYOub5kWzWZ+gofV2koVN1j2rMW7pEfSSlCHGJ31xmuyFyfLOg==", "dev": true, "requires": { - "bluebird-lst": "^1.0.5", - "fs-extra": "^6.0.1" + "@types/http-proxy": "^1.17.5", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" }, "dependencies": { - "fs-extra": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", - "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - } - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz", - "integrity": "sha512-Sn44E5wQW4bTHXvQmvSHwqbuiXtduD6Rrjm2ZtUEGbyrig+nUH3t/QD4M4/ZXViY556TBpRgZkHLDx3JxPwxiw==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.3.0", - "node-pre-gyp": "^0.6.36" - }, - "dependencies": { - "abbrev": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "ajv": { - "version": "4.11.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "co": "^4.6.0", - "json-stable-stringify": "^1.0.1" - } - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "asn1": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "assert-plus": { - "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "asynckit": { - "version": "0.4.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws-sign2": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws4": { - "version": "1.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "balanced-match": { - "version": "0.4.2", - "bundled": true, - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "block-stream": { - "version": "0.0.9", - "bundled": true, - "dev": true, - "requires": { - "inherits": "~2.0.0" - } - }, - "boom": { - "version": "2.10.1", - "bundled": true, - "dev": true, - "requires": { - "hoek": "2.x.x" - } - }, - "brace-expansion": { - "version": "1.1.7", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "^0.4.1", - "concat-map": "0.0.1" - } - }, - "buffer-shims": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "caseless": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true - }, - "co": { - "version": "4.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "combined-stream": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "cryptiles": { - "version": "2.0.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "boom": "2.x.x" - } - }, - "dashdash": { - "version": "1.14.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "^1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "debug": { - "version": "2.6.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "bundled": true, - "dev": true, - "optional": true - }, - "delayed-stream": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "ecc-jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "~0.1.0" - } - }, - "extend": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "extsprintf": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "bundled": true, - "dev": true, - "optional": true - }, - "form-data": { - "version": "2.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.5", - "mime-types": "^2.1.12" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "fstream": { - "version": "1.0.11", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fstream": "^1.0.0", - "inherits": "2", - "minimatch": "^3.0.0" - } - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "getpass": { - "version": "0.1.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "^1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "requires": { - "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" - } - }, - "graceful-fs": { - "version": "4.1.11", - "bundled": true, - "dev": true - }, - "har-schema": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "har-validator": { - "version": "4.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ajv": "^4.9.1", - "har-schema": "^1.0.5" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "hawk": { - "version": "3.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "boom": "2.x.x", - "cryptiles": "2.x.x", - "hoek": "2.x.x", - "sntp": "1.x.x" - } - }, - "hoek": { - "version": "2.16.3", - "bundled": true, - "dev": true - }, - "http-signature": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "^0.2.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.4", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "isstream": { - "version": "0.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "jodid25519": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "~0.1.0" - } - }, - "jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsonify": "~0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "jsonify": { - "version": "0.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "jsprim": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.0.2", - "json-schema": "0.2.3", - "verror": "1.3.6" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "mime-db": { - "version": "1.27.0", - "bundled": true, - "dev": true - }, - "mime-types": { - "version": "2.1.15", - "bundled": true, - "dev": true, - "requires": { - "mime-db": "~1.27.0" - } - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "node-pre-gyp": { - "version": "0.6.36", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "mkdirp": "^0.5.1", - "nopt": "^4.0.1", - "npmlog": "^4.0.2", - "rc": "^1.1.7", - "request": "^2.81.0", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^2.2.1", - "tar-pack": "^3.4.0" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npmlog": { - "version": "4.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "oauth-sign": { - "version": "0.8.2", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "performance-now": { - "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "1.0.7", - "bundled": true, - "dev": true - }, - "punycode": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true - }, - "qs": { - "version": "6.4.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "~0.4.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.2.9", - "bundled": true, - "dev": true, - "requires": { - "buffer-shims": "~1.0.0", - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "~1.0.0", - "process-nextick-args": "~1.0.6", - "string_decoder": "~1.0.0", - "util-deprecate": "~1.0.1" - } - }, - "request": { - "version": "2.81.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aws-sign2": "~0.6.0", - "aws4": "^1.2.1", - "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.0", - "forever-agent": "~0.6.1", - "form-data": "~2.1.1", - "har-validator": "~4.2.1", - "hawk": "~3.1.3", - "http-signature": "~1.1.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.7", - "oauth-sign": "~0.8.1", - "performance-now": "^0.2.0", - "qs": "~6.4.0", - "safe-buffer": "^5.0.1", - "stringstream": "~0.0.4", - "tough-cookie": "~2.3.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.0.0" - } - }, - "rimraf": { - "version": "2.6.1", - "bundled": true, - "dev": true, - "requires": { - "glob": "^7.0.5" - } - }, - "safe-buffer": { - "version": "5.0.1", - "bundled": true, - "dev": true - }, - "semver": { - "version": "5.3.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sntp": { - "version": "1.0.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "hoek": "2.x.x" - } - }, - "sshpk": { - "version": "1.13.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jodid25519": "^1.0.0", - "jsbn": "~0.1.0", - "tweetnacl": "~0.14.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "stringstream": { - "version": "0.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "2.2.1", - "bundled": true, - "dev": true, - "requires": { - "block-stream": "*", - "fstream": "^1.0.2", - "inherits": "2" - } - }, - "tar-pack": { - "version": "3.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^2.2.0", - "fstream": "^1.0.10", - "fstream-ignore": "^1.0.5", - "once": "^1.3.3", - "readable-stream": "^2.1.4", - "rimraf": "^2.5.1", - "tar": "^2.2.1", - "uid-number": "^0.0.6" - } - }, - "tough-cookie": { - "version": "2.3.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "punycode": "^1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "bundled": true, - "dev": true, - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "uuid": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "verror": { - "version": "1.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "extsprintf": "1.0.2" - } - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - } - } - }, - "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", - "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", - "optional": true, - "requires": { - "fstream": "^1.0.0", - "inherits": "2", - "minimatch": "^3.0.0" - } - }, - "function-bind": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", - "integrity": "sha1-FhdnFMgBeY5Ojyz391KUZ7tKV3E=", - "dev": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", - "dev": true, - "requires": { - "globule": "^1.0.0" - } - }, - "get-caller-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", - "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz", - "integrity": "sha1-KD/9n8ElaECHUxHBtg6MQBhxEOY=", - "requires": { - "assert-plus": "^1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", - "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.2", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "dev": true, - "requires": { - "glob-parent": "^2.0.0", - "is-glob": "^2.0.0" - } - }, - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "dev": true, - "requires": { - "is-glob": "^2.0.0" - } - }, - "global-dirs": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", - "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", - "dev": true, - "requires": { - "ini": "^1.3.4" - } - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - }, - "globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "globule": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", - "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", - "dev": true, - "requires": { - "glob": "~7.1.1", - "lodash": "~4.17.10", - "minimatch": "~3.0.2" - }, - "dependencies": { - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", - "dev": true - } - } - }, - "got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", - "dev": true, - "requires": { - "create-error-class": "^3.0.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "handle-thing": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz", - "integrity": "sha1-/Xqtcmvxpf0W38KbL3pmAdJxOcQ=", - "dev": true - }, - "har-schema": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", - "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" - }, - "har-validator": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", - "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", - "requires": { - "ajv": "^4.9.1", - "har-schema": "^1.0.5" - } - }, - "has": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", - "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", - "dev": true, - "requires": { - "function-bind": "^1.0.2" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { + "is-plain-obj": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hash-base": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", - "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=", - "dev": true, - "requires": { - "inherits": "^2.0.1" - } - }, - "hash.js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", - "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.0" - } - }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "requires": { - "boom": "2.x.x", - "cryptiles": "2.x.x", - "hoek": "2.x.x", - "sntp": "1.x.x" - } - }, - "history": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz", - "integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==", - "requires": { - "invariant": "^2.2.1", - "loose-envify": "^1.2.0", - "resolve-pathname": "^2.2.0", - "value-equal": "^0.4.0", - "warning": "^3.0.0" - } - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" - }, - "home-or-tmp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", - "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.1" - } - }, - "home-path": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/home-path/-/home-path-1.0.6.tgz", - "integrity": "sha512-wo+yjrdAtoXt43Vy92a+0IPCYViiyLAHyp0QVS4xL/tfvVz5sXIW1ubLZk3nhVkD92fQpUMKX+fzMjr5F489vw==" - }, - "hosted-git-info": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", - "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==" - }, - "hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "html-comment-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.1.tgz", - "integrity": "sha1-ZouTd26q5V696POtRkswekljYl4=", - "dev": true - }, - "html-entities": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", - "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=", - "dev": true - }, - "http-basic": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-2.5.1.tgz", - "integrity": "sha1-jORHvbW2xXf4pj4/p4BW7Eu02/s=", - "requires": { - "caseless": "~0.11.0", - "concat-stream": "^1.4.6", - "http-response-object": "^1.0.0" - }, - "dependencies": { - "caseless": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true } } }, - "http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", - "dev": true - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "http-parser-js": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.13.tgz", - "integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc=", - "dev": true - }, - "http-proxy": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.17.0.tgz", - "integrity": "sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==", - "dev": true, - "requires": { - "eventemitter3": "^3.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - } - }, - "http-proxy-middleware": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz", - "integrity": "sha1-ZC6ISIUdZvCdTxJJEoRtuutBuDM=", - "dev": true, + "http-response-object": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-3.0.2.tgz", + "integrity": "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==", "requires": { - "http-proxy": "^1.16.2", - "is-glob": "^3.1.0", - "lodash": "^4.17.2", - "micromatch": "^2.3.11" + "@types/node": "^10.0.3" }, "dependencies": { - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } + "@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" } } }, - "http-response-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-1.1.0.tgz", - "integrity": "sha1-p8TnWq6C87tJBOT0P2FWc7TVGMM=" - }, "http-signature": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "requires": { - "assert-plus": "^0.2.0", + "assert-plus": "^1.0.0", "jsprim": "^1.2.2", "sshpk": "^1.7.0" } }, - "https-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", - "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=", + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, "iconv-lite": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -5949,34 +4052,28 @@ "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", "dev": true }, - "ieee754": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", - "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", + "icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", "dev": true }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "ignore": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", + "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", "dev": true }, "import-local": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", - "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.3.tgz", + "integrity": "sha512-bE9iaUY3CXH8Cwfan/abDKAxe1KGT9kyGsBPqf6DMK/z0a2OzAsrukeYNgIH6cH5Xr452jb1TUL8rSfCLjZ9uA==", "dev": true, "requires": { - "pkg-dir": "^2.0.0", - "resolve-cwd": "^2.0.0" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" } }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, "in-publish": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", @@ -5987,26 +4084,16 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, "requires": { "repeating": "^2.0.0" } }, - "indexes-of": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", - "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", - "dev": true - }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", - "dev": true - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -6017,249 +4104,182 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, "internal-ip": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-1.2.0.tgz", - "integrity": "sha1-rp+/k7mEh4eF1QqN4bNWlWBYz1w=", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-6.2.0.tgz", + "integrity": "sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==", "dev": true, "requires": { - "meow": "^3.3.0" + "default-gateway": "^6.0.0", + "ipaddr.js": "^1.9.1", + "is-ip": "^3.1.0", + "p-event": "^4.2.0" + }, + "dependencies": { + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + } } }, + "internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==" + }, "interpret": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz", - "integrity": "sha1-y8NcYu7uc/Gat7EKgBURQBr8D5A=", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", "dev": true }, "invariant": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz", - "integrity": "sha1-nh9WrArNtr8wMwbzOL47IErmA2A=", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "requires": { "loose-envify": "^1.0.0" } }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" - }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", "dev": true }, - "ipaddr.js": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", - "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=", - "dev": true - }, - "is-absolute-url": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", - "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", - "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", - "dev": true - }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", - "requires": { - "builtin-modules": "^1.0.0" - } + "ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "dev": true }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "ipaddr.js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", + "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", "dev": true }, - "is-ci": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.0.tgz", - "integrity": "sha512-plgvKjQtalH2P3Gytb7L61Lmz95g2DlpzFiQyRSFew8WoJKxtKRzrZMeyRN2supblm3Psc8OQGy7Xjb6XG11jw==", + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", "dev": true, "requires": { - "ci-info": "^1.3.0" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" } }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "requires": { - "kind-of": "^3.0.2" + "binary-extensions": "^2.0.0" } }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, + "is-core-module": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "has": "^1.0.3" }, "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } } } }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", - "dev": true - }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", "dev": true, "requires": { - "is-primitive": "^2.0.0" + "has-tostringtag": "^1.0.0" } }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true }, "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "requires": { - "number-is-nan": "^1.0.0" - } + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "dev": true }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, "requires": { "number-is-nan": "^1.0.0" } }, "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "requires": { - "is-extglob": "^1.0.0" + "is-extglob": "^2.1.1" } }, - "is-installed-globally": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", - "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "is-ip": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", + "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", "dev": true, "requires": { - "global-dirs": "^0.1.0", - "is-path-inside": "^1.0.0" + "ip-regex": "^4.0.0" } }, - "is-npm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", - "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", - "dev": true - }, "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", "dev": true }, - "is-path-in-cwd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", - "dev": true, - "requires": { - "is-path-inside": "^1.0.0" - } - }, "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "^1.0.1" - } + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true }, "is-plain-obj": { "version": "1.1.0", @@ -6274,106 +4294,49 @@ "dev": true, "requires": { "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } } }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", - "dev": true - }, - "is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true - }, - "is-redirect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", - "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", - "dev": true - }, "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "requires": { - "has": "^1.0.1" - } - }, - "is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-svg": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-2.1.0.tgz", - "integrity": "sha1-z2EJDaDZ77yrhyLeum8DIgjbsOk=", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "dev": true, "requires": { - "html-comment-regex": "^1.1.0" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" } }, - "is-symbol": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", - "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", - "dev": true - }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", "dev": true }, "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", - "dev": true + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, - "isbinaryfile": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-3.0.3.tgz", - "integrity": "sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==", - "dev": true, - "requires": { - "buffer-alloc": "^1.2.0" - } - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -6381,69 +4344,79 @@ "dev": true }, "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, - "jodid25519": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", - "integrity": "sha1-BtSRIlUJNBlHfUJWM2BuDpB4KWc=", - "optional": true, + "jest-worker": { + "version": "27.3.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.3.1.tgz", + "integrity": "sha512-ks3WCzsiZaOPJl/oMsDjaf0TRiSv7ctNgs0FqRr2nARsovz6AWWy4oLElwcquGSz692DzgZQrCLScPNs5YlC4g==", + "dev": true, "requires": { - "jsbn": "~0.1.0" + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "jquery": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz", - "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==", "dev": true }, "js-base64": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.1.9.tgz", - "integrity": "sha1-8OgK4DmkvWVLXygfyT8EqRSn/M4=" + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", + "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", + "dev": true }, "js-tokens": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.1.tgz", - "integrity": "sha1-COnxMkhKLEWjCQfp3E1VZ7fxFNc=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.7.0.tgz", - "integrity": "sha1-XJZ93YN6m/3KXy3oQlOr6KHAO4A=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, "requires": { - "argparse": "^1.0.7", - "esprima": "^2.6.0" + "argparse": "^2.0.1" } }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", - "dev": true + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, - "json-loader": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", - "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==", + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, "json-schema": { @@ -6457,43 +4430,19 @@ "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", "dev": true }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "requires": { - "jsonify": "~0.0.0" - } - }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, - "json3": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", - "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", - "dev": true - }, "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=" - }, - "jsonfile": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", - "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "requires": { - "graceful-fs": "^4.1.6" + "minimist": "^1.2.0" } }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" - }, "jsprim": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", @@ -6512,388 +4461,609 @@ } } }, - "killable": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.0.tgz", - "integrity": "sha1-2ouEvUfeU5WHj5XWTQLyRJ/gXms=", + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "klona": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", + "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==", + "dev": true + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { - "is-buffer": "^1.1.5" + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "loader-runner": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", + "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "dev": true + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" } }, - "klaw": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", - "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "dev": true, "requires": { - "graceful-fs": "^4.1.9" + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" } }, - "latest-version": { + "make-dir": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", - "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", - "dev": true, + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "requires": { - "package-json": "^4.0.0" + "semver": "^6.0.0" } }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", "dev": true }, - "lazy-val": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.3.tgz", - "integrity": "sha512-pjCf3BYk+uv3ZcPzEVM0BFvO9Uw58TmlrU0oG5tTrr9Kcid3+kdKxapH8CjdYmVa2nO5wOoZn2rdvZx2PKj/xg==", + "material-design-icons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/material-design-icons/-/material-design-icons-3.0.1.tgz", + "integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78=", "dev": true }, - "lcid": { + "materialize-css": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", - "requires": { - "invert-kv": "^1.0.0" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "loader-runner": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.0.tgz", - "integrity": "sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=", - "dev": true - }, - "loader-utils": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", - "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", - "requires": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0" - } + "resolved": "https://registry.npmjs.org/materialize-css/-/materialize-css-1.0.0.tgz", + "integrity": "sha512-4/oecXl8y/1i8RDZvyvwAICyqwNoKU4or5uf8uoAd74k76KzZ0Llym4zhJ5lLNUskcqjO0AuMcvNyDkpz8Z6zw==" }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "materialize-loader": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/materialize-loader/-/materialize-loader-3.0.1.tgz", + "integrity": "sha512-cAxKPIh5FLiIl8o2cctsbbtfnSWfxDy6Tf/y2hLTTiRtIZBHXz3NBompVrp/kJJzJxE+8MxbrC/vM+gCY8VU2g==", "dev": true, "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" + "css-loader": "^2.1.0", + "node-sass": "^4.11.0", + "sass-loader": "^7.1.0", + "style-loader": "^0.23.1" }, "dependencies": { - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "css-loader": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-2.1.1.tgz", + "integrity": "sha512-OcKJU/lt232vl1P9EEDamhoO9iKY3tIjY5GU+XDLblAykTdgs6Ux9P1hTHve8nFKy5KPpOXOsVI/hIwi3841+w==", + "dev": true, + "requires": { + "camelcase": "^5.2.0", + "icss-utils": "^4.1.0", + "loader-utils": "^1.2.3", + "normalize-path": "^3.0.0", + "postcss": "^7.0.14", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^2.0.6", + "postcss-modules-scope": "^2.1.0", + "postcss-modules-values": "^2.0.0", + "postcss-value-parser": "^3.3.0", + "schema-utils": "^1.0.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "dev": true, + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "dev": true + }, + "mime-types": { + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "dev": true, + "requires": { + "mime-db": "1.51.0" + } + }, + "node-sass": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.14.1.tgz", + "integrity": "sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g==", + "dev": true, + "requires": { + "async-foreach": "^0.1.3", + "chalk": "^1.1.1", + "cross-spawn": "^3.0.0", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "in-publish": "^2.0.0", + "lodash": "^4.17.15", + "meow": "^3.7.0", + "mkdirp": "^0.5.1", + "nan": "^2.13.2", + "node-gyp": "^3.8.0", + "npmlog": "^4.0.0", + "request": "^2.88.0", + "sass-graph": "2.2.5", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + } + }, + "postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + } + }, + "postcss-modules-local-by-default": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-2.0.6.tgz", + "integrity": "sha512-oLUV5YNkeIBa0yQl7EYnxMgy4N6noxmiwZStaEJUSe2xPMcdNc8WmBQuQCx18H5psYbVxz8zoHk0RAAYZXP9gA==", + "dev": true, + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0", + "postcss-value-parser": "^3.3.1" + } + }, + "postcss-modules-scope": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", + "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", + "dev": true, + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + } + }, + "postcss-modules-values": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-2.0.0.tgz", + "integrity": "sha512-Ki7JZa7ff1N3EIMlPnGTZfUMe69FFwiQPnVSXC9mnn3jozCRBYIxiZd44yJOV2AmabOo4qFf8s0dC/+lweG7+w==", + "dev": true, + "requires": { + "icss-replace-symbols": "^1.1.0", + "postcss": "^7.0.6" + } + }, + "postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "sass-graph": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz", + "integrity": "sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==", + "dev": true, + "requires": { + "glob": "^7.0.0", + "lodash": "^4.0.0", + "scss-tokenizer": "^0.2.3", + "yargs": "^13.3.2" + } + }, + "sass-loader": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.3.1.tgz", + "integrity": "sha512-tuU7+zm0pTCynKYHpdqaPpe+MMTQ76I9TPZ7i4/5dZsigE350shQWe5EZNl5dBidM49TPET75tNqRbcsUZWeNA==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "loader-utils": "^1.0.1", + "neo-async": "^2.5.0", + "pify": "^4.0.1", + "semver": "^6.3.0" + } + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "style-loader": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.23.1.tgz", + "integrity": "sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "schema-utils": "^1.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", "dev": true - } - } - }, - "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", - "dev": true - }, - "lodash._baseassign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", - "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", - "dev": true, - "requires": { - "lodash._basecopy": "^3.0.0", - "lodash.keys": "^3.0.0" - } - }, - "lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", - "dev": true - }, - "lodash._bindcallback": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", - "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", - "dev": true - }, - "lodash._createassigner": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", - "integrity": "sha1-g4pbri/aymOsIt7o4Z+k5taXCxE=", - "dev": true, - "requires": { - "lodash._bindcallback": "^3.0.0", - "lodash._isiterateecall": "^3.0.0", - "lodash.restparam": "^3.0.0" - } - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", - "dev": true - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", - "dev": true - }, - "lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" - }, - "lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", - "dev": true - }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true - }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true - }, - "lodash.defaults": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-3.1.2.tgz", - "integrity": "sha1-xzCLGNv4vJNy1wGnNJPGEZK9Liw=", - "dev": true, - "requires": { - "lodash.assign": "^3.0.0", - "lodash.restparam": "^3.0.0" - }, - "dependencies": { - "lodash.assign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", - "integrity": "sha1-POnwI0tLIiPilrj6CsH+6OvKZPo=", + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "dev": true, "requires": { - "lodash._baseassign": "^3.0.0", - "lodash._createassigner": "^3.0.0", - "lodash.keys": "^3.0.0" + "psl": "^1.1.28", + "punycode": "^2.1.1" } - } - } - }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", - "dev": true - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", - "dev": true - }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true, - "requires": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", - "dev": true - }, - "lodash.mergewith": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", - "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==", - "dev": true - }, - "lodash.restparam": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", - "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", - "dev": true - }, - "lodash.tail": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz", - "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=", - "dev": true - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, - "loglevel": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz", - "integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po=", - "dev": true - }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true - }, - "loose-envify": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", - "requires": { - "js-tokens": "^3.0.0" - } - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "dev": true - }, - "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "macaddress": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/macaddress/-/macaddress-0.2.8.tgz", - "integrity": "sha1-WQTcU3w57G2+/q6QIycTX6hRHxI=", - "dev": true - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "dev": true } } }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "material-design-icons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/material-design-icons/-/material-design-icons-3.0.1.tgz", - "integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78=", - "dev": true - }, - "materialize-css": { - "version": "1.0.0-rc.2", - "resolved": "https://registry.npmjs.org/materialize-css/-/materialize-css-1.0.0-rc.2.tgz", - "integrity": "sha512-FuQmSyq4Qv0ov7A2qXw0E6/jbQzSWx2P1pg2/XQDYTkkSc/GyiFAxu3fw9zgShwuTvyumEiw5jkxQWT9siqMBQ==" - }, - "math-expression-evaluator": { - "version": "1.2.17", - "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", - "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=", - "dev": true - }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, - "mem": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", - "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "memfs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.0.tgz", + "integrity": "sha512-o/RfP0J1d03YwsAxyHxAYs2kyJp55AFkMazlFAZFR2I2IXkxiUTXRabJ6RmNNCQ83LAD2jy52Khj0m3OffpNdA==", "dev": true, "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" + "fs-monkey": "1.0.3" } }, "meow": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, "requires": { "camelcase-keys": "^2.0.0", "decamelize": "^1.1.2", @@ -6905,13 +5075,6 @@ "read-pkg-up": "^1.0.1", "redent": "^1.0.0", "trim-newlines": "^1.0.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } } }, "merge-descriptors": { @@ -6920,46 +5083,37 @@ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", "dev": true }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "dev": true, - "requires": { - "arr-diff": "^2.0.0", - "array-unique": "^0.2.1", - "braces": "^1.8.2", - "expand-brackets": "^0.1.4", - "extglob": "^0.3.1", - "filename-regex": "^2.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.1", - "kind-of": "^3.0.2", - "normalize-path": "^2.0.1", - "object.omit": "^2.0.0", - "parse-glob": "^3.0.4", - "regex-cache": "^0.4.2" - } - }, - "miller-rabin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.0.tgz", - "integrity": "sha1-SmL7HUKTPAVYOYL0xxb2+55sbT0=", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "dev": true, "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" + "braces": "^3.0.1", + "picomatch": "^2.2.3" } }, "mime": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", - "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", "dev": true }, "mime-db": { @@ -6975,420 +5129,428 @@ "mime-db": "~1.27.0" } }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "minimalistic-assert": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", - "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=", - "dev": true - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true - }, - "minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=", - "requires": { - "brace-expansion": "^1.0.0" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "mini-css-extract-plugin": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.2.tgz", + "integrity": "sha512-a3Y4of27Wz+mqK3qrcd3VhYz6cU0iW5x3Sgvqzbj+XmlrSizmvu8QQMl5oMYJjgHOC4iyt+w7l4umP+dQeW3bw==", "dev": true, "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" + "loader-utils": "^1.1.0", + "normalize-url": "1.9.1", + "schema-utils": "^1.0.0", + "webpack-sources": "^1.1.0" }, "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "requires": { - "is-plain-object": "^2.0.4" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", - "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=" - }, - "multicast-dns": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", - "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", - "dev": true, - "requires": { - "dns-packet": "^1.3.1", - "thunky": "^1.0.2" - } - }, - "multicast-dns-service-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", - "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", - "dev": true - }, - "nan": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.5.1.tgz", - "integrity": "sha1-1bAWkSUzJql6K77p5hxV2NYDUeI=", - "dev": true, - "optional": true - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true + }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } } } }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", - "dev": true - }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" - }, - "node-forge": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", - "integrity": "sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ==", + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", "dev": true }, - "node-gyp": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", - "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "^2.87.0", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "mocha": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz", + "integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==", + "dev": true, + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.2", + "debug": "4.3.2", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.7", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "3.0.4", + "ms": "2.1.3", + "nanoid": "3.1.25", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.1.5", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" }, "dependencies": { - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" + "color-convert": "^2.0.1" } }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { - "delayed-stream": "~1.0.0" + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } } }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, - "har-validator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", - "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "requires": { - "ajv": "^5.3.0", - "har-schema": "^2.0.0" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" } }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", "dev": true, "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" + "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" } }, - "mime-db": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", - "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==", + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "mime-types": { - "version": "2.1.19", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", - "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "requires": { - "mime-db": "~1.35.0" + "p-locate": "^5.0.0" } }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "nanoid": { + "version": "3.1.25", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", + "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", "dev": true }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } }, - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" } }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", "dev": true } } }, - "node-libs-browser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.0.0.tgz", - "integrity": "sha1-o6WeyXAkmFtG6Vg3lkb5bEthZkY=", - "dev": true, - "requires": { - "assert": "^1.1.1", - "browserify-zlib": "^0.1.4", - "buffer": "^4.3.0", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.11.0", - "domain-browser": "^1.1.1", - "events": "^1.0.0", - "https-browserify": "0.0.1", - "os-browserify": "^0.2.0", - "path-browserify": "0.0.0", - "process": "^0.11.0", - "punycode": "^1.2.4", - "querystring-es3": "^0.2.0", - "readable-stream": "^2.0.5", - "stream-browserify": "^2.0.1", - "stream-http": "^2.3.1", - "string_decoder": "^0.10.25", - "timers-browserify": "^2.0.2", - "tty-browserify": "0.0.0", - "url": "^0.11.0", - "util": "^0.10.3", - "vm-browserify": "0.0.4" - } - }, - "node-pre-gyp": { - "version": "0.6.39", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz", - "integrity": "sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==", - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "hawk": "3.1.3", - "mkdirp": "^0.5.1", - "nopt": "^4.0.1", - "npmlog": "^4.0.2", - "rc": "^1.1.7", - "request": "2.81.0", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^2.2.1", - "tar-pack": "^3.4.0" + "most": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/most/-/most-1.9.0.tgz", + "integrity": "sha512-M7yHMcMGaclzEL6eg8Yh8PlAsaWfL/oSThF4+ZuWKM5CKXcbzmLh+qESwgZFzMKHJ+iVJwb28yFvDEOobI653w==", + "requires": { + "@most/multicast": "^1.2.5", + "@most/prelude": "^1.4.0", + "globalthis": "^1.0.1", + "symbol-observable": "^2.0.3" }, "dependencies": { - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } + "symbol-observable": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz", + "integrity": "sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==" } } }, - "node-sass": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.3.tgz", - "integrity": "sha512-XzXyGjO+84wxyH7fV6IwBOTrEBe2f0a6SBze9QWWYR/cL74AcQUks2AsqcCZenl/Fp/JVbuEaLpgrLtocwBUww==", + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "dev": true, + "requires": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "nan": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", + "dev": true + }, + "nanoid": { + "version": "3.1.30", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", + "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==", + "dev": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node-fetch": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", + "dev": true + }, + "node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", "dev": true, "requires": { - "async-foreach": "^0.1.3", - "chalk": "^1.1.1", - "cross-spawn": "^3.0.0", - "gaze": "^1.0.0", - "get-stdin": "^4.0.1", + "fstream": "^1.0.0", "glob": "^7.0.3", - "in-publish": "^2.0.0", - "lodash.assign": "^4.2.0", - "lodash.clonedeep": "^4.3.2", - "lodash.mergewith": "^4.6.0", - "meow": "^3.7.0", - "mkdirp": "^0.5.1", - "nan": "^2.10.0", - "node-gyp": "^3.8.0", - "npmlog": "^4.0.0", - "request": "2.87.0", - "sass-graph": "^2.2.4", - "stdout-stream": "^1.4.0", - "true-case-path": "^1.0.2" + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" }, "dependencies": { "ajv": { @@ -7415,14 +5577,19 @@ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", "dev": true }, - "cross-spawn": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", - "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", "dev": true, "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" + "delayed-stream": "~1.0.0" } }, "extend": { @@ -7438,12 +5605,12 @@ "dev": true }, "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", + "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", "dev": true, "requires": { - "ajv": "^5.1.0", + "ajv": "^5.3.0", "har-schema": "^2.0.0" } }, @@ -7473,10 +5640,10 @@ "mime-db": "~1.35.0" } }, - "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", "dev": true }, "performance-now": { @@ -7492,1229 +5659,569 @@ "dev": true }, "request": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", "dev": true, "requires": { "aws-sign2": "~0.7.0", - "aws4": "^1.6.0", + "aws4": "^1.8.0", "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.1", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", "forever-agent": "~0.6.1", - "form-data": "~2.3.1", - "har-validator": "~5.0.3", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.17", - "oauth-sign": "~0.8.2", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", "performance-now": "^2.1.0", - "qs": "~6.5.1", - "safe-buffer": "^5.1.1", - "tough-cookie": "~2.3.3", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", "tunnel-agent": "^0.6.0", - "uuid": "^3.1.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", - "dev": true, - "requires": { - "punycode": "^1.4.1" - } - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "dev": true - } - } - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", - "requires": { - "hosted-git-info": "^2.1.4", - "is-builtin-module": "^1.0.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", - "dev": true - }, - "normalize-url": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", - "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", - "dev": true, - "requires": { - "object-assign": "^4.0.1", - "prepend-http": "^1.0.0", - "query-string": "^4.1.0", - "sort-keys": "^1.0.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "nugget": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nugget/-/nugget-2.0.1.tgz", - "integrity": "sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA=", - "requires": { - "debug": "^2.1.3", - "minimist": "^1.1.0", - "pretty-bytes": "^1.0.2", - "progress-stream": "^1.1.0", - "request": "^2.45.0", - "single-line-log": "^1.1.2", - "throttleit": "0.0.2" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "num2fraction": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", - "dev": true - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "object-keys": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", - "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=" - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "dev": true, - "requires": { - "for-own": "^0.1.4", - "is-extendable": "^0.1.1" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - }, - "dependencies": { - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", - "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "opn": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", - "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", - "dev": true, - "requires": { - "is-wsl": "^1.1.0" - } - }, - "original": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", - "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", - "dev": true, - "requires": { - "url-parse": "^1.4.3" - } - }, - "os-browserify": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.2.1.tgz", - "integrity": "sha1-Y/xMzuXS13Y9Jrv4YBB45sLgBE8=", - "dev": true - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", - "requires": { - "lcid": "^1.0.0" - } - }, - "os-shim": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", - "integrity": "sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=" - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", - "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" + "uuid": "^3.3.2" + }, + "dependencies": { + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + } + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + } } }, - "p-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", - "dev": true - }, - "p-try": { + "node-modules-regexp": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", "dev": true }, - "package-json": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", - "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", - "dev": true, - "requires": { - "got": "^6.7.1", - "registry-auth-token": "^3.0.1", - "registry-url": "^3.0.3", - "semver": "^5.1.0" - } - }, - "pako": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=", + "node-releases": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", + "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", "dev": true }, - "parse-asn1": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", - "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=", + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", "dev": true, "requires": { - "asn1.js": "^4.0.0", - "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3" + "abbrev": "1" } }, - "parse-color": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-color/-/parse-color-1.0.0.tgz", - "integrity": "sha1-e3SLlag/A/FqlPU15S1/PZRlhhk=", + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, "requires": { - "color-convert": "~0.5.0" + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" }, "dependencies": { - "color-convert": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", - "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=", + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true } } }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "dev": true, - "requires": { - "glob-base": "^0.3.0", - "is-dotfile": "^1.0.0", - "is-extglob": "^1.0.0", - "is-glob": "^2.0.0" - } + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "dev": true, "requires": { - "error-ex": "^1.2.0" + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" } }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", - "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, "requires": { - "pinkie-promise": "^2.0.0" + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" } }, - "path-is-absolute": { + "number-is-nan": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pbkdf2": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.12.tgz", - "integrity": "sha1-vjZ4XFBn6kjYBv+SMojF91C2uKI=", - "dev": true, - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" - }, - "performance-now": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", - "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "requires": { - "pinkie": "^2.0.0" - } - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - } - } + "object-inspect": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==" }, - "plist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.1.tgz", - "integrity": "sha512-GpgvHHocGRyQm74b6FWEZZVRroHKE1I0/BTjAmySaohK+cUn+hZpbqXkc3KWgW3gQYkqcQej35FohcT0FRlkRQ==", + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", "dev": true, "requires": { - "base64-js": "^1.2.3", - "xmlbuilder": "^9.0.7", - "xmldom": "0.1.x" - }, - "dependencies": { - "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==", - "dev": true - }, - "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", - "dev": true - } + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" } }, - "portfinder": { - "version": "1.0.17", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.17.tgz", - "integrity": "sha512-syFcRIRzVI1BoEFOCaAiizwDolh1S1YXSodsVhncbhjzjZQulhczNRbqnUl9N31Q4dKGOXsNDqxC2BWBgSMqeQ==", + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", "dev": true, "requires": { - "async": "^1.5.2", - "debug": "^2.2.0", - "mkdirp": "0.5.x" + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" }, "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true } } }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, - "postcss": { - "version": "5.2.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.16.tgz", - "integrity": "sha1-cysxAAAPn/g3mkilODntCXN2rVc=", - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - }, - "dependencies": { - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=" - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "requires": { - "has-flag": "^1.0.0" - } - } - } - }, - "postcss-calc": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-5.3.1.tgz", - "integrity": "sha1-d7rnypKK2FcW4v2kLyYb98HWW14=", - "dev": true, - "requires": { - "postcss": "^5.0.2", - "postcss-message-helpers": "^2.0.0", - "reduce-css-calc": "^1.2.6" - } - }, - "postcss-colormin": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-2.2.2.tgz", - "integrity": "sha1-ZjFBfV8OkJo9fsJrJMio0eT5bks=", - "dev": true, - "requires": { - "colormin": "^1.0.5", - "postcss": "^5.0.13", - "postcss-value-parser": "^3.2.3" - } - }, - "postcss-convert-values": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz", - "integrity": "sha1-u9hZPFwf0uPRwyK7kl3K6Nrk1i0=", - "dev": true, - "requires": { - "postcss": "^5.0.11", - "postcss-value-parser": "^3.1.2" - } - }, - "postcss-discard-comments": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz", - "integrity": "sha1-vv6J+v1bPazlzM5Rt2uBUUvgDj0=", - "dev": true, - "requires": { - "postcss": "^5.0.14" - } - }, - "postcss-discard-duplicates": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz", - "integrity": "sha1-uavye4isGIFYpesSq8riAmO5GTI=", - "dev": true, - "requires": { - "postcss": "^5.0.4" - } - }, - "postcss-discard-empty": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz", - "integrity": "sha1-0rS9nVztXr2Nyt52QMfXzX9PkrU=", - "dev": true, - "requires": { - "postcss": "^5.0.14" - } - }, - "postcss-discard-overridden": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz", - "integrity": "sha1-ix6vVU9ob7KIzYdMVWZ7CqNmjVg=", - "dev": true, - "requires": { - "postcss": "^5.0.16" - } - }, - "postcss-discard-unused": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz", - "integrity": "sha1-vOMLLMWR/8Y0Mitfs0ZLbZNPRDM=", - "dev": true, - "requires": { - "postcss": "^5.0.14", - "uniqs": "^2.0.0" - } - }, - "postcss-filter-plugins": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz", - "integrity": "sha1-bYWGJTTXNaxCDkqFgG4fXUKG2Ew=", + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", "dev": true, "requires": { - "postcss": "^5.0.4", - "uniqid": "^4.0.0" - } - }, - "postcss-load-config": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz", - "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=", - "requires": { - "cosmiconfig": "^2.1.0", - "object-assign": "^4.1.0", - "postcss-load-options": "^1.2.0", - "postcss-load-plugins": "^2.3.0" + "ee-first": "1.1.1" } }, - "postcss-load-options": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postcss-load-options/-/postcss-load-options-1.2.0.tgz", - "integrity": "sha1-sJixVZ3awt8EvAuzdfmaXP4rbYw=", - "requires": { - "cosmiconfig": "^2.1.0", - "object-assign": "^4.1.0" - } + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true }, - "postcss-load-plugins": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz", - "integrity": "sha1-dFdoEWWZrKLwCfrUJrABdQSdjZI=", + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "requires": { - "cosmiconfig": "^2.1.1", - "object-assign": "^4.1.0" + "wrappy": "1" } }, - "postcss-loader": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-1.3.3.tgz", - "integrity": "sha1-piHqH6KQYqg5cqRvVEhncTAZFus=", + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, "requires": { - "loader-utils": "^1.0.2", - "object-assign": "^4.1.1", - "postcss": "^5.2.15", - "postcss-load-config": "^1.2.0" + "mimic-fn": "^2.1.0" + }, + "dependencies": { + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + } } }, - "postcss-merge-idents": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz", - "integrity": "sha1-TFUwMTwI4dWzu/PSu8dH4njuonA=", + "open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", "dev": true, "requires": { - "has": "^1.0.1", - "postcss": "^5.0.10", - "postcss-value-parser": "^3.1.1" + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" } }, - "postcss-merge-longhand": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz", - "integrity": "sha1-I9kM0Sewp3mUkVMyc5A0oaTz1lg=", + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "dev": true, "requires": { - "postcss": "^5.0.4" + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" } }, - "postcss-merge-rules": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz", - "integrity": "sha1-0d9d+qexrMO+VT8OnhDofGG19yE=", + "p-event": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", + "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", "dev": true, "requires": { - "browserslist": "^1.5.2", - "caniuse-api": "^1.5.2", - "postcss": "^5.0.4", - "postcss-selector-parser": "^2.2.2", - "vendors": "^1.0.0" + "p-timeout": "^3.1.0" } }, - "postcss-message-helpers": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz", - "integrity": "sha1-pPL0+rbk/gAvCu0ABHjN9S+bpg4=", + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "dev": true }, - "postcss-minify-font-values": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz", - "integrity": "sha1-S1jttWZB66fIR0qzUmyv17vey2k=", - "dev": true, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "requires": { - "object-assign": "^4.0.1", - "postcss": "^5.0.4", - "postcss-value-parser": "^3.0.2" + "p-try": "^2.0.0" } }, - "postcss-minify-gradients": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz", - "integrity": "sha1-Xb2hE3NwP4PPtKPqOIHY11/15uE=", - "dev": true, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "requires": { - "postcss": "^5.0.12", - "postcss-value-parser": "^3.3.0" + "p-limit": "^2.2.0" } }, - "postcss-minify-params": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz", - "integrity": "sha1-rSzgcTc7lDs9kwo/pZo1jCjW8fM=", + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, "requires": { - "alphanum-sort": "^1.0.1", - "postcss": "^5.0.2", - "postcss-value-parser": "^3.0.2", - "uniqs": "^2.0.0" + "aggregate-error": "^3.0.0" } }, - "postcss-minify-selectors": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz", - "integrity": "sha1-ssapjAByz5G5MtGkllCBFDEXNb8=", + "p-retry": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.1.tgz", + "integrity": "sha512-e2xXGNhZOZ0lfgR9kL34iGlU8N/KO0xZnQxVEwdeOvpqNDQfdnxIYizvWtK8RglUa3bGqI8g0R/BdfzLMxRkiA==", "dev": true, "requires": { - "alphanum-sort": "^1.0.2", - "has": "^1.0.1", - "postcss": "^5.0.14", - "postcss-selector-parser": "^2.0.0" + "@types/retry": "^0.12.0", + "retry": "^0.13.1" } }, - "postcss-modules-extract-imports": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.1.0.tgz", - "integrity": "sha1-thTJcgvmgW6u41+zpfqh26agXds=", + "p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", "dev": true, "requires": { - "postcss": "^6.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.0.1.tgz", - "integrity": "sha512-Mp+FXEI+FrwY/XYV45b2YD3E8i3HwnEAoFcM0qlZzq/RZ9RwWitt2Y/c7cqRAz70U7hfekqx6qNYthuKFO6K0g==", - "dev": true, - "requires": { - "ansi-styles": "^3.1.0", - "escape-string-regexp": "^1.0.5", - "supports-color": "^4.0.0" - } - }, - "postcss": { - "version": "6.0.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.8.tgz", - "integrity": "sha512-G6WnRmdTt2jvJvY+aY+M0AO4YlbxE+slKPZb+jG2P2U9Tyxi3h1fYZ/DgiFU6DC6bv3XIEJoZt+f/kNh8BrWFw==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "source-map": "^0.5.6", - "supports-color": "^4.2.0" - } - }, - "supports-color": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.1.tgz", - "integrity": "sha512-qxzYsob3yv6U+xMzPrv170y8AwGP7i74g+pbixCfD6rgso8BscLT2qXIuz6TpOaiJZ3mFgT5O9lyT9nMU4LfaA==", - "dev": true, - "requires": { - "has-flag": "^2.0.0" - } - } + "p-finally": "^1.0.0" } }, - "postcss-modules-local-by-default": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz", - "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=", + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha1-juqz5U+laSD+Fro493+iGqzC104=" + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", "dev": true, "requires": { - "css-selector-tokenizer": "^0.7.0", - "postcss": "^6.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.0.1.tgz", - "integrity": "sha512-Mp+FXEI+FrwY/XYV45b2YD3E8i3HwnEAoFcM0qlZzq/RZ9RwWitt2Y/c7cqRAz70U7hfekqx6qNYthuKFO6K0g==", - "dev": true, - "requires": { - "ansi-styles": "^3.1.0", - "escape-string-regexp": "^1.0.5", - "supports-color": "^4.0.0" - } - }, - "postcss": { - "version": "6.0.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.8.tgz", - "integrity": "sha512-G6WnRmdTt2jvJvY+aY+M0AO4YlbxE+slKPZb+jG2P2U9Tyxi3h1fYZ/DgiFU6DC6bv3XIEJoZt+f/kNh8BrWFw==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "source-map": "^0.5.6", - "supports-color": "^4.2.0" - } - }, - "supports-color": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.1.tgz", - "integrity": "sha512-qxzYsob3yv6U+xMzPrv170y8AwGP7i74g+pbixCfD6rgso8BscLT2qXIuz6TpOaiJZ3mFgT5O9lyT9nMU4LfaA==", - "dev": true, - "requires": { - "has-flag": "^2.0.0" - } - } + "error-ex": "^1.2.0" } }, - "postcss-modules-scope": { + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "path-type": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", - "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", "dev": true, "requires": { - "css-selector-tokenizer": "^0.7.0", - "postcss": "^6.0.1" + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" }, "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.0.1.tgz", - "integrity": "sha512-Mp+FXEI+FrwY/XYV45b2YD3E8i3HwnEAoFcM0qlZzq/RZ9RwWitt2Y/c7cqRAz70U7hfekqx6qNYthuKFO6K0g==", - "dev": true, - "requires": { - "ansi-styles": "^3.1.0", - "escape-string-regexp": "^1.0.5", - "supports-color": "^4.0.0" - } - }, - "postcss": { - "version": "6.0.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.8.tgz", - "integrity": "sha512-G6WnRmdTt2jvJvY+aY+M0AO4YlbxE+slKPZb+jG2P2U9Tyxi3h1fYZ/DgiFU6DC6bv3XIEJoZt+f/kNh8BrWFw==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "source-map": "^0.5.6", - "supports-color": "^4.2.0" - } - }, - "supports-color": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.1.tgz", - "integrity": "sha512-qxzYsob3yv6U+xMzPrv170y8AwGP7i74g+pbixCfD6rgso8BscLT2qXIuz6TpOaiJZ3mFgT5O9lyT9nMU4LfaA==", - "dev": true, - "requires": { - "has-flag": "^2.0.0" - } + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true } } }, - "postcss-modules-values": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", - "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", - "dev": true, - "requires": { - "icss-replace-symbols": "^1.1.0", - "postcss": "^6.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.0.1.tgz", - "integrity": "sha512-Mp+FXEI+FrwY/XYV45b2YD3E8i3HwnEAoFcM0qlZzq/RZ9RwWitt2Y/c7cqRAz70U7hfekqx6qNYthuKFO6K0g==", - "dev": true, - "requires": { - "ansi-styles": "^3.1.0", - "escape-string-regexp": "^1.0.5", - "supports-color": "^4.0.0" - } - }, - "postcss": { - "version": "6.0.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.8.tgz", - "integrity": "sha512-G6WnRmdTt2jvJvY+aY+M0AO4YlbxE+slKPZb+jG2P2U9Tyxi3h1fYZ/DgiFU6DC6bv3XIEJoZt+f/kNh8BrWFw==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "source-map": "^0.5.6", - "supports-color": "^4.2.0" - } - }, - "supports-color": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.1.tgz", - "integrity": "sha512-qxzYsob3yv6U+xMzPrv170y8AwGP7i74g+pbixCfD6rgso8BscLT2qXIuz6TpOaiJZ3mFgT5O9lyT9nMU4LfaA==", - "dev": true, - "requires": { - "has-flag": "^2.0.0" - } - } - } + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, - "postcss-normalize-charset": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz", - "integrity": "sha1-757nEhLX/nWceO0WL2HtYrXLk/E=", + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", "dev": true, "requires": { - "postcss": "^5.0.5" + "pinkie": "^2.0.0" } }, - "postcss-normalize-url": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz", - "integrity": "sha1-EI90s/L82viRov+j6kWSJ5/HgiI=", + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", "dev": true, "requires": { - "is-absolute-url": "^2.0.0", - "normalize-url": "^1.4.0", - "postcss": "^5.0.14", - "postcss-value-parser": "^3.2.3" + "node-modules-regexp": "^1.0.0" } }, - "postcss-ordered-values": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz", - "integrity": "sha1-7sbCpntsQSqNsgQud/6NpD+VwR0=", - "dev": true, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "requires": { - "postcss": "^5.0.4", - "postcss-value-parser": "^3.0.1" + "find-up": "^4.0.0" } }, - "postcss-reduce-idents": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz", - "integrity": "sha1-wsbSDMlYKE9qv75j92Cb9AkFmtM=", + "portfinder": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", + "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", "dev": true, "requires": { - "postcss": "^5.0.4", - "postcss-value-parser": "^3.0.2" + "async": "^2.6.2", + "debug": "^3.1.1", + "mkdirp": "^0.5.5" } }, - "postcss-reduce-initial": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz", - "integrity": "sha1-aPgGlfBF0IJjqHmtJA343WT2ROo=", + "postcss": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.1.tgz", + "integrity": "sha512-WqLs/TTzXdG+/A4ZOOK9WDZiikrRaiA+eoEb/jz2DT9KUhMNHgP7yKPO8vwi62ZCsb703Gwb7BMZwDzI54Y2Ag==", "dev": true, "requires": { - "postcss": "^5.0.4" + "nanoid": "^3.1.30", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.1" } }, - "postcss-reduce-transforms": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz", - "integrity": "sha1-/3b02CEkN7McKYpC0uFEQCV3GuE=", + "postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true + }, + "postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", "dev": true, "requires": { - "has": "^1.0.1", - "postcss": "^5.0.8", - "postcss-value-parser": "^3.0.1" + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" } }, - "postcss-selector-parser": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz", - "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=", + "postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", "dev": true, "requires": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" + "postcss-selector-parser": "^6.0.4" } }, - "postcss-svgo": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-2.1.6.tgz", - "integrity": "sha1-tt8YqmE7Zm4TPwittSGcJoSsEI0=", + "postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", "dev": true, "requires": { - "is-svg": "^2.0.0", - "postcss": "^5.0.14", - "postcss-value-parser": "^3.2.3", - "svgo": "^0.7.0" + "icss-utils": "^5.0.0" } }, - "postcss-unique-selectors": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz", - "integrity": "sha1-mB1X0p3csz57Hf4f1DuGSfkzyh0=", + "postcss-selector-parser": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", + "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", "dev": true, "requires": { - "alphanum-sort": "^1.0.1", - "postcss": "^5.0.4", - "uniqs": "^2.0.0" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" } }, "postcss-value-parser": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz", - "integrity": "sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", "dev": true }, - "postcss-zindex": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-2.2.0.tgz", - "integrity": "sha1-0hCd3AVbka9n/EyzsCWUZjnSryI=", - "dev": true, - "requires": { - "has": "^1.0.1", - "postcss": "^5.0.4", - "uniqs": "^2.0.0" - } - }, "prepend-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", "dev": true }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", - "dev": true - }, - "pretty-bytes": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz", - "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=", - "requires": { - "get-stdin": "^4.0.1", - "meow": "^3.1.0" - } - }, - "private": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.7.tgz", - "integrity": "sha1-aM5eih7woju1cMwoU3tTMqumPvE=", - "dev": true - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true - }, "process-nextick-args": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "progress-stream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-1.2.0.tgz", - "integrity": "sha1-LNPP6jO6OonJwSHsM0er6asSX3c=", - "requires": { - "speedometer": "~0.1.2", - "through2": "~0.2.3" - } + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" }, "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz", + "integrity": "sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==", "requires": { - "asap": "~2.0.3" + "asap": "~2.0.6" } }, "proxy-addr": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", - "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.8.0" + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "dependencies": { + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + } } }, - "prr": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz", - "integrity": "sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=", - "dev": true - }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -8724,42 +6231,23 @@ "psl": { "version": "1.1.29", "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", - "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==", - "dev": true - }, - "public-encrypt": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", - "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1" - } + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "q": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.0.tgz", - "integrity": "sha1-3QG6ydBtMObyGa7LglPunr3DCPE=", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", "dev": true }, "qs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "query-string": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.2.tgz", - "integrity": "sha1-7A/XZfWKUAMaOWjCQxOG+JR6XN0=", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", "dev": true, "requires": { "object-assign": "^4.1.0", @@ -8772,16 +6260,10 @@ "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", "dev": true }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", - "dev": true - }, - "querystringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz", - "integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw==", + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, "quicktask": { @@ -8790,207 +6272,59 @@ "integrity": "sha512-b3w19IEXnt5auacLAbePVsqPyVQUwmuhJQrrWnVhm4pP8PAMg2U9vFHbAD9XYXXbMDjdLJs0x5NLqwTV8uFK4g==" }, "ramda": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.25.0.tgz", - "integrity": "sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ==" - }, - "randomatic": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", - "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", + "integrity": "sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==" }, "randombytes": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz", - "integrity": "sha512-8T7Zn1AhMsQ/HI1SjcCfT/t4ii3eAqco3yOcSzS4mozsOz69lHLsoMXmF9nZgnFanYscnSlUSgs8uZyKzpE6kg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "requires": { "safe-buffer": "^5.1.0" }, "dependencies": { "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true } } }, "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true }, "raw-body": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", "dev": true, "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", "unpipe": "1.0.0" }, "dependencies": { - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", "dev": true }, - "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", - "dev": true, - "requires": { - "depd": "1.1.1", - "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": ">= 1.3.1 < 2" - } - }, "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", - "dev": true - }, - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=", - "dev": true - } - } - }, - "rc": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.7.tgz", - "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", - "requires": { - "deep-extend": "^0.5.1", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "read-config-file": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-3.1.2.tgz", - "integrity": "sha512-QCATYzlYHvmWps/W/eP7rcKuhYRYZg5XKeXFxSJRIXvn+KSw1+Ntz2et1aBz5TrEpawGrxWZ7zBipj+/v0xwWQ==", - "dev": true, - "requires": { - "ajv": "^6.5.2", - "ajv-keywords": "^3.2.0", - "bluebird-lst": "^1.0.5", - "dotenv": "^6.0.0", - "dotenv-expand": "^4.2.0", - "fs-extra-p": "^4.6.1", - "js-yaml": "^3.12.0", - "json5": "^1.0.1", - "lazy-val": "^1.0.3" - }, - "dependencies": { - "ajv": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.3.tgz", - "integrity": "sha512-LqZ9wY+fx3UMiiPd741yB2pj3hhil+hQc8taf4o2QGRFpWgZ2V5C8HA165DY9sS3fJwsk7uT7ZlFEyC3Ig3lLg==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "js-yaml": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", - "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "requires": { - "minimist": "^1.2.0" + "safer-buffer": ">= 2.1.2 < 3" } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true } } }, @@ -8998,6 +6332,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, "requires": { "load-json-file": "^1.0.0", "normalize-package-data": "^2.3.2", @@ -9008,9 +6343,31 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, "requires": { "find-up": "^1.0.0", "read-pkg": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + } } }, "readable-stream": { @@ -9028,129 +6385,68 @@ } }, "readdirp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", - "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "minimatch": "^3.0.2", - "readable-stream": "^2.0.2", - "set-immediate-shim": "^1.0.1" + "picomatch": "^2.2.1" + } + }, + "rechoir": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", + "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", + "dev": true, + "requires": { + "resolve": "^1.9.0" } }, "redent": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, "requires": { "indent-string": "^2.1.0", "strip-indent": "^1.0.1" } }, - "reduce-css-calc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", - "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=", - "dev": true, - "requires": { - "balanced-match": "^0.4.2", - "math-expression-evaluator": "^1.2.14", - "reduce-function-call": "^1.0.1" - } - }, - "reduce-function-call": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.2.tgz", - "integrity": "sha1-WiAL+S4ON3UXUv5FsKszD9S2vpk=", - "dev": true, - "requires": { - "balanced-match": "^0.4.2" - } - }, "regenerate": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.3.2.tgz", - "integrity": "sha1-0ZQcZ7rUN+G+dkM63Vs4X5WxkmA=", - "dev": true - }, - "regenerator-runtime": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", - "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", "dev": true }, - "regenerator-transform": { - "version": "0.9.11", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.9.11.tgz", - "integrity": "sha1-On0GdSDLe3F2dp61/4aGkb7+EoM=", - "dev": true, - "requires": { - "babel-runtime": "^6.18.0", - "babel-types": "^6.19.0", - "private": "^0.1.6" - } - }, - "regex-cache": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.3.tgz", - "integrity": "sha1-mxpsNdTQ3871cRrmUejp09cRQUU=", - "dev": true, - "requires": { - "is-equal-shallow": "^0.1.3", - "is-primitive": "^2.0.0" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "regexpu-core": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", - "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", - "dev": true, - "requires": { - "regenerate": "^1.2.1", - "regjsgen": "^0.2.0", - "regjsparser": "^0.1.4" - } - }, - "registry-auth-token": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", - "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "regenerate-unicode-properties": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz", + "integrity": "sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA==", "dev": true, "requires": { - "rc": "^1.1.6", - "safe-buffer": "^5.0.1" + "regenerate": "^1.4.2" } }, - "registry-url": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", - "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "regexp.prototype.flags": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", + "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", "dev": true, "requires": { - "rc": "^1.0.1" + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" } }, "regjsgen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", - "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", + "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", "dev": true }, "regjsparser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", - "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.7.0.tgz", + "integrity": "sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ==", "dev": true, "requires": { "jsesc": "~0.5.0" @@ -9164,87 +6460,83 @@ } } }, - "remove-trailing-separator": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz", - "integrity": "sha1-abBi2XhyetFNxrVrpKt3L9jXBRE=", - "dev": true - }, - "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, "repeating": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, "requires": { "is-finite": "^1.0.0" } }, "request": { - "version": "2.81.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", - "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "requires": { - "aws-sign2": "~0.6.0", - "aws4": "^1.2.1", + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", "forever-agent": "~0.6.1", - "form-data": "~2.1.1", - "har-validator": "~4.2.1", - "hawk": "~3.1.3", - "http-signature": "~1.1.0", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.7", - "oauth-sign": "~0.8.1", - "performance-now": "^0.2.0", - "qs": "~6.4.0", - "safe-buffer": "^5.0.1", - "stringstream": "~0.0.4", - "tough-cookie": "~2.3.0", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", - "uuid": "^3.0.0" + "uuid": "^3.3.2" }, "dependencies": { "form-data": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.2.tgz", - "integrity": "sha1-icNTQAi5fq2ky7FX1Y9vXfAl6uQ=", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "requires": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.5", + "combined-stream": "^1.0.6", "mime-types": "^2.1.12" } + }, + "mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" + }, + "mime-types": { + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", + "requires": { + "mime-db": "1.51.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" } } }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-from-string": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz", - "integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=" + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true }, "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true }, "requires-port": { "version": "1.0.0", @@ -9252,132 +6544,63 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - }, - "resolve-pathname": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz", - "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==" - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "resolve-url-loader": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-1.6.1.tgz", - "integrity": "sha1-Sm4Dx03TjV393w9AS0ddbpACVjU=", - "dev": true, - "requires": { - "camelcase": "^1.2.1", - "convert-source-map": "^1.1.1", - "loader-utils": "^0.2.11", - "lodash.defaults": "^3.1.2", - "rework": "^1.0.1", - "rework-visit": "^1.0.0", - "source-map": "^0.1.43", - "urix": "^0.1.0" - }, - "dependencies": { - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true - }, - "loader-utils": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", - "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", - "dev": true, - "requires": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0", - "object-assign": "^4.0.1" - } - }, - "source-map": { - "version": "0.1.43", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", - "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "rework": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rework/-/rework-1.0.1.tgz", - "integrity": "sha1-MIBqhBNCtUUQqkEQhQzUhTQUSqc=", - "dev": true, - "requires": { - "convert-source-map": "^0.3.3", - "css": "^2.0.0" - }, - "dependencies": { - "convert-source-map": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz", - "integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=", - "dev": true - } - } - }, - "rework-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rework-visit/-/rework-visit-1.0.0.tgz", - "integrity": "sha1-mUWygD8hni96ygCtuLyfZA+ELJo=", - "dev": true - }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, "requires": { - "align-text": "^0.1.1" + "resolve-from": "^5.0.0" } }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve-pathname": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz", + "integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==" + }, + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, "rimraf": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", - "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" } }, - "ripemd160": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", - "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "requires": { - "hash-base": "^2.0.0", - "inherits": "^2.0.1" + "queue-microtask": "^1.2.2" } }, "rw": { @@ -9385,144 +6608,43 @@ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "requires": { + "tslib": "^1.9.0" + } + }, "safe-buffer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=" }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "sanitize-filename": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.1.tgz", - "integrity": "sha1-YS2hyWRz+gLczaktzVtKsWSmdyo=", - "dev": true, - "requires": { - "truncate-utf8-bytes": "^1.0.0" - } - }, - "sass-graph": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", - "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "sass": { + "version": "1.43.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.43.5.tgz", + "integrity": "sha512-WuNm+eAryMgQluL7Mbq9M4EruyGGMyal7Lu58FfnRMVWxgUzIvI7aSn60iNt3kn5yZBMR7G84fAGDcwqOF5JOg==", "dev": true, "requires": { - "glob": "^7.0.0", - "lodash": "^4.0.0", - "scss-tokenizer": "^0.2.3", - "yargs": "^7.0.0" - }, - "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - }, - "yargs": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", - "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", - "dev": true, - "requires": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^5.0.0" - } - }, - "yargs-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", - "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", - "dev": true, - "requires": { - "camelcase": "^3.0.0" - } - } + "chokidar": ">=3.0.0 <4.0.0" } }, "sass-loader": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-5.0.1.tgz", - "integrity": "sha1-SqgvRCQqvmLtj9zSjWMxo07yfC0=", - "dev": true, - "requires": { - "async": "^2.0.1", - "loader-utils": "^0.2.15", - "lodash.tail": "^4.1.1", - "pify": "^2.3.0" - }, - "dependencies": { - "loader-utils": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", - "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", - "dev": true, - "requires": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0", - "object-assign": "^4.0.1" - } - } - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "schema-utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.3.0.tgz", - "integrity": "sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8=", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-11.1.1.tgz", + "integrity": "sha512-fOCp/zLmj1V1WHDZbUbPgrZhA7HKXHEqkslzB+05U5K9SbSbcmH91C7QLW31AsXikxUMaxXRhhcqWZAxUMLDyA==", "dev": true, "requires": { - "ajv": "^5.0.0" - }, - "dependencies": { - "ajv": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.2.tgz", - "integrity": "sha1-R8aNaehvXZUxA7AHSpQw3GPaXjk=", - "dev": true, - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "json-schema-traverse": "^0.3.0", - "json-stable-stringify": "^1.0.1" - } - } + "klona": "^2.0.4", + "neo-async": "^2.6.2" } }, - "scss-loader": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/scss-loader/-/scss-loader-0.0.1.tgz", - "integrity": "sha1-6uAXueDzjBKlMtslwiC5Avs05nE=" - }, "scss-tokenizer": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", @@ -9551,32 +6673,23 @@ "dev": true }, "selfsigned": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.3.tgz", - "integrity": "sha512-vmZenZ+8Al3NLHkWnhBQ0x6BkML1eCP2xEi3JE+f3D9wW9fipD9NNJHYtE9XJM4TsPaHGZJIamrSI6MTg1dU2Q==", + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.11.tgz", + "integrity": "sha512-aVmbPOfViZqOZPgRBT0+3u4yZFHpmnIghLMlAcb5/xhp5ZtB/RVnKhz5vl2M32CLXAqR4kha9zfhNg0Lf/sxKA==", "dev": true, "requires": { - "node-forge": "0.7.5" + "node-forge": "^0.10.0" } }, "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" - }, - "semver-diff": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", - "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", - "dev": true, - "requires": { - "semver": "^5.0.3" - } + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" }, "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", "dev": true, "requires": { "debug": "2.6.9", @@ -9586,12 +6699,12 @@ "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" + "range-parser": "~1.2.1", + "statuses": "~1.5.0" }, "dependencies": { "debug": { @@ -9601,22 +6714,39 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } }, "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true } } }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", @@ -9641,19 +6771,31 @@ "ms": "2.0.0" } }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, "mime-db": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", - "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==", + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", "dev": true }, "mime-types": { - "version": "2.1.19", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", - "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "dev": true, "requires": { - "mime-db": "~1.35.0" + "mime-db": "1.51.0" } }, "ms": { @@ -9661,310 +6803,92 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true } } }, "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", "dev": true, "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" + "parseurl": "~1.3.3", + "send": "0.17.1" } }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "dev": true - }, - "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "dev": true }, "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", "dev": true }, - "sha.js": { - "version": "2.4.8", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.8.tgz", - "integrity": "sha1-NwaMLEdra69ALRSknGf1l5IfY08=", + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", "dev": true, "requires": { - "inherits": "^2.0.1" + "kind-of": "^6.0.2" } }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", "requires": { - "shebang-regex": "^1.0.0" + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" } }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "single-line-log": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-1.1.2.tgz", - "integrity": "sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q=", - "requires": { - "string-width": "^1.0.1" - } - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==", "dev": true }, "snabbdom": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-0.7.0.tgz", - "integrity": "sha512-LCg6lH9p2OD5n52SI4LlpYmDW2bscxsyN7rhnGJB/R3LQy/FdJfqNBM5aVST+zOfM4OdKFl8pxVUhjGsPtQA1w==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.1.0.tgz", + "integrity": "sha512-mcmPJMMKbkkPDPeCQ5D7RzqMHlLUyjl+OxOGblsutkzDbuYijCQGBOWJInjnWZ85DtoHdElrDTjA9g85s2YQ5Q==" }, "snabbdom-selector": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/snabbdom-selector/-/snabbdom-selector-3.0.0.tgz", - "integrity": "sha512-+FVGXrYEzCs83PNE4ZMqNEbnsM3T8lyJqOV7bilp9JKewlU6eKKfHDahrSJY9x8IJLwWdJqJBHydBuhad5EUiw==", - "requires": { - "tree-selector": "^1.1.0" - } - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - } - }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/snabbdom-selector/-/snabbdom-selector-5.0.0.tgz", + "integrity": "sha512-bJgpomacLM3jVhghsFxLcws8B6k+GVcnB/Af3avannj/zavIM1vLagvQHTFXzRVkf6nfxYaf/q58gekClDJoYA==", "requires": { - "hoek": "2.x.x" + "tree-selector": "^2.1.0" } }, "sockjs": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", - "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", - "dev": true, - "requires": { - "faye-websocket": "^0.10.0", - "uuid": "^3.0.1" - } - }, - "sockjs-client": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.4.tgz", - "integrity": "sha1-W6vjhrd15M8U51IJEUUmVAFsixI=", + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz", + "integrity": "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==", "dev": true, "requires": { - "debug": "^2.6.6", - "eventsource": "0.1.6", - "faye-websocket": "~0.11.0", - "inherits": "^2.0.1", - "json3": "^3.3.2", - "url-parse": "^1.1.8" + "faye-websocket": "^0.11.3", + "uuid": "^3.4.0", + "websocket-driver": "^0.7.4" }, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "faye-websocket": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz", - "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "dev": true } } @@ -9979,202 +6903,146 @@ } }, "source-list-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", - "integrity": "sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", "dev": true }, "source-map": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", - "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=" + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true }, - "source-map-resolve": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.3.1.tgz", - "integrity": "sha1-YQ9hIqRFuN1RU1oqcbeD38Ekh2E=", - "dev": true, - "requires": { - "atob": "~1.1.0", - "resolve-url": "~0.2.1", - "source-map-url": "~0.3.0", - "urix": "~0.1.0" - } + "source-map-js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.1.tgz", + "integrity": "sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==", + "dev": true }, - "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", "dev": true, "requires": { - "source-map": "^0.5.6" + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" } }, - "source-map-url": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.3.0.tgz", - "integrity": "sha1-fsrxO1e80J2opAxdJp2zN5nUqvk=", + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", "dev": true }, - "spawn-sync": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", - "integrity": "sha1-sAeZVX63+wyDdsKdROih6mfldHY=", - "requires": { - "concat-stream": "^1.4.7", - "os-shim": "^0.1.2" - } - }, - "spdx-correct": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", - "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, "requires": { - "spdx-license-ids": "^1.0.2" + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" } }, - "spdx-expression-parse": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", - "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=" - }, "spdx-license-ids": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", - "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=" + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz", + "integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==", + "dev": true }, "spdy": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-3.4.7.tgz", - "integrity": "sha1-Qv9B7OXMD5mjpsKKq7c/XDsDrLw=", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", "dev": true, "requires": { - "debug": "^2.6.8", - "handle-thing": "^1.2.5", + "debug": "^4.1.0", + "handle-thing": "^2.0.0", "http-deceiver": "^1.2.7", - "safe-buffer": "^5.0.1", "select-hose": "^2.0.0", - "spdy-transport": "^2.0.18" + "spdy-transport": "^3.0.0" }, "dependencies": { "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "2.1.2" } }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } }, "spdy-transport": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-2.1.0.tgz", - "integrity": "sha512-bpUeGpZcmZ692rrTiqf9/2EUakI6/kXX1Rpe0ib/DyOzbiexVfXkw6GnvI9hVGvIwVaUhkaBojjCZwLNRGQg1g==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", "dev": true, "requires": { - "debug": "^2.6.8", - "detect-node": "^2.0.3", + "debug": "^4.1.0", + "detect-node": "^2.0.4", "hpack.js": "^2.1.6", - "obuf": "^1.1.1", - "readable-stream": "^2.2.9", - "safe-buffer": "^5.0.1", - "wbuf": "^1.7.2" + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" }, "dependencies": { "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dev": true, "requires": { - "ms": "2.0.0" + "ms": "2.1.2" } }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - } + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "requires": { - "safe-buffer": "~5.1.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - } + "safe-buffer": "~5.2.0" } } } }, - "speedometer": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-0.1.4.tgz", - "integrity": "sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0=" - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, "sshpk": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.11.0.tgz", - "integrity": "sha1-LY1eu0pvqyj/ujf6YqkPSj6lnXc=", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -10182,8 +7050,8 @@ "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", - "jodid25519": "^1.0.0", "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "dependencies": { @@ -10194,37 +7062,10 @@ } } }, - "stat-mode": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-0.2.2.tgz", - "integrity": "sha1-5sgLYjEj19gM8TLOU480YokHJQI=", - "dev": true - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true }, "stdout-stream": { @@ -10236,29 +7077,6 @@ "readable-stream": "^2.0.1" } }, - "stream-browserify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", - "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", - "dev": true, - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "stream-http": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz", - "integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==", - "dev": true, - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.2.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - } - }, "strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", @@ -10269,6 +7087,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -10280,15 +7099,11 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -10297,107 +7112,176 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, "requires": { "is-utf8": "^0.2.0" } }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true }, "strip-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, "requires": { "get-stdin": "^4.0.1" } }, "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true }, "style-loader": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.13.2.tgz", - "integrity": "sha1-dFMzhM9pjHEEx5URULSXF63C87s=", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.3.0.tgz", + "integrity": "sha512-V7TCORko8rs9rIqkSrlMfkqA63DfoGBBJmK1kKGCcSi+BWb4cqz0SRsnp4l6rU5iwOEd0/2ePv68SV22VXon4Q==", "dev": true, "requires": { - "loader-utils": "^1.0.2" - } - }, - "sumchecker": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-1.3.1.tgz", - "integrity": "sha1-ebs7RFbdBPGOvbwNcDodHa7FEF0=", - "requires": { - "debug": "^2.2.0", - "es6-promise": "^4.0.5" + "loader-utils": "^2.0.0", + "schema-utils": "^2.7.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "loader-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", + "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } + } } }, "superagent": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.2.tgz", - "integrity": "sha512-gVH4QfYHcY3P0f/BZzavLreHW3T1v7hG9B+hpMQotGQqurOvhv87GcMCd6LWySmBuf+BDR44TQd0aISjVHLeNQ==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", "requires": { "component-emitter": "^1.2.0", "cookiejar": "^2.1.0", "debug": "^3.1.0", "extend": "^3.0.0", "form-data": "^2.3.1", - "formidable": "^1.1.1", + "formidable": "^1.2.0", "methods": "^1.1.1", "mime": "^1.4.1", "qs": "^6.5.1", - "readable-stream": "^2.0.5" + "readable-stream": "^2.3.5" }, "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } } } }, "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - }, - "svgo": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.7.2.tgz", - "integrity": "sha1-n1dyQTlSE1xv779Ar+ak+qiLS7U=", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { - "coa": "~1.0.1", - "colors": "~1.1.2", - "csso": "~2.3.1", - "js-yaml": "~3.7.0", - "mkdirp": "~0.5.1", - "sax": "~1.2.1", - "whet.extend": "~0.9.9" + "has-flag": "^3.0.0" } }, "switch-path": { @@ -10411,240 +7295,229 @@ "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" }, "sync-request": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-2.2.0.tgz", - "integrity": "sha1-p70sES+glGPrkUnP8OnUKMR5do8=", - "requires": { - "concat-stream": "^1.4.7", - "http-response-object": "^1.0.1", - "spawn-sync": "^1.0.1", - "then-request": "^2.0.1" - } - }, - "tapable": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.7.tgz", - "integrity": "sha1-5GwNqsuyuKmLmwzqD0BSEFgX7Vw=", - "dev": true - }, - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "requires": { - "block-stream": "*", - "fstream": "^1.0.2", - "inherits": "2" - } - }, - "tar-pack": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", - "integrity": "sha512-PPRybI9+jM5tjtCbN2cxmmRU7YmqT3Zv/UDy48tAh2XRkLa9bAORtSWLkVc13+GJF+cdTh1yEnHEk3cpTaL5Kg==", - "optional": true, - "requires": { - "debug": "^2.2.0", - "fstream": "^1.0.10", - "fstream-ignore": "^1.0.5", - "once": "^1.3.3", - "readable-stream": "^2.1.4", - "rimraf": "^2.5.1", - "tar": "^2.2.1", - "uid-number": "^0.0.6" - } - }, - "temp-file": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.1.3.tgz", - "integrity": "sha512-oz2J77loDE9sGrlRTqBzwbsUvoBD2BpyXeaRPKyGwBIwaamSs2jdqAfhutw7Tch9llr1u8E2ruoug09rNPa3PA==", - "dev": true, - "requires": { - "async-exit-hook": "^2.0.1", - "bluebird-lst": "^1.0.5", - "fs-extra-p": "^4.6.1", - "lazy-val": "^1.0.3" - } - }, - "term-size": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", - "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", - "dev": true, - "requires": { - "execa": "^0.7.0" - } - }, - "then-request": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/then-request/-/then-request-2.2.0.tgz", - "integrity": "sha1-ZnizL6DKIY/laZgbvYhxtZQGDYE=", - "requires": { - "caseless": "~0.11.0", - "concat-stream": "^1.4.7", - "http-basic": "^2.5.1", - "http-response-object": "^1.1.0", - "promise": "^7.1.1", - "qs": "^6.1.0" - }, - "dependencies": { - "caseless": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" - } - } - }, - "throttleit": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", - "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=" - }, - "through2": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz", - "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", - "requires": { - "readable-stream": "~1.1.9", - "xtend": "~2.1.1" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "xtend": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", - "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", - "requires": { - "object-keys": "~0.4.0" - } - } - } - }, - "thunky": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.0.2.tgz", - "integrity": "sha1-qGLgGOP7HqLsP85dVWBc9X8kc3E=", - "dev": true - }, - "time-stamp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-2.0.1.tgz", - "integrity": "sha512-KUnkvOWC3C+pEbwE/0u3CcmNpGCDqkYGYZOphe1QFxApYQkJ5g195TDBjgZch/zG6chU1NcabLwnM7BCpWAzTQ==", - "dev": true - }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true - }, - "timers-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.2.tgz", - "integrity": "sha1-q0iDz1l9zVCvIRNJoA+8pWrIa4Y=", - "dev": true, + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz", + "integrity": "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==", "requires": { - "setimmediate": "^1.0.4" + "http-response-object": "^3.0.1", + "sync-rpc": "^1.2.1", + "then-request": "^6.0.0" } }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", - "dev": true + "sync-rpc": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/sync-rpc/-/sync-rpc-1.3.6.tgz", + "integrity": "sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==", + "requires": { + "get-port": "^3.1.0" + } }, - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "tar": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", "dev": true, "requires": { - "kind-of": "^3.0.2" + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" } }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "terser": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz", + "integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==", "dev": true, "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" + "commander": "^2.20.0", + "source-map": "~0.7.2", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + } } }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "terser-webpack-plugin": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.2.5.tgz", + "integrity": "sha512-3luOVHku5l0QBeYS8r4CdHYWEGMmIj3H1U64jgkdZzECcSOJAyJ9TjuqcQZvw1Y+4AOBN9SeYJPJmFn2cM4/2g==", "dev": true, "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" + "jest-worker": "^27.0.6", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0", + "source-map": "^0.6.1", + "terser": "^5.7.2" }, "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", "dev": true, "requires": { - "kind-of": "^3.0.2" + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "then-request": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/then-request/-/then-request-6.0.2.tgz", + "integrity": "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==", + "requires": { + "@types/concat-stream": "^1.6.0", + "@types/form-data": "0.0.33", + "@types/node": "^8.0.0", + "@types/qs": "^6.2.31", + "caseless": "~0.12.0", + "concat-stream": "^1.6.0", + "form-data": "^2.2.0", + "http-basic": "^8.1.1", + "http-response-object": "^3.0.1", + "promise": "^8.0.0", + "qs": "^6.4.0" + }, + "dependencies": { + "@types/node": { + "version": "8.10.66", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.66.tgz", + "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==" } } }, + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, "topojson-client": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.0.0.tgz", - "integrity": "sha1-H5kpOnfvQqRI0DKoGqmCtz82DS8=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", "requires": { "commander": "2" } }, "tough-cookie": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", - "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "requires": { - "punycode": "^1.4.1" + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } } }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, "tree-selector": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tree-selector/-/tree-selector-1.1.0.tgz", - "integrity": "sha512-Zczqo5FCAqOEUOLcOLb7kRHWn9dMoWTe/b0uZb42h8ckzsEoWyBY7pEOXTeHlDT1Kq94mHvWft1Hq1fB+KmQdw==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tree-selector/-/tree-selector-2.1.1.tgz", + "integrity": "sha512-NVFYU6YzyUHSMS7+qbhdHlBHG5P9IPjycQa+8MnIBDpq/UWEkMD+lAL/56x7Fty+mAq9SWgtg9D4flZfuOZdDg==" }, "trim-newlines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", "dev": true }, "true-case-path": { @@ -10671,20 +7544,10 @@ } } }, - "truncate-utf8-bytes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=", - "dev": true, - "requires": { - "utf8-byte-length": "^1.0.1" - } - }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", - "dev": true + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "tunnel-agent": { "version": "0.6.0", @@ -10697,32 +7560,31 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, "type-is": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, "requires": { "media-typer": "0.3.0", - "mime-types": "~2.1.18" + "mime-types": "~2.1.24" }, "dependencies": { "mime-db": { - "version": "1.35.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.35.0.tgz", - "integrity": "sha512-JWT/IcCTsB0Io3AhWUMjRqucrHSPsSf2xKLaRldJVULioggvkJvggZ3VXNNSRkCddE6D+BUI4HEIZIA2OjwIvg==", + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", "dev": true }, "mime-types": { - "version": "2.1.19", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.19.tgz", - "integrity": "sha512-P1tKYHVSZ6uFo26mtnve4HQFE3koh1UWVkp8YUC+ESBHe945xWSoXuHHiGarDqcEZ+whpCDnlNw5LON0kLo+sw==", + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "dev": true, "requires": { - "mime-db": "~1.35.0" + "mime-db": "1.51.0" } } } @@ -10732,142 +7594,32 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "dev": true, - "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" - }, - "dependencies": { - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true - }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "dev": true, - "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - } - }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "dev": true - }, - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "dev": true - }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dev": true, - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" - } - } - } - }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "dev": true, - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", - "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=", - "optional": true - }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^0.4.3" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } - } - }, - "uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", "dev": true }, - "uniqid": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/uniqid/-/uniqid-4.1.1.tgz", - "integrity": "sha1-iSIN32t1GuUrX3JISGNShZa7hME=", + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "dev": true, "requires": { - "macaddress": "^0.2.8" + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" } }, - "uniqs": { + "unicode-match-property-value-ecmascript": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", - "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz", + "integrity": "sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==", "dev": true }, - "unique-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", - "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", - "dev": true, - "requires": { - "crypto-random-string": "^1.0.0" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "unicode-property-aliases-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz", + "integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==", "dev": true }, "unpipe": { @@ -10876,124 +7628,10 @@ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", "dev": true }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - } - } - }, - "unzip-response": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", - "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", - "dev": true - }, - "upath": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", - "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", - "dev": true - }, - "update-notifier": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", - "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", - "dev": true, - "requires": { - "boxen": "^1.2.1", - "chalk": "^2.0.1", - "configstore": "^3.0.0", - "import-lazy": "^2.1.0", - "is-ci": "^1.0.10", - "is-installed-globally": "^0.1.0", - "is-npm": "^1.0.0", - "latest-version": "^3.0.0", - "semver-diff": "^2.0.0", - "xdg-basedir": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, "requires": { "punycode": "^2.1.0" }, @@ -11001,17 +7639,10 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" } } }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, "url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", @@ -11031,60 +7662,56 @@ } }, "url-loader": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-0.5.9.tgz", - "integrity": "sha512-B7QYFyvv+fOBqBVeefsxv6koWWtjmHaMFT6KZWti4KRw8YUD/hOU+3AECvXuzyVawIBx3z7zQRejXCDSO5kk1Q==", - "dev": true, - "requires": { - "loader-utils": "^1.0.2", - "mime": "1.3.x" - } - }, - "url-parse": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.3.tgz", - "integrity": "sha512-rh+KuAW36YKo0vClhQzLLveoj8FwPJNu65xLb7Mrt+eZht0IPT0IXgSv8gcMegZ6NvjJUALf6Mf25POlMwD1Fw==", - "dev": true, - "requires": { - "querystringify": "^2.0.0", - "requires-port": "^1.0.0" - } - }, - "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "dev": true, - "requires": { - "prepend-http": "^1.0.1" - } - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "utf8-byte-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", - "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E=", - "dev": true - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-3.0.0.tgz", + "integrity": "sha512-a84JJbIA5xTFTWyjjcPdnsu+41o/SNE8SpXMdUvXs6Q+LuhCD9E2+0VCiuDWqgo3GGXVlFHzArDmBpj9PgWn4A==", "dev": true, "requires": { - "inherits": "2.0.1" + "loader-utils": "^1.2.3", + "mime": "^2.4.4", + "schema-utils": "^2.5.0" }, "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } } } }, @@ -11100,17 +7727,18 @@ "dev": true }, "uuid": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", - "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=" + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, "validate-npm-package-license": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", - "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, "requires": { - "spdx-correct": "~1.0.0", - "spdx-expression-parse": "~1.0.0" + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" } }, "value-equal": { @@ -11125,1576 +7753,809 @@ "dev": true }, "vega": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/vega/-/vega-3.3.1.tgz", - "integrity": "sha512-JVBLqSRXcWfqb4aNinf6uuH0hcUYl8BUTsoJhCv1QQcFZ6NkU+zt4/9E4KYmqzCAr35Z0BvmQMJcsE3mOrUDNA==", - "requires": { - "canvas": "^1.6", - "canvas-prebuilt": "^1.6", - "vega-crossfilter": "2", - "vega-dataflow": "3", - "vega-encode": "2", - "vega-expression": "^2.3", - "vega-force": "2", - "vega-geo": "^2.2", - "vega-hierarchy": "^2.1", - "vega-loader": "2", - "vega-parser": "^2.5", - "vega-projection": "1", - "vega-runtime": "2", - "vega-scale": "^2.1", - "vega-scenegraph": "^2.3", - "vega-statistics": "^1.2", - "vega-transforms": "^1.2", - "vega-typings": "*", - "vega-util": "^1.7", - "vega-view": "^2.2", - "vega-view-transforms": "^1.2", - "vega-voronoi": "2", - "vega-wordcloud": "^2.1", - "yargs": "4" + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/vega/-/vega-5.21.0.tgz", + "integrity": "sha512-yqqRa9nAqYoAxe7sVhRpsh0b001fly7Yx05klPkXmrvzjxXd07gClW1mOuGgSnVQqo7jTp/LYgbO1bD37FbEig==", + "requires": { + "vega-crossfilter": "~4.0.5", + "vega-dataflow": "~5.7.4", + "vega-encode": "~4.8.3", + "vega-event-selector": "~3.0.0", + "vega-expression": "~5.0.0", + "vega-force": "~4.0.7", + "vega-format": "~1.0.4", + "vega-functions": "~5.12.1", + "vega-geo": "~4.3.8", + "vega-hierarchy": "~4.0.9", + "vega-label": "~1.1.0", + "vega-loader": "~4.4.1", + "vega-parser": "~6.1.4", + "vega-projection": "~1.4.5", + "vega-regression": "~1.0.9", + "vega-runtime": "~6.1.3", + "vega-scale": "~7.1.1", + "vega-scenegraph": "~4.9.4", + "vega-statistics": "~1.7.10", + "vega-time": "~2.0.4", + "vega-transforms": "~4.9.4", + "vega-typings": "~0.22.0", + "vega-util": "~1.17.0", + "vega-view": "~5.10.1", + "vega-view-transforms": "~4.5.8", + "vega-voronoi": "~4.1.5", + "vega-wordcloud": "~4.1.3" } }, "vega-canvas": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vega-canvas/-/vega-canvas-1.1.0.tgz", - "integrity": "sha512-0RBwjnrFf4VRhNm5ICY+o1/q6mynli+VheOXKGML9mElRsplZgzd/pv9NnNRNigU+M66Kr7zUpskoakh8EhW9Q==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/vega-canvas/-/vega-canvas-1.2.6.tgz", + "integrity": "sha512-rgeYUpslYn/amIfnuv3Sw6n4BGns94OjjZNtUc9IDji6b+K8LGS/kW+Lvay8JX/oFqtulBp8RLcHN6QjqPLA9Q==" }, "vega-crossfilter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vega-crossfilter/-/vega-crossfilter-2.0.0.tgz", - "integrity": "sha512-haSPtAcSv3TjwAQHLAv8xVB/GM1+lWgFIPmarPX6No/Mq0hewwKuYm2xDlp6zbKKyuDvpW3GzNmLMJfLvzat8A==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/vega-crossfilter/-/vega-crossfilter-4.0.5.tgz", + "integrity": "sha512-yF+iyGP+ZxU7Tcj5yBsMfoUHTCebTALTXIkBNA99RKdaIHp1E690UaGVLZe6xde2n5WaYpho6I/I6wdAW3NXcg==", "requires": { - "d3-array": "1", - "vega-dataflow": "3", - "vega-util": "1" + "d3-array": "^2.7.1", + "vega-dataflow": "^5.7.3", + "vega-util": "^1.15.2" } }, "vega-dataflow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vega-dataflow/-/vega-dataflow-3.1.0.tgz", - "integrity": "sha512-ZQkXqtaNik2RijWya1xMq4zWKHpdxz139JE/l8o9KCGgebEGqh8z4GGpGi0pqROqnGjbcy7fQrDFw2oceY8oyQ==", + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/vega-dataflow/-/vega-dataflow-5.7.4.tgz", + "integrity": "sha512-JGHTpUo8XGETH3b1V892we6hdjzCWB977ybycIu8DPqRoyrZuj6t1fCVImazfMgQD1LAfJlQybWP+alwKDpKig==", "requires": { - "vega-loader": "2", - "vega-util": "1" + "vega-format": "^1.0.4", + "vega-loader": "^4.3.2", + "vega-util": "^1.16.1" } }, "vega-encode": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/vega-encode/-/vega-encode-2.0.8.tgz", - "integrity": "sha512-R7Zg9RHQkAVyFMUJMbNvryB/2+eaHejhOZ8N/rYDPETwIQYPRzepZ+kF+NOVFXWt5zHccqHHL/go9WlE4xYMVA==", - "requires": { - "d3-array": "1", - "d3-format": "1", - "d3-interpolate": "1", - "vega-dataflow": "3", - "vega-scale": "^2.1", - "vega-util": "1" - }, - "dependencies": { - "d3-format": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.0.tgz", - "integrity": "sha512-ycfLEIzHVZC3rOvuBOKVyQXSiUyCDjeAPIj9n/wugrr+s5AcTQC2Bz6aKkubG7rQaQF0SGW/OV4UEJB9nfioFg==" - } + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/vega-encode/-/vega-encode-4.8.3.tgz", + "integrity": "sha512-JoRYtaV2Hs8spWLzTu/IjR7J9jqRmuIOEicAaWj6T9NSZrNWQzu2zF3IVsX85WnrIDIRUDaehXaFZvy9uv9RQg==", + "requires": { + "d3-array": "^2.7.1", + "d3-interpolate": "^2.0.1", + "vega-dataflow": "^5.7.3", + "vega-scale": "^7.0.3", + "vega-util": "^1.15.2" } }, "vega-event-selector": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vega-event-selector/-/vega-event-selector-2.0.0.tgz", - "integrity": "sha512-EZeStM/7LNfJiRuop0lvhOR52Q1l9i/EIYUnm/XddhjR+UqhPkeCmZcffMTr41z3aGm/zciVLlKanUWNT+jQ1A==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vega-event-selector/-/vega-event-selector-3.0.0.tgz", + "integrity": "sha512-Gls93/+7tEJGE3kUuUnxrBIxtvaNeF01VIFB2Q2Of2hBIBvtHX74jcAdDtkh5UhhoYGD8Q1J30P5cqEBEwtPoQ==" }, "vega-expression": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-2.3.1.tgz", - "integrity": "sha512-BOf+7XuzsubcbiLJkgyvnywse1+NY72HhzFPQqwUIfloaH9U6m2sNfMhc353ODWF9UFpKXSQ9r4tWohTj64kNQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-5.0.0.tgz", + "integrity": "sha512-y5+c2frq0tGwJ7vYXzZcfVcIRF/QGfhf2e+bV1Z0iQs+M2lI1II1GPDdmOcMKimpoCVp/D61KUJDIGE1DSmk2w==", "requires": { - "vega-util": "1" + "@types/estree": "^0.0.50", + "vega-util": "^1.16.0" } }, "vega-force": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vega-force/-/vega-force-2.0.0.tgz", - "integrity": "sha512-pQ+r2E7kVRofo2+63jHv5P4qBcCoXHd6asi5HQ9zt4O9cncQ2HTmIfPPWpa6Cy4r8sBWXZHh80nyTuaV6awn8A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/vega-force/-/vega-force-4.0.7.tgz", + "integrity": "sha512-pyLKdwXSZ9C1dVIqdJOobvBY29rLvZjvRRTla9BU/nMwAiAGlGi6WKUFdRGdneyGe3zo2nSZDTZlZM/Z5VaQNA==", + "requires": { + "d3-force": "^2.1.1", + "vega-dataflow": "^5.7.3", + "vega-util": "^1.15.2" + } + }, + "vega-format": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/vega-format/-/vega-format-1.0.4.tgz", + "integrity": "sha512-oTAeub3KWm6nKhXoYCx1q9G3K43R6/pDMXvqDlTSUtjoY7b/Gixm8iLcir5S9bPjvH40n4AcbZsPmNfL/Up77A==", + "requires": { + "d3-array": "^2.7.1", + "d3-format": "^2.0.0", + "d3-time-format": "^3.0.0", + "vega-time": "^2.0.3", + "vega-util": "^1.15.2" + }, + "dependencies": { + "d3-format": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-2.0.0.tgz", + "integrity": "sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA==" + }, + "d3-time": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz", + "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==", + "requires": { + "d3-array": "2" + } + }, + "d3-time-format": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-3.0.0.tgz", + "integrity": "sha512-UXJh6EKsHBTjopVqZBhFysQcoXSv/5yLONZvkQ5Kk3qbwiUYkdX17Xa1PT6U1ZWXGGfB1ey5L8dKMlFq2DO0Ag==", + "requires": { + "d3-time": "1 - 2" + } + } + } + }, + "vega-functions": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/vega-functions/-/vega-functions-5.12.1.tgz", + "integrity": "sha512-7cHfcjXOj27qEbh2FTzWDl7FJK4xGcMFF7+oiyqa0fp7BU/wNT5YdNV0t5kCX9WjV7mfJWACKV74usLJbyM6GA==", "requires": { - "d3-force": "1", - "vega-dataflow": "3", - "vega-util": "1" + "d3-array": "^2.7.1", + "d3-color": "^2.0.0", + "d3-geo": "^2.0.1", + "vega-dataflow": "^5.7.3", + "vega-expression": "^5.0.0", + "vega-scale": "^7.1.1", + "vega-scenegraph": "^4.9.3", + "vega-selections": "^5.3.1", + "vega-statistics": "^1.7.9", + "vega-time": "^2.0.4", + "vega-util": "^1.16.0" } }, "vega-geo": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/vega-geo/-/vega-geo-2.2.2.tgz", - "integrity": "sha512-YD9RJMZPxPYjbqAvhLW1K1tQmzniwAkQMj7CyfFb3Gu70rX49ncf8Meu8NESHP0vUe9zWEFHEA/FrpaXgxuF7w==", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/vega-geo/-/vega-geo-4.3.8.tgz", + "integrity": "sha512-fsGxV96Q/QRgPqOPtMBZdI+DneIiROKTG3YDZvGn0EdV16OG5LzFhbNgLT5GPzI+kTwgLpAsucBHklexlB4kfg==", "requires": { - "d3-array": "1", - "d3-contour": "1", - "d3-geo": "1", - "vega-dataflow": "3", - "vega-projection": "1", - "vega-util": "1" + "d3-array": "^2.7.1", + "d3-color": "^2.0.0", + "d3-geo": "^2.0.1", + "vega-canvas": "^1.2.5", + "vega-dataflow": "^5.7.3", + "vega-projection": "^1.4.5", + "vega-statistics": "^1.7.9", + "vega-util": "^1.15.2" } }, "vega-hierarchy": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/vega-hierarchy/-/vega-hierarchy-2.1.2.tgz", - "integrity": "sha512-9I+/rRU7XLYGFa01dBgAA3QvWEzpLWYwGLgRnFmLkZmOn5YenaRB+2gNG9Zkju2hxGXIx/AEA/i+gGTRdHXn4Q==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/vega-hierarchy/-/vega-hierarchy-4.0.9.tgz", + "integrity": "sha512-4XaWK6V38/QOZ+vllKKTafiwL25m8Kd+ebHmDV+Q236ONHmqc/gv82wwn9nBeXPEfPv4FyJw2SRoqa2Jol6fug==", + "requires": { + "d3-hierarchy": "^2.0.0", + "vega-dataflow": "^5.7.3", + "vega-util": "^1.15.2" + } + }, + "vega-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vega-label/-/vega-label-1.1.0.tgz", + "integrity": "sha512-LAThIiDEsZxYvbSkvPLJ93eJF+Ts8RXv1IpBh8gmew8XGmaLJvVkzdsMe7WJJwuaVEsK7ZZFyB/Inkp842GW6w==", "requires": { - "d3-collection": "1", - "d3-hierarchy": "1", - "vega-dataflow": "^3.1", - "vega-util": "1" + "vega-canvas": "^1.2.5", + "vega-dataflow": "^5.7.3", + "vega-scenegraph": "^4.9.2", + "vega-util": "^1.15.2" } }, "vega-loader": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vega-loader/-/vega-loader-2.1.0.tgz", - "integrity": "sha512-rSLMaRnLqBvmqQqjB7/25fjm4KYxu+P5x3ONngTEpq5vPOk+SUTwVv7saIR1dVHPgaGri7Wsn+xn4kDV8dMJ+w==", - "requires": { - "d3-dsv": "1", - "d3-request": "1", - "d3-time-format": "2", - "topojson-client": "3", - "vega-util": "1" + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/vega-loader/-/vega-loader-4.4.1.tgz", + "integrity": "sha512-dj65i4qlNhK0mOmjuchHgUrF5YUaWrYpx0A8kXA68lBk5Hkx8FNRztkcl07CZJ1+8V81ymEyJii9jzGbhEX0ag==", + "requires": { + "d3-dsv": "^2.0.0", + "node-fetch": "^2.6.1", + "topojson-client": "^3.1.0", + "vega-format": "^1.0.4", + "vega-util": "^1.16.0" }, "dependencies": { "d3-dsv": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.0.8.tgz", - "integrity": "sha512-IVCJpQ+YGe3qu6odkPQI0KPqfxkhbP/oM1XhhE/DFiYmcXKfCRub4KXyiuehV1d4drjWVXHUWx4gHqhdZb6n/A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-2.0.0.tgz", + "integrity": "sha512-E+Pn8UJYx9mViuIUkoc93gJGGYut6mSDKy2+XaPwccwkRGlR+LO97L2VCCRjQivTwLHkSnAJG7yo00BWY6QM+w==", "requires": { "commander": "2", "iconv-lite": "0.4", "rw": "1" } }, - "d3-time": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.8.tgz", - "integrity": "sha512-YRZkNhphZh3KcnBfitvF3c6E0JOFGikHZ4YqD+Lzv83ZHn1/u6yGenRU1m+KAk9J1GnZMnKcrtfvSktlA1DXNQ==" - }, - "d3-time-format": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.1.tgz", - "integrity": "sha512-8kAkymq2WMfzW7e+s/IUNAtN/y3gZXGRrdGfo6R8NKPAA85UBTxZg5E61bR6nLwjPjj4d3zywSQe1CkYLPFyrw==", - "requires": { - "d3-time": "1" - } - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", "requires": { - "safer-buffer": ">= 2.1.2 < 3" + "commander": "2" } } } }, "vega-parser": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/vega-parser/-/vega-parser-2.7.0.tgz", - "integrity": "sha512-i8J4F1CyADFz533FH1y6rfSp8BBszhvYjy4Cbse/fCbXJ3QyCwxil1OdA5AOl7/K7m+sSSEd985p0o/Cd3Kd5w==", - "requires": { - "d3-array": "1", - "d3-color": "1", - "d3-format": "1", - "d3-geo": "1", - "d3-time-format": "2", - "vega-dataflow": "3", - "vega-event-selector": "2", - "vega-expression": "2", - "vega-scale": "2", - "vega-scenegraph": "2", - "vega-statistics": "^1.2", - "vega-util": "^1.7" - }, - "dependencies": { - "d3-format": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.0.tgz", - "integrity": "sha512-ycfLEIzHVZC3rOvuBOKVyQXSiUyCDjeAPIj9n/wugrr+s5AcTQC2Bz6aKkubG7rQaQF0SGW/OV4UEJB9nfioFg==" - }, - "d3-time": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.8.tgz", - "integrity": "sha512-YRZkNhphZh3KcnBfitvF3c6E0JOFGikHZ4YqD+Lzv83ZHn1/u6yGenRU1m+KAk9J1GnZMnKcrtfvSktlA1DXNQ==" - }, - "d3-time-format": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.1.tgz", - "integrity": "sha512-8kAkymq2WMfzW7e+s/IUNAtN/y3gZXGRrdGfo6R8NKPAA85UBTxZg5E61bR6nLwjPjj4d3zywSQe1CkYLPFyrw==", - "requires": { - "d3-time": "1" - } - } + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/vega-parser/-/vega-parser-6.1.4.tgz", + "integrity": "sha512-tORdpWXiH/kkXcpNdbSVEvtaxBuuDtgYp9rBunVW9oLsjFvFXbSWlM1wvJ9ZFSaTfx6CqyTyGMiJemmr1QnTjQ==", + "requires": { + "vega-dataflow": "^5.7.3", + "vega-event-selector": "^3.0.0", + "vega-functions": "^5.12.1", + "vega-scale": "^7.1.1", + "vega-util": "^1.16.0" } }, "vega-projection": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vega-projection/-/vega-projection-1.1.0.tgz", - "integrity": "sha512-Gx9RgaqQy58YDyszjQc69As+1XUDuBln+yKYOtUTHXPiqrnYcW7W6chRSegtFm7Z5oqzXOEH3z2cvkwxyU8Teg==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/vega-projection/-/vega-projection-1.4.5.tgz", + "integrity": "sha512-85kWcPv0zrrNfxescqHtSYpRknilrS0K3CVRZc7IYQxnLtL1oma9WEbrSr1LCmDoCP5hl2Z1kKbomPXkrQX5Ag==", + "requires": { + "d3-geo": "^2.0.1", + "d3-geo-projection": "^3.0.0" + } + }, + "vega-regression": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/vega-regression/-/vega-regression-1.0.9.tgz", + "integrity": "sha512-KSr3QbCF0vJEAWFVY2MA9X786oiJncTTr3gqRMPoaLr/Yo3f7OPKXRoUcw36RiWa0WCOEMgTYtM28iK6ZuSgaA==", "requires": { - "d3-geo": "1" + "d3-array": "^2.7.1", + "vega-dataflow": "^5.7.3", + "vega-statistics": "^1.7.9", + "vega-util": "^1.15.2" } }, "vega-runtime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/vega-runtime/-/vega-runtime-2.0.1.tgz", - "integrity": "sha512-IO4Rd75g2XAmQq3FCi7MqLUGM9CwLZRMeGsBftfjpYuWMgQUDK0xyIOD1qui/RzYVOiN/ENbnY6tPPKYPNhmtA==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/vega-runtime/-/vega-runtime-6.1.3.tgz", + "integrity": "sha512-gE+sO2IfxMUpV0RkFeQVnHdmPy3K7LjHakISZgUGsDI/ZFs9y+HhBf8KTGSL5pcZPtQsZh3GBQ0UonqL1mp9PA==", "requires": { - "vega-dataflow": "3", - "vega-util": "1" + "vega-dataflow": "^5.7.3", + "vega-util": "^1.15.2" } }, "vega-scale": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/vega-scale/-/vega-scale-2.1.1.tgz", - "integrity": "sha512-+Ag6366h/gqnrBtpWQpiyu0+ionPs/HBUPPZnsciidcrOB8sDpB9sU1ecz8nTw6rci05+VhDJeKwyRMHcMf7gQ==", - "requires": { - "d3-array": "1", - "d3-interpolate": "1", - "d3-scale": "2", - "d3-scale-chromatic": "^1.2", - "d3-time": "1", - "vega-util": "1" - }, - "dependencies": { - "d3-time": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.8.tgz", - "integrity": "sha512-YRZkNhphZh3KcnBfitvF3c6E0JOFGikHZ4YqD+Lzv83ZHn1/u6yGenRU1m+KAk9J1GnZMnKcrtfvSktlA1DXNQ==" - } + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/vega-scale/-/vega-scale-7.1.1.tgz", + "integrity": "sha512-yE0to0prA9E5PBJ/XP77TO0BMkzyUVyt7TH5PAwj+CZT7PMsMO6ozihelRhoIiVcP0Ae/ByCEQBUQkzN5zJ0ZA==", + "requires": { + "d3-array": "^2.7.1", + "d3-interpolate": "^2.0.1", + "d3-scale": "^3.2.2", + "vega-time": "^2.0.4", + "vega-util": "^1.15.2" } }, "vega-scenegraph": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/vega-scenegraph/-/vega-scenegraph-2.5.1.tgz", - "integrity": "sha512-1dinfcd7YBJtY5ttfCKpsL1w85Nz/OnVkaV3bmEb4LITcfASbDIVMmGAAjniS7V6Zmi6dM+t2EYN8VMDd7LnIQ==", + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/vega-scenegraph/-/vega-scenegraph-4.9.4.tgz", + "integrity": "sha512-QaegQzbFE2yhYLNWAmHwAuguW3yTtQrmwvfxYT8tk0g+KKodrQ5WSmNrphWXhqwtsgVSvtdZkfp2IPeumcOQJg==", + "requires": { + "d3-path": "^2.0.0", + "d3-shape": "^2.0.0", + "vega-canvas": "^1.2.5", + "vega-loader": "^4.3.3", + "vega-scale": "^7.1.1", + "vega-util": "^1.15.2" + } + }, + "vega-selections": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/vega-selections/-/vega-selections-5.3.1.tgz", + "integrity": "sha512-cm4Srw1WHjcLGXX7GpxiUlfESv8XPu5b6Vh3mqMDPU94P2FO91SR9gei+EtRdt+KCFgIjr//MnRUjg/hAWwjkQ==", "requires": { - "d3-path": "^1.0.5", - "d3-shape": "^1.2.0", - "vega-canvas": "^1.0.1", - "vega-loader": "^2.1.0", - "vega-util": "^1.7.0" + "vega-expression": "^5.0.0", + "vega-util": "^1.16.0" } }, "vega-statistics": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/vega-statistics/-/vega-statistics-1.2.1.tgz", - "integrity": "sha512-70CC6rbS1RFVEpKT/qOGHhHATeh92cB2gXnjg+zErrY8255cBlOW8X0s4iZBsoa4vibGXb42imyM+rxAwL1VPA==", + "version": "1.7.10", + "resolved": "https://registry.npmjs.org/vega-statistics/-/vega-statistics-1.7.10.tgz", + "integrity": "sha512-QLb12gcfpDZ9K5h3TLGrlz4UXDH9wSPyg9LLfOJZacxvvJEPohacUQNrGEAVtFO9ccUCerRfH9cs25ZtHsOZrw==", + "requires": { + "d3-array": "^2.7.1" + } + }, + "vega-time": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vega-time/-/vega-time-2.0.4.tgz", + "integrity": "sha512-U314UDR9+ZlWrD3KBaeH+j/c2WSMdvcZq5yJfFT0yTg1jsBKAQBYFGvl+orackD8Zx3FveHOxx3XAObaQeDX+Q==", "requires": { - "d3-array": "1" + "d3-array": "^2.7.1", + "d3-time": "^2.0.0", + "vega-util": "^1.15.2" + }, + "dependencies": { + "d3-time": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-2.1.1.tgz", + "integrity": "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ==", + "requires": { + "d3-array": "2" + } + } } }, "vega-transforms": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/vega-transforms/-/vega-transforms-1.3.1.tgz", - "integrity": "sha512-Br5AouMJhJiT7WJ0CIeDMGSk/MT2i+6PogC/ufJUFnbaBQ9RlMTG+c25fmXwiKYZC02SmdUeNZF1xmSnhRTbLg==", + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/vega-transforms/-/vega-transforms-4.9.4.tgz", + "integrity": "sha512-JGBhm5Bf6fiGTUSB5Qr5ckw/KU9FJcSV5xIe/y4IobM/i/KNwI1i1fP45LzP4F4yZc0DMTwJod2UvFHGk9plKA==", "requires": { - "d3-array": "1", - "vega-dataflow": "3", - "vega-statistics": "^1.2", - "vega-util": "1" + "d3-array": "^2.7.1", + "vega-dataflow": "^5.7.4", + "vega-statistics": "^1.7.9", + "vega-time": "^2.0.4", + "vega-util": "^1.16.1" } }, "vega-typings": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/vega-typings/-/vega-typings-0.2.17.tgz", - "integrity": "sha512-ebvLsbzFnkr7VM/Ir0l07enyLJMcaQeFyOaz9STvlyVH5kUkQZxURhCVPQT405u2IZ8CLCtJDawO7B0yLUsmDg==" + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/vega-typings/-/vega-typings-0.22.1.tgz", + "integrity": "sha512-88cIrjmoTxo/0nWTf+GuitkFhirHWVWCfymADiCUXt6s9arpQ6XPP5xjrN5KDc0LZd9xr7p4FIiEgADghgLTgw==", + "requires": { + "vega-event-selector": "^3.0.0", + "vega-expression": "^5.0.0", + "vega-util": "^1.15.2" + } }, "vega-util": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-1.7.0.tgz", - "integrity": "sha512-IlmYqW0t3UP8AJX4QuOOm5cMPKPOUa8fSTcCvNkfVOR5zvMNFKCVhoZmJSXgcbEhkfboB+ysI2aaWOeW2kKBog==" + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/vega-util/-/vega-util-1.17.0.tgz", + "integrity": "sha512-HTaydZd9De3yf+8jH66zL4dXJ1d1p5OIFyoBzFiOli4IJbwkL1jrefCKz6AHDm1kYBzDJ0X4bN+CzZSCTvNk1w==" }, "vega-view": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/vega-view/-/vega-view-2.3.2.tgz", - "integrity": "sha512-Q781vmfQxHlUneOtCd2wnQirIG0ABpd+Jku5n1PQDnnqE2y4KiVSDR2IINMJ1KHpsFkiIoRITD6T7XLGAo95hQ==", + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/vega-view/-/vega-view-5.10.1.tgz", + "integrity": "sha512-4xvQ5KZcgKdZx1Z7jjenCUumvlyr/j4XcHLRf9gyeFrFvvS596dVpL92V8twhV6O++DmS2+fj+rHagO8Di4nMg==", "requires": { - "d3-array": "1", - "vega-dataflow": "3", - "vega-parser": "2", - "vega-runtime": "2", - "vega-scenegraph": "2", - "vega-util": "1" + "d3-array": "^2.7.1", + "d3-timer": "^2.0.0", + "vega-dataflow": "^5.7.3", + "vega-format": "^1.0.4", + "vega-functions": "^5.10.0", + "vega-runtime": "^6.1.3", + "vega-scenegraph": "^4.9.4", + "vega-util": "^1.16.1" } }, "vega-view-transforms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/vega-view-transforms/-/vega-view-transforms-1.2.1.tgz", - "integrity": "sha512-AAxMoCyg/JNR7Q5mI8u3l4g7bheUH34eRPUrDZ+3ae1a9nJzlvX3sPQwYqcrOj6q39djOYvnSVNw/cTBsRwr0w==", + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/vega-view-transforms/-/vega-view-transforms-4.5.8.tgz", + "integrity": "sha512-966m7zbzvItBL8rwmF2nKG14rBp7q+3sLCKWeMSUrxoG+M15Smg5gWEGgwTG3A/RwzrZ7rDX5M1sRaAngRH25g==", "requires": { - "vega-dataflow": "3", - "vega-scenegraph": "2", - "vega-util": "1" + "vega-dataflow": "^5.7.3", + "vega-scenegraph": "^4.9.2", + "vega-util": "^1.15.2" } }, "vega-voronoi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/vega-voronoi/-/vega-voronoi-2.0.0.tgz", - "integrity": "sha512-qM6f4RMebKJoOVTw5+/qeFf5FlzVdSV95n+z17MAFBFNwUsTr3luRROR8OAbMlPuUegeavlKeJQLajbQAhH9AA==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/vega-voronoi/-/vega-voronoi-4.1.5.tgz", + "integrity": "sha512-950IkgCFLj0zG33EWLAm1hZcp+FMqWcNQliMYt+MJzOD5S4MSpZpZ7K4wp2M1Jktjw/CLKFL9n38JCI0i3UonA==", "requires": { - "d3-voronoi": "1", - "vega-dataflow": "3", - "vega-util": "1" + "d3-delaunay": "^5.3.0", + "vega-dataflow": "^5.7.3", + "vega-util": "^1.15.2" } }, "vega-wordcloud": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/vega-wordcloud/-/vega-wordcloud-2.1.0.tgz", - "integrity": "sha512-5kKjcse73d72OM1rBqWcbOpWKQeZrk/oVOxAG7EkGyElWQ+vIHBwj5qE4XYa1oIhhez25X1PVqhbzGMj1ZuKoQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/vega-wordcloud/-/vega-wordcloud-4.1.3.tgz", + "integrity": "sha512-is4zYn9FMAyp9T4SAcz2P/U/wqc0Lx3P5YtpWKCbOH02a05vHjUQrQ2TTPOuvmMfAEDCSKvbMSQIJMOE018lJA==", "requires": { - "vega-canvas": "1", - "vega-dataflow": "3", - "vega-scale": "2", - "vega-statistics": "^1.2", - "vega-util": "1" + "vega-canvas": "^1.2.5", + "vega-dataflow": "^5.7.3", + "vega-scale": "^7.1.1", + "vega-statistics": "^1.7.9", + "vega-util": "^1.15.2" } }, - "vendors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.1.tgz", - "integrity": "sha1-N61zyO5Bf7PVgOeFMSMH0nSEfyI=", - "dev": true - }, "verror": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", - "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", - "requires": { - "extsprintf": "1.0.2" - } - }, - "vm-browserify": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", - "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", - "dev": true, - "requires": { - "indexof": "0.0.1" - } - }, - "warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", - "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", - "requires": { - "loose-envify": "^1.0.0" - } - }, - "watchpack": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.4.0.tgz", - "integrity": "sha1-ShRyvLuVK9Cpu0A2gB+VTfs5+qw=", - "dev": true, - "requires": { - "async": "^2.1.2", - "chokidar": "^1.7.0", - "graceful-fs": "^4.1.2" - } - }, - "wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "requires": { - "minimalistic-assert": "^1.0.0" - } - }, - "webpack": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-2.7.0.tgz", - "integrity": "sha512-MjAA0ZqO1ba7ZQJRnoCdbM56mmFpipOPUv/vQpwwfSI42p5PVDdoiuK2AL2FwFUVgT859Jr43bFZXRg/LNsqvg==", - "dev": true, - "requires": { - "acorn": "^5.0.0", - "acorn-dynamic-import": "^2.0.0", - "ajv": "^4.7.0", - "ajv-keywords": "^1.1.1", - "async": "^2.1.2", - "enhanced-resolve": "^3.3.0", - "interpret": "^1.0.0", - "json-loader": "^0.5.4", - "json5": "^0.5.1", - "loader-runner": "^2.3.0", - "loader-utils": "^0.2.16", - "memory-fs": "~0.4.1", - "mkdirp": "~0.5.0", - "node-libs-browser": "^2.0.0", - "source-map": "^0.5.3", - "supports-color": "^3.1.0", - "tapable": "~0.2.5", - "uglify-js": "^2.8.27", - "watchpack": "^1.3.1", - "webpack-sources": "^1.0.1", - "yargs": "^6.0.0" - }, - "dependencies": { - "acorn": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.1.tgz", - "integrity": "sha512-vOk6uEMctu0vQrvuSqFdJyqj1Q0S5VTDL79qtjo+DhRr+1mmaD+tluFSCZqhvi/JUhXSzoZN2BhtstaPEeE8cw==", - "dev": true - }, - "ajv-keywords": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", - "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", - "dev": true - }, - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "loader-utils": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", - "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", - "dev": true, - "requires": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0", - "object-assign": "^4.0.1" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } - }, - "which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", - "dev": true - }, - "yargs": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", - "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", - "dev": true, - "requires": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^4.2.0" - } - }, - "yargs-parser": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", - "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", - "dev": true, - "requires": { - "camelcase": "^3.0.0" - } - } + "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", + "requires": { + "extsprintf": "1.0.2" } }, - "webpack-dev-middleware": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz", - "integrity": "sha512-FCrqPy1yy/sN6U/SaEZcHKRXGlqU0DUaEBL45jkUYoB8foVb6wCnbIJ1HKIx+qUFTW+3JpVcCJCxZ8VATL4e+A==", + "warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", + "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", + "requires": { + "loose-envify": "^1.0.0" + } + }, + "watchpack": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.0.tgz", + "integrity": "sha512-MnN0Q1OsvB/GGHETrFeZPQaOelWh/7O+EiFlj8sM9GPjtQkis7k01aAxrg/18kTfoIVcLL+haEVFlXDaSRwKRw==", "dev": true, "requires": { - "memory-fs": "~0.4.1", - "mime": "^1.5.0", - "path-is-absolute": "^1.0.0", - "range-parser": "^1.0.3", - "time-stamp": "^2.0.0" - }, - "dependencies": { - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - } + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" } }, - "webpack-dev-server": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-2.11.2.tgz", - "integrity": "sha512-zrPoX97bx47vZiAXfDrkw8pe9QjJ+lunQl3dypojyWwWr1M5I2h0VSrMPfTjopHQPRNn+NqfjcMmhoLcUJe2gA==", + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", "dev": true, "requires": { - "ansi-html": "0.0.7", - "array-includes": "^3.0.3", - "bonjour": "^3.5.0", - "chokidar": "^2.0.0", - "compression": "^1.5.2", - "connect-history-api-fallback": "^1.3.0", - "debug": "^3.1.0", - "del": "^3.0.0", - "express": "^4.16.2", - "html-entities": "^1.2.0", - "http-proxy-middleware": "~0.17.4", - "import-local": "^1.0.0", - "internal-ip": "1.2.0", - "ip": "^1.1.5", - "killable": "^1.0.0", - "loglevel": "^1.4.1", - "opn": "^5.1.0", - "portfinder": "^1.0.9", - "selfsigned": "^1.9.1", - "serve-index": "^1.7.2", - "sockjs": "0.3.19", - "sockjs-client": "1.1.4", - "spdy": "^3.4.1", - "strip-ansi": "^3.0.0", - "supports-color": "^5.1.0", - "webpack-dev-middleware": "1.12.2", - "yargs": "6.6.0" + "minimalistic-assert": "^1.0.0" + } + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "webpack": { + "version": "5.64.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.64.3.tgz", + "integrity": "sha512-XF6/IL9Bw2PPQioiR1UYA8Bs4tX3QXJtSelezKECdLFeSFzWoe44zqTzPW5N+xI3fACaRl2/G3sNA4WYHD7Iww==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.0", + "@types/estree": "^0.0.50", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.4.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.8.3", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.4", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.2.0", + "webpack-sources": "^3.2.2" }, "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, - "chokidar": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", - "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.0", - "braces": "^2.3.0", - "fsevents": "^1.2.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.1", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "lodash.debounce": "^4.0.8", - "normalize-path": "^2.1.1", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0", - "upath": "^1.0.5" - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } + "mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", + "dev": true }, - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "mime-types": { + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "dev": true, - "optional": true, "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "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" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.21", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "^2.1.0" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "^5.1.1", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.0", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.1.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.5.1", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.0.5" - } - }, - "safe-buffer": { - "version": "5.1.1", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.5.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.0.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.2", - "bundled": true, - "dev": true - } + "mime-db": "1.51.0" } }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", "dev": true, "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "webpack-sources": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.2.tgz", + "integrity": "sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw==", + "dev": true + } + } + }, + "webpack-cli": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.1.tgz", + "integrity": "sha512-JYRFVuyFpzDxMDB+v/nanUdQYcZtqFPGzmlW4s+UkPMFhSpfRNmf1z4AwYcHJVdvEFAM7FFCQdNTpsBYhDLusQ==", + "dev": true, + "requires": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^1.1.0", + "@webpack-cli/info": "^1.4.0", + "@webpack-cli/serve": "^1.6.0", + "colorette": "^2.0.14", + "commander": "^7.0.0", + "execa": "^5.0.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^2.2.0", + "rechoir": "^0.7.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, "requires": { - "kind-of": "^6.0.0" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "requires": { - "kind-of": "^6.0.0" + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" } }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "path-key": "^3.0.0" } }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, - "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "requires": { - "is-extglob": "^2.1.1" + "shebang-regex": "^3.0.0" } }, - "is-number": { + "shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } + "isexe": "^2.0.0" + } + } + } + }, + "webpack-dev-middleware": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.2.2.tgz", + "integrity": "sha512-DjZyYrsHhkikAFNvSNKrpnziXukU1EChFAh9j4LAm6ndPLPW8cN0KhM7T+RAiOqsQ6ABfQ8hoKIs9IWMTjov+w==", + "dev": true, + "requires": { + "colorette": "^2.0.10", + "memfs": "^3.2.2", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "dependencies": { + "ajv": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz", + "integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" } }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "mime-db": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", "dev": true }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "mime-types": { + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "dev": true, "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" + "mime-db": "1.51.0" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, - "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", "dev": true, - "optional": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + } + } + }, + "webpack-dev-server": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.5.0.tgz", + "integrity": "sha512-Ss4WptsUjYa+3hPI4iYZYEc8FrtnfkaPrm5WTjk9ux5kiCS718836srs0ppKMHRaCHP5mQ6g4JZGcfDdGbCjpQ==", + "dev": true, + "requires": { + "ansi-html-community": "^0.0.8", + "bonjour": "^3.5.0", + "chokidar": "^3.5.2", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "del": "^6.0.0", + "express": "^4.17.1", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.0", + "internal-ip": "^6.2.0", + "ipaddr.js": "^2.0.1", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "portfinder": "^1.0.28", + "schema-utils": "^3.1.0", + "selfsigned": "^1.10.11", + "serve-index": "^1.9.1", + "sockjs": "^0.3.21", + "spdy": "^4.0.2", + "strip-ansi": "^7.0.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^5.2.1", + "ws": "^8.1.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, - "yargs": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", - "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true + }, + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", "dev": true, "requires": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^4.2.0" + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" } }, - "yargs-parser": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", - "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", + "strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", "dev": true, "requires": { - "camelcase": "^3.0.0" + "ansi-regex": "^6.0.1" } } } @@ -12708,37 +8569,67 @@ "material-design-icons": "^3.0.1" } }, + "webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, "webpack-sources": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.0.1.tgz", - "integrity": "sha512-05tMxipUCwHqYaVS8xc7sYPTly8PzXayRCB4dTxLhWTqlKUiwH6ezmEe0OSreL1c30LAuA3Zqmc+uEBUGFJDjw==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", "dev": true, "requires": { "source-list-map": "^2.0.0", - "source-map": "~0.5.3" + "source-map": "~0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "websocket-driver": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", - "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", "dev": true, "requires": { - "http-parser-js": ">=0.4.0", + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "websocket-extensions": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", - "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true }, - "whet.extend": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz", - "integrity": "sha1-+HfVv2SMl+WqVC+twW1qJZucEaE=", - "dev": true + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } }, "which": { "version": "1.3.1", @@ -12750,31 +8641,47 @@ } }, "which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true }, "wide-align": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, "requires": { - "string-width": "^1.0.2" + "string-width": "^1.0.2 || 2 || 3 || 4" } }, - "widest-line": { + "wildcard": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.0.tgz", - "integrity": "sha1-AUKk6KJD+IgsAjOqDgKBqnYVInM=", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + }, + "workerpool": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", + "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", "dev": true, "requires": { - "string-width": "^2.1.1" + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, "is-fullwidth-code-point": { @@ -12784,103 +8691,60 @@ "dev": true }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "dev": true, "requires": { + "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "strip-ansi": "^5.1.0" } }, "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^4.1.0" } } } }, - "window-size": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", - "integrity": "sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU=" - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - } - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write-file-atomic": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", - "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "xdg-basedir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", - "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", - "dev": true - }, - "xmlbuilder": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", - "integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=", - "dev": true - }, - "xmldom": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", - "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, - "xmlhttprequest": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", - "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=" - }, - "xregexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", - "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==", + "ws": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.3.0.tgz", + "integrity": "sha512-Gs5EZtpqZzLvmIM59w4igITU57lrtYVFneaa434VROv4thzJyV6UjIL3D42lslWlI+D4KzLYnxSwtfuiO79sNw==", "dev": true }, "xstream": { - "version": "11.7.0", - "resolved": "https://registry.npmjs.org/xstream/-/xstream-11.7.0.tgz", - "integrity": "sha512-wO3TXiQd2/1UZNVsixDIcQgAN6TU4sGH7qIXvs1CRp1kgtkpU8YTfyKt/z/Z1psqcGnR0cJJxHaCnBxtktLx9w==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/xstream/-/xstream-11.14.0.tgz", + "integrity": "sha512-1bLb+kKKtKPbgTK6i/BaoAn03g47PpFstlbe1BA+y3pNS/LfvcaghS5BFf9+EE1J+KwSQsEpfJvFN5GqFtiNmw==", "requires": { - "symbol-observable": "1.2.0" + "globalthis": "^1.0.1", + "symbol-observable": "^2.0.3" + }, + "dependencies": { + "symbol-observable": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-2.0.3.tgz", + "integrity": "sha512-sQV7phh2WCYAn81oAkakC5qjq2Ml0g8ozqz03wOGnx9dDlG1de6yrF+0RAzSJD8fPUow3PTSMf2SAbOGxb93BA==" + } } }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true - }, "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true }, "yallist": { "version": "2.1.2", @@ -12889,66 +8753,138 @@ "dev": true }, "yargs": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-4.8.1.tgz", - "integrity": "sha1-wMQpJMpKqmsObaFznfshZDn53cA=", - "requires": { - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "lodash.assign": "^4.0.3", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", + "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", - "string-width": "^1.0.1", - "which-module": "^1.0.0", - "window-size": "^0.2.0", - "y18n": "^3.2.1", - "yargs-parser": "^2.4.1" + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" }, "dependencies": { - "camelcase": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } }, "yargs-parser": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz", - "integrity": "sha1-hVaN488VD/SfpRgl8DqMiA3cxcQ=", + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, "requires": { - "camelcase": "^3.0.0", - "lodash.assign": "^4.0.6" + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } } } }, - "yargs-parser": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", - "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, "requires": { - "camelcase": "^4.1.0" + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" }, "dependencies": { "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", + "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", + "dev": true + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true } } }, - "yauzl": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", - "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", - "requires": { - "fd-slicer": "~1.0.1" - } + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true } } } diff --git a/package.json b/package.json index 22f3ae52..aecec159 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,9 @@ "@babel/core": "^7.15.5", "@babel/preset-env": "^7.15.4", "@babel/register": "^7.15.3", - "css-loader": "^0.26.4", + "css-loader": "^6.5.1", "file-loader": "^5.0.2", - "jquery": "^3.3.1", + "jquery": "^3.6.0", "materialize-loader": "^3.0.1", "mini-css-extract-plugin": "^0.8.0", "mocha": "^9.1.1", @@ -41,7 +41,7 @@ "url-loader": "^3.0.0", "webpack": "^5.36.2", "webpack-cli": "^4.6.0", - "webpack-dev-server": "^3.11.2", + "webpack-dev-server": "^4.5.0", "webpack-material-design-icons": "^0.1.0" }, "dependencies": { @@ -55,7 +55,7 @@ "cycle-onionify": "^3.3.0", "cycle-storageify": "^3.2.0", "cyclic-router": "^6.0.0", - "datalib": "^1.9.1", + "datalib": "^1.9.3", "materialize-css": "^1.0.0", "ramda": "^0.27.1", "switch-path": "^1.2.0", diff --git a/webpack.config.js b/webpack.config.js index a0b98120..262e15f8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -11,9 +11,11 @@ module.exports = { filename: 'bundle.js', }, devServer: { - inline: true, + static: { + directory: './', + }, + //inline: true, // removed historyApiFallback: true, - contentBase: './', hot: false, port: 3000 }, From 3953e1bd6c902557cd84cd42283dab3b11122760 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 3 Dec 2021 16:56:55 +0100 Subject: [PATCH 053/191] Add documentation in Table component --- src/js/components/Table.js | 207 ++++++++++++++++++++++++++++++++----- 1 file changed, 182 insertions(+), 25 deletions(-) diff --git a/src/js/components/Table.js b/src/js/components/Table.js index e9bbc1be..d22c8461 100644 --- a/src/js/components/Table.js +++ b/src/js/components/Table.js @@ -52,9 +52,20 @@ import debounce from "xstream/extra/debounce" import pairwise from "xstream/extra/pairwise" import { dirtyWrapperStream } from "../utils/ui" -// Granular access to the settings -// We _copy_ the results array to the root of this element's scope. -// This makes it easier to apply fixed scope later in the process +/** + * @module components/Table + */ + +/** + * Lens for head/top table + * + * Granular access to the settings + * We _copy_ the results array to the root of this element's scope. + * This makes it easier to apply fixed scope later in the process + * + * @const headTableLens + * @type {Lens} + */ const headTableLens = { get: (state) => ({ core: state.headTable, @@ -74,9 +85,16 @@ const headTableLens = { }), } -// Granular access to the settings -// We _copy_ the results array to the root of this element's scope. -// This makes it easier to apply fixed scope later in the process +/** + * Lens for tail/bottom table + * + * Granular access to the settings + * We _copy_ the results array to the root of this element's scope. + * This makes it easier to apply fixed scope later in the process + * + * @const tailTableLens + * @type {Lens} + */ const tailTableLens = { get: (state) => ({ core: state.tailTable, @@ -96,9 +114,16 @@ const tailTableLens = { }), } -// Granular access to the settings -// We _copy_ the results array to the root of this element's scope. -// This makes it easier to apply fixed scope later in the process +/** + * Lens for compound container table + * + * Granular access to the settings + * We _copy_ the results array to the root of this element's scope. + * This makes it easier to apply fixed scope later in the process + * + * @const compoundContainerTableLens + * @type {Lens} + */ const compoundContainerTableLens = { get: (state) => ({ core: state.compoundTable, @@ -118,6 +143,17 @@ const compoundContainerTableLens = { }), } +/** + * Wrapper for the inner Table function. Needed as we need to pass a function into 'isolate' when isolating components + * + * Returns a function that wraps a title and options bar around a standard table and the lens passed as parameters + * + * @function makeTable + * @param {Component} tableComponent Inner table component + * @param {Lens} tableLens Lens used in the inner table component + * @param {*} scope + * @returns a component getter function + */ function makeTable(tableComponent, tableLens, scope = "scope1") { /** * This is a general table container: a post query is sent to the endpoint (configured via settings). @@ -126,6 +162,14 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { * In other words, for what we use it, query can be either a signature or a (list of) known/predicted target(s). * * The lens that defines the rendering of the data is injected using the makeTable function. + * @function makeTable/Table + * @param {*} sources + * - onion.state$: default onion atom containing the input data + * - input: trigger data from the previous component + * - HTTP: HTTP responses stream + * - DOM: user click events + * - props: settings for e.g. background and foreground colors + * @returns a component containing a table with a title and option bar */ return function Table(sources) { const logger = loggerFactory( @@ -143,10 +187,21 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { // state$.map(state => state.core.input).compose(dropRepeats(equals)) ) + /** + * Provides a stream that only updates when state.core.input is not empty and changes occurred + * @const makeTable/Table/modifierState$ + * @type {Stream} + */ const modifiedState$ = state$ .filter((state) => !isEmptyState(state)) .compose(dropRepeats((x, y) => equals(x.core.input, y.core.input))) + /** + * Provides a stream that updates when state.core.input is updated or there is a new input + * Combines the default onion atom with the extra input stream + * @const makeTable/Table/newInput$ + * @type {Stream} + */ const newInput$ = xs .combine(input$, modifiedState$) .map(([newInput, state]) => ({ @@ -155,7 +210,12 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { })) .compose(dropRepeats((x, y) => equals(x.core.input, y.core.input))) - // When the component should not be shown, including empty query + /** + * When the component should not be shown, including empty query + * @function makeTable/Table/isEmptyState + * @param {*} state default onion atom containing the input data + * @returns {boolean} state.core.input is empty or undefined + */ const isEmptyState = (state) => { if (typeof state.core === "undefined") { return true @@ -173,36 +233,80 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { } } - // Split off properties in separate stream to make life easier in simple subcomponents - // Be aware: this is required to be a memory stream!!! + /** + * Split off settings in separate stream to make life easier in simple subcomponents + * This is required to be a memory stream! + * @const makeTable/Table/props$ + * @type {MemoryStream} + */ const props$ = state$ .filter((state) => !isEmptyState(state)) .map((state) => state.settings) .compose(dropRepeats(equals)) .remember() + /** + * Split of settings in a separate stream to drop irrelevant state updates + * Drops repeats to avoid updates to vdom$ when there is no real change + * @const makeTable/Table/settings$ + * @type {Stream} + */ const settings$ = state$ .map((state) => state.settings) - .compose(dropRepeats(equals)) // Avoid updates to vdom$ when no real change + .compose(dropRepeats(equals)) + /** + * Stream of boolean whether the options bar is currently expanded or not + * Drops repeats to avoid updates to vdom$ when there is no real change + * @const makeTable/Table/expandOptions$ + * @type {Stream} + */ const expandOptions$ = state$ .map((state) => state.core.expandOptions) - .compose(dropRepeats(equals)) // Avoid updates to vdom$ when no real change + .compose(dropRepeats(equals)) + /** + * State stream that only updates when state.core.input.query is empty or undefined + * @const makeTable/Table/emptyState$ + * @type {Stream} + */ const emptyState$ = state$.filter((state) => isEmptyState(state)) + /** + * Stream of user events when the +5 button is pressed + * @const makeTable/Table/plus5$ + * @type {Stream} + */ const plus5$ = sources.DOM.select(".plus5") .events("click") .mapTo(5) .startWith(0) + + /** + * Stream of user events when the -5 button is pressed + * @const makeTable/Table/min5$ + * @type {Stream} + */ const min5$ = sources.DOM.select(".min5") .events("click") .mapTo(-5) .startWith(0) + + /** + * Stream of user events when the +10 button is pressed + * @const makeTable/Table/plus10$ + * @type {Stream} + */ const plus10$ = sources.DOM.select(".plus10") .events("click") .mapTo(10) .startWith(0) + + /** + * Stream of user events when the -10 button is pressed + * @const makeTable/Table/min10$ + * @type {Stream} + */ const min10$ = sources.DOM.select(".min10") .events("click") .mapTo(-10) @@ -210,38 +314,38 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { /** * Only take the difference of the default value compared to old value - * + * * Prevent state changes adding the default value in the accumulator. * Needs to have previous and new value and take difference. Simple accumulator with fold would cycle between zero and new value. - * + * * Desired behaviour * acc newInput output * initial 0 5 5 * first update 5 5 0 * second update 5 5 0 - * + * * Highlight why .fold((acc, newValue) => newValue - acc, 0) doesn't work: * acc newInput output * initial 0 5 5 * first update 5 5 0 * second update 0 5 5 - * + * * pairwise gives us previous and new value but need to make sure that if we only receive 1 value we do get an output, so use .startWith(0) - * - * @const defaultAmountToDisplay$ + * + * @const makeTable/Table/defaultAmountToDisplay$ * @type {Stream} */ const defaultAmountToDisplay$ = newInput$.map(state => parseInt(state.settings.table.count)) .startWith(0) .compose(pairwise) .map((v) => (v[1] - v[0])) - + /** * Merge all + and - buttons with default value * Default value needs to be in the accumulator otherwise we can't reduce the amount of lines less than the default setting * By folding & limiting the value here we prevent (hidden) negative numbers that the user would have to increase before seeing changes again - * - * @const amountToDisplay + * + * @const makeTable/Table/amountToDisplay * @type {Stream} */ const amountToDisplay$ = xs @@ -265,6 +369,12 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { .compose(dropRepeats((x, y) => equals(x.core, y.core))) .filter((state) => state.core.input.query) + + /** + * Makes a HTTP request to the API + * @const makeTable/Table/request$ + * @type {Stream} + */ const request$ = triggerRequest$.map((state) => ({ send: merge(state.core.count, { query: state.core.input.query, @@ -280,8 +390,19 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { category: "table", })) + /** + * Get HTTP answer of the HTTP request made to the API + * Is stream of streams + * @const makeTable/Table/response$$ + * @type {Stream} + */ const response$$ = sources.HTTP.select("table") + /** + * Split off invalid responses from the HTTP answers and create a stream of errors + * @const makeTable/Table/invalidResponse$ + * @type {Stream} + */ const invalidResponse$ = response$$ .map( (response$) => @@ -291,12 +412,22 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { ) .flatten() + /** + * Split off valid responses from the HTTP answers and create a stream of valid responses + * @const makeTable/Table/validResponse$ + * @type {Stream} + */ const validResponse$ = response$$ .map((response$) => response$.replaceError((error) => xs.empty())) .flatten() // ======================================================================== + /** + * Create stream of valid data received from the API + * @const makeTable/Table/data$ + * @type {Stream} + */ const data$ = validResponse$.map((result) => result.body.result.data) // Convert to TSV and JSON @@ -312,12 +443,23 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { // ======================================================================== - // Table with samples/compounds -- depending on the tableComponent + /** + * Table with samples/compounds -- depending on the tableComponent + * + * Additionally pass settings into a separate props stream into the component + * @const makeTable/Table/tableContent + * @type {Isolated(Component)} + */ const tableContent = isolate(tableComponent, { onion: tableLens, "*": scope, })({ ...sources, props: props$ }) + /** + * Style used to display in the title which filters are applied + * @const makeTable/Table/chipStyle + * @type {Object} + */ const chipStyle = { style: { fontWeight: "lighter", @@ -379,6 +521,12 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { }) .startWith([]) + /** + * Style to make buttons for tsv & json export and plus & min buttons in expanding option bar + * @const makeTable/Table/smallBtnStyle + * @param {string} bgcolor background color of the button + * @type {Object} + */ const smallBtnStyle = (bgcolor) => ({ style: { "margin-bottom": "0px", @@ -394,6 +542,11 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { // ======================================================================== + /** + * Display an empty div on vdom when state.core.input.query is empty or undefined + * @const makeTable/Table/initVdom$ + * @type {Stream} + */ const initVdom$ = emptyState$.mapTo(div()) const loadingVdom$ = request$ @@ -611,7 +764,11 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { div(".red .white-text", [p("An error occured !!!")]) ) - // Wrap component vdom with an extra div that handles being dirty + /** + * Wrap component vdom with an extra div that handles being dirty + * @const makeTable/Table/vdom$ + * @type {Stream} + */ const vdom$ = dirtyWrapperStream(state$, xs.merge(initVdom$, errorVdom$, loadingVdom$.remember(), loadedVdom$) ) // ======================================================================== From b3d1253b3e0f084879c53cf48700020bb0fe4cfd Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 6 Dec 2021 08:30:32 +0100 Subject: [PATCH 054/191] In Filter component Protocol should be rename to Cell (#80) Refactor Protocol -> Cell --- src/js/components/Filter.js | 44 ++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/js/components/Filter.js b/src/js/components/Filter.js index 73b32146..a2910e10 100644 --- a/src/js/components/Filter.js +++ b/src/js/components/Filter.js @@ -88,15 +88,15 @@ function intent(domSource$) { ) .remember() - const showProtocolUI$ = domSource$ - .select(".protocol") + const showCellUI$ = domSource$ + .select(".cell") .events("click") .fold((x, _) => ({ cell: !x.cell }), { cell: false }) .startWith({ cell: false }) - const showProtocol$ = xs + const showCell$ = xs .merge( - showProtocolUI$ + showCellUI$ // showAnyGhost$ ) .remember() @@ -115,7 +115,7 @@ function intent(domSource$) { .remember() const filterAction$ = xs - .combine(showDose$, showProtocol$, showType$) + .combine(showDose$, showCell$, showType$) .map(mergeAll) // Toggles for filter options @@ -135,8 +135,8 @@ function intent(domSource$) { .map((ev) => ev.ownerTarget.id) .map((value) => ({ dose: value })) - const protocolToggled$ = domSource$ - .select(".protocol-options") + const cellToggled$ = domSource$ + .select(".cell-options") .events("click") .map(function (ev) { ev.preventDefault() @@ -178,7 +178,7 @@ function intent(domSource$) { const action$ = xs.merge( doseToggled$, - protocolToggled$, + cellToggled$, typeToggled$, // toggledGhost$ ) @@ -196,9 +196,9 @@ function intent(domSource$) { * @function model * @param {Stream} possibleValues$ object with 'key': 'array of strings' * @param {Stream} input$ signature string, used to pass to view for it to check if there is any input at all - * @param {Stream} filterValuesAction$ object where key is filter group (top level; dose, protocol, type) and value is which option is being clicked/modified + * @param {Stream} filterValuesAction$ object where key is filter group (top level; dose, cell, type) and value is which option is being clicked/modified * @param {Stream} modifier$ boolean of modifier key being pressed or not - * @param {Stream} filterAction$ object where key is filter group (top level; dose, protocol, type) and value is boolean of the group being clicked open or not + * @param {Stream} filterAction$ object where key is filter group (top level; dose, cell, type) and value is boolean of the group being clicked open or not * @param {Stream} state$ readback of full state object used for comparing committed state vs current state, if not identical means ui is dirty * @returns {Stream} reducers */ @@ -459,21 +459,21 @@ function view(state$) { ), ]), ]) - : div(".protocol .col .s10 .offset-s1", [""]) + : div(".cell .col .s10 .offset-s1", [""]) const loadedVdom$ = modifiedState$.map((state) => { const possibleDoses = state.settings.filter.values.dose || [ "Populating filter dialog...", ] - const possibleProtocols = state.settings.filter.values.cell || [ "Populating filter dialog...", ] + const possibleCells = state.settings.filter.values.cell || [ "Populating filter dialog...", ] const possibleTypes = state.settings.filter.values.trtType || [ "Populating filter dialog...", ] const selectedDoses = state.core.output.dose == undefined ? possibleDoses : state.core.output.dose - const selectedProtocols = + const selectedCells = state.core.output.cell == undefined - ? possibleProtocols + ? possibleCells : state.core.output.cell const selectedTypes = state.core.output.trtType == undefined @@ -501,18 +501,18 @@ function view(state$) { ), ]), div(".col .s12", [ - div(".chip .protocol .col .s12", [ - span(".protocol .blue-grey-text", [ - noFilter(selectedProtocols, possibleProtocols) - ? "No Protocol Filter" - : "Protocols: " + selectedProtocols.join(", "), + div(".chip .cell .col .s12", [ + span(".cell .blue-grey-text", [ + noFilter(selectedCells, possibleCells) + ? "No Cell Filter" + : "Cells: " + selectedCells.join(", "), ]), ]), togglableFilter( - "protocol", + "cell", state.core.state.cell, - possibleProtocols, - selectedProtocols + possibleCells, + selectedCells ), ]), div(".col .s12", [ From 384d4aab2410abeee8617f43858c419366f0c3e2 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 6 Dec 2021 11:42:14 +0100 Subject: [PATCH 055/191] Add documentation to Table.js --- src/js/components/Table.js | 63 +++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/src/js/components/Table.js b/src/js/components/Table.js index d22c8461..6835c9e0 100644 --- a/src/js/components/Table.js +++ b/src/js/components/Table.js @@ -430,16 +430,25 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { */ const data$ = validResponse$.map((result) => result.body.result.data) - // Convert to TSV and JSON + /** + * Stream of table data converted into TSV format + * @const makeTable/Table/csvData$ + * @type {Stream} + */ const csvData$ = data$ .map((data) => convertToCSV(data)) .map((csv) => "text/tsv;charset=utf-8," + encodeURIComponent(csv)) + /** + * Stream of table data converted into JSON + * @const makeTable/Table/jsonData$ + * @type {Stream} + */ const jsonData$ = data$.map( (json) => "text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(json)) ) - const datas$ = xs.combine(data$, csvData$, jsonData$) + //const datas$ = xs.combine(data$, csvData$, jsonData$) // ======================================================================== @@ -468,6 +477,11 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { }, } + /** + * Stream of vdom displaying the applied filter filters + * @const makeTable/Table/filterDom$ + * @type {Stream} + */ const filterDom$ = modifiedState$ .compose( dropRepeats((x, y) => equals(x.core.input.filter, y.core.input.filter)) @@ -549,6 +563,11 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { */ const initVdom$ = emptyState$.mapTo(div()) + /** + * Placeholder while a request is under way + * @const makeTable/Table/loadingVdom$ + * @type {Stream} + */ const loadingVdom$ = request$ .compose(sampleCombine(filterDom$, modifiedState$)) .map(([r, filterText, state]) => @@ -575,6 +594,11 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { ) .remember() + /** + * Full vdom content once request is received + * @const makeTable/Table/loadedVdom$ + * @type {Stream} + */ const loadedVdom$ = xs .combine( tableContent.DOM, @@ -760,6 +784,11 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { ]) ) + /** + * Display error message when an invalid response is received + * @const makeTable/Table/errorVdom$ + * @type {Stream} + */ const errorVdom$ = invalidResponse$.mapTo( div(".red .white-text", [p("An error occured !!!")]) ) @@ -773,7 +802,11 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { // ======================================================================== - // Default Reducer + /** + * Default Reducer + * @const makeTable/Table/defaultReducer$ + * @type {Reducer} + */ const defaultReducer$ = xs .of(function defaultReducer(prevState) { if (typeof prevState === "undefined") { @@ -783,27 +816,43 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { } }) - // Add input to state + /** + * Add input to state + * @const makeTable/Table/inputReducer$ + * @type {Reducer} + */ const inputReducer$ = input$ .map((i) => (prevState) => // inputReducer ({ ...prevState, core: { ...prevState.core, input: i } }) ) - // Add request body to state + /** + * Add request body to state + * @const makeTable/Table/requestReducer$ + * @type {Reducer} + */ const requestReducer$ = request$.map((req) => (prevState) => ({ ...prevState, core: { ...prevState.core, request: req }, })) - // Data reducer + /** + * Add reply data to state + * @const makeTable/Table/dataReducer$ + * @type {Reducer} + */ const dataReducer$ = data$ .map((newData) => (prevState) => ({ ...prevState, core: { ...prevState.core, data: newData }, })) - // Reducer for opening and closing option drawer + /** + * Reducer for opening and closing option drawer + * @const makeTable/Table/switchReducer$ + * @type {Reducer} + */ const switchReducer$ = sources.DOM.select(".switch") .events("click") .fold((x, y) => !x, false) From bf6b8d8a748f9e25f3b0bd80631ba5fb7cb158d6 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 6 Dec 2021 15:39:09 +0100 Subject: [PATCH 056/191] Fix documentation in SampleInfo and add TODO comment in Table --- src/js/components/SampleTable/SampleInfo.js | 4 ++-- src/js/components/Table.js | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/js/components/SampleTable/SampleInfo.js b/src/js/components/SampleTable/SampleInfo.js index c2daff8c..9e756e0c 100644 --- a/src/js/components/SampleTable/SampleInfo.js +++ b/src/js/components/SampleTable/SampleInfo.js @@ -111,7 +111,7 @@ export function SampleInfo(sources) { * Uses materialize.css grid features to display basic sample data in a row * Depending on the width of the screen the content is either in one single line * or details get spread into multiple lines - * @function row + * @function SampleInfo/row * @param {object} sample the data to be displayed * @param {object} props semi-static settings for ie. sourire url or background colors * @param {style} blur component style to contain blur settings @@ -298,7 +298,7 @@ export function SampleInfo(sources) { /** * Constant lambda function to create a data row details for a sample - * @function rowDetails + * @function SampleInfo/rowDetails * @param {object} sample the data to be displayed * @param {object} props static settings for ie. sourire url or background colors * @param {style} blur component style to contain blur settings diff --git a/src/js/components/Table.js b/src/js/components/Table.js index 6835c9e0..71b15011 100644 --- a/src/js/components/Table.js +++ b/src/js/components/Table.js @@ -187,6 +187,11 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { // state$.map(state => state.core.input).compose(dropRepeats(equals)) ) + // TODO: modifiedState$, newInput$ and inputReducer$ should be reworked together + // modifiedState$ triggers on input changes from inputReducer$ + // newInput adds input to state and then also triggers on input changes, but sets the input value itself instead of using inputReducer + // The code would be clearer if either the input stream is kept separate and just use state, or input is added to state and is then used together + /** * Provides a stream that only updates when state.core.input is not empty and changes occurred * @const makeTable/Table/modifierState$ From 83bea4f361628b1a6eb3b34413094db9879057b9 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 7 Dec 2021 12:55:18 +0100 Subject: [PATCH 057/191] Use state directly to get default line amount for tables (#81) By waiting for new input, the default stream came too late as it only updated after an API call was already made --- src/js/components/Table.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/js/components/Table.js b/src/js/components/Table.js index 71b15011..c2d38db6 100644 --- a/src/js/components/Table.js +++ b/src/js/components/Table.js @@ -340,7 +340,8 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { * @const makeTable/Table/defaultAmountToDisplay$ * @type {Stream} */ - const defaultAmountToDisplay$ = newInput$.map(state => parseInt(state.settings.table.count)) + const defaultAmountToDisplay$ = state$.map(state => parseInt(state.settings.table.count)) + .compose(dropRepeats(equals)) .startWith(0) .compose(pairwise) .map((v) => (v[1] - v[0])) From f9a053a91f0c6eb14fcad8e051d508a1abe6684e Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 7 Dec 2021 12:55:43 +0100 Subject: [PATCH 058/191] Fix GSKCMP-57, prevent state updates by making sure dirty ui is pre-defined (#82) startWith was originally removed to solve an issue in the filter tests but this change is no longer needed --- src/js/utils/ui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/utils/ui.js b/src/js/utils/ui.js index 09cd21d7..360103c5 100644 --- a/src/js/utils/ui.js +++ b/src/js/utils/ui.js @@ -12,7 +12,7 @@ function dirtyUiStream(output$, current$) { .map(([output, current]) => !equals(output, current)) .compose(debounce(10)) .compose(dropRepeats(equals)) - //.startWith(false) + .startWith(false) } // Reducer dedicated to outputting the dirty state of a component into the component onion From dfe0c7e40aca412935c219675b10ea7e986cc7d6 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 8 Dec 2021 16:56:46 +0100 Subject: [PATCH 059/191] Rename compound annotation to treatment annotation (#85) * Rename compound annotation to treatment annotation * Increment settings version number as there was a structural change --- deployments.json | 6 +++--- src/js/components/SampleSelection.js | 18 +++++++++--------- ...undAnnotation.js => TreatmentAnnotation.js} | 16 ++++++++-------- src/js/configuration.js | 4 ++-- src/js/pages/adminSettings.js | 6 +++--- src/js/pages/genericTreatment.js | 2 +- 6 files changed, 26 insertions(+), 26 deletions(-) rename src/js/components/{CompoundAnnotation.js => TreatmentAnnotation.js} (91%) diff --git a/deployments.json b/deployments.json index 1c96e89f..50a14488 100644 --- a/deployments.json +++ b/deployments.json @@ -58,7 +58,7 @@ "geneAnnotations": { "url": "http://localhost:8082/gene/symbol/" }, - "compoundAnnotations": { + "treatmentAnnotations": { "url": "http://localhost:8082/ca/" } } @@ -132,7 +132,7 @@ "geneAnnotations": { "url": "https://compass.data-intuitive.app/gene/symbol/" }, - "compoundAnnotations": { + "treatmentAnnotations": { "url": "http://localhost:8082/ca/" } } @@ -176,7 +176,7 @@ "geneAnnotations": { "url": "http://localhost:8082/gene/symbol/" }, - "compoundAnnotations": { + "treatmentAnnotations": { "url": "http://localhost:8082/drugbank/" } } diff --git a/src/js/components/SampleSelection.js b/src/js/components/SampleSelection.js index e2057d4b..39e1074e 100644 --- a/src/js/components/SampleSelection.js +++ b/src/js/components/SampleSelection.js @@ -18,7 +18,7 @@ import xs from "xstream" import dropRepeats from "xstream/extra/dropRepeats" import debounce from 'xstream/extra/debounce' import { loggerFactory } from "../utils/logger" -import { CompoundAnnotation } from "../components/CompoundAnnotation" +import { TreatmentAnnotation } from "./TreatmentAnnotation" import { safeModelToUi } from "../modelTranslations" import { dirtyUiReducer, dirtyWrapperStream } from "../utils/ui" @@ -44,13 +44,13 @@ const sampleSelectionLens = { } /** - * Based on a (list of) compound(s), get the samples that correspond to it and allow users to select them. + * Based on a (list of) treatment(s), get the samples that correspond to it and allow users to select them. * - * input: compound(s) (string) + * input: treatment(s) (string) * output: list of samples (array) */ function SampleSelection(sources) { - const compoundAnnotations = CompoundAnnotation(sources) + const treatmentAnnotations = TreatmentAnnotation(sources) const logger = loggerFactory( "sampleSelection", @@ -63,7 +63,7 @@ function SampleSelection(sources) { const input$ = sources.input // .startWith("BRD-K28907958") // REMOVE ME !!! - // When the component should not be shown, including empty signature + // When the treatment should not be shown, including empty signature const isEmptyState = (state) => { if (typeof state.core === "undefined") { return true @@ -149,7 +149,7 @@ function SampleSelection(sources) { span([""]), ]), ]), - td(".compoundPopup" + selectedClass(entry.use), blurStyle, entry.trt_id), + td(".treatmentPopup" + selectedClass(entry.use), blurStyle, entry.trt_id), td( selectedClass(entry.use), blurStyle, @@ -227,7 +227,7 @@ function SampleSelection(sources) { .remember() const loadedVdom$ = xs - .combine(modifiedState$, compoundAnnotations.DOM) + .combine(modifiedState$, treatmentAnnotations.DOM) .map(([state, annotation]) => makeTable(state, annotation, false)) // Wrap component vdom with an extra div that handles being dirty @@ -336,7 +336,7 @@ function SampleSelection(sources) { return { log: xs.merge(logger(state$, "state$")), DOM: vdom$, - HTTP: xs.merge(request$, compoundAnnotations.HTTP), + HTTP: xs.merge(request$, treatmentAnnotations.HTTP), onion: xs.merge( defaultReducer$, inputReducer$, @@ -346,7 +346,7 @@ function SampleSelection(sources) { dirtyReducer$, ), output: sampleSelection$, - modal: compoundAnnotations.modal, + modal: treatmentAnnotations.modal, } } diff --git a/src/js/components/CompoundAnnotation.js b/src/js/components/TreatmentAnnotation.js similarity index 91% rename from src/js/components/CompoundAnnotation.js rename to src/js/components/TreatmentAnnotation.js index 2a56ef23..77e9bcb3 100644 --- a/src/js/components/CompoundAnnotation.js +++ b/src/js/components/TreatmentAnnotation.js @@ -13,9 +13,9 @@ import delay from 'xstream/extra/delay' * Please note: * - No isolation is performed, but make sure the appropriate config key is pushed through! */ -function CompoundAnnotation(sources, id = ".compoundPopup") { +function TreatmentAnnotation(sources, id = ".treatmentPopup") { - const logger = loggerFactory('compoundAnnotation', sources.onion.state$, 'settings.compoundAnnotations.debug') + const logger = loggerFactory('treatmentAnnotation', sources.onion.state$, 'settings.treatmentAnnotations.debug') const state$ = sources.onion.state$ const trigger$ = @@ -30,16 +30,16 @@ function CompoundAnnotation(sources, id = ".compoundPopup") { trigger$ .compose(sampleCombine(state$)) .map(([el, state]) => { - const url = state.settings.compoundAnnotations.url + const url = state.settings.treatmentAnnotations.url return { url: url + 'id' + '/' + el, method: 'GET', - 'category': 'compound' + 'category': 'treatment' } }) const response$ = sources.HTTP - .select('compound') + .select('treatment') .map((response$) => response$.replaceError(() => xs.of({ body: { id: 'NA'} })) ) @@ -55,7 +55,7 @@ function CompoundAnnotation(sources, id = ".compoundPopup") { span('.col.l9', [ content ]) ]) - // Display code for CompoundAnnotation in Brutus + // Display code for TreatmentAnnotation in Brutus const displayAnnotation = (annotation) => { const title = annotation.name const targets = annotation.targetGenes @@ -111,7 +111,7 @@ function CompoundAnnotation(sources, id = ".compoundPopup") { return div('#modal-' + annotation.id + '.modal.bottom-sheet.grey.darken-4.grey-text.row', [ div('.col.s12.modal-content', [ div('.grey-text.col.l12.s12', [ - h4('.grey-text.text-lighten-2', [ titleCase('No annotations available for this compound') ]), + h4('.grey-text.text-lighten-2', [ titleCase('No annotations available for this treatment') ]), ]), ]) ]) @@ -132,4 +132,4 @@ function CompoundAnnotation(sources, id = ".compoundPopup") { } -export { CompoundAnnotation } +export { TreatmentAnnotation } diff --git a/src/js/configuration.js b/src/js/configuration.js index 6a891519..7cc536c3 100644 --- a/src/js/configuration.js +++ b/src/js/configuration.js @@ -1,5 +1,5 @@ export const initSettings = { - version: 5.2, + version: 5.3, deployment: { "name": "default", }, @@ -68,7 +68,7 @@ export const initSettings = { geneAnnotations: { debug: false }, - compoundAnnotations: { + treatmentAnnotations: { debug: false, version: 'v1', } diff --git a/src/js/pages/adminSettings.js b/src/js/pages/adminSettings.js index c0b14d6b..67e8c123 100644 --- a/src/js/pages/adminSettings.js +++ b/src/js/pages/adminSettings.js @@ -118,8 +118,8 @@ export function AdminSettings(sources) { ], }, { - group: "compoundAnnotations", - title: "Compound Annotation Settings", + group: "treatmentAnnotations", + title: "Treatment Annotation Settings", settings: [ { field: "version", @@ -139,7 +139,7 @@ export function AdminSettings(sources) { field: "url", class: ".input-field", type: "text", - title: "URL for Compound Annotations", + title: "URL for Treatment Annotations", props: {}, }, ], diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index 2ffbe8d0..a1011588 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -66,7 +66,7 @@ export default function GenericTreatmentWorkflow(sources) { api: state.settings.api, common: state.settings.common, geneAnnotations: state.settings.geneAnnotations, - compoundAnnotations: state.settings.compoundAnnotations, + treatmentAnnotations: state.settings.treatmentAnnotations, treatmentLike: workflowTreatmentType, }, ui: (state.ui ?? {} ).form ?? {}, From 74342e80a9fb76c9c209b1209e58aeb7e3b220ff Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 8 Dec 2021 17:19:22 +0100 Subject: [PATCH 060/191] Check Brutus answer and display correct modal (#84) * Add checks so a Brutus call does not crash the app string replace and trim on empty object caused the crashes * Display different modal when no annotation was retrieved Display 'No annotations available for this gene' instead of default modal with all fields empty --- src/js/components/GeneAnnotationQuery.js | 53 ++++++++++++++---------- src/js/utils/utils.js | 2 +- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/js/components/GeneAnnotationQuery.js b/src/js/components/GeneAnnotationQuery.js index 012fca57..f80b123f 100644 --- a/src/js/components/GeneAnnotationQuery.js +++ b/src/js/components/GeneAnnotationQuery.js @@ -31,32 +31,43 @@ function GeneAnnotationQuery(sources, id = ".genePopup") { const geneResponse$ = sources.HTTP .select('gene') .map((response$) => - response$.replaceError(() => xs.of({ body: {} })) + response$.replaceError(() => xs.of({ body: {symbol: 'NA'} })) ) .flatten() .map(r => r.body) - const DOM$ = geneResponse$.map(annotation => - div('#modal-' + absGene(annotation.symbol) + '.modal.bottom-sheet.grey.darken-4.grey-text', [ - div('.col.s12.modal-content', [ - div('.grey-text.col.l6.s12', [ - h4('.grey-text.text-lighten-2', [titleCase(annotation.name)]), - p([b('.grey-text.text-lighten-1', "Protein: "), annotation.protein]), - p([b('.grey-text.text-lighten-1', "EntrezID: "), annotation.entrezid]), - p([b('.grey-text.text-lighten-1', "ProbesetID: "), annotation.probesetID]), - p([b('.grey-text.text-lighten-1', "Ensembl: "),annotation.ensembl]), - p([b('.grey-text.text-lighten-1', "Synonyms: "), annotation.synonyms]), - p([b('.grey-text.text-lighten-1', "Link: "), a({ props: { href: annotation.uniprot, target: "_blank" } }, annotation.uniprot)]), - ]), - div('.col .l6.s12', [ - // h4('.grey-text.text-darken-2', 'Target Information'), - p([b('.grey-text.text-lighten-1', 'Function: '), annotation.function]), - p([b('.grey-text.text-lighten-1', 'Involved in: '), (annotation.involved != null) ? annotation.involved : "N/A"]), - p([b('.grey-text.text-lighten-1', 'Remarks: '), (annotation.remarks != null) ? annotation.remarks : "N/A"]) + const DOM$ = geneResponse$.map(annotation => { + const isAvailable = (annotation.symbol != "NA") + if (isAvailable) { + return div('#modal-' + absGene(annotation.symbol) + '.modal.bottom-sheet.grey.darken-4.grey-text', [ + div('.col.s12.modal-content', [ + div('.grey-text.col.l6.s12', [ + h4('.grey-text.text-lighten-2', [titleCase(annotation.name)]), + p([b('.grey-text.text-lighten-1', "Protein: "), annotation.protein]), + p([b('.grey-text.text-lighten-1', "EntrezID: "), annotation.entrezid]), + p([b('.grey-text.text-lighten-1', "ProbesetID: "), annotation.probesetID]), + p([b('.grey-text.text-lighten-1', "Ensembl: "),annotation.ensembl]), + p([b('.grey-text.text-lighten-1', "Synonyms: "), annotation.synonyms]), + p([b('.grey-text.text-lighten-1', "Link: "), a({ props: { href: annotation.uniprot, target: "_blank" } }, annotation.uniprot)]), + ]), + div('.col .l6.s12', [ + // h4('.grey-text.text-darken-2', 'Target Information'), + p([b('.grey-text.text-lighten-1', 'Function: '), annotation.function]), + p([b('.grey-text.text-lighten-1', 'Involved in: '), (annotation.involved != null) ? annotation.involved : "N/A"]), + p([b('.grey-text.text-lighten-1', 'Remarks: '), (annotation.remarks != null) ? annotation.remarks : "N/A"]) + ]) + ]) ]) - ]) - ]) - ).startWith(div()) + } else { + return div('#modal-' + absGene(annotation.symbol) + '.modal.bottom-sheet.grey.darken-4.grey-text.row', [ + div('.col.s12.modal-content', [ + div('.grey-text.col.l12.s12', [ + h4('.grey-text.text-lighten-2', [ titleCase('No annotations available for this gene') ]), + ]), + ]) + ]) + } + }).startWith(div()) const openModal$ = geneResponse$ .map(annotation => ({ el: '#modal-' + annotation.symbol, state: 'open' })) diff --git a/src/js/utils/utils.js b/src/js/utils/utils.js index d2b2af2b..7388ff50 100644 --- a/src/js/utils/utils.js +++ b/src/js/utils/utils.js @@ -28,7 +28,7 @@ export const titleCase = (phrase) => /** * Take the absolute value of a gene in a very basic way. */ -export const absGene = (signedGene) => signedGene.replace('-', '').trim() +export const absGene = (signedGene) => signedGene?.replace('-', '').trim() /** * For later use: Array extensions From 179d40a44704a0f7591fb118e509f78e7cae1f27 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 13 Dec 2021 09:55:03 +0100 Subject: [PATCH 061/191] Change copyright notice to open-source text and link to LuciusWeb on github (#83) * Change copyright notice to open-source text and link to LuciusWeb on github * Fix footer text to keep using the same font * Vertical align 'open source code ...' to the bottom of the left-side text --- src/js/index.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/js/index.js b/src/js/index.js index d27e5880..6bb54b8d 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -124,8 +124,8 @@ export default function Index(sources) { const footer$ = state$ .map(state => footer('.page-footer .grey .darken-4 .grey-text', [ - div('.row', { style: { margin: '0px' } }, [ - div('.col .s12', { style: { margin: '0px' } }, [ + div('.valign-wrapper .row', { style: { margin: '0px' } }, [ + div('.col .s8', { style: { margin: '0px' } }, [ p({ style: { margin: '0px' } }, [ 'Please use ', a({ props: { href: '/statistics' } }, @@ -138,11 +138,14 @@ export default function Index(sources) { 'ComPass does not make any claims. ', 'In case of issues, please include the contents of ', a({ props: { href: '/debug' } }, 'this page'), ' in your bug report' ]), - ]) + ]), + div('.col .s4 .right-align', { style: {height: '100%', alignSelf: 'flex-end'} }, [ + p({ style: { margin: '0px' } }, [ + 'Open-source code can be found on ', + a({props: { href: 'https://github.com/data-intuitive/LuciusWeb' }}, 'GitHub') + ]), + ]), ]), - div('.footer-copyright .row', { style: { margin: '0px' } }, [ - div('.col .s12 .right-align', ['© 2020 By Data intuitive']), - ]) ]) ) From de703a248c8f6fc327cb393daaf5195cc8615f2d Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 14 Dec 2021 14:08:06 +0100 Subject: [PATCH 062/191] Gskcmp 48 (#86) * Capture non-existent fields and display message instead of crashing page * Create safelyMakeSetting and safelyMakeSettingsGroup as wrapper around isolate Wrapper checks what isolate will try to perform and pre-check if it would cause issues * During loading of settings, recursively check if all values are present Reset settings to defaults if not all values are present Add console print on resetting either due version difference or missing values * Rework safelyMake... functions to not require a state object and instead rely on state$ in sources Returning a stream instead of objects, so need to aggregate it after the map * Use Ramda functions to check all booleans in array are true * Rename badVdom to missingFieldVdom or missingGroupVdom Name is much more descriptive this way * Add documentation for Settings page Most bottom level functions are documented, top level still to do * Expand documentation of the Settings documentation Rename 'topTableEntries' to 'dom' in vdom$. Must be a copy/pasta mistake. Makes no functional change. * Move the bulk of creating of the settings page to a new component Let AdminSettings make use this new component as well and gain the safely creation too --- src/js/components/SettingsEditor.js | 367 ++++++++++++++++++++++++++++ src/js/index.js | 34 ++- src/js/pages/adminSettings.js | 169 +------------ src/js/pages/settings.js | 234 ++++++------------ 4 files changed, 481 insertions(+), 323 deletions(-) create mode 100644 src/js/components/SettingsEditor.js diff --git a/src/js/components/SettingsEditor.js b/src/js/components/SettingsEditor.js new file mode 100644 index 00000000..9b94e16d --- /dev/null +++ b/src/js/components/SettingsEditor.js @@ -0,0 +1,367 @@ +import { + div, + label, + input, + h4, + span, + ul, + li, + } from "@cycle/dom" + import xs from "xstream" + import isolate from "@cycle/isolate" + import { merge } from "ramda" + import { pick, mix } from "cycle-onionify" + +/** + * @module components/SettingsEditor + */ + +/** + * Generate a component to display and edit settings + * @function SettingsEditor + * @param {*} sources + * - onion.state$: default onion atom + * - DOM: DOM events + * - settings$: Stream containing top level object array containing group and field information + * - 'group' has to match to the member name in state$ -> settings.'group' + * - 'title' is text displayed on the page for this group + * - 'settings' contains information for each nested field + * - 'field' has to match to the member name in state$ -> setting.'group'.'field' + * - 'type' text field determining how the value will be displayed, text, checkbox or range + * - 'class' identifier for the field vdom div + * - 'title' is text displayed on the page for this field + * - 'options' possible values for a multiple choice field + * - 'props' extra properties to pass to the input field + * @returns + * - onion: reducers to update the field values to the newly set values + * - DOM: vdom stream of ul and sub-elements + */ +export function SettingsEditor(sources) { + + const settings$ = sources.settings$ + + /** + * Display field setting, rendered according config.type + * @const SettingsEditor/makeSetting + * @param {Object} config description how the field should be displayed and options of e.g. minimum and maximum values + * 'type' text field determining how the value will be displayed, text, checkbox or range + * 'class' identifier for the field vdom div + * 'title' is text displayed on the page for this field + * 'options' possible values for a multiple choice field + * 'props' extra properties to pass to the input field + * @returns Object with + * - onion: reducer to update the value to the new set value + * - DOM: vdom stream of li and sub-elements + * @type {Object} + */ + const makeSetting = (config) => (sources) => { + const state$ = sources.onion.state$ + + /** + * Stream of DOM updates, clicks for checkboxes or the new value for other input types + * @const SettingsEditor/makeSetting/update$ + * @type {Stream} + */ + const update$ = + config.type == "checkbox" + ? sources.DOM.select("input") + .events("click") + .map((event) => event) + : sources.DOM.events("input").map((event) => event.target.value) + + /** + * @function SettingsEditor/makeSetting/renderField + * @param {Object} config description how the input field should be rendered or what options to give + * 'type' text field determining how the value will be displayed, text, checkbox or range + * 'options' possible values for a multiple choice field + * 'props' extra properties to pass to the input field + * @param {*} _state current value of the field + * @returns vdom element according specification in config + */ + function renderField(config, _state) { + if (config.type == "checkbox") { + return [ + label(".active", [ + input({ props: merge(config.props, { checked: _state }) }), + span(".lever"), + ]), + ] + } + if (config.type == "text" || config.type == "range") { + return [input({ props: merge(config.props, { value: _state }) })] + } + if (config.type == "select") { + const options = config.options + const selectedOption = (option) => + _state == option + ? ".grey.lighten-3.black-text" + : ".grey.lighten-3.grey-text.text-lighten-1" + const optionButtons = options.map((o) => + div( + ".col.selection" + selectedOption(o) + "." + o, + { style: { "border-style": "solid", margin: "2px" } }, + [ + label(selectedOption(o), [ + input("", { props: merge(config.props, { value: o }) }, ""), + o, + ]), + ] + ) + ) + return optionButtons + } + } + + /** + * Display field with title, value and input field depending on config + * @const SettingsEditor/makeSetting/vdom$ + * @type {Stream} + */ + const vdom$ = state$.map((state) => + li( + ".collection-item .row", + div(".valign-wrapper", [ + span(".col .l6 .s12 .truncate", [ + span(".flow-text", [config.title]), + span([" "]), + span(".grey-text .text-lighten-1 .right-align", [ + "(", + state.toString(), + ")", + ]), + ]), + + div( + ".col .s6 " + config.class, + renderField(config, state) + ), + ]) + ) + ) + + /** + * Conditional reducer depending on field type + * @const SettingsEditor/makeSetting/updateReducer$ + * @type {Reducer} + */ + const updateReducer$ = + config.type == "checkbox" + ? update$.map((_) => (prevState) => !prevState) + : update$.map((update) => (_) => update) + + return { + DOM: vdom$, + onion: updateReducer$, + } + } + + /** + * Perform sanity check before calling isolate(makeSetting(...),...) + * If we detect that the required member value is not present in onionified state, + * display static vdom with an error message instead. + * If isolate is called without the passed member is present in the onionified state, + * the whole system breaks down and we end up with an empty page without any error messages being displayed, + * making it quite difficult to debug what setting is wrong. + * @const SettingsEditor/safelyMakeSetting$ + * @param {Object} config description how the field should be displayed and options of e.g. minimum and maximum values + * 'field' has to match to the member name in state$ -> setting.'group'.'field' + * 'type' text field determining how the value will be displayed, text, checkbox or range + * 'class' identifier for the field vdom div + * 'title' is text displayed on the page for this field + * 'options' possible values for a multiple choice field + * 'props' extra properties to pass to the input field + * @param {Stream} sources onionified state for the group this setting is in, and to be further onionified + * @returns Stream of Object with + * - onion: reducer to update the value to the newly set value + * - DOM: vdom stream of li and sub-elements + * @type {Stream(Object)} + */ + const safelyMakeSetting$ = (config, sources) => { + + /** + * Minimalistic vdom to be displayed when a field can't be found in the sources + * Mimics same member values as MakeSettings so they can be combined later + * - DOM: stream of fixed vdom li + * - onion: empty stream instead of reducers + * @const SettingsEditor/safelyMakeSetting$/missingFieldVdom + * @type {Object} + */ + const missingFieldVdom = { + DOM: xs.of( + li(".collection-item .row", + div(".valign-wrapper", [ + span(".col .l6 .s12 .truncate .flow-text", [config.title]), + span(".col .s6 .red-text", ["No value '" + config.field + "' in configuration"]), + ]) + )), + onion: xs.empty() + } + + /** + * Stream of either isolated MakeSetting in case field is found in state$ or static vdom in case field is missing in state$ + * @const SettingsEditor/safelyMakeSetting$/vdom$ + * @type {Stream(Object)} + */ + const vdom$ = sources.onion.state$ + .map((state) => ( + (config.field in state) ? + isolate(makeSetting(config), config.field)(sources) : + missingFieldVdom + ) + ) + return vdom$ + } + + /** + * Display group header and create line of each field with name and input field + * @const SettingsEditor/settingsGroupObj + * @param {Object} settingsGroupObj Information how the group should be displayed: + * - title: Name to be displayed + * - settings: Array of Objects for fields to be displayed + * @returns Object with + * - onion: reducers to update the field values to the newly set values + * - DOM: vdom stream of ul and sub-elements + */ + const makeSettingsGroup = (settingsGroupObj) => (sources) => { + const settingsArray = settingsGroupObj.settings + const title = settingsGroupObj.title + + /** + * Convert array of field settings to stream of objects contains DOM and reducers + * @const Settings/settingsGroupObj/components$ + */ + const components$ = xs.of(settingsArray) + .map((settings) => + settings.map((setting) => + safelyMakeSetting$(setting, sources) + ) + ) + .compose(mix(xs.combine)) + .remember() + + /** + * Combines all vdoms from the group field components and adds a title above them + * @const SettingsEditor/settingsGroupObj/vdom$ + * @type {MemoryStream} + */ + const vdom$ = components$ + .compose(pick("DOM")) + .compose(mix(xs.combine)) + .map((vdoms) => + ul( + ".collection .with-header", + [li(".collection-header .grey .lighten-2", [h4(title)])].concat(vdoms) + ) + ) + .remember() + + /** + * Combines all reducer streams from the group field components + * @const SettingsEditor/settingsGroupObj/reducer$ + * @type {Stream} + */ + const reducer$ = components$.compose(pick("onion")).compose(mix(xs.merge)) + + return { + onion: reducer$, + DOM: vdom$, + } + } + + /** + * Perform sanity check before calling isolate(makeSettingsGroup(...),...) + * If we detect that the required group value is not present in onionified state, + * display static vdom with an error message instead. + * If isolate is called without the passed member is present in the onionified state, + * the whole system breaks down and we end up with an empty page without any error messages being displayed, + * making it quite difficult to debug what setting is wrong. + * @const SettingsEditor/safelyMakeSettingsGroup$ + * @param {Object} group description how the group should be displayed + * - 'group' has to match to the member name in state$ -> settings.'group' + * - 'title' is text displayed on the page for this group + * - 'settings' contains information for each nested field + * @param {Stream} sources onionified state of settings, and to be further onionified + * @returns Stream of Object with + * - onion: reducers to update the field values to the newly set values + * - DOM: vdom stream of ul and sub-elements + * @type {Stream(Object)} + */ + const safelyMakeSettingsGroup$ = (group, sources) => { + /** + * Minimalistic vdom to be displayed when a group can't be found in the sources + * Mimics same member values as MakeSettingsGroup so they can be combined later + * - DOM: stream of fixed vdom ul + * - onion: empty stream instead of reducers + * @const SettingsEditor/safelyMakeSettingsGroup$/missingGroupVdom + * @type {Object} + */ + const missingGroupVdom = { + DOM: xs.of( + ul( + ".collection .with-header", + [ + li(".collection-header .grey .lighten-2", [h4(group.title)]), + span(".col .s6 .red-text", ["No value '" + group.group + "' in configuration"]), + ] + )), + onion: xs.empty() + } + + /** + * Stream of either isolated MakeSettingsGroup in case group is found in state$ or static vdom in case group is missing in state$ + * @const SettingsEditor/safelyMakeSettingsGroup$/vdom$ + * @type {Stream(Object)} + */ + const vdom$ = sources.onion.state$ + .map((state) => ( + (group.group in state) ? + isolate(makeSettingsGroup(group), group.group)(sources) : + missingGroupVdom + ) + ) + return vdom$ + } + + /** + * Convert groups to vdom and reducer objects + * Do this safely instead of just calling isolate(group, group.group) directly + * If the group is not present in the sources state onion, things go bad and we end up with a blank page + * @const SettingsEditor/groups$ + * @type {MemoryStream} + */ + const groups$ = settings$ + .map((groups) => + groups.map((group) => + safelyMakeSettingsGroup$(group, sources) + ) + ) + .compose(mix(xs.combine)) + .remember() + + /** + * Stream of group vdom div + * @const SettingsEditor/vdom$ + * @type {MemoryStream} + */ + const vdom$ = groups$ + .compose(pick("DOM")) + .compose(mix(xs.combine)) + .map((vdoms) => div(".col .l8 .offset-l2 .s12", vdoms)) + .remember() + + /** + * Steam of reducers + * @const SettingsEditor/reducer$ + * @type {MemoryStream} + */ + const reducer$ = groups$ + .compose(pick("onion")) + .compose(mix(xs.merge)) + .remember() + + return { + onion: reducer$, + DOM: vdom$, + } + +} \ No newline at end of file diff --git a/src/js/index.js b/src/js/index.js index 6bb54b8d..3a7d4260 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -185,6 +185,28 @@ export default function Index(sources) { const updatedSettings = merge(initSettings, { deployment : updatedDeployment}) // Do the same with the administrative settings const distributedAdminSettings = mergeDeepRight(updatedSettings, updatedSettings.deployment.services) + + /** + * Recursively check if all members of obj are present in value + * Does not check for equalitiy, just the value being present + * Completely ignores the required functionality for arrays + */ + const allPresent = (obj, value) => { + const keys = Object.keys(obj) + const valueKeys = Object.keys(value) + + const present = keys.map((key) => { + if (!R.contains(key, valueKeys)) + return false + if (typeof obj[key] === "object") + return allPresent(obj[key], value[key]) + return true + }) + + // return/check if all booleans in the array are true + return R.all(R.identity)(present) + } + if (typeof prevState === 'undefined') { // No pre-existing state information, use default settings return ({ @@ -192,8 +214,16 @@ export default function Index(sources) { }) } else { // Pre-existing state information. - // If default settings are newer, use those. - return (prevState.settings.version == initSettings.version) ? + // Safety check on old information + const sameVersionInSettings = prevState.settings.version == initSettings.version + const allPresentInSettings = allPresent(initSettings, prevState?.settings) + + if (!sameVersionInSettings) + console.log("Stored settings version doesn't match application settings version. Resetting settings to default values.") + if (!allPresentInSettings) + console.log("Stored settings don't match application settings structure. Resetting settings to default values.") + // If stored settings are different version or are invalid, use default settings. + return (sameVersionInSettings && allPresentInSettings) ? ({ settings: prevState.settings }) : ({ settings: distributedAdminSettings }) } diff --git a/src/js/pages/adminSettings.js b/src/js/pages/adminSettings.js index 67e8c123..557be08c 100644 --- a/src/js/pages/adminSettings.js +++ b/src/js/pages/adminSettings.js @@ -1,21 +1,15 @@ import { div, - label, - input, button, - h4, - span, - ul, - li, } from "@cycle/dom" import xs from "xstream" import isolate from "@cycle/isolate" -import { mergeWith, merge, props, keys, cond } from "ramda" import * as R from "ramda" -import { pick, mix } from "cycle-onionify" import debounce from "xstream/extra/debounce" import dropRepeats from "xstream/extra/dropRepeats" +import { SettingsEditor } from "../components/SettingsEditor" + export function IsolatedAdminSettings(sources) { return isolate(AdminSettings, "settings")(sources) } @@ -224,164 +218,7 @@ export function AdminSettings(sources) { }, ]) - // Depending on the type of config settings, render the appropriate vdom representation - function renderField(config, _state) { - if (config.type == "checkbox") { - return [ - label(".active", [ - input({ props: merge(config.props, { checked: _state }) }), - span(".lever"), - ]), - ] - } - if (config.type == "text") { - return [input({ props: merge(config.props, { value: _state }) })] - } - if (config.type == "select") { - const options = config.options - const selectedOption = (option) => - _state == option - ? ".grey.lighten-3.black-text" - : ".grey.lighten-3.grey-text.text-lighten-1" - const optionButtons = options.map((o) => - div( - ".col.selection" + selectedOption(o) + "." + o, - { style: { "border-style": "solid", margin: "2px" } }, - [ - label(selectedOption(o), [ - input("", { props: merge(config.props, { value: o }) }, ""), - o, - ]), - ] - ) - ) - return optionButtons - } - } - - const makeSetting = (config) => (sources) => { - const state$ = sources.onion.state$ - - const update$ = R.cond([ - [ - R.equals("checkbox"), - (_) => - sources.DOM.select("input") - .events("click") - .map((event) => event), - ], - [ - R.equals("text"), - (_) => sources.DOM.events("input").map((event) => event.target.value), - ], - [ - R.equals("select"), - (_) => - sources.DOM.select("input") - .events("click") - .map((event) => event.target.value), - ], - ])(config.type) - - const vdom$ = state$.map((state) => - li( - ".collection-item .row", - div(".valign-wrapper", [ - span(".col .l6 .s12 .truncate", [ - span(".flow-text", [config.title]), - span([" "]), - span(".grey-text .text-lighten-1 .right-align", [ - "(", - state.toString(), - ")", - ]), - ]), - - div(".col .s6 " + config.class, renderField(config, state)), - ]) - ) - ) - - const updateReducer$ = cond([ - [ - R.equals("checkbox"), - (_) => update$.map((_) => (prevState) => !prevState), - ], - [R.equals("text"), (_) => update$.map((update) => (_) => update)], - [ - R.equals("select"), - (_) => update$.map((update) => (_) => update), - ], - ])(config.type) - - return { - DOM: vdom$, - onion: updateReducer$, - } - } - - const makeSettingsGroup = (settingsGroupObj) => (sources) => { - const group$ = sources.onion.state$ - const settingsArray = settingsGroupObj.settings - const title = settingsGroupObj.title - - const components$ = xs - .of(settingsArray) - .map((settings) => - settings.map((setting) => - isolate(makeSetting(setting), setting.field)(sources) - ) - ) - .remember() - - const vdom$ = components$ - .compose(pick("DOM")) - .compose(mix(xs.combine)) - .map((vdoms) => - ul( - ".collection .with-header", - [li(".collection-header .grey .lighten-2", [h4(title)])].concat(vdoms) - ) - ) - .remember() - - const reducer$ = components$.compose(pick("onion")).compose(mix(xs.merge)) - - return { - onion: reducer$, - DOM: vdom$, - } - } - - const makeSettings = (settingsConf$, sources) => { - const settings$ = sources.onion.state$ - - const groups$ = settingsConf$ - .map((groups) => - groups.map((group) => - isolate(makeSettingsGroup(group), group.group)(sources) - ) - ) - .remember() - - const vdom$ = groups$ - .compose(pick("DOM")) - .compose(mix(xs.combine)) - .map((vdoms) => div(".col .l8 .offset-l2 .s12", vdoms)) - .remember() - - const reducer$ = groups$ - .compose(pick("onion")) - .compose(mix(xs.merge)) - .remember() - - return { - onion: reducer$, - DOM: vdom$, - } - } - - const AdminSettings = makeSettings(settingsConfig$, sources) + const AdminSettings = SettingsEditor({...sources, settings$: settingsConfig$}) const vdom$ = xs .combine(settings$, AdminSettings.DOM) diff --git a/src/js/pages/settings.js b/src/js/pages/settings.js index 9481c25b..43baccc7 100644 --- a/src/js/pages/settings.js +++ b/src/js/pages/settings.js @@ -1,12 +1,6 @@ import { div, - label, - input, button, - h4, - span, - ul, - li, } from "@cycle/dom" import xs from "xstream" import isolate from "@cycle/isolate" @@ -14,13 +8,45 @@ import { mergeWith, merge, mergeAll } from "ramda" import { pick, mix } from "cycle-onionify" import debounce from "xstream/extra/debounce" +import { SettingsEditor } from "../components/SettingsEditor" + +/** + * @module pages/Settings + */ + +/** + * Isolate the Settings component/page + * @function IsolatedSettings + * @param {Stream} sources + * @returns Isolated Settings component/page + */ export function IsolatedSettings(sources) { return isolate(Settings, "settings")(sources) } +/** + * Create a component/page that allows configuring the application + * @function Settings + * @param {Stream} sources + * @returns Settings component/page + */ export function Settings(sources) { const settings$ = sources.onion.state$ + /** + * Settings to display on the page + * 'group' has to match to the member name in state$ -> settings.'group' + * 'title' is text displayed on the page for this group + * 'settings' contains information for each nested field + * 'field' has to match to the member name in state$ -> setting.'group'.'field' + * 'type' text field determining how the value will be displayed, text, checkbox or range + * 'class' identifier for the field vdom div + * 'title' is text displayed on the page for this field + * 'options' possible values for a multiple choice field + * 'props' extra properties to pass to the input field + * @const Settings/settingsConfig + * @type {Array(Object)} + */ const settingsConfig = [ { group: "common", @@ -125,154 +151,24 @@ export function Settings(sources) { }, ] - const makeSetting = (config) => (sources) => { - const state$ = sources.onion.state$ - - const update$ = - config.type == "checkbox" - ? sources.DOM.select("input") - .events("click") - .map((event) => event) - : sources.DOM.events("input").map((event) => event.target.value) - - function renderField(config, _state) { - if (config.type == "checkbox") { - return [ - label(".active", [ - input({ props: merge(config.props, { checked: _state }) }), - span(".lever"), - ]), - ] - } - if (config.type == "text" || config.type == "range") { - return [input({ props: merge(config.props, { value: _state }) })] - } - if (config.type == "select") { - const options = config.options - const selectedOption = (option) => - _state == option - ? ".grey.lighten-3.black-text" - : ".grey.lighten-3.grey-text.text-lighten-1" - const optionButtons = options.map((o) => - div( - ".col.selection" + selectedOption(o) + "." + o, - { style: { "border-style": "solid", margin: "2px" } }, - [ - label(selectedOption(o), [ - input("", { props: merge(config.props, { value: o }) }, ""), - o, - ]), - ] - ) - ) - return optionButtons - } - } - - - const vdom$ = state$.map((state) => - li( - ".collection-item .row", - div(".valign-wrapper", [ - span(".col .l6 .s12 .truncate", [ - span(".flow-text", [config.title]), - span([" "]), - span(".grey-text .text-lighten-1 .right-align", [ - "(", - state.toString(), - ")", - ]), - ]), - - div( - ".col .s6 " + config.class, - renderField(config, state) - ), - ]) - ) - ) - - const updateReducer$ = - config.type == "checkbox" - ? update$.map((_) => (prevState) => !prevState) - : update$.map((update) => (_) => update) - - return { - DOM: vdom$, - onion: updateReducer$, - } - } - - const makeSettingsGroup = (settingsGroupObj) => (sources) => { - const group$ = sources.onion.state$ - const settingsArray = settingsGroupObj.settings - const title = settingsGroupObj.title - - const components$ = xs - .of(settingsArray) - .map((settings) => - settings.map((setting) => - isolate(makeSetting(setting), setting.field)(sources) - ) - ) - .remember() - - const vdom$ = components$ - .compose(pick("DOM")) - .compose(mix(xs.combine)) - .map((vdoms) => - ul( - ".collection .with-header", - [li(".collection-header .grey .lighten-2", [h4(title)])].concat(vdoms) - ) - ) - .remember() - - const reducer$ = components$.compose(pick("onion")).compose(mix(xs.merge)) - - return { - onion: reducer$, - DOM: vdom$, - } - } - - const makeSettings = (settingsObj) => (sources) => { - const settings$ = sources.onion.state$ - - const groups$ = xs - .of(settingsObj) - .map((groups) => - groups.map((group) => - isolate(makeSettingsGroup(group), group.group)(sources) - ) - ) - .remember() - - const vdom$ = groups$ - .compose(pick("DOM")) - .compose(mix(xs.combine)) - .map((vdoms) => div(".col .l8 .offset-l2 .s12", vdoms)) - .remember() - - const reducer$ = groups$ - .compose(pick("onion")) - .compose(mix(xs.merge)) - .remember() - - return { - onion: reducer$, - DOM: vdom$, - } - } - - const Settings = makeSettings(settingsConfig)(sources) - + /** + * SettingsConfig object converted to vdom object and reducers streams + * @const Settings/Settings + * @type {Object} + */ + const Settings = SettingsEditor({...sources, settings$: xs.of(settingsConfig)}) + + /** + * Full page layout for settings + * @const Settings/vdom$ + * @type {MemoryStream} + */ const vdom$ = xs .combine(settings$, Settings.DOM) - .map(([_, topTableEntries]) => + .map(([_, dom]) => div(".row .grey .lighten-3", { style: { margin: "0px 0px 0px 0px" } }, [ div(".row .s12", [""]), - topTableEntries, + dom, div(".row .s12", [""]), button(".reset .col .s2 .offset-s3 .btn .grey", "Reset to Default"), button( @@ -284,20 +180,47 @@ export function Settings(sources) { ) .remember() - // When the reset button is pressed, we remove the ComPass key from the local storage - // and reload the page. The `defaultReducer$` in `index.js` handles taking care of - // the deployment scenario. + + /** + * Listener stream for reset button presses + * When the reset button is pressed, we remove the ComPass key from the local storage + * and reload the page. The `defaultReducer$` in `index.js` handles taking care of + * the deployment scenario. + * @const Settings/reset$ + * @type {MemoryStream} + */ const reset$ = sources.DOM.select(".reset").events("click").remember() - // Reset the storage by removing the ComPass key + + /** + * Reset the storage by removing the ComPass key + * @const Settings/resetStorage$ + * @type {Stream} + */ const resetStorage$ = reset$.mapTo({ action: "removeItem", key: "ComPass" }) + /** + * Listener stream for admin button presses + * Sends router to the /settings page + * @const Settings/admin$ + * @type {MemoryStream} + */ const admin$ = sources.DOM.select(".admin").events("click").remember() - // The router does not reload the same page, so use the browser functionality for that... + /** + * The router does not reload the same page, so use the browser functionality for that... + * @const Settings/resetRouter$ + * @type {MemoryStream} + */ const resetRouter$ = reset$ .map((_) => location.reload()) .mapTo("/settings") .remember() + + /** + * Trigger router to load the /admin page + * @const Settings/adminRouter$ + * @type {MemoryStream} + */ const adminRouter$ = admin$.mapTo("/admin").remember() // This is an effect and should be moved to a driver... @@ -310,4 +233,5 @@ export function Settings(sources) { router: xs.merge(resetRouter$, adminRouter$), storage: resetStorage$, } + } From 45dbb9a6dedfe6f47a7ce8493fbe54405a9d3bd7 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 14 Dec 2021 14:17:39 +0100 Subject: [PATCH 063/191] Add config field in settings to be able to tweak the functionality in the application (#87) Used to enable or disable the admin settings on the settings page --- src/js/configuration.js | 5 ++++- src/js/pages/adminSettings.js | 13 +++++++++++++ src/js/pages/settings.js | 25 ++++++++++++++++++------- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/js/configuration.js b/src/js/configuration.js index 7cc536c3..a58218af 100644 --- a/src/js/configuration.js +++ b/src/js/configuration.js @@ -1,5 +1,5 @@ export const initSettings = { - version: 5.3, + version: 5.4, deployment: { "name": "default", }, @@ -71,5 +71,8 @@ export const initSettings = { treatmentAnnotations: { debug: false, version: 'v1', + }, + config: { + showAdminButton: false, } }; diff --git a/src/js/pages/adminSettings.js b/src/js/pages/adminSettings.js index 557be08c..c510306f 100644 --- a/src/js/pages/adminSettings.js +++ b/src/js/pages/adminSettings.js @@ -216,6 +216,19 @@ export function AdminSettings(sources) { }, ], }, + { + group: "config", + title: "Configuration Settings", + settings: [ + { + field: "showAdminButton", + type: "checkbox", + class: ".switch", + title: "Show Admin button?", + props: { type: "checkbox" }, + } + ], + }, ]) const AdminSettings = SettingsEditor({...sources, settings$: settingsConfig$}) diff --git a/src/js/pages/settings.js b/src/js/pages/settings.js index 43baccc7..be14b72d 100644 --- a/src/js/pages/settings.js +++ b/src/js/pages/settings.js @@ -158,23 +158,34 @@ export function Settings(sources) { */ const Settings = SettingsEditor({...sources, settings$: xs.of(settingsConfig)}) + const buttons$ = settings$ + .map((state) => + state.config.showAdminButton ? + div([ + button(".reset .col .s4 .offset-s1 .l2 .offset-l3 .btn .grey", "Reset to Default"), + button( + ".admin .col .s4 .l2 .offset-s1 .offset-l2 .btn .grey .lighten-2 .grey-text", + "Go to Admin Settings" + ) + ]) : + div([ + button(".reset .col .s4 .offset-s4 .btn .grey", "Reset to Default"), + ]) + ) + /** * Full page layout for settings * @const Settings/vdom$ * @type {MemoryStream} */ const vdom$ = xs - .combine(settings$, Settings.DOM) - .map(([_, dom]) => + .combine(settings$, Settings.DOM, buttons$) + .map(([_, dom, buttons]) => div(".row .grey .lighten-3", { style: { margin: "0px 0px 0px 0px" } }, [ div(".row .s12", [""]), dom, div(".row .s12", [""]), - button(".reset .col .s2 .offset-s3 .btn .grey", "Reset to Default"), - button( - ".admin .col .s2 .offset-s2 .btn .grey .lighten-2 .grey-text", - "Go to Admin Settings" - ), + buttons, div(".row .s12", [""]), ]) ) From f48af0387b42da81162207a59930cf6e7234db05 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 15 Dec 2021 14:15:15 +0100 Subject: [PATCH 064/191] A logo should be shown in the web interface (#88) * Add config field in settings to be able to tweak the functionality in the application Used to enable or disable the admin settings on the settings page * Add configurable company logo in the navbar Add image URL in admin settings page and add in deployment json --- deployments.json | 9 ++++ src/js/configuration.js | 1 + src/js/index.js | 85 +++++++++++++++++++++++------------ src/js/main.scss | 8 ++++ src/js/pages/adminSettings.js | 7 +++ src/js/pages/settings.js | 7 +-- 6 files changed, 83 insertions(+), 34 deletions(-) diff --git a/deployments.json b/deployments.json index 50a14488..3bd576ff 100644 --- a/deployments.json +++ b/deployments.json @@ -60,6 +60,9 @@ }, "treatmentAnnotations": { "url": "http://localhost:8082/ca/" + }, + "config": { + "logoUrl": "https://www.data-intuitive.com/images/logo_white.png" } } }, @@ -134,6 +137,9 @@ }, "treatmentAnnotations": { "url": "http://localhost:8082/ca/" + }, + "config": { + "logoUrl": "" } } }, @@ -178,6 +184,9 @@ }, "treatmentAnnotations": { "url": "http://localhost:8082/drugbank/" + }, + "config": { + "logoUrl": "" } } } diff --git a/src/js/configuration.js b/src/js/configuration.js index a58218af..1e22eebb 100644 --- a/src/js/configuration.js +++ b/src/js/configuration.js @@ -74,5 +74,6 @@ export const initSettings = { }, config: { showAdminButton: false, + // logoUrl: 'https://www.data-intuitive.com/images/logo_white.png', } }; diff --git a/src/js/index.js b/src/js/index.js index 3a7d4260..39424bf2 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -1,6 +1,6 @@ import xs from 'xstream' -import { div, nav, a, h3, p, ul, li, h1, h2, i, footer, header, main, svg, g, path, span } from '@cycle/dom' +import { div, nav, a, h3, p, ul, li, h1, h2, i, footer, header, main, svg, g, path, span, img } from '@cycle/dom' import { merge, prop, mergeDeepRight } from 'ramda' import * as R from 'ramda' @@ -89,35 +89,64 @@ export default function Index(sources) { ]) ]) - const nav$ = xs.of(header([ - nav('#navigation .grey .darken-4', [ - div('.nav-wrapper', [ - a('.brand-logo .right .grey-text', { props: { href: "/" } }, - div({ style: { width: '140px' } }, logoSVG), - // span('.gradient', 'ComPass') - ), - a('.sidenav-trigger', { props: { href: '#' }, attrs: {'data-target': 'mobile-demo' }}, i('.material-icons', 'menu')), - ul('.left .hide-on-med-and-down', [ - makeLink('/compound', span(['Compound', ' ', compoundSVG]), '.orange-text'), - // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), - makeLink('/genetic', span(['Genetic', ' ', targetSVG]), '.red-text'), - makeLink('/disease', span(['Disease', ' ', diseaseSVG]), '.pink-text'), - makeLink('/settings', span(['Settings', ' ', settingsSVG]), '.grey-text'), - // makeLink('/admin', span(['Admin']), '.blue-text'), - makeLink('/correlation', span('.grey-text .text-darken-3','', ["v", VERSION]), ''), + const nav$ = state$.map((state) => { + + const leftLogo = + state.settings.config.logoUrl ? + a('.left .grey-text .hide-on-med-and-down', { props: { href: '/' }, style: {margin: '5px'} }, + img(".logo_img .left", { props: { alt: 'logo', src: state.settings.config.logoUrl}, style: {height: '40px'}}), + ) + : + span() + + const centerLogo = + state.settings.config.logoUrl ? + div('.brand-logo .center', [ + a('.grey-text .hide-on-large-only', { props: { href: '/' } }, + img(".logo_img", { props: { alt: 'logo', src: state.settings.config.logoUrl}, + style: {height: '40px'} + }), + ), ]) + : + div() + + return header({ style: {display: 'flex'} },[ + nav('#navigation .grey .darken-4', [ + div('.nav-wrapper .valign-wrapper', [ + a('.brand-logo .right .grey-text', { props: { href: "/" } }, + div({ style: { width: '140px' } }, logoSVG), + // span('.gradient', 'ComPass') + ), + leftLogo, + a('.sidenav-trigger', { props: { href: '#' }, attrs: {'data-target': 'mobile-demo' }}, i('.material-icons', 'menu')), + ul('.left .hide-on-med-and-down', [ + makeLink('/compound', span(['Compound', ' ', compoundSVG]), '.orange-text'), + // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), + makeLink('/genetic', span(['Genetic', ' ', targetSVG]), '.red-text'), + makeLink('/disease', span(['Disease', ' ', diseaseSVG]), '.pink-text'), + makeLink('/settings', span(['Settings', ' ', settingsSVG]), '.grey-text'), + // makeLink('/admin', span(['Admin']), '.blue-text'), + makeLink('/correlation', span('.grey-text .text-darken-3','', ["v", VERSION]), ''), + ]), + centerLogo, + ]), + ]), + ul(".sidenav", {props: {id: 'mobile-demo'}}, [ + makeLink('/compound', span(['Compound', ' ', compoundSVG]), '.orange-text'), + // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), + makeLink('/genetic', span(['Genetic', ' ', targetSVG]), '.red-text'), + makeLink('/disease', span(['Disease', ' ', diseaseSVG]), '.pink-text'), + makeLink('/settings', span(['Settings', ' ', settingsSVG]), '.grey-text'), + // makeLink('/admin', span(['Admin']), '.blue-text'), + makeLink('/correlation', span('.grey-text .text-darken-3','', ["v", VERSION]), ''), ]) - ]), - ul(".sidenav", {props: {id: 'mobile-demo'}}, [ - makeLink('/compound', span(['Compound', ' ', compoundSVG]), '.orange-text'), - // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), - makeLink('/genetic', span(['Genetic', ' ', targetSVG]), '.red-text'), - makeLink('/disease', span(['Disease', ' ', diseaseSVG]), '.pink-text'), - makeLink('/settings', span(['Settings', ' ', settingsSVG]), '.grey-text'), - // makeLink('/admin', span(['Admin']), '.blue-text'), - makeLink('/correlation', span('.grey-text .text-darken-3','', ["v", VERSION]), ''), ]) - ])); + }) + + + + // We combine with state in order to read the customizations // This works because the defaultReducer runs before anything else @@ -141,7 +170,7 @@ export default function Index(sources) { ]), div('.col .s4 .right-align', { style: {height: '100%', alignSelf: 'flex-end'} }, [ p({ style: { margin: '0px' } }, [ - 'Open-source code can be found on ', + 'Open-source code can be found on ', a({props: { href: 'https://github.com/data-intuitive/LuciusWeb' }}, 'GitHub') ]), ]), diff --git a/src/js/main.scss b/src/js/main.scss index d9fd52ab..e7a6afe3 100644 --- a/src/js/main.scss +++ b/src/js/main.scss @@ -60,6 +60,14 @@ nav ul li.active span { padding-bottom: 0.2em; } +nav a.brand-logo svg{ + vertical-align: middle; +} + +nav img.logo_img { + vertical-align: middle; +} + /* home svg styling and hover */ a:hover #border { diff --git a/src/js/pages/adminSettings.js b/src/js/pages/adminSettings.js index c510306f..21c4ff2b 100644 --- a/src/js/pages/adminSettings.js +++ b/src/js/pages/adminSettings.js @@ -226,6 +226,13 @@ export function AdminSettings(sources) { class: ".switch", title: "Show Admin button?", props: { type: "checkbox" }, + }, + { + field: "logoUrl", + class: ".input-field", + type: "text", + title: "URL for logo image", + props: {}, } ], }, diff --git a/src/js/pages/settings.js b/src/js/pages/settings.js index be14b72d..a453bbb9 100644 --- a/src/js/pages/settings.js +++ b/src/js/pages/settings.js @@ -172,12 +172,7 @@ export function Settings(sources) { button(".reset .col .s4 .offset-s4 .btn .grey", "Reset to Default"), ]) ) - - /** - * Full page layout for settings - * @const Settings/vdom$ - * @type {MemoryStream} - */ + const vdom$ = xs .combine(settings$, Settings.DOM, buttons$) .map(([_, dom, buttons]) => From 078cdfbbb4ede2e86748953334571afb53cdc23a Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 15 Dec 2021 14:16:03 +0100 Subject: [PATCH 065/191] Correct Compass logo font to a Serif type (#89) * Correct Compass logo font to a Serif type Add websafe fallbacks on font definitions on index * Change text from logo to SVG path --- src/js/index.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/js/index.js b/src/js/index.js index 39424bf2..ae7ca1f2 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -328,7 +328,14 @@ export const logoSVG = svg({ id: 'logo', attrs: { viewBox: "159 26 1060 460" } } svg.path({ attrs: { d: 'M 389 256 L 159 256 L 334 303 Z' } }), svg.path({ attrs: { d: 'M 389 256 L 389 486 L 436 311 Z' } }), svg.path({ attrs: { d: 'M 389 256 L 619 256 L 444 209 Z' } }), - svg.text({ attrs: { 'font-family': "Calibri", 'font-size': "160", 'font-weight': "bold", x: "450", y: "398" } }, 'COMPASS'), + //svg.text({ attrs: { 'font-family': 'Garamond, serif', 'font-size': "160", 'font-weight': "bold", x: "450", y: "398" } }, 'COMPASS'), + svg.path({ attrs: { transform:"translate(460,300)", d: "M 42.517 108.894 A 73.307 73.307 0 0 0 60.16 110.965 A 111.506 111.506 0 0 0 67.956 110.701 A 87.869 87.869 0 0 0 78.4 109.365 A 102.036 102.036 0 0 0 79.766 109.093 Q 87.225 107.553 92.8 105.205 A 11.877 11.877 0 0 0 94.135 104.431 A 8.802 8.802 0 0 0 95.44 103.365 A 5.185 5.185 0 0 0 95.872 102.873 Q 96.619 101.909 97.12 100.405 Q 98.24 96.885 99.04 93.525 A 74.101 74.101 0 0 0 99.604 90.961 A 59.633 59.633 0 0 0 100.24 87.205 A 55.183 55.183 0 0 0 100.451 85.441 Q 100.64 83.595 100.64 82.165 A 5.632 5.632 0 0 0 100.64 82.147 Q 100.634 80.24 99.332 79.674 A 3.055 3.055 0 0 0 98.32 79.445 Q 96.55 79.323 95.385 80.972 A 6.972 6.972 0 0 0 94.72 82.165 A 56.752 56.752 0 0 1 93.8 84.01 Q 91.762 87.912 89.352 90.983 A 31.476 31.476 0 0 1 82.4 97.605 Q 77.685 100.834 71.132 101.994 A 45.53 45.53 0 0 1 63.2 102.645 Q 54.08 102.645 47.36 98.965 Q 40.64 95.285 36 88.725 Q 31.36 82.165 29.12 73.125 A 72.598 72.598 0 0 1 27.744 66.056 A 87.496 87.496 0 0 1 26.88 53.525 Q 26.88 45.205 29.44 37.525 Q 32 29.845 36.72 23.765 Q 41.44 17.685 48.08 14.165 A 29.342 29.342 0 0 1 53.049 12.09 A 32.658 32.658 0 0 1 62.88 10.645 A 58.028 58.028 0 0 1 64.994 10.683 Q 71.646 10.926 76.56 12.725 A 36.033 36.033 0 0 1 78.031 13.3 Q 82.667 15.232 85.44 18.005 Q 88.48 20.725 90 24.405 Q 91.52 28.085 92.64 31.765 A 6.448 6.448 0 0 0 92.647 31.785 Q 92.855 32.403 93.155 32.879 A 3.239 3.239 0 0 0 94.8 34.245 Q 95.51 34.507 96.167 34.507 A 3.165 3.165 0 0 0 97.6 34.165 A 2.181 2.181 0 0 0 98.779 32.667 A 3.73 3.73 0 0 0 98.88 31.765 Q 98.88 29.734 98.798 27.455 A 165.242 165.242 0 0 0 98.56 22.805 Q 98.24 17.845 97.6 12.565 Q 97.528 11.695 97.275 11.006 A 3.802 3.802 0 0 0 96.4 9.605 A 7.672 7.672 0 0 0 95.926 9.169 A 8.327 8.327 0 0 0 93.92 7.925 Q 89.92 6.485 85.04 5.205 Q 80.16 3.925 74.96 3.205 Q 69.76 2.485 64.64 2.485 Q 49.6 2.485 37.68 7.045 A 73.108 73.108 0 0 0 32.282 9.359 A 60.996 60.996 0 0 0 17.28 19.365 Q 8.8 27.125 4.4 37.125 Q 0 47.125 0 58.165 A 57.845 57.845 0 0 0 0.137 62.174 A 48.394 48.394 0 0 0 4.64 79.765 Q 9.28 89.525 17.6 96.485 Q 25.92 103.445 36.8 107.205 A 65.874 65.874 0 0 0 42.517 108.894 Z", id:"0", vectorEffect:"non-scaling-stroke" } }), + svg.path({ attrs: { transform:"translate(460,300)", d: "M 168.8 110.965 Q 157.76 110.965 147.76 107.205 Q 137.76 103.445 130 96.485 Q 122.24 89.525 117.84 79.845 A 49.402 49.402 0 0 1 113.682 63.757 A 59.923 59.923 0 0 1 113.44 58.325 Q 113.44 47.445 117.04 37.445 Q 120.64 27.445 127.84 19.605 A 50.961 50.961 0 0 1 142.36 8.694 A 60.247 60.247 0 0 1 145.68 7.125 A 54.565 54.565 0 0 1 159.472 3.251 A 72.704 72.704 0 0 1 170.24 2.485 Q 181.44 2.485 191.44 6.245 Q 201.44 10.005 209.12 16.965 Q 216.8 23.925 221.28 33.605 Q 225.76 43.285 225.76 55.125 Q 225.76 65.845 222.08 75.925 Q 218.4 86.005 211.2 93.845 A 50.961 50.961 0 0 1 196.68 104.757 A 60.247 60.247 0 0 1 193.36 106.325 A 54.565 54.565 0 0 1 179.569 110.2 A 72.704 72.704 0 0 1 168.8 110.965 Z M 171.68 103.605 A 26.844 26.844 0 0 0 179.094 102.625 A 21.365 21.365 0 0 0 186.96 98.485 A 27.514 27.514 0 0 0 193.442 90.523 A 37.424 37.424 0 0 0 196.16 84.245 Q 199.2 75.125 199.2 63.285 Q 199.2 53.525 197.36 44.005 Q 195.52 34.485 191.6 26.805 Q 187.68 19.125 181.6 14.565 Q 175.52 10.005 167.2 10.005 A 26.016 26.016 0 0 0 159.655 11.057 A 21.4 21.4 0 0 0 152.08 15.125 A 27.98 27.98 0 0 0 145.339 23.499 A 37.303 37.303 0 0 0 142.88 29.205 Q 139.932 37.894 139.843 49.291 A 91.218 91.218 0 0 0 139.84 50.005 Q 139.84 59.925 141.76 69.445 A 73.734 73.734 0 0 0 144.886 80.561 A 61.444 61.444 0 0 0 147.52 86.645 Q 151.36 94.325 157.36 98.965 A 22.185 22.185 0 0 0 169.511 103.523 A 28.279 28.279 0 0 0 171.68 103.605 Z", id:"1", vectorEffect:"non-scaling-stroke" } }), + svg.path({ attrs: { transform:"translate(460,300)", d: "M 280 18.965 L 303.52 75.125 A 1.737 1.737 0 0 0 303.731 75.456 Q 303.908 75.669 304.133 75.769 A 1.008 1.008 0 0 0 304.4 75.845 A 0.705 0.705 0 0 0 304.5 75.853 Q 304.88 75.853 305.14 75.414 A 2.079 2.079 0 0 0 305.28 75.125 L 328.16 22.005 Q 329.162 16.996 329.844 13.651 A 1135.171 1135.171 0 0 1 330 12.885 A 101.259 101.259 0 0 1 330.442 10.826 Q 330.92 8.715 331.36 7.285 A 3.896 3.896 0 0 1 331.783 6.339 Q 332.447 5.294 333.703 5.212 A 3.35 3.35 0 0 1 333.92 5.205 L 340.8 5.205 Q 347.52 4.885 354.8 4.165 Q 360.801 3.572 364.681 3.468 A 58.182 58.182 0 0 1 366.24 3.445 A 7.727 7.727 0 0 1 367.651 3.568 A 5.754 5.754 0 0 1 369.04 4.005 A 2.337 2.337 0 0 1 369.686 4.438 Q 370.18 4.917 370.234 5.659 A 2.568 2.568 0 0 1 370.24 5.845 A 3.116 3.116 0 0 1 370.139 6.667 A 2.064 2.064 0 0 1 369.2 7.925 Q 368.16 8.565 366.56 9.045 Q 365.825 9.29 364.903 9.676 A 38.566 38.566 0 0 0 364.32 9.925 A 60.68 60.68 0 0 0 363.262 10.4 Q 362.781 10.622 362.357 10.829 A 34.278 34.278 0 0 0 362.08 10.965 A 24.408 24.408 0 0 0 359.476 12.352 Q 358.179 13.153 357.193 14.035 A 11.367 11.367 0 0 0 355.92 15.365 Q 353.998 17.749 354.219 22.868 A 26.82 26.82 0 0 0 354.24 23.285 Q 354.56 34.805 355.04 43.445 Q 355.52 52.085 355.92 59.445 Q 356.32 66.805 356.88 74.725 Q 357.265 80.17 357.688 86.863 A 2486.888 2486.888 0 0 1 358.08 93.205 Q 358.4 97.365 361.52 99.845 A 17.172 17.172 0 0 0 364.885 101.907 Q 367.04 102.925 369.76 103.605 Q 371.36 104.085 372.48 104.725 A 2.102 2.102 0 0 1 373.55 106.233 A 3.129 3.129 0 0 1 373.6 106.805 Q 373.6 107.925 372.64 108.565 Q 371.68 109.205 370.24 109.205 A 124.521 124.521 0 0 1 366.961 109.159 Q 365.284 109.115 363.422 109.028 A 214.634 214.634 0 0 1 362.16 108.965 Q 357.6 108.725 353.2 108.565 Q 348.8 108.405 345.6 108.405 Q 342.4 108.405 338.48 108.565 Q 334.56 108.725 330.56 108.965 Q 326.56 109.205 322.88 109.205 A 6.326 6.326 0 0 1 321.48 109.056 A 5.104 5.104 0 0 1 320.32 108.645 A 2.421 2.421 0 0 1 319.742 108.252 A 1.688 1.688 0 0 1 319.2 106.965 A 2.958 2.958 0 0 1 319.371 105.938 A 2.56 2.56 0 0 1 320.32 104.725 Q 321.44 103.925 323.04 103.605 A 33.68 33.68 0 0 0 325.926 102.937 Q 328.739 102.142 330.36 101.01 A 6.886 6.886 0 0 0 331.2 100.325 Q 333.291 98.309 333.291 93.854 A 20.13 20.13 0 0 0 333.28 93.205 Q 332.8 83.285 332.48 73.445 Q 332.16 63.605 331.92 54.085 A 657.231 657.231 0 0 0 331.259 36.728 A 600.597 600.597 0 0 0 331.2 35.605 Q 331.2 34.485 330.64 34.565 A 0.762 0.762 0 0 0 330.265 34.743 Q 329.915 35.03 329.6 35.765 L 299.04 108.405 A 5.387 5.387 0 0 1 298.705 109.169 Q 297.956 110.543 296.715 110.339 A 2.202 2.202 0 0 1 296.64 110.325 A 3.756 3.756 0 0 1 295.619 109.974 A 2.786 2.786 0 0 1 294.4 108.725 Q 286.56 90.165 278 72.085 Q 269.44 54.005 262.24 36.245 A 3.156 3.156 0 0 0 262.06 35.806 Q 261.635 34.977 260.906 35.147 A 1.42 1.42 0 0 0 260.72 35.205 A 1.58 1.58 0 0 0 259.891 35.962 Q 259.736 36.247 259.641 36.626 A 4.851 4.851 0 0 0 259.52 37.365 Q 259.04 45.365 258.72 54.885 Q 258.4 64.405 258.4 74.085 L 258.4 92.085 A 11.336 11.336 0 0 0 258.894 95.507 A 9.135 9.135 0 0 0 261.28 99.365 A 12.265 12.265 0 0 0 263.913 101.274 Q 266.48 102.694 270.339 103.591 A 39.254 39.254 0 0 0 270.4 103.605 Q 272 103.925 273.44 104.645 A 3.112 3.112 0 0 1 274.184 105.151 A 2.171 2.171 0 0 1 274.88 106.805 Q 274.88 107.905 273.646 108.542 A 4.015 4.015 0 0 1 273.6 108.565 Q 272.32 109.205 270.88 109.205 Q 266.56 109.205 264.16 108.965 Q 261.76 108.725 259.84 108.645 Q 258.37 108.584 256.056 108.57 A 241.657 241.657 0 0 0 254.56 108.565 Q 251.04 108.565 247.36 108.725 Q 243.68 108.885 240.96 109.045 Q 238.738 109.176 237.798 109.2 A 14.405 14.405 0 0 1 237.44 109.205 Q 235.52 109.205 234.56 108.645 A 2.008 2.008 0 0 1 233.961 108.128 Q 233.652 107.721 233.608 107.159 A 2.45 2.45 0 0 1 233.6 106.965 A 2.295 2.295 0 0 1 233.708 106.238 Q 233.983 105.413 234.96 105.125 A 41.882 41.882 0 0 0 236.227 104.729 Q 236.874 104.516 237.593 104.261 A 74.939 74.939 0 0 0 238.08 104.085 A 14.817 14.817 0 0 0 243.664 100.558 A 17.927 17.927 0 0 0 244.56 99.605 Q 247.36 96.405 248 91.445 Q 248.66 86.385 249.358 79.699 A 808.068 808.068 0 0 0 250 73.285 Q 251.04 62.485 252.16 50.165 Q 253.28 37.845 254.08 25.845 Q 254.197 25.148 254.228 24.197 A 22.532 22.532 0 0 0 254.24 23.445 L 254.24 20.405 A 15.303 15.303 0 0 0 254.146 18.645 Q 254.044 17.77 253.834 17.044 A 6.444 6.444 0 0 0 253.36 15.845 Q 252.48 14.165 250.4 13.045 Q 248.48 11.765 246.24 10.885 Q 244 10.005 241.44 9.365 A 12.798 12.798 0 0 1 240.164 8.979 Q 239.543 8.753 239.037 8.48 A 5.984 5.984 0 0 1 238.4 8.085 Q 237.28 7.285 237.28 6.005 A 2.466 2.466 0 0 1 237.39 5.244 Q 237.655 4.425 238.56 4.085 A 7.122 7.122 0 0 1 239.913 3.733 Q 240.571 3.625 241.32 3.608 A 12.488 12.488 0 0 1 241.6 3.605 Q 244 3.605 247.52 3.765 Q 251.04 3.925 255.28 4.005 Q 259.52 4.085 263.92 4.245 Q 268.32 4.405 272 4.405 A 3.689 3.689 0 0 1 274.011 5.009 A 4.772 4.772 0 0 1 274.4 5.285 A 4.895 4.895 0 0 1 275.175 6.036 A 3.481 3.481 0 0 1 275.84 7.285 Q 276.96 10.325 277.92 13.125 Q 278.88 15.925 280 18.965 Z", id:"2", vectorEffect:"non-scaling-stroke" } }), + svg.path({ attrs: { transform:"translate(460,300)", d: "M 393.92 93.045 L 393.92 21.365 A 24.447 24.447 0 0 0 393.773 18.576 Q 393.438 15.671 392.344 13.937 A 6.247 6.247 0 0 0 392 13.445 A 6.811 6.811 0 0 0 390.327 11.947 Q 388.579 10.8 385.67 10.016 A 31.125 31.125 0 0 0 384.32 9.685 Q 382.496 9.264 381.656 8.536 A 2.551 2.551 0 0 1 381.44 8.325 A 3.112 3.112 0 0 1 380.834 7.37 A 2.829 2.829 0 0 1 380.64 6.325 Q 380.64 5.365 381.6 4.645 Q 382.444 4.013 383.782 3.936 A 6.615 6.615 0 0 1 384.16 3.925 Q 387.84 3.925 391.6 4.085 Q 395.36 4.245 398.96 4.485 A 109.621 109.621 0 0 0 403.304 4.692 A 91.493 91.493 0 0 0 405.76 4.725 A 63.208 63.208 0 0 0 408.952 4.649 Q 410.895 4.55 412.56 4.325 Q 415.52 3.925 419.12 3.525 A 57.801 57.801 0 0 1 422.034 3.282 Q 424.64 3.125 427.84 3.125 A 74.922 74.922 0 0 1 440.24 4.088 Q 446.789 5.189 452.105 7.548 A 37.317 37.317 0 0 1 458.8 11.365 A 27.236 27.236 0 0 1 467.187 20.771 Q 469.954 26.023 470.213 32.625 A 35.159 35.159 0 0 1 470.24 34.005 A 41.752 41.752 0 0 1 467.704 48.641 A 39.421 39.421 0 0 1 466 52.565 Q 461.76 61.045 453.2 66.245 Q 444.64 71.445 431.52 71.445 A 21.718 21.718 0 0 1 429.227 71.333 Q 426.719 71.065 425.2 70.165 Q 423.04 68.885 423.04 67.125 A 2.072 2.072 0 0 1 423.781 65.52 A 3.105 3.105 0 0 1 424.08 65.285 Q 424.729 64.836 425.284 64.667 A 2.186 2.186 0 0 1 425.92 64.565 A 13.015 13.015 0 0 1 427.32 64.637 Q 428.22 64.734 428.96 64.965 Q 430.24 65.365 432.48 65.365 A 9.839 9.839 0 0 0 440.581 61.31 A 17.306 17.306 0 0 0 442.64 57.845 A 33.299 33.299 0 0 0 444.803 51.238 Q 445.521 48.018 445.836 44.233 A 75.351 75.351 0 0 0 446.08 38.005 A 71.948 71.948 0 0 0 445.742 30.761 Q 444.99 23.366 442.573 18.803 A 16.941 16.941 0 0 0 441.28 16.725 A 15.331 15.331 0 0 0 430.194 10.295 A 20.798 20.798 0 0 0 427.84 10.165 Q 424.873 10.165 422.853 11.043 A 7.065 7.065 0 0 0 421.04 12.165 Q 418.982 13.94 418.75 17.414 A 13.668 13.668 0 0 0 418.72 18.325 L 418.56 92.725 A 15.111 15.111 0 0 0 418.774 95.363 Q 419.023 96.762 419.56 97.846 A 6.266 6.266 0 0 0 421.28 100.005 A 11.172 11.172 0 0 0 423.248 101.224 Q 425.345 102.269 428.593 103.106 A 53.266 53.266 0 0 0 430.72 103.605 A 9.395 9.395 0 0 1 431.734 103.844 Q 432.229 103.993 432.626 104.185 A 3.715 3.715 0 0 1 433.36 104.645 Q 434.221 105.35 434.24 106.744 A 4.638 4.638 0 0 1 434.24 106.805 A 2.515 2.515 0 0 1 434.121 107.6 A 2.037 2.037 0 0 1 433.44 108.565 A 2.681 2.681 0 0 1 432.532 109.03 Q 432.153 109.145 431.698 109.185 A 5.749 5.749 0 0 1 431.2 109.205 A 124.521 124.521 0 0 1 427.921 109.159 Q 426.244 109.115 424.382 109.028 A 214.634 214.634 0 0 1 423.12 108.965 Q 418.56 108.725 414.08 108.565 Q 409.6 108.405 406.4 108.405 Q 403.04 108.405 399.04 108.565 A 495.61 495.61 0 0 0 391.329 108.936 A 541.268 541.268 0 0 0 390.8 108.965 Q 386.56 109.205 383.04 109.205 A 4.947 4.947 0 0 1 381.858 109.07 A 3.961 3.961 0 0 1 380.8 108.645 A 2.008 2.008 0 0 1 380.201 108.128 Q 379.892 107.721 379.848 107.159 A 2.45 2.45 0 0 1 379.84 106.965 A 2.907 2.907 0 0 1 381.219 104.408 Q 381.748 104.056 382.48 103.81 A 8.312 8.312 0 0 1 383.2 103.605 Q 389.28 102.325 391.6 100.325 Q 393.778 98.448 393.912 93.681 A 22.691 22.691 0 0 0 393.92 93.045 Z", id:"3", vectorEffect:"non-scaling-stroke" } }), + svg.path({ attrs: { transform:"translate(460,300)", d: "M 540.16 71.445 L 511.84 71.445 A 3.161 3.161 0 0 0 511.112 71.524 A 2.113 2.113 0 0 0 510.08 72.085 A 2.62 2.62 0 0 0 509.653 72.663 Q 509.323 73.254 509.12 74.165 Q 508.48 76.405 507.68 78.645 Q 506.88 80.885 506.16 83.125 Q 505.44 85.365 504.8 87.605 Q 504.16 89.845 503.52 91.925 A 16.266 16.266 0 0 0 503.252 93.343 Q 502.719 97.117 504.48 99.285 A 7.072 7.072 0 0 0 506.138 100.721 Q 508.603 102.32 513.44 103.445 A 9.231 9.231 0 0 1 514.913 103.87 Q 517.28 104.795 517.28 106.805 A 2.25 2.25 0 0 1 517.172 107.524 A 1.797 1.797 0 0 1 516.4 108.485 Q 515.615 108.985 514 109.039 A 12.054 12.054 0 0 1 513.6 109.045 Q 508.96 109.045 504.96 108.805 A 128.917 128.917 0 0 0 500.76 108.625 A 168.958 168.958 0 0 0 496.16 108.565 A 86.201 86.201 0 0 0 489.508 108.813 A 74.292 74.292 0 0 0 487.04 109.045 Q 482.72 109.525 477.76 109.525 A 9.189 9.189 0 0 1 476.471 109.44 Q 475.532 109.307 474.8 108.965 Q 473.804 108.5 473.635 107.429 A 2.973 2.973 0 0 1 473.6 106.965 Q 473.6 105.365 474.64 104.645 A 6.568 6.568 0 0 1 475.583 104.11 Q 476.382 103.734 477.44 103.445 A 23.679 23.679 0 0 0 483.013 101.145 A 20.837 20.837 0 0 0 485.52 99.445 Q 488.96 96.725 490.72 92.725 Q 494.72 83.765 498.24 75.045 Q 501.76 66.325 505.12 57.765 Q 508.48 49.205 511.84 40.325 Q 515.2 31.445 518.56 22.325 Q 520.37 17.38 521.179 14.48 A 29.609 29.609 0 0 0 521.6 12.805 Q 522.214 9.968 522.533 9.409 A 0.757 0.757 0 0 1 522.56 9.365 Q 525.44 8.565 528 7.925 A 41.102 41.102 0 0 0 530.982 7.053 A 52.081 52.081 0 0 0 533.44 6.165 Q 536.32 5.045 538.32 3.285 Q 539.852 1.938 540.773 1.059 A 29.826 29.826 0 0 0 541.28 0.565 Q 541.92 0.085 542.56 0.005 A 0.687 0.687 0 0 1 542.646 0 Q 543.149 0 543.653 0.743 A 4.663 4.663 0 0 1 543.84 1.045 Q 544.32 2.005 544.72 3.045 A 17.874 17.874 0 0 1 545.145 4.291 A 23.023 23.023 0 0 1 545.44 5.365 Q 548.8 16.085 552.32 27.205 Q 555.84 38.325 559.44 49.365 Q 563.04 60.405 566.56 71.205 Q 570.08 82.005 573.44 92.245 Q 574.4 95.445 576.4 97.685 Q 578.4 99.925 580.96 101.205 A 32.783 32.783 0 0 0 584.226 102.613 A 41.388 41.388 0 0 0 586.72 103.445 A 13.072 13.072 0 0 1 587.892 103.821 Q 588.463 104.037 588.93 104.287 A 6.08 6.08 0 0 1 589.52 104.645 A 2.343 2.343 0 0 1 590.532 106.352 A 3.539 3.539 0 0 1 590.56 106.805 Q 590.56 107.925 589.6 108.565 A 3.817 3.817 0 0 1 587.81 109.185 A 4.8 4.8 0 0 1 587.36 109.205 Q 584.198 109.205 579.552 108.971 A 271.839 271.839 0 0 1 579.44 108.965 Q 574.72 108.725 569.92 108.565 Q 565.795 108.428 562.497 108.408 A 180.368 180.368 0 0 0 561.44 108.405 Q 558.4 108.405 554 108.565 Q 549.6 108.725 545.12 108.885 Q 540.64 109.045 537.44 109.045 A 6.221 6.221 0 0 1 536.257 108.938 A 4.536 4.536 0 0 1 534.96 108.485 Q 533.958 107.946 533.922 106.886 A 2.348 2.348 0 0 1 533.92 106.805 A 2.958 2.958 0 0 1 534.091 105.778 A 2.56 2.56 0 0 1 535.04 104.565 Q 536.16 103.765 537.76 103.445 A 40.685 40.685 0 0 0 540.837 102.662 Q 544.445 101.58 546.4 100.085 Q 548.799 98.251 548.336 94.8 A 10.393 10.393 0 0 0 548.16 93.845 Q 547.52 90.805 546.56 87.445 Q 545.6 84.085 544.72 80.645 A 170.044 170.044 0 0 0 543.119 74.809 A 155.128 155.128 0 0 0 542.88 74.005 Q 542.568 72.755 542.102 72.115 A 2.548 2.548 0 0 0 542.08 72.085 A 1.317 1.317 0 0 0 541.562 71.685 Q 541.123 71.483 540.43 71.451 A 5.877 5.877 0 0 0 540.16 71.445 Z M 516.32 62.805 L 537.12 62.805 A 7.767 7.767 0 0 0 537.905 62.769 Q 538.69 62.689 539.094 62.431 A 0.844 0.844 0 0 0 539.52 61.685 Q 539.52 61.329 539.476 61.06 A 2.187 2.187 0 0 0 539.44 60.885 A 7.273 7.273 0 0 0 539.376 60.646 Q 539.341 60.523 539.297 60.383 A 16.936 16.936 0 0 0 539.2 60.085 L 528.48 23.925 Q 528 22.165 527.52 22.165 Q 527.232 22.165 526.554 23.594 A 22.258 22.258 0 0 0 526.4 23.925 L 513.76 60.085 A 3.082 3.082 0 0 0 513.502 60.805 A 2.614 2.614 0 0 0 513.44 61.365 A 1.102 1.102 0 0 0 513.584 61.934 Q 513.717 62.159 513.971 62.318 A 1.85 1.85 0 0 0 514.32 62.485 A 5.205 5.205 0 0 0 515.418 62.748 A 6.869 6.869 0 0 0 516.32 62.805 Z", id:"4", vectorEffect:"non-scaling-stroke" } }), + svg.path({ attrs: { transform:"translate(460,300)", d: "M 624.48 110.965 Q 617.6 110.965 611.2 109.365 Q 604.8 107.765 599.2 104.245 Q 598.24 103.445 597.28 102.485 A 7.09 7.09 0 0 1 596.273 101.231 A 6.069 6.069 0 0 1 595.84 100.405 Q 594.24 96.085 593.68 89.845 A 109.626 109.626 0 0 1 593.245 80.042 A 113.493 113.493 0 0 1 593.28 77.205 Q 593.28 76.15 594.133 75.663 A 2.198 2.198 0 0 1 594.24 75.605 Q 595.2 75.125 596.48 75.125 Q 598.08 75.125 598.64 75.525 Q 599.2 75.925 599.84 77.045 A 56.914 56.914 0 0 0 603.579 86.657 A 40.784 40.784 0 0 0 610.32 96.405 A 24.816 24.816 0 0 0 616.606 101.305 A 20.744 20.744 0 0 0 626.56 103.765 Q 631.68 103.765 634.96 101.445 Q 638.24 99.125 640 95.525 A 17.225 17.225 0 0 0 641.55 90.78 A 15.4 15.4 0 0 0 641.76 88.245 A 21.789 21.789 0 0 0 641.43 84.349 Q 641.012 82.048 640.063 80.17 A 13.127 13.127 0 0 0 639.76 79.605 A 18.002 18.002 0 0 0 636.877 75.848 A 24.514 24.514 0 0 0 633.76 73.205 Q 631.01 71.225 627.202 69.018 A 135.313 135.313 0 0 0 623.52 66.965 Q 616.8 63.125 610.56 58.885 Q 604.32 54.645 600.32 48.565 Q 596.428 42.649 596.323 33.853 A 41.035 41.035 0 0 1 596.32 33.365 Q 596.32 23.925 601.2 17.045 Q 606.08 10.165 614.56 6.325 A 43.184 43.184 0 0 1 627.747 2.798 A 53.582 53.582 0 0 1 633.6 2.485 Q 639.68 2.485 645.2 3.765 Q 650.72 5.045 654.08 6.325 Q 656 7.125 656.88 8.085 A 4.088 4.088 0 0 1 657.401 8.815 Q 657.958 9.786 658.4 11.445 A 51.999 51.999 0 0 1 658.967 14.168 Q 659.244 15.691 659.485 17.434 A 103.878 103.878 0 0 1 659.76 19.605 Q 660.32 24.405 660.32 30.165 Q 660.32 32.245 657.44 32.245 Q 656 32.245 654.8 31.525 Q 653.6 30.805 653.28 29.685 A 43.999 43.999 0 0 0 651.108 23.634 Q 649.65 20.391 647.772 17.91 A 22.151 22.151 0 0 0 645.28 15.125 A 18.127 18.127 0 0 0 632.798 10.17 A 23.441 23.441 0 0 0 632.32 10.165 A 24.049 24.049 0 0 0 628.658 10.429 Q 626.644 10.74 624.959 11.416 A 13.834 13.834 0 0 0 624.16 11.765 Q 620.8 13.365 619.12 16.565 A 13.649 13.649 0 0 0 617.914 19.917 Q 617.44 22.015 617.44 24.565 A 10.869 10.869 0 0 0 619.444 30.83 A 13.95 13.95 0 0 0 620.08 31.685 Q 622.72 34.965 627.68 38.325 Q 632.562 41.632 638.838 45.404 A 336.973 336.973 0 0 0 639.04 45.525 Q 645.655 49.445 650.207 53.272 A 49.549 49.549 0 0 1 652.8 55.605 A 36.832 36.832 0 0 1 657.162 60.623 A 28.158 28.158 0 0 1 660.24 66.085 A 29.077 29.077 0 0 1 662.163 73.131 A 38.837 38.837 0 0 1 662.56 78.805 A 41.422 41.422 0 0 1 661.84 86.744 Q 660.892 91.599 658.696 95.481 A 25.101 25.101 0 0 1 657.84 96.885 A 27.512 27.512 0 0 1 645.745 107.023 A 33.765 33.765 0 0 1 644.56 107.525 A 45.856 45.856 0 0 1 634.709 110.197 Q 630.224 110.917 625.158 110.962 A 76.284 76.284 0 0 1 624.48 110.965 Z", id:"5", vectorEffect:"non-scaling-stroke" } }), + svg.path({ attrs: { transform:"translate(460,300)", d: "M 703.52 110.965 Q 696.64 110.965 690.24 109.365 Q 683.84 107.765 678.24 104.245 Q 677.28 103.445 676.32 102.485 A 7.09 7.09 0 0 1 675.313 101.231 A 6.069 6.069 0 0 1 674.88 100.405 Q 673.28 96.085 672.72 89.845 A 109.626 109.626 0 0 1 672.285 80.042 A 113.493 113.493 0 0 1 672.32 77.205 Q 672.32 76.15 673.173 75.663 A 2.198 2.198 0 0 1 673.28 75.605 Q 674.24 75.125 675.52 75.125 Q 677.12 75.125 677.68 75.525 Q 678.24 75.925 678.88 77.045 A 56.914 56.914 0 0 0 682.619 86.657 A 40.784 40.784 0 0 0 689.36 96.405 A 24.816 24.816 0 0 0 695.646 101.305 A 20.744 20.744 0 0 0 705.6 103.765 Q 710.72 103.765 714 101.445 Q 717.28 99.125 719.04 95.525 A 17.225 17.225 0 0 0 720.59 90.78 A 15.4 15.4 0 0 0 720.8 88.245 A 21.789 21.789 0 0 0 720.47 84.349 Q 720.052 82.048 719.103 80.17 A 13.127 13.127 0 0 0 718.8 79.605 A 18.002 18.002 0 0 0 715.917 75.848 A 24.514 24.514 0 0 0 712.8 73.205 Q 710.05 71.225 706.242 69.018 A 135.313 135.313 0 0 0 702.56 66.965 Q 695.84 63.125 689.6 58.885 Q 683.36 54.645 679.36 48.565 Q 675.468 42.649 675.363 33.853 A 41.035 41.035 0 0 1 675.36 33.365 Q 675.36 23.925 680.24 17.045 Q 685.12 10.165 693.6 6.325 A 43.184 43.184 0 0 1 706.787 2.798 A 53.582 53.582 0 0 1 712.64 2.485 Q 718.72 2.485 724.24 3.765 Q 729.76 5.045 733.12 6.325 Q 735.04 7.125 735.92 8.085 A 4.088 4.088 0 0 1 736.441 8.815 Q 736.998 9.786 737.44 11.445 A 51.999 51.999 0 0 1 738.007 14.168 Q 738.284 15.691 738.525 17.434 A 103.878 103.878 0 0 1 738.8 19.605 Q 739.36 24.405 739.36 30.165 Q 739.36 32.245 736.48 32.245 Q 735.04 32.245 733.84 31.525 Q 732.64 30.805 732.32 29.685 A 43.999 43.999 0 0 0 730.148 23.634 Q 728.69 20.391 726.812 17.91 A 22.151 22.151 0 0 0 724.32 15.125 A 18.127 18.127 0 0 0 711.838 10.17 A 23.441 23.441 0 0 0 711.36 10.165 A 24.049 24.049 0 0 0 707.698 10.429 Q 705.684 10.74 703.999 11.416 A 13.834 13.834 0 0 0 703.2 11.765 Q 699.84 13.365 698.16 16.565 A 13.649 13.649 0 0 0 696.954 19.917 Q 696.48 22.015 696.48 24.565 A 10.869 10.869 0 0 0 698.484 30.83 A 13.95 13.95 0 0 0 699.12 31.685 Q 701.76 34.965 706.72 38.325 Q 711.602 41.632 717.878 45.404 A 336.973 336.973 0 0 0 718.08 45.525 Q 724.695 49.445 729.247 53.272 A 49.549 49.549 0 0 1 731.84 55.605 A 36.832 36.832 0 0 1 736.202 60.623 A 28.158 28.158 0 0 1 739.28 66.085 A 29.077 29.077 0 0 1 741.203 73.131 A 38.837 38.837 0 0 1 741.6 78.805 A 41.422 41.422 0 0 1 740.88 86.744 Q 739.932 91.599 737.736 95.481 A 25.101 25.101 0 0 1 736.88 96.885 A 27.512 27.512 0 0 1 724.785 107.023 A 33.765 33.765 0 0 1 723.6 107.525 A 45.856 45.856 0 0 1 713.749 110.197 Q 709.264 110.917 704.198 110.962 A 76.284 76.284 0 0 1 703.52 110.965 Z", id:"6", vectorEffect:"non-scaling-stroke" } }), svg.rect({ attrs: { x: "466", y: "240.5", width: "800", height: "15.5" } }), // svg.text({ attrs: { 'font-family': "Calibri", 'font-size': "40", 'font-weight': "bold", x: "497", y: "450" } }, 'COMPUTATIONAL SCIENCES'), // svg.path({ attrs: { d: "M 1111.1316 329.63885 C 1111.7629 340.6366 1119.8291 350.53146 1131.0461 350.7098 C 1142.2632 350.8882 1154.6805 342.4446 1153.2188 331.05904 C 1151.7572 319.67348 1142.7102 319.24054 1132.1177 316.17612 C 1121.5253 313.11165 1130.3999 308.0055 1130.3999 308.0055 C 1130.3999 308.0055 1134.739 305.7016 1139.3956 304.96687 C 1144.0522 304.23215 1147.434 306.17886 1149.1073 306.1567 C 1150.7806 306.13453 1150.2077 300.1209 1149.0792 298.99964 C 1147.9508 297.87838 1138.8401 295.20424 1130.1026 299.3452 C 1121.365 303.48615 1118.0291 308.5461 1118.0291 308.5461 C 1118.0291 308.5461 1110.5002 318.6411 1111.1316 329.63885 Z" } }), @@ -352,7 +359,7 @@ const homeSVG = svg({ attrs: { 'vertical-align': 'baseline', height: '30pt', wid }), svg.text([ svg.textPath({ attrs: { 'xlink:href': "#target", startOffset: "80%" } }, [ - svg.tspan({ attrs: { 'font-family': "Roboto", 'font-size': "60", 'text-anchor': "middle", 'letter-spacing': 15, 'font-weight': "bold", fill: "#ebebeb", dy: "-20" } }, "TARGET") + svg.tspan({ attrs: { 'font-family': '"Roboto", Arial, Helvetica, sans-serif', 'font-size': "60", 'text-anchor': "middle", 'letter-spacing': 15, 'font-weight': "bold", fill: "#ebebeb", dy: "-20" } }, "TARGET") ]) ]), svg.path({ @@ -377,7 +384,7 @@ const homeSVG = svg({ attrs: { 'vertical-align': 'baseline', height: '30pt', wid }), svg.text([ svg.textPath({ attrs: { 'xlink:href': "#disease", startOffset: "80%" } }, [ - svg.tspan({ attrs: { 'font-family': "Roboto", 'font-size': "60", 'text-anchor': "middle", 'letter-spacing': 15, 'font-weight': "bold", fill: "#ebebeb", dy: "-20" } }, "DISEASE") + svg.tspan({ attrs: { 'font-family': '"Roboto", Arial, Helvetica, sans-serif', 'font-size': "60", 'text-anchor': "middle", 'letter-spacing': 15, 'font-weight': "bold", fill: "#ebebeb", dy: "-20" } }, "DISEASE") ]) ]), svg.path({ @@ -402,7 +409,7 @@ const homeSVG = svg({ attrs: { 'vertical-align': 'baseline', height: '30pt', wid }), svg.text([ svg.textPath({ attrs: { 'xlink:href': "#compound", startOffset: "29%" } }, [ - svg.tspan({ attrs: { 'font-family': "Roboto", 'font-size': "60", 'text-anchor': "middle", 'letter-spacing': 15, 'font-weight': "bold", fill: "#ebebeb", dy: "-20" } }, "COMPOUND") + svg.tspan({ attrs: { 'font-family': '"Roboto", Arial, Helvetica, sans-serif', 'font-size': "60", 'text-anchor': "middle", 'letter-spacing': 15, 'font-weight': "bold", fill: "#ebebeb", dy: "-20" } }, "COMPOUND") ]) ]), svg.path({ @@ -421,7 +428,7 @@ const homeSVG = svg({ attrs: { 'vertical-align': 'baseline', height: '30pt', wid svg.path({ attrs: { d: "M 1506.0454 251.9644 L 1506.0454 -.035595944 C 1645.2211 -.035595944 1758.0454 112.78865 1758.0454 251.9644 C 1758.0454 296.19964 1746.4014 339.65556 1724.2838 377.9644 Z", fill: "#e92363", 'fill-opacity': ".5" } }), svg.path({ attrs: { d: "M 1506.0454 251.9644 L 1506.0454 -.035595944 C 1645.2211 -.035595944 1758.0454 112.78865 1758.0454 251.9644 C 1758.0454 296.19964 1746.4014 339.65556 1724.2838 377.9644 Z", stroke: "white", 'stroke-linecap': "round", 'stroke-linejoin': "round", 'stroke-width': "6", 'fill-opacity': '0' } }), svg.text({ attrs: { transform: "translate(1529.807 150.726)", fill: "white" } }, [ - svg.tspan({ attrs: { 'font-family': "Roboto", 'font-size': "60", 'font-weight': "bold", fill: "white", x: ".18359375", y: "56", textLength: "198.63281" } }, 'PHENO') + svg.tspan({ attrs: { 'font-family': '"Roboto", Arial, Helvetica, sans-serif', 'font-size': "60", 'font-weight': "bold", fill: "white", x: ".18359375", y: "56", textLength: "198.63281" } }, 'PHENO') ]), svg.path({ attrs: { d: "M 1506.1296 251.9644 L 1724.368 377.9644 C 1654.78 498.49415 1500.6593 539.7907 1380.1296 470.2028 C 1341.8207 448.0852 1310.0088 416.27324 1287.8912 377.9644 Z", fill: "#fe9801", 'fill-opacity': ".5" } }), @@ -433,12 +440,12 @@ const homeSVG = svg({ attrs: { 'vertical-align': 'baseline', height: '30pt', wid svg.path({ attrs: { d: "M 1506.0454 251.9644 L 1287.807 377.9644 C 1218.2191 257.43466 1259.5156 103.31388 1380.0454 33.726002 C 1418.3542 11.608383 1461.8101 -.035595944 1506.0454 -.035595944 Z", stroke: "white", 'stroke-linecap': "round", 'stroke-linejoin': "round", 'stroke-width': "6", 'fill-opacity': '0' } }), svg.text({ attrs: { transform: "translate(1309.807 150.726)", fill: "white" } }, [ - svg.tspan({ attrs: { 'font-family': "Roboto", 'font-size': "60", 'font-weight': "bold", fill: "white", x: ".29589844", y: "56", textLength: "158.4082" } }, 'GENO') + svg.tspan({ attrs: { 'font-family': '"Roboto", Arial, Helvetica, sans-serif', 'font-size': "60", 'font-weight': "bold", fill: "white", x: ".29589844", y: "56", textLength: "158.4082" } }, 'GENO') ]), svg.text({ attrs: { transform: "translate(1406.807 358.726)", fill: "white" } }, [ - svg.tspan({ attrs: { 'font-family': "Roboto", 'font-size': "60", 'font-weight': "bold", fill: "white", x: ".3076172", y: "56", textLength: "209.38477" } }, 'CHEMO') + svg.tspan({ attrs: { 'font-family': '"Roboto", Arial, Helvetica, sans-serif', 'font-size': "60", 'font-weight': "bold", fill: "white", x: ".3076172", y: "56", textLength: "209.38477" } }, 'CHEMO') ]), From fd514f70996dc598b800a50eb655eb21eae614e2 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 15 Dec 2021 15:00:08 +0100 Subject: [PATCH 066/191] Configure webpack to provide inline source maps Solves browsers giving warnings and provides proper source code lines for eventual code warnings/errors --- webpack.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/webpack.config.js b/webpack.config.js index 262e15f8..850b9e60 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -10,6 +10,7 @@ module.exports = { publicPath: '/dist/', filename: 'bundle.js', }, + devtool: 'inline-source-map', devServer: { static: { directory: './', From a9d6cfd89bfcce31f10b4914819d754bd5e76efb Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 16 Dec 2021 17:04:30 +0100 Subject: [PATCH 067/191] Fix ghost scenarios causing issues when url paths get changed (#90) Default reducer in form was wrong --- src/js/components/TreatmentForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/components/TreatmentForm.js b/src/js/components/TreatmentForm.js index 1d58bd70..5c4c7020 100644 --- a/src/js/components/TreatmentForm.js +++ b/src/js/components/TreatmentForm.js @@ -48,7 +48,7 @@ function TreatmentForm(sources) { const defaultReducer$ = xs.of((prevState) => { // treatmentForm -- default Reducer - return { ...prevState, form: {}, sampleSelection: {}, signature: {} } + return { ...prevState, check: {}, sampleSelection: {}, signature: {} } }) return { From 4c1b5698ae3f0be94d4e2788f712ea87e5df0b82 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 16 Dec 2021 17:05:29 +0100 Subject: [PATCH 068/191] Display time unit and dose unit in tables (#92) Additionally fix length difference in id truncation, happened in e137c16cc7e896c51e8fde8213ac5f11950da64c --- src/js/components/SampleSelection.js | 12 +++++++----- src/js/components/SampleTable/SampleInfo.js | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/js/components/SampleSelection.js b/src/js/components/SampleSelection.js index 39e1074e..26a4fa69 100644 --- a/src/js/components/SampleSelection.js +++ b/src/js/components/SampleSelection.js @@ -159,16 +159,18 @@ function SampleSelection(sources) { ), td( ".left-align" + selectedClass(entry.use), - entry.id.length > 30 ? entry.id.substring(0, 40) + "..." : entry.id + entry.id.length > 40 ? entry.id.substring(0, 40) + "..." : entry.id ), td(selectedClass(entry.use), entry.cell), - td( - selectedClass(entry.use), - entry.dose.length > 6 ? entry.dose.substring(0, 6) + "..." : entry.dose + td(selectedClass(entry.use), + ((_) => { + const dose = entry.dose !== "N/A" ? entry.dose + " " + entry.dose_unit : entry.dose + return dose.length > 6 ? dose.substring(0, 6) + "..." : dose + })() ), // td(selectedClass(entry.use), entry.batch), // td(selectedClass(entry.use), entry.year), - td(selectedClass(entry.use), entry.time), + td(selectedClass(entry.use), entry.time !== "N/A" ? entry.time + " " + entry.time_unit : entry.time), td(selectedClass(entry.use), entry.significantGenes), ]) const header = tr([ diff --git a/src/js/components/SampleTable/SampleInfo.js b/src/js/components/SampleTable/SampleInfo.js index 9e756e0c..252b0322 100644 --- a/src/js/components/SampleTable/SampleInfo.js +++ b/src/js/components/SampleTable/SampleInfo.js @@ -315,8 +315,8 @@ export function SampleInfo(sources) { p(".col .s12 .grey-text", hStyle, "Sample Info:"), p(pStyle, entry("Sample ID: ", sample.id)), p(pStyle, entry("Cell: ", sample.cell)), - p(pStyle, entry("Dose: ", sample.dose)), - p(pStyle, entry("Time: ", sample.time)), + p(pStyle, entry("Dose: ", sample.dose !== "N/A" ? sample.dose + " " + sample.dose_unit : sample.dose_unit)), + p(pStyle, entry("Time: ", sample.time !== "N/A" ? sample.time + " " + sample.time_unit : sample.time)), p(pStyle, entry("Year: ", sample.year)), p(pStyle, entry("Plate: ", sample.plate)), ] From 3a4e41ce5b8bdd3b62c20f0168529b865d1a630e Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 16 Dec 2021 17:05:59 +0100 Subject: [PATCH 069/191] Rename key words on the statistics page to new nomenclature (#93) protocol -> cell concentration -> dose compound -> treatment --- src/js/components/Statistics.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/js/components/Statistics.js b/src/js/components/Statistics.js index b42fb356..bd113dd4 100644 --- a/src/js/components/Statistics.js +++ b/src/js/components/Statistics.js @@ -86,19 +86,19 @@ function Statistics(sources) { div('.col .s4 .center-align', [p(data.informative.total)]) ]), div('.row', [ - div('.col .s4', [p(['# unique compounds'])]), - div('.col .s4', [p('', { style : { fontSize : 'small' }}, extracts(data.compounds.sample))]), - div('.col .s4 .center-align', [p(data.compounds.total)]) + div('.col .s4', [p(['# unique treatments'])]), + div('.col .s4', [p('', { style : { fontSize : 'small' }}, extracts(data.treatments.sample))]), + div('.col .s4 .center-align', [p(data.treatments.total)]) ]), div('.row', [ - div('.col .s4', [p(['# concentrations'])]), - div('.col .s4', [p('', { style : { fontSize : 'small' }}, extracts(data.concentrations.sample))]), - div('.col .s4 .center-align', [p(data.concentrations.total)]) + div('.col .s4', [p(['# doses'])]), + div('.col .s4', [p('', { style : { fontSize : 'small' }}, extracts(data.doses.sample))]), + div('.col .s4 .center-align', [p(data.doses.total)]) ]), div('.row', [ - div('.col .s4', [p(['# protocols'])]), - div('.col .s4', [p('', { style : { fontSize : 'small' }}, extracts(data.protocols.sample))]), - div('.col .s4 .center-align', [p(data.protocols.total)]) + div('.col .s4', [p(['# cells'])]), + div('.col .s4', [p('', { style : { fontSize : 'small' }}, extracts(data.cells.sample))]), + div('.col .s4 .center-align', [p(data.cells.total)]) ]), div('.row', [ div('.col .s4', [p(['# types'])]), From 3ba76bc60da152104f76546451c30010ec21a500 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 22 Dec 2021 17:58:24 +0100 Subject: [PATCH 070/191] Properly handle missing dose_unit or dose_time (#99) Handle missing dose_unit or time_unit by printing "?" instead Make entry and entrySmall more robust for invalid data by using optional chaining; we can't call .length on undefined --- src/js/components/SampleTable/SampleInfo.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/js/components/SampleTable/SampleInfo.js b/src/js/components/SampleTable/SampleInfo.js index 252b0322..93ed07ae 100644 --- a/src/js/components/SampleTable/SampleInfo.js +++ b/src/js/components/SampleTable/SampleInfo.js @@ -82,7 +82,7 @@ export function SampleInfo(sources) { span( ".col .s8", { style: { overflow: "hidden", "text-overflow": "ellipsis" } }, - value.length != 0 ? value : "" + value?.length != 0 ? value : "" ), ] } @@ -90,7 +90,7 @@ export function SampleInfo(sources) { function entrySmall(key, value) { return [ span(".col .s4 .m2", { style: { fontWeight: "lighter" } }, key), - span(".col .s8 .m4", value.length != 0 ? value : ""), + span(".col .s8 .m4", value?.length != 0 ? value : ""), ] } @@ -315,8 +315,8 @@ export function SampleInfo(sources) { p(".col .s12 .grey-text", hStyle, "Sample Info:"), p(pStyle, entry("Sample ID: ", sample.id)), p(pStyle, entry("Cell: ", sample.cell)), - p(pStyle, entry("Dose: ", sample.dose !== "N/A" ? sample.dose + " " + sample.dose_unit : sample.dose_unit)), - p(pStyle, entry("Time: ", sample.time !== "N/A" ? sample.time + " " + sample.time_unit : sample.time)), + p(pStyle, entry("Dose: ", sample.dose !== "N/A" ? sample.dose + " " + (sample?.dose_unit ?? "?") : sample.dose)), + p(pStyle, entry("Time: ", sample.time !== "N/A" ? sample.time + " " + (sample?.time_unit ?? "?") : sample.time)), p(pStyle, entry("Year: ", sample.year)), p(pStyle, entry("Plate: ", sample.plate)), ] From ce6ce200926a5fd5a6e049f2ca75a93ea4c067cd Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 22 Dec 2021 17:59:09 +0100 Subject: [PATCH 071/191] Create sidenav driver (#91) * Start to put things together and get calls in more or less correct position Still calling the original js code during driver creation but allows more flexibility during testing * Clean up code & function calls Calling the init function once does everything we ever need Delay init until first time pressing the icon/opening the menu * Remove standard event handlers and use syntax to only create a single sidenav instead of an array * Remove unused sidenav$ in index.js sidenav code requires html elements instead of vdom --- index.html | 1 - navbar.js | 11 -------- src/js/drivers/makeSidenavDriver.js | 42 +++++++++++++++++++++++++++++ src/js/index.js | 9 +++---- src/js/main.js | 2 ++ 5 files changed, 48 insertions(+), 17 deletions(-) delete mode 100644 navbar.js create mode 100644 src/js/drivers/makeSidenavDriver.js diff --git a/index.html b/index.html index 7bfc9bba..eafbff84 100644 --- a/index.html +++ b/index.html @@ -17,7 +17,6 @@
- diff --git a/navbar.js b/navbar.js deleted file mode 100644 index 3cc0fb80..00000000 --- a/navbar.js +++ /dev/null @@ -1,11 +0,0 @@ - -// document.addEventListener('DOMContentLoaded', function() { -// var elems = document.querySelectorAll('.sidenav'); -// var instances = M.Sidenav.init(elems, options); -// }); - - // Or with jQuery - - $(document).ready(function(){ - $('.sidenav').sidenav(); - }); \ No newline at end of file diff --git a/src/js/drivers/makeSidenavDriver.js b/src/js/drivers/makeSidenavDriver.js new file mode 100644 index 00000000..95ef575f --- /dev/null +++ b/src/js/drivers/makeSidenavDriver.js @@ -0,0 +1,42 @@ +import * as M from 'materialize-css' + +function makeSidenavDriver() { + + var sidenav = undefined + + function sidenavDriver(in$) { + + in$.addListener({ + next: (nav) => { + if (nav.state == 'open') { + + if (sidenav == undefined) { + const elem = document.querySelector(nav.element) + sidenav = M.Sidenav.init(elem) + // We handle opening of the sidenav ourselves. + // Unless also implemented, this removes swipe-closing support though, which is one option to close the sidenav on mobile. + // Opening sidenav creates an overlay which gets an on-click listener, which is used to close the sidenav + // This is not removed by this call. + sidenav._removeEventHandlers() + } + + sidenav.open() + } + else if (nav.state == 'close') { + // can be used to e.g. add a button in the sidenav that closes the sidenav + if (sidenav != undefined) + sidenav.close() + } + }, + error: (e) => { + console.error(e) + } + }) + + } + + return sidenavDriver + +} + +export { makeSidenavDriver }; \ No newline at end of file diff --git a/src/js/index.js b/src/js/index.js index ae7ca1f2..27281383 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -24,8 +24,6 @@ import { initSettings } from './configuration.js' import initDeployments from '../../deployments.json' import { loggerFactory } from './utils/logger' -import { navbarModule } from "../../navbar.js"; - export default function Index(sources) { const {router} = sources; @@ -144,9 +142,9 @@ export default function Index(sources) { ]) }) - - - + const sidenavTrigger$ = sources.DOM.select('.sidenav-trigger').events('click') + const sidenavEvent$ = sidenavTrigger$ + .map((trigger) => ({element: '.sidenav', state: 'open'})) // We combine with state in order to read the customizations // This works because the defaultReducer runs before anything else @@ -309,6 +307,7 @@ export default function Index(sources) { popup: page$.map(prop('popup')).filter(Boolean).flatten(), modal: page$.map(prop('modal')).filter(Boolean).flatten(), ac: page$.map(prop('ac')).filter(Boolean).flatten(), + sidenav: sidenavEvent$, storage: page$.map(prop('storage')).filter(Boolean).flatten(), deployments: page$.map(prop('deployments')).filter(Boolean).flatten() } diff --git a/src/js/main.js b/src/js/main.js index f993c1c8..389adb8a 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -23,6 +23,7 @@ import { preventDefaultDriver } from './drivers/preventDefaultDriver'; import switchPath from 'switch-path' import { makeModalDriver } from './drivers/makeModalDriver' import { makeAutocompleteDriver } from './drivers/makeAutocompleteDriver'; +import { makeSidenavDriver } from './drivers/makeSidenavDriver'; import './main.scss' import fromEvent from 'xstream/extra/fromEvent' @@ -44,6 +45,7 @@ const drivers = { log: logDriver, modal: makeModalDriver(), ac: makeAutocompleteDriver(), + sidenav: makeSidenavDriver(), deployments: () => xs.fromPromise(fetch('/deployments.json').then(m => m.json())) }; From ade57ba640e77aff7c2e69a69d3f92b00a11e61c Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 22 Dec 2021 17:59:59 +0100 Subject: [PATCH 072/191] Update ghostmodes (#95) * Fix treatment ghost scenario filter value 'not sticking' The filter component works on 'filter_output' to output the state Setting 'output' is needed to set the UI state * Rework disease ghostmode to new values and updated component behaviour --- src/js/scenarios/diseaseScenario.js | 121 +++++++++++++++----------- src/js/scenarios/treatmentScenario.js | 1 + 2 files changed, 72 insertions(+), 50 deletions(-) diff --git a/src/js/scenarios/diseaseScenario.js b/src/js/scenarios/diseaseScenario.js index 2bf31456..1dd61243 100644 --- a/src/js/scenarios/diseaseScenario.js +++ b/src/js/scenarios/diseaseScenario.js @@ -1,19 +1,19 @@ const filterValues = { - concentration: ['0.1', '1', '10', '30'], - protocol: ['MCF7', 'PBMC'], - type: ['test', 'poscon'] + dose: ["<= 4.0", "Other"], + cell: ["kidney", "haematopoietic", "adipose", "endometrium", "blood", "skin", "bone", "central nervous system", "lung", "muscle", "large intestine", "other", "breast", "liver", "prostate", "ovary", "stomach", "N/A"], + trtType: ["trt_cp", "trt_lig"] } -const filterMCF7 = { - concentration: ['0.1', '1', '10', '30'], - protocol: ['MCF7'], - type: ['test', 'poscon'] +const filterCell = { + dose: ['<= 4.0', 'Other'], + cell: ['breast'], + trtType: ['trt_cp', 'trt_lig'] } -const filterMCF7Conc = { - concentration: [ '1' ], - protocol: ['MCF7'], - type: ['test', 'poscon'] +const filterCellDose = { + dose: ['<= 4.0'], + cell: ['breast'], + trtType: ['trt_cp', 'trt_lig'] } export const scenario = [ @@ -123,10 +123,9 @@ export const scenario = [ filter: { input: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS', output: filterValues, - ghost: { expand: false } + filter_output: filterValues, + state: {dose: false, cell: false, trtType: false}, }, - headTable: { input: { query: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS' } }, - tailTable: { input: { query: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS' } }, }, message: { text: 'When ready, press play_arrow', @@ -136,77 +135,99 @@ export const scenario = [ // Wait a bit for everything to load... - { // Filter + { // open Filter #1 delay: 12000, state: { filter: { - input: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS', - output: filterMCF7, - ghost: { expand: true, deselect: {protocol: 'MCF'} } + state: {dose: true, cell: false, trtType: false}, // expand filters }, - headTable: { input: { filter: filterMCF7, query: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS' } }, - tailTable: { input: { filter: filterMCF7, query: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS' } }, }, message: { - text: 'Set filter to MCF7', + text: 'Open filters', duration: 7000 } }, - { - delay: 8000, + { // open Filter #2 + delay: 500, state: { filter: { - input: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS', - output: filterMCF7Conc, - ghost: { expand: true, deselect: {concentration: '0.1'} } + state: {dose: true, cell: true, trtType: false}, // expand filters + }, + }, + }, + { // open Filter #3 + delay: 500, + state: { + filter: { + state: {dose: true, cell: true, trtType: true}, // expand filters + }, + }, + }, + { // Select cell + delay: 4000, + state: { + filter: { + output: filterCell, + state: {dose: true, cell: true, trtType: true}, // expand filters }, - headTable: { input: { filter: filterMCF7Conc, query: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS' } }, - tailTable: { input: { filter: filterMCF7Conc, query: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS' } }, }, message: { - text: 'Set filter to 1mM', + text: 'Set filter to breast', duration: 7000 } }, - { - delay: 500, + { // select dose + delay: 4000, state: { filter: { - input: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS', - output: filterMCF7Conc, - ghost: { expand: true, deselect: {concentration: '10'} } + output: filterCellDose, + state: {dose: true, cell: true, trtType: true}, // expand filters }, - headTable: { input: { filter: filterMCF7Conc, query: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS' } }, - tailTable: { input: { filter: filterMCF7Conc, query: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS' } }, + }, + message: { + text: 'Set filter to <= 4.0', + duration: 7000 } }, - { - delay: 500, + { // close Filter #1 + delay: 4000, state: { filter: { - input: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS', - output: filterMCF7Conc, - ghost: { expand: true, deselect: {concentration: '30'} } + state: {dose: false, cell: true, trtType: true}, // expand filters }, - headTable: { input: { filter: filterMCF7Conc, query: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS' } }, - tailTable: { input: { filter: filterMCF7Conc, query: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS' } }, + }, + message: { + text: 'Close filters to apply filter values', + duration: 7000 } }, - { - delay: 6000, + { // close Filter #2 + delay: 500, state: { filter: { - input: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS', - output: filterMCF7Conc, - ghost: { expand: false } - } + state: {dose: false, cell: false, trtType: true}, // expand filters + }, }, + }, + { // close Filter #3 + delay: 500, + state: { + filter: { + output: filterCellDose, + filter_output: filterCellDose, + state: {dose: false, cell: false, trtType: false}, // expand filters + }, + }, + }, + { // output text + delay: 6000, + state: {}, message: { text: 'The corresponding plots are vizualised for the entire compound database', duration: 4000 } }, - { + { // output text delay: 3000, state: {}, message: { diff --git a/src/js/scenarios/treatmentScenario.js b/src/js/scenarios/treatmentScenario.js index a0cce206..53b9c053 100644 --- a/src/js/scenarios/treatmentScenario.js +++ b/src/js/scenarios/treatmentScenario.js @@ -110,6 +110,7 @@ export const scenario = config => filter: { input: config.signature, output: { trtType: [ "trt_cp" ] }, + filter_output: { trtType: [ "trt_cp" ] }, ghost: { expand: false } }, headTable: { input: { query: config.signature } }, From 010dd2fd5e70f6e08157fa590fe24c62df689d85 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 22 Dec 2021 18:00:24 +0100 Subject: [PATCH 073/191] Gskcmp 77 (#97) * Override deployment settings only when missing the deployment or when it is changed * Only change deployment settings when the value is missing or the user changed which deployment to use --- src/js/index.js | 6 ++--- src/js/pages/adminSettings.js | 45 ++++++++++++++++++++++++++++------- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/js/index.js b/src/js/index.js index 27281383..a67cf229 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -1,7 +1,7 @@ import xs from 'xstream' import { div, nav, a, h3, p, ul, li, h1, h2, i, footer, header, main, svg, g, path, span, img } from '@cycle/dom' -import { merge, prop, mergeDeepRight } from 'ramda' +import { merge, prop, mergeDeepLeft, mergeDeepRight } from 'ramda' import * as R from 'ramda' // Workflows @@ -265,8 +265,8 @@ export default function Index(sources) { const updatedDeployment = mergeDeepRight(prevState.settings.deployment, desiredDeployment) // Merge the updated deployment with the settings, by key. const updatedSettings = merge(prevState.settings, { deployment : updatedDeployment}) - // Do the same with the administrative settings - const distributedAdminSettings = mergeDeepRight(updatedSettings, updatedSettings.deployment.services) + // Do the same with the administrative settings, keep value in settings if exist - only add from deployments if value is missing + const distributedAdminSettings = mergeDeepLeft(updatedSettings, updatedSettings.deployment.services) return ({...prevState, settings: distributedAdminSettings }) }) diff --git a/src/js/pages/adminSettings.js b/src/js/pages/adminSettings.js index 21c4ff2b..4e2daa99 100644 --- a/src/js/pages/adminSettings.js +++ b/src/js/pages/adminSettings.js @@ -7,9 +7,14 @@ import isolate from "@cycle/isolate" import * as R from "ramda" import debounce from "xstream/extra/debounce" import dropRepeats from "xstream/extra/dropRepeats" +import pairwise from "xstream/extra/pairwise" import { SettingsEditor } from "../components/SettingsEditor" +/** + * @module pages/adminSettings + */ + export function IsolatedAdminSettings(sources) { return isolate(AdminSettings, "settings")(sources) } @@ -265,20 +270,42 @@ export function AdminSettings(sources) { .mapTo("/admin") .remember() - // Deployment needs to be tackled globally, does not work in _isolation_ - const deploymentUpdated$ = settings$.compose( - dropRepeats((x, y) => x.deployment.name == y.deployment.name) - ) + /** + * Update deployment settings but only if it was changed + * DropRepeats would 'let through' the initial value + * + * Deployment needs to be tackled globally, does not work in _isolation_ + * @const AdminSettings/deploymentUpdate$ + * @type {Stream} + */ + const deploymentUpdated$ = settings$ + .compose(pairwise) + .filter(([a, b]) => !R.equals(a.deployment.name, b.deployment.name)) + .map(([_, b]) => b) + + /** + * Settings update when deployment name is not set + * @const AdminSettings/deploymentMissing$ + * @type {Stream} + */ + const deploymentMissing$ = settings$ + .filter((settings) => settings.deployment.name === undefined) - // Just like in index.js: - // - fetch the appropriate deployment from deployments.js - // - overwrite the relevant entries (endpoints) of the various settings with the correct one - // + /** + * When deployment is changed or missing, fetch the appropriate deployment from deployments.js + * overwrite the relevant entries (endpoints) of the various settings with the correct one + * + * @const AdminSettings/deploymentReducer$ + * @type {Reducer} + */ // TODOs: // - align this with index.js // - restructure deployments.js to an array of deployments rather than a hashmap const deploymentReducer$ = xs - .combine(deploymentUpdated$, sources.deployments) + .combine( + xs.merge(deploymentUpdated$, deploymentMissing$), + sources.deployments + ) .map(([settings, deployments]) => (prevState) => { const desiredDeploymentName = settings.deployment.name const desiredDeployment = R.head( From e7daaa2c71f72be6a137ea3654f488fa5688cfd6 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 22 Dec 2021 18:00:42 +0100 Subject: [PATCH 074/191] Tweak correlation plot to define the plot div better so the size can't bounce between two states (#98) Create extra utils function to get both width and height of an element set height of div to 65vh, so 65% of screen height set maxHeight to 75vw, so 75% of screen width for tall screens so we don't get a very tall & narrow plot --- .../components/BinnedPlots/CorrelationPlot.js | 14 +++++++------- src/js/main.scss | 4 ++++ src/js/utils/utils.js | 19 ++++++++++++++++++- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/js/components/BinnedPlots/CorrelationPlot.js b/src/js/components/BinnedPlots/CorrelationPlot.js index 46af1bcf..e93a5596 100644 --- a/src/js/components/BinnedPlots/CorrelationPlot.js +++ b/src/js/components/BinnedPlots/CorrelationPlot.js @@ -5,7 +5,7 @@ import debounce from 'xstream/extra/debounce' import { h, p, div, br, label, input, code, table, tr, td, b, h2, button, svg, h1 } from '@cycle/dom'; import { clone, equals, omit } from 'ramda'; import { CorrelationVegaSpec } from './CorrelationSpec.js' -import { widthStream } from '../../utils/utils' +import { widthStream, widthHeightStream } from '../../utils/utils' import { loggerFactory } from '../../utils/logger' import { parse } from 'vega-parser' @@ -38,14 +38,14 @@ const isEmptyData = (data) => { } const makeVega = (elementID) => { - return div('.card-panel .center-align', [div(elementID)]) + return div('.card-panel .center-align', { style: { height: '65vh', maxHeight: '75vw', width: '100%' } }, [div(elementID)]) } function CorrelationPlot(sources) { const logger = loggerFactory('plots', sources.onion.state$, 'settings.plots.debug') - const state$ = sources.onion.state$.debug() + const state$ = sources.onion.state$ const input$ = sources.input const visibility$ = (el) => sources.DOM.select(el) @@ -57,7 +57,7 @@ function CorrelationPlot(sources) { .remember() // Size stream - const width$ = (el) => widthStream(sources.DOM, el) + const size$ = (el) => widthHeightStream(sources.DOM, el) const resize$ = sources.resize.startWith('go!') @@ -128,8 +128,8 @@ function CorrelationPlot(sources) { .filter(data => !isEmptyData(data)) // Ingest the data in the spec and return to the driver - const spec$ = xs.combine(nonEmptyData$, width$('#corrplot'), visibility$('#corrplot'), input$, resize$) - .map(([data, newwidth, visible]) => ({ spec: CorrelationVegaSpec(data), el: '#corrplot', width: newwidth, height: newwidth-100 })) + const spec$ = xs.combine(nonEmptyData$, size$('#corrplot'), visibility$('#corrplot'), input$, resize$) + .map(([data, newSize, visible]) => ({ spec: CorrelationVegaSpec(data), el: '#corrplot', width: newSize[0], height: newSize[1] })) .compose(debounce(10)) const runtime$ = spec$ @@ -138,7 +138,7 @@ function CorrelationPlot(sources) { // ======================================================================== const plotContainer = (plot) => { - return div('.col .s8 .offset-s2', {style : { padding: '0px'}}, [div('.col .s12', {style : { margin: '0 0 0 0', padding: 0}}, [ + return div('.col .s10 .l8 .offset-s1 .offset-l2', {style : { padding: '0px'}}, [div('.col .s12', {style : { margin: '0 0 0 0', padding: 0}}, [ plot, ]) ]) } diff --git a/src/js/main.scss b/src/js/main.scss index e7a6afe3..620c6a73 100644 --- a/src/js/main.scss +++ b/src/js/main.scss @@ -75,6 +75,10 @@ a:hover #border { fill-opacity: 0.5; } +div #corrplot { + height: 100% +} + @media print { @page { margin: 40pt 0pt 40pt 0pt diff --git a/src/js/utils/utils.js b/src/js/utils/utils.js index 7388ff50..360e3a1d 100644 --- a/src/js/utils/utils.js +++ b/src/js/utils/utils.js @@ -1,5 +1,5 @@ import dropRepeats from 'xstream/extra/dropRepeats' -import { prop } from 'ramda' +import { prop, equals } from 'ramda' // Size stream, make it dependent on the size of container which is managed by CSS. // TODO: Make it update immediately, currently only updates on new query @@ -20,6 +20,23 @@ export function widthStream(domSource$, el) { // .debug(log) } +export function widthHeightStream(domSource$, el) { + return domSource$ + .select(el) + .elements() + .map(elements => elements[0]) + .map(container => { + if (container != undefined) { + return [container.offsetWidth, container.offsetHeight] + } else { + return [100, 100] + } + }) + .compose(dropRepeats(equals)) + .remember() + // .debug(log) +} + export const titleCase = (phrase) => (phrase != null) ? phrase.toLowerCase().replace(/^\w|\s\w|\(\w|-\w\/\w/g, function(w) {return w.toUpperCase()}) From 9cbf4b9da4eb7479e0ec955c1c66064516030c4f Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 22 Dec 2021 18:01:09 +0100 Subject: [PATCH 075/191] Add Ligand Workflow (#100) * Properly handle missing dose_unit or dose_time Handle missing dose_unit or time_unit by printing "?" instead Make entry and entrySmall more robust for invalid data by using optional chaining; we can't call .length on undefined * Add Ligand Workflow --- deployments.json | 7 +++++++ src/js/components/TreatmentCheck.js | 5 +++-- src/js/index.js | 4 ++++ src/js/main.scss | 8 ++++++++ src/js/pages/genericTreatment.js | 2 +- src/js/pages/ligand.js | 18 ++++++++++++++++++ 6 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 src/js/pages/ligand.js diff --git a/deployments.json b/deployments.json index 3bd576ff..43dbd0b8 100644 --- a/deployments.json +++ b/deployments.json @@ -16,6 +16,7 @@ "example": { "compound": "BRD-K93645900", "signature": "-WRONG HSPA1A DNAJB1 DDIT4 -TSEN2", + "ligand": "ANGPT1", "genetic": "PARP2", "target": "MELK" }, @@ -26,6 +27,12 @@ "sample": ["A375_6H_BRD_K93645900_10"], "signature": "-CRCP" }, + "ligand": { + "treatment": "ANGPT1", + "index": 0, + "sample": ["HCC515_4H_CMAP_CYT_SRP3007_50"], + "signature": "FBXO11 SLC35 F5BAG4 RIF1" + }, "genetic": { "treatment": "PARP2", "index": 0, diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index dea3b6b7..09081ea4 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -20,8 +20,9 @@ const checkLens = { const treatmentLikeFilter = { COMPOUND : "compound", - GENETIC : "genetic", - COMPOUND_AND_GENETIC : "compound genetic" + LIGAND : "ligand", + GENETIC : "genetic", + ALL : "compound ligand genetic" } /** diff --git a/src/js/index.js b/src/js/index.js index a67cf229..d46103bf 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -7,6 +7,7 @@ import * as R from 'ramda' // Workflows import DiseaseWorkflow from './pages/disease' import CompoundWorkflow from './pages/compound' +import LigandWorkflow from './pages/ligand' import GeneticWorkflow from './pages/genetic' import GenericTreatmentWorkflow from './pages/genericTreatment' import TargetWorkflow from './pages/target' @@ -41,6 +42,7 @@ export default function Index(sources) { '/compound': CompoundWorkflow, '/target': TargetWorkflow, '/genetic': GeneticWorkflow, + '/ligand': LigandWorkflow, '/generic': GenericTreatmentWorkflow, '/statistics': StatisticsWorkflow, '/settings': IsolatedSettings, @@ -122,6 +124,7 @@ export default function Index(sources) { makeLink('/compound', span(['Compound', ' ', compoundSVG]), '.orange-text'), // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), makeLink('/genetic', span(['Genetic', ' ', targetSVG]), '.red-text'), + makeLink('/ligand', span(['Ligand', ' ', ]), '.purple-text'), makeLink('/disease', span(['Disease', ' ', diseaseSVG]), '.pink-text'), makeLink('/settings', span(['Settings', ' ', settingsSVG]), '.grey-text'), // makeLink('/admin', span(['Admin']), '.blue-text'), @@ -134,6 +137,7 @@ export default function Index(sources) { makeLink('/compound', span(['Compound', ' ', compoundSVG]), '.orange-text'), // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), makeLink('/genetic', span(['Genetic', ' ', targetSVG]), '.red-text'), + makeLink('/ligand', span(['Ligand', ' ', ]), '.purple-text'), makeLink('/disease', span(['Disease', ' ', diseaseSVG]), '.pink-text'), makeLink('/settings', span(['Settings', ' ', settingsSVG]), '.grey-text'), // makeLink('/admin', span(['Admin']), '.blue-text'), diff --git a/src/js/main.scss b/src/js/main.scss index 620c6a73..df035520 100644 --- a/src/js/main.scss +++ b/src/js/main.scss @@ -27,6 +27,11 @@ main { @extend .lighten-5; } +.ligand { + @extend .purple; + @extend .lighten-5; +} + .disease { @extend .pink; @extend .lighten-5; @@ -86,7 +91,10 @@ div #corrplot { /* no background color for printing */ .compound, .disease, + .ligand, .genetic, + .generic, + .correlation, .target { background-color: white !important } diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index a1011588..c8f7f8d4 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -30,7 +30,7 @@ export default function GenericTreatmentWorkflow(sources) { const workflow = (sources??{}).workflow ?? {} const workflowWelcomeText = workflow.welcomeText ?? "This is the generic treatment workflow template" const workflowMainDivClass = workflow.mainDivClass ?? ".row .generic" - const workflowTreatmentType = workflow.treatmentType ?? treatmentLikeFilter.COMPOUND_AND_GENETIC + const workflowTreatmentType = workflow.treatmentType ?? treatmentLikeFilter.ALL const workflowLoggerName = workflow.loggerName ?? "generic" const workflowGhostModeScenarioSelector = workflow.ghostModeScenarioSelector ?? ((state) => state.settings.common.ghost.genetic) diff --git a/src/js/pages/ligand.js b/src/js/pages/ligand.js new file mode 100644 index 00000000..ffadd920 --- /dev/null +++ b/src/js/pages/ligand.js @@ -0,0 +1,18 @@ +import { treatmentLikeFilter } from "../components/TreatmentForm" +import GenericTreatmentWorkflow from "./genericTreatment" + +export default function LigandWorkflow(sources) { + + const enhancedSources = { + ...sources, + workflow: { + treatmentType: treatmentLikeFilter.LIGAND, + welcomeText: "Welcome to Ligand Workflow", + mainDivClass: ".row .ligand", + loggerName: "ligand", + ghostModeScenarioSelector: ((state) => state.settings.common.ghost.ligand), + }, + } + + return GenericTreatmentWorkflow(enhancedSources) +} From 9bd1b588148c9211df30b414de8d080340bbc817 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 22 Dec 2021 18:01:28 +0100 Subject: [PATCH 076/191] Add statistics duration field in deployments & admin page (#101) Can be used to configure the correct threshold to declare the cluster being busy instead of a fixed value --- deployments.json | 9 ++++++--- src/js/components/Check.js | 12 +++++++----- src/js/pages/adminSettings.js | 7 +++++++ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/deployments.json b/deployments.json index 43dbd0b8..a21729c7 100644 --- a/deployments.json +++ b/deployments.json @@ -69,7 +69,8 @@ "url": "http://localhost:8082/ca/" }, "config": { - "logoUrl": "https://www.data-intuitive.com/images/logo_white.png" + "logoUrl": "https://www.data-intuitive.com/images/logo_white.png", + "normalStatisticsResponseTime": 1.0 } } }, @@ -146,7 +147,8 @@ "url": "http://localhost:8082/ca/" }, "config": { - "logoUrl": "" + "logoUrl": "", + "normalStatisticsResponseTime": 1.0 } } }, @@ -193,7 +195,8 @@ "url": "http://localhost:8082/drugbank/" }, "config": { - "logoUrl": "" + "logoUrl": "", + "normalStatisticsResponseTime": 1.0 } } } diff --git a/src/js/components/Check.js b/src/js/components/Check.js index 96ffe64a..735824fb 100644 --- a/src/js/components/Check.js +++ b/src/js/components/Check.js @@ -114,14 +114,16 @@ function Check(sources) { const responseMetric$ = jobs$ .map(jobs => differenceWithStatisticsResponses(jobs, LATENCY_FACTOR)) + const maxNormalTime$ = state$.map((state) => state.settings.config.normalStatisticsResponseTime) + // When the performance metric is higher than 1, we show the user a message. - const delay$ = responseMetric$ - .filter(metric => metric > 1) + const delay$ = xs.combine(responseMetric$, maxNormalTime$) + .filter(([metric, max]) => metric > max) .mapTo({ text: 'The cluster seems to be slower than expected.\n Please have patience or try again in 5"...', duration: 15000 }) - const loadedVdom$ = responseMetric$ - .map(metric => - (metric < 1) ? + const loadedVdom$ = xs.combine(responseMetric$, maxNormalTime$) + .map(([metric, max]) => + (metric < max) ? i('.material-icons .green-text .medium', 'done') : i('.material-icons .red-text .medium', 'done') ) diff --git a/src/js/pages/adminSettings.js b/src/js/pages/adminSettings.js index 4e2daa99..7ec4dd5b 100644 --- a/src/js/pages/adminSettings.js +++ b/src/js/pages/adminSettings.js @@ -238,6 +238,13 @@ export function AdminSettings(sources) { type: "text", title: "URL for logo image", props: {}, + }, + { + field: "normalStatisticsResponseTime", + class: ".input-field", + type: "text", + title: "Max normal time for statistics query", + props: { type: "text" }, } ], }, From 1680cbfa23d748eec111fc2dfc1e21be97d1a9ee Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 22 Dec 2021 18:02:57 +0100 Subject: [PATCH 077/191] Update home screen with list of workflow options (#94) Move WF icon svg definitions to separate file as they are used in index and home Add svg icon for ligand and correlation WF --- src/js/index.js | 33 +++------- src/js/pages/home.js | 142 +++++-------------------------------------- src/js/svg.js | 52 ++++++++++++++++ 3 files changed, 74 insertions(+), 153 deletions(-) create mode 100644 src/js/svg.js diff --git a/src/js/index.js b/src/js/index.js index d46103bf..9d5a2aef 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -25,6 +25,10 @@ import { initSettings } from './configuration.js' import initDeployments from '../../deployments.json' import { loggerFactory } from './utils/logger' +import { compoundSVG, targetSVG, ligandSVG, diseaseSVG, correlationSVG, settingsSVG } from './svg' + +import { navbarModule } from "../../navbar.js"; + export default function Index(sources) { const {router} = sources; @@ -66,29 +70,6 @@ export default function Index(sources) { // .mapTo(i('.small .material-icons', 'flight_takeoff')) // .startWith(span()) - const compoundSVG = svg({ attrs: { height: '16pt', viewBox: '0 0 20 30' } }, [ - svg.g([ - svg.path({ attrs: { d: 'M17,2L17,2c-2.1,0-3,0.5-3,3v7h-4V5c0-2.5-0.9-3-3-3l0,0L6,2v2h1c0.7,0,1,0.4,1,1v12c0,5.2,4,5,4,5s4,0.2,4-5V5c0-0.6,0.3-1,1-1h1V2L17,2z', stroke: '#ff9800' } }) - ]) - ]) - - const targetSVG = svg({ attrs: { height: '18pt', viewBox: '0 0 20 30' } }, [ - svg.g([ - svg.path({ attrs: { stroke: '#f44335', d: 'M22.7,9.7l-1.4-1.4c-0.4,0.4-0.8,0.8-1.2,1.1l-5.4-5.4c0.3-0.4,0.7-0.8,1.1-1.2l-1.4-1.4c-3.4,3.4-3.6,6.7-3.4,9.6c-3-0.2-6.3,0-9.6,3.4l1.4,1.4c0.4-0.4,0.8-0.8,1.2-1.1l5.4,5.4c-0.3,0.4-0.7,0.8-1.1,1.2l1.4,1.4c3.4-3.4,3.5-6.6,3.4-9.6C16,13.3,19.3,13.1,22.7,9.7z M19.2,9.9c-0.9,0.5-1.7,0.8-2.6,1l-3.6-3.6c0.2-0.9,0.5-1.7,1-2.6L19.2,9.9z M11.1,12.9c0.1,0.8,0.1,1.6,0,2.4l-2.5-2.5C9.4,12.9,10.2,12.9,11.1,12.9z M4.8,14.1c0.9-0.5,1.7-0.8,2.6-1l3.6,3.6c-0.2,0.9-0.5,1.7-1,2.6L4.8,14.1z M12.9,11.1c-0.1-0.8-0.1-1.7,0-2.5l2.5,2.5C14.6,11.2,13.8,11.1,12.9,11.1z' } }) - ]) - ]) - - const diseaseSVG = svg({ attrs: { height: '16pt', viewBox: "0 0 13.421 13.421" } }, [ - svg.g([ - svg.path({ attrs: { stroke: '#e91e63', d: 'M12.843,8.669c-0.907-0.542-2.619-0.031-4.944,1.476c-1.507,0.977-2.618,1.269-3.215,0.846c-0.871-0.616-0.709-2.674-0.498-3.862h0.179c0.122,0,0.222-0.099,0.222-0.223V6.467C5.192,6,7.47,4.183,7.786,3.013c0.361-1.327-1.672-2.181-2.264-2.399c0.01-0.127-0.061-0.249-0.188-0.29l-0.22-0.072C4.967,0.204,4.809,0.285,4.76,0.433L4.63,0.831C4.582,0.978,4.663,1.138,4.811,1.185l0.222,0.072c0.08,0.026,0.161,0.011,0.228-0.028c0.742,0.266,2.06,0.957,1.884,1.609C6.907,3.713,4.932,5.367,4.121,5.984H3.71C2.9,5.366,0.924,3.713,0.687,2.838c-0.175-0.645,1.116-1.329,1.86-1.602c0.069,0.056,0.159,0.084,0.25,0.058l0.225-0.061c0.149-0.041,0.237-0.195,0.195-0.345l-0.11-0.404c-0.042-0.15-0.196-0.238-0.346-0.196l-0.225,0.06C2.416,0.381,2.34,0.487,2.333,0.604c-0.553,0.2-2.657,1.06-2.291,2.409C0.348,4.14,2.475,5.869,3.17,6.411v0.495c0,0.124,0.099,0.223,0.222,0.223h0.116c-0.205,1.189-0.429,3.543,0.79,4.406c0.296,0.212,0.646,0.316,1.051,0.316c0.767,0,1.731-0.381,2.913-1.146c2.975-1.928,3.998-1.606,4.241-1.462c0.187,0.11,0.27,0.305,0.249,0.576c-0.03,0.38-0.284,0.863-0.708,1.177c-0.249-0.324-0.641-0.535-1.08-0.535c-0.751,0-1.36,0.611-1.36,1.361s0.609,1.361,1.36,1.361s1.361-0.611,1.361-1.361c0-0.067-0.007-0.133-0.016-0.198c0.608-0.394,1.053-1.076,1.106-1.753C13.456,9.348,13.247,8.909,12.843,8.669z M10.963,12.517c-0.383,0-0.694-0.313-0.694-0.695s0.312-0.694,0.694-0.694c0.216,0,0.405,0.101,0.533,0.255c0.14,0.115,0.106,0.252,0.159,0.431c0,0.003,0.003,0.005,0.003,0.009C11.659,12.204,11.347,12.517,10.963,12.517z' } }) - ]) - ]) - const settingsSVG = svg({ attrs: { height: '18pt', viewBox: "10 5 34 34" } }, [ - svg.g([ - svg.path({ attrs: { stroke: 'grey', d: 'M31.92529,22.74756l-2.11786-0.23932c-0.13806-0.56036-0.35303-1.08813-0.6424-1.5777 l1.31934-1.65271c0.31543-0.39502,0.28467-0.96387-0.07129-1.32324l-0.3667-0.36963 c-0.3584-0.36084-0.92773-0.39746-1.32764-0.0791l-1.64514,1.30609c-0.51428-0.31042-1.04895-0.55225-1.60339-0.68939 l-0.22852-2.04688c-0.05615-0.50488-0.48193-0.88574-0.99023-0.88574H23.7373c-0.50781,0-0.93359,0.38037-0.98975,0.88477 l-0.22705,2.01465c-0.60547,0.14062-1.16748,0.36572-1.67676,0.67139l-1.56299-1.25195 c-0.39795-0.31836-0.96826-0.28662-1.32764,0.07275l-0.37207,0.37207c-0.35889,0.35938-0.39062,0.9292-0.07373,1.32617 l1.23047,1.54004c-0.3335,0.53223-0.57666,1.10547-0.7251,1.71045l-1.93848,0.21729 c-0.50391,0.05713-0.88428,0.48291-0.88428,0.99023v0.51416c0,0.50732,0.38037,0.93311,0.88477,0.99023l1.9375,0.21729 c0.15039,0.62012,0.39062,1.19727,0.71631,1.72021l-1.22314,1.53955c-0.31543,0.39844-0.28174,0.96875,0.0791,1.32666 l0.37012,0.36719c0.19336,0.19141,0.44727,0.28906,0.70166,0.28906c0.21924,0,0.43896-0.07227,0.62158-0.21826l1.54688-1.23584 c0.52277,0.31171,1.09503,0.52338,1.69385,0.66364l0.22852,2.0141c0.05713,0.50391,0.48291,0.88428,0.98975,0.88428h0.51514 c0.50732,0,0.93311-0.38037,0.99023-0.88525l0.2301-2.05066c0.57428-0.14661,1.1181-0.3772,1.62494-0.6897l1.6225,1.29749 c0.18311,0.14648,0.40283,0.21826,0.62207,0.21826c0.25586,0,0.51074-0.09814,0.70459-0.29199l0.36377-0.36377 c0.35742-0.35791,0.39014-0.92725,0.0752-1.32373l-1.31097-1.65137c0.29071-0.49188,0.50586-1.02307,0.64325-1.58789 l2.10815-0.23877c0.50391-0.05713,0.88428-0.48291,0.88428-0.98975V23.7373 C32.81006,23.22949,32.42969,22.80371,31.92529,22.74756z M27.03174,24.00879c0,1.71094-1.39209,3.10303-3.10303,3.10303 c-1.71533,0-3.11084-1.39209-3.11084-3.10303c0-1.71533,1.39551-3.11084,3.11084-3.11084 C25.63965,20.89795,27.03174,22.29346,27.03174,24.00879z' } }) - ]) - ]) - const nav$ = state$.map((state) => { const leftLogo = @@ -126,9 +107,10 @@ export default function Index(sources) { makeLink('/genetic', span(['Genetic', ' ', targetSVG]), '.red-text'), makeLink('/ligand', span(['Ligand', ' ', ]), '.purple-text'), makeLink('/disease', span(['Disease', ' ', diseaseSVG]), '.pink-text'), + makeLink('/correlation', span(['Correlation', ' ', correlationSVG]), '.blue-text'), makeLink('/settings', span(['Settings', ' ', settingsSVG]), '.grey-text'), // makeLink('/admin', span(['Admin']), '.blue-text'), - makeLink('/correlation', span('.grey-text .text-darken-3','', ["v", VERSION]), ''), + li(span('.grey-text .text-darken-3','', ["v", VERSION])), ]), centerLogo, ]), @@ -139,9 +121,10 @@ export default function Index(sources) { makeLink('/genetic', span(['Genetic', ' ', targetSVG]), '.red-text'), makeLink('/ligand', span(['Ligand', ' ', ]), '.purple-text'), makeLink('/disease', span(['Disease', ' ', diseaseSVG]), '.pink-text'), + makeLink('/correlation', span(['Correlation', ' ', correlationSVG]), '.blue-text'), makeLink('/settings', span(['Settings', ' ', settingsSVG]), '.grey-text'), // makeLink('/admin', span(['Admin']), '.blue-text'), - makeLink('/correlation', span('.grey-text .text-darken-3','', ["v", VERSION]), ''), + li(span('.grey-text .text-darken-3', { style: { padding: '0 32px' } }, ["v", VERSION])), ]) ]) }) diff --git a/src/js/pages/home.js b/src/js/pages/home.js index 285f3470..1266b882 100644 --- a/src/js/pages/home.js +++ b/src/js/pages/home.js @@ -5,6 +5,7 @@ import { merge, prop, equals } from 'ramda'; import { Check } from '../components/Check' import dropRepeats from 'xstream/extra/dropRepeats' import { logoSVG } from '../index' +import { compoundSVG, targetSVG, ligandSVG, diseaseSVG, correlationSVG, settingsSVG } from '../svg' const appear = { style: { @@ -22,6 +23,10 @@ function Home(sources) { .map(state => merge(state.settings.form, state.settings.api)) const CheckSink = Check(merge(sources, { props: checkProps$ })) + const makeLink = (path, label, options) => { + return li([a(options, { props: { href: path } }, label)]) + } + const vdom$ = xs.combine(CheckSink.DOM) .map(([check]) => div([ div({ style: { 'z-index': -1, height: '100%', overflow: 'hidden', position: 'absolute', opacity: 0.08, 'text-align': 'center', width: '100%' } }, @@ -38,136 +43,17 @@ function Home(sources) { ]), div('.row', []), div('.col .l6 .m8 .s10 offset-l3 .offset-m2 .offset-s1 .center-align', appear, [ - svg({ attrs: { viewBox: '1018 -228 972 974' }, style: { 'max-width': '400px' } }, [ - svg.a({ attrs: { 'xlink:href': "/target" } }, [ - // TARGET - svg.path({ - attrs: { - id: "target", - d: "M 1506.0454 -136.98166 L 1506.0454 -225 C 1420.9163 -225 1337.2871 -202.5916 1263.5632 -160.02709 C 1031.6083 -26.107868 952.1347 270.4917 1086.0539 502.4466 L 1162.28 458.43743 C 1052.6664 268.58106 1117.716 25.81266 1307.5724 -83.80097 C 1367.9158 -118.64026 1436.3668 -136.98165 1506.0454 -136.98166 Z", - fill: "#f44335", - 'stroke-width': '0pt' - } - }), - svg.text([ - svg.textPath({ attrs: { 'xlink:href': "#target", startOffset: "80%" } }, [ - svg.tspan({ attrs: { 'font-family': "Roboto", 'font-size': "60", 'text-anchor': "middle", 'letter-spacing': 15, 'font-weight': "bold", fill: "#ebebeb", dy: "-20" } }, "TARGET") - ]) - ]), - svg.path({ - attrs: { - id: 'border', - d: "M 1506.0454 -136.98166 L 1506.0454 -225 C 1420.9163 -225 1337.2871 -202.5916 1263.5632 -160.02709 C 1031.6083 -26.107868 952.1347 270.4917 1086.0539 502.4466 L 1162.28 458.43743 C 1052.6664 268.58106 1117.716 25.81266 1307.5724 -83.80097 C 1367.9158 -118.64026 1436.3668 -136.98165 1506.0454 -136.98166 Z", - stroke: "white", - 'fill-opacity': '0', - 'stroke-linecap': "round", - 'stroke-linejoin': "round", - 'stroke-width': "6" - } - }) - ]), - // DISEASE - svg.a({ attrs: { 'xlink:href': "/disease" } }, [ - svg.path({ - attrs: { - id: "disease", - d: "M 1849.8108 458.43743 L 1926.0369 502.4466 C 1968.6014 428.7227 1991.0098 345.09344 1991.0098 259.9644 C 1991.0098 -7.8740405 1773.8838 -225 1506.0454 -225 L 1506.0454 -136.98166 C 1725.2726 -136.98166 1902.9914 40.73715 1902.9914 259.9644 C 1902.9914 329.643 1884.65 398.094 1849.8108 458.4374 Z", - fill: "#e91e63" - } - }), - svg.text([ - svg.textPath({ attrs: { 'xlink:href': "#disease", startOffset: "80%" } }, [ - svg.tspan({ attrs: { 'font-family': "Roboto", 'font-size': "60", 'text-anchor': "middle", 'letter-spacing': 15, 'font-weight': "bold", fill: "#ebebeb", dy: "-20" } }, "DISEASE") - ]) - ]), - svg.path({ - attrs: { - id: 'border', - d: "M 1849.8108 458.43743 L 1926.0369 502.4466 C 1968.6014 428.7227 1991.0098 345.09344 1991.0098 259.9644 C 1991.0098 -7.8740405 1773.8838 -225 1506.0454 -225 L 1506.0454 -136.98166 C 1725.2726 -136.98166 1902.9914 40.73715 1902.9914 259.9644 C 1902.9914 329.643 1884.65 398.094 1849.8108 458.4374 Z", - stroke: "white", - 'fill-opacity': '0', - 'stroke-linecap': "round", - 'stroke-linejoin': "round", - 'stroke-width': "6" - } - }) - ]), - // COMPOUND - svg.a({ attrs: { 'xlink:href': "/compound" } }, [ - svg.path({ - attrs: { - id: "compound", - d: "M 1162.28 458.43743 L 1086.0539 502.4466 C 1128.6184 576.1705 1189.8393 637.3914 1263.5632 679.9559 C 1495.518 813.8751 1792.1177 734.4015 1926.0369 502.4466 L 1849.8108 458.43743 C 1740.1971 648.2938 1497.4287 713.3434 1307.5724 603.7298 C 1247.229 568.8905 1197.1193 518.7809 1162.28 458.43745 Z", - fill: "#ff9800" - } - }), - svg.text([ - svg.textPath({ attrs: { 'xlink:href': "#compound", startOffset: "29%" } }, [ - svg.tspan({ attrs: { 'font-family': "Roboto", 'font-size': "60", 'text-anchor': "middle", 'letter-spacing': 15, 'font-weight': "bold", fill: "#ebebeb", dy: "-20" } }, "COMPOUND") - ]) - ]), - svg.path({ - attrs: { - id: 'border', - d: "M 1162.28 458.43743 L 1086.0539 502.4466 C 1128.6184 576.1705 1189.8393 637.3914 1263.5632 679.9559 C 1495.518 813.8751 1792.1177 734.4015 1926.0369 502.4466 L 1849.8108 458.43743 C 1740.1971 648.2938 1497.4287 713.3434 1307.5724 603.7298 C 1247.229 568.8905 1197.1193 518.7809 1162.28 458.43745 Z", - stroke: "white", - 'fill-opacity': '0', - 'stroke-linecap': "round", - 'stroke-linejoin': "round", - 'stroke-width': "6" - } - }) - ]), - svg.g([ - // DISEASE - PHENO - svg.path({ attrs: { d: "M 1506.0454 251.9644 L 1506.0454 -.035595944 C 1645.2211 -.035595944 1758.0454 112.78865 1758.0454 251.9644 C 1758.0454 296.19964 1746.4014 339.65556 1724.2838 377.9644 Z", fill: "#e92363", 'fill-opacity': ".5" } }), - svg.path({ attrs: { d: "M 1506.0454 251.9644 L 1506.0454 -.035595944 C 1645.2211 -.035595944 1758.0454 112.78865 1758.0454 251.9644 C 1758.0454 296.19964 1746.4014 339.65556 1724.2838 377.9644 Z", stroke: "white", 'stroke-linecap': "round", 'stroke-linejoin': "round", 'stroke-width': "6", 'fill-opacity': '0' } }), - svg.text({ attrs: { transform: "translate(1529.807 150.726)", fill: "white" } }, [ - svg.tspan({ attrs: { 'font-family': "Roboto", 'font-size': "60", 'font-weight': "bold", fill: "white", x: ".18359375", y: "56", textLength: "198.63281" } }, 'PHENO') - ]), - - svg.path({ attrs: { d: "M 1506.1296 251.9644 L 1724.368 377.9644 C 1654.78 498.49415 1500.6593 539.7907 1380.1296 470.2028 C 1341.8207 448.0852 1310.0088 416.27324 1287.8912 377.9644 Z", fill: "#fe9801", 'fill-opacity': ".5" } }), - svg.path({ attrs: { d: "M 1506.1296 251.9644 L 1724.368 377.9644 C 1654.78 498.49415 1500.6593 539.7907 1380.1296 470.2028 C 1341.8207 448.0852 1310.0088 416.27324 1287.8912 377.9644 Z", stroke: "white", 'stroke-linecap': "round", 'stroke-linejoin': "round", 'stroke-width': "6", 'fill-opacity': '0' } }), - // svg.path({ attrs: { d: "M 1506.1296 251.9644 L 1724.368 377.9644 C 1654.78 498.49415 1500.6593 539.7907 1380.1296 470.2028 C 1341.8207 448.0852 1310.0088 416.27324 1287.8912 377.9644 Z", stroke: "white", 'stroke-linecap': "round", 'stroke-linejoin': "round", 'stroke-width': "6" } }), - - - svg.path({ attrs: { d: "M 1506.0454 251.9644 L 1287.807 377.9644 C 1218.2191 257.43466 1259.5156 103.31388 1380.0454 33.726002 C 1418.3542 11.608383 1461.8101 -.035595944 1506.0454 -.035595944 Z", fill: "#f44335", 'fill-opacity': ".5" } }), - svg.path({ attrs: { d: "M 1506.0454 251.9644 L 1287.807 377.9644 C 1218.2191 257.43466 1259.5156 103.31388 1380.0454 33.726002 C 1418.3542 11.608383 1461.8101 -.035595944 1506.0454 -.035595944 Z", stroke: "white", 'stroke-linecap': "round", 'stroke-linejoin': "round", 'stroke-width': "6", 'fill-opacity': '0' } }), - - svg.text({ attrs: { transform: "translate(1309.807 150.726)", fill: "white" } }, [ - svg.tspan({ attrs: { 'font-family': "Roboto", 'font-size': "60", 'font-weight': "bold", fill: "white", x: ".29589844", y: "56", textLength: "158.4082" } }, 'GENO') - ]), - - - svg.text({ attrs: { transform: "translate(1406.807 358.726)", fill: "white" } }, [ - svg.tspan({ attrs: { 'font-family': "Roboto", 'font-size': "60", 'font-weight': "bold", fill: "white", x: ".3076172", y: "56", textLength: "209.38477" } }, 'CHEMO') - ]), - - - ]) + ul('.left', [ + makeLink('/compound', span({ style: { fontSize: "2rem" } }, ['Compound', ' ', compoundSVG]), '.orange-text .left'), + // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), + makeLink('/genetic', span({ style: { fontSize: "2rem" } }, ['Genetic', ' ', targetSVG]), '.red-text .left'), + //makeLink('/generic', span({ style: { fontSize: "2rem" } }, ['Ligand', ' ', ligandSVG]), '.purple-text .left'), + makeLink('/disease', span({ style: { fontSize: "2rem" } }, ['Disease', ' ', diseaseSVG]), '.pink-text .left'), + makeLink('/correlation', span({ style: { fontSize: "2rem" } }, ['Correlation', ' ', correlationSVG]), '.blue-text .left'), + makeLink('/settings', span({ style: { fontSize: "2rem" } }, ['Settings', ' ', settingsSVG]), '.grey-text .left'), + // makeLink('/admin', span(['Admin']), '.blue-text'), ]), ]), - - // img({ attrs: { src: 'src/js/pages/home.svg', usemap: '#homemap'} }), - // div('.col .s6 .offset-s3', [ - // div('.col .s12 .orange .darken-4 .pink-text', { style: { padding: '10px 10px 10px 10px' } }, - // [ - // i('.orange-text .text-lighten-1 .material-icons', 'play_arrow'), - // a('.orange-text .text-lighten-3', { props: { href: '/compound' }, style: { fontWeight: 'bolder', 'font-size': '32px' } }, ' Compound Workflow') - // ]), - // div('.row', []), - // div('.col .s12 .red .darken-4 .pink-text', { style: { padding: '10px 10px 10px 10px' } }, - // [ - // i('.red-text .text-lighten-1 .material-icons', 'play_arrow'), - // a('.red-text .text-lighten-4', { props: { href: '/target' }, style: { fontWeight: 'bolder', 'font-size': '32px' } }, ' Target Workflow') - // ]), - // div('.row', []), - // div('.col .s12 .pink .darken-4', { style: { padding: '10px 10px 10px 10px' } }, - // [ - // i('.pink-text .text-lighten-1 .material-icons', 'play_arrow'), - // a('.pink-text .text-lighten-3', { props: { href: '/disease' }, style: { fontWeight: 'bolder', 'font-size': '32px' } }, ' Disease Workflow') - // ]), - // ]), div('.row', []), p('.col .l6 .m8 .s10 offset-l3 .offset-m2 .offset-s1 .flow-text', [ 'You can click on one of the workflows above to start it.', diff --git a/src/js/svg.js b/src/js/svg.js new file mode 100644 index 00000000..c486644e --- /dev/null +++ b/src/js/svg.js @@ -0,0 +1,52 @@ +import { svg } from '@cycle/dom' + + +export const compoundSVG = svg({ attrs: { height: '16pt', viewBox: '0 0 20 30' } }, [ + svg.g([ + svg.path({ attrs: { d: 'M17,2L17,2c-2.1,0-3,0.5-3,3v7h-4V5c0-2.5-0.9-3-3-3l0,0L6,2v2h1c0.7,0,1,0.4,1,1v12c0,5.2,4,5,4,5s4,0.2,4-5V5c0-0.6,0.3-1,1-1h1V2L17,2z', stroke: '#ff9800' } }) + ]) +]) + +export const targetSVG = svg({ attrs: { height: '18pt', viewBox: '0 0 20 30' } }, [ + svg.g([ + svg.path({ attrs: { stroke: '#f44335', d: 'M22.7,9.7l-1.4-1.4c-0.4,0.4-0.8,0.8-1.2,1.1l-5.4-5.4c0.3-0.4,0.7-0.8,1.1-1.2l-1.4-1.4c-3.4,3.4-3.6,6.7-3.4,9.6c-3-0.2-6.3,0-9.6,3.4l1.4,1.4c0.4-0.4,0.8-0.8,1.2-1.1l5.4,5.4c-0.3,0.4-0.7,0.8-1.1,1.2l1.4,1.4c3.4-3.4,3.5-6.6,3.4-9.6C16,13.3,19.3,13.1,22.7,9.7z M19.2,9.9c-0.9,0.5-1.7,0.8-2.6,1l-3.6-3.6c0.2-0.9,0.5-1.7,1-2.6L19.2,9.9z M11.1,12.9c0.1,0.8,0.1,1.6,0,2.4l-2.5-2.5C9.4,12.9,10.2,12.9,11.1,12.9z M4.8,14.1c0.9-0.5,1.7-0.8,2.6-1l3.6,3.6c-0.2,0.9-0.5,1.7-1,2.6L4.8,14.1z M12.9,11.1c-0.1-0.8-0.1-1.7,0-2.5l2.5,2.5C14.6,11.2,13.8,11.1,12.9,11.1z' } }) + ]) +]) + +export const ligandSVG = svg({ attrs: { height: '18pt', viewBox: '15 4 18 16' } }, [ + svg.g([ + svg.rect({attrs: { stroke:'#9c27b0', fill:'#9c27b0', x:'19.5', y:'12.5', width:'1', height:'4'}}), +// svg.path({ attrs: { stroke:'#000000', fill: '#000000', d:'M23.6,9.7c-0.15,1.86-1.7,3.32-3.6,3.32c-1.9,0-3.45-1.46-3.6-3.32l1.42,0C17.97,10.77,18.9,11.6,20.0,11.6 c1.1,0,2.03-0.83,2.17-1.91L23.6,9.7z'}}), + svg.path({ attrs: { stroke:'#9c27b0', fill: '#9c27b0', d:'M23.6,9.7 c0,1.9-1.7,3.3-3.6,3.3 c-1.9,0-3.5-1.5-3.6-3.3 l1.5,0 C18,10.8,18.9,11.6,20,11.6 c1.1,0,2-0.8,2.2-1.9 L23.6,9.7z'}}), + svg.circle({attrs: { stroke: '#9c27b0', fill:'#9c27b0', cx:'20', cy:'9.2', r:'0.8'}}), + ]) +]) + +export const diseaseSVG = svg({ attrs: { height: '16pt', viewBox: "0 0 13.421 13.421" } }, [ + svg.g([ + svg.path({ attrs: { stroke: '#e91e63', d: 'M12.843,8.669c-0.907-0.542-2.619-0.031-4.944,1.476c-1.507,0.977-2.618,1.269-3.215,0.846c-0.871-0.616-0.709-2.674-0.498-3.862h0.179c0.122,0,0.222-0.099,0.222-0.223V6.467C5.192,6,7.47,4.183,7.786,3.013c0.361-1.327-1.672-2.181-2.264-2.399c0.01-0.127-0.061-0.249-0.188-0.29l-0.22-0.072C4.967,0.204,4.809,0.285,4.76,0.433L4.63,0.831C4.582,0.978,4.663,1.138,4.811,1.185l0.222,0.072c0.08,0.026,0.161,0.011,0.228-0.028c0.742,0.266,2.06,0.957,1.884,1.609C6.907,3.713,4.932,5.367,4.121,5.984H3.71C2.9,5.366,0.924,3.713,0.687,2.838c-0.175-0.645,1.116-1.329,1.86-1.602c0.069,0.056,0.159,0.084,0.25,0.058l0.225-0.061c0.149-0.041,0.237-0.195,0.195-0.345l-0.11-0.404c-0.042-0.15-0.196-0.238-0.346-0.196l-0.225,0.06C2.416,0.381,2.34,0.487,2.333,0.604c-0.553,0.2-2.657,1.06-2.291,2.409C0.348,4.14,2.475,5.869,3.17,6.411v0.495c0,0.124,0.099,0.223,0.222,0.223h0.116c-0.205,1.189-0.429,3.543,0.79,4.406c0.296,0.212,0.646,0.316,1.051,0.316c0.767,0,1.731-0.381,2.913-1.146c2.975-1.928,3.998-1.606,4.241-1.462c0.187,0.11,0.27,0.305,0.249,0.576c-0.03,0.38-0.284,0.863-0.708,1.177c-0.249-0.324-0.641-0.535-1.08-0.535c-0.751,0-1.36,0.611-1.36,1.361s0.609,1.361,1.36,1.361s1.361-0.611,1.361-1.361c0-0.067-0.007-0.133-0.016-0.198c0.608-0.394,1.053-1.076,1.106-1.753C13.456,9.348,13.247,8.909,12.843,8.669z M10.963,12.517c-0.383,0-0.694-0.313-0.694-0.695s0.312-0.694,0.694-0.694c0.216,0,0.405,0.101,0.533,0.255c0.14,0.115,0.106,0.252,0.159,0.431c0,0.003,0.003,0.005,0.003,0.009C11.659,12.204,11.347,12.517,10.963,12.517z' } }) + ]) +]) + +export const correlationSVG = svg({ attrs: { height: '16pt', viewBox: '0 0 48 48', enableBackground:'new 0 0 48 48' } }, [ + svg.polygon({attrs: {points:[9,39, 9,6, 7,6, 7,41, 42,41, 42,39], stroke:'#2196F3', fill:'#2196F3'}}), + svg.g([ + svg.circle({ attrs: { cx: '14', cy:'11', r:'2', fill:'#2196F3' } }), + svg.circle({ attrs: { cx: '32', cy:'11', r:'2', fill:'#2196F3' } }), + svg.circle({ attrs: { cx: '39', cy:'11', r:'2', fill:'#2196F3' } }), + svg.circle({ attrs: { cx: '23', cy:'11', r:'4', fill:'#2196F3' } }), + svg.circle({ attrs: { cx: '14', cy:'33', r:'2', fill:'#2196F3' } }), + svg.circle({ attrs: { cx: '30', cy:'33', r:'2', fill:'#2196F3' } }), + svg.circle({ attrs: { cx: '22', cy:'33', r:'3', fill:'#2196F3' } }), + svg.circle({ attrs: { cx: '39', cy:'33', r:'4', fill:'#2196F3' } }), + svg.circle({ attrs: { cx: '14', cy:'22', r:'2', fill:'#2196F3' } }), + svg.circle({ attrs: { cx: '39', cy:'22', r:'2', fill:'#2196F3' } }), + svg.circle({ attrs: { cx: '32', cy:'22', r:'3', fill:'#2196F3' } }), + ]) +]) + +export const settingsSVG = svg({ attrs: { height: '18pt', viewBox: "10 5 34 34" } }, [ + svg.g([ + svg.path({ attrs: { stroke: 'grey', d: 'M31.92529,22.74756l-2.11786-0.23932c-0.13806-0.56036-0.35303-1.08813-0.6424-1.5777 l1.31934-1.65271c0.31543-0.39502,0.28467-0.96387-0.07129-1.32324l-0.3667-0.36963 c-0.3584-0.36084-0.92773-0.39746-1.32764-0.0791l-1.64514,1.30609c-0.51428-0.31042-1.04895-0.55225-1.60339-0.68939 l-0.22852-2.04688c-0.05615-0.50488-0.48193-0.88574-0.99023-0.88574H23.7373c-0.50781,0-0.93359,0.38037-0.98975,0.88477 l-0.22705,2.01465c-0.60547,0.14062-1.16748,0.36572-1.67676,0.67139l-1.56299-1.25195 c-0.39795-0.31836-0.96826-0.28662-1.32764,0.07275l-0.37207,0.37207c-0.35889,0.35938-0.39062,0.9292-0.07373,1.32617 l1.23047,1.54004c-0.3335,0.53223-0.57666,1.10547-0.7251,1.71045l-1.93848,0.21729 c-0.50391,0.05713-0.88428,0.48291-0.88428,0.99023v0.51416c0,0.50732,0.38037,0.93311,0.88477,0.99023l1.9375,0.21729 c0.15039,0.62012,0.39062,1.19727,0.71631,1.72021l-1.22314,1.53955c-0.31543,0.39844-0.28174,0.96875,0.0791,1.32666 l0.37012,0.36719c0.19336,0.19141,0.44727,0.28906,0.70166,0.28906c0.21924,0,0.43896-0.07227,0.62158-0.21826l1.54688-1.23584 c0.52277,0.31171,1.09503,0.52338,1.69385,0.66364l0.22852,2.0141c0.05713,0.50391,0.48291,0.88428,0.98975,0.88428h0.51514 c0.50732,0,0.93311-0.38037,0.99023-0.88525l0.2301-2.05066c0.57428-0.14661,1.1181-0.3772,1.62494-0.6897l1.6225,1.29749 c0.18311,0.14648,0.40283,0.21826,0.62207,0.21826c0.25586,0,0.51074-0.09814,0.70459-0.29199l0.36377-0.36377 c0.35742-0.35791,0.39014-0.92725,0.0752-1.32373l-1.31097-1.65137c0.29071-0.49188,0.50586-1.02307,0.64325-1.58789 l2.10815-0.23877c0.50391-0.05713,0.88428-0.48291,0.88428-0.98975V23.7373 C32.81006,23.22949,32.42969,22.80371,31.92529,22.74756z M27.03174,24.00879c0,1.71094-1.39209,3.10303-3.10303,3.10303 c-1.71533,0-3.11084-1.39209-3.11084-3.10303c0-1.71533,1.39551-3.11084,3.11084-3.11084 C25.63965,20.89795,27.03174,22.29346,27.03174,24.00879z' } }) + ]) +]) From 18f9278700b3b09cff18e3efaa9d6f439197758a Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 22 Dec 2021 18:03:15 +0100 Subject: [PATCH 078/191] Move workflow dependent coloring to the style sheet (#103) Simplifies color changes Removes a bit of styling from the code Allows Generic treatment form to be correctly styled without having to pass styling strings into sub-components --- src/js/components/CorrelationForm.js | 16 +++--- src/js/components/SignatureForm.js | 10 ++-- src/js/components/TreatmentCheck.js | 8 +-- src/js/main.scss | 74 ++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 17 deletions(-) diff --git a/src/js/components/CorrelationForm.js b/src/js/components/CorrelationForm.js index daad020c..2afb796d 100644 --- a/src/js/components/CorrelationForm.js +++ b/src/js/components/CorrelationForm.js @@ -51,29 +51,29 @@ function CorrelationForm(sources) { const query2 = state.form.query2 return div( [ - div('.row .blue .darken-3 .white-text', { style: { margin: '0px', padding: '20px 10px 10px 10px' } }, [ + div('.row .WF-header .white-text', { style: { margin: '0px', padding: '20px 10px 10px 10px' } }, [ // label('Query: '), div('.Default1 .waves-effect .col .s1 .center-align', [ - i('.large .center-align .material-icons .blue-text', { style: { fontSize: '45px', fontColor: 'gray' } }, 'search'), + i('.large .center-align .material-icons', { style: { fontSize: '45px', fontColor: 'gray' } }, 'search'), ]), input('.Query1 .col s10 .white-text', { style: { fontSize: '20px' }, props: { type: 'text', value: query1 }, value: query1 }), ]), div([ - (!validated1 || query1 == '') ? div('.blue.lighten-3',[checkdom1]) : div() + (!validated1 || query1 == '') ? div('.validation',[checkdom1]) : div() ]), - div('.row .blue .darken-3 .white-text', { style: { margin: '0px', padding: '20px 10px 10px 10px' } }, [ + div('.row .WF-header .white-text', { style: { margin: '0px', padding: '20px 10px 10px 10px' } }, [ div('.Default2 .waves-effect .col .s1 .center-align', [ - i('.large .center-align .material-icons .blue-text', { style: { fontSize: '45px', fontColor: 'gray' } }, 'search'), + i('.large .center-align .material-icons', { style: { fontSize: '45px', fontColor: 'gray' } }, 'search'), ]), input('.Query2 .col s10 .white-text', { style: { fontSize: '20px' }, props: { type: 'text', value: query2 }, value: query2 }), (validated1 && validated2) ? div('.SignatureCheck2 .waves-effect .col .s1 .center-align', [ - i('.large .material-icons', { style: { fontSize: '45px', fontColor: 'grey' } }, ['play_arrow'])]) + i('.large .material-icons .validated', { style: { fontSize: '45px', fontColor: 'grey' } }, ['play_arrow'])]) : div('.SignatureCheck2 .col .s1 .center-align', [ - i('.large .material-icons .blue-text', { style: { fontSize: '45px', fontColor: 'grey' } }, 'play_arrow')]) + i('.large .material-icons', { style: { fontSize: '45px', fontColor: 'grey' } }, 'play_arrow')]) ]), div([ - (!validated2 || query2 == '') ? div('.blue.lighten-3', [checkdom2]) : div() + (!validated2 || query2 == '') ? div('.validation', [checkdom2]) : div() ]) ]) }); diff --git a/src/js/components/SignatureForm.js b/src/js/components/SignatureForm.js index 99754ff3..f95c250a 100644 --- a/src/js/components/SignatureForm.js +++ b/src/js/components/SignatureForm.js @@ -101,21 +101,21 @@ function view(state$, signatureCheckDom$) { const query = state.form.query return div( [ - div('.row .pink .darken-4 .white-text', { style: { padding: '20px 10px 10px 10px' } }, [ + div('.row .WF-header .white-text', { style: { padding: '20px 10px 10px 10px' } }, [ // label('Query: '), div('.Default .waves-effect .col .s1 .center-align', [ - i('.large .center-align .material-icons .pink-text', { style: { fontSize: '45px', fontColor: 'gray' } }, 'search'), + i('.large .center-align .material-icons', { style: { fontSize: '45px', fontColor: 'gray' } }, 'search'), ]), input('.Query .col s10 .white-text', { style: { fontSize: '20px' }, props: { type: 'text', value: query }, value: query }), (validated) ? div('.SignatureCheck .waves-effect .col .s1 .center-align', [ - i('.large .material-icons', { style: { fontSize: '45px', fontColor: 'grey' } }, ['play_arrow'])]) + i('.large .material-icons .validated', { style: { fontSize: '45px', fontColor: 'grey' } }, ['play_arrow'])]) : div('.SignatureCheck .col .s1 .center-align', [ - i('.large .material-icons .pink-text', { style: { fontSize: '45px', fontColor: 'grey' } }, 'play_arrow')]) + i('.large .material-icons', { style: { fontSize: '45px', fontColor: 'grey' } }, 'play_arrow')]) // ]) ]), div([ - (!validated || query == '') ? div([checkdom]) : div() + (!validated || query == '') ? div(".validation", [checkdom]) : div() ]) ]) }) diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index 09081ea4..947bef45 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -126,12 +126,12 @@ function TreatmentCheck(sources) { const validated = state.core.validated return div([ div( - ".row .genetic .darken-4 .white-text", + ".row .WF-header .white-text", { style: { padding: "20px 10px 10px 10px" } }, [ div(".Default .waves-effect .col .s1 .center-align", [ i( - ".large .center-align .material-icons .orange-text", + ".large .center-align .material-icons", { style: { fontSize: "45px", fontColor: "gray" } }, "search" ), @@ -153,14 +153,14 @@ function TreatmentCheck(sources) { validated ? div(".treatmentCheck .waves-effect .col .s1 .center-align", [ i( - ".large .material-icons", + ".large .material-icons .validated", { style: { fontSize: "45px", fontColor: "grey" } }, ["play_arrow"] ), ]) : div(".treatmentCheck .col .s1 .center-align", [ i( - ".large .material-icons .orange-text", + ".large .material-icons", { style: { fontSize: "45px", fontColor: "grey" } }, "play_arrow" ), diff --git a/src/js/main.scss b/src/js/main.scss index df035520..2d34f899 100644 --- a/src/js/main.scss +++ b/src/js/main.scss @@ -20,11 +20,43 @@ main { .genetic { @extend .red; @extend .lighten-5; + + .WF-header { + @extend .red; + @extend .darken-4; + + i { + @extend .red-text; + } + } } .compound { @extend .orange; @extend .lighten-5; + + .WF-header { + @extend .orange; + @extend .darken-4; + + i { + @extend .orange-text; + } + } +} + +.ligand { + @extend .purple; + @extend .lighten-5; + + .WF-header { + @extend .purple; + @extend .darken-4; + + i { + @extend .purple-text; + } + } } .ligand { @@ -35,6 +67,20 @@ main { .disease { @extend .pink; @extend .lighten-5; + + .WF-header { + @extend .pink; + @extend .darken-4; + + i { + @extend .pink-text; + } + } + + .validation { + @extend .pink; + @extend .lighten-5; + } } .target { @@ -45,11 +91,39 @@ main { .correlation { @extend .blue; @extend .lighten-5; + + .WF-header { + @extend .blue; + @extend .darken-3; + + i { + @extend .blue-text; + } + } + + .validation { + @extend .blue; + @extend .lighten-3; + } } .generic { @extend .green; @extend .lighten-5; + + .WF-header { + @extend .green; + @extend .darken-4; + + i { + @extend .green-text; + } + } +} + +// general style for query 'go' button is that when it is validated that it becomes white +.WF-header i.validated { + @extend .white-text; } img.trt_img { From 4a57ebb10836e59eda10feea25f640390761fd9f Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 22 Dec 2021 18:10:23 +0100 Subject: [PATCH 079/191] Fix Github merging gone wrong Remove no longer needed import --- src/js/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/js/index.js b/src/js/index.js index 9d5a2aef..0fe8ad8c 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -27,8 +27,6 @@ import { loggerFactory } from './utils/logger' import { compoundSVG, targetSVG, ligandSVG, diseaseSVG, correlationSVG, settingsSVG } from './svg' -import { navbarModule } from "../../navbar.js"; - export default function Index(sources) { const {router} = sources; From 6e9034dcd868305a9a9b21b805bf834caa725b8b Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 22 Dec 2021 18:16:23 +0100 Subject: [PATCH 080/191] Add header Ligand WF SVG and home page link Needed extra modifications that couldn't easily be solved by the PRs as the changes were developed beside each other --- src/js/index.js | 4 ++-- src/js/pages/home.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/js/index.js b/src/js/index.js index 0fe8ad8c..338a4007 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -103,7 +103,7 @@ export default function Index(sources) { makeLink('/compound', span(['Compound', ' ', compoundSVG]), '.orange-text'), // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), makeLink('/genetic', span(['Genetic', ' ', targetSVG]), '.red-text'), - makeLink('/ligand', span(['Ligand', ' ', ]), '.purple-text'), + makeLink('/ligand', span(['Ligand', ' ', ligandSVG]), '.purple-text'), makeLink('/disease', span(['Disease', ' ', diseaseSVG]), '.pink-text'), makeLink('/correlation', span(['Correlation', ' ', correlationSVG]), '.blue-text'), makeLink('/settings', span(['Settings', ' ', settingsSVG]), '.grey-text'), @@ -117,7 +117,7 @@ export default function Index(sources) { makeLink('/compound', span(['Compound', ' ', compoundSVG]), '.orange-text'), // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), makeLink('/genetic', span(['Genetic', ' ', targetSVG]), '.red-text'), - makeLink('/ligand', span(['Ligand', ' ', ]), '.purple-text'), + makeLink('/ligand', span(['Ligand', ' ', ligandSVG]), '.purple-text'), makeLink('/disease', span(['Disease', ' ', diseaseSVG]), '.pink-text'), makeLink('/correlation', span(['Correlation', ' ', correlationSVG]), '.blue-text'), makeLink('/settings', span(['Settings', ' ', settingsSVG]), '.grey-text'), diff --git a/src/js/pages/home.js b/src/js/pages/home.js index 1266b882..d7c41740 100644 --- a/src/js/pages/home.js +++ b/src/js/pages/home.js @@ -47,6 +47,7 @@ function Home(sources) { makeLink('/compound', span({ style: { fontSize: "2rem" } }, ['Compound', ' ', compoundSVG]), '.orange-text .left'), // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), makeLink('/genetic', span({ style: { fontSize: "2rem" } }, ['Genetic', ' ', targetSVG]), '.red-text .left'), + makeLink('/ligand', span({ style: { fontSize: "2rem" } }, ['Ligand', ' ', ligandSVG]), '.purple-text .left'), //makeLink('/generic', span({ style: { fontSize: "2rem" } }, ['Ligand', ' ', ligandSVG]), '.purple-text .left'), makeLink('/disease', span({ style: { fontSize: "2rem" } }, ['Disease', ' ', diseaseSVG]), '.pink-text .left'), makeLink('/correlation', span({ style: { fontSize: "2rem" } }, ['Correlation', ' ', correlationSVG]), '.blue-text .left'), From d2758ddb791b046b28a1e158e03a8e60963f231c Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 24 Dec 2021 09:05:07 +0100 Subject: [PATCH 081/191] Display a message how the signature was created or why no signature was made (#96) * Display a message how the signature was created or why no signature was made For valid signatures: - 1 input sample: These are the [amountOfGenes] significant genes from the selected sample. - > 1 input samples: The intersection of the significant genes from the selected samples resulted in [amountOfGenes] genes. - 0 input samples: No samples were selected and yet a valid signature was generated. Please create a bug report! For invalid signatures: - 1 input sample: The selected sample didn't return any significant genes from which to create a signature! - > 1 input samples: The resulting signature is empty as there is no intersection of significant genes in the selected samples! - 0 input samples: No samples were selected from which to create a signature! * Add specific messages for the case there is only 1 significant gene * Remove concatenation of strings and amountOfGenes in case it is 1 and change it to a fixed string --- src/js/components/SignatureGenerator.js | 56 ++++++++++++++++++------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/src/js/components/SignatureGenerator.js b/src/js/components/SignatureGenerator.js index b24d4b33..94727902 100644 --- a/src/js/components/SignatureGenerator.js +++ b/src/js/components/SignatureGenerator.js @@ -33,7 +33,7 @@ function model(newInput$, request$, data$) { // Initialization const defaultReducer$ = xs.of(prevState => ({ ...prevState, core: { input: '' } })) // Add input to state - const inputReducer$ = newInput$.map(i => prevState => ({ ...prevState, core: { ...prevState.core, input: i } })) + const inputReducer$ = newInput$.map(i => prevState => ({ ...prevState, core: { ...prevState.core, input: i.core.input } })) // Add request body to state const requestReducer$ = request$.map(req => prevState => ({ ...prevState, core: { ...prevState.core, request: req } })) // Add data from API to state, update output key when relevant @@ -60,6 +60,8 @@ function view(state$, request$, response$, geneAnnotationQuery) { .map(r => r.body.result.join(" ")) .filter(s => s == '') + const amountOfInputs$ = state$.map((state) => (state.core?.input?.length)).compose(dropRepeats(equals)).remember() + const signature$ = xs.merge(validSignature$, invalidSignature$).remember() const geneStyle = { @@ -75,22 +77,46 @@ function view(state$, request$, response$, geneAnnotationQuery) { thisGene ]) - const validVdom$ = xs.combine(validSignature$, geneAnnotationQuery.DOM) - .map(([s, annotation]) => div('.card .orange .lighten-3', [ + const validVdom$ = xs.combine(validSignature$, geneAnnotationQuery.DOM, amountOfInputs$) + .map(([s, annotation, amount]) => div('.card .orange .lighten-3', [ div('.card-content .orange-text .text-darken-4', [ span('.card-title', 'Signature:'), div('.row', { style: { fontSize: "16px", fontStyle: 'bold' } }, s.split(" ").map(gene => showGene(gene))), - annotation + annotation, + p( { style: { fontSize: "16px" } }, + (() => { + const amountOfGenes = s.split(" ").length + if (amount == 1) + return amountOfGenes == 1 ? + "This is the 1 significant gene from the selected sample." : + "These are the " + amountOfGenes + " significant genes from the selected sample." + else if (amount > 1) + return amountOfGenes == 1 ? + "The intersection of the significant genes from the selected samples resulted in 1 gene." : + "The intersection of the significant genes from the selected samples resulted in " + amountOfGenes + " genes." + else + return "No samples were selected and yet a valid signature was generated. Please create a bug report!" + })() + ), ]) ])) .startWith(div('.card .orange .lighten-3', [])) - const invalidVdom$ = invalidSignature$ - .map(_ => div('.card .orange .lighten-3', [ + const invalidVdom$ = xs.combine(invalidSignature$, amountOfInputs$) + .map(([_, amount]) => div('.card .orange .lighten-3', [ div('.card-content .red-text .text-darken-1', [ div('.row', { style: { fontSize: "16px", fontStyle: 'bold' } }, [ - p('.center', { style: { fontSize: "26px" } }, "The resulting signature is empty, please check the sample selection!") + p('.center', { style: { fontSize: "26px" } }, + (() => { + if (amount == 1) + return "The selected sample didn't return any significant genes from which to create a signature!" + else if (amount > 1) + return "The resulting signature is empty as there is no intersection of significant genes in the selected samples!" + else + return "No samples were selected from which to create a signature!" + })() + ) ]) ]) ])) @@ -98,14 +124,14 @@ function view(state$, request$, response$, geneAnnotationQuery) { const loadingVdom$ = request$.compose(sampleCombine(state$)) .mapTo( - div('.card .orange .lighten-3', [ - div('.card-content .orange-text .text-darken-4', [ - span('.card-title', 'Signature:'), - div('.progress.orange.lighten-3.yellow-text', { style: { margin: '2px 0px 2px 0px'} }, [ - div('.indeterminate', {style : { "background-color" : 'orange' }}) - ]) - ]) - ])) + div('.card .orange .lighten-3', [ + div('.card-content .orange-text .text-darken-4', [ + span('.card-title', 'Signature:'), + div('.progress.orange.lighten-3.yellow-text', { style: { margin: '2px 0px 2px 0px'} }, [ + div('.indeterminate', {style : { "background-color" : 'orange' }}) + ]) + ]) + ])) .startWith(div('.card .orange .lighten-3', [])) .remember() From 38e7eb82637fdee62747674685b6b97fd91c9ea1 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 24 Dec 2021 11:06:48 +0100 Subject: [PATCH 082/191] Limit the amount of genes shown in the signature generator (#102) * Limit the amount of genes shown in the signature generator Add button to show the full signature, pressing again reduces the amount back to the limited view * Tweak displaying of 'Show More' button Right align button, make distinct color and smaller Add three dots at end of signature if it was truncated, using the gene style Clicking the three dots opens the full signature (and removes the dots) Shrinking to reduced signature can be done by the button * Change button to flat button Effectively only show text but still keep functionality of a button (cursor change) Add extra underline to highlight functionality, tbd if it will stay --- src/js/components/SignatureGenerator.js | 182 +++++++++++++++++++----- 1 file changed, 145 insertions(+), 37 deletions(-) diff --git a/src/js/components/SignatureGenerator.js b/src/js/components/SignatureGenerator.js index 94727902..d3f2d31b 100644 --- a/src/js/components/SignatureGenerator.js +++ b/src/js/components/SignatureGenerator.js @@ -13,6 +13,10 @@ import { GeneAnnotationQuery } from './GeneAnnotationQuery' import { absGene } from '../utils/utils' import { busyUiReducer, dirtyWrapperStream } from "../utils/ui" +/** + * @module components/SignatureGenerator + */ + const emptyData = { body: { result: { @@ -28,18 +32,27 @@ const signatureLens = { set: (state, childState) => ({ ...state, form: { ...state.form, signature: childState.core } }) }; -function model(newInput$, request$, data$) { - - // Initialization - const defaultReducer$ = xs.of(prevState => ({ ...prevState, core: { input: '' } })) - // Add input to state - const inputReducer$ = newInput$.map(i => prevState => ({ ...prevState, core: { ...prevState.core, input: i.core.input } })) - // Add request body to state - const requestReducer$ = request$.map(req => prevState => ({ ...prevState, core: { ...prevState.core, request: req } })) - // Add data from API to state, update output key when relevant - const dataReducer$ = data$.map(newData => prevState => ({ ...prevState, core: { ...prevState.core, data: newData, output: newData.join(" ") } })) - // Logic and reducer stream that monitors if this component is busy - const busyReducer$ = busyUiReducer(newInput$, data$) +function model(newInput$, request$, data$, showMore$) { + + // Initialization + const defaultReducer$ = xs.of(prevState => ({ ...prevState, core: { input: '' } })) + // Add input to state + const inputReducer$ = newInput$.map(i => prevState => ({ ...prevState, core: { ...prevState.core, input: i.core.input } })) + // Add request body to state + const requestReducer$ = request$.map(req => prevState => ({ ...prevState, core: { ...prevState.core, request: req } })) + // Add data from API to state, update output key when relevant + const dataReducer$ = data$.map(newData => prevState => ({ ...prevState, core: { ...prevState.core, data: newData, output: newData.join(" ") } })) + // Logic and reducer stream that monitors if this component is busy + const busyReducer$ = busyUiReducer(newInput$, data$) + // Toggle how many signature genes can be displayed + const limitReducer$ = showMore$.map(showMore => prevState => ({ + ...prevState, + core: { + ...prevState.core, + showMore: showMore, + showLimit: (showMore ? 0 : 100) + } + })) return xs.merge( defaultReducer$, @@ -47,9 +60,21 @@ function model(newInput$, request$, data$) { dataReducer$, requestReducer$, busyReducer$, + limitReducer$, ) } +/** + * SignatureGenerator view, display the component on the vdom + * @function view + * @param {Stream} state$ full state onion + * @param {Stream} request$ Trigger from API request to display the loading vdom + * @param {Stream} response$ Reply from API containing signature + * @param {Object} geneAnnotationQuery Object with DOM member to be added in vdom + * @returns Object with: + * - vdom$: VNodes object + * - signature$: stream of the processed signature + */ function view(state$, request$, response$, geneAnnotationQuery) { const validSignature$ = response$ @@ -72,35 +97,93 @@ function view(state$, request$, response$, geneAnnotationQuery) { } } + /** + * Wrap a gene string into a div with styling + * @const view/showGene + * @param {String} thisGene + * @returns VNode div + */ const showGene = (thisGene) => div('#' + absGene(thisGene) + '.col.orange.lighten-4.genePopup', geneStyle, [ thisGene ]) - const validVdom$ = xs.combine(validSignature$, geneAnnotationQuery.DOM, amountOfInputs$) - .map(([s, annotation, amount]) => div('.card .orange .lighten-3', [ - div('.card-content .orange-text .text-darken-4', [ - span('.card-title', 'Signature:'), - div('.row', { style: { fontSize: "16px", fontStyle: 'bold' } }, - s.split(" ").map(gene => showGene(gene))), - annotation, - p( { style: { fontSize: "16px" } }, - (() => { - const amountOfGenes = s.split(" ").length - if (amount == 1) - return amountOfGenes == 1 ? - "This is the 1 significant gene from the selected sample." : - "These are the " + amountOfGenes + " significant genes from the selected sample." - else if (amount > 1) - return amountOfGenes == 1 ? - "The intersection of the significant genes from the selected samples resulted in 1 gene." : - "The intersection of the significant genes from the selected samples resulted in " + amountOfGenes + " genes." - else - return "No samples were selected and yet a valid signature was generated. Please create a bug report!" - })() - ), + /** + * Vdom to be displayed when the signature is received and valid + * @const view/validVdom$ + * @type {Stream} + */ + const validVdom$ = xs.combine(validSignature$, geneAnnotationQuery.DOM, state$, amountOfInputs$) + .map(([s, annotation, state, amount]) => { + /** + * Signature split into an array + * @const view/validVdom$/arr + * @type {Array} + */ + const arr = s.split(" ") + /** + * Show full signature or not + * @const view/validVdom$/showMore + * @type {Boolean} + */ + const showMore = state.core.showMore + /** + * Signature size limit + * If set to 0 in the settings it means there is no limit, + * however if we would pass 0 to Array.splice we get nothing. + * Instead we have to pass Array.splice(0, undefined) which would be the same as Array.splice(0) + * @const view/validVdom$/showLimit + * @type {Number} + */ + const showLimit = state.core.showLimit > 0 ? state.core.showLimit : undefined + + /** + * Display div with a button when the signature is long, otherwise show empty div + * @const view/validVdom$/showLimit + * @type {VNode} + */ + const showMoreButton = (showMore || arr.length > showLimit) ? + div('.row', + div(".showMore .btn-flat .orange-text .text-darken-4 .right", + { style: { textDecoration: "underline"} }, + showMore ? "Show less" : + "Show " + (arr.length - showLimit) + " more") + ) : + div() + + return div('.card .orange .lighten-3', [ + div('.card-content .orange-text .text-darken-4', [ + span('.card-title', 'Signature:'), + div('.row', { style: { fontSize: "16px", fontStyle: 'bold' } }, + // genes to be shown, if needed limited amount + arr.slice(0, showLimit).map(gene => showGene(gene)) + .concat( + /// ... -> Show more + showLimit !== undefined ? + div('.showMore.col.orange.lighten-4', geneStyle, [ '...' ]) : + div() + ) + ), + showMoreButton, + annotation, + p( { style: { fontSize: "16px" } }, + (() => { + const amountOfGenes = s.split(" ").length + if (amount == 1) + return amountOfGenes == 1 ? + "This is the 1 significant gene from the selected sample." : + "These are the " + amountOfGenes + " significant genes from the selected sample." + else if (amount > 1) + return amountOfGenes == 1 ? + "The intersection of the significant genes from the selected samples resulted in 1 gene." : + "The intersection of the significant genes from the selected samples resulted in " + amountOfGenes + " genes." + else + return "No samples were selected and yet a valid signature was generated. Please create a bug report!" + })() + ), + ]) ]) - ])) + }) .startWith(div('.card .orange .lighten-3', [])) const invalidVdom$ = xs.combine(invalidSignature$, amountOfInputs$) @@ -144,7 +227,30 @@ function view(state$, request$, response$, geneAnnotationQuery) { } } -//function intent() {} +/** + * SignatureGenerator intent, convert events on the dom to actions + * @function intent + * @param {Stream} domSource$ events from the dom + * @returns Object with: + * - showMore: boolean when 'showMore' is toggled on or off + */ +function intent(domSource$) { + /** + * Toggles 'showMore' on and off + * @const intent/showMore$ + * @type {MemoryStream} + */ + const showMore$ = domSource$ + .select(".showMore") + .events("click") + .fold((x, _) => !x , false) + .startWith(false) + .remember() + + return { + showMore$: showMore$, + } +} /** @@ -204,7 +310,9 @@ function SignatureGenerator(sources) { const data$ = response$ .map(r => r.body.result) - const reducers$ = model(newInput$, request$, data$) + const actions = intent(sources.DOM) + + const reducers$ = model(newInput$, request$, data$, actions.showMore$) const views = view(state$, request$, response$, geneAnnotationQuery) From d9ae8ecc8eecedc4cfc557080bbd8030846cc17b Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Fri, 7 Jan 2022 10:25:21 +0100 Subject: [PATCH 083/191] Add deployments merging strategy ours/theirs + cleanup (#105) --- deployments.json | 5 +- src/js/configuration.js | 3 + src/js/index.js | 1158 +++++++++++++++++++++------------ src/js/pages/adminSettings.js | 38 +- 4 files changed, 790 insertions(+), 414 deletions(-) diff --git a/deployments.json b/deployments.json index a21729c7..225d840c 100644 --- a/deployments.json +++ b/deployments.json @@ -71,6 +71,9 @@ "config": { "logoUrl": "https://www.data-intuitive.com/images/logo_white.png", "normalStatisticsResponseTime": 1.0 + }, + "strategy": { + "deployments": "theirs" } } }, @@ -183,7 +186,7 @@ "endpoint": "classPath=com.dataintuitive.luciusapi.statistics" }, "api": { - "url": "http://localhost:8090/jobs?context=luciusapi&appName=luciusapi&sync=true&timeout=30" + "url": "http://localhost-test:8090/jobs?context=luciusapi&appName=luciusapi&sync=true&timeout=30" }, "sourire": { "url": "http://localhost:9999/molecule/" diff --git a/src/js/configuration.js b/src/js/configuration.js index 1e22eebb..b1b1f424 100644 --- a/src/js/configuration.js +++ b/src/js/configuration.js @@ -75,5 +75,8 @@ export const initSettings = { config: { showAdminButton: false, // logoUrl: 'https://www.data-intuitive.com/images/logo_white.png', + }, + strategy: { + deployments: "theirs" } }; diff --git a/src/js/index.js b/src/js/index.js index 338a4007..ada850c6 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -1,437 +1,793 @@ -import xs from 'xstream' - -import { div, nav, a, h3, p, ul, li, h1, h2, i, footer, header, main, svg, g, path, span, img } from '@cycle/dom' -import { merge, prop, mergeDeepLeft, mergeDeepRight } from 'ramda' -import * as R from 'ramda' +import xs from "xstream" +import delay from "xstream/extra/delay" +import dropRepeats from "xstream/extra/dropRepeats" + +import { + div, + nav, + a, + h3, + p, + ul, + li, + h1, + h2, + i, + footer, + header, + main, + svg, + g, + path, + span, + img, +} from "@cycle/dom" +import { merge, prop, mergeDeepLeft, mergeDeepRight } from "ramda" +import * as R from "ramda" // Workflows -import DiseaseWorkflow from './pages/disease' -import CompoundWorkflow from './pages/compound' -import LigandWorkflow from './pages/ligand' -import GeneticWorkflow from './pages/genetic' -import GenericTreatmentWorkflow from './pages/genericTreatment' -import TargetWorkflow from './pages/target' -import CorrelationWorkflow from './pages/correlation' +import DiseaseWorkflow from "./pages/disease" +import CompoundWorkflow from "./pages/compound" +import LigandWorkflow from "./pages/ligand" +import GeneticWorkflow from "./pages/genetic" +import GenericTreatmentWorkflow from "./pages/genericTreatment" +import TargetWorkflow from "./pages/target" +import CorrelationWorkflow from "./pages/correlation" // Pages -import StatisticsWorkflow from './pages/statistics' -import Debug from './pages/debug' -import Home from './pages/home' -import { IsolatedSettings } from './pages/settings' -import { IsolatedAdminSettings } from './pages/adminSettings' +import StatisticsWorkflow from "./pages/statistics" +import Debug from "./pages/debug" +import Home from "./pages/home" +import { IsolatedSettings } from "./pages/settings" +import { IsolatedAdminSettings } from "./pages/adminSettings" // Utilities -import { initSettings } from './configuration.js' -import initDeployments from '../../deployments.json' -import { loggerFactory } from './utils/logger' - -import { compoundSVG, targetSVG, ligandSVG, diseaseSVG, correlationSVG, settingsSVG } from './svg' +import { initSettings } from "./configuration.js" +import initDeployments from "../../deployments.json" +import { loggerFactory } from "./utils/logger" + +import { + compoundSVG, + targetSVG, + ligandSVG, + diseaseSVG, + correlationSVG, + settingsSVG, +} from "./svg" export default function Index(sources) { - const {router} = sources; + const { router } = sources - const logger = loggerFactory('index', sources.onion.state$, 'settings.common.debug') + const logger = loggerFactory( + "index", + sources.onion.state$, + "settings.common.debug" + ) - const state$ = sources.onion.state$ + const state$ = sources.onion.state$.debug("state") const page$ = router.routedComponent({ - '/': Home, - '/disease' : DiseaseWorkflow, + "/": Home, + "/disease": DiseaseWorkflow, // '/disease': { // '/': DiseaseWorkflow, // // '/:id': id => sources => DiseaseWorkflow({props$: id, ...sources}) // }, - '/compound': CompoundWorkflow, - '/target': TargetWorkflow, - '/genetic': GeneticWorkflow, - '/ligand': LigandWorkflow, - '/generic': GenericTreatmentWorkflow, - '/statistics': StatisticsWorkflow, - '/settings': IsolatedSettings, - '/correlation': CorrelationWorkflow, - '/debug': Debug, - '/admin': IsolatedAdminSettings, - '*': Home + "/compound": CompoundWorkflow, + "/target": TargetWorkflow, + "/genetic": GeneticWorkflow, + "/ligand": LigandWorkflow, + "/generic": GenericTreatmentWorkflow, + "/statistics": StatisticsWorkflow, + "/settings": IsolatedSettings, + "/correlation": CorrelationWorkflow, + "/debug": Debug, + "/admin": IsolatedAdminSettings, + "*": Home, })(sources) - const makeLink = (path, label, options) => { - const currentPage = window.location.href - const highlight = currentPage.endsWith(path) - - return li(highlight ? ".active" : "", [a(options, { props: { href: path } }, label)]) - } - - // TODO: Add a visual reference for ghost mode - // const ghost$ = state$ - // .filter(state => state.common.ghost) - // .compose(dropRepeats(equals)) - // .mapTo(i('.small .material-icons', 'flight_takeoff')) - // .startWith(span()) - - const nav$ = state$.map((state) => { - - const leftLogo = - state.settings.config.logoUrl ? - a('.left .grey-text .hide-on-med-and-down', { props: { href: '/' }, style: {margin: '5px'} }, - img(".logo_img .left", { props: { alt: 'logo', src: state.settings.config.logoUrl}, style: {height: '40px'}}), - ) - : - span() - - const centerLogo = - state.settings.config.logoUrl ? - div('.brand-logo .center', [ - a('.grey-text .hide-on-large-only', { props: { href: '/' } }, - img(".logo_img", { props: { alt: 'logo', src: state.settings.config.logoUrl}, - style: {height: '40px'} - }), - ), - ]) - : - div() - - return header({ style: {display: 'flex'} },[ - nav('#navigation .grey .darken-4', [ - div('.nav-wrapper .valign-wrapper', [ - a('.brand-logo .right .grey-text', { props: { href: "/" } }, - div({ style: { width: '140px' } }, logoSVG), - // span('.gradient', 'ComPass') - ), - leftLogo, - a('.sidenav-trigger', { props: { href: '#' }, attrs: {'data-target': 'mobile-demo' }}, i('.material-icons', 'menu')), - ul('.left .hide-on-med-and-down', [ - makeLink('/compound', span(['Compound', ' ', compoundSVG]), '.orange-text'), - // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), - makeLink('/genetic', span(['Genetic', ' ', targetSVG]), '.red-text'), - makeLink('/ligand', span(['Ligand', ' ', ligandSVG]), '.purple-text'), - makeLink('/disease', span(['Disease', ' ', diseaseSVG]), '.pink-text'), - makeLink('/correlation', span(['Correlation', ' ', correlationSVG]), '.blue-text'), - makeLink('/settings', span(['Settings', ' ', settingsSVG]), '.grey-text'), - // makeLink('/admin', span(['Admin']), '.blue-text'), - li(span('.grey-text .text-darken-3','', ["v", VERSION])), - ]), - centerLogo, - ]), - ]), - ul(".sidenav", {props: {id: 'mobile-demo'}}, [ - makeLink('/compound', span(['Compound', ' ', compoundSVG]), '.orange-text'), - // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), - makeLink('/genetic', span(['Genetic', ' ', targetSVG]), '.red-text'), - makeLink('/ligand', span(['Ligand', ' ', ligandSVG]), '.purple-text'), - makeLink('/disease', span(['Disease', ' ', diseaseSVG]), '.pink-text'), - makeLink('/correlation', span(['Correlation', ' ', correlationSVG]), '.blue-text'), - makeLink('/settings', span(['Settings', ' ', settingsSVG]), '.grey-text'), - // makeLink('/admin', span(['Admin']), '.blue-text'), - li(span('.grey-text .text-darken-3', { style: { padding: '0 32px' } }, ["v", VERSION])), - ]) - ]) - }) + const makeLink = (path, label, options) => { + const currentPage = window.location.href + const highlight = currentPage.endsWith(path) - const sidenavTrigger$ = sources.DOM.select('.sidenav-trigger').events('click') - const sidenavEvent$ = sidenavTrigger$ - .map((trigger) => ({element: '.sidenav', state: 'open'})) - - // We combine with state in order to read the customizations - // This works because the defaultReducer runs before anything else - const footer$ = state$ - .map(state => - footer('.page-footer .grey .darken-4 .grey-text', [ - div('.valign-wrapper .row', { style: { margin: '0px' } }, [ - div('.col .s8', { style: { margin: '0px' } }, [ - p({ style: { margin: '0px' } }, [ - 'Please use ', - a({ props: { href: '/statistics' } }, - 'the information'), - ' provided in ComPass with care. ', - 'Work instructions can be found via this link: ', - a({ props: { href: state.settings.deployment.customizations.wi } }, 'Work Instructions.') - ]), - p({ style: { margin: '0px' } }, [ - 'ComPass does not make any claims. ', - 'In case of issues, please include the contents of ', a({ props: { href: '/debug' } }, 'this page'), ' in your bug report' - ]), - ]), - div('.col .s4 .right-align', { style: {height: '100%', alignSelf: 'flex-end'} }, [ - p({ style: { margin: '0px' } }, [ - 'Open-source code can be found on ', - a({props: { href: 'https://github.com/data-intuitive/LuciusWeb' }}, 'GitHub') - ]), - ]), - ]), - ]) - ) - - const view$ = page$.map(prop('DOM')).flatten().remember() - - const vdom$ = xs.combine(nav$, view$, footer$) - .map(([navDom, viewDom, footerDom]) => div({ - style: { - display: 'flex', - 'min-height': '100vh', - 'flex-direction': 'column', - 'height': '100%' - } - }, [ - navDom, - main([viewDom]), - footerDom - ])) - .remember() - - // Initialize state - // Storageify ensures the state of the application is constantly cached. - // We only use the settings part of the stored state. - // Please note: with the addition of 'deployments', the requested deployment is added to the settings - // Overwrite recursively with the values from `deployments.json` using Ramda's `mergeDeepRight` - // The wanted deployment is contained in initSettings.deployment already without further details - // When it comes to component isolation, having the admin and user configuration together under the - // related key in settings makes sense. So we add the respective entries from deployment to where they should appear - const defaultReducer$ = xs.of(prevState => { - // Which deployment to use? - const desiredDeploymentName = initSettings.deployment.name - // Fetch the deployment - const desiredDeployment = R.head(initDeployments.filter(x => x.name == desiredDeploymentName)) - // Merge the deployment in settings.deployment - const updatedDeployment = mergeDeepRight(initSettings.deployment, desiredDeployment) - // Merge the updated deployment with the settings, by key. - const updatedSettings = merge(initSettings, { deployment : updatedDeployment}) - // Do the same with the administrative settings - const distributedAdminSettings = mergeDeepRight(updatedSettings, updatedSettings.deployment.services) - - /** - * Recursively check if all members of obj are present in value - * Does not check for equalitiy, just the value being present - * Completely ignores the required functionality for arrays - */ - const allPresent = (obj, value) => { - const keys = Object.keys(obj) - const valueKeys = Object.keys(value) - - const present = keys.map((key) => { - if (!R.contains(key, valueKeys)) - return false - if (typeof obj[key] === "object") - return allPresent(obj[key], value[key]) - return true - }) - - // return/check if all booleans in the array are true - return R.all(R.identity)(present) - } - - if (typeof prevState === 'undefined') { - // No pre-existing state information, use default settings - return ({ - settings: distributedAdminSettings, + return li(highlight ? ".active" : "", [ + a(options, { props: { href: path } }, label), + ]) + } + + // TODO: Add a visual reference for ghost mode + // const ghost$ = state$ + // .filter(state => state.common.ghost) + // .compose(dropRepeats(equals)) + // .mapTo(i('.small .material-icons', 'flight_takeoff')) + // .startWith(span()) + + const nav$ = state$.map((state) => { + const leftLogo = state.settings.config.logoUrl + ? a( + ".left .grey-text .hide-on-med-and-down", + { props: { href: "/" }, style: { margin: "5px" } }, + img(".logo_img .left", { + props: { alt: "logo", src: state.settings.config.logoUrl }, + style: { height: "40px" }, + }) + ) + : span() + + const centerLogo = state.settings.config.logoUrl + ? div(".brand-logo .center", [ + a( + ".grey-text .hide-on-large-only", + { props: { href: "/" } }, + img(".logo_img", { + props: { alt: "logo", src: state.settings.config.logoUrl }, + style: { height: "40px" }, }) - } else { - // Pre-existing state information. - // Safety check on old information - const sameVersionInSettings = prevState.settings.version == initSettings.version - const allPresentInSettings = allPresent(initSettings, prevState?.settings) - - if (!sameVersionInSettings) - console.log("Stored settings version doesn't match application settings version. Resetting settings to default values.") - if (!allPresentInSettings) - console.log("Stored settings don't match application settings structure. Resetting settings to default values.") - // If stored settings are different version or are invalid, use default settings. - return (sameVersionInSettings && allPresentInSettings) ? - ({ settings: prevState.settings }) : - ({ settings: distributedAdminSettings }) - } - }) - - const deploymentsReducer$ = sources.deployments.map(deployments => prevState => { - // Which deployment to use? - const desiredDeploymentName = prevState.settings.deployment.name - // Fetch the deployment - const desiredDeployment = R.head(deployments.filter(x => x.name == desiredDeploymentName)) - // Merge the deployment in settings.deployment - const updatedDeployment = mergeDeepRight(prevState.settings.deployment, desiredDeployment) - // Merge the updated deployment with the settings, by key. - const updatedSettings = merge(prevState.settings, { deployment : updatedDeployment}) - // Do the same with the administrative settings, keep value in settings if exist - only add from deployments if value is missing - const distributedAdminSettings = mergeDeepLeft(updatedSettings, updatedSettings.deployment.services) - return ({...prevState, settings: distributedAdminSettings }) - }) - - // Capture link targets and send to router driver - const router$ = sources.DOM.select('a').events('click') - .map(ev => ev.target.pathname) - .remember() - - // All clicks on links should be sent to the preventDefault driver - const prevent$ = sources.DOM.select('a').events('click').filter(ev => ev.target.pathname == '/debug'); - - const history$ = sources.onion.state$.fold((acc, x) => acc.concat([x]), [{}]) - - - - return { - log: xs.merge( - // logger(page$, 'page$', '>> ', ' > ', ''), - logger(state$, 'state$'), - logger(history$, 'history$'), - // logger(prevent$, 'prevent$'), - page$.map(prop('log')).filter(Boolean).flatten() + ), + ]) + : div() + + return header({ style: { display: "flex" } }, [ + nav("#navigation .grey .darken-4", [ + div(".nav-wrapper .valign-wrapper", [ + a( + ".brand-logo .right .grey-text", + { props: { href: "/" } }, + div({ style: { width: "140px" } }, logoSVG) + // span('.gradient', 'ComPass') + ), + leftLogo, + a( + ".sidenav-trigger", + { props: { href: "#" }, attrs: { "data-target": "mobile-demo" } }, + i(".material-icons", "menu") + ), + ul(".left .hide-on-med-and-down", [ + makeLink( + "/compound", + span(["Compound", " ", compoundSVG]), + ".orange-text" + ), + // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), + makeLink( + "/genetic", + span(["Genetic", " ", targetSVG]), + ".red-text" + ), + makeLink( + "/ligand", + span(["Ligand", " ", ligandSVG]), + ".purple-text" + ), + makeLink( + "/disease", + span(["Disease", " ", diseaseSVG]), + ".pink-text" + ), + makeLink( + "/correlation", + span(["Correlation", " ", correlationSVG]), + ".blue-text" + ), + makeLink( + "/settings", + span(["Settings", " ", settingsSVG]), + ".grey-text" + ), + // makeLink('/admin', span(['Admin']), '.blue-text'), + li(span(".grey-text .text-darken-3", "", ["v", VERSION])), + ]), + centerLogo, + ]), + ]), + ul(".sidenav", { props: { id: "mobile-demo" } }, [ + makeLink( + "/compound", + span(["Compound", " ", compoundSVG]), + ".orange-text" + ), + // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), + makeLink("/genetic", span(["Genetic", " ", targetSVG]), ".red-text"), + makeLink("/ligand", span(["Ligand", " ", ligandSVG]), ".purple-text"), + makeLink("/disease", span(["Disease", " ", diseaseSVG]), ".pink-text"), + makeLink( + "/correlation", + span(["Correlation", " ", correlationSVG]), + ".blue-text" ), - onion: xs.merge( - defaultReducer$, - deploymentsReducer$, - page$.map(prop('onion')).filter(Boolean).flatten() + makeLink( + "/settings", + span(["Settings", " ", settingsSVG]), + ".grey-text" ), - DOM: vdom$, - router: xs.merge(router$, page$.map(prop('router')).filter(Boolean).flatten()).remember(), - HTTP: page$.map(prop('HTTP')).filter(Boolean).flatten(), - vega: page$.map(prop('vega')).filter(Boolean).flatten(), - alert: page$.map(prop('alert')).filter(Boolean).flatten(), - preventDefault: xs.merge( - prevent$, - page$.map(prop('preventDefault')).filter(Boolean).flatten() + // makeLink('/admin', span(['Admin']), '.blue-text'), + li( + span(".grey-text .text-darken-3", { style: { padding: "0 32px" } }, [ + "v", + VERSION, + ]) ), - popup: page$.map(prop('popup')).filter(Boolean).flatten(), - modal: page$.map(prop('modal')).filter(Boolean).flatten(), - ac: page$.map(prop('ac')).filter(Boolean).flatten(), - sidenav: sidenavEvent$, - storage: page$.map(prop('storage')).filter(Boolean).flatten(), - deployments: page$.map(prop('deployments')).filter(Boolean).flatten() + ]), + ]) + }) + + const sidenavTrigger$ = sources.DOM.select(".sidenav-trigger").events("click") + const sidenavEvent$ = sidenavTrigger$.map((trigger) => ({ + element: ".sidenav", + state: "open", + })) + + // We combine with state in order to read the customizations + // This works because the defaultReducer runs before anything else + const footer$ = state$.map((state) => + footer(".page-footer .grey .darken-4 .grey-text", [ + div(".valign-wrapper .row", { style: { margin: "0px" } }, [ + div(".col .s8", { style: { margin: "0px" } }, [ + p({ style: { margin: "0px" } }, [ + "Please use ", + a({ props: { href: "/statistics" } }, "the information"), + " provided in ComPass with care. ", + "Work instructions can be found via this link: ", + a( + { props: { href: state.settings.deployment.customizations.wi } }, + "Work Instructions." + ), + ]), + p({ style: { margin: "0px" } }, [ + "ComPass does not make any claims. ", + "In case of issues, please include the contents of ", + a({ props: { href: "/debug" } }, "this page"), + " in your bug report", + ]), + ]), + div( + ".col .s4 .right-align", + { style: { height: "100%", alignSelf: "flex-end" } }, + [ + p({ style: { margin: "0px" } }, [ + "Open-source code can be found on ", + a( + { + props: { + href: "https://github.com/data-intuitive/LuciusWeb", + }, + }, + "GitHub" + ), + ]), + ] + ), + ]), + ]) + ) + + const view$ = page$.map(prop("DOM")).flatten().remember() + + const vdom$ = xs + .combine(nav$, view$, footer$) + .map(([navDom, viewDom, footerDom]) => + div( + { + style: { + display: "flex", + "min-height": "100vh", + "flex-direction": "column", + height: "100%", + }, + }, + [navDom, main([viewDom]), footerDom] + ) + ) + .remember() + + // Initialize state + // Storageify ensures the state of the application is constantly cached. + // We only use the settings part of the stored state. + // Please note: with the addition of 'deployments', the requested deployment is added to the settings + // Overwrite recursively with the values from `deployments.json` using Ramda's `mergeDeepRight` + // The wanted deployment is contained in initSettings.deployment already without further details + // When it comes to component isolation, having the admin and user configuration together under the + // related key in settings makes sense. So we add the respective entries from deployment to where they should appear + const defaultReducer$ = xs.of((prevState) => { + // Which deployment to use? + const desiredDeploymentName = initSettings.deployment.name + // Fetch the deployment + const desiredDeployment = R.head( + initDeployments.filter((x) => x.name == desiredDeploymentName) + ) + // Merge the deployment in settings.deployment + const updatedDeployment = mergeDeepRight( + initSettings.deployment, + desiredDeployment + ) + // Merge the updated deployment with the settings, by key. + const updatedSettings = merge(initSettings, { + deployment: updatedDeployment, + }) + // Do the same with the administrative settings + const distributedAdminSettings = mergeDeepRight( + updatedSettings, + updatedSettings.deployment.services + ) + + /** + * Recursively check if all members of obj are present in value + * Does not check for equalitiy, just the value being present + * Completely ignores the required functionality for arrays + */ + const allPresent = (obj, value) => { + const keys = Object.keys(obj) + const valueKeys = Object.keys(value) + + const present = keys.map((key) => { + if (!R.contains(key, valueKeys)) return false + if (typeof obj[key] === "object") + return allPresent(obj[key], value[key]) + return true + }) + + // return/check if all booleans in the array are true + return R.all(R.identity)(present) } + if (typeof prevState === "undefined") { + // No pre-existing state information, use default settings + console.log("prevState = undefined") + return { + settings: distributedAdminSettings, + } + } else { + // Pre-existing state information. + // Safety check on old information + const sameVersionInSettings = + prevState.settings.version == initSettings.version + const allPresentInSettings = allPresent(initSettings, prevState?.settings) + + if (!sameVersionInSettings) + console.log( + "Stored settings version doesn't match application settings version. Resetting settings to default values." + ) + if (!allPresentInSettings) + console.log( + "Stored settings don't match application settings structure. Resetting settings to default values." + ) + // If stored settings are different version or are invalid, use default settings. + return sameVersionInSettings && allPresentInSettings + ? { settings: prevState.settings } + : { settings: distributedAdminSettings } + } + }) + + const deploymentsReducer$ = sources.deployments.map( + (deployments) => (prevState) => { + // Which deployment to use? + const desiredDeploymentName = prevState.settings.deployment.name + // Fetch the deployment + const desiredDeployment = R.head( + deployments.filter((x) => x.name == desiredDeploymentName) + ) + // Merge the deployment in settings.deployment + const updatedDeployment = mergeDeepRight( + prevState.settings.deployment, + desiredDeployment + ) + // Merge the updated deployment with the settings, by key. + const updatedSettings = merge(prevState.settings, { + deployment: updatedDeployment, + }) + // Do the same with the administrative settings, keep value in settings if exist - only add from deployments if value is missing + // Take into account the strategy for dealing with deployments configuration + const distributedAdminSettings = + prevState.settings.strategy.deployments == "theirs" + ? mergeDeepRight(updatedSettings, updatedSettings.deployment.services) + : mergeDeepLeft(updatedSettings, updatedSettings.deployment.services) + return { ...prevState, settings: distributedAdminSettings } + } + ) + + // Capture link targets and send to router driver + const router$ = sources.DOM.select("a") + .events("click") + .map((ev) => ev.target.pathname) + .remember() + + // All clicks on links should be sent to the preventDefault driver + const prevent$ = sources.DOM.select("a") + .events("click") + .filter((ev) => ev.target.pathname == "/debug") + + const history$ = sources.onion.state$.fold((acc, x) => acc.concat([x]), [{}]) + + return { + log: xs.merge( + // logger(page$, 'page$', '>> ', ' > ', ''), + logger(state$, "state$"), + logger(history$, "history$"), + // logger(prevent$, 'prevent$'), + page$.map(prop("log")).filter(Boolean).flatten() + ), + onion: xs.merge( + defaultReducer$.debug("defaultReducer"), + deploymentsReducer$.debug("deplRed"), + page$.map(prop("onion")).filter(Boolean).flatten() + ), + DOM: vdom$, + router: xs + .merge(router$, page$.map(prop("router")).filter(Boolean).flatten()) + .remember(), + HTTP: page$.map(prop("HTTP")).filter(Boolean).flatten(), + vega: page$.map(prop("vega")).filter(Boolean).flatten(), + alert: page$.map(prop("alert")).filter(Boolean).flatten(), + preventDefault: xs.merge( + prevent$, + page$.map(prop("preventDefault")).filter(Boolean).flatten() + ), + popup: page$.map(prop("popup")).filter(Boolean).flatten(), + modal: page$.map(prop("modal")).filter(Boolean).flatten(), + ac: page$.map(prop("ac")).filter(Boolean).flatten(), + sidenav: sidenavEvent$, + storage: page$.map(prop("storage")).filter(Boolean).flatten(), + deployments: page$ + .map(prop("deployments")) + .filter(Boolean) + .flatten() + .debug("deployments"), + } } -export const logoSVG = svg({ id: 'logo', attrs: { viewBox: "159 26 1060 460" } }, [ +export const logoSVG = svg( + { id: "logo", attrs: { viewBox: "159 26 1060 460" } }, + [ svg.defs([ - svg.linearGradient({ attrs: { id: 'gradient', x1: '0%', y1: '0%', x2: '100%', y2: '100%', gradientUnits: "userSpaceOnUse" } }, [ - svg.stop({ attrs: { offset: "0%" }, style: { "stop-color": "#ff9800", "stop-opacity": "1" } }), - svg.stop({ attrs: { offset: "50%" }, style: { "stop-color": "#f44336", "stop-opacity": "1" } }), - svg.stop({ attrs: { offset: "100%" }, style: { "stop-color": "#e91e63", "stop-opacity": "1" } }) - ]) + svg.linearGradient( + { + attrs: { + id: "gradient", + x1: "0%", + y1: "0%", + x2: "100%", + y2: "100%", + gradientUnits: "userSpaceOnUse", + }, + }, + [ + svg.stop({ + attrs: { offset: "0%" }, + style: { "stop-color": "#ff9800", "stop-opacity": "1" }, + }), + svg.stop({ + attrs: { offset: "50%" }, + style: { "stop-color": "#f44336", "stop-opacity": "1" }, + }), + svg.stop({ + attrs: { offset: "100%" }, + style: { "stop-color": "#e91e63", "stop-opacity": "1" }, + }), + ] + ), ]), svg.g({ attrs: { fill: "url(#gradient)" } }, [ - svg.path({ attrs: { d: 'M 389 256 L 389 26 L 342 201 Z', } }), - svg.path({ attrs: { d: 'M 389 256 L 159 256 L 334 303 Z' } }), - svg.path({ attrs: { d: 'M 389 256 L 389 486 L 436 311 Z' } }), - svg.path({ attrs: { d: 'M 389 256 L 619 256 L 444 209 Z' } }), - //svg.text({ attrs: { 'font-family': 'Garamond, serif', 'font-size': "160", 'font-weight': "bold", x: "450", y: "398" } }, 'COMPASS'), - svg.path({ attrs: { transform:"translate(460,300)", d: "M 42.517 108.894 A 73.307 73.307 0 0 0 60.16 110.965 A 111.506 111.506 0 0 0 67.956 110.701 A 87.869 87.869 0 0 0 78.4 109.365 A 102.036 102.036 0 0 0 79.766 109.093 Q 87.225 107.553 92.8 105.205 A 11.877 11.877 0 0 0 94.135 104.431 A 8.802 8.802 0 0 0 95.44 103.365 A 5.185 5.185 0 0 0 95.872 102.873 Q 96.619 101.909 97.12 100.405 Q 98.24 96.885 99.04 93.525 A 74.101 74.101 0 0 0 99.604 90.961 A 59.633 59.633 0 0 0 100.24 87.205 A 55.183 55.183 0 0 0 100.451 85.441 Q 100.64 83.595 100.64 82.165 A 5.632 5.632 0 0 0 100.64 82.147 Q 100.634 80.24 99.332 79.674 A 3.055 3.055 0 0 0 98.32 79.445 Q 96.55 79.323 95.385 80.972 A 6.972 6.972 0 0 0 94.72 82.165 A 56.752 56.752 0 0 1 93.8 84.01 Q 91.762 87.912 89.352 90.983 A 31.476 31.476 0 0 1 82.4 97.605 Q 77.685 100.834 71.132 101.994 A 45.53 45.53 0 0 1 63.2 102.645 Q 54.08 102.645 47.36 98.965 Q 40.64 95.285 36 88.725 Q 31.36 82.165 29.12 73.125 A 72.598 72.598 0 0 1 27.744 66.056 A 87.496 87.496 0 0 1 26.88 53.525 Q 26.88 45.205 29.44 37.525 Q 32 29.845 36.72 23.765 Q 41.44 17.685 48.08 14.165 A 29.342 29.342 0 0 1 53.049 12.09 A 32.658 32.658 0 0 1 62.88 10.645 A 58.028 58.028 0 0 1 64.994 10.683 Q 71.646 10.926 76.56 12.725 A 36.033 36.033 0 0 1 78.031 13.3 Q 82.667 15.232 85.44 18.005 Q 88.48 20.725 90 24.405 Q 91.52 28.085 92.64 31.765 A 6.448 6.448 0 0 0 92.647 31.785 Q 92.855 32.403 93.155 32.879 A 3.239 3.239 0 0 0 94.8 34.245 Q 95.51 34.507 96.167 34.507 A 3.165 3.165 0 0 0 97.6 34.165 A 2.181 2.181 0 0 0 98.779 32.667 A 3.73 3.73 0 0 0 98.88 31.765 Q 98.88 29.734 98.798 27.455 A 165.242 165.242 0 0 0 98.56 22.805 Q 98.24 17.845 97.6 12.565 Q 97.528 11.695 97.275 11.006 A 3.802 3.802 0 0 0 96.4 9.605 A 7.672 7.672 0 0 0 95.926 9.169 A 8.327 8.327 0 0 0 93.92 7.925 Q 89.92 6.485 85.04 5.205 Q 80.16 3.925 74.96 3.205 Q 69.76 2.485 64.64 2.485 Q 49.6 2.485 37.68 7.045 A 73.108 73.108 0 0 0 32.282 9.359 A 60.996 60.996 0 0 0 17.28 19.365 Q 8.8 27.125 4.4 37.125 Q 0 47.125 0 58.165 A 57.845 57.845 0 0 0 0.137 62.174 A 48.394 48.394 0 0 0 4.64 79.765 Q 9.28 89.525 17.6 96.485 Q 25.92 103.445 36.8 107.205 A 65.874 65.874 0 0 0 42.517 108.894 Z", id:"0", vectorEffect:"non-scaling-stroke" } }), - svg.path({ attrs: { transform:"translate(460,300)", d: "M 168.8 110.965 Q 157.76 110.965 147.76 107.205 Q 137.76 103.445 130 96.485 Q 122.24 89.525 117.84 79.845 A 49.402 49.402 0 0 1 113.682 63.757 A 59.923 59.923 0 0 1 113.44 58.325 Q 113.44 47.445 117.04 37.445 Q 120.64 27.445 127.84 19.605 A 50.961 50.961 0 0 1 142.36 8.694 A 60.247 60.247 0 0 1 145.68 7.125 A 54.565 54.565 0 0 1 159.472 3.251 A 72.704 72.704 0 0 1 170.24 2.485 Q 181.44 2.485 191.44 6.245 Q 201.44 10.005 209.12 16.965 Q 216.8 23.925 221.28 33.605 Q 225.76 43.285 225.76 55.125 Q 225.76 65.845 222.08 75.925 Q 218.4 86.005 211.2 93.845 A 50.961 50.961 0 0 1 196.68 104.757 A 60.247 60.247 0 0 1 193.36 106.325 A 54.565 54.565 0 0 1 179.569 110.2 A 72.704 72.704 0 0 1 168.8 110.965 Z M 171.68 103.605 A 26.844 26.844 0 0 0 179.094 102.625 A 21.365 21.365 0 0 0 186.96 98.485 A 27.514 27.514 0 0 0 193.442 90.523 A 37.424 37.424 0 0 0 196.16 84.245 Q 199.2 75.125 199.2 63.285 Q 199.2 53.525 197.36 44.005 Q 195.52 34.485 191.6 26.805 Q 187.68 19.125 181.6 14.565 Q 175.52 10.005 167.2 10.005 A 26.016 26.016 0 0 0 159.655 11.057 A 21.4 21.4 0 0 0 152.08 15.125 A 27.98 27.98 0 0 0 145.339 23.499 A 37.303 37.303 0 0 0 142.88 29.205 Q 139.932 37.894 139.843 49.291 A 91.218 91.218 0 0 0 139.84 50.005 Q 139.84 59.925 141.76 69.445 A 73.734 73.734 0 0 0 144.886 80.561 A 61.444 61.444 0 0 0 147.52 86.645 Q 151.36 94.325 157.36 98.965 A 22.185 22.185 0 0 0 169.511 103.523 A 28.279 28.279 0 0 0 171.68 103.605 Z", id:"1", vectorEffect:"non-scaling-stroke" } }), - svg.path({ attrs: { transform:"translate(460,300)", d: "M 280 18.965 L 303.52 75.125 A 1.737 1.737 0 0 0 303.731 75.456 Q 303.908 75.669 304.133 75.769 A 1.008 1.008 0 0 0 304.4 75.845 A 0.705 0.705 0 0 0 304.5 75.853 Q 304.88 75.853 305.14 75.414 A 2.079 2.079 0 0 0 305.28 75.125 L 328.16 22.005 Q 329.162 16.996 329.844 13.651 A 1135.171 1135.171 0 0 1 330 12.885 A 101.259 101.259 0 0 1 330.442 10.826 Q 330.92 8.715 331.36 7.285 A 3.896 3.896 0 0 1 331.783 6.339 Q 332.447 5.294 333.703 5.212 A 3.35 3.35 0 0 1 333.92 5.205 L 340.8 5.205 Q 347.52 4.885 354.8 4.165 Q 360.801 3.572 364.681 3.468 A 58.182 58.182 0 0 1 366.24 3.445 A 7.727 7.727 0 0 1 367.651 3.568 A 5.754 5.754 0 0 1 369.04 4.005 A 2.337 2.337 0 0 1 369.686 4.438 Q 370.18 4.917 370.234 5.659 A 2.568 2.568 0 0 1 370.24 5.845 A 3.116 3.116 0 0 1 370.139 6.667 A 2.064 2.064 0 0 1 369.2 7.925 Q 368.16 8.565 366.56 9.045 Q 365.825 9.29 364.903 9.676 A 38.566 38.566 0 0 0 364.32 9.925 A 60.68 60.68 0 0 0 363.262 10.4 Q 362.781 10.622 362.357 10.829 A 34.278 34.278 0 0 0 362.08 10.965 A 24.408 24.408 0 0 0 359.476 12.352 Q 358.179 13.153 357.193 14.035 A 11.367 11.367 0 0 0 355.92 15.365 Q 353.998 17.749 354.219 22.868 A 26.82 26.82 0 0 0 354.24 23.285 Q 354.56 34.805 355.04 43.445 Q 355.52 52.085 355.92 59.445 Q 356.32 66.805 356.88 74.725 Q 357.265 80.17 357.688 86.863 A 2486.888 2486.888 0 0 1 358.08 93.205 Q 358.4 97.365 361.52 99.845 A 17.172 17.172 0 0 0 364.885 101.907 Q 367.04 102.925 369.76 103.605 Q 371.36 104.085 372.48 104.725 A 2.102 2.102 0 0 1 373.55 106.233 A 3.129 3.129 0 0 1 373.6 106.805 Q 373.6 107.925 372.64 108.565 Q 371.68 109.205 370.24 109.205 A 124.521 124.521 0 0 1 366.961 109.159 Q 365.284 109.115 363.422 109.028 A 214.634 214.634 0 0 1 362.16 108.965 Q 357.6 108.725 353.2 108.565 Q 348.8 108.405 345.6 108.405 Q 342.4 108.405 338.48 108.565 Q 334.56 108.725 330.56 108.965 Q 326.56 109.205 322.88 109.205 A 6.326 6.326 0 0 1 321.48 109.056 A 5.104 5.104 0 0 1 320.32 108.645 A 2.421 2.421 0 0 1 319.742 108.252 A 1.688 1.688 0 0 1 319.2 106.965 A 2.958 2.958 0 0 1 319.371 105.938 A 2.56 2.56 0 0 1 320.32 104.725 Q 321.44 103.925 323.04 103.605 A 33.68 33.68 0 0 0 325.926 102.937 Q 328.739 102.142 330.36 101.01 A 6.886 6.886 0 0 0 331.2 100.325 Q 333.291 98.309 333.291 93.854 A 20.13 20.13 0 0 0 333.28 93.205 Q 332.8 83.285 332.48 73.445 Q 332.16 63.605 331.92 54.085 A 657.231 657.231 0 0 0 331.259 36.728 A 600.597 600.597 0 0 0 331.2 35.605 Q 331.2 34.485 330.64 34.565 A 0.762 0.762 0 0 0 330.265 34.743 Q 329.915 35.03 329.6 35.765 L 299.04 108.405 A 5.387 5.387 0 0 1 298.705 109.169 Q 297.956 110.543 296.715 110.339 A 2.202 2.202 0 0 1 296.64 110.325 A 3.756 3.756 0 0 1 295.619 109.974 A 2.786 2.786 0 0 1 294.4 108.725 Q 286.56 90.165 278 72.085 Q 269.44 54.005 262.24 36.245 A 3.156 3.156 0 0 0 262.06 35.806 Q 261.635 34.977 260.906 35.147 A 1.42 1.42 0 0 0 260.72 35.205 A 1.58 1.58 0 0 0 259.891 35.962 Q 259.736 36.247 259.641 36.626 A 4.851 4.851 0 0 0 259.52 37.365 Q 259.04 45.365 258.72 54.885 Q 258.4 64.405 258.4 74.085 L 258.4 92.085 A 11.336 11.336 0 0 0 258.894 95.507 A 9.135 9.135 0 0 0 261.28 99.365 A 12.265 12.265 0 0 0 263.913 101.274 Q 266.48 102.694 270.339 103.591 A 39.254 39.254 0 0 0 270.4 103.605 Q 272 103.925 273.44 104.645 A 3.112 3.112 0 0 1 274.184 105.151 A 2.171 2.171 0 0 1 274.88 106.805 Q 274.88 107.905 273.646 108.542 A 4.015 4.015 0 0 1 273.6 108.565 Q 272.32 109.205 270.88 109.205 Q 266.56 109.205 264.16 108.965 Q 261.76 108.725 259.84 108.645 Q 258.37 108.584 256.056 108.57 A 241.657 241.657 0 0 0 254.56 108.565 Q 251.04 108.565 247.36 108.725 Q 243.68 108.885 240.96 109.045 Q 238.738 109.176 237.798 109.2 A 14.405 14.405 0 0 1 237.44 109.205 Q 235.52 109.205 234.56 108.645 A 2.008 2.008 0 0 1 233.961 108.128 Q 233.652 107.721 233.608 107.159 A 2.45 2.45 0 0 1 233.6 106.965 A 2.295 2.295 0 0 1 233.708 106.238 Q 233.983 105.413 234.96 105.125 A 41.882 41.882 0 0 0 236.227 104.729 Q 236.874 104.516 237.593 104.261 A 74.939 74.939 0 0 0 238.08 104.085 A 14.817 14.817 0 0 0 243.664 100.558 A 17.927 17.927 0 0 0 244.56 99.605 Q 247.36 96.405 248 91.445 Q 248.66 86.385 249.358 79.699 A 808.068 808.068 0 0 0 250 73.285 Q 251.04 62.485 252.16 50.165 Q 253.28 37.845 254.08 25.845 Q 254.197 25.148 254.228 24.197 A 22.532 22.532 0 0 0 254.24 23.445 L 254.24 20.405 A 15.303 15.303 0 0 0 254.146 18.645 Q 254.044 17.77 253.834 17.044 A 6.444 6.444 0 0 0 253.36 15.845 Q 252.48 14.165 250.4 13.045 Q 248.48 11.765 246.24 10.885 Q 244 10.005 241.44 9.365 A 12.798 12.798 0 0 1 240.164 8.979 Q 239.543 8.753 239.037 8.48 A 5.984 5.984 0 0 1 238.4 8.085 Q 237.28 7.285 237.28 6.005 A 2.466 2.466 0 0 1 237.39 5.244 Q 237.655 4.425 238.56 4.085 A 7.122 7.122 0 0 1 239.913 3.733 Q 240.571 3.625 241.32 3.608 A 12.488 12.488 0 0 1 241.6 3.605 Q 244 3.605 247.52 3.765 Q 251.04 3.925 255.28 4.005 Q 259.52 4.085 263.92 4.245 Q 268.32 4.405 272 4.405 A 3.689 3.689 0 0 1 274.011 5.009 A 4.772 4.772 0 0 1 274.4 5.285 A 4.895 4.895 0 0 1 275.175 6.036 A 3.481 3.481 0 0 1 275.84 7.285 Q 276.96 10.325 277.92 13.125 Q 278.88 15.925 280 18.965 Z", id:"2", vectorEffect:"non-scaling-stroke" } }), - svg.path({ attrs: { transform:"translate(460,300)", d: "M 393.92 93.045 L 393.92 21.365 A 24.447 24.447 0 0 0 393.773 18.576 Q 393.438 15.671 392.344 13.937 A 6.247 6.247 0 0 0 392 13.445 A 6.811 6.811 0 0 0 390.327 11.947 Q 388.579 10.8 385.67 10.016 A 31.125 31.125 0 0 0 384.32 9.685 Q 382.496 9.264 381.656 8.536 A 2.551 2.551 0 0 1 381.44 8.325 A 3.112 3.112 0 0 1 380.834 7.37 A 2.829 2.829 0 0 1 380.64 6.325 Q 380.64 5.365 381.6 4.645 Q 382.444 4.013 383.782 3.936 A 6.615 6.615 0 0 1 384.16 3.925 Q 387.84 3.925 391.6 4.085 Q 395.36 4.245 398.96 4.485 A 109.621 109.621 0 0 0 403.304 4.692 A 91.493 91.493 0 0 0 405.76 4.725 A 63.208 63.208 0 0 0 408.952 4.649 Q 410.895 4.55 412.56 4.325 Q 415.52 3.925 419.12 3.525 A 57.801 57.801 0 0 1 422.034 3.282 Q 424.64 3.125 427.84 3.125 A 74.922 74.922 0 0 1 440.24 4.088 Q 446.789 5.189 452.105 7.548 A 37.317 37.317 0 0 1 458.8 11.365 A 27.236 27.236 0 0 1 467.187 20.771 Q 469.954 26.023 470.213 32.625 A 35.159 35.159 0 0 1 470.24 34.005 A 41.752 41.752 0 0 1 467.704 48.641 A 39.421 39.421 0 0 1 466 52.565 Q 461.76 61.045 453.2 66.245 Q 444.64 71.445 431.52 71.445 A 21.718 21.718 0 0 1 429.227 71.333 Q 426.719 71.065 425.2 70.165 Q 423.04 68.885 423.04 67.125 A 2.072 2.072 0 0 1 423.781 65.52 A 3.105 3.105 0 0 1 424.08 65.285 Q 424.729 64.836 425.284 64.667 A 2.186 2.186 0 0 1 425.92 64.565 A 13.015 13.015 0 0 1 427.32 64.637 Q 428.22 64.734 428.96 64.965 Q 430.24 65.365 432.48 65.365 A 9.839 9.839 0 0 0 440.581 61.31 A 17.306 17.306 0 0 0 442.64 57.845 A 33.299 33.299 0 0 0 444.803 51.238 Q 445.521 48.018 445.836 44.233 A 75.351 75.351 0 0 0 446.08 38.005 A 71.948 71.948 0 0 0 445.742 30.761 Q 444.99 23.366 442.573 18.803 A 16.941 16.941 0 0 0 441.28 16.725 A 15.331 15.331 0 0 0 430.194 10.295 A 20.798 20.798 0 0 0 427.84 10.165 Q 424.873 10.165 422.853 11.043 A 7.065 7.065 0 0 0 421.04 12.165 Q 418.982 13.94 418.75 17.414 A 13.668 13.668 0 0 0 418.72 18.325 L 418.56 92.725 A 15.111 15.111 0 0 0 418.774 95.363 Q 419.023 96.762 419.56 97.846 A 6.266 6.266 0 0 0 421.28 100.005 A 11.172 11.172 0 0 0 423.248 101.224 Q 425.345 102.269 428.593 103.106 A 53.266 53.266 0 0 0 430.72 103.605 A 9.395 9.395 0 0 1 431.734 103.844 Q 432.229 103.993 432.626 104.185 A 3.715 3.715 0 0 1 433.36 104.645 Q 434.221 105.35 434.24 106.744 A 4.638 4.638 0 0 1 434.24 106.805 A 2.515 2.515 0 0 1 434.121 107.6 A 2.037 2.037 0 0 1 433.44 108.565 A 2.681 2.681 0 0 1 432.532 109.03 Q 432.153 109.145 431.698 109.185 A 5.749 5.749 0 0 1 431.2 109.205 A 124.521 124.521 0 0 1 427.921 109.159 Q 426.244 109.115 424.382 109.028 A 214.634 214.634 0 0 1 423.12 108.965 Q 418.56 108.725 414.08 108.565 Q 409.6 108.405 406.4 108.405 Q 403.04 108.405 399.04 108.565 A 495.61 495.61 0 0 0 391.329 108.936 A 541.268 541.268 0 0 0 390.8 108.965 Q 386.56 109.205 383.04 109.205 A 4.947 4.947 0 0 1 381.858 109.07 A 3.961 3.961 0 0 1 380.8 108.645 A 2.008 2.008 0 0 1 380.201 108.128 Q 379.892 107.721 379.848 107.159 A 2.45 2.45 0 0 1 379.84 106.965 A 2.907 2.907 0 0 1 381.219 104.408 Q 381.748 104.056 382.48 103.81 A 8.312 8.312 0 0 1 383.2 103.605 Q 389.28 102.325 391.6 100.325 Q 393.778 98.448 393.912 93.681 A 22.691 22.691 0 0 0 393.92 93.045 Z", id:"3", vectorEffect:"non-scaling-stroke" } }), - svg.path({ attrs: { transform:"translate(460,300)", d: "M 540.16 71.445 L 511.84 71.445 A 3.161 3.161 0 0 0 511.112 71.524 A 2.113 2.113 0 0 0 510.08 72.085 A 2.62 2.62 0 0 0 509.653 72.663 Q 509.323 73.254 509.12 74.165 Q 508.48 76.405 507.68 78.645 Q 506.88 80.885 506.16 83.125 Q 505.44 85.365 504.8 87.605 Q 504.16 89.845 503.52 91.925 A 16.266 16.266 0 0 0 503.252 93.343 Q 502.719 97.117 504.48 99.285 A 7.072 7.072 0 0 0 506.138 100.721 Q 508.603 102.32 513.44 103.445 A 9.231 9.231 0 0 1 514.913 103.87 Q 517.28 104.795 517.28 106.805 A 2.25 2.25 0 0 1 517.172 107.524 A 1.797 1.797 0 0 1 516.4 108.485 Q 515.615 108.985 514 109.039 A 12.054 12.054 0 0 1 513.6 109.045 Q 508.96 109.045 504.96 108.805 A 128.917 128.917 0 0 0 500.76 108.625 A 168.958 168.958 0 0 0 496.16 108.565 A 86.201 86.201 0 0 0 489.508 108.813 A 74.292 74.292 0 0 0 487.04 109.045 Q 482.72 109.525 477.76 109.525 A 9.189 9.189 0 0 1 476.471 109.44 Q 475.532 109.307 474.8 108.965 Q 473.804 108.5 473.635 107.429 A 2.973 2.973 0 0 1 473.6 106.965 Q 473.6 105.365 474.64 104.645 A 6.568 6.568 0 0 1 475.583 104.11 Q 476.382 103.734 477.44 103.445 A 23.679 23.679 0 0 0 483.013 101.145 A 20.837 20.837 0 0 0 485.52 99.445 Q 488.96 96.725 490.72 92.725 Q 494.72 83.765 498.24 75.045 Q 501.76 66.325 505.12 57.765 Q 508.48 49.205 511.84 40.325 Q 515.2 31.445 518.56 22.325 Q 520.37 17.38 521.179 14.48 A 29.609 29.609 0 0 0 521.6 12.805 Q 522.214 9.968 522.533 9.409 A 0.757 0.757 0 0 1 522.56 9.365 Q 525.44 8.565 528 7.925 A 41.102 41.102 0 0 0 530.982 7.053 A 52.081 52.081 0 0 0 533.44 6.165 Q 536.32 5.045 538.32 3.285 Q 539.852 1.938 540.773 1.059 A 29.826 29.826 0 0 0 541.28 0.565 Q 541.92 0.085 542.56 0.005 A 0.687 0.687 0 0 1 542.646 0 Q 543.149 0 543.653 0.743 A 4.663 4.663 0 0 1 543.84 1.045 Q 544.32 2.005 544.72 3.045 A 17.874 17.874 0 0 1 545.145 4.291 A 23.023 23.023 0 0 1 545.44 5.365 Q 548.8 16.085 552.32 27.205 Q 555.84 38.325 559.44 49.365 Q 563.04 60.405 566.56 71.205 Q 570.08 82.005 573.44 92.245 Q 574.4 95.445 576.4 97.685 Q 578.4 99.925 580.96 101.205 A 32.783 32.783 0 0 0 584.226 102.613 A 41.388 41.388 0 0 0 586.72 103.445 A 13.072 13.072 0 0 1 587.892 103.821 Q 588.463 104.037 588.93 104.287 A 6.08 6.08 0 0 1 589.52 104.645 A 2.343 2.343 0 0 1 590.532 106.352 A 3.539 3.539 0 0 1 590.56 106.805 Q 590.56 107.925 589.6 108.565 A 3.817 3.817 0 0 1 587.81 109.185 A 4.8 4.8 0 0 1 587.36 109.205 Q 584.198 109.205 579.552 108.971 A 271.839 271.839 0 0 1 579.44 108.965 Q 574.72 108.725 569.92 108.565 Q 565.795 108.428 562.497 108.408 A 180.368 180.368 0 0 0 561.44 108.405 Q 558.4 108.405 554 108.565 Q 549.6 108.725 545.12 108.885 Q 540.64 109.045 537.44 109.045 A 6.221 6.221 0 0 1 536.257 108.938 A 4.536 4.536 0 0 1 534.96 108.485 Q 533.958 107.946 533.922 106.886 A 2.348 2.348 0 0 1 533.92 106.805 A 2.958 2.958 0 0 1 534.091 105.778 A 2.56 2.56 0 0 1 535.04 104.565 Q 536.16 103.765 537.76 103.445 A 40.685 40.685 0 0 0 540.837 102.662 Q 544.445 101.58 546.4 100.085 Q 548.799 98.251 548.336 94.8 A 10.393 10.393 0 0 0 548.16 93.845 Q 547.52 90.805 546.56 87.445 Q 545.6 84.085 544.72 80.645 A 170.044 170.044 0 0 0 543.119 74.809 A 155.128 155.128 0 0 0 542.88 74.005 Q 542.568 72.755 542.102 72.115 A 2.548 2.548 0 0 0 542.08 72.085 A 1.317 1.317 0 0 0 541.562 71.685 Q 541.123 71.483 540.43 71.451 A 5.877 5.877 0 0 0 540.16 71.445 Z M 516.32 62.805 L 537.12 62.805 A 7.767 7.767 0 0 0 537.905 62.769 Q 538.69 62.689 539.094 62.431 A 0.844 0.844 0 0 0 539.52 61.685 Q 539.52 61.329 539.476 61.06 A 2.187 2.187 0 0 0 539.44 60.885 A 7.273 7.273 0 0 0 539.376 60.646 Q 539.341 60.523 539.297 60.383 A 16.936 16.936 0 0 0 539.2 60.085 L 528.48 23.925 Q 528 22.165 527.52 22.165 Q 527.232 22.165 526.554 23.594 A 22.258 22.258 0 0 0 526.4 23.925 L 513.76 60.085 A 3.082 3.082 0 0 0 513.502 60.805 A 2.614 2.614 0 0 0 513.44 61.365 A 1.102 1.102 0 0 0 513.584 61.934 Q 513.717 62.159 513.971 62.318 A 1.85 1.85 0 0 0 514.32 62.485 A 5.205 5.205 0 0 0 515.418 62.748 A 6.869 6.869 0 0 0 516.32 62.805 Z", id:"4", vectorEffect:"non-scaling-stroke" } }), - svg.path({ attrs: { transform:"translate(460,300)", d: "M 624.48 110.965 Q 617.6 110.965 611.2 109.365 Q 604.8 107.765 599.2 104.245 Q 598.24 103.445 597.28 102.485 A 7.09 7.09 0 0 1 596.273 101.231 A 6.069 6.069 0 0 1 595.84 100.405 Q 594.24 96.085 593.68 89.845 A 109.626 109.626 0 0 1 593.245 80.042 A 113.493 113.493 0 0 1 593.28 77.205 Q 593.28 76.15 594.133 75.663 A 2.198 2.198 0 0 1 594.24 75.605 Q 595.2 75.125 596.48 75.125 Q 598.08 75.125 598.64 75.525 Q 599.2 75.925 599.84 77.045 A 56.914 56.914 0 0 0 603.579 86.657 A 40.784 40.784 0 0 0 610.32 96.405 A 24.816 24.816 0 0 0 616.606 101.305 A 20.744 20.744 0 0 0 626.56 103.765 Q 631.68 103.765 634.96 101.445 Q 638.24 99.125 640 95.525 A 17.225 17.225 0 0 0 641.55 90.78 A 15.4 15.4 0 0 0 641.76 88.245 A 21.789 21.789 0 0 0 641.43 84.349 Q 641.012 82.048 640.063 80.17 A 13.127 13.127 0 0 0 639.76 79.605 A 18.002 18.002 0 0 0 636.877 75.848 A 24.514 24.514 0 0 0 633.76 73.205 Q 631.01 71.225 627.202 69.018 A 135.313 135.313 0 0 0 623.52 66.965 Q 616.8 63.125 610.56 58.885 Q 604.32 54.645 600.32 48.565 Q 596.428 42.649 596.323 33.853 A 41.035 41.035 0 0 1 596.32 33.365 Q 596.32 23.925 601.2 17.045 Q 606.08 10.165 614.56 6.325 A 43.184 43.184 0 0 1 627.747 2.798 A 53.582 53.582 0 0 1 633.6 2.485 Q 639.68 2.485 645.2 3.765 Q 650.72 5.045 654.08 6.325 Q 656 7.125 656.88 8.085 A 4.088 4.088 0 0 1 657.401 8.815 Q 657.958 9.786 658.4 11.445 A 51.999 51.999 0 0 1 658.967 14.168 Q 659.244 15.691 659.485 17.434 A 103.878 103.878 0 0 1 659.76 19.605 Q 660.32 24.405 660.32 30.165 Q 660.32 32.245 657.44 32.245 Q 656 32.245 654.8 31.525 Q 653.6 30.805 653.28 29.685 A 43.999 43.999 0 0 0 651.108 23.634 Q 649.65 20.391 647.772 17.91 A 22.151 22.151 0 0 0 645.28 15.125 A 18.127 18.127 0 0 0 632.798 10.17 A 23.441 23.441 0 0 0 632.32 10.165 A 24.049 24.049 0 0 0 628.658 10.429 Q 626.644 10.74 624.959 11.416 A 13.834 13.834 0 0 0 624.16 11.765 Q 620.8 13.365 619.12 16.565 A 13.649 13.649 0 0 0 617.914 19.917 Q 617.44 22.015 617.44 24.565 A 10.869 10.869 0 0 0 619.444 30.83 A 13.95 13.95 0 0 0 620.08 31.685 Q 622.72 34.965 627.68 38.325 Q 632.562 41.632 638.838 45.404 A 336.973 336.973 0 0 0 639.04 45.525 Q 645.655 49.445 650.207 53.272 A 49.549 49.549 0 0 1 652.8 55.605 A 36.832 36.832 0 0 1 657.162 60.623 A 28.158 28.158 0 0 1 660.24 66.085 A 29.077 29.077 0 0 1 662.163 73.131 A 38.837 38.837 0 0 1 662.56 78.805 A 41.422 41.422 0 0 1 661.84 86.744 Q 660.892 91.599 658.696 95.481 A 25.101 25.101 0 0 1 657.84 96.885 A 27.512 27.512 0 0 1 645.745 107.023 A 33.765 33.765 0 0 1 644.56 107.525 A 45.856 45.856 0 0 1 634.709 110.197 Q 630.224 110.917 625.158 110.962 A 76.284 76.284 0 0 1 624.48 110.965 Z", id:"5", vectorEffect:"non-scaling-stroke" } }), - svg.path({ attrs: { transform:"translate(460,300)", d: "M 703.52 110.965 Q 696.64 110.965 690.24 109.365 Q 683.84 107.765 678.24 104.245 Q 677.28 103.445 676.32 102.485 A 7.09 7.09 0 0 1 675.313 101.231 A 6.069 6.069 0 0 1 674.88 100.405 Q 673.28 96.085 672.72 89.845 A 109.626 109.626 0 0 1 672.285 80.042 A 113.493 113.493 0 0 1 672.32 77.205 Q 672.32 76.15 673.173 75.663 A 2.198 2.198 0 0 1 673.28 75.605 Q 674.24 75.125 675.52 75.125 Q 677.12 75.125 677.68 75.525 Q 678.24 75.925 678.88 77.045 A 56.914 56.914 0 0 0 682.619 86.657 A 40.784 40.784 0 0 0 689.36 96.405 A 24.816 24.816 0 0 0 695.646 101.305 A 20.744 20.744 0 0 0 705.6 103.765 Q 710.72 103.765 714 101.445 Q 717.28 99.125 719.04 95.525 A 17.225 17.225 0 0 0 720.59 90.78 A 15.4 15.4 0 0 0 720.8 88.245 A 21.789 21.789 0 0 0 720.47 84.349 Q 720.052 82.048 719.103 80.17 A 13.127 13.127 0 0 0 718.8 79.605 A 18.002 18.002 0 0 0 715.917 75.848 A 24.514 24.514 0 0 0 712.8 73.205 Q 710.05 71.225 706.242 69.018 A 135.313 135.313 0 0 0 702.56 66.965 Q 695.84 63.125 689.6 58.885 Q 683.36 54.645 679.36 48.565 Q 675.468 42.649 675.363 33.853 A 41.035 41.035 0 0 1 675.36 33.365 Q 675.36 23.925 680.24 17.045 Q 685.12 10.165 693.6 6.325 A 43.184 43.184 0 0 1 706.787 2.798 A 53.582 53.582 0 0 1 712.64 2.485 Q 718.72 2.485 724.24 3.765 Q 729.76 5.045 733.12 6.325 Q 735.04 7.125 735.92 8.085 A 4.088 4.088 0 0 1 736.441 8.815 Q 736.998 9.786 737.44 11.445 A 51.999 51.999 0 0 1 738.007 14.168 Q 738.284 15.691 738.525 17.434 A 103.878 103.878 0 0 1 738.8 19.605 Q 739.36 24.405 739.36 30.165 Q 739.36 32.245 736.48 32.245 Q 735.04 32.245 733.84 31.525 Q 732.64 30.805 732.32 29.685 A 43.999 43.999 0 0 0 730.148 23.634 Q 728.69 20.391 726.812 17.91 A 22.151 22.151 0 0 0 724.32 15.125 A 18.127 18.127 0 0 0 711.838 10.17 A 23.441 23.441 0 0 0 711.36 10.165 A 24.049 24.049 0 0 0 707.698 10.429 Q 705.684 10.74 703.999 11.416 A 13.834 13.834 0 0 0 703.2 11.765 Q 699.84 13.365 698.16 16.565 A 13.649 13.649 0 0 0 696.954 19.917 Q 696.48 22.015 696.48 24.565 A 10.869 10.869 0 0 0 698.484 30.83 A 13.95 13.95 0 0 0 699.12 31.685 Q 701.76 34.965 706.72 38.325 Q 711.602 41.632 717.878 45.404 A 336.973 336.973 0 0 0 718.08 45.525 Q 724.695 49.445 729.247 53.272 A 49.549 49.549 0 0 1 731.84 55.605 A 36.832 36.832 0 0 1 736.202 60.623 A 28.158 28.158 0 0 1 739.28 66.085 A 29.077 29.077 0 0 1 741.203 73.131 A 38.837 38.837 0 0 1 741.6 78.805 A 41.422 41.422 0 0 1 740.88 86.744 Q 739.932 91.599 737.736 95.481 A 25.101 25.101 0 0 1 736.88 96.885 A 27.512 27.512 0 0 1 724.785 107.023 A 33.765 33.765 0 0 1 723.6 107.525 A 45.856 45.856 0 0 1 713.749 110.197 Q 709.264 110.917 704.198 110.962 A 76.284 76.284 0 0 1 703.52 110.965 Z", id:"6", vectorEffect:"non-scaling-stroke" } }), - svg.rect({ attrs: { x: "466", y: "240.5", width: "800", height: "15.5" } }), - // svg.text({ attrs: { 'font-family': "Calibri", 'font-size': "40", 'font-weight': "bold", x: "497", y: "450" } }, 'COMPUTATIONAL SCIENCES'), - // svg.path({ attrs: { d: "M 1111.1316 329.63885 C 1111.7629 340.6366 1119.8291 350.53146 1131.0461 350.7098 C 1142.2632 350.8882 1154.6805 342.4446 1153.2188 331.05904 C 1151.7572 319.67348 1142.7102 319.24054 1132.1177 316.17612 C 1121.5253 313.11165 1130.3999 308.0055 1130.3999 308.0055 C 1130.3999 308.0055 1134.739 305.7016 1139.3956 304.96687 C 1144.0522 304.23215 1147.434 306.17886 1149.1073 306.1567 C 1150.7806 306.13453 1150.2077 300.1209 1149.0792 298.99964 C 1147.9508 297.87838 1138.8401 295.20424 1130.1026 299.3452 C 1121.365 303.48615 1118.0291 308.5461 1118.0291 308.5461 C 1118.0291 308.5461 1110.5002 318.6411 1111.1316 329.63885 Z" } }), - // svg.path({ attrs: { d: "M 1123.1925 334.362 C 1124.9592 338.72802 1129.5795 341.5972 1134.138 340.08526 C 1138.6965 338.57332 1142.5552 333.3991 1140.3996 328.99328 C 1138.244 324.58742 1134.0469 325.6506 1129.3442 325.9052 C 1124.6415 326.1598 1122.6041 322.6 1122.6041 322.6 L 1122.3587 325.91298 C 1122.3587 325.91298 1121.4258 329.996 1123.1925 334.362 Z", fill: "white" } }), - // svg.ellipse({ attrs: { cx: "1130.6581", cy: "328.0728", rx: "5.658064", ry: "6.727217" } }), - // svg.ellipse({ attrs: { cx: "1130.6581", cy: "329.1", rx: "2.554036", ry: "2.1000034", fill: "white" } }), - - ]) -]) - - -const homeSVG = svg({ attrs: { 'vertical-align': 'baseline', height: '30pt', width: '30pt', viewBox: '1020 -226 972 972' } }, [ - svg.a({ attrs: { 'xlink:href': "/target" } }, [ - // TARGET - svg.path({ - attrs: { - id: "target", - d: "M 1506.0454 -136.98166 L 1506.0454 -225 C 1420.9163 -225 1337.2871 -202.5916 1263.5632 -160.02709 C 1031.6083 -26.107868 952.1347 270.4917 1086.0539 502.4466 L 1162.28 458.43743 C 1052.6664 268.58106 1117.716 25.81266 1307.5724 -83.80097 C 1367.9158 -118.64026 1436.3668 -136.98165 1506.0454 -136.98166 Z", - fill: "#f44335" - } - }), - svg.text([ - svg.textPath({ attrs: { 'xlink:href': "#target", startOffset: "80%" } }, [ - svg.tspan({ attrs: { 'font-family': '"Roboto", Arial, Helvetica, sans-serif', 'font-size': "60", 'text-anchor': "middle", 'letter-spacing': 15, 'font-weight': "bold", fill: "#ebebeb", dy: "-20" } }, "TARGET") - ]) - ]), - svg.path({ - attrs: { - d: "M 1506.0454 -136.98166 L 1506.0454 -225 C 1420.9163 -225 1337.2871 -202.5916 1263.5632 -160.02709 C 1031.6083 -26.107868 952.1347 270.4917 1086.0539 502.4466 L 1162.28 458.43743 C 1052.6664 268.58106 1117.716 25.81266 1307.5724 -83.80097 C 1367.9158 -118.64026 1436.3668 -136.98165 1506.0454 -136.98166 Z", - stroke: "white", - 'fill-opacity': '0', - 'stroke-linecap': "round", - 'stroke-linejoin': "round", - 'stroke-width': "6" - } - }) + svg.path({ attrs: { d: "M 389 256 L 389 26 L 342 201 Z" } }), + svg.path({ attrs: { d: "M 389 256 L 159 256 L 334 303 Z" } }), + svg.path({ attrs: { d: "M 389 256 L 389 486 L 436 311 Z" } }), + svg.path({ attrs: { d: "M 389 256 L 619 256 L 444 209 Z" } }), + //svg.text({ attrs: { 'font-family': 'Garamond, serif', 'font-size': "160", 'font-weight': "bold", x: "450", y: "398" } }, 'COMPASS'), + svg.path({ + attrs: { + transform: "translate(460,300)", + d: "M 42.517 108.894 A 73.307 73.307 0 0 0 60.16 110.965 A 111.506 111.506 0 0 0 67.956 110.701 A 87.869 87.869 0 0 0 78.4 109.365 A 102.036 102.036 0 0 0 79.766 109.093 Q 87.225 107.553 92.8 105.205 A 11.877 11.877 0 0 0 94.135 104.431 A 8.802 8.802 0 0 0 95.44 103.365 A 5.185 5.185 0 0 0 95.872 102.873 Q 96.619 101.909 97.12 100.405 Q 98.24 96.885 99.04 93.525 A 74.101 74.101 0 0 0 99.604 90.961 A 59.633 59.633 0 0 0 100.24 87.205 A 55.183 55.183 0 0 0 100.451 85.441 Q 100.64 83.595 100.64 82.165 A 5.632 5.632 0 0 0 100.64 82.147 Q 100.634 80.24 99.332 79.674 A 3.055 3.055 0 0 0 98.32 79.445 Q 96.55 79.323 95.385 80.972 A 6.972 6.972 0 0 0 94.72 82.165 A 56.752 56.752 0 0 1 93.8 84.01 Q 91.762 87.912 89.352 90.983 A 31.476 31.476 0 0 1 82.4 97.605 Q 77.685 100.834 71.132 101.994 A 45.53 45.53 0 0 1 63.2 102.645 Q 54.08 102.645 47.36 98.965 Q 40.64 95.285 36 88.725 Q 31.36 82.165 29.12 73.125 A 72.598 72.598 0 0 1 27.744 66.056 A 87.496 87.496 0 0 1 26.88 53.525 Q 26.88 45.205 29.44 37.525 Q 32 29.845 36.72 23.765 Q 41.44 17.685 48.08 14.165 A 29.342 29.342 0 0 1 53.049 12.09 A 32.658 32.658 0 0 1 62.88 10.645 A 58.028 58.028 0 0 1 64.994 10.683 Q 71.646 10.926 76.56 12.725 A 36.033 36.033 0 0 1 78.031 13.3 Q 82.667 15.232 85.44 18.005 Q 88.48 20.725 90 24.405 Q 91.52 28.085 92.64 31.765 A 6.448 6.448 0 0 0 92.647 31.785 Q 92.855 32.403 93.155 32.879 A 3.239 3.239 0 0 0 94.8 34.245 Q 95.51 34.507 96.167 34.507 A 3.165 3.165 0 0 0 97.6 34.165 A 2.181 2.181 0 0 0 98.779 32.667 A 3.73 3.73 0 0 0 98.88 31.765 Q 98.88 29.734 98.798 27.455 A 165.242 165.242 0 0 0 98.56 22.805 Q 98.24 17.845 97.6 12.565 Q 97.528 11.695 97.275 11.006 A 3.802 3.802 0 0 0 96.4 9.605 A 7.672 7.672 0 0 0 95.926 9.169 A 8.327 8.327 0 0 0 93.92 7.925 Q 89.92 6.485 85.04 5.205 Q 80.16 3.925 74.96 3.205 Q 69.76 2.485 64.64 2.485 Q 49.6 2.485 37.68 7.045 A 73.108 73.108 0 0 0 32.282 9.359 A 60.996 60.996 0 0 0 17.28 19.365 Q 8.8 27.125 4.4 37.125 Q 0 47.125 0 58.165 A 57.845 57.845 0 0 0 0.137 62.174 A 48.394 48.394 0 0 0 4.64 79.765 Q 9.28 89.525 17.6 96.485 Q 25.92 103.445 36.8 107.205 A 65.874 65.874 0 0 0 42.517 108.894 Z", + id: "0", + vectorEffect: "non-scaling-stroke", + }, + }), + svg.path({ + attrs: { + transform: "translate(460,300)", + d: "M 168.8 110.965 Q 157.76 110.965 147.76 107.205 Q 137.76 103.445 130 96.485 Q 122.24 89.525 117.84 79.845 A 49.402 49.402 0 0 1 113.682 63.757 A 59.923 59.923 0 0 1 113.44 58.325 Q 113.44 47.445 117.04 37.445 Q 120.64 27.445 127.84 19.605 A 50.961 50.961 0 0 1 142.36 8.694 A 60.247 60.247 0 0 1 145.68 7.125 A 54.565 54.565 0 0 1 159.472 3.251 A 72.704 72.704 0 0 1 170.24 2.485 Q 181.44 2.485 191.44 6.245 Q 201.44 10.005 209.12 16.965 Q 216.8 23.925 221.28 33.605 Q 225.76 43.285 225.76 55.125 Q 225.76 65.845 222.08 75.925 Q 218.4 86.005 211.2 93.845 A 50.961 50.961 0 0 1 196.68 104.757 A 60.247 60.247 0 0 1 193.36 106.325 A 54.565 54.565 0 0 1 179.569 110.2 A 72.704 72.704 0 0 1 168.8 110.965 Z M 171.68 103.605 A 26.844 26.844 0 0 0 179.094 102.625 A 21.365 21.365 0 0 0 186.96 98.485 A 27.514 27.514 0 0 0 193.442 90.523 A 37.424 37.424 0 0 0 196.16 84.245 Q 199.2 75.125 199.2 63.285 Q 199.2 53.525 197.36 44.005 Q 195.52 34.485 191.6 26.805 Q 187.68 19.125 181.6 14.565 Q 175.52 10.005 167.2 10.005 A 26.016 26.016 0 0 0 159.655 11.057 A 21.4 21.4 0 0 0 152.08 15.125 A 27.98 27.98 0 0 0 145.339 23.499 A 37.303 37.303 0 0 0 142.88 29.205 Q 139.932 37.894 139.843 49.291 A 91.218 91.218 0 0 0 139.84 50.005 Q 139.84 59.925 141.76 69.445 A 73.734 73.734 0 0 0 144.886 80.561 A 61.444 61.444 0 0 0 147.52 86.645 Q 151.36 94.325 157.36 98.965 A 22.185 22.185 0 0 0 169.511 103.523 A 28.279 28.279 0 0 0 171.68 103.605 Z", + id: "1", + vectorEffect: "non-scaling-stroke", + }, + }), + svg.path({ + attrs: { + transform: "translate(460,300)", + d: "M 280 18.965 L 303.52 75.125 A 1.737 1.737 0 0 0 303.731 75.456 Q 303.908 75.669 304.133 75.769 A 1.008 1.008 0 0 0 304.4 75.845 A 0.705 0.705 0 0 0 304.5 75.853 Q 304.88 75.853 305.14 75.414 A 2.079 2.079 0 0 0 305.28 75.125 L 328.16 22.005 Q 329.162 16.996 329.844 13.651 A 1135.171 1135.171 0 0 1 330 12.885 A 101.259 101.259 0 0 1 330.442 10.826 Q 330.92 8.715 331.36 7.285 A 3.896 3.896 0 0 1 331.783 6.339 Q 332.447 5.294 333.703 5.212 A 3.35 3.35 0 0 1 333.92 5.205 L 340.8 5.205 Q 347.52 4.885 354.8 4.165 Q 360.801 3.572 364.681 3.468 A 58.182 58.182 0 0 1 366.24 3.445 A 7.727 7.727 0 0 1 367.651 3.568 A 5.754 5.754 0 0 1 369.04 4.005 A 2.337 2.337 0 0 1 369.686 4.438 Q 370.18 4.917 370.234 5.659 A 2.568 2.568 0 0 1 370.24 5.845 A 3.116 3.116 0 0 1 370.139 6.667 A 2.064 2.064 0 0 1 369.2 7.925 Q 368.16 8.565 366.56 9.045 Q 365.825 9.29 364.903 9.676 A 38.566 38.566 0 0 0 364.32 9.925 A 60.68 60.68 0 0 0 363.262 10.4 Q 362.781 10.622 362.357 10.829 A 34.278 34.278 0 0 0 362.08 10.965 A 24.408 24.408 0 0 0 359.476 12.352 Q 358.179 13.153 357.193 14.035 A 11.367 11.367 0 0 0 355.92 15.365 Q 353.998 17.749 354.219 22.868 A 26.82 26.82 0 0 0 354.24 23.285 Q 354.56 34.805 355.04 43.445 Q 355.52 52.085 355.92 59.445 Q 356.32 66.805 356.88 74.725 Q 357.265 80.17 357.688 86.863 A 2486.888 2486.888 0 0 1 358.08 93.205 Q 358.4 97.365 361.52 99.845 A 17.172 17.172 0 0 0 364.885 101.907 Q 367.04 102.925 369.76 103.605 Q 371.36 104.085 372.48 104.725 A 2.102 2.102 0 0 1 373.55 106.233 A 3.129 3.129 0 0 1 373.6 106.805 Q 373.6 107.925 372.64 108.565 Q 371.68 109.205 370.24 109.205 A 124.521 124.521 0 0 1 366.961 109.159 Q 365.284 109.115 363.422 109.028 A 214.634 214.634 0 0 1 362.16 108.965 Q 357.6 108.725 353.2 108.565 Q 348.8 108.405 345.6 108.405 Q 342.4 108.405 338.48 108.565 Q 334.56 108.725 330.56 108.965 Q 326.56 109.205 322.88 109.205 A 6.326 6.326 0 0 1 321.48 109.056 A 5.104 5.104 0 0 1 320.32 108.645 A 2.421 2.421 0 0 1 319.742 108.252 A 1.688 1.688 0 0 1 319.2 106.965 A 2.958 2.958 0 0 1 319.371 105.938 A 2.56 2.56 0 0 1 320.32 104.725 Q 321.44 103.925 323.04 103.605 A 33.68 33.68 0 0 0 325.926 102.937 Q 328.739 102.142 330.36 101.01 A 6.886 6.886 0 0 0 331.2 100.325 Q 333.291 98.309 333.291 93.854 A 20.13 20.13 0 0 0 333.28 93.205 Q 332.8 83.285 332.48 73.445 Q 332.16 63.605 331.92 54.085 A 657.231 657.231 0 0 0 331.259 36.728 A 600.597 600.597 0 0 0 331.2 35.605 Q 331.2 34.485 330.64 34.565 A 0.762 0.762 0 0 0 330.265 34.743 Q 329.915 35.03 329.6 35.765 L 299.04 108.405 A 5.387 5.387 0 0 1 298.705 109.169 Q 297.956 110.543 296.715 110.339 A 2.202 2.202 0 0 1 296.64 110.325 A 3.756 3.756 0 0 1 295.619 109.974 A 2.786 2.786 0 0 1 294.4 108.725 Q 286.56 90.165 278 72.085 Q 269.44 54.005 262.24 36.245 A 3.156 3.156 0 0 0 262.06 35.806 Q 261.635 34.977 260.906 35.147 A 1.42 1.42 0 0 0 260.72 35.205 A 1.58 1.58 0 0 0 259.891 35.962 Q 259.736 36.247 259.641 36.626 A 4.851 4.851 0 0 0 259.52 37.365 Q 259.04 45.365 258.72 54.885 Q 258.4 64.405 258.4 74.085 L 258.4 92.085 A 11.336 11.336 0 0 0 258.894 95.507 A 9.135 9.135 0 0 0 261.28 99.365 A 12.265 12.265 0 0 0 263.913 101.274 Q 266.48 102.694 270.339 103.591 A 39.254 39.254 0 0 0 270.4 103.605 Q 272 103.925 273.44 104.645 A 3.112 3.112 0 0 1 274.184 105.151 A 2.171 2.171 0 0 1 274.88 106.805 Q 274.88 107.905 273.646 108.542 A 4.015 4.015 0 0 1 273.6 108.565 Q 272.32 109.205 270.88 109.205 Q 266.56 109.205 264.16 108.965 Q 261.76 108.725 259.84 108.645 Q 258.37 108.584 256.056 108.57 A 241.657 241.657 0 0 0 254.56 108.565 Q 251.04 108.565 247.36 108.725 Q 243.68 108.885 240.96 109.045 Q 238.738 109.176 237.798 109.2 A 14.405 14.405 0 0 1 237.44 109.205 Q 235.52 109.205 234.56 108.645 A 2.008 2.008 0 0 1 233.961 108.128 Q 233.652 107.721 233.608 107.159 A 2.45 2.45 0 0 1 233.6 106.965 A 2.295 2.295 0 0 1 233.708 106.238 Q 233.983 105.413 234.96 105.125 A 41.882 41.882 0 0 0 236.227 104.729 Q 236.874 104.516 237.593 104.261 A 74.939 74.939 0 0 0 238.08 104.085 A 14.817 14.817 0 0 0 243.664 100.558 A 17.927 17.927 0 0 0 244.56 99.605 Q 247.36 96.405 248 91.445 Q 248.66 86.385 249.358 79.699 A 808.068 808.068 0 0 0 250 73.285 Q 251.04 62.485 252.16 50.165 Q 253.28 37.845 254.08 25.845 Q 254.197 25.148 254.228 24.197 A 22.532 22.532 0 0 0 254.24 23.445 L 254.24 20.405 A 15.303 15.303 0 0 0 254.146 18.645 Q 254.044 17.77 253.834 17.044 A 6.444 6.444 0 0 0 253.36 15.845 Q 252.48 14.165 250.4 13.045 Q 248.48 11.765 246.24 10.885 Q 244 10.005 241.44 9.365 A 12.798 12.798 0 0 1 240.164 8.979 Q 239.543 8.753 239.037 8.48 A 5.984 5.984 0 0 1 238.4 8.085 Q 237.28 7.285 237.28 6.005 A 2.466 2.466 0 0 1 237.39 5.244 Q 237.655 4.425 238.56 4.085 A 7.122 7.122 0 0 1 239.913 3.733 Q 240.571 3.625 241.32 3.608 A 12.488 12.488 0 0 1 241.6 3.605 Q 244 3.605 247.52 3.765 Q 251.04 3.925 255.28 4.005 Q 259.52 4.085 263.92 4.245 Q 268.32 4.405 272 4.405 A 3.689 3.689 0 0 1 274.011 5.009 A 4.772 4.772 0 0 1 274.4 5.285 A 4.895 4.895 0 0 1 275.175 6.036 A 3.481 3.481 0 0 1 275.84 7.285 Q 276.96 10.325 277.92 13.125 Q 278.88 15.925 280 18.965 Z", + id: "2", + vectorEffect: "non-scaling-stroke", + }, + }), + svg.path({ + attrs: { + transform: "translate(460,300)", + d: "M 393.92 93.045 L 393.92 21.365 A 24.447 24.447 0 0 0 393.773 18.576 Q 393.438 15.671 392.344 13.937 A 6.247 6.247 0 0 0 392 13.445 A 6.811 6.811 0 0 0 390.327 11.947 Q 388.579 10.8 385.67 10.016 A 31.125 31.125 0 0 0 384.32 9.685 Q 382.496 9.264 381.656 8.536 A 2.551 2.551 0 0 1 381.44 8.325 A 3.112 3.112 0 0 1 380.834 7.37 A 2.829 2.829 0 0 1 380.64 6.325 Q 380.64 5.365 381.6 4.645 Q 382.444 4.013 383.782 3.936 A 6.615 6.615 0 0 1 384.16 3.925 Q 387.84 3.925 391.6 4.085 Q 395.36 4.245 398.96 4.485 A 109.621 109.621 0 0 0 403.304 4.692 A 91.493 91.493 0 0 0 405.76 4.725 A 63.208 63.208 0 0 0 408.952 4.649 Q 410.895 4.55 412.56 4.325 Q 415.52 3.925 419.12 3.525 A 57.801 57.801 0 0 1 422.034 3.282 Q 424.64 3.125 427.84 3.125 A 74.922 74.922 0 0 1 440.24 4.088 Q 446.789 5.189 452.105 7.548 A 37.317 37.317 0 0 1 458.8 11.365 A 27.236 27.236 0 0 1 467.187 20.771 Q 469.954 26.023 470.213 32.625 A 35.159 35.159 0 0 1 470.24 34.005 A 41.752 41.752 0 0 1 467.704 48.641 A 39.421 39.421 0 0 1 466 52.565 Q 461.76 61.045 453.2 66.245 Q 444.64 71.445 431.52 71.445 A 21.718 21.718 0 0 1 429.227 71.333 Q 426.719 71.065 425.2 70.165 Q 423.04 68.885 423.04 67.125 A 2.072 2.072 0 0 1 423.781 65.52 A 3.105 3.105 0 0 1 424.08 65.285 Q 424.729 64.836 425.284 64.667 A 2.186 2.186 0 0 1 425.92 64.565 A 13.015 13.015 0 0 1 427.32 64.637 Q 428.22 64.734 428.96 64.965 Q 430.24 65.365 432.48 65.365 A 9.839 9.839 0 0 0 440.581 61.31 A 17.306 17.306 0 0 0 442.64 57.845 A 33.299 33.299 0 0 0 444.803 51.238 Q 445.521 48.018 445.836 44.233 A 75.351 75.351 0 0 0 446.08 38.005 A 71.948 71.948 0 0 0 445.742 30.761 Q 444.99 23.366 442.573 18.803 A 16.941 16.941 0 0 0 441.28 16.725 A 15.331 15.331 0 0 0 430.194 10.295 A 20.798 20.798 0 0 0 427.84 10.165 Q 424.873 10.165 422.853 11.043 A 7.065 7.065 0 0 0 421.04 12.165 Q 418.982 13.94 418.75 17.414 A 13.668 13.668 0 0 0 418.72 18.325 L 418.56 92.725 A 15.111 15.111 0 0 0 418.774 95.363 Q 419.023 96.762 419.56 97.846 A 6.266 6.266 0 0 0 421.28 100.005 A 11.172 11.172 0 0 0 423.248 101.224 Q 425.345 102.269 428.593 103.106 A 53.266 53.266 0 0 0 430.72 103.605 A 9.395 9.395 0 0 1 431.734 103.844 Q 432.229 103.993 432.626 104.185 A 3.715 3.715 0 0 1 433.36 104.645 Q 434.221 105.35 434.24 106.744 A 4.638 4.638 0 0 1 434.24 106.805 A 2.515 2.515 0 0 1 434.121 107.6 A 2.037 2.037 0 0 1 433.44 108.565 A 2.681 2.681 0 0 1 432.532 109.03 Q 432.153 109.145 431.698 109.185 A 5.749 5.749 0 0 1 431.2 109.205 A 124.521 124.521 0 0 1 427.921 109.159 Q 426.244 109.115 424.382 109.028 A 214.634 214.634 0 0 1 423.12 108.965 Q 418.56 108.725 414.08 108.565 Q 409.6 108.405 406.4 108.405 Q 403.04 108.405 399.04 108.565 A 495.61 495.61 0 0 0 391.329 108.936 A 541.268 541.268 0 0 0 390.8 108.965 Q 386.56 109.205 383.04 109.205 A 4.947 4.947 0 0 1 381.858 109.07 A 3.961 3.961 0 0 1 380.8 108.645 A 2.008 2.008 0 0 1 380.201 108.128 Q 379.892 107.721 379.848 107.159 A 2.45 2.45 0 0 1 379.84 106.965 A 2.907 2.907 0 0 1 381.219 104.408 Q 381.748 104.056 382.48 103.81 A 8.312 8.312 0 0 1 383.2 103.605 Q 389.28 102.325 391.6 100.325 Q 393.778 98.448 393.912 93.681 A 22.691 22.691 0 0 0 393.92 93.045 Z", + id: "3", + vectorEffect: "non-scaling-stroke", + }, + }), + svg.path({ + attrs: { + transform: "translate(460,300)", + d: "M 540.16 71.445 L 511.84 71.445 A 3.161 3.161 0 0 0 511.112 71.524 A 2.113 2.113 0 0 0 510.08 72.085 A 2.62 2.62 0 0 0 509.653 72.663 Q 509.323 73.254 509.12 74.165 Q 508.48 76.405 507.68 78.645 Q 506.88 80.885 506.16 83.125 Q 505.44 85.365 504.8 87.605 Q 504.16 89.845 503.52 91.925 A 16.266 16.266 0 0 0 503.252 93.343 Q 502.719 97.117 504.48 99.285 A 7.072 7.072 0 0 0 506.138 100.721 Q 508.603 102.32 513.44 103.445 A 9.231 9.231 0 0 1 514.913 103.87 Q 517.28 104.795 517.28 106.805 A 2.25 2.25 0 0 1 517.172 107.524 A 1.797 1.797 0 0 1 516.4 108.485 Q 515.615 108.985 514 109.039 A 12.054 12.054 0 0 1 513.6 109.045 Q 508.96 109.045 504.96 108.805 A 128.917 128.917 0 0 0 500.76 108.625 A 168.958 168.958 0 0 0 496.16 108.565 A 86.201 86.201 0 0 0 489.508 108.813 A 74.292 74.292 0 0 0 487.04 109.045 Q 482.72 109.525 477.76 109.525 A 9.189 9.189 0 0 1 476.471 109.44 Q 475.532 109.307 474.8 108.965 Q 473.804 108.5 473.635 107.429 A 2.973 2.973 0 0 1 473.6 106.965 Q 473.6 105.365 474.64 104.645 A 6.568 6.568 0 0 1 475.583 104.11 Q 476.382 103.734 477.44 103.445 A 23.679 23.679 0 0 0 483.013 101.145 A 20.837 20.837 0 0 0 485.52 99.445 Q 488.96 96.725 490.72 92.725 Q 494.72 83.765 498.24 75.045 Q 501.76 66.325 505.12 57.765 Q 508.48 49.205 511.84 40.325 Q 515.2 31.445 518.56 22.325 Q 520.37 17.38 521.179 14.48 A 29.609 29.609 0 0 0 521.6 12.805 Q 522.214 9.968 522.533 9.409 A 0.757 0.757 0 0 1 522.56 9.365 Q 525.44 8.565 528 7.925 A 41.102 41.102 0 0 0 530.982 7.053 A 52.081 52.081 0 0 0 533.44 6.165 Q 536.32 5.045 538.32 3.285 Q 539.852 1.938 540.773 1.059 A 29.826 29.826 0 0 0 541.28 0.565 Q 541.92 0.085 542.56 0.005 A 0.687 0.687 0 0 1 542.646 0 Q 543.149 0 543.653 0.743 A 4.663 4.663 0 0 1 543.84 1.045 Q 544.32 2.005 544.72 3.045 A 17.874 17.874 0 0 1 545.145 4.291 A 23.023 23.023 0 0 1 545.44 5.365 Q 548.8 16.085 552.32 27.205 Q 555.84 38.325 559.44 49.365 Q 563.04 60.405 566.56 71.205 Q 570.08 82.005 573.44 92.245 Q 574.4 95.445 576.4 97.685 Q 578.4 99.925 580.96 101.205 A 32.783 32.783 0 0 0 584.226 102.613 A 41.388 41.388 0 0 0 586.72 103.445 A 13.072 13.072 0 0 1 587.892 103.821 Q 588.463 104.037 588.93 104.287 A 6.08 6.08 0 0 1 589.52 104.645 A 2.343 2.343 0 0 1 590.532 106.352 A 3.539 3.539 0 0 1 590.56 106.805 Q 590.56 107.925 589.6 108.565 A 3.817 3.817 0 0 1 587.81 109.185 A 4.8 4.8 0 0 1 587.36 109.205 Q 584.198 109.205 579.552 108.971 A 271.839 271.839 0 0 1 579.44 108.965 Q 574.72 108.725 569.92 108.565 Q 565.795 108.428 562.497 108.408 A 180.368 180.368 0 0 0 561.44 108.405 Q 558.4 108.405 554 108.565 Q 549.6 108.725 545.12 108.885 Q 540.64 109.045 537.44 109.045 A 6.221 6.221 0 0 1 536.257 108.938 A 4.536 4.536 0 0 1 534.96 108.485 Q 533.958 107.946 533.922 106.886 A 2.348 2.348 0 0 1 533.92 106.805 A 2.958 2.958 0 0 1 534.091 105.778 A 2.56 2.56 0 0 1 535.04 104.565 Q 536.16 103.765 537.76 103.445 A 40.685 40.685 0 0 0 540.837 102.662 Q 544.445 101.58 546.4 100.085 Q 548.799 98.251 548.336 94.8 A 10.393 10.393 0 0 0 548.16 93.845 Q 547.52 90.805 546.56 87.445 Q 545.6 84.085 544.72 80.645 A 170.044 170.044 0 0 0 543.119 74.809 A 155.128 155.128 0 0 0 542.88 74.005 Q 542.568 72.755 542.102 72.115 A 2.548 2.548 0 0 0 542.08 72.085 A 1.317 1.317 0 0 0 541.562 71.685 Q 541.123 71.483 540.43 71.451 A 5.877 5.877 0 0 0 540.16 71.445 Z M 516.32 62.805 L 537.12 62.805 A 7.767 7.767 0 0 0 537.905 62.769 Q 538.69 62.689 539.094 62.431 A 0.844 0.844 0 0 0 539.52 61.685 Q 539.52 61.329 539.476 61.06 A 2.187 2.187 0 0 0 539.44 60.885 A 7.273 7.273 0 0 0 539.376 60.646 Q 539.341 60.523 539.297 60.383 A 16.936 16.936 0 0 0 539.2 60.085 L 528.48 23.925 Q 528 22.165 527.52 22.165 Q 527.232 22.165 526.554 23.594 A 22.258 22.258 0 0 0 526.4 23.925 L 513.76 60.085 A 3.082 3.082 0 0 0 513.502 60.805 A 2.614 2.614 0 0 0 513.44 61.365 A 1.102 1.102 0 0 0 513.584 61.934 Q 513.717 62.159 513.971 62.318 A 1.85 1.85 0 0 0 514.32 62.485 A 5.205 5.205 0 0 0 515.418 62.748 A 6.869 6.869 0 0 0 516.32 62.805 Z", + id: "4", + vectorEffect: "non-scaling-stroke", + }, + }), + svg.path({ + attrs: { + transform: "translate(460,300)", + d: "M 624.48 110.965 Q 617.6 110.965 611.2 109.365 Q 604.8 107.765 599.2 104.245 Q 598.24 103.445 597.28 102.485 A 7.09 7.09 0 0 1 596.273 101.231 A 6.069 6.069 0 0 1 595.84 100.405 Q 594.24 96.085 593.68 89.845 A 109.626 109.626 0 0 1 593.245 80.042 A 113.493 113.493 0 0 1 593.28 77.205 Q 593.28 76.15 594.133 75.663 A 2.198 2.198 0 0 1 594.24 75.605 Q 595.2 75.125 596.48 75.125 Q 598.08 75.125 598.64 75.525 Q 599.2 75.925 599.84 77.045 A 56.914 56.914 0 0 0 603.579 86.657 A 40.784 40.784 0 0 0 610.32 96.405 A 24.816 24.816 0 0 0 616.606 101.305 A 20.744 20.744 0 0 0 626.56 103.765 Q 631.68 103.765 634.96 101.445 Q 638.24 99.125 640 95.525 A 17.225 17.225 0 0 0 641.55 90.78 A 15.4 15.4 0 0 0 641.76 88.245 A 21.789 21.789 0 0 0 641.43 84.349 Q 641.012 82.048 640.063 80.17 A 13.127 13.127 0 0 0 639.76 79.605 A 18.002 18.002 0 0 0 636.877 75.848 A 24.514 24.514 0 0 0 633.76 73.205 Q 631.01 71.225 627.202 69.018 A 135.313 135.313 0 0 0 623.52 66.965 Q 616.8 63.125 610.56 58.885 Q 604.32 54.645 600.32 48.565 Q 596.428 42.649 596.323 33.853 A 41.035 41.035 0 0 1 596.32 33.365 Q 596.32 23.925 601.2 17.045 Q 606.08 10.165 614.56 6.325 A 43.184 43.184 0 0 1 627.747 2.798 A 53.582 53.582 0 0 1 633.6 2.485 Q 639.68 2.485 645.2 3.765 Q 650.72 5.045 654.08 6.325 Q 656 7.125 656.88 8.085 A 4.088 4.088 0 0 1 657.401 8.815 Q 657.958 9.786 658.4 11.445 A 51.999 51.999 0 0 1 658.967 14.168 Q 659.244 15.691 659.485 17.434 A 103.878 103.878 0 0 1 659.76 19.605 Q 660.32 24.405 660.32 30.165 Q 660.32 32.245 657.44 32.245 Q 656 32.245 654.8 31.525 Q 653.6 30.805 653.28 29.685 A 43.999 43.999 0 0 0 651.108 23.634 Q 649.65 20.391 647.772 17.91 A 22.151 22.151 0 0 0 645.28 15.125 A 18.127 18.127 0 0 0 632.798 10.17 A 23.441 23.441 0 0 0 632.32 10.165 A 24.049 24.049 0 0 0 628.658 10.429 Q 626.644 10.74 624.959 11.416 A 13.834 13.834 0 0 0 624.16 11.765 Q 620.8 13.365 619.12 16.565 A 13.649 13.649 0 0 0 617.914 19.917 Q 617.44 22.015 617.44 24.565 A 10.869 10.869 0 0 0 619.444 30.83 A 13.95 13.95 0 0 0 620.08 31.685 Q 622.72 34.965 627.68 38.325 Q 632.562 41.632 638.838 45.404 A 336.973 336.973 0 0 0 639.04 45.525 Q 645.655 49.445 650.207 53.272 A 49.549 49.549 0 0 1 652.8 55.605 A 36.832 36.832 0 0 1 657.162 60.623 A 28.158 28.158 0 0 1 660.24 66.085 A 29.077 29.077 0 0 1 662.163 73.131 A 38.837 38.837 0 0 1 662.56 78.805 A 41.422 41.422 0 0 1 661.84 86.744 Q 660.892 91.599 658.696 95.481 A 25.101 25.101 0 0 1 657.84 96.885 A 27.512 27.512 0 0 1 645.745 107.023 A 33.765 33.765 0 0 1 644.56 107.525 A 45.856 45.856 0 0 1 634.709 110.197 Q 630.224 110.917 625.158 110.962 A 76.284 76.284 0 0 1 624.48 110.965 Z", + id: "5", + vectorEffect: "non-scaling-stroke", + }, + }), + svg.path({ + attrs: { + transform: "translate(460,300)", + d: "M 703.52 110.965 Q 696.64 110.965 690.24 109.365 Q 683.84 107.765 678.24 104.245 Q 677.28 103.445 676.32 102.485 A 7.09 7.09 0 0 1 675.313 101.231 A 6.069 6.069 0 0 1 674.88 100.405 Q 673.28 96.085 672.72 89.845 A 109.626 109.626 0 0 1 672.285 80.042 A 113.493 113.493 0 0 1 672.32 77.205 Q 672.32 76.15 673.173 75.663 A 2.198 2.198 0 0 1 673.28 75.605 Q 674.24 75.125 675.52 75.125 Q 677.12 75.125 677.68 75.525 Q 678.24 75.925 678.88 77.045 A 56.914 56.914 0 0 0 682.619 86.657 A 40.784 40.784 0 0 0 689.36 96.405 A 24.816 24.816 0 0 0 695.646 101.305 A 20.744 20.744 0 0 0 705.6 103.765 Q 710.72 103.765 714 101.445 Q 717.28 99.125 719.04 95.525 A 17.225 17.225 0 0 0 720.59 90.78 A 15.4 15.4 0 0 0 720.8 88.245 A 21.789 21.789 0 0 0 720.47 84.349 Q 720.052 82.048 719.103 80.17 A 13.127 13.127 0 0 0 718.8 79.605 A 18.002 18.002 0 0 0 715.917 75.848 A 24.514 24.514 0 0 0 712.8 73.205 Q 710.05 71.225 706.242 69.018 A 135.313 135.313 0 0 0 702.56 66.965 Q 695.84 63.125 689.6 58.885 Q 683.36 54.645 679.36 48.565 Q 675.468 42.649 675.363 33.853 A 41.035 41.035 0 0 1 675.36 33.365 Q 675.36 23.925 680.24 17.045 Q 685.12 10.165 693.6 6.325 A 43.184 43.184 0 0 1 706.787 2.798 A 53.582 53.582 0 0 1 712.64 2.485 Q 718.72 2.485 724.24 3.765 Q 729.76 5.045 733.12 6.325 Q 735.04 7.125 735.92 8.085 A 4.088 4.088 0 0 1 736.441 8.815 Q 736.998 9.786 737.44 11.445 A 51.999 51.999 0 0 1 738.007 14.168 Q 738.284 15.691 738.525 17.434 A 103.878 103.878 0 0 1 738.8 19.605 Q 739.36 24.405 739.36 30.165 Q 739.36 32.245 736.48 32.245 Q 735.04 32.245 733.84 31.525 Q 732.64 30.805 732.32 29.685 A 43.999 43.999 0 0 0 730.148 23.634 Q 728.69 20.391 726.812 17.91 A 22.151 22.151 0 0 0 724.32 15.125 A 18.127 18.127 0 0 0 711.838 10.17 A 23.441 23.441 0 0 0 711.36 10.165 A 24.049 24.049 0 0 0 707.698 10.429 Q 705.684 10.74 703.999 11.416 A 13.834 13.834 0 0 0 703.2 11.765 Q 699.84 13.365 698.16 16.565 A 13.649 13.649 0 0 0 696.954 19.917 Q 696.48 22.015 696.48 24.565 A 10.869 10.869 0 0 0 698.484 30.83 A 13.95 13.95 0 0 0 699.12 31.685 Q 701.76 34.965 706.72 38.325 Q 711.602 41.632 717.878 45.404 A 336.973 336.973 0 0 0 718.08 45.525 Q 724.695 49.445 729.247 53.272 A 49.549 49.549 0 0 1 731.84 55.605 A 36.832 36.832 0 0 1 736.202 60.623 A 28.158 28.158 0 0 1 739.28 66.085 A 29.077 29.077 0 0 1 741.203 73.131 A 38.837 38.837 0 0 1 741.6 78.805 A 41.422 41.422 0 0 1 740.88 86.744 Q 739.932 91.599 737.736 95.481 A 25.101 25.101 0 0 1 736.88 96.885 A 27.512 27.512 0 0 1 724.785 107.023 A 33.765 33.765 0 0 1 723.6 107.525 A 45.856 45.856 0 0 1 713.749 110.197 Q 709.264 110.917 704.198 110.962 A 76.284 76.284 0 0 1 703.52 110.965 Z", + id: "6", + vectorEffect: "non-scaling-stroke", + }, + }), + svg.rect({ + attrs: { x: "466", y: "240.5", width: "800", height: "15.5" }, + }), + // svg.text({ attrs: { 'font-family': "Calibri", 'font-size': "40", 'font-weight': "bold", x: "497", y: "450" } }, 'COMPUTATIONAL SCIENCES'), + // svg.path({ attrs: { d: "M 1111.1316 329.63885 C 1111.7629 340.6366 1119.8291 350.53146 1131.0461 350.7098 C 1142.2632 350.8882 1154.6805 342.4446 1153.2188 331.05904 C 1151.7572 319.67348 1142.7102 319.24054 1132.1177 316.17612 C 1121.5253 313.11165 1130.3999 308.0055 1130.3999 308.0055 C 1130.3999 308.0055 1134.739 305.7016 1139.3956 304.96687 C 1144.0522 304.23215 1147.434 306.17886 1149.1073 306.1567 C 1150.7806 306.13453 1150.2077 300.1209 1149.0792 298.99964 C 1147.9508 297.87838 1138.8401 295.20424 1130.1026 299.3452 C 1121.365 303.48615 1118.0291 308.5461 1118.0291 308.5461 C 1118.0291 308.5461 1110.5002 318.6411 1111.1316 329.63885 Z" } }), + // svg.path({ attrs: { d: "M 1123.1925 334.362 C 1124.9592 338.72802 1129.5795 341.5972 1134.138 340.08526 C 1138.6965 338.57332 1142.5552 333.3991 1140.3996 328.99328 C 1138.244 324.58742 1134.0469 325.6506 1129.3442 325.9052 C 1124.6415 326.1598 1122.6041 322.6 1122.6041 322.6 L 1122.3587 325.91298 C 1122.3587 325.91298 1121.4258 329.996 1123.1925 334.362 Z", fill: "white" } }), + // svg.ellipse({ attrs: { cx: "1130.6581", cy: "328.0728", rx: "5.658064", ry: "6.727217" } }), + // svg.ellipse({ attrs: { cx: "1130.6581", cy: "329.1", rx: "2.554036", ry: "2.1000034", fill: "white" } }), + ]), + ] +) + +const homeSVG = svg( + { + attrs: { + "vertical-align": "baseline", + height: "30pt", + width: "30pt", + viewBox: "1020 -226 972 972", + }, + }, + [ + svg.a({ attrs: { "xlink:href": "/target" } }, [ + // TARGET + svg.path({ + attrs: { + id: "target", + d: "M 1506.0454 -136.98166 L 1506.0454 -225 C 1420.9163 -225 1337.2871 -202.5916 1263.5632 -160.02709 C 1031.6083 -26.107868 952.1347 270.4917 1086.0539 502.4466 L 1162.28 458.43743 C 1052.6664 268.58106 1117.716 25.81266 1307.5724 -83.80097 C 1367.9158 -118.64026 1436.3668 -136.98165 1506.0454 -136.98166 Z", + fill: "#f44335", + }, + }), + svg.text([ + svg.textPath( + { attrs: { "xlink:href": "#target", startOffset: "80%" } }, + [ + svg.tspan( + { + attrs: { + "font-family": '"Roboto", Arial, Helvetica, sans-serif', + "font-size": "60", + "text-anchor": "middle", + "letter-spacing": 15, + "font-weight": "bold", + fill: "#ebebeb", + dy: "-20", + }, + }, + "TARGET" + ), + ] + ), + ]), + svg.path({ + attrs: { + d: "M 1506.0454 -136.98166 L 1506.0454 -225 C 1420.9163 -225 1337.2871 -202.5916 1263.5632 -160.02709 C 1031.6083 -26.107868 952.1347 270.4917 1086.0539 502.4466 L 1162.28 458.43743 C 1052.6664 268.58106 1117.716 25.81266 1307.5724 -83.80097 C 1367.9158 -118.64026 1436.3668 -136.98165 1506.0454 -136.98166 Z", + stroke: "white", + "fill-opacity": "0", + "stroke-linecap": "round", + "stroke-linejoin": "round", + "stroke-width": "6", + }, + }), ]), // DISEASE - svg.a({ attrs: { 'xlink:href': "/disease" } }, [ - svg.path({ - attrs: { - id: "disease", - d: "M 1849.8108 458.43743 L 1926.0369 502.4466 C 1968.6014 428.7227 1991.0098 345.09344 1991.0098 259.9644 C 1991.0098 -7.8740405 1773.8838 -225 1506.0454 -225 L 1506.0454 -136.98166 C 1725.2726 -136.98166 1902.9914 40.73715 1902.9914 259.9644 C 1902.9914 329.643 1884.65 398.094 1849.8108 458.4374 Z", - fill: "#e91e63" - } - }), - svg.text([ - svg.textPath({ attrs: { 'xlink:href': "#disease", startOffset: "80%" } }, [ - svg.tspan({ attrs: { 'font-family': '"Roboto", Arial, Helvetica, sans-serif', 'font-size': "60", 'text-anchor': "middle", 'letter-spacing': 15, 'font-weight': "bold", fill: "#ebebeb", dy: "-20" } }, "DISEASE") - ]) - ]), - svg.path({ - attrs: { - d: "M 1849.8108 458.43743 L 1926.0369 502.4466 C 1968.6014 428.7227 1991.0098 345.09344 1991.0098 259.9644 C 1991.0098 -7.8740405 1773.8838 -225 1506.0454 -225 L 1506.0454 -136.98166 C 1725.2726 -136.98166 1902.9914 40.73715 1902.9914 259.9644 C 1902.9914 329.643 1884.65 398.094 1849.8108 458.4374 Z", - stroke: "white", - 'fill-opacity': '0', - 'stroke-linecap': "round", - 'stroke-linejoin': "round", - 'stroke-width': "6" - } - }) + svg.a({ attrs: { "xlink:href": "/disease" } }, [ + svg.path({ + attrs: { + id: "disease", + d: "M 1849.8108 458.43743 L 1926.0369 502.4466 C 1968.6014 428.7227 1991.0098 345.09344 1991.0098 259.9644 C 1991.0098 -7.8740405 1773.8838 -225 1506.0454 -225 L 1506.0454 -136.98166 C 1725.2726 -136.98166 1902.9914 40.73715 1902.9914 259.9644 C 1902.9914 329.643 1884.65 398.094 1849.8108 458.4374 Z", + fill: "#e91e63", + }, + }), + svg.text([ + svg.textPath( + { attrs: { "xlink:href": "#disease", startOffset: "80%" } }, + [ + svg.tspan( + { + attrs: { + "font-family": '"Roboto", Arial, Helvetica, sans-serif', + "font-size": "60", + "text-anchor": "middle", + "letter-spacing": 15, + "font-weight": "bold", + fill: "#ebebeb", + dy: "-20", + }, + }, + "DISEASE" + ), + ] + ), + ]), + svg.path({ + attrs: { + d: "M 1849.8108 458.43743 L 1926.0369 502.4466 C 1968.6014 428.7227 1991.0098 345.09344 1991.0098 259.9644 C 1991.0098 -7.8740405 1773.8838 -225 1506.0454 -225 L 1506.0454 -136.98166 C 1725.2726 -136.98166 1902.9914 40.73715 1902.9914 259.9644 C 1902.9914 329.643 1884.65 398.094 1849.8108 458.4374 Z", + stroke: "white", + "fill-opacity": "0", + "stroke-linecap": "round", + "stroke-linejoin": "round", + "stroke-width": "6", + }, + }), ]), // COMPOUND - svg.a({ attrs: { 'xlink:href': "/compound" } }, [ - svg.path({ - attrs: { - id: "compound", - d: "M 1162.28 458.43743 L 1086.0539 502.4466 C 1128.6184 576.1705 1189.8393 637.3914 1263.5632 679.9559 C 1495.518 813.8751 1792.1177 734.4015 1926.0369 502.4466 L 1849.8108 458.43743 C 1740.1971 648.2938 1497.4287 713.3434 1307.5724 603.7298 C 1247.229 568.8905 1197.1193 518.7809 1162.28 458.43745 Z", - fill: "#ff9800" - } - }), - svg.text([ - svg.textPath({ attrs: { 'xlink:href': "#compound", startOffset: "29%" } }, [ - svg.tspan({ attrs: { 'font-family': '"Roboto", Arial, Helvetica, sans-serif', 'font-size': "60", 'text-anchor': "middle", 'letter-spacing': 15, 'font-weight': "bold", fill: "#ebebeb", dy: "-20" } }, "COMPOUND") - ]) - ]), - svg.path({ - attrs: { - d: "M 1162.28 458.43743 L 1086.0539 502.4466 C 1128.6184 576.1705 1189.8393 637.3914 1263.5632 679.9559 C 1495.518 813.8751 1792.1177 734.4015 1926.0369 502.4466 L 1849.8108 458.43743 C 1740.1971 648.2938 1497.4287 713.3434 1307.5724 603.7298 C 1247.229 568.8905 1197.1193 518.7809 1162.28 458.43745 Z", - stroke: "white", - 'fill-opacity': '0', - 'stroke-linecap': "round", - 'stroke-linejoin': "round", - 'stroke-width': "6" - } - }) + svg.a({ attrs: { "xlink:href": "/compound" } }, [ + svg.path({ + attrs: { + id: "compound", + d: "M 1162.28 458.43743 L 1086.0539 502.4466 C 1128.6184 576.1705 1189.8393 637.3914 1263.5632 679.9559 C 1495.518 813.8751 1792.1177 734.4015 1926.0369 502.4466 L 1849.8108 458.43743 C 1740.1971 648.2938 1497.4287 713.3434 1307.5724 603.7298 C 1247.229 568.8905 1197.1193 518.7809 1162.28 458.43745 Z", + fill: "#ff9800", + }, + }), + svg.text([ + svg.textPath( + { attrs: { "xlink:href": "#compound", startOffset: "29%" } }, + [ + svg.tspan( + { + attrs: { + "font-family": '"Roboto", Arial, Helvetica, sans-serif', + "font-size": "60", + "text-anchor": "middle", + "letter-spacing": 15, + "font-weight": "bold", + fill: "#ebebeb", + dy: "-20", + }, + }, + "COMPOUND" + ), + ] + ), + ]), + svg.path({ + attrs: { + d: "M 1162.28 458.43743 L 1086.0539 502.4466 C 1128.6184 576.1705 1189.8393 637.3914 1263.5632 679.9559 C 1495.518 813.8751 1792.1177 734.4015 1926.0369 502.4466 L 1849.8108 458.43743 C 1740.1971 648.2938 1497.4287 713.3434 1307.5724 603.7298 C 1247.229 568.8905 1197.1193 518.7809 1162.28 458.43745 Z", + stroke: "white", + "fill-opacity": "0", + "stroke-linecap": "round", + "stroke-linejoin": "round", + "stroke-width": "6", + }, + }), ]), svg.g([ - // DISEASE - PHENO - svg.path({ attrs: { d: "M 1506.0454 251.9644 L 1506.0454 -.035595944 C 1645.2211 -.035595944 1758.0454 112.78865 1758.0454 251.9644 C 1758.0454 296.19964 1746.4014 339.65556 1724.2838 377.9644 Z", fill: "#e92363", 'fill-opacity': ".5" } }), - svg.path({ attrs: { d: "M 1506.0454 251.9644 L 1506.0454 -.035595944 C 1645.2211 -.035595944 1758.0454 112.78865 1758.0454 251.9644 C 1758.0454 296.19964 1746.4014 339.65556 1724.2838 377.9644 Z", stroke: "white", 'stroke-linecap': "round", 'stroke-linejoin': "round", 'stroke-width': "6", 'fill-opacity': '0' } }), - svg.text({ attrs: { transform: "translate(1529.807 150.726)", fill: "white" } }, [ - svg.tspan({ attrs: { 'font-family': '"Roboto", Arial, Helvetica, sans-serif', 'font-size': "60", 'font-weight': "bold", fill: "white", x: ".18359375", y: "56", textLength: "198.63281" } }, 'PHENO') - ]), - - svg.path({ attrs: { d: "M 1506.1296 251.9644 L 1724.368 377.9644 C 1654.78 498.49415 1500.6593 539.7907 1380.1296 470.2028 C 1341.8207 448.0852 1310.0088 416.27324 1287.8912 377.9644 Z", fill: "#fe9801", 'fill-opacity': ".5" } }), - svg.path({ attrs: { d: "M 1506.1296 251.9644 L 1724.368 377.9644 C 1654.78 498.49415 1500.6593 539.7907 1380.1296 470.2028 C 1341.8207 448.0852 1310.0088 416.27324 1287.8912 377.9644 Z", stroke: "white", 'stroke-linecap': "round", 'stroke-linejoin': "round", 'stroke-width': "6", 'fill-opacity': '0' } }), - // svg.path({ attrs: { d: "M 1506.1296 251.9644 L 1724.368 377.9644 C 1654.78 498.49415 1500.6593 539.7907 1380.1296 470.2028 C 1341.8207 448.0852 1310.0088 416.27324 1287.8912 377.9644 Z", stroke: "white", 'stroke-linecap': "round", 'stroke-linejoin': "round", 'stroke-width': "6" } }), - - - svg.path({ attrs: { d: "M 1506.0454 251.9644 L 1287.807 377.9644 C 1218.2191 257.43466 1259.5156 103.31388 1380.0454 33.726002 C 1418.3542 11.608383 1461.8101 -.035595944 1506.0454 -.035595944 Z", fill: "#f44335", 'fill-opacity': ".5" } }), - svg.path({ attrs: { d: "M 1506.0454 251.9644 L 1287.807 377.9644 C 1218.2191 257.43466 1259.5156 103.31388 1380.0454 33.726002 C 1418.3542 11.608383 1461.8101 -.035595944 1506.0454 -.035595944 Z", stroke: "white", 'stroke-linecap': "round", 'stroke-linejoin': "round", 'stroke-width': "6", 'fill-opacity': '0' } }), - - svg.text({ attrs: { transform: "translate(1309.807 150.726)", fill: "white" } }, [ - svg.tspan({ attrs: { 'font-family': '"Roboto", Arial, Helvetica, sans-serif', 'font-size': "60", 'font-weight': "bold", fill: "white", x: ".29589844", y: "56", textLength: "158.4082" } }, 'GENO') - ]), - - - svg.text({ attrs: { transform: "translate(1406.807 358.726)", fill: "white" } }, [ - svg.tspan({ attrs: { 'font-family': '"Roboto", Arial, Helvetica, sans-serif', 'font-size': "60", 'font-weight': "bold", fill: "white", x: ".3076172", y: "56", textLength: "209.38477" } }, 'CHEMO') - ]), - - - ]) -]) + // DISEASE - PHENO + svg.path({ + attrs: { + d: "M 1506.0454 251.9644 L 1506.0454 -.035595944 C 1645.2211 -.035595944 1758.0454 112.78865 1758.0454 251.9644 C 1758.0454 296.19964 1746.4014 339.65556 1724.2838 377.9644 Z", + fill: "#e92363", + "fill-opacity": ".5", + }, + }), + svg.path({ + attrs: { + d: "M 1506.0454 251.9644 L 1506.0454 -.035595944 C 1645.2211 -.035595944 1758.0454 112.78865 1758.0454 251.9644 C 1758.0454 296.19964 1746.4014 339.65556 1724.2838 377.9644 Z", + stroke: "white", + "stroke-linecap": "round", + "stroke-linejoin": "round", + "stroke-width": "6", + "fill-opacity": "0", + }, + }), + svg.text( + { attrs: { transform: "translate(1529.807 150.726)", fill: "white" } }, + [ + svg.tspan( + { + attrs: { + "font-family": '"Roboto", Arial, Helvetica, sans-serif', + "font-size": "60", + "font-weight": "bold", + fill: "white", + x: ".18359375", + y: "56", + textLength: "198.63281", + }, + }, + "PHENO" + ), + ] + ), + + svg.path({ + attrs: { + d: "M 1506.1296 251.9644 L 1724.368 377.9644 C 1654.78 498.49415 1500.6593 539.7907 1380.1296 470.2028 C 1341.8207 448.0852 1310.0088 416.27324 1287.8912 377.9644 Z", + fill: "#fe9801", + "fill-opacity": ".5", + }, + }), + svg.path({ + attrs: { + d: "M 1506.1296 251.9644 L 1724.368 377.9644 C 1654.78 498.49415 1500.6593 539.7907 1380.1296 470.2028 C 1341.8207 448.0852 1310.0088 416.27324 1287.8912 377.9644 Z", + stroke: "white", + "stroke-linecap": "round", + "stroke-linejoin": "round", + "stroke-width": "6", + "fill-opacity": "0", + }, + }), + // svg.path({ attrs: { d: "M 1506.1296 251.9644 L 1724.368 377.9644 C 1654.78 498.49415 1500.6593 539.7907 1380.1296 470.2028 C 1341.8207 448.0852 1310.0088 416.27324 1287.8912 377.9644 Z", stroke: "white", 'stroke-linecap': "round", 'stroke-linejoin': "round", 'stroke-width': "6" } }), + + svg.path({ + attrs: { + d: "M 1506.0454 251.9644 L 1287.807 377.9644 C 1218.2191 257.43466 1259.5156 103.31388 1380.0454 33.726002 C 1418.3542 11.608383 1461.8101 -.035595944 1506.0454 -.035595944 Z", + fill: "#f44335", + "fill-opacity": ".5", + }, + }), + svg.path({ + attrs: { + d: "M 1506.0454 251.9644 L 1287.807 377.9644 C 1218.2191 257.43466 1259.5156 103.31388 1380.0454 33.726002 C 1418.3542 11.608383 1461.8101 -.035595944 1506.0454 -.035595944 Z", + stroke: "white", + "stroke-linecap": "round", + "stroke-linejoin": "round", + "stroke-width": "6", + "fill-opacity": "0", + }, + }), + + svg.text( + { attrs: { transform: "translate(1309.807 150.726)", fill: "white" } }, + [ + svg.tspan( + { + attrs: { + "font-family": '"Roboto", Arial, Helvetica, sans-serif', + "font-size": "60", + "font-weight": "bold", + fill: "white", + x: ".29589844", + y: "56", + textLength: "158.4082", + }, + }, + "GENO" + ), + ] + ), + + svg.text( + { attrs: { transform: "translate(1406.807 358.726)", fill: "white" } }, + [ + svg.tspan( + { + attrs: { + "font-family": '"Roboto", Arial, Helvetica, sans-serif', + "font-size": "60", + "font-weight": "bold", + fill: "white", + x: ".3076172", + y: "56", + textLength: "209.38477", + }, + }, + "CHEMO" + ), + ] + ), + ]), + ] +) diff --git a/src/js/pages/adminSettings.js b/src/js/pages/adminSettings.js index 7ec4dd5b..15cf8a33 100644 --- a/src/js/pages/adminSettings.js +++ b/src/js/pages/adminSettings.js @@ -1,12 +1,8 @@ -import { - div, - button, -} from "@cycle/dom" +import { div, button } from "@cycle/dom" import xs from "xstream" import isolate from "@cycle/isolate" import * as R from "ramda" import debounce from "xstream/extra/debounce" -import dropRepeats from "xstream/extra/dropRepeats" import pairwise from "xstream/extra/pairwise" import { SettingsEditor } from "../components/SettingsEditor" @@ -57,6 +53,20 @@ export function AdminSettings(sources) { }, ], }, + { + group: "strategy", + title: "Strategy Settings", + settings: [ + { + field: "deployments", + type: "select", + class: ".switch", + title: "Deployments Strategy", + options: ["ours", "theirs"], + props: { type: "checkbox" }, + }, + ], + }, { group: "api", title: "API Settings", @@ -245,12 +255,15 @@ export function AdminSettings(sources) { type: "text", title: "Max normal time for statistics query", props: { type: "text" }, - } + }, ], }, ]) - const AdminSettings = SettingsEditor({...sources, settings$: settingsConfig$}) + const AdminSettings = SettingsEditor({ + ...sources, + settings$: settingsConfig$, + }) const vdom$ = xs .combine(settings$, AdminSettings.DOM) @@ -280,7 +293,7 @@ export function AdminSettings(sources) { /** * Update deployment settings but only if it was changed * DropRepeats would 'let through' the initial value - * + * * Deployment needs to be tackled globally, does not work in _isolation_ * @const AdminSettings/deploymentUpdate$ * @type {Stream} @@ -289,19 +302,20 @@ export function AdminSettings(sources) { .compose(pairwise) .filter(([a, b]) => !R.equals(a.deployment.name, b.deployment.name)) .map(([_, b]) => b) - + /** * Settings update when deployment name is not set * @const AdminSettings/deploymentMissing$ * @type {Stream} */ - const deploymentMissing$ = settings$ - .filter((settings) => settings.deployment.name === undefined) + const deploymentMissing$ = settings$.filter( + (settings) => settings.deployment.name === undefined + ) /** * When deployment is changed or missing, fetch the appropriate deployment from deployments.js * overwrite the relevant entries (endpoints) of the various settings with the correct one - * + * * @const AdminSettings/deploymentReducer$ * @type {Reducer} */ From 4b2e3e4d95a2889a836f4fb45d9bfac34dd19427 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 11 Jan 2022 13:05:09 +0100 Subject: [PATCH 084/191] Fix SignatureGenerator displaying '...' even for small signatures (#106) --- src/js/components/SignatureGenerator.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/js/components/SignatureGenerator.js b/src/js/components/SignatureGenerator.js index d3f2d31b..fe9b47aa 100644 --- a/src/js/components/SignatureGenerator.js +++ b/src/js/components/SignatureGenerator.js @@ -135,7 +135,10 @@ function view(state$, request$, response$, geneAnnotationQuery) { * @const view/validVdom$/showLimit * @type {Number} */ - const showLimit = state.core.showLimit > 0 ? state.core.showLimit : undefined + const showLimit = + state.core.showLimit > 0 && state.core.showLimit < arr.length + ? state.core.showLimit + : undefined /** * Display div with a button when the signature is long, otherwise show empty div From 9e36de984755220f2bb1be5f8a7076398a46f57a Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Tue, 11 Jan 2022 14:32:31 +0100 Subject: [PATCH 085/191] Add busy flag that propagates to below components (#107) * Add busy flag that propagates to below components and filter loaded vdom on busy flag * Don't display old data during loading in SampleSelect Create a copy of the state$ stream and erase data, same behaviour as initial loading --- src/js/components/SampleSelection.js | 16 ++++++++++++++-- src/js/pages/genericTreatment.js | 9 +++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/js/components/SampleSelection.js b/src/js/components/SampleSelection.js index 26a4fa69..6208a2c8 100644 --- a/src/js/components/SampleSelection.js +++ b/src/js/components/SampleSelection.js @@ -20,7 +20,7 @@ import debounce from 'xstream/extra/debounce' import { loggerFactory } from "../utils/logger" import { TreatmentAnnotation } from "./TreatmentAnnotation" import { safeModelToUi } from "../modelTranslations" -import { dirtyUiReducer, dirtyWrapperStream } from "../utils/ui" +import { dirtyUiReducer, dirtyWrapperStream, busyUiReducer } from "../utils/ui" const emptyData = { body: { @@ -104,6 +104,13 @@ function SampleSelection(sources) { dropRepeats((x, y) => equals(x.core, y.core)) ) + // State without data erased, to be used for the loading vdom as we don't want to display old data + const loadingState$ = state$ + .map((state) => ({ + ...state, + core: { ...state.core, data: []} + })) + const request$ = newInput$.map((state) => { return { url: @@ -211,7 +218,7 @@ function SampleSelection(sources) { const initVdom$ = emptyState$.mapTo(div()) const loadingVdom$ = request$ - .compose(sampleCombine(state$)) + .compose(sampleCombine(loadingState$)) .map(([_, state]) => // Use the same makeTable function, pass a initialization=true parameter and a body DOM with preloading makeTable( @@ -230,6 +237,7 @@ function SampleSelection(sources) { const loadedVdom$ = xs .combine(modifiedState$, treatmentAnnotations.DOM) + .filter(([state, _]) => state.core.busy == false) .map(([state, annotation]) => makeTable(state, annotation, false)) // Wrap component vdom with an extra div that handles being dirty @@ -332,6 +340,9 @@ function SampleSelection(sources) { .compose(sampleCombine(state$)) .map(([ev, state]) => state.core.output) + // Logic and reducer stream that monitors if this component is busy + const busyReducer$ = busyUiReducer(newInput$, data$) + // Logic and reducer stream that monitors if this component became dirty const dirtyReducer$ = dirtyUiReducer(sampleSelection$, state$.map(state => state.core.output)) @@ -345,6 +356,7 @@ function SampleSelection(sources) { requestReducer$, dataReducer$, selectReducer$, + busyReducer$, dirtyReducer$, ), output: sampleSelection$, diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index c8f7f8d4..e2bdbd64 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -105,6 +105,7 @@ export default function GenericTreatmentWorkflow(sources) { .map(state => prevState => { const dirtyCheck = state.form.check.dirty + const busySampleSelection = state.form.sampleSelection.busy const dirtySampleSelection = state.form.sampleSelection.dirty const busySignature = state.form.signature.busy const dirtyFilter = state.filter.dirty @@ -112,11 +113,11 @@ export default function GenericTreatmentWorkflow(sources) { ui: { form: { sampleSelection: {dirty: dirtyCheck }, - signature: {dirty: dirtyCheck || dirtySampleSelection }, + signature: {dirty: dirtyCheck || busySampleSelection || dirtySampleSelection }, }, - headTable: {dirty: dirtyCheck || dirtySampleSelection || busySignature || dirtyFilter }, - tailTable: {dirty: dirtyCheck || dirtySampleSelection || busySignature || dirtyFilter }, - plots: {dirty: dirtyCheck || dirtySampleSelection || busySignature || dirtyFilter }, + headTable: {dirty: dirtyCheck || busySampleSelection || dirtySampleSelection || busySignature || dirtyFilter }, + tailTable: {dirty: dirtyCheck || busySampleSelection || dirtySampleSelection || busySignature || dirtyFilter }, + plots: {dirty: dirtyCheck || busySampleSelection || dirtySampleSelection || busySignature || dirtyFilter }, }, }) } From f362d2869964f6eb20c1358f923483429bfdb251 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 14 Jan 2022 11:59:49 +0100 Subject: [PATCH 086/191] Display gene space during signature check (#104) * Display gene space in signature check component Displays if gene is found in L1000, BING or AIG Requires updated LuciusCore to work * Remove gene space mapping from client side this needs to be implemented in API side * Provide backwards compatibility with older LuciusApi translate entry.found and entry.dataType from entry.inL1000 Not as good but that's the best we can do * Fix SignatureCheck becoming empty when validating with old API UI was fixed but actual validation still only used .found --- src/js/components/SignatureCheck.js | 32 +++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/js/components/SignatureCheck.js b/src/js/components/SignatureCheck.js index 93eb44b5..5cff93b7 100644 --- a/src/js/components/SignatureCheck.js +++ b/src/js/components/SignatureCheck.js @@ -78,15 +78,31 @@ function SignatureCheck(sources) { // Helper function for rendering the table, based on the state const makeTable = (data) => { // let visible = visible1 //state.ux.checkSignatureVisible; - let rows = data.map(entry => [ - (entry.inL1000) ? td([i('.small .material-icons', 'done')] ) : td('.red .lighten-4 .red-text .text-darken-4', [i('.small .material-icons', 'mode_edit')] ), - (entry.inL1000) ? td(entry.query) : td('.red .lighten-4 .red-text .text-darken-4', entry.query), - (entry.inL1000) ? td(entry.symbol) : td('.red .lighten-4 .red-text .text-darken-4', entry.symbol) - ]); + + const entryToRow = (entry) => { + const e = + entry.hasOwnProperty("found") && entry.hasOwnProperty("dataType") + ? entry + : { + ...entry, + found: entry.inL1000, + dataType: entry.inL1000 ? "found" : "", + } + + return [ + (e.found) ? td([i('.small .material-icons', 'done')] ) : td('.red .lighten-4 .red-text .text-darken-4', [i('.small .material-icons', 'mode_edit')] ), + (e.found) ? td(e.query) : td('.red .lighten-4 .red-text .text-darken-4', e.query), + (e.found) ? td(e.symbol) : td('.red .lighten-4 .red-text .text-darken-4', e.symbol), + (e.found) ? td(e.dataType) : td('.red .lighten-4 .red-text .text-darken-4', 'N/A'), + ] + } + + let rows = data.map(entry => entryToRow(entry)); const header = tr([ - th('In L1000?'), + th('Found?'), th('Input'), - th('Symbol') + th('Symbol'), + th('Gene space') ]); let body = []; @@ -114,7 +130,7 @@ function SignatureCheck(sources) { const collapseUpdate$ = domSource$.select('.collapseUpdate').events('click'); const collapseUpdateReducer$ = collapseUpdate$.compose(sampleCombine(data$)) .map(([collapse, data]) => prevState => { - return ({...prevState, query : data.map(x => (x.inL1000) ? x.symbol : '').join(" ").replace(/\s\s+/g, ' ').trim()}); + return ({...prevState, query : data.map(x => (x.found ?? x.inL1000) ? x.symbol : '').join(" ").replace(/\s\s+/g, ' ').trim()}); }); // The result of this component is an event when valid From 85d3e5c12df6f0a3beefc26a2f88bb5d585f66a0 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 18 Jan 2022 15:21:29 +0100 Subject: [PATCH 087/191] attempt at working with the history driver --- src/js/index.js | 28 +++++++++++++++++++++++++++- src/js/main.js | 12 ++++++++++-- src/js/pages/genericTreatment.js | 4 ++-- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/js/index.js b/src/js/index.js index ada850c6..eb4bb093 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -57,6 +57,10 @@ import { export default function Index(sources) { const { router } = sources + console.log("sources:") + console.log(sources) + console.log("router:") + console.log(router) const logger = loggerFactory( "index", @@ -84,7 +88,7 @@ export default function Index(sources) { "/debug": Debug, "/admin": IsolatedAdminSettings, "*": Home, - })(sources) + })(sources).debug("page$") const makeLink = (path, label, options) => { const currentPage = window.location.href @@ -398,6 +402,27 @@ export default function Index(sources) { const history$ = sources.onion.state$.fold((acc, x) => acc.concat([x]), [{}]) + const historyDriver$ = xs.periodic(30000).map(i => ({ + type: 'replace', + hash: "tailTable"+i, + //pathname: "/genetic", + // search: "", + // state: undefined, + }) + // '/genetic#tailTable' + ) + + // PushHistoryInput( + // type: 'push' + // pathname?: Pathname; + // search?: Search; + // state?: any; + // hash?: Hash; + // key?: LocationKey; + // ) + + // const historyDriver$ = xs.periodic(1000).map(i => '/genetic') + return { log: xs.merge( // logger(page$, 'page$', '>> ', ' > ', ''), @@ -432,6 +457,7 @@ export default function Index(sources) { .filter(Boolean) .flatten() .debug("deployments"), + history: historyDriver$, } } diff --git a/src/js/main.js b/src/js/main.js index 389adb8a..37b90ddb 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -36,7 +36,7 @@ const drivers = { vega: makeVegaDriver(), HTTP: makeHTTPDriver(), // router: makeRouterDriver(captureClicks(makeHistoryDriver()), switchPath), - history: makeHistoryDriver(), + history: makeHistoryDriver({forceRefresh: false}), preventDefault: preventDefaultDriver, alert: alertDriver, storage: storageDriver, @@ -49,5 +49,13 @@ const drivers = { deployments: () => xs.fromPromise(fetch('/deployments.json').then(m => m.json())) }; -let StatifiedMain = onionify(storageify(routerify(Index, switchPath), { key: 'ComPass' })); +const customSwitchPath = (sourcePath, routes) => { + + console.log("sourcePath: " + sourcePath) + console.log("routes:") + console.log(routes) + return switchPath(sourcePath, routes) +} + +let StatifiedMain = onionify(storageify(routerify(Index, customSwitchPath, {omitHistory: false}), { key: 'ComPass' })); run(StatifiedMain, drivers); diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index e2bdbd64..ab3ffa7d 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -276,7 +276,7 @@ export default function GenericTreatmentWorkflow(sources) { div(".row", [displayPlots === "before tables" ? plots : div()]), div(".col .s12", [headTable]), div(".row", []), - div(".col .s12", [tailTable]), + div(".col .s12", {props: {id: "tailTable"}}, [tailTable]), div(".row", []), div(".row", [displayPlots === "after tables" ? plots : div()]), ]), @@ -292,7 +292,7 @@ export default function GenericTreatmentWorkflow(sources) { headTable.log, tailTable.log ), - DOM: vdom$.startWith(div()), + DOM: vdom$,//.startWith(div()), onion: xs.merge( defaultReducer$, TreatmentFormSink.onion, From 36b47cd40ce300ac7b58fb0fff131bab88e0beef Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 18 Jan 2022 16:39:01 +0100 Subject: [PATCH 088/191] UI redesign (#112) * Use mixin to create page styles Create common structure for page style and next fill in values using mixin significantly reduces common code declare background & text colors more directly instead of using @extend the resulting css was not nice still use lighten & darken properties from materialize-css by using their color method, so there should be no difference (which was confirmed visually) * Move styling from SampleSelection to css and start work on home page * Add placeholder in home menu page and add css to add content Easier to customize this way instead of having to add it through code * Move home page background to css * Add router info in state, use for nav current page indication and home background Add current page in first page div for further identification in css Add the svg background on home page to the homePage div * Move navbar/sidenav styling to css * Add placeholder title Allows customization of the title name * Remove placeholder WF text in main.scss and button styling, add .row to facilitate text flow * on home page provide extra span for tooltips on WF links To be used as placeholders for adding content in CSS * Move color settings and variables to separate file and create custom style file Provide option to create a specific color for WF icon and title colors rest of WF page colors is still determined from color name and lighten/darken * Fix sass import order * Move svg icon coloring to css for ligand and correlation, we need to use fill too * Move basic styling of header and footer to scss * Add homepage class descriptors on text components Allows changing introduction and afterword text/style * Move hamburger icon to the left as is standard, mainly conflicted with extraTitle * add navbar version identification and move styling to css * split navbar text & svg so we can address the text separately move svg outside span and wrap around with div * By default fill SVGs with the background colors Otherwise it gets filled with black and depending on the background of the theme this causes issues * Move home page list font size to css * Move top table entry color styling from code to css * Move version number to footer * rename sidenav trigger to a more suitable name * Explicitly name logo icons on the navbar remove styling from code and move to css * Add selectors in cluster status check vdom Allows applying extra styling per status * cluster checking, add 'off' dots in light grey so that the length remains constant --- src/js/_compass_svg.scss | 1 + src/js/_main_custom.scss | 10 + src/js/_variables.scss | 15 + src/js/components/Check.js | 11 +- src/js/components/SampleSelection.js | 4 +- src/js/components/SampleTable/SampleInfo.js | 20 +- src/js/index.js | 634 ++++++++++---------- src/js/main.scss | 219 ++++--- src/js/pages/home.js | 45 +- src/js/svg.js | 38 +- 10 files changed, 546 insertions(+), 451 deletions(-) create mode 100644 src/js/_compass_svg.scss create mode 100644 src/js/_main_custom.scss create mode 100644 src/js/_variables.scss diff --git a/src/js/_compass_svg.scss b/src/js/_compass_svg.scss new file mode 100644 index 00000000..3833d91c --- /dev/null +++ b/src/js/_compass_svg.scss @@ -0,0 +1 @@ +$compass_background_svg: url("data:image/svg+xml;utf8,"); \ No newline at end of file diff --git a/src/js/_main_custom.scss b/src/js/_main_custom.scss new file mode 100644 index 00000000..58711bdb --- /dev/null +++ b/src/js/_main_custom.scss @@ -0,0 +1,10 @@ +/* Override colors in correlation workflow */ +main div.correlation { + .WF-header { + background-color: color("blue", "darken-3"); + } + + .validation { + background-color: color("blue", "lighten-3"); + } +} diff --git a/src/js/_variables.scss b/src/js/_variables.scss new file mode 100644 index 00000000..dfbd6215 --- /dev/null +++ b/src/js/_variables.scss @@ -0,0 +1,15 @@ +$color-text: black; +$color-footer-text: color('grey', 'base'); +$color-header-background: color('grey', 'darken-4'); +$color-footer-background: color('grey', 'darken-4'); + +$colors-WF: ( + compound: 'orange', + genetic: 'red', + ligand: 'purple', + disease: 'pink', + target: 'red', + correlation: 'blue', + generic: 'green', + settings: 'grey', +); diff --git a/src/js/components/Check.js b/src/js/components/Check.js index 735824fb..cef2b264 100644 --- a/src/js/components/Check.js +++ b/src/js/components/Check.js @@ -91,7 +91,10 @@ function Check(sources) { */ const initVdom$ = xs.periodic(200) .map(i => i % 4) - .map(i => span(".grey-text", ".".repeat(i))) + .map(i => [ + span(".grey-text .testing", ".".repeat(i)), + span(".grey-text .text-lighten-4 .testing2", ".".repeat(3-i)) + ]) .endWhen(validResponseJobs$) /** @@ -124,11 +127,11 @@ function Check(sources) { const loadedVdom$ = xs.combine(responseMetric$, maxNormalTime$) .map(([metric, max]) => (metric < max) ? - i('.material-icons .green-text .medium', 'done') : - i('.material-icons .red-text .medium', 'done') + i('.material-icons .green-text .medium .result-good', 'done') : + i('.material-icons .red-text .medium .result-busy', 'done') ) - const errorVdom$ = invalidResponse$.mapTo(i('.material-icons .red-text .medium', 'trending_down')) + const errorVdom$ = invalidResponse$.mapTo(i('.material-icons .red-text .medium .result-down', 'trending_down')) const vdom$ = xs.merge( initVdom$, diff --git a/src/js/components/SampleSelection.js b/src/js/components/SampleSelection.js index 6208a2c8..1ad1677a 100644 --- a/src/js/components/SampleSelection.js +++ b/src/js/components/SampleSelection.js @@ -144,7 +144,7 @@ function SampleSelection(sources) { } : {} const selectedClass = (selected) => - selected ? ".black-text" : ".grey-text .text-lighten-2" + selected ? ".sampleSelected" : ".sampleDeselected" let rows = data.map((entry) => [ td(".selection", { props: { id: entry.id } }, [ label("", { props: { id: entry.id } }, [ @@ -197,7 +197,7 @@ function SampleSelection(sources) { rows.map((row) => body.push(tr(row))) const tableContent = [thead([header]), tbody(body)] - return div([ + return div(".sampleSelection",[ div(".row", [ div(".col .s10 .offset-s1 .l10 .offset-l1", [ table(".striped .centered", tableContent), diff --git a/src/js/components/SampleTable/SampleInfo.js b/src/js/components/SampleTable/SampleInfo.js index 93ed07ae..a85759af 100644 --- a/src/js/components/SampleTable/SampleInfo.js +++ b/src/js/components/SampleTable/SampleInfo.js @@ -75,12 +75,12 @@ export function SampleInfo(sources) { function entry(key, value) { return [ span( - ".col .s4 .grey-text.text-darken-1", + ".col .s4 .entryKey", { style: { fontWeight: "lighter", whiteSpace: "nowrap"} }, key ), span( - ".col .s8", + ".col .s8 .entryValue", { style: { overflow: "hidden", "text-overflow": "ellipsis" } }, value?.length != 0 ? value : "" ), @@ -312,7 +312,7 @@ export function SampleInfo(sources) { const samplePart = [ - p(".col .s12 .grey-text", hStyle, "Sample Info:"), + p(".col .s12 .sampleHeader", hStyle, "Sample Info:"), p(pStyle, entry("Sample ID: ", sample.id)), p(pStyle, entry("Cell: ", sample.cell)), p(pStyle, entry("Dose: ", sample.dose !== "N/A" ? sample.dose + " " + (sample?.dose_unit ?? "?") : sample.dose)), @@ -323,7 +323,7 @@ export function SampleInfo(sources) { const treatmentPart = [ - p(".col .s12 .grey-text", hStyle, "Treatment Info:"), + p(".col .s12 .treatmentHeader", hStyle, "Treatment Info:"), p(pStylewBlur, entry("Name: ", sample.trt_name)), p( pStylewBlur, @@ -378,7 +378,7 @@ export function SampleInfo(sources) { div( ".col .s12 .l12", { style: { margin: "15px 0px 0px 0px" } }, - [p(".col .s12.grey-text", hStyle, "Filter Info:")].concat( + [p(".col .s12 .filterHeader", hStyle, "Filter Info:")].concat( _filters.map((x) => p(pStyle, entrySmall(x.key, x.value))) ) ), @@ -395,7 +395,7 @@ export function SampleInfo(sources) { div( ".row", { style: { margin: "15px 0px 0px 0px" } }, - [p(".col .s12.grey-text", hStyle, "Filter Info:")].concat( + [p(".col .s12.filterHeader", hStyle, "Filter Info:")].concat( _filters.map((x) => p(pStyle, entrySmall(x.key, x.value))) ) ), @@ -412,7 +412,7 @@ export function SampleInfo(sources) { div( ".row", { style: { margin: "15px 0px 0px 0px" } }, - [p(".col .s12.grey-text", hStyle, "Filter Info:")].concat( + [p(".col .s12 .filterHeader", hStyle, "Filter Info:")].concat( _filters.map((x) => p(pStyle, entrySmall(x.key, x.value))) ) ), @@ -429,7 +429,7 @@ export function SampleInfo(sources) { div( ".row", { style: { margin: "15px 0px 0px 0px" } }, - [p(".col .s12.grey-text", hStyle, "Filter Info:")].concat( + [p(".col .s12 .filterHeader", hStyle, "Filter Info:")].concat( _filters.map((x) => p(pStyle, entrySmall(x.key, x.value))) ) ), @@ -445,7 +445,7 @@ export function SampleInfo(sources) { div( ".col .s12 .l12", { style: { margin: "15px 0px 0px 0px" } }, - [p(".col .s12.grey-text", hStyle, "Filter Info:")].concat( + [p(".col .s12 .filterHeader", hStyle, "Filter Info:")].concat( _filters.map((x) => p(pStyle, entrySmall(x.key, x.value))) ) ), @@ -465,7 +465,7 @@ export function SampleInfo(sources) { const thisRowDetail = rowDetail(sample, updtProps, blur) return li( - ".collection-item .zoom", + ".collection-item .zoom .sampleInfo", { style: { "background-color": bgcolor } }, [ thisRow[sample.trt] ? thisRow[sample.trt] : thisRow["_default"], diff --git a/src/js/index.js b/src/js/index.js index ada850c6..a4f57e2d 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -86,15 +86,6 @@ export default function Index(sources) { "*": Home, })(sources) - const makeLink = (path, label, options) => { - const currentPage = window.location.href - const highlight = currentPage.endsWith(path) - - return li(highlight ? ".active" : "", [ - a(options, { props: { href: path } }, label), - ]) - } - // TODO: Add a visual reference for ghost mode // const ghost$ = state$ // .filter(state => state.common.ghost) @@ -103,10 +94,18 @@ export default function Index(sources) { // .startWith(span()) const nav$ = state$.map((state) => { + const makeLink = (path, label, options) => { + const highlight = state.routerInformation.pathname === path + + return li(highlight ? ".active" : "", [ + a(options, { props: { href: path } }, label), + ]) + } + const leftLogo = state.settings.config.logoUrl ? a( - ".left .grey-text .hide-on-med-and-down", - { props: { href: "/" }, style: { margin: "5px" } }, + ".left .brand-logo .left-logo .hide-on-med-and-down", + { props: { href: "/" } }, img(".logo_img .left", { props: { alt: "logo", src: state.settings.config.logoUrl }, style: { height: "40px" }, @@ -115,9 +114,9 @@ export default function Index(sources) { : span() const centerLogo = state.settings.config.logoUrl - ? div(".brand-logo .center", [ + ? div(".brand-logo .center-logo .center", [ a( - ".grey-text .hide-on-large-only", + ".hide-on-large-only", { props: { href: "/" } }, img(".logo_img", { props: { alt: "logo", src: state.settings.config.logoUrl }, @@ -128,8 +127,13 @@ export default function Index(sources) { : div() return header({ style: { display: "flex" } }, [ - nav("#navigation .grey .darken-4", [ + nav("#navigation", [ div(".nav-wrapper .valign-wrapper", [ + a( + ".sidenav-trigger", + { props: { href: "#" }, attrs: { "data-target": "mobile-navbar" } }, + i(".material-icons", "menu") + ), a( ".brand-logo .right .grey-text", { props: { href: "/" } }, @@ -138,75 +142,80 @@ export default function Index(sources) { ), leftLogo, a( - ".sidenav-trigger", - { props: { href: "#" }, attrs: { "data-target": "mobile-demo" } }, - i(".material-icons", "menu") + ".extraTitle", + { props: { href: "/" }, style: { margin: "5px" } }, + "" ), ul(".left .hide-on-med-and-down", [ makeLink( "/compound", - span(["Compound", " ", compoundSVG]), - ".orange-text" + div([span(["Compound", " "]), compoundSVG]), + ".compound" ), // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), makeLink( "/genetic", - span(["Genetic", " ", targetSVG]), - ".red-text" + div([span(["Genetic", " "]), targetSVG]), + ".genetic" ), makeLink( "/ligand", - span(["Ligand", " ", ligandSVG]), - ".purple-text" + div([span(["Ligand", " "]), ligandSVG]), + ".ligand" ), makeLink( "/disease", - span(["Disease", " ", diseaseSVG]), - ".pink-text" + div([span(["Disease", " "]), diseaseSVG]), + ".disease" ), makeLink( "/correlation", - span(["Correlation", " ", correlationSVG]), - ".blue-text" + div([span(["Correlation", " "]), correlationSVG]), + ".correlation" ), makeLink( "/settings", - span(["Settings", " ", settingsSVG]), - ".grey-text" + div([span(["Settings", " "]), settingsSVG]), + ".settings" ), // makeLink('/admin', span(['Admin']), '.blue-text'), - li(span(".grey-text .text-darken-3", "", ["v", VERSION])), ]), centerLogo, ]), ]), - ul(".sidenav", { props: { id: "mobile-demo" } }, [ + ul(".sidenav", { props: { id: "mobile-navbar" } }, [ makeLink( "/compound", - span(["Compound", " ", compoundSVG]), - ".orange-text" + div([span(["Compound", " "]), compoundSVG]), + ".compound" + ), + // makeLink('/target', span(['Target', ' ', targetSVG]), '.target'), + makeLink( + "/genetic", + div([span(["Genetic", " "]), targetSVG]), + ".genetic" + ), + makeLink( + "/ligand", + div([span(["Ligand", " "]), ligandSVG]), + ".ligand" + ), + makeLink( + "/disease", + div([span(["Disease", " "]), diseaseSVG]), + ".disease" ), - // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), - makeLink("/genetic", span(["Genetic", " ", targetSVG]), ".red-text"), - makeLink("/ligand", span(["Ligand", " ", ligandSVG]), ".purple-text"), - makeLink("/disease", span(["Disease", " ", diseaseSVG]), ".pink-text"), makeLink( "/correlation", - span(["Correlation", " ", correlationSVG]), - ".blue-text" + div([span(["Correlation", " "]), correlationSVG]), + ".correlation" ), makeLink( "/settings", - span(["Settings", " ", settingsSVG]), - ".grey-text" + div([span(["Settings", " "]), settingsSVG]), + ".settings" ), // makeLink('/admin', span(['Admin']), '.blue-text'), - li( - span(".grey-text .text-darken-3", { style: { padding: "0 32px" } }, [ - "v", - VERSION, - ]) - ), ]), ]) }) @@ -220,7 +229,7 @@ export default function Index(sources) { // We combine with state in order to read the customizations // This works because the defaultReducer runs before anything else const footer$ = state$.map((state) => - footer(".page-footer .grey .darken-4 .grey-text", [ + footer(".page-footer", [ div(".valign-wrapper .row", { style: { margin: "0px" } }, [ div(".col .s8", { style: { margin: "0px" } }, [ p({ style: { margin: "0px" } }, [ @@ -239,6 +248,10 @@ export default function Index(sources) { a({ props: { href: "/debug" } }, "this page"), " in your bug report", ]), + p({ style: { margin: "0px" } }, [ + span(".versionText", "v"), + span(".versionNumber", VERSION), + ]), ]), div( ".col .s4 .right-align", @@ -263,10 +276,20 @@ export default function Index(sources) { const view$ = page$.map(prop("DOM")).flatten().remember() + const pageName$ = state$.map((state) => { + const pageName = state.routerInformation.pathname.substr(1) + if (pageName == "") + return ".homePage" + else + return "." + pageName + "Page" + }) + .compose(dropRepeats()) + const vdom$ = xs - .combine(nav$, view$, footer$) - .map(([navDom, viewDom, footerDom]) => + .combine(pageName$, nav$, view$, footer$) + .map(([pageName, navDom, viewDom, footerDom]) => div( + pageName, { style: { display: "flex", @@ -385,6 +408,13 @@ export default function Index(sources) { } ) + const routerReducer$ = router.history$.map((router) => (prevState) => { + return { + ...prevState, + routerInformation: router, + } + }) + // Capture link targets and send to router driver const router$ = sources.DOM.select("a") .events("click") @@ -403,12 +433,14 @@ export default function Index(sources) { // logger(page$, 'page$', '>> ', ' > ', ''), logger(state$, "state$"), logger(history$, "history$"), + logger(router.history$, "router_history$"), // logger(prevent$, 'prevent$'), page$.map(prop("log")).filter(Boolean).flatten() ), onion: xs.merge( defaultReducer$.debug("defaultReducer"), deploymentsReducer$.debug("deplRed"), + routerReducer$, page$.map(prop("onion")).filter(Boolean).flatten() ), DOM: vdom$, @@ -540,254 +572,254 @@ export const logoSVG = svg( ] ) -const homeSVG = svg( - { - attrs: { - "vertical-align": "baseline", - height: "30pt", - width: "30pt", - viewBox: "1020 -226 972 972", - }, - }, - [ - svg.a({ attrs: { "xlink:href": "/target" } }, [ - // TARGET - svg.path({ - attrs: { - id: "target", - d: "M 1506.0454 -136.98166 L 1506.0454 -225 C 1420.9163 -225 1337.2871 -202.5916 1263.5632 -160.02709 C 1031.6083 -26.107868 952.1347 270.4917 1086.0539 502.4466 L 1162.28 458.43743 C 1052.6664 268.58106 1117.716 25.81266 1307.5724 -83.80097 C 1367.9158 -118.64026 1436.3668 -136.98165 1506.0454 -136.98166 Z", - fill: "#f44335", - }, - }), - svg.text([ - svg.textPath( - { attrs: { "xlink:href": "#target", startOffset: "80%" } }, - [ - svg.tspan( - { - attrs: { - "font-family": '"Roboto", Arial, Helvetica, sans-serif', - "font-size": "60", - "text-anchor": "middle", - "letter-spacing": 15, - "font-weight": "bold", - fill: "#ebebeb", - dy: "-20", - }, - }, - "TARGET" - ), - ] - ), - ]), - svg.path({ - attrs: { - d: "M 1506.0454 -136.98166 L 1506.0454 -225 C 1420.9163 -225 1337.2871 -202.5916 1263.5632 -160.02709 C 1031.6083 -26.107868 952.1347 270.4917 1086.0539 502.4466 L 1162.28 458.43743 C 1052.6664 268.58106 1117.716 25.81266 1307.5724 -83.80097 C 1367.9158 -118.64026 1436.3668 -136.98165 1506.0454 -136.98166 Z", - stroke: "white", - "fill-opacity": "0", - "stroke-linecap": "round", - "stroke-linejoin": "round", - "stroke-width": "6", - }, - }), - ]), - // DISEASE - svg.a({ attrs: { "xlink:href": "/disease" } }, [ - svg.path({ - attrs: { - id: "disease", - d: "M 1849.8108 458.43743 L 1926.0369 502.4466 C 1968.6014 428.7227 1991.0098 345.09344 1991.0098 259.9644 C 1991.0098 -7.8740405 1773.8838 -225 1506.0454 -225 L 1506.0454 -136.98166 C 1725.2726 -136.98166 1902.9914 40.73715 1902.9914 259.9644 C 1902.9914 329.643 1884.65 398.094 1849.8108 458.4374 Z", - fill: "#e91e63", - }, - }), - svg.text([ - svg.textPath( - { attrs: { "xlink:href": "#disease", startOffset: "80%" } }, - [ - svg.tspan( - { - attrs: { - "font-family": '"Roboto", Arial, Helvetica, sans-serif', - "font-size": "60", - "text-anchor": "middle", - "letter-spacing": 15, - "font-weight": "bold", - fill: "#ebebeb", - dy: "-20", - }, - }, - "DISEASE" - ), - ] - ), - ]), - svg.path({ - attrs: { - d: "M 1849.8108 458.43743 L 1926.0369 502.4466 C 1968.6014 428.7227 1991.0098 345.09344 1991.0098 259.9644 C 1991.0098 -7.8740405 1773.8838 -225 1506.0454 -225 L 1506.0454 -136.98166 C 1725.2726 -136.98166 1902.9914 40.73715 1902.9914 259.9644 C 1902.9914 329.643 1884.65 398.094 1849.8108 458.4374 Z", - stroke: "white", - "fill-opacity": "0", - "stroke-linecap": "round", - "stroke-linejoin": "round", - "stroke-width": "6", - }, - }), - ]), - // COMPOUND - svg.a({ attrs: { "xlink:href": "/compound" } }, [ - svg.path({ - attrs: { - id: "compound", - d: "M 1162.28 458.43743 L 1086.0539 502.4466 C 1128.6184 576.1705 1189.8393 637.3914 1263.5632 679.9559 C 1495.518 813.8751 1792.1177 734.4015 1926.0369 502.4466 L 1849.8108 458.43743 C 1740.1971 648.2938 1497.4287 713.3434 1307.5724 603.7298 C 1247.229 568.8905 1197.1193 518.7809 1162.28 458.43745 Z", - fill: "#ff9800", - }, - }), - svg.text([ - svg.textPath( - { attrs: { "xlink:href": "#compound", startOffset: "29%" } }, - [ - svg.tspan( - { - attrs: { - "font-family": '"Roboto", Arial, Helvetica, sans-serif', - "font-size": "60", - "text-anchor": "middle", - "letter-spacing": 15, - "font-weight": "bold", - fill: "#ebebeb", - dy: "-20", - }, - }, - "COMPOUND" - ), - ] - ), - ]), - svg.path({ - attrs: { - d: "M 1162.28 458.43743 L 1086.0539 502.4466 C 1128.6184 576.1705 1189.8393 637.3914 1263.5632 679.9559 C 1495.518 813.8751 1792.1177 734.4015 1926.0369 502.4466 L 1849.8108 458.43743 C 1740.1971 648.2938 1497.4287 713.3434 1307.5724 603.7298 C 1247.229 568.8905 1197.1193 518.7809 1162.28 458.43745 Z", - stroke: "white", - "fill-opacity": "0", - "stroke-linecap": "round", - "stroke-linejoin": "round", - "stroke-width": "6", - }, - }), - ]), - svg.g([ - // DISEASE - PHENO - svg.path({ - attrs: { - d: "M 1506.0454 251.9644 L 1506.0454 -.035595944 C 1645.2211 -.035595944 1758.0454 112.78865 1758.0454 251.9644 C 1758.0454 296.19964 1746.4014 339.65556 1724.2838 377.9644 Z", - fill: "#e92363", - "fill-opacity": ".5", - }, - }), - svg.path({ - attrs: { - d: "M 1506.0454 251.9644 L 1506.0454 -.035595944 C 1645.2211 -.035595944 1758.0454 112.78865 1758.0454 251.9644 C 1758.0454 296.19964 1746.4014 339.65556 1724.2838 377.9644 Z", - stroke: "white", - "stroke-linecap": "round", - "stroke-linejoin": "round", - "stroke-width": "6", - "fill-opacity": "0", - }, - }), - svg.text( - { attrs: { transform: "translate(1529.807 150.726)", fill: "white" } }, - [ - svg.tspan( - { - attrs: { - "font-family": '"Roboto", Arial, Helvetica, sans-serif', - "font-size": "60", - "font-weight": "bold", - fill: "white", - x: ".18359375", - y: "56", - textLength: "198.63281", - }, - }, - "PHENO" - ), - ] - ), - - svg.path({ - attrs: { - d: "M 1506.1296 251.9644 L 1724.368 377.9644 C 1654.78 498.49415 1500.6593 539.7907 1380.1296 470.2028 C 1341.8207 448.0852 1310.0088 416.27324 1287.8912 377.9644 Z", - fill: "#fe9801", - "fill-opacity": ".5", - }, - }), - svg.path({ - attrs: { - d: "M 1506.1296 251.9644 L 1724.368 377.9644 C 1654.78 498.49415 1500.6593 539.7907 1380.1296 470.2028 C 1341.8207 448.0852 1310.0088 416.27324 1287.8912 377.9644 Z", - stroke: "white", - "stroke-linecap": "round", - "stroke-linejoin": "round", - "stroke-width": "6", - "fill-opacity": "0", - }, - }), - // svg.path({ attrs: { d: "M 1506.1296 251.9644 L 1724.368 377.9644 C 1654.78 498.49415 1500.6593 539.7907 1380.1296 470.2028 C 1341.8207 448.0852 1310.0088 416.27324 1287.8912 377.9644 Z", stroke: "white", 'stroke-linecap': "round", 'stroke-linejoin': "round", 'stroke-width': "6" } }), - - svg.path({ - attrs: { - d: "M 1506.0454 251.9644 L 1287.807 377.9644 C 1218.2191 257.43466 1259.5156 103.31388 1380.0454 33.726002 C 1418.3542 11.608383 1461.8101 -.035595944 1506.0454 -.035595944 Z", - fill: "#f44335", - "fill-opacity": ".5", - }, - }), - svg.path({ - attrs: { - d: "M 1506.0454 251.9644 L 1287.807 377.9644 C 1218.2191 257.43466 1259.5156 103.31388 1380.0454 33.726002 C 1418.3542 11.608383 1461.8101 -.035595944 1506.0454 -.035595944 Z", - stroke: "white", - "stroke-linecap": "round", - "stroke-linejoin": "round", - "stroke-width": "6", - "fill-opacity": "0", - }, - }), - - svg.text( - { attrs: { transform: "translate(1309.807 150.726)", fill: "white" } }, - [ - svg.tspan( - { - attrs: { - "font-family": '"Roboto", Arial, Helvetica, sans-serif', - "font-size": "60", - "font-weight": "bold", - fill: "white", - x: ".29589844", - y: "56", - textLength: "158.4082", - }, - }, - "GENO" - ), - ] - ), - - svg.text( - { attrs: { transform: "translate(1406.807 358.726)", fill: "white" } }, - [ - svg.tspan( - { - attrs: { - "font-family": '"Roboto", Arial, Helvetica, sans-serif', - "font-size": "60", - "font-weight": "bold", - fill: "white", - x: ".3076172", - y: "56", - textLength: "209.38477", - }, - }, - "CHEMO" - ), - ] - ), - ]), - ] -) +// const homeSVG = svg( +// { +// attrs: { +// "vertical-align": "baseline", +// height: "30pt", +// width: "30pt", +// viewBox: "1020 -226 972 972", +// }, +// }, +// [ +// svg.a({ attrs: { "xlink:href": "/target" } }, [ +// // TARGET +// svg.path({ +// attrs: { +// id: "target", +// d: "M 1506.0454 -136.98166 L 1506.0454 -225 C 1420.9163 -225 1337.2871 -202.5916 1263.5632 -160.02709 C 1031.6083 -26.107868 952.1347 270.4917 1086.0539 502.4466 L 1162.28 458.43743 C 1052.6664 268.58106 1117.716 25.81266 1307.5724 -83.80097 C 1367.9158 -118.64026 1436.3668 -136.98165 1506.0454 -136.98166 Z", +// fill: "#f44335", +// }, +// }), +// svg.text([ +// svg.textPath( +// { attrs: { "xlink:href": "#target", startOffset: "80%" } }, +// [ +// svg.tspan( +// { +// attrs: { +// "font-family": '"Roboto", Arial, Helvetica, sans-serif', +// "font-size": "60", +// "text-anchor": "middle", +// "letter-spacing": 15, +// "font-weight": "bold", +// fill: "#ebebeb", +// dy: "-20", +// }, +// }, +// "TARGET" +// ), +// ] +// ), +// ]), +// svg.path({ +// attrs: { +// d: "M 1506.0454 -136.98166 L 1506.0454 -225 C 1420.9163 -225 1337.2871 -202.5916 1263.5632 -160.02709 C 1031.6083 -26.107868 952.1347 270.4917 1086.0539 502.4466 L 1162.28 458.43743 C 1052.6664 268.58106 1117.716 25.81266 1307.5724 -83.80097 C 1367.9158 -118.64026 1436.3668 -136.98165 1506.0454 -136.98166 Z", +// stroke: "white", +// "fill-opacity": "0", +// "stroke-linecap": "round", +// "stroke-linejoin": "round", +// "stroke-width": "6", +// }, +// }), +// ]), +// // DISEASE +// svg.a({ attrs: { "xlink:href": "/disease" } }, [ +// svg.path({ +// attrs: { +// id: "disease", +// d: "M 1849.8108 458.43743 L 1926.0369 502.4466 C 1968.6014 428.7227 1991.0098 345.09344 1991.0098 259.9644 C 1991.0098 -7.8740405 1773.8838 -225 1506.0454 -225 L 1506.0454 -136.98166 C 1725.2726 -136.98166 1902.9914 40.73715 1902.9914 259.9644 C 1902.9914 329.643 1884.65 398.094 1849.8108 458.4374 Z", +// fill: "#e91e63", +// }, +// }), +// svg.text([ +// svg.textPath( +// { attrs: { "xlink:href": "#disease", startOffset: "80%" } }, +// [ +// svg.tspan( +// { +// attrs: { +// "font-family": '"Roboto", Arial, Helvetica, sans-serif', +// "font-size": "60", +// "text-anchor": "middle", +// "letter-spacing": 15, +// "font-weight": "bold", +// fill: "#ebebeb", +// dy: "-20", +// }, +// }, +// "DISEASE" +// ), +// ] +// ), +// ]), +// svg.path({ +// attrs: { +// d: "M 1849.8108 458.43743 L 1926.0369 502.4466 C 1968.6014 428.7227 1991.0098 345.09344 1991.0098 259.9644 C 1991.0098 -7.8740405 1773.8838 -225 1506.0454 -225 L 1506.0454 -136.98166 C 1725.2726 -136.98166 1902.9914 40.73715 1902.9914 259.9644 C 1902.9914 329.643 1884.65 398.094 1849.8108 458.4374 Z", +// stroke: "white", +// "fill-opacity": "0", +// "stroke-linecap": "round", +// "stroke-linejoin": "round", +// "stroke-width": "6", +// }, +// }), +// ]), +// // COMPOUND +// svg.a({ attrs: { "xlink:href": "/compound" } }, [ +// svg.path({ +// attrs: { +// id: "compound", +// d: "M 1162.28 458.43743 L 1086.0539 502.4466 C 1128.6184 576.1705 1189.8393 637.3914 1263.5632 679.9559 C 1495.518 813.8751 1792.1177 734.4015 1926.0369 502.4466 L 1849.8108 458.43743 C 1740.1971 648.2938 1497.4287 713.3434 1307.5724 603.7298 C 1247.229 568.8905 1197.1193 518.7809 1162.28 458.43745 Z", +// fill: "#ff9800", +// }, +// }), +// svg.text([ +// svg.textPath( +// { attrs: { "xlink:href": "#compound", startOffset: "29%" } }, +// [ +// svg.tspan( +// { +// attrs: { +// "font-family": '"Roboto", Arial, Helvetica, sans-serif', +// "font-size": "60", +// "text-anchor": "middle", +// "letter-spacing": 15, +// "font-weight": "bold", +// fill: "#ebebeb", +// dy: "-20", +// }, +// }, +// "COMPOUND" +// ), +// ] +// ), +// ]), +// svg.path({ +// attrs: { +// d: "M 1162.28 458.43743 L 1086.0539 502.4466 C 1128.6184 576.1705 1189.8393 637.3914 1263.5632 679.9559 C 1495.518 813.8751 1792.1177 734.4015 1926.0369 502.4466 L 1849.8108 458.43743 C 1740.1971 648.2938 1497.4287 713.3434 1307.5724 603.7298 C 1247.229 568.8905 1197.1193 518.7809 1162.28 458.43745 Z", +// stroke: "white", +// "fill-opacity": "0", +// "stroke-linecap": "round", +// "stroke-linejoin": "round", +// "stroke-width": "6", +// }, +// }), +// ]), +// svg.g([ +// // DISEASE - PHENO +// svg.path({ +// attrs: { +// d: "M 1506.0454 251.9644 L 1506.0454 -.035595944 C 1645.2211 -.035595944 1758.0454 112.78865 1758.0454 251.9644 C 1758.0454 296.19964 1746.4014 339.65556 1724.2838 377.9644 Z", +// fill: "#e92363", +// "fill-opacity": ".5", +// }, +// }), +// svg.path({ +// attrs: { +// d: "M 1506.0454 251.9644 L 1506.0454 -.035595944 C 1645.2211 -.035595944 1758.0454 112.78865 1758.0454 251.9644 C 1758.0454 296.19964 1746.4014 339.65556 1724.2838 377.9644 Z", +// stroke: "white", +// "stroke-linecap": "round", +// "stroke-linejoin": "round", +// "stroke-width": "6", +// "fill-opacity": "0", +// }, +// }), +// svg.text( +// { attrs: { transform: "translate(1529.807 150.726)", fill: "white" } }, +// [ +// svg.tspan( +// { +// attrs: { +// "font-family": '"Roboto", Arial, Helvetica, sans-serif', +// "font-size": "60", +// "font-weight": "bold", +// fill: "white", +// x: ".18359375", +// y: "56", +// textLength: "198.63281", +// }, +// }, +// "PHENO" +// ), +// ] +// ), + +// svg.path({ +// attrs: { +// d: "M 1506.1296 251.9644 L 1724.368 377.9644 C 1654.78 498.49415 1500.6593 539.7907 1380.1296 470.2028 C 1341.8207 448.0852 1310.0088 416.27324 1287.8912 377.9644 Z", +// fill: "#fe9801", +// "fill-opacity": ".5", +// }, +// }), +// svg.path({ +// attrs: { +// d: "M 1506.1296 251.9644 L 1724.368 377.9644 C 1654.78 498.49415 1500.6593 539.7907 1380.1296 470.2028 C 1341.8207 448.0852 1310.0088 416.27324 1287.8912 377.9644 Z", +// stroke: "white", +// "stroke-linecap": "round", +// "stroke-linejoin": "round", +// "stroke-width": "6", +// "fill-opacity": "0", +// }, +// }), +// // svg.path({ attrs: { d: "M 1506.1296 251.9644 L 1724.368 377.9644 C 1654.78 498.49415 1500.6593 539.7907 1380.1296 470.2028 C 1341.8207 448.0852 1310.0088 416.27324 1287.8912 377.9644 Z", stroke: "white", 'stroke-linecap': "round", 'stroke-linejoin': "round", 'stroke-width': "6" } }), + +// svg.path({ +// attrs: { +// d: "M 1506.0454 251.9644 L 1287.807 377.9644 C 1218.2191 257.43466 1259.5156 103.31388 1380.0454 33.726002 C 1418.3542 11.608383 1461.8101 -.035595944 1506.0454 -.035595944 Z", +// fill: "#f44335", +// "fill-opacity": ".5", +// }, +// }), +// svg.path({ +// attrs: { +// d: "M 1506.0454 251.9644 L 1287.807 377.9644 C 1218.2191 257.43466 1259.5156 103.31388 1380.0454 33.726002 C 1418.3542 11.608383 1461.8101 -.035595944 1506.0454 -.035595944 Z", +// stroke: "white", +// "stroke-linecap": "round", +// "stroke-linejoin": "round", +// "stroke-width": "6", +// "fill-opacity": "0", +// }, +// }), + +// svg.text( +// { attrs: { transform: "translate(1309.807 150.726)", fill: "white" } }, +// [ +// svg.tspan( +// { +// attrs: { +// "font-family": '"Roboto", Arial, Helvetica, sans-serif', +// "font-size": "60", +// "font-weight": "bold", +// fill: "white", +// x: ".29589844", +// y: "56", +// textLength: "158.4082", +// }, +// }, +// "GENO" +// ), +// ] +// ), + +// svg.text( +// { attrs: { transform: "translate(1406.807 358.726)", fill: "white" } }, +// [ +// svg.tspan( +// { +// attrs: { +// "font-family": '"Roboto", Arial, Helvetica, sans-serif', +// "font-size": "60", +// "font-weight": "bold", +// fill: "white", +// x: ".3076172", +// y: "56", +// textLength: "209.38477", +// }, +// }, +// "CHEMO" +// ), +// ] +// ), +// ]), +// ] +// ) diff --git a/src/js/main.scss b/src/js/main.scss index 2d34f899..42209a0a 100644 --- a/src/js/main.scss +++ b/src/js/main.scss @@ -1,7 +1,13 @@ -/* Colors for workflows, to be overriden when printed */ +@import 'materialize-css/sass/components/color-variables'; +@import 'materialize-css/sass/components/color-classes'; +@import 'variables'; +@import 'compass_svg'; + +/* Colors for workflows, to be overriden when printed */ html { font-family: 'Roboto'; + color: $color-text; } body { @@ -11,119 +17,149 @@ body { height: 100%; } +div.homePage { + background-size: 25%; + background-position-y: 64px; + // TODO convert svg to base64: https://stackoverflow.com/a/21626701 + background-image: $compass_background_svg; + main li.home-menu span { + font-size: 2rem; + } + } + main { flex: 1 0 auto; } -@import 'materialize-css/sass/components/color-variables'; -@import 'materialize-css/sass/components/color-classes'; -.genetic { - @extend .red; - @extend .lighten-5; - - .WF-header { - @extend .red; - @extend .darken-4; - - i { - @extend .red-text; +// helper to signal which svgs need 'fill' set +$svg-fill: ( + ligand: 'yes', + correlation: 'yes', +); + +@mixin WF-svg($page, $color) { + svg { + stroke: $color; + @if map-has-key($svg-fill, $page) { + fill: $color; + } @else { + fill: $color-header-background; } } } -.compound { - @extend .orange; - @extend .lighten-5; - - .WF-header { - @extend .orange; - @extend .darken-4; - - i { - @extend .orange-text; +/* Styling of pages/workflows */ +@mixin page-WF-style($page) { + $color-name: map-get($colors-WF, $page); + $has-color-specific: map-has-key($colors-WF, #{$page}+"-specific"); + $base-color: color($color-name, "base"); + @if $has-color-specific { + $base-color: map-get($colors-WF, #{$page}+"-specific"); + } + nav a.#{$page} { + color: $base-color; + @include WF-svg($page, $base-color); + } + ul.sidenav a.#{$page} { + color: $base-color; + @include WF-svg($page, $base-color); + } + main div.#{$page} { + background-color: color($color-name, "lighten-5"); + + .WF-header { + background-color: color($color-name, "darken-4"); + + i { + color: $base-color; + &.validated { + color:white; + } + } + } + .validation { + background-color: color($color-name, "lighten-5"); + } + a.home-menu { + color: $base-color; + @include WF-svg($page, $base-color); } } } -.ligand { - @extend .purple; - @extend .lighten-5; +@include page-WF-style("compound"); +@include page-WF-style("genetic"); +@include page-WF-style("ligand"); +@include page-WF-style("disease"); +@include page-WF-style("target"); +@include page-WF-style("correlation"); +@include page-WF-style("generic"); +@include page-WF-style("settings"); - .WF-header { - @extend .purple; - @extend .darken-4; +/* Styling of components */ +nav { + background-color: $color-header-background; - i { - @extend .purple-text; - } + .extraTitle { + display: none; } -} -.ligand { - @extend .purple; - @extend .lighten-5; -} + /* Left logo */ + a.brand-logo.left-logo { + position: initial; + margin: 5px; + } -.disease { - @extend .pink; - @extend .lighten-5; + ul li.active span { + border-bottom-width: medium; + border-bottom-style: solid; + padding-bottom: 0.2em; + } - .WF-header { - @extend .pink; - @extend .darken-4; - - i { - @extend .pink-text; - } + a.brand-logo svg{ + vertical-align: middle; } - .validation { - @extend .pink; - @extend .lighten-5; + img.logo_img { + vertical-align: middle; } -} -.target { - @extend .red; - @extend .lighten-5; + span.version { + color: color('grey', 'darken-3'); + } } -.correlation { - @extend .blue; - @extend .lighten-5; - - .WF-header { - @extend .blue; - @extend .darken-3; +li.home-menu div { + float: left !important; + background-color: transparent; +} - i { - @extend .blue-text; - } +.sampleSelection { + th { + color: $color-text; } - - .validation { - @extend .blue; - @extend .lighten-3; + td.sampleSelected { + color: $color-text; } -} - -.generic { - @extend .green; - @extend .lighten-5; - .WF-header { - @extend .green; - @extend .darken-4; - - i { - @extend .green-text; - } + td.sampleDeselected{ + color: color("grey", "lighten-2") } } -// general style for query 'go' button is that when it is validated that it becomes white -.WF-header i.validated { - @extend .white-text; +li.sampleInfo { + .entryKey { + color: color('grey', 'darken-1'); + } + .sampleHeader { + color: color('grey', 'base'); + } + .treatmentHeader { + color: color('grey', 'base'); + } + .filterHeader { + color: color('grey', 'base'); + } } img.trt_img { @@ -133,18 +169,9 @@ img.trt_img { width: fill-available; } -nav ul li.active span { - border-bottom-width: medium; - border-bottom-style: solid; - padding-bottom: 0.2em; -} - -nav a.brand-logo svg{ - vertical-align: middle; -} - -nav img.logo_img { - vertical-align: middle; +.page-footer { + background-color: $color-footer-background; + color: $color-footer-text; } /* home svg styling and hover */ @@ -195,3 +222,5 @@ div #corrplot { font-size: 9pt } } + +@import 'main_custom'; diff --git a/src/js/pages/home.js b/src/js/pages/home.js index d7c41740..8cfc9f89 100644 --- a/src/js/pages/home.js +++ b/src/js/pages/home.js @@ -4,7 +4,6 @@ import { merge, prop, equals } from 'ramda'; import { Check } from '../components/Check' import dropRepeats from 'xstream/extra/dropRepeats' -import { logoSVG } from '../index' import { compoundSVG, targetSVG, ligandSVG, diseaseSVG, correlationSVG, settingsSVG } from '../svg' const appear = { @@ -23,40 +22,46 @@ function Home(sources) { .map(state => merge(state.settings.form, state.settings.api)) const CheckSink = Check(merge(sources, { props: checkProps$ })) - const makeLink = (path, label, options) => { - return li([a(options, { props: { href: path } }, label)]) + const makeLink = (path, label, selector) => { + return li(".home-menu .row", + div(selector, + [ + a(".home-menu", { props: { href: path } }, label), + span(".extraText", + span(".tooltiptext") + ) + ] + ) + ) } const vdom$ = xs.combine(CheckSink.DOM) .map(([check]) => div([ - div({ style: { 'z-index': -1, height: '100%', overflow: 'hidden', position: 'absolute', opacity: 0.08, 'text-align': 'center', width: '100%' } }, - Array(60).fill().map(_ => div({ style: { width: '25%', display: 'inline-block' } }, [logoSVG]))), + // div({ style: { 'z-index': -1, height: '100%', overflow: 'hidden', position: 'absolute', opacity: 0.08, 'text-align': 'center', width: '100%' } }, + // Array(60).fill().map(_ => div({ style: { width: '25%', display: 'inline-block' } }, [logoSVG]))), div('.row .transparent', [ - h2('.col .l6 .m8 .s10 offset-l3 .offset-m2 .offset-s1', { style: { 'vertical-align': 'top' } }, [ - 'Welcome to ComPass', + h2('.title .col .l6 .m8 .s10 offset-l3 .offset-m2 .offset-s1', { style: { 'vertical-align': 'top' } }, [ + span('.main', 'Welcome to ComPass'), // div({ style: { display: 'inline-block', width: '200px', 'vertical-align': '-40%' } }, [logoSVG]), - ' ', - check + span('.spacer',' '), + span('.check', check), ]), - p('.col .l6 .m8 .s10 offset-l3 .offset-m2 .offset-s1 .flow-text', [ + p('.introduction .col .l6 .m8 .s10 offset-l3 .offset-m2 .offset-s1 .flow-text', [ 'This application is the interface with L1000 data.' ]), div('.row', []), div('.col .l6 .m8 .s10 offset-l3 .offset-m2 .offset-s1 .center-align', appear, [ ul('.left', [ - makeLink('/compound', span({ style: { fontSize: "2rem" } }, ['Compound', ' ', compoundSVG]), '.orange-text .left'), - // makeLink('/target', span(['Target', ' ', targetSVG]), '.red-text'), - makeLink('/genetic', span({ style: { fontSize: "2rem" } }, ['Genetic', ' ', targetSVG]), '.red-text .left'), - makeLink('/ligand', span({ style: { fontSize: "2rem" } }, ['Ligand', ' ', ligandSVG]), '.purple-text .left'), - //makeLink('/generic', span({ style: { fontSize: "2rem" } }, ['Ligand', ' ', ligandSVG]), '.purple-text .left'), - makeLink('/disease', span({ style: { fontSize: "2rem" } }, ['Disease', ' ', diseaseSVG]), '.pink-text .left'), - makeLink('/correlation', span({ style: { fontSize: "2rem" } }, ['Correlation', ' ', correlationSVG]), '.blue-text .left'), - makeLink('/settings', span({ style: { fontSize: "2rem" } }, ['Settings', ' ', settingsSVG]), '.grey-text .left'), - // makeLink('/admin', span(['Admin']), '.blue-text'), + makeLink('/compound', span(['Compound', ' ', compoundSVG]), '.compound'), + makeLink('/genetic', span(['Genetic', ' ', targetSVG]), '.genetic'), + makeLink('/ligand', span(['Ligand', ' ', ligandSVG]), '.ligand'), + makeLink('/disease', span(['Disease', ' ', diseaseSVG]), '.disease'), + makeLink('/correlation', span(['Correlation', ' ', correlationSVG]), '.correlation'), + makeLink('/settings', span(['Settings', ' ', settingsSVG]), '.settings'), ]), ]), div('.row', []), - p('.col .l6 .m8 .s10 offset-l3 .offset-m2 .offset-s1 .flow-text', [ + p('.afterword .col .l6 .m8 .s10 offset-l3 .offset-m2 .offset-s1 .flow-text', [ 'You can click on one of the workflows above to start it.', ' Alternatively, you can initiate ghost mode in the settings.' ]), diff --git a/src/js/svg.js b/src/js/svg.js index c486644e..c46b4750 100644 --- a/src/js/svg.js +++ b/src/js/svg.js @@ -3,50 +3,50 @@ import { svg } from '@cycle/dom' export const compoundSVG = svg({ attrs: { height: '16pt', viewBox: '0 0 20 30' } }, [ svg.g([ - svg.path({ attrs: { d: 'M17,2L17,2c-2.1,0-3,0.5-3,3v7h-4V5c0-2.5-0.9-3-3-3l0,0L6,2v2h1c0.7,0,1,0.4,1,1v12c0,5.2,4,5,4,5s4,0.2,4-5V5c0-0.6,0.3-1,1-1h1V2L17,2z', stroke: '#ff9800' } }) + svg.path({ attrs: { d: 'M17,2L17,2c-2.1,0-3,0.5-3,3v7h-4V5c0-2.5-0.9-3-3-3l0,0L6,2v2h1c0.7,0,1,0.4,1,1v12c0,5.2,4,5,4,5s4,0.2,4-5V5c0-0.6,0.3-1,1-1h1V2L17,2z' } }) ]) ]) export const targetSVG = svg({ attrs: { height: '18pt', viewBox: '0 0 20 30' } }, [ svg.g([ - svg.path({ attrs: { stroke: '#f44335', d: 'M22.7,9.7l-1.4-1.4c-0.4,0.4-0.8,0.8-1.2,1.1l-5.4-5.4c0.3-0.4,0.7-0.8,1.1-1.2l-1.4-1.4c-3.4,3.4-3.6,6.7-3.4,9.6c-3-0.2-6.3,0-9.6,3.4l1.4,1.4c0.4-0.4,0.8-0.8,1.2-1.1l5.4,5.4c-0.3,0.4-0.7,0.8-1.1,1.2l1.4,1.4c3.4-3.4,3.5-6.6,3.4-9.6C16,13.3,19.3,13.1,22.7,9.7z M19.2,9.9c-0.9,0.5-1.7,0.8-2.6,1l-3.6-3.6c0.2-0.9,0.5-1.7,1-2.6L19.2,9.9z M11.1,12.9c0.1,0.8,0.1,1.6,0,2.4l-2.5-2.5C9.4,12.9,10.2,12.9,11.1,12.9z M4.8,14.1c0.9-0.5,1.7-0.8,2.6-1l3.6,3.6c-0.2,0.9-0.5,1.7-1,2.6L4.8,14.1z M12.9,11.1c-0.1-0.8-0.1-1.7,0-2.5l2.5,2.5C14.6,11.2,13.8,11.1,12.9,11.1z' } }) + svg.path({ attrs: { d: 'M22.7,9.7l-1.4-1.4c-0.4,0.4-0.8,0.8-1.2,1.1l-5.4-5.4c0.3-0.4,0.7-0.8,1.1-1.2l-1.4-1.4c-3.4,3.4-3.6,6.7-3.4,9.6c-3-0.2-6.3,0-9.6,3.4l1.4,1.4c0.4-0.4,0.8-0.8,1.2-1.1l5.4,5.4c-0.3,0.4-0.7,0.8-1.1,1.2l1.4,1.4c3.4-3.4,3.5-6.6,3.4-9.6C16,13.3,19.3,13.1,22.7,9.7z M19.2,9.9c-0.9,0.5-1.7,0.8-2.6,1l-3.6-3.6c0.2-0.9,0.5-1.7,1-2.6L19.2,9.9z M11.1,12.9c0.1,0.8,0.1,1.6,0,2.4l-2.5-2.5C9.4,12.9,10.2,12.9,11.1,12.9z M4.8,14.1c0.9-0.5,1.7-0.8,2.6-1l3.6,3.6c-0.2,0.9-0.5,1.7-1,2.6L4.8,14.1z M12.9,11.1c-0.1-0.8-0.1-1.7,0-2.5l2.5,2.5C14.6,11.2,13.8,11.1,12.9,11.1z' } }) ]) ]) export const ligandSVG = svg({ attrs: { height: '18pt', viewBox: '15 4 18 16' } }, [ svg.g([ - svg.rect({attrs: { stroke:'#9c27b0', fill:'#9c27b0', x:'19.5', y:'12.5', width:'1', height:'4'}}), + svg.rect({attrs: { x:'19.5', y:'12.5', width:'1', height:'4'}}), // svg.path({ attrs: { stroke:'#000000', fill: '#000000', d:'M23.6,9.7c-0.15,1.86-1.7,3.32-3.6,3.32c-1.9,0-3.45-1.46-3.6-3.32l1.42,0C17.97,10.77,18.9,11.6,20.0,11.6 c1.1,0,2.03-0.83,2.17-1.91L23.6,9.7z'}}), - svg.path({ attrs: { stroke:'#9c27b0', fill: '#9c27b0', d:'M23.6,9.7 c0,1.9-1.7,3.3-3.6,3.3 c-1.9,0-3.5-1.5-3.6-3.3 l1.5,0 C18,10.8,18.9,11.6,20,11.6 c1.1,0,2-0.8,2.2-1.9 L23.6,9.7z'}}), - svg.circle({attrs: { stroke: '#9c27b0', fill:'#9c27b0', cx:'20', cy:'9.2', r:'0.8'}}), + svg.path({ attrs: { d:'M23.6,9.7 c0,1.9-1.7,3.3-3.6,3.3 c-1.9,0-3.5-1.5-3.6-3.3 l1.5,0 C18,10.8,18.9,11.6,20,11.6 c1.1,0,2-0.8,2.2-1.9 L23.6,9.7z'}}), + svg.circle({attrs: { cx:'20', cy:'9.2', r:'0.8'}}), ]) ]) export const diseaseSVG = svg({ attrs: { height: '16pt', viewBox: "0 0 13.421 13.421" } }, [ svg.g([ - svg.path({ attrs: { stroke: '#e91e63', d: 'M12.843,8.669c-0.907-0.542-2.619-0.031-4.944,1.476c-1.507,0.977-2.618,1.269-3.215,0.846c-0.871-0.616-0.709-2.674-0.498-3.862h0.179c0.122,0,0.222-0.099,0.222-0.223V6.467C5.192,6,7.47,4.183,7.786,3.013c0.361-1.327-1.672-2.181-2.264-2.399c0.01-0.127-0.061-0.249-0.188-0.29l-0.22-0.072C4.967,0.204,4.809,0.285,4.76,0.433L4.63,0.831C4.582,0.978,4.663,1.138,4.811,1.185l0.222,0.072c0.08,0.026,0.161,0.011,0.228-0.028c0.742,0.266,2.06,0.957,1.884,1.609C6.907,3.713,4.932,5.367,4.121,5.984H3.71C2.9,5.366,0.924,3.713,0.687,2.838c-0.175-0.645,1.116-1.329,1.86-1.602c0.069,0.056,0.159,0.084,0.25,0.058l0.225-0.061c0.149-0.041,0.237-0.195,0.195-0.345l-0.11-0.404c-0.042-0.15-0.196-0.238-0.346-0.196l-0.225,0.06C2.416,0.381,2.34,0.487,2.333,0.604c-0.553,0.2-2.657,1.06-2.291,2.409C0.348,4.14,2.475,5.869,3.17,6.411v0.495c0,0.124,0.099,0.223,0.222,0.223h0.116c-0.205,1.189-0.429,3.543,0.79,4.406c0.296,0.212,0.646,0.316,1.051,0.316c0.767,0,1.731-0.381,2.913-1.146c2.975-1.928,3.998-1.606,4.241-1.462c0.187,0.11,0.27,0.305,0.249,0.576c-0.03,0.38-0.284,0.863-0.708,1.177c-0.249-0.324-0.641-0.535-1.08-0.535c-0.751,0-1.36,0.611-1.36,1.361s0.609,1.361,1.36,1.361s1.361-0.611,1.361-1.361c0-0.067-0.007-0.133-0.016-0.198c0.608-0.394,1.053-1.076,1.106-1.753C13.456,9.348,13.247,8.909,12.843,8.669z M10.963,12.517c-0.383,0-0.694-0.313-0.694-0.695s0.312-0.694,0.694-0.694c0.216,0,0.405,0.101,0.533,0.255c0.14,0.115,0.106,0.252,0.159,0.431c0,0.003,0.003,0.005,0.003,0.009C11.659,12.204,11.347,12.517,10.963,12.517z' } }) + svg.path({ attrs: { d: 'M12.843,8.669c-0.907-0.542-2.619-0.031-4.944,1.476c-1.507,0.977-2.618,1.269-3.215,0.846c-0.871-0.616-0.709-2.674-0.498-3.862h0.179c0.122,0,0.222-0.099,0.222-0.223V6.467C5.192,6,7.47,4.183,7.786,3.013c0.361-1.327-1.672-2.181-2.264-2.399c0.01-0.127-0.061-0.249-0.188-0.29l-0.22-0.072C4.967,0.204,4.809,0.285,4.76,0.433L4.63,0.831C4.582,0.978,4.663,1.138,4.811,1.185l0.222,0.072c0.08,0.026,0.161,0.011,0.228-0.028c0.742,0.266,2.06,0.957,1.884,1.609C6.907,3.713,4.932,5.367,4.121,5.984H3.71C2.9,5.366,0.924,3.713,0.687,2.838c-0.175-0.645,1.116-1.329,1.86-1.602c0.069,0.056,0.159,0.084,0.25,0.058l0.225-0.061c0.149-0.041,0.237-0.195,0.195-0.345l-0.11-0.404c-0.042-0.15-0.196-0.238-0.346-0.196l-0.225,0.06C2.416,0.381,2.34,0.487,2.333,0.604c-0.553,0.2-2.657,1.06-2.291,2.409C0.348,4.14,2.475,5.869,3.17,6.411v0.495c0,0.124,0.099,0.223,0.222,0.223h0.116c-0.205,1.189-0.429,3.543,0.79,4.406c0.296,0.212,0.646,0.316,1.051,0.316c0.767,0,1.731-0.381,2.913-1.146c2.975-1.928,3.998-1.606,4.241-1.462c0.187,0.11,0.27,0.305,0.249,0.576c-0.03,0.38-0.284,0.863-0.708,1.177c-0.249-0.324-0.641-0.535-1.08-0.535c-0.751,0-1.36,0.611-1.36,1.361s0.609,1.361,1.36,1.361s1.361-0.611,1.361-1.361c0-0.067-0.007-0.133-0.016-0.198c0.608-0.394,1.053-1.076,1.106-1.753C13.456,9.348,13.247,8.909,12.843,8.669z M10.963,12.517c-0.383,0-0.694-0.313-0.694-0.695s0.312-0.694,0.694-0.694c0.216,0,0.405,0.101,0.533,0.255c0.14,0.115,0.106,0.252,0.159,0.431c0,0.003,0.003,0.005,0.003,0.009C11.659,12.204,11.347,12.517,10.963,12.517z' } }) ]) ]) export const correlationSVG = svg({ attrs: { height: '16pt', viewBox: '0 0 48 48', enableBackground:'new 0 0 48 48' } }, [ - svg.polygon({attrs: {points:[9,39, 9,6, 7,6, 7,41, 42,41, 42,39], stroke:'#2196F3', fill:'#2196F3'}}), + svg.polygon({attrs: {points:[9,39, 9,6, 7,6, 7,41, 42,41, 42,39]}}), svg.g([ - svg.circle({ attrs: { cx: '14', cy:'11', r:'2', fill:'#2196F3' } }), - svg.circle({ attrs: { cx: '32', cy:'11', r:'2', fill:'#2196F3' } }), - svg.circle({ attrs: { cx: '39', cy:'11', r:'2', fill:'#2196F3' } }), - svg.circle({ attrs: { cx: '23', cy:'11', r:'4', fill:'#2196F3' } }), - svg.circle({ attrs: { cx: '14', cy:'33', r:'2', fill:'#2196F3' } }), - svg.circle({ attrs: { cx: '30', cy:'33', r:'2', fill:'#2196F3' } }), - svg.circle({ attrs: { cx: '22', cy:'33', r:'3', fill:'#2196F3' } }), - svg.circle({ attrs: { cx: '39', cy:'33', r:'4', fill:'#2196F3' } }), - svg.circle({ attrs: { cx: '14', cy:'22', r:'2', fill:'#2196F3' } }), - svg.circle({ attrs: { cx: '39', cy:'22', r:'2', fill:'#2196F3' } }), - svg.circle({ attrs: { cx: '32', cy:'22', r:'3', fill:'#2196F3' } }), + svg.circle({ attrs: { cx: '14', cy:'11', r:'2' } }), + svg.circle({ attrs: { cx: '32', cy:'11', r:'2' } }), + svg.circle({ attrs: { cx: '39', cy:'11', r:'2' } }), + svg.circle({ attrs: { cx: '23', cy:'11', r:'4' } }), + svg.circle({ attrs: { cx: '14', cy:'33', r:'2' } }), + svg.circle({ attrs: { cx: '30', cy:'33', r:'2' } }), + svg.circle({ attrs: { cx: '22', cy:'33', r:'3' } }), + svg.circle({ attrs: { cx: '39', cy:'33', r:'4' } }), + svg.circle({ attrs: { cx: '14', cy:'22', r:'2' } }), + svg.circle({ attrs: { cx: '39', cy:'22', r:'2' } }), + svg.circle({ attrs: { cx: '32', cy:'22', r:'3' } }), ]) ]) export const settingsSVG = svg({ attrs: { height: '18pt', viewBox: "10 5 34 34" } }, [ svg.g([ - svg.path({ attrs: { stroke: 'grey', d: 'M31.92529,22.74756l-2.11786-0.23932c-0.13806-0.56036-0.35303-1.08813-0.6424-1.5777 l1.31934-1.65271c0.31543-0.39502,0.28467-0.96387-0.07129-1.32324l-0.3667-0.36963 c-0.3584-0.36084-0.92773-0.39746-1.32764-0.0791l-1.64514,1.30609c-0.51428-0.31042-1.04895-0.55225-1.60339-0.68939 l-0.22852-2.04688c-0.05615-0.50488-0.48193-0.88574-0.99023-0.88574H23.7373c-0.50781,0-0.93359,0.38037-0.98975,0.88477 l-0.22705,2.01465c-0.60547,0.14062-1.16748,0.36572-1.67676,0.67139l-1.56299-1.25195 c-0.39795-0.31836-0.96826-0.28662-1.32764,0.07275l-0.37207,0.37207c-0.35889,0.35938-0.39062,0.9292-0.07373,1.32617 l1.23047,1.54004c-0.3335,0.53223-0.57666,1.10547-0.7251,1.71045l-1.93848,0.21729 c-0.50391,0.05713-0.88428,0.48291-0.88428,0.99023v0.51416c0,0.50732,0.38037,0.93311,0.88477,0.99023l1.9375,0.21729 c0.15039,0.62012,0.39062,1.19727,0.71631,1.72021l-1.22314,1.53955c-0.31543,0.39844-0.28174,0.96875,0.0791,1.32666 l0.37012,0.36719c0.19336,0.19141,0.44727,0.28906,0.70166,0.28906c0.21924,0,0.43896-0.07227,0.62158-0.21826l1.54688-1.23584 c0.52277,0.31171,1.09503,0.52338,1.69385,0.66364l0.22852,2.0141c0.05713,0.50391,0.48291,0.88428,0.98975,0.88428h0.51514 c0.50732,0,0.93311-0.38037,0.99023-0.88525l0.2301-2.05066c0.57428-0.14661,1.1181-0.3772,1.62494-0.6897l1.6225,1.29749 c0.18311,0.14648,0.40283,0.21826,0.62207,0.21826c0.25586,0,0.51074-0.09814,0.70459-0.29199l0.36377-0.36377 c0.35742-0.35791,0.39014-0.92725,0.0752-1.32373l-1.31097-1.65137c0.29071-0.49188,0.50586-1.02307,0.64325-1.58789 l2.10815-0.23877c0.50391-0.05713,0.88428-0.48291,0.88428-0.98975V23.7373 C32.81006,23.22949,32.42969,22.80371,31.92529,22.74756z M27.03174,24.00879c0,1.71094-1.39209,3.10303-3.10303,3.10303 c-1.71533,0-3.11084-1.39209-3.11084-3.10303c0-1.71533,1.39551-3.11084,3.11084-3.11084 C25.63965,20.89795,27.03174,22.29346,27.03174,24.00879z' } }) + svg.path({ attrs: { d: 'M31.92529,22.74756l-2.11786-0.23932c-0.13806-0.56036-0.35303-1.08813-0.6424-1.5777 l1.31934-1.65271c0.31543-0.39502,0.28467-0.96387-0.07129-1.32324l-0.3667-0.36963 c-0.3584-0.36084-0.92773-0.39746-1.32764-0.0791l-1.64514,1.30609c-0.51428-0.31042-1.04895-0.55225-1.60339-0.68939 l-0.22852-2.04688c-0.05615-0.50488-0.48193-0.88574-0.99023-0.88574H23.7373c-0.50781,0-0.93359,0.38037-0.98975,0.88477 l-0.22705,2.01465c-0.60547,0.14062-1.16748,0.36572-1.67676,0.67139l-1.56299-1.25195 c-0.39795-0.31836-0.96826-0.28662-1.32764,0.07275l-0.37207,0.37207c-0.35889,0.35938-0.39062,0.9292-0.07373,1.32617 l1.23047,1.54004c-0.3335,0.53223-0.57666,1.10547-0.7251,1.71045l-1.93848,0.21729 c-0.50391,0.05713-0.88428,0.48291-0.88428,0.99023v0.51416c0,0.50732,0.38037,0.93311,0.88477,0.99023l1.9375,0.21729 c0.15039,0.62012,0.39062,1.19727,0.71631,1.72021l-1.22314,1.53955c-0.31543,0.39844-0.28174,0.96875,0.0791,1.32666 l0.37012,0.36719c0.19336,0.19141,0.44727,0.28906,0.70166,0.28906c0.21924,0,0.43896-0.07227,0.62158-0.21826l1.54688-1.23584 c0.52277,0.31171,1.09503,0.52338,1.69385,0.66364l0.22852,2.0141c0.05713,0.50391,0.48291,0.88428,0.98975,0.88428h0.51514 c0.50732,0,0.93311-0.38037,0.99023-0.88525l0.2301-2.05066c0.57428-0.14661,1.1181-0.3772,1.62494-0.6897l1.6225,1.29749 c0.18311,0.14648,0.40283,0.21826,0.62207,0.21826c0.25586,0,0.51074-0.09814,0.70459-0.29199l0.36377-0.36377 c0.35742-0.35791,0.39014-0.92725,0.0752-1.32373l-1.31097-1.65137c0.29071-0.49188,0.50586-1.02307,0.64325-1.58789 l2.10815-0.23877c0.50391-0.05713,0.88428-0.48291,0.88428-0.98975V23.7373 C32.81006,23.22949,32.42969,22.80371,31.92529,22.74756z M27.03174,24.00879c0,1.71094-1.39209,3.10303-3.10303,3.10303 c-1.71533,0-3.11084-1.39209-3.11084-3.10303c0-1.71533,1.39551-3.11084,3.11084-3.11084 C25.63965,20.89795,27.03174,22.29346,27.03174,24.00879z' } }) ]) ]) From 107d8dc567c9e6049e3ba92a70822b477da5b564 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 19 Jan 2022 10:47:20 +0100 Subject: [PATCH 089/191] Gskcmp 81 add dirty logic to disease workflow (#111) * Copy dirty logic from GenericTreatment to Disease WF Form part still WIP * Move query logic to model, add dirty UI logic in main form * Rework Disease WF form to use state.core instead of state.form This is now in line with the rest of the code & allows reusing common code Correctly abstracts the components from the rest of the code & only use the Lens to make the translation * Change correlation WF form to use core instead of form for its onionified state Changes needed because it shares the validation component with the disease WF * Cleanup of main form UI code and add comment --- src/js/components/CorrelationForm.js | 44 +++++++------- src/js/components/SignatureCheck.js | 12 ++-- src/js/components/SignatureForm.js | 86 +++++++++++++++++----------- src/js/pages/disease.js | 26 ++++++++- src/js/pages/genericTreatment.js | 2 +- 5 files changed, 105 insertions(+), 65 deletions(-) diff --git a/src/js/components/CorrelationForm.js b/src/js/components/CorrelationForm.js index 2afb796d..e540b7d6 100644 --- a/src/js/components/CorrelationForm.js +++ b/src/js/components/CorrelationForm.js @@ -21,8 +21,8 @@ const stateTemplate = { // Granular access to global state and parts of settings const formLens = { - get: state => ({ form: state.form, settings: { form: state.settings.form, api: state.settings.api } }), - set: (state, childState) => ({ ...state, form: childState.form }) + get: state => ({ core: state.form, settings: { form: state.settings.form, api: state.settings.api } }), + set: (state, childState) => ({ ...state, form: childState.core }) }; function CorrelationForm(sources) { @@ -36,8 +36,8 @@ function CorrelationForm(sources) { const signatureCheck2 = isolate(SignatureCheck, { onion: checkLens2 })(sources) // Valid query? - const validated1$ = state$.map(state => state.form.validated1) - const validated2$ = state$.map(state => state.form.validated2) + const validated1$ = state$.map(state => state.core.validated1) + const validated2$ = state$.map(state => state.core.validated2) const vdom1$ = xs.combine(state$) .map(([state, check1dom, check2dom, validated1, validated2]) => { @@ -47,8 +47,8 @@ function CorrelationForm(sources) { const vdom$ = xs.combine(state$, signatureCheck1.DOM, signatureCheck2.DOM, validated1$, validated2$) .map( ([state, checkdom1, checkdom2, validated1, validated2]) => { - const query1 = state.form.query1 - const query2 = state.form.query2 + const query1 = state.core.query1 + const query2 = state.core.query2 return div( [ div('.row .WF-header .white-text', { style: { margin: '0px', padding: '20px 10px 10px 10px' } }, [ @@ -82,12 +82,12 @@ function CorrelationForm(sources) { const newQuery1$ = xs.merge( sources.DOM.select('.Query1').events('input').map(ev => ev.target.value), // Ghost - state$.map(state => state.form.query1).compose(dropRepeats()) + state$.map(state => state.core.query1).compose(dropRepeats()) ) const newQuery2$ = xs.merge( sources.DOM.select('.Query2').events('input').map(ev => ev.target.value), // Ghost - state$.map(state => state.form.query2).compose(dropRepeats()) + state$.map(state => state.core.query2).compose(dropRepeats()) ) // Updated state is propagated and picked up by the necessary components @@ -100,15 +100,15 @@ function CorrelationForm(sources) { const setDefault1$ = sources.DOM.select('.Default1').events('click') const setDefaultReducer1$ = setDefault1$.map(events => prevState => { let newState = clone(prevState) - newState.form.query1 = 'ENSG00000012048 -WRONG HSPA1A DNAJB1 DDIT4 -TSEN2' - newState.form.validated1 = false + newState.core.query1 = 'ENSG00000012048 -WRONG HSPA1A DNAJB1 DDIT4 -TSEN2' + newState.core.validated1 = false return newState }) const setDefault2$ = sources.DOM.select('.Default2').events('click') const setDefaultReducer2$ = setDefault2$.map(events => prevState => { let newState = clone(prevState) - newState.form.query2 = 'ENSG00000012048 -WRONG HSPA1A DNAJB1 DDIT4 -TSEN2' - newState.form.validated2 = false + newState.core.query2 = 'ENSG00000012048 -WRONG HSPA1A DNAJB1 DDIT4 -TSEN2' + newState.core.validated2 = false return newState }) @@ -118,7 +118,7 @@ function CorrelationForm(sources) { if (typeof prevState === 'undefined') { // Settings are handled higher up, but in case we use this component standalone, ... return { - form: { + core: { query1: '', query2: '', validated1: false, @@ -129,7 +129,7 @@ function CorrelationForm(sources) { } else { return ({ ...prevState, - form: { + core: { query1: '', query2: '', validated1: false, @@ -143,13 +143,13 @@ function CorrelationForm(sources) { const query1Reducer$ = newQuery1$.map(query => prevState => { // Signatureform -- queryReducer let newState = clone(prevState) - newState.form.query1 = query + newState.core.query1 = query return newState }) const query2Reducer$ = newQuery2$.map(query => prevState => { // Signatureform -- queryReducer let newState = clone(prevState) - newState.form.query2 = query + newState.core.query2 = query return newState }) @@ -157,13 +157,13 @@ function CorrelationForm(sources) { const invalidateReducer1$ = newQuery1$.map(query => prevState => { // Signatureform -- invalidateReducer let newState = clone(prevState) - newState.form.validated1 = false + newState.core.validated1 = false return newState }) const invalidateReducer2$ = newQuery2$.map(query => prevState => { // Signatureform -- invalidateReducer let newState = clone(prevState) - newState.form.validated2 = false + newState.core.validated2 = false return newState }) @@ -172,14 +172,14 @@ function CorrelationForm(sources) { .map(signal => prevState => { // Signatureform -- validateReducer let newState = clone(prevState) - newState.form.validated1 = true + newState.core.validated1 = true return newState }) const validateReducer2$ = signatureCheck2.validated .map(signal => prevState => { // Signatureform -- validateReducer let newState = clone(prevState) - newState.form.validated2 = true + newState.core.validated2 = true return newState }) @@ -192,10 +192,10 @@ function CorrelationForm(sources) { const query$ = xs.merge( update$, // Ghost mode - sources.onion.state$.map(state => state.form.ghost).filter(ghost => ghost).compose(dropRepeats()) + sources.onion.state$.map(state => state.core.ghost).filter(ghost => ghost).compose(dropRepeats()) ) .compose(sampleCombine(state$)) - .map(([update, state]) => ({query1: state.form.query1, query2: state.form.query2})) + .map(([update, state]) => ({query1: state.core.query1, query2: state.core.query2})) .remember() return { diff --git a/src/js/components/SignatureCheck.js b/src/js/components/SignatureCheck.js index 5cff93b7..22b2ae28 100644 --- a/src/js/components/SignatureCheck.js +++ b/src/js/components/SignatureCheck.js @@ -24,18 +24,18 @@ const stateTemplate = { } const checkLens = { - get: state => ({query: state.form.query, settings: state.settings}), - set: (state, childState) => ({...state, form : {...state.form, query: childState.query}}) + get: state => ({query: state.core.query, settings: state.settings}), + set: (state, childState) => ({...state, core : {...state.core, query: childState.query}}) }; const checkLens1 = { - get: state => ({query: state.form.query1, settings: state.settings}), - set: (state, childState) => ({...state, form : {...state.form, query1: childState.query}}) + get: state => ({query: state.core.query1, settings: state.settings}), + set: (state, childState) => ({...state, core : {...state.core, query1: childState.query}}) }; const checkLens2 = { - get: state => ({query: state.form.query2, settings: state.settings}), - set: (state, childState) => ({...state, form : {...state.form, query2: childState.query}}) + get: state => ({query: state.core.query2, settings: state.settings}), + set: (state, childState) => ({...state, core : {...state.core, query2: childState.query}}) }; function SignatureCheck(sources) { diff --git a/src/js/components/SignatureForm.js b/src/js/components/SignatureForm.js index f95c250a..c2c945cf 100644 --- a/src/js/components/SignatureForm.js +++ b/src/js/components/SignatureForm.js @@ -8,14 +8,23 @@ import { ENTER_KEYCODE } from '../utils/keycodes.js' import { SignatureCheck, checkLens } from '../components/SignatureCheck' import dropRepeats from 'xstream/extra/dropRepeats' import { loggerFactory } from '../utils/logger' +import { dirtyUiReducer } from '../utils/ui' // Granular access to global state and parts of settings const formLens = { - get: state => ({ form: state.form, settings: { form: state.settings.form, api: state.settings.api, common: state.settings.common } }), - set: (state, childState) => ({ ...state, form: childState.form }) + get: (state) => ({ + core: state.form, + settings: { + form: state.settings.form, + api: state.settings.api, + common: state.settings.common, + }, + ui: state.ui?.form ?? {}, + }), + set: (state, childState) => ({ ...state, form: {...childState.core } }), } -function model(newQuery$, state$, sources, signatureCheckSink) { +function model(newQuery$, state$, sources, signatureCheckSink, actions$) { // Set a default signature for demo purposes const setDefault$ = sources.DOM.select('.Default').events('click') @@ -24,8 +33,8 @@ function model(newQuery$, state$, sources, signatureCheckSink) { .compose(sampleCombine(state$)) .map(([_, state]) => prevState => { let newState = clone(prevState) - newState.form.query = state.settings.common.example.signature //'ENSG00000012048 -WRONG HSPA1A DNAJB1 DDIT4 -TSEN2' - newState.form.validated = false + newState.core.query = state.settings.common.example.signature //'ENSG00000012048 -WRONG HSPA1A DNAJB1 DDIT4 -TSEN2' + newState.core.validated = false return newState }) @@ -35,7 +44,7 @@ function model(newQuery$, state$, sources, signatureCheckSink) { if (typeof prevState === 'undefined') { // Settings are handled higher up, but in case we use this component standalone, ... return { - form: { + core: { query: '', validated: false }, @@ -44,7 +53,7 @@ function model(newQuery$, state$, sources, signatureCheckSink) { } else { return ({ ...prevState, - form: { + core: { query: '', validated: false }, @@ -56,7 +65,7 @@ function model(newQuery$, state$, sources, signatureCheckSink) { const queryReducer$ = newQuery$.map(query => prevState => { // Signatureform -- queryReducer let newState = clone(prevState) - newState.form.query = query + newState.core.query = query return newState }) @@ -64,7 +73,7 @@ function model(newQuery$, state$, sources, signatureCheckSink) { const invalidateReducer$ = newQuery$.map(_ => prevState => { // Signatureform -- invalidateReducer let newState = clone(prevState) - newState.form.validated = false + newState.core.validated = false return newState }) @@ -73,32 +82,50 @@ function model(newQuery$, state$, sources, signatureCheckSink) { .map(_ => prevState => { // Signatureform -- validateReducer let newState = clone(prevState) - newState.form.validated = true + newState.core.validated = true return newState }) // When update is clicked, update the query. Onionify does the rest const childReducer$ = signatureCheckSink.onion - return xs.merge( - defaultReducer$, - setDefaultReducer$, - queryReducer$, - invalidateReducer$, - childReducer$, - validateReducer$, - ) + // When GO clicked or enter -> send updated 'value' to sink + // Maybe catch when no valid query? + const query$ = xs.merge( + actions$.update$, + // Ghost mode + sources.onion.state$.map(state => state.core.ghost).filter(ghost => ghost).compose(dropRepeats()) + ) + .compose(sampleCombine(state$)) + .map(([_, state]) => state.core.query) + .remember() + + // Compare current query with committed query output to see if this component is currently in a dirty state + const dirtyUiReducer$ = dirtyUiReducer(query$, newQuery$) + + return [ + xs.merge( + defaultReducer$, + setDefaultReducer$, + queryReducer$, + invalidateReducer$, + childReducer$, + validateReducer$, + dirtyUiReducer$, + ), + query$ + ] } function view(state$, signatureCheckDom$) { // Valid query? - const validated$ = state$.map(state => state.form.validated) + const validated$ = state$.map(state => state.core.validated) var result$ = xs.combine(state$, signatureCheckDom$, validated$) .map( ([state, checkdom, validated]) => { - const query = state.form.query + const query = state.core.query return div( [ div('.row .WF-header .white-text', { style: { padding: '20px 10px 10px 10px' } }, [ @@ -151,25 +178,14 @@ function SignatureForm(sources) { const newQuery$ = xs.merge( domSource$.select('.Query').events('input').map(ev => ev.target.value), // Ghost - state$.map(state => state.form.query).compose(dropRepeats()) + state$.map(state => state.core.query).compose(dropRepeats()) ) - const reducers$ = model(newQuery$, state$, sources, signatureCheckSink) - - const vdom$ = view(state$, signatureCheckDom$) - const actions$ = intent(domSource$) - // When GO clicked or enter -> send updated 'value' to sink - // Maybe catch when no valid query? - const query$ = xs.merge( - actions$.update$, - // Ghost mode - sources.onion.state$.map(state => state.form.ghost).filter(ghost => ghost).compose(dropRepeats()) - ) - .compose(sampleCombine(state$)) - .map(([_, state]) => state.form.query) - .remember() + const [reducers$, query$] = model(newQuery$, state$, sources, signatureCheckSink, actions$) + + const vdom$ = view(state$, signatureCheckDom$) return { log: xs.merge( diff --git a/src/js/pages/disease.js b/src/js/pages/disease.js index 35bc06f6..1cd02ded 100644 --- a/src/js/pages/disease.js +++ b/src/js/pages/disease.js @@ -68,6 +68,29 @@ function DiseaseWorkflow(sources) { } }) + /** + * UI dirty logic, checks SignatureForm and Filter components if they are dirty or busy + * If components are dirty/busy, enable UI dirty overlay in subsequent components. + * + * Use dropRepeats else the stream gets in an infinite loop + * @const uiReducer$ + * @type {Reducer} + */ + const uiReducer$ = state$.compose(dropRepeats(equals)) + .map(state => + prevState => { + const dirtyForm = state.form.dirty + const dirtyFilter = state.filter.dirty + return ({...prevState, + ui: { + headTable: {dirty: dirtyForm || dirtyFilter }, + tailTable: {dirty: dirtyForm || dirtyFilter }, + plots: {dirty: dirtyForm || dirtyFilter }, + }, + }) + } + ) + // Filter Form const filterForm = isolate(Filter, { onion: filterLens })({ ...sources, @@ -231,7 +254,8 @@ function DiseaseWorkflow(sources) { binnedPlots.onion, headTable.onion, tailTable.onion, - scenarioReducer$ + scenarioReducer$, + uiReducer$, ), vega: xs.merge(binnedPlots.vega), HTTP: xs.merge( diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index e2bdbd64..ec6a8716 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -69,7 +69,7 @@ export default function GenericTreatmentWorkflow(sources) { treatmentAnnotations: state.settings.treatmentAnnotations, treatmentLike: workflowTreatmentType, }, - ui: (state.ui ?? {} ).form ?? {}, + ui: state.ui?.form ?? {}, }), set: (state, childState) => ({ ...state, form: childState.form }), } From 157b972ecc1b46853dcaf11c860a2cd67ba92541 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 19 Jan 2022 11:11:08 +0100 Subject: [PATCH 090/191] Prevent state changes to update signature vdom (#113) the extra updates either don't do anything useful or overwrite the loadingVdom so the user gets to see outdated information Also add extra debouncing on the dirty UI logic as it could flash dirty-clean-dirty --- src/js/components/SignatureGenerator.js | 26 ++++++++++++++++++----- src/js/index.js | 2 +- src/js/pages/genericTreatment.js | 28 +++++++++++++++---------- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/js/components/SignatureGenerator.js b/src/js/components/SignatureGenerator.js index fe9b47aa..a0110f00 100644 --- a/src/js/components/SignatureGenerator.js +++ b/src/js/components/SignatureGenerator.js @@ -108,25 +108,40 @@ function view(state$, request$, response$, geneAnnotationQuery) { thisGene ]) + /** + * Split off signature display limits from full state + * Needed to prevent lots of vdom updates whenever state changes without vdom changes + * Otherwise would e.g. cause ~ 6-10 validVdom updates while loadingVdom should be displayed + * @const view/signatureLimits$ + * @type {Stream} + */ + const signatureLimit$ = state$.map((state) => ({ + showMore: state.core.showMore, + showLimit: state.core.showLimit + })) + .compose(dropRepeats(equals)) + /** * Vdom to be displayed when the signature is received and valid * @const view/validVdom$ * @type {Stream} */ - const validVdom$ = xs.combine(validSignature$, geneAnnotationQuery.DOM, state$, amountOfInputs$) - .map(([s, annotation, state, amount]) => { + const validVdom$ = xs.combine(validSignature$, geneAnnotationQuery.DOM, signatureLimit$, amountOfInputs$) + .map(([s, annotation, signatureLimit, amount]) => { /** * Signature split into an array * @const view/validVdom$/arr * @type {Array} */ const arr = s.split(" ") + /** * Show full signature or not * @const view/validVdom$/showMore * @type {Boolean} */ - const showMore = state.core.showMore + const showMore = signatureLimit.showMore + /** * Signature size limit * If set to 0 in the settings it means there is no limit, @@ -136,8 +151,9 @@ function view(state$, request$, response$, geneAnnotationQuery) { * @type {Number} */ const showLimit = - state.core.showLimit > 0 && state.core.showLimit < arr.length - ? state.core.showLimit + signatureLimit.showLimit > 0 && + signatureLimit.showLimit < arr.length + ? signatureLimit.showLimit : undefined /** diff --git a/src/js/index.js b/src/js/index.js index a4f57e2d..05d5224a 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -64,7 +64,7 @@ export default function Index(sources) { "settings.common.debug" ) - const state$ = sources.onion.state$.debug("state") + const state$ = sources.onion.state$ const page$ = router.routedComponent({ "/": Home, diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index ec6a8716..880cf8e3 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -17,6 +17,7 @@ import { scenario } from "../scenarios/treatmentScenario" import { runScenario } from "../utils/scenario" import dropRepeats from "xstream/extra/dropRepeats" +import debounce from "xstream/extra/debounce" import { equals } from "ramda" @@ -93,6 +94,16 @@ export default function GenericTreatmentWorkflow(sources) { } }) + const dirtyBusyStates$ = state$.map((state) => ({ + dirtyCheck: state.form.check.dirty, + busySampleSelection: state.form.sampleSelection.busy, + dirtySampleSelection: state.form.sampleSelection.dirty, + busySignature: state.form.signature.busy, + dirtyFilter: state.filter.dirty, + })) + .compose(dropRepeats(equals)) + .compose(debounce(10)) + /** * UI dirty logic, checks TreatmentCheck, SampleSelection, SignatureGenerator and Filter components if they are dirty or busy * If components are dirty/busy, enable UI dirty overlay in subsequent components. @@ -101,23 +112,18 @@ export default function GenericTreatmentWorkflow(sources) { * @const uiReducer$ * @type {Reducer} */ - const uiReducer$ = state$.compose(dropRepeats(equals)) + const uiReducer$ = dirtyBusyStates$ .map(state => prevState => { - const dirtyCheck = state.form.check.dirty - const busySampleSelection = state.form.sampleSelection.busy - const dirtySampleSelection = state.form.sampleSelection.dirty - const busySignature = state.form.signature.busy - const dirtyFilter = state.filter.dirty return ({...prevState, ui: { form: { - sampleSelection: {dirty: dirtyCheck }, - signature: {dirty: dirtyCheck || busySampleSelection || dirtySampleSelection }, + sampleSelection: {dirty: state.dirtyCheck }, + signature: {dirty: state.dirtyCheck || state.busySampleSelection || state.dirtySampleSelection }, }, - headTable: {dirty: dirtyCheck || busySampleSelection || dirtySampleSelection || busySignature || dirtyFilter }, - tailTable: {dirty: dirtyCheck || busySampleSelection || dirtySampleSelection || busySignature || dirtyFilter }, - plots: {dirty: dirtyCheck || busySampleSelection || dirtySampleSelection || busySignature || dirtyFilter }, + headTable: {dirty: state.dirtyCheck || state.busySampleSelection || state.dirtySampleSelection || state.busySignature || state.dirtyFilter }, + tailTable: {dirty: state.dirtyCheck || state.busySampleSelection || state.dirtySampleSelection || state.busySignature || state.dirtyFilter }, + plots: {dirty: state.dirtyCheck || state.busySampleSelection || state.dirtySampleSelection || state.busySignature || state.dirtyFilter }, }, }) } From e0777a4fdb43a7e20894afaa7703c5af2eedb588 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 19 Jan 2022 11:13:18 +0100 Subject: [PATCH 091/191] Force update of input field after autocomplete happened (#110) Autocomplete fills with wrong text, normally overwritten by vdom, but when the autocompleted text is the same as the full text, the autocomplete library updates to the full (wrong) text but our code isn't fired properly (as there is no more change) and thus we can't properly overwrite it later. Code from the autocomplete library: let text = el.text().trim(); this.el.value = text; this.$el.trigger('change'); this._resetAutocomplete(); this.close(); // Handle onAutocomplete callback. if (typeof this.options.onAutocomplete === 'function') { this.options.onAutocomplete.call(this, text); } So all we're adding is an extra this.el.value = 'correct text' --- src/js/drivers/makeAutocompleteDriver.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/js/drivers/makeAutocompleteDriver.js b/src/js/drivers/makeAutocompleteDriver.js index bd0e8c68..e2e3c4dc 100644 --- a/src/js/drivers/makeAutocompleteDriver.js +++ b/src/js/drivers/makeAutocompleteDriver.js @@ -32,7 +32,9 @@ function makeAutocompleteDriver() { ac = M.Autocomplete.init(elem, { data: acInfo.render(acInfo.data), onAutocomplete: function (str) { - listener.next(acInfo.strip(str)) + const stripped = acInfo.strip(str) + listener.next(stripped) + elem.value = acInfo.strip(stripped) } }) ac.open() From 3acfffb5f1a710051f970da2e0a86014b0f61a0c Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 19 Jan 2022 16:25:34 +0100 Subject: [PATCH 092/191] WIP allow a search query to fill in the treatment to solve: validation & optional 'go' --- src/js/components/TreatmentCheck.js | 41 ++++++++++++++++++++++++----- src/js/index.js | 36 ++++++++++++++++++------- src/js/pages/genericTreatment.js | 3 ++- 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index 947bef45..e10f178c 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -4,6 +4,8 @@ import { prop, equals, mergeAll } from "ramda" import xs from "xstream" import dropRepeats from "xstream/extra/dropRepeats" import debounce from "xstream/extra/debounce" +import delay from "xstream/extra/delay" +import flattenConcurrently from "xstream/extra/flattenConcurrently" import { loggerFactory } from "../utils/logger" import { dirtyUiReducer } from "../utils/ui" @@ -11,6 +13,7 @@ const checkLens = { get: (state) => ({ core: typeof state.form !== "undefined" ? state.form.check : {}, settings: state.settings, + search: state.params?.treatment, }), set: (state, childState) => ({ ...state, @@ -49,6 +52,27 @@ function TreatmentCheck(sources) { const acInput$ = sources.ac + const search$ = state$ + .map((state) => state.search) + .filter((search) => search !== undefined) + .compose(dropRepeats(equals)) + .compose(delay(100)) // add delay so newInput$ can see a difference later + .debug("search$") + + const searchTyper$ = search$ + .map( + (search) => { + const l = search.length + const range = Array(l).fill().map((_, index) => index + 1) + // const texts = range.map(i => search.substr(0, i)) + // console.log(texts) + + return xs.fromArray(range.map(i => xs.of(search.substr(0, i)).compose(delay(500 * i)))).compose(flattenConcurrently) + //return xs.fromArray(texts) + } + ) + .flatten() + const input$ = xs.merge( sources.DOM.select(".treatmentQuery") .events("input") @@ -58,8 +82,10 @@ function TreatmentCheck(sources) { state$ .filter((state) => typeof state.core.ghostinput !== "undefined") .map((state) => state.core.input) - .compose(dropRepeats()) - ) + .compose(dropRepeats()), + // searchTyper$, + search$ + ).debug("input$") // When the component should not be shown, including empty signature const isEmptyState = (state) => { @@ -76,13 +102,13 @@ function TreatmentCheck(sources) { const emptyState$ = state$ .filter((state) => isEmptyState(state)) - .compose(dropRepeats(equals)) + .compose(dropRepeats(equals)).debug("emptyState$") // When the state is cycled because of an internal update const modifiedState$ = state$ .filter((state) => !isEmptyState(state)) .compose(dropRepeats((x, y) => equals(x, y))) - .remember() + .remember().debug("modifiedState$") // An update to the input$, join it with state$ const newInput$ = xs @@ -92,11 +118,12 @@ function TreatmentCheck(sources) { core: { ...state.core, input: newinput }, })) .compose(dropRepeats((x, y) => equals(x.core.input, y.core.input))) + .debug("newInput$") const triggerRequest$ = newInput$ .filter((state) => state.core.input.length >= 1) .filter((state) => state.core.showSuggestions) - .compose(debounce(200)) + .compose(debounce(200)).debug("triggerRequest$") const request$ = triggerRequest$.map((state) => { return { @@ -111,11 +138,11 @@ function TreatmentCheck(sources) { }, category: "treatments", } - }) + }).debug("request$") const response$ = sources.HTTP.select("treatments") .map((response$) => response$.replaceError(() => xs.of([]))) - .flatten() + .flatten().debug("response$") const data$ = response$.map((res) => res.body.result.data).remember() diff --git a/src/js/index.js b/src/js/index.js index 8aea20fb..c703a031 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -413,9 +413,24 @@ export default function Index(sources) { ) const routerReducer$ = router.history$.map((router) => (prevState) => { + + function paramsToObject(entries) { + const result = {} + for(const [key, value] of entries) { // each 'entry' is a [key, value] tupple + result[key] = value + } + return result + } + + const params = new URLSearchParams(router.search) + const paramsObject = paramsToObject(params) + return { ...prevState, - routerInformation: router, + routerInformation: { + ...router, + params: paramsObject, + } } }) @@ -432,15 +447,16 @@ export default function Index(sources) { const history$ = sources.onion.state$.fold((acc, x) => acc.concat([x]), [{}]) - const historyDriver$ = xs.periodic(30000).map(i => ({ - type: 'replace', - hash: "tailTable"+i, - //pathname: "/genetic", - // search: "", - // state: undefined, - }) - // '/genetic#tailTable' - ) + const historyDriver$ = xs.empty() + // const historyDriver$ = xs.periodic(30000).map(i => ({ + // type: 'replace', + // hash: "tailTable"+i, + // //pathname: "/genetic", + // // search: "", + // // state: undefined, + // }) + // // '/genetic#tailTable' + // ) // PushHistoryInput( // type: 'push' diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index 76296f15..7dd72576 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -41,7 +41,7 @@ export default function GenericTreatmentWorkflow(sources) { "settings.common.debug" ) - const state$ = sources.onion.state$ + const state$ = sources.onion.state$.debug("state$") // Scenario for ghost mode const scenarios$ = sources.onion.state$ @@ -71,6 +71,7 @@ export default function GenericTreatmentWorkflow(sources) { treatmentLike: workflowTreatmentType, }, ui: state.ui?.form ?? {}, + params: state.routerInformation.params, }), set: (state, childState) => ({ ...state, form: childState.form }), } From 063dc639e1750207cef9e4b12bbca19f48f31162 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 19 Jan 2022 17:13:02 +0100 Subject: [PATCH 093/191] Add validation for treatment type when copy/pasting or using search query When there is new input and is validated by the API, validate the output auto complete only works when there are more than 1 option --- src/js/components/TreatmentCheck.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index e10f178c..30daee1a 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -294,6 +294,27 @@ function TreatmentCheck(sources) { } }) + /** + * When there is new input and is validated by the API, validate the output + * auto complete only works when there are more than 1 option + * @const TreatmentCheck/fullInputValidationReducer$ + * @type {Reducer} + */ + const fullInputValidationReducer$ = data$.compose(sampleCombine(input$)) + .filter(([data, input]) => data.length == 1 && + data[0].trtId == input + ) + .map(([data, input]) => (prevState) => ({ + ...prevState, + core: { + ...prevState.core, + input: input, + showSuggestions: false, + validated: true, + output: input, + }, + })) + // GO!!! const run$ = sources.DOM.select(".treatmentCheck").events("click") @@ -327,6 +348,7 @@ function TreatmentCheck(sources) { requestReducer$, setDefaultReducer$, autocompleteReducer$, + fullInputValidationReducer$, dirtyReducer$, ), output: query$, From 6a9a0b15ccdbe2f7e06315a30c3eb8f7975f16f4 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 19 Jan 2022 17:45:43 +0100 Subject: [PATCH 094/191] Add parameter to automatically start search Still need to fix edge conditions when the search field is changed later Remove temporary debug outputs --- src/js/components/TreatmentCheck.js | 47 ++++++++++++++++++----------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index 30daee1a..0b4d6f3d 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -14,6 +14,7 @@ const checkLens = { core: typeof state.form !== "undefined" ? state.form.check : {}, settings: state.settings, search: state.params?.treatment, + searchAutoRun: state.params?.autorun, }), set: (state, childState) => ({ ...state, @@ -57,21 +58,21 @@ function TreatmentCheck(sources) { .filter((search) => search !== undefined) .compose(dropRepeats(equals)) .compose(delay(100)) // add delay so newInput$ can see a difference later - .debug("search$") const searchTyper$ = search$ - .map( - (search) => { + .map((search) => { const l = search.length - const range = Array(l).fill().map((_, index) => index + 1) - // const texts = range.map(i => search.substr(0, i)) - // console.log(texts) + const range = Array(l) + .fill() + .map((_, index) => index + 1) - return xs.fromArray(range.map(i => xs.of(search.substr(0, i)).compose(delay(500 * i)))).compose(flattenConcurrently) - //return xs.fromArray(texts) - } - ) - .flatten() + return xs + .fromArray( + range.map((i) => xs.of(search.substr(0, i)).compose(delay(100 * i))) + ) + .compose(flattenConcurrently) + }) + .flatten() const input$ = xs.merge( sources.DOM.select(".treatmentQuery") @@ -85,7 +86,7 @@ function TreatmentCheck(sources) { .compose(dropRepeats()), // searchTyper$, search$ - ).debug("input$") + ) // When the component should not be shown, including empty signature const isEmptyState = (state) => { @@ -102,13 +103,13 @@ function TreatmentCheck(sources) { const emptyState$ = state$ .filter((state) => isEmptyState(state)) - .compose(dropRepeats(equals)).debug("emptyState$") + .compose(dropRepeats(equals)) // When the state is cycled because of an internal update const modifiedState$ = state$ .filter((state) => !isEmptyState(state)) .compose(dropRepeats((x, y) => equals(x, y))) - .remember().debug("modifiedState$") + .remember() // An update to the input$, join it with state$ const newInput$ = xs @@ -118,12 +119,11 @@ function TreatmentCheck(sources) { core: { ...state.core, input: newinput }, })) .compose(dropRepeats((x, y) => equals(x.core.input, y.core.input))) - .debug("newInput$") const triggerRequest$ = newInput$ .filter((state) => state.core.input.length >= 1) .filter((state) => state.core.showSuggestions) - .compose(debounce(200)).debug("triggerRequest$") + .compose(debounce(200)) const request$ = triggerRequest$.map((state) => { return { @@ -138,11 +138,11 @@ function TreatmentCheck(sources) { }, category: "treatments", } - }).debug("request$") + }) const response$ = sources.HTTP.select("treatments") .map((response$) => response$.replaceError(() => xs.of([]))) - .flatten().debug("response$") + .flatten() const data$ = response$.map((res) => res.body.result.data).remember() @@ -318,9 +318,20 @@ function TreatmentCheck(sources) { // GO!!! const run$ = sources.DOM.select(".treatmentCheck").events("click") + // Auto start query + // TODO fix conditions when query is changed later & then reverted back to original query + const searchAutoRun$ = state$ + .filter( + (state) => state.searchAutoRun == "" || state.searchAutoRun == "yes" + ) + .filter((state) => state.search == state.core.input) + .filter((state) => state.core.validated == true) + .compose(dropRepeats(equals)) + const query$ = xs .merge( run$, + searchAutoRun$, // Ghost mode sources.onion.state$ .map((state) => state.core.ghostoutput) From cb73aed22e6755c9775cebc832130638c59a32df Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 20 Jan 2022 08:58:02 +0100 Subject: [PATCH 095/191] only trigger autorun once When the query is changed and then changed back to the original value, autorun should not trigger --- src/js/components/TreatmentCheck.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index 0b4d6f3d..082a632f 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -327,11 +327,13 @@ function TreatmentCheck(sources) { .filter((state) => state.search == state.core.input) .filter((state) => state.core.validated == true) .compose(dropRepeats(equals)) + + const singleSearchAutoRun$ = searchAutoRun$.endWhen(searchAutoRun$.compose(delay(100))) const query$ = xs .merge( run$, - searchAutoRun$, + singleSearchAutoRun$, // Ghost mode sources.onion.state$ .map((state) => state.core.ghostoutput) From a7854772f2250ba6e75a77da1c863b74cc13800d Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 20 Jan 2022 09:11:13 +0100 Subject: [PATCH 096/191] Simplify single autorun logic --- src/js/components/TreatmentCheck.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index 082a632f..156deacd 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -319,21 +319,20 @@ function TreatmentCheck(sources) { const run$ = sources.DOM.select(".treatmentCheck").events("click") // Auto start query - // TODO fix conditions when query is changed later & then reverted back to original query + // Only run once, even if query is changed and then reverted to original value const searchAutoRun$ = state$ .filter( (state) => state.searchAutoRun == "" || state.searchAutoRun == "yes" ) .filter((state) => state.search == state.core.input) .filter((state) => state.core.validated == true) + .mapTo(true) .compose(dropRepeats(equals)) - - const singleSearchAutoRun$ = searchAutoRun$.endWhen(searchAutoRun$.compose(delay(100))) const query$ = xs .merge( run$, - singleSearchAutoRun$, + searchAutoRun$, // Ghost mode sources.onion.state$ .map((state) => state.core.ghostoutput) From 50ef72cf5cc2010b129881482dc6b4d79df0ff0a Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 20 Jan 2022 10:52:13 +0100 Subject: [PATCH 097/191] Support a search query string to automatically select specific samples at load Only perform the action once We should add a check whether all specified samples were in fact found/selected --- src/js/components/SampleSelection.js | 39 +++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/js/components/SampleSelection.js b/src/js/components/SampleSelection.js index 1ad1677a..00e85be7 100644 --- a/src/js/components/SampleSelection.js +++ b/src/js/components/SampleSelection.js @@ -35,6 +35,8 @@ const sampleSelectionLens = { core: typeof state.form !== "undefined" ? state.form.sampleSelection : {}, settings: state.settings, ui: (state.ui??{}).sampleSelection ?? {dirty: false}, // Get state.ui.sampleSelection in a safe way or else get a default + search: state.params?.samples, + searchAutoRun: state.params?.autorun, }), // get: state => ({core: state.form.sampleSelection, settings: state.settings}), set: (state, childState) => ({ @@ -315,6 +317,39 @@ function SampleSelection(sources) { } }) + const autoSelect$ = data$.compose(sampleCombine(state$)) + .filter(([_, state]) => (state.search != undefined)) + .mapTo(true) + .compose(dropRepeats(equals)) + + const autoSelectReducer$ = autoSelect$ + .map((_) => (prevState) => { + const samplesToUse = prevState.search.split(",") + + const newData = prevState.core.data.map((el) => { + // One sample object + var newEl = clone(el) + const use = samplesToUse.includes(el.id)//id === el.id + newEl.use = use + // console.log(el) + // console.log(newEl) + return newEl + }) + return { + ...prevState, + core: { + ...prevState.core, + data: newData, + output: newData.filter((x) => x.use).map((x) => x.id), + }, + } + }) + + const autoRun$ = autoSelectReducer$.compose(sampleCombine(state$)) + .filter(([_, state]) => state.searchAutoRun == "" || state.searchAutoRun == "yes") + .mapTo(true) + .compose(dropRepeats(equals)) + const defaultReducer$ = xs.of((prevState) => ({ ...prevState, core: { input: "", data: [] }, @@ -335,7 +370,8 @@ function SampleSelection(sources) { sources.onion.state$ .map((state) => state.core.ghostoutput) .filter((ghost) => ghost) - .compose(dropRepeats()) + .compose(dropRepeats()), + autoRun$, ) .compose(sampleCombine(state$)) .map(([ev, state]) => state.core.output) @@ -356,6 +392,7 @@ function SampleSelection(sources) { requestReducer$, dataReducer$, selectReducer$, + autoSelectReducer$, busyReducer$, dirtyReducer$, ), From 3d93980a7ee4cf2c5e613549e42a2858ab4e9e14 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 20 Jan 2022 17:08:53 +0100 Subject: [PATCH 098/191] Add filter search query handling --- src/js/components/Filter.js | 52 ++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/js/components/Filter.js b/src/js/components/Filter.js index a2910e10..18331c99 100644 --- a/src/js/components/Filter.js +++ b/src/js/components/Filter.js @@ -30,6 +30,11 @@ export const filterLens = { get: (state) => ({ core: state.filter, settings: { filter: state.settings.filter, api: state.settings.api }, + search: { + dose: state.routerInformation.params?.dose, + cell: state.routerInformation.params?.cell, + trtType: state.routerInformation.params?.trtType, + } }), set: (state, childState) => ({ ...state, @@ -199,7 +204,7 @@ function intent(domSource$) { * @param {Stream} filterValuesAction$ object where key is filter group (top level; dose, cell, type) and value is which option is being clicked/modified * @param {Stream} modifier$ boolean of modifier key being pressed or not * @param {Stream} filterAction$ object where key is filter group (top level; dose, cell, type) and value is boolean of the group being clicked open or not - * @param {Stream} state$ readback of full state object used for comparing committed state vs current state, if not identical means ui is dirty + * @param {Stream} search$ search query values for filter settings * @returns {Stream} reducers */ export function model( @@ -208,6 +213,7 @@ export function model( filterValuesAction$, modifier$, filterAction$, + search$, ) { /** @@ -354,6 +360,48 @@ export function model( core: { ...prevState.core, filter_output: minimizeFilterOutput(prevState) }, })) + /** + * Set filter values during page load when search query contains filter values + * @const model/searchReducer$ + * @type {Reducer} + */ + const searchReducer$ = input$.compose(sampleCombine(xs.combine(possibleValues$, search$))) + .map(([_, [possibleValues, search]]) => { + + const matchedFilters = (searchValue, possibleValues) => { + const values = searchValue.split(',') + return values.filter(v => possibleValues.includes(v)) + } + + const matchedDoses = search.dose == undefined ? undefined : matchedFilters(search.dose, possibleValues.dose) + const matchedCells = search.cell == undefined ? undefined : matchedFilters(search.cell, possibleValues.cell) + const matchedTypes = search.trtType == undefined ? undefined : matchedFilters(search.trtType, possibleValues.trtType) + return { + dose: matchedDoses, + cell: matchedCells, + trtType: matchedTypes, + } + }) + .filter((output) => (output.dose != undefined || output.cell != undefined || trtType != undefined)) // Only set filter if filter values are set + .compose(dropRepeats(equals)) // only do this once. Changes in the WF should not be overwritten + .map((output) => (prevState) => { + const filter_output = minimizeFilterOutput({ + ...prevState, + core: { + ...prevState.core, + output: output, + } + }) + return { + ...prevState, + core: { + ...prevState.core, + output: output, + filter_output: filter_output, + } + } + }) + /** * Dirty state reducer, custom version than in ui.js as more logic is required and otherwise need to loop state$ back into model * Uses value change or opening/closing of the filters and compares current state with committed state @@ -377,6 +425,7 @@ export function model( possibleValuesReducer$, filterReducer$, toggleReducer$, + searchReducer$, outputReducer$, dirtyReducer$, ) @@ -583,6 +632,7 @@ function Filter(sources) { actions.filterValuesAction$, actions.modifier$, actions.filterAction$, + state$.map((state) => (state.search)), ) const outputTrigger$ = From 62486d998774db1f98acbc2d37d9c6a54649f335 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 20 Jan 2022 17:35:31 +0100 Subject: [PATCH 099/191] Fix Filter testbench filter model has a new input stream, add xs.empty() for each test --- src/test/FilterTest.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/test/FilterTest.js b/src/test/FilterTest.js index f090f9b1..810f534b 100644 --- a/src/test/FilterTest.js +++ b/src/test/FilterTest.js @@ -12,6 +12,7 @@ describe("defaultReducer", function () { xs.empty(), xs.empty(), xs.empty(), + xs.empty(), xs.empty() ) @@ -38,6 +39,7 @@ describe("defaultReducer", function () { xs.empty(), xs.empty(), xs.empty(), + xs.empty(), xs.empty() ) @@ -73,6 +75,7 @@ describe("possibleValuesReducer", function () { xs.empty(), xs.empty(), xs.empty(), + xs.empty(), xs.empty() ) @@ -112,6 +115,7 @@ describe("inputReducer", function () { input$, xs.empty(), xs.empty(), + xs.empty(), xs.empty() ) @@ -169,6 +173,7 @@ describe("toggleReducer with and without modifier", function () { input$, filterValuesAction$, modifier$, + xs.empty(), xs.empty() ) @@ -247,7 +252,8 @@ describe("uiReducer", function () { input$, filterValuesAction$, modifierFalse$, - openFilter$ + openFilter$, + xs.empty() ) // Predefine filters in settings as possible filters will be updated there From 4a74211264317b02e4729bb29ab619078d531b38 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 21 Jan 2022 08:11:57 +0100 Subject: [PATCH 100/191] Fix page failure when no filter search query present Added missing object selector --- src/js/components/Filter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/components/Filter.js b/src/js/components/Filter.js index 18331c99..de6c055e 100644 --- a/src/js/components/Filter.js +++ b/src/js/components/Filter.js @@ -382,7 +382,7 @@ export function model( trtType: matchedTypes, } }) - .filter((output) => (output.dose != undefined || output.cell != undefined || trtType != undefined)) // Only set filter if filter values are set + .filter((output) => (output.dose != undefined || output.cell != undefined || output.trtType != undefined)) // Only set filter if filter values are set .compose(dropRepeats(equals)) // only do this once. Changes in the WF should not be overwritten .map((output) => (prevState) => { const filter_output = minimizeFilterOutput({ From 44cffd9fae2ddf8baf4d4250d4f81217d49a42bb Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Thu, 6 Jan 2022 15:59:00 +0100 Subject: [PATCH 101/191] Add deployments merging strategy ours/theirs + cleanup --- deployments.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployments.json b/deployments.json index 225d840c..505aac72 100644 --- a/deployments.json +++ b/deployments.json @@ -57,7 +57,7 @@ "endpoint": "classPath=com.dataintuitive.luciusapi.statistics" }, "api": { - "url": "http://localhost:8090/jobs?context=luciusapi&appName=luciusapi&sync=true&timeout=30" + "url": "http://localhost-diebah:8090/jobs?context=luciusapi&appName=luciusapi&sync=true&timeout=30" }, "sourire": { "url": "http://localhost:9999/molecule/" From f46f143974f747f3995b6407728f5c871c4f19ce Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Mon, 10 Jan 2022 23:07:56 +0100 Subject: [PATCH 102/191] Correct error in deployments.json --- deployments.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployments.json b/deployments.json index 505aac72..225d840c 100644 --- a/deployments.json +++ b/deployments.json @@ -57,7 +57,7 @@ "endpoint": "classPath=com.dataintuitive.luciusapi.statistics" }, "api": { - "url": "http://localhost-diebah:8090/jobs?context=luciusapi&appName=luciusapi&sync=true&timeout=30" + "url": "http://localhost:8090/jobs?context=luciusapi&appName=luciusapi&sync=true&timeout=30" }, "sourire": { "url": "http://localhost:9999/molecule/" From bb925dbfc35e61e6da397be18a4836729a8c8a3d Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Fri, 21 Jan 2022 14:51:16 +0100 Subject: [PATCH 103/191] Cleanup --- src/js/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/index.js b/src/js/index.js index 05d5224a..e8e63852 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -438,8 +438,8 @@ export default function Index(sources) { page$.map(prop("log")).filter(Boolean).flatten() ), onion: xs.merge( - defaultReducer$.debug("defaultReducer"), - deploymentsReducer$.debug("deplRed"), + defaultReducer$, + deploymentsReducer$, routerReducer$, page$.map(prop("onion")).filter(Boolean).flatten() ), From 7cb7f354648a610a90eb66cc0607149a454f068d Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Fri, 21 Jan 2022 14:51:27 +0100 Subject: [PATCH 104/191] Bump version to 5.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aecec159..ce61b0e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "LuciusWeb", - "version": "5.0.0-alpha5", + "version": "5.0.0", "description": "Web interface for ComPass aka Lucius", "repository": { "type": "git", From 5271978790628ef1748acfe3d06f0445c89f5335 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 21 Jan 2022 14:54:39 +0100 Subject: [PATCH 105/191] combine the page state to create a sharable URL --- src/js/components/Filter.js | 9 +++++++++ src/js/components/SampleSelection.js | 4 ++++ src/js/components/TreatmentCheck.js | 4 ++++ src/js/index.js | 20 +++++++++++++++++++- src/js/pages/genericTreatment.js | 12 +++++++++++- 5 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/js/components/Filter.js b/src/js/components/Filter.js index de6c055e..a14fe793 100644 --- a/src/js/components/Filter.js +++ b/src/js/components/Filter.js @@ -43,6 +43,15 @@ export const filterLens = { ...state.settings, filter: childState.settings.filter, }, + routerInformation: { + ...state.routerInformation, + pageState: { + ...state.routerInformation.pageState, + dose: childState.core.filter_output?.dose, + cell: childState.core.filter_output?.cell, + trtType: childState.core.filter_output?.trtType, + } + } }), } diff --git a/src/js/components/SampleSelection.js b/src/js/components/SampleSelection.js index 00e85be7..0cdce378 100644 --- a/src/js/components/SampleSelection.js +++ b/src/js/components/SampleSelection.js @@ -42,6 +42,10 @@ const sampleSelectionLens = { set: (state, childState) => ({ ...state, form: { ...state.form, sampleSelection: childState.core}, + pageState: { + ...state.pageState, + samples: childState.core.output, + } }), } diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index 156deacd..9475d5b2 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -19,6 +19,10 @@ const checkLens = { set: (state, childState) => ({ ...state, form: { ...state.form, check: childState.core }, + pageState: { + ...state.pageState, + treatment: childState.core.input, + } }), } diff --git a/src/js/index.js b/src/js/index.js index c703a031..0bc14886 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -22,7 +22,7 @@ import { span, img, } from "@cycle/dom" -import { merge, prop, mergeDeepLeft, mergeDeepRight } from "ramda" +import { merge, prop, mergeDeepLeft, mergeDeepRight, equals, pickBy, identity } from "ramda" import * as R from "ramda" // Workflows @@ -430,10 +430,27 @@ export default function Index(sources) { routerInformation: { ...router, params: paramsObject, + pageState: {} } } }) + const pageStateReducer$ = state$.map((state) => state.routerInformation.pageState).compose(dropRepeats(equals)) + .map((state) => (prevState) => { + // Don't output fields with undefined values + const filteredState = pickBy(identity, state) + const url = new URLSearchParams(filteredState) + + return { + ...prevState, + routerInformation: { + ...prevState.routerInformation, + pageStateURL: prevState.routerInformation.pathname + "?" + url.toString(), + } + } + + }) + // Capture link targets and send to router driver const router$ = sources.DOM.select("a") .events("click") @@ -482,6 +499,7 @@ export default function Index(sources) { defaultReducer$.debug("defaultReducer"), deploymentsReducer$.debug("deplRed"), routerReducer$, + pageStateReducer$, page$.map(prop("onion")).filter(Boolean).flatten() ), DOM: vdom$, diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index 7dd72576..7c7a6f64 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -73,7 +73,17 @@ export default function GenericTreatmentWorkflow(sources) { ui: state.ui?.form ?? {}, params: state.routerInformation.params, }), - set: (state, childState) => ({ ...state, form: childState.form }), + set: (state, childState) => ({ + ...state, + form: childState.form, + routerInformation: { + ...state.routerInformation, + pageState: { + ...state.routerInformation.pageState, + ...childState.pageState, + } + } + }), } /** From cc9840cadb1effbb424f50a22d659c23952cdd8f Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 24 Jan 2022 11:41:07 +0100 Subject: [PATCH 106/191] Pass strings in the page state instead of arrays so page state is uniform Moving burden of knowing what format the data is in to the one where it is generated --- src/js/components/Filter.js | 6 +++--- src/js/components/SampleSelection.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/js/components/Filter.js b/src/js/components/Filter.js index a14fe793..5f17c35a 100644 --- a/src/js/components/Filter.js +++ b/src/js/components/Filter.js @@ -47,9 +47,9 @@ export const filterLens = { ...state.routerInformation, pageState: { ...state.routerInformation.pageState, - dose: childState.core.filter_output?.dose, - cell: childState.core.filter_output?.cell, - trtType: childState.core.filter_output?.trtType, + dose: childState.core.filter_output?.dose?.join(), + cell: childState.core.filter_output?.cell?.join(), + trtType: childState.core.filter_output?.trtType?.join(), } } }), diff --git a/src/js/components/SampleSelection.js b/src/js/components/SampleSelection.js index 0cdce378..a755ef3f 100644 --- a/src/js/components/SampleSelection.js +++ b/src/js/components/SampleSelection.js @@ -44,7 +44,7 @@ const sampleSelectionLens = { form: { ...state.form, sampleSelection: childState.core}, pageState: { ...state.pageState, - samples: childState.core.output, + samples: childState.core.output?.join(), } }), } From c7b329f4343ffb18556a34d6ca7024fee87a2248 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 24 Jan 2022 12:41:33 +0100 Subject: [PATCH 107/191] temporarily display generated URL on the bottom of the page Add server name, port, protocol in the URL. Haven't found it in the router though. --- src/js/index.js | 2 +- src/js/pages/genericTreatment.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/js/index.js b/src/js/index.js index 0bc14886..1bc60782 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -445,7 +445,7 @@ export default function Index(sources) { ...prevState, routerInformation: { ...prevState.routerInformation, - pageStateURL: prevState.routerInformation.pathname + "?" + url.toString(), + pageStateURL: location.protocol + "//" + location.host + prevState.routerInformation.pathname + "?" + url.toString(), } } diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index 7c7a6f64..7ed8e922 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -284,8 +284,9 @@ export default function GenericTreatmentWorkflow(sources) { headTable.DOM, tailTable.DOM, displayPlots$, + state$.map((state) => state.routerInformation.pageStateURL) ) - .map(([formDOM, filter, plots, headTable, tailTable, displayPlots]) => + .map(([formDOM, filter, plots, headTable, tailTable, displayPlots, url]) => div(workflowMainDivClass /* something like ".row .genetic" */ , { style: { margin: "0px 0px 0px 0px" } }, [ formDOM, div(".col .s10 .offset-s1", pageStyle, [ @@ -297,6 +298,7 @@ export default function GenericTreatmentWorkflow(sources) { div(".row", []), div(".row", [displayPlots === "after tables" ? plots : div()]), ]), + div(".col .s10 .offset-s1 .blue.lighten-3", {style: {wordWrap: "break-word"}},url) ]) ) From 06cf9925b106d6c40daf8542b85d31ef9d10b017 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 27 Jan 2022 10:05:03 +0100 Subject: [PATCH 108/191] Create placeholder for clipboardDriver and add functionality to genericTreatment page --- src/js/drivers/clipboardDriver.js | 12 ++++++++++++ src/js/index.js | 1 + src/js/main.js | 2 ++ src/js/pages/genericTreatment.js | 15 +++++++++++++-- 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 src/js/drivers/clipboardDriver.js diff --git a/src/js/drivers/clipboardDriver.js b/src/js/drivers/clipboardDriver.js new file mode 100644 index 00000000..f6651283 --- /dev/null +++ b/src/js/drivers/clipboardDriver.js @@ -0,0 +1,12 @@ +function clipboardDriver(stream$) { + stream$.addListener({ + next: message => { + console.log("text that should be placed in clipboard: " + message) + //message.map(m => console.log(m)) + }, + error: e => console.error(e), + complete: () => {} + }) +} + +export { clipboardDriver } \ No newline at end of file diff --git a/src/js/index.js b/src/js/index.js index 8e2073a5..95aaf310 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -524,6 +524,7 @@ export default function Index(sources) { .flatten() .debug("deployments"), history: historyDriver$, + clipboard: page$.map(prop("clipboard")).filter(Boolean).flatten(), } } diff --git a/src/js/main.js b/src/js/main.js index 37b90ddb..4412f429 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -24,6 +24,7 @@ import switchPath from 'switch-path' import { makeModalDriver } from './drivers/makeModalDriver' import { makeAutocompleteDriver } from './drivers/makeAutocompleteDriver'; import { makeSidenavDriver } from './drivers/makeSidenavDriver'; +import { clipboardDriver } from './drivers/clipboardDriver'; import './main.scss' import fromEvent from 'xstream/extra/fromEvent' @@ -46,6 +47,7 @@ const drivers = { modal: makeModalDriver(), ac: makeAutocompleteDriver(), sidenav: makeSidenavDriver(), + clipboard: clipboardDriver, deployments: () => xs.fromPromise(fetch('/deployments.json').then(m => m.json())) }; diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index 7ed8e922..23d8ec7e 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -1,4 +1,4 @@ -import { div } from "@cycle/dom" +import { div, button } from "@cycle/dom" import xs from "xstream" import isolate from "@cycle/isolate" import { TreatmentForm, treatmentLikeFilter } from "../components/TreatmentForm" @@ -18,6 +18,7 @@ import { runScenario } from "../utils/scenario" import dropRepeats from "xstream/extra/dropRepeats" import debounce from "xstream/extra/debounce" +import sampleCombine from "xstream/extra/sampleCombine" import { equals } from "ramda" @@ -298,10 +299,19 @@ export default function GenericTreatmentWorkflow(sources) { div(".row", []), div(".row", [displayPlots === "after tables" ? plots : div()]), ]), - div(".col .s10 .offset-s1 .blue.lighten-3", {style: {wordWrap: "break-word"}},url) + div(".col .s10 .offset-s1 .blue.lighten-3", {style: {wordWrap: "break-word"}},url), + div([ + button(".clipboard .col .s4 .offset-s4 .btn .grey", "Copy to clipboard"), + ]) ]) ) + const clipboardTrigger$ = sources.DOM.select(".clipboard").events("click").remember() + const clipboard$ = clipboardTrigger$ + .compose(sampleCombine(state$.map((state) => state.routerInformation.pageStateURL))) + .map(([_, url]) => url) + .remember() + return { log: xs.merge( logger(state$, "state$"), @@ -333,5 +343,6 @@ export default function GenericTreatmentWorkflow(sources) { popup: scenarioPopup$, modal: xs.merge(TreatmentFormSink.modal), ac: TreatmentFormSink.ac, + clipboard: clipboard$, } } From 764b807a65168c353ed5ec265a5b6f04524cd673 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 27 Jan 2022 10:40:44 +0100 Subject: [PATCH 109/191] Add clipboard driver functionality --- src/js/drivers/clipboardDriver.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/js/drivers/clipboardDriver.js b/src/js/drivers/clipboardDriver.js index f6651283..af1b0751 100644 --- a/src/js/drivers/clipboardDriver.js +++ b/src/js/drivers/clipboardDriver.js @@ -1,8 +1,13 @@ function clipboardDriver(stream$) { stream$.addListener({ next: message => { - console.log("text that should be placed in clipboard: " + message) - //message.map(m => console.log(m)) + // console.log("text that should be placed in clipboard: " + message) + navigator.clipboard.writeText(message).then(function() { + /* clipboard successfully set */ + }, function() { + /* clipboard write failed */ + console.warn("Writing to clipboard failed") + }); }, error: e => console.error(e), complete: () => {} From 2263e3a80ddf26a8c4637ea6ab66dce8b0cab977 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 27 Jan 2022 14:07:48 +0100 Subject: [PATCH 110/191] Restyle temporary vdom search query divs Just making it looking a little bit less bad --- src/js/pages/genericTreatment.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index 23d8ec7e..624f885d 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -299,9 +299,11 @@ export default function GenericTreatmentWorkflow(sources) { div(".row", []), div(".row", [displayPlots === "after tables" ? plots : div()]), ]), - div(".col .s10 .offset-s1 .blue.lighten-3", {style: {wordWrap: "break-word"}},url), - div([ - button(".clipboard .col .s4 .offset-s4 .btn .grey", "Copy to clipboard"), + div(".col .s10 .offset-s1", [ + div(".col .s12 .blue.lighten-3", {style: {wordWrap: "break-word"}}, url), + div([ + button(".clipboard .col .s4 .offset-s4 .btn .grey", "Copy to clipboard"), + ]) ]) ]) ) From 912050dd3b37de4c0a0f5d60e88bf6a0bebc457f Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 27 Jan 2022 16:11:24 +0100 Subject: [PATCH 111/191] Add POC floating action button buttons/icons are still from example somehow materialize css doesn't get applied --- src/js/components/Exporter.js | 78 +++++++++++++++++++ .../drivers/makeFloatingActionButtonDriver.js | 60 ++++++++++++++ src/js/index.js | 1 + src/js/main.js | 2 + src/js/pages/genericTreatment.js | 12 ++- 5 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 src/js/components/Exporter.js create mode 100644 src/js/drivers/makeFloatingActionButtonDriver.js diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js new file mode 100644 index 00000000..eb40daaa --- /dev/null +++ b/src/js/components/Exporter.js @@ -0,0 +1,78 @@ +import xs from "xstream" +import { div, a, i, ul, li } from "@cycle/dom" +import { loggerFactory } from "../utils/logger" +import delay from "xstream/extra/delay" + +function intent(domSource$) { + +} + +function model() { + return xs.empty() +} + +function view(state$) { + + // const placeholder = div(".red.lighten-4 .green-text.darken-4", "Exporter placeholder") + const placeholder = div(".fixed-action-button", [ + a(".btn-floating .btn-large .red", i(".large .material-icons", "mode_edit")), + ul([ + li(a(".btn-floating .red", i(".material-icons", "insert_chart"))), + li(a(".btn-floating .yellow.darken-1", i(".material-icons", "format_quote"))), + li(a(".btn-floating .green", i(".material-icons", "publish"))), + li(a(".btn-floating .blue", i(".material-icons", "attach_file"))), + ]) + ]) + +// + + return xs.of(placeholder) +} + + + +function Exporter(sources) { + + + const logger = loggerFactory( + "exporter", + sources.onion.state$, + "settings.common.debug" + ) + + const state$ = sources.onion.state$ + + const actions = intent(sources.DOM) + + const vdom$ = view(state$) + + const reducers$ = model() + + const fabInit$ = xs.of({ + state: "init", + element: ".fixed-action-button", + options: { + direction: "left", + // hoverEnabled: false, + } + }).compose(delay(1000)).remember() + + return { + log: xs.merge(logger(state$, "state$")), + DOM: vdom$, + onion: reducers$, + fab: fabInit$, + } +} + +export {Exporter} \ No newline at end of file diff --git a/src/js/drivers/makeFloatingActionButtonDriver.js b/src/js/drivers/makeFloatingActionButtonDriver.js new file mode 100644 index 00000000..ccee5aeb --- /dev/null +++ b/src/js/drivers/makeFloatingActionButtonDriver.js @@ -0,0 +1,60 @@ +import * as M from 'materialize-css' + +function makeFloatingActionButtonDriver() { + + var fab = undefined + + // options are: + // Name Type Default Description + // direction String 'top' Direction FAB menu opens. Can be 'top', 'right', 'buttom', 'left' + // hoverEnabled Boolean true If true, FAB menu will open on hover instead of click + // toolbarEnabled Boolean false Enable transit the FAB into a toolbar on click + + function fabDriver(in$) { + + in$.addListener({ + next: (ev) => { + // console.log("FAB" + ev.state) + if (ev.state == 'init') { + if (fab == undefined) { + + const elem = document.querySelector(ev.element) + var fab = M.FloatingActionButton.init(elem, ev.options); + } + fab.close() + } + if (ev.state == 'open') { + + if (fab == undefined) { + + const elem = document.querySelector(ev.element) + var fab = M.FloatingActionButton.init(elems, ev.options); + + + // We handle opening of the sidenav ourselves. + // Unless also implemented, this removes swipe-closing support though, which is one option to close the sidenav on mobile. + // Opening sidenav creates an overlay which gets an on-click listener, which is used to close the sidenav + // This is not removed by this call. + // sidenav._removeEventHandlers() + } + + fab.open() + } + else if (ev.state == 'close') { + // can be used to e.g. add a button in the sidenav that closes the sidenav + if (fab != undefined) + fab.close() + } + }, + error: (e) => { + console.error(e) + } + }) + + } + + return fabDriver + +} + +export { makeFloatingActionButtonDriver }; \ No newline at end of file diff --git a/src/js/index.js b/src/js/index.js index e8e63852..4edb66bc 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -458,6 +458,7 @@ export default function Index(sources) { modal: page$.map(prop("modal")).filter(Boolean).flatten(), ac: page$.map(prop("ac")).filter(Boolean).flatten(), sidenav: sidenavEvent$, + fab: page$.map(prop("fab")).filter(Boolean).flatten(), storage: page$.map(prop("storage")).filter(Boolean).flatten(), deployments: page$ .map(prop("deployments")) diff --git a/src/js/main.js b/src/js/main.js index 389adb8a..6ea8dd96 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -24,6 +24,7 @@ import switchPath from 'switch-path' import { makeModalDriver } from './drivers/makeModalDriver' import { makeAutocompleteDriver } from './drivers/makeAutocompleteDriver'; import { makeSidenavDriver } from './drivers/makeSidenavDriver'; +import { makeFloatingActionButtonDriver } from './drivers/makeFloatingActionButtonDriver'; import './main.scss' import fromEvent from 'xstream/extra/fromEvent' @@ -46,6 +47,7 @@ const drivers = { modal: makeModalDriver(), ac: makeAutocompleteDriver(), sidenav: makeSidenavDriver(), + fab: makeFloatingActionButtonDriver(), deployments: () => xs.fromPromise(fetch('/deployments.json').then(m => m.json())) }; diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index 880cf8e3..df18170f 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -11,6 +11,7 @@ import { SampleTable, sampleTableLens, } from "../components/SampleTable/SampleTable" +import { Exporter } from "../components/Exporter" // Support for ghost mode import { scenario } from "../scenarios/treatmentScenario" @@ -250,6 +251,8 @@ export default function GenericTreatmentWorkflow(sources) { .remember(), }) + const exporter = Exporter(sources) + /** * Style object used in div capsulating filter, displayPlots and tables * @const pageStyle @@ -273,8 +276,9 @@ export default function GenericTreatmentWorkflow(sources) { headTable.DOM, tailTable.DOM, displayPlots$, + exporter.DOM, ) - .map(([formDOM, filter, plots, headTable, tailTable, displayPlots]) => + .map(([formDOM, filter, plots, headTable, tailTable, displayPlots, exporter]) => div(workflowMainDivClass /* something like ".row .genetic" */ , { style: { margin: "0px 0px 0px 0px" } }, [ formDOM, div(".col .s10 .offset-s1", pageStyle, [ @@ -286,6 +290,7 @@ export default function GenericTreatmentWorkflow(sources) { div(".row", []), div(".row", [displayPlots === "after tables" ? plots : div()]), ]), + exporter, ]) ) @@ -296,7 +301,8 @@ export default function GenericTreatmentWorkflow(sources) { filterForm.log, binnedPlots.log, headTable.log, - tailTable.log + tailTable.log, + exporter.log, ), DOM: vdom$.startWith(div()), onion: xs.merge( @@ -306,6 +312,7 @@ export default function GenericTreatmentWorkflow(sources) { filterForm.onion, headTable.onion, tailTable.onion, + exporter.onion, scenarioReducer$, uiReducer$, ), @@ -320,5 +327,6 @@ export default function GenericTreatmentWorkflow(sources) { popup: scenarioPopup$, modal: xs.merge(TreatmentFormSink.modal), ac: TreatmentFormSink.ac, + fab: exporter.fab, } } From b104bf85bd96eb8539aa9dd43cab5c2633108e8e Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 27 Jan 2022 17:03:13 +0100 Subject: [PATCH 112/191] Improve POC --- src/js/components/Exporter.js | 28 ++++++------------- .../drivers/makeFloatingActionButtonDriver.js | 7 +++-- src/js/main.scss | 4 +++ 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index eb40daaa..449210e8 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -14,28 +14,16 @@ function model() { function view(state$) { // const placeholder = div(".red.lighten-4 .green-text.darken-4", "Exporter placeholder") - const placeholder = div(".fixed-action-button", [ - a(".btn-floating .btn-large .red", i(".large .material-icons", "mode_edit")), + const placeholder = div(".fixed-action-btn", [ + a(".btn-floating .btn-large .red", i(".large .material-icons", "share")), ul([ - li(a(".btn-floating .red", i(".material-icons", "insert_chart"))), - li(a(".btn-floating .yellow.darken-1", i(".material-icons", "format_quote"))), - li(a(".btn-floating .green", i(".material-icons", "publish"))), - li(a(".btn-floating .blue", i(".material-icons", "attach_file"))), + li(a(".btn-floating .red", i(".material-icons", "link"))), + li(a(".btn-floating .yellow.darken-1", i(".material-icons", "content_copy"))), + li(a(".btn-floating .green", i(".material-icons", "picture_as_pdf"))), + li(a(".btn-floating .blue", i(".material-icons", "open_with"))), ]) ]) -// - return xs.of(placeholder) } @@ -60,9 +48,9 @@ function Exporter(sources) { const fabInit$ = xs.of({ state: "init", - element: ".fixed-action-button", + element: ".fixed-action-btn", options: { - direction: "left", + direction: "top", // hoverEnabled: false, } }).compose(delay(1000)).remember() diff --git a/src/js/drivers/makeFloatingActionButtonDriver.js b/src/js/drivers/makeFloatingActionButtonDriver.js index ccee5aeb..37a0be99 100644 --- a/src/js/drivers/makeFloatingActionButtonDriver.js +++ b/src/js/drivers/makeFloatingActionButtonDriver.js @@ -19,7 +19,10 @@ function makeFloatingActionButtonDriver() { if (fab == undefined) { const elem = document.querySelector(ev.element) - var fab = M.FloatingActionButton.init(elem, ev.options); + if (elem == undefined) + console.warn("fabDriver couldn't find element") + else + fab = M.FloatingActionButton.init(elem, ev.options); } fab.close() } @@ -28,7 +31,7 @@ function makeFloatingActionButtonDriver() { if (fab == undefined) { const elem = document.querySelector(ev.element) - var fab = M.FloatingActionButton.init(elems, ev.options); + fab = M.FloatingActionButton.init(elem, ev.options); // We handle opening of the sidenav ourselves. diff --git a/src/js/main.scss b/src/js/main.scss index 42209a0a..449db9a7 100644 --- a/src/js/main.scss +++ b/src/js/main.scss @@ -174,6 +174,10 @@ img.trt_img { color: $color-footer-text; } +div.fixed-action-btn { + bottom: 120px; +} + /* home svg styling and hover */ a:hover #border { From badf69ccab95c225277a36b25e4e2b9ef199af8f Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 28 Jan 2022 10:54:56 +0100 Subject: [PATCH 113/191] WIP Work on getting modal to display Text for modal is hidden and opens when fab is pressed, however often/always? requires two presses initially Modal is shown but only inline with current position of div instead of floating over rest of the page Closing modal disables scrolling the page too --- src/js/components/Exporter.js | 33 ++++++++++++++++++++++++++------ src/js/pages/genericTreatment.js | 2 +- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 449210e8..f8e831b5 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -4,27 +4,41 @@ import { loggerFactory } from "../utils/logger" import delay from "xstream/extra/delay" function intent(domSource$) { + const modalTrigger$ = domSource$.select(".modal-open-btn").events("click")//.debug("modalTrigger$").remember() + return { + modalTrigger$: modalTrigger$, + } } function model() { + return xs.empty() } -function view(state$) { +function view(state$, modalTrigger$) { - // const placeholder = div(".red.lighten-4 .green-text.darken-4", "Exporter placeholder") - const placeholder = div(".fixed-action-btn", [ + const fab = div(".fixed-action-btn", [ a(".btn-floating .btn-large .red", i(".large .material-icons", "share")), ul([ li(a(".btn-floating .red", i(".material-icons", "link"))), li(a(".btn-floating .yellow.darken-1", i(".material-icons", "content_copy"))), li(a(".btn-floating .green", i(".material-icons", "picture_as_pdf"))), - li(a(".btn-floating .blue", i(".material-icons", "open_with"))), + li(a(".btn-floating .blue .modal-open-btn", i(".material-icons", "open_with"))), ]) ]) - return xs.of(placeholder) + const modal = div("#modal-exporter","Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") + + const modal$ = modalTrigger$.mapTo(modal).startWith(div()) + + const vdom$ = xs.combine( + xs.of(fab), + modal$ + ) + .map(([fab, modal]) => (div(".col",[fab, modal]))) + + return vdom$ } @@ -42,7 +56,7 @@ function Exporter(sources) { const actions = intent(sources.DOM) - const vdom$ = view(state$) + const vdom$ = view(state$, actions.modalTrigger$) const reducers$ = model() @@ -55,11 +69,18 @@ function Exporter(sources) { } }).compose(delay(1000)).remember() + + const modalTrigger$ = sources.DOM.select(".modal-open-btn").events("click").remember() + + const openModal$ = modalTrigger$ + .map(_ => ({ el: '#modal-exporter', state: 'open' })) + return { log: xs.merge(logger(state$, "state$")), DOM: vdom$, onion: reducers$, fab: fabInit$, + modal: openModal$ } } diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index df18170f..85d331e1 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -325,7 +325,7 @@ export default function GenericTreatmentWorkflow(sources) { ), vega: binnedPlots.vega, popup: scenarioPopup$, - modal: xs.merge(TreatmentFormSink.modal), + modal: xs.merge(TreatmentFormSink.modal, exporter.modal), ac: TreatmentFormSink.ac, fab: exporter.fab, } From a066d1610bc9aaa086ece8889b094715a5979445 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 28 Jan 2022 15:36:31 +0100 Subject: [PATCH 114/191] Open & close of modal somewhere triggers to the modal are getting delayed between the stream in index.js & the driver --- src/js/components/Exporter.js | 47 +++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index f8e831b5..26839d92 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -1,13 +1,15 @@ import xs from "xstream" -import { div, a, i, ul, li } from "@cycle/dom" +import { div, a, i, ul, li, p, input, button } from "@cycle/dom" import { loggerFactory } from "../utils/logger" import delay from "xstream/extra/delay" function intent(domSource$) { - const modalTrigger$ = domSource$.select(".modal-open-btn").events("click")//.debug("modalTrigger$").remember() + const modalTrigger$ = domSource$.select(".modal-open-btn").events("click") + const modalCloseTrigger$ = domSource$.select(".export-close").events("click") return { modalTrigger$: modalTrigger$, + modalCloseTrigger$: modalCloseTrigger$, } } @@ -28,15 +30,38 @@ function view(state$, modalTrigger$) { ]) ]) - const modal = div("#modal-exporter","Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") - - const modal$ = modalTrigger$.mapTo(modal).startWith(div()) + // const modal = div("#modal-exporter.modal","Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") + + const modal$ = //modalTrigger$.mapTo(modal).startWith(div()) + + state$.map(state => div([ + div('#modal-exporter.modal', [ + div('.modal-content', [ + // + div('.col .s12 .m6', [ + p('.col .s12', ['Value for ', ' : ', 3, ' (doubling period ', ')']), + input('.mu .col .s12', { props: { type: 'range', min: 0, max: 30, step: 0.1, value: 3 }}), + ]), + div('.col .s12 .m6', [ + p('.col .s12', ['Value for ', ' : ', 4]), + input('.sigma .col .s12', { props: { type: 'range', min: 0, max: 5, step: 0.1, value: 4 }}), + ]), + div('.col .s12 .m6', [ + p('.col .s12 ', ['Size of population: ', 10]), + input('.size .col .s12', { props: { type: 'range', min: 1, max: 500, step: 1 , value: 10 }}), + ]) + // + ]), + div('.modal-footer', [ + button('.export-close .col .s8 .offset-s2 .btn .blue-grey', 'Close') + ]) + ])])).startWith(div()) const vdom$ = xs.combine( xs.of(fab), modal$ ) - .map(([fab, modal]) => (div(".col",[fab, modal]))) + .map(([fab, modal]) => (div([fab, modal]))) return vdom$ } @@ -70,17 +95,19 @@ function Exporter(sources) { }).compose(delay(1000)).remember() - const modalTrigger$ = sources.DOM.select(".modal-open-btn").events("click").remember() - const openModal$ = modalTrigger$ - .map(_ => ({ el: '#modal-exporter', state: 'open' })) + const openModal$ = actions.modalTrigger$ + .map(_ => ({ el: '#modal-exporter', state: 'open' })).debug("openModal$") + const closeModal$ = actions.modalCloseTrigger$ + .map(_ => ({ el: '#modal-exporter', state: 'close' })).debug("closeModal$") + return { log: xs.merge(logger(state$, "state$")), DOM: vdom$, onion: reducers$, fab: fabInit$, - modal: openModal$ + modal: xs.merge(openModal$, closeModal$) } } From ccb7afd6e679045517dcd9583dc801b52e7be6f0 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 31 Jan 2022 09:58:00 +0100 Subject: [PATCH 115/191] Use span instead of a to prevent errors in history driver errors caused the modal not opening due delayed streams Add same styling for fab 'a' for 'span' --- src/js/components/Exporter.js | 22 +++++++++------------- src/js/main.scss | 9 +++++++++ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 26839d92..650637ab 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -1,5 +1,5 @@ import xs from "xstream" -import { div, a, i, ul, li, p, input, button } from "@cycle/dom" +import { div, a, i, ul, li, p, input, button, span } from "@cycle/dom" import { loggerFactory } from "../utils/logger" import delay from "xstream/extra/delay" @@ -21,20 +21,16 @@ function model() { function view(state$, modalTrigger$) { const fab = div(".fixed-action-btn", [ - a(".btn-floating .btn-large .red", i(".large .material-icons", "share")), + span(".btn-floating .btn-large .red", i(".large .material-icons", "share")), ul([ - li(a(".btn-floating .red", i(".material-icons", "link"))), - li(a(".btn-floating .yellow.darken-1", i(".material-icons", "content_copy"))), - li(a(".btn-floating .green", i(".material-icons", "picture_as_pdf"))), - li(a(".btn-floating .blue .modal-open-btn", i(".material-icons", "open_with"))), + li(span(".btn-floating .red", i(".material-icons", "link"))), + li(span(".btn-floating .yellow.darken-1", i(".material-icons", "content_copy"))), + li(span(".btn-floating .green", i(".material-icons", "picture_as_pdf"))), + li(span(".btn-floating .blue .modal-open-btn", i(".material-icons", "open_with"))), ]) ]) - // const modal = div("#modal-exporter.modal","Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") - - const modal$ = //modalTrigger$.mapTo(modal).startWith(div()) - - state$.map(state => div([ + const modal$ = state$.map(state => div([ div('#modal-exporter.modal', [ div('.modal-content', [ // @@ -97,9 +93,9 @@ function Exporter(sources) { const openModal$ = actions.modalTrigger$ - .map(_ => ({ el: '#modal-exporter', state: 'open' })).debug("openModal$") + .map(_ => ({ el: '#modal-exporter', state: 'open' })) const closeModal$ = actions.modalCloseTrigger$ - .map(_ => ({ el: '#modal-exporter', state: 'close' })).debug("closeModal$") + .map(_ => ({ el: '#modal-exporter', state: 'close' })) return { diff --git a/src/js/main.scss b/src/js/main.scss index 449db9a7..f9c4572b 100644 --- a/src/js/main.scss +++ b/src/js/main.scss @@ -178,6 +178,15 @@ div.fixed-action-btn { bottom: 120px; } +// fab usually uses a, so add styling for span +.fixed-action-btn { + ul { + span.btn-floating { + opacity: 0; + } + } +} + /* home svg styling and hover */ a:hover #border { From 86b7695d43e0b00c677844422cbf79b165f3116a Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 31 Jan 2022 10:50:04 +0100 Subject: [PATCH 116/191] Move modal styling to own sass file, move sass files to separate folder --- src/js/components/Exporter.js | 16 ++++++------ src/js/main.js | 2 +- src/{js => sass}/_compass_svg.scss | 0 src/sass/_exporter.scss | 40 ++++++++++++++++++++++++++++++ src/{js => sass}/_main_custom.scss | 0 src/{js => sass}/_variables.scss | 0 src/{js => sass}/main.scss | 9 +------ 7 files changed, 50 insertions(+), 17 deletions(-) rename src/{js => sass}/_compass_svg.scss (100%) create mode 100644 src/sass/_exporter.scss rename src/{js => sass}/_main_custom.scss (100%) rename src/{js => sass}/_variables.scss (100%) rename src/{js => sass}/main.scss (96%) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 650637ab..c096d0bd 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -18,15 +18,15 @@ function model() { return xs.empty() } -function view(state$, modalTrigger$) { +function view(state$) { const fab = div(".fixed-action-btn", [ - span(".btn-floating .btn-large .red", i(".large .material-icons", "share")), + span(".btn-floating .btn-large", i(".large .material-icons", "share")), ul([ - li(span(".btn-floating .red", i(".material-icons", "link"))), - li(span(".btn-floating .yellow.darken-1", i(".material-icons", "content_copy"))), - li(span(".btn-floating .green", i(".material-icons", "picture_as_pdf"))), - li(span(".btn-floating .blue .modal-open-btn", i(".material-icons", "open_with"))), + li(span(".btn-floating .export-link", i(".material-icons", "link"))), + li(span(".btn-floating .export-copy", i(".material-icons", "content_copy"))), + li(span(".btn-floating .export-pdf", i(".material-icons", "picture_as_pdf"))), + li(span(".btn-floating .modal-open-btn", i(".material-icons", "open_with"))), ]) ]) @@ -49,7 +49,7 @@ function view(state$, modalTrigger$) { // ]), div('.modal-footer', [ - button('.export-close .col .s8 .offset-s2 .btn .blue-grey', 'Close') + button('.export-close .col .s8 .offset-s2 .btn', 'Close') ]) ])])).startWith(div()) @@ -77,7 +77,7 @@ function Exporter(sources) { const actions = intent(sources.DOM) - const vdom$ = view(state$, actions.modalTrigger$) + const vdom$ = view(state$) const reducers$ = model() diff --git a/src/js/main.js b/src/js/main.js index 6ea8dd96..135b7ec7 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -25,7 +25,7 @@ import { makeModalDriver } from './drivers/makeModalDriver' import { makeAutocompleteDriver } from './drivers/makeAutocompleteDriver'; import { makeSidenavDriver } from './drivers/makeSidenavDriver'; import { makeFloatingActionButtonDriver } from './drivers/makeFloatingActionButtonDriver'; -import './main.scss' +import '../sass/main.scss' import fromEvent from 'xstream/extra/fromEvent' import xs from 'xstream' diff --git a/src/js/_compass_svg.scss b/src/sass/_compass_svg.scss similarity index 100% rename from src/js/_compass_svg.scss rename to src/sass/_compass_svg.scss diff --git a/src/sass/_exporter.scss b/src/sass/_exporter.scss new file mode 100644 index 00000000..5249931f --- /dev/null +++ b/src/sass/_exporter.scss @@ -0,0 +1,40 @@ +// style copy from materialize but add usage of 'span' instead of 'a' +.fixed-action-btn { + ul { + span.btn-floating { + opacity: 0; + } + } +} + +// own styling +.fixed-action-btn { + > span.btn-floating { + background-color: color('red', 'base'); + } + > ul { + span.export-link { + background-color: color('red', 'base') + } + span.export-copy { + background-color: color('yellow', 'darken-1') + } + span.export-pdf { + background-color: color('green', 'base') + } + span.modal-open-btn { + background-color: color('blue', 'base') + } + } + +} + +div #modal-exporter { + // background-color: green; + + div.modal-footer { + > button.export-close{ + background-color: color('blue-grey', 'base') + } + } +} \ No newline at end of file diff --git a/src/js/_main_custom.scss b/src/sass/_main_custom.scss similarity index 100% rename from src/js/_main_custom.scss rename to src/sass/_main_custom.scss diff --git a/src/js/_variables.scss b/src/sass/_variables.scss similarity index 100% rename from src/js/_variables.scss rename to src/sass/_variables.scss diff --git a/src/js/main.scss b/src/sass/main.scss similarity index 96% rename from src/js/main.scss rename to src/sass/main.scss index f9c4572b..93c960b7 100644 --- a/src/js/main.scss +++ b/src/sass/main.scss @@ -178,14 +178,7 @@ div.fixed-action-btn { bottom: 120px; } -// fab usually uses a, so add styling for span -.fixed-action-btn { - ul { - span.btn-floating { - opacity: 0; - } - } -} +@import 'exporter'; /* home svg styling and hover */ From 7e2cb25fc330d0f5fbad0f71e1ace426f3672a00 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 31 Jan 2022 11:41:45 +0100 Subject: [PATCH 117/191] Add trigger placeholders for fab restructure code to pass triggers to the model --- src/js/components/Exporter.js | 35 +++++++++++++++++++++-------------- src/sass/_exporter.scss | 2 +- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index c096d0bd..1ea1ba73 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -4,18 +4,33 @@ import { loggerFactory } from "../utils/logger" import delay from "xstream/extra/delay" function intent(domSource$) { + const exportLinkTrigger$ = domSource$.select(".export-link").events("click") + const exportSignatureTrigger$ = domSource$.select(".export-signature").events("click") + const exportPdfTrigger$ = domSource$.select(".export-pdf").events("click") + const modalTrigger$ = domSource$.select(".modal-open-btn").events("click") const modalCloseTrigger$ = domSource$.select(".export-close").events("click") return { + exportLinkTrigger$: exportLinkTrigger$, + exportSignatureTrigger$: exportSignatureTrigger$, + exportPdfTrigger$: exportPdfTrigger$, modalTrigger$: modalTrigger$, modalCloseTrigger$: modalCloseTrigger$, } } -function model() { +function model(actions) { - return xs.empty() + const openModal$ = actions.modalTrigger$ + .map(_ => ({ el: '#modal-exporter', state: 'open' })) + const closeModal$ = actions.modalCloseTrigger$ + .map(_ => ({ el: '#modal-exporter', state: 'close' })) + + return { + reducers$: xs.empty(), + modal$: xs.merge(openModal$, closeModal$), + } } function view(state$) { @@ -24,7 +39,7 @@ function view(state$) { span(".btn-floating .btn-large", i(".large .material-icons", "share")), ul([ li(span(".btn-floating .export-link", i(".material-icons", "link"))), - li(span(".btn-floating .export-copy", i(".material-icons", "content_copy"))), + li(span(".btn-floating .export-signature", i(".material-icons", "content_copy"))), li(span(".btn-floating .export-pdf", i(".material-icons", "picture_as_pdf"))), li(span(".btn-floating .modal-open-btn", i(".material-icons", "open_with"))), ]) @@ -79,7 +94,7 @@ function Exporter(sources) { const vdom$ = view(state$) - const reducers$ = model() + const model_ = model(actions) const fabInit$ = xs.of({ state: "init", @@ -90,20 +105,12 @@ function Exporter(sources) { } }).compose(delay(1000)).remember() - - - const openModal$ = actions.modalTrigger$ - .map(_ => ({ el: '#modal-exporter', state: 'open' })) - const closeModal$ = actions.modalCloseTrigger$ - .map(_ => ({ el: '#modal-exporter', state: 'close' })) - - return { log: xs.merge(logger(state$, "state$")), DOM: vdom$, - onion: reducers$, + onion: model_.reducers$, fab: fabInit$, - modal: xs.merge(openModal$, closeModal$) + modal: model_.modal$, } } diff --git a/src/sass/_exporter.scss b/src/sass/_exporter.scss index 5249931f..5a165ccf 100644 --- a/src/sass/_exporter.scss +++ b/src/sass/_exporter.scss @@ -16,7 +16,7 @@ span.export-link { background-color: color('red', 'base') } - span.export-copy { + span.export-signature { background-color: color('yellow', 'darken-1') } span.export-pdf { From 7fc074d6fc8e7bfadc9564df21cdbcb9542ddcdd Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 31 Jan 2022 14:16:43 +0100 Subject: [PATCH 118/191] Create first draft of what the export menu could possibly look like Add some text and buttons with options that are at least a bit topical --- src/js/components/Exporter.js | 64 ++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 1ea1ba73..1f2a6b04 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -1,5 +1,5 @@ import xs from "xstream" -import { div, a, i, ul, li, p, input, button, span } from "@cycle/dom" +import { div, i, ul, li, p, input, button, span } from "@cycle/dom" import { loggerFactory } from "../utils/logger" import delay from "xstream/extra/delay" @@ -45,28 +45,52 @@ function view(state$) { ]) ]) - const modal$ = state$.map(state => div([ - div('#modal-exporter.modal', [ - div('.modal-content', [ - // - div('.col .s12 .m6', [ - p('.col .s12', ['Value for ', ' : ', 3, ' (doubling period ', ')']), - input('.mu .col .s12', { props: { type: 'range', min: 0, max: 30, step: 0.1, value: 3 }}), + const modal$ = state$ + .map((state) => + div([ + div("#modal-exporter.modal", [ + div(".modal-content", [ + div(".row", + p(".col .s12", "Export to clipboard or file") + ), + div(".row", [ + span(".col .s4", "Create link to this page's state"), + span(".btn .col .s1 .offset-s1", i(".material-icons", "content_copy")), + span(".btn .col .s1 .offset-s1", i(".material-icons", "file_download")), ]), - div('.col .s12 .m6', [ - p('.col .s12', ['Value for ', ' : ', 4]), - input('.sigma .col .s12', { props: { type: 'range', min: 0, max: 5, step: 0.1, value: 4 }}), + div(".row", [ + span(".col .s4", "Copy signature"), + span(".btn .col .s1 .offset-s1", i(".material-icons", "content_copy")), + span(".btn .col .s1 .offset-s1", i(".material-icons", "file_download")), ]), - div('.col .s12 .m6', [ - p('.col .s12 ', ['Size of population: ', 10]), - input('.size .col .s12', { props: { type: 'range', min: 1, max: 500, step: 1 , value: 10 }}), - ]) - // + div(".row", [ + span(".col .s4", "Copy binned plots"), + span(".btn .col .s1 .offset-s1", i(".material-icons", "content_copy")), + span(".btn .col .s1 .offset-s1", i(".material-icons", "file_download")), + ]), + div(".row", [ + span(".col .s4", "Copy top table"), + span(".btn .col .s1 .offset-s1", i(".material-icons", "content_copy")), + span(".btn .col .s1 .offset-s1", i(".material-icons", "file_download")), + ]), + div(".row", [ + span(".col .s4", "Copy bottom table"), + span(".btn .col .s1 .offset-s1", i(".material-icons", "content_copy")), + span(".btn .col .s1 .offset-s1", i(".material-icons", "file_download")), + ]), + div(".row", [ + span(".col .s4", "Export report"), + // span(".btn .col .s1 .offset-s1", i(".material-icons", "content_copy")), + span(".btn .col .s1 .offset-s3 .disabled", i(".material-icons", "file_download")), + ]), + ]), + div(".modal-footer", [ + button(".export-close .col .s8 .push-s2 .btn", "Close"), + ]), ]), - div('.modal-footer', [ - button('.export-close .col .s8 .offset-s2 .btn', 'Close') - ]) - ])])).startWith(div()) + ]) + ) + .startWith(div()) const vdom$ = xs.combine( xs.of(fab), From ab87000b0d61be51ba44cc4f520a409312eec15d Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 31 Jan 2022 15:03:52 +0100 Subject: [PATCH 119/191] Disable signature export buttons when no signature present does not yet work for FAB, probably code loses track as the classes change --- src/js/components/Exporter.js | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 1f2a6b04..62abbb33 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -35,18 +35,23 @@ function model(actions) { function view(state$) { - const fab = div(".fixed-action-btn", [ - span(".btn-floating .btn-large", i(".large .material-icons", "share")), - ul([ - li(span(".btn-floating .export-link", i(".material-icons", "link"))), - li(span(".btn-floating .export-signature", i(".material-icons", "content_copy"))), - li(span(".btn-floating .export-pdf", i(".material-icons", "picture_as_pdf"))), - li(span(".btn-floating .modal-open-btn", i(".material-icons", "open_with"))), - ]) - ]) - - const modal$ = state$ - .map((state) => + const signaturePresent$ = state$.map((state) => state.form.signature.output != undefined && state.form.signature.output != "") + + const fab$ = signaturePresent$ + .map((signature) => + div(".fixed-action-btn", [ + span(".btn-floating .btn-large", i(".large .material-icons", "share")), + ul([ + li(span(".btn-floating .export-link", i(".material-icons", "link"))), + li(span(".btn-floating .export-signature"/* + (signature ? "" : " .disabled")*/, i(".material-icons", "content_copy"))), + li(span(".btn-floating .export-pdf", i(".material-icons", "picture_as_pdf"))), + li(span(".btn-floating .modal-open-btn", i(".material-icons", "open_with"))), + ]) + ])) + .startWith(div()) + + const modal$ = signaturePresent$ + .map((signature) => div([ div("#modal-exporter.modal", [ div(".modal-content", [ @@ -60,8 +65,8 @@ function view(state$) { ]), div(".row", [ span(".col .s4", "Copy signature"), - span(".btn .col .s1 .offset-s1", i(".material-icons", "content_copy")), - span(".btn .col .s1 .offset-s1", i(".material-icons", "file_download")), + span(".btn .col .s1 .offset-s1" + (signature ? "" : " .disabled"), i(".material-icons", "content_copy")), + span(".btn .col .s1 .offset-s1" + (signature ? "" : " .disabled"), i(".material-icons", "file_download")), ]), div(".row", [ span(".col .s4", "Copy binned plots"), @@ -93,7 +98,7 @@ function view(state$) { .startWith(div()) const vdom$ = xs.combine( - xs.of(fab), + fab$, modal$ ) .map(([fab, modal]) => (div([fab, modal]))) From 00985cd8c929337e8d5463d62c1ce40f1b29d38d Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 1 Feb 2022 12:28:44 +0100 Subject: [PATCH 120/191] add option in ghost scenario runner to wait for a condition that must be matched This allows the scenario to be more reactive to backend delays instead of having to wait fixed times --- src/js/pages/genericTreatment.js | 2 +- src/js/scenarios/treatmentScenario.js | 8 ++++++- src/js/utils/scenario.js | 32 ++++++++++++++++++++++----- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index 880cf8e3..dac1cb4a 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -47,7 +47,7 @@ export default function GenericTreatmentWorkflow(sources) { const scenarios$ = sources.onion.state$ .take(1) .filter((state) => state.settings.common.ghostMode) - .map(state => runScenario(scenario( workflowGhostModeScenarioSelector(state) ))) + .map(state => runScenario(scenario( workflowGhostModeScenarioSelector(state) ), state$)) const scenarioReducer$ = scenarios$.map(s => s.scenarioReducer$) .flatten() const scenarioPopup$ = scenarios$.map(s => s.scenarioPopup$) diff --git a/src/js/scenarios/treatmentScenario.js b/src/js/scenarios/treatmentScenario.js index 53b9c053..c85cf7c5 100644 --- a/src/js/scenarios/treatmentScenario.js +++ b/src/js/scenarios/treatmentScenario.js @@ -19,6 +19,10 @@ const typer = (text, json) => { ) } +function notEmpty(data) { + return data != undefined && data != "" +} + /** * Definition of the scenario * Configuration is provided by means of `config` object: @@ -56,6 +60,7 @@ export const scenario = config => { // Sample Selection delay: 500, + continue: (s) => (notEmpty(s.form.sampleSelection.output)), state: {}, message: { text: 'All samples that correspond to the selected compound are tabulated', @@ -105,7 +110,8 @@ export const scenario = config => }, { // Filter - delay: 7000, + delay: 500, + continue: (s) => (notEmpty(s.form.signature.output)), state: { filter: { input: config.signature, diff --git a/src/js/utils/scenario.js b/src/js/utils/scenario.js index 922c957c..7aa9b47a 100644 --- a/src/js/utils/scenario.js +++ b/src/js/utils/scenario.js @@ -1,7 +1,9 @@ import xs from "xstream" -import { mergeDeepRight, mergeDeepWithKey } from "ramda" +import { mergeDeepRight, mergeDeepWithKey, equals } from "ramda" import delay from "xstream/extra/delay" import concat from "xstream/extra/concat" +import dropRepeats from "xstream/extra/dropRepeats" +import debounce from "xstream/extra/debounce" /** * We expect the following input for the right-hand side: @@ -19,10 +21,30 @@ const updateData = (k, l, r) => * The hardest part is providing the new state and merging that to the previous one. * Especially for Arrays, because we don't want to provide the full array in every step. */ -export const runScenario = (scenario) => { - const scenarioStreamArray = scenario.map((step) => - xs.of(step).compose(delay(step.delay)) - ) +export const runScenario = (scenario, state$) => { + + const scenarioStreamArray = scenario.map((step) => { + if (step.continue != undefined) { + const trigger$ = state$ + .map((s) => step.continue(s)) + .compose(debounce(equals)) + .filter(a => a == true) + .compose(dropRepeats(equals)) + .debug("trigger$") + const triggerDelayed$ = trigger$ + .compose(delay(10)) + .debug("triggerDelayed$") + + const triggerOutput$ = trigger$ + .mapTo(step) + .endWhen(triggerDelayed$) + .debug("triggerOutput$") + + return triggerOutput$.compose(delay(step.delay)) + } + else + return xs.of(step).compose(delay(step.delay)) + }) const reducerStreamArray = scenarioStreamArray.map((step$) => step$.map( From e5c751011d6d4545829504b1ead48541a4d698f6 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 3 Feb 2022 10:36:30 +0100 Subject: [PATCH 121/191] Add option in genericTreatments to use the typer for the treatment search value add typer as parameter, if empty or "yes" it uses a default speed of 100ms minimum value 50ms, maximum value 5000ms --- src/js/components/TreatmentCheck.js | 29 ++--------- src/js/utils/searchUtils.js | 78 +++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 24 deletions(-) create mode 100644 src/js/utils/searchUtils.js diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index 9475d5b2..7cf1b412 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -4,10 +4,9 @@ import { prop, equals, mergeAll } from "ramda" import xs from "xstream" import dropRepeats from "xstream/extra/dropRepeats" import debounce from "xstream/extra/debounce" -import delay from "xstream/extra/delay" -import flattenConcurrently from "xstream/extra/flattenConcurrently" import { loggerFactory } from "../utils/logger" import { dirtyUiReducer } from "../utils/ui" +import { typer } from "../utils/searchUtils" const checkLens = { get: (state) => ({ @@ -15,6 +14,7 @@ const checkLens = { settings: state.settings, search: state.params?.treatment, searchAutoRun: state.params?.autorun, + searchTyper: state.params?.typer, }), set: (state, childState) => ({ ...state, @@ -57,26 +57,8 @@ function TreatmentCheck(sources) { const acInput$ = sources.ac - const search$ = state$ - .map((state) => state.search) - .filter((search) => search !== undefined) - .compose(dropRepeats(equals)) - .compose(delay(100)) // add delay so newInput$ can see a difference later - - const searchTyper$ = search$ - .map((search) => { - const l = search.length - const range = Array(l) - .fill() - .map((_, index) => index + 1) - - return xs - .fromArray( - range.map((i) => xs.of(search.substr(0, i)).compose(delay(100 * i))) - ) - .compose(flattenConcurrently) - }) - .flatten() + // Get input from search query, either slowly typed or in one go + const typer$ = typer(state$, "search", "searchTyper") const input$ = xs.merge( sources.DOM.select(".treatmentQuery") @@ -88,8 +70,7 @@ function TreatmentCheck(sources) { .filter((state) => typeof state.core.ghostinput !== "undefined") .map((state) => state.core.input) .compose(dropRepeats()), - // searchTyper$, - search$ + typer$, ) // When the component should not be shown, including empty signature diff --git a/src/js/utils/searchUtils.js b/src/js/utils/searchUtils.js new file mode 100644 index 00000000..0b1ac0d6 --- /dev/null +++ b/src/js/utils/searchUtils.js @@ -0,0 +1,78 @@ +import xs from "xstream" +import flattenConcurrently from "xstream/extra/flattenConcurrently" +import delay from "xstream/extra/delay" +import dropRepeats from "xstream/extra/dropRepeats" +import { prop, equals, min, max } from "ramda" + +/** + * Create stream with text to be used as input + * @function typer + * @param {Stream} state$ full state onion of the component + * @param {String} valueName name of the property in the state to be used; search value + * @param {String} speedName name of the property in the state to be used; enables slow typing + * @returns Stream + */ +export function typer(state$, valueName, speedName) { + /** + * Get the search value from state$ + * @const typer/value$ + * @type {Stream} + */ + const value$ = state$ + .map((state) => prop(valueName, state)) + .filter((value) => value !== undefined) + .compose(dropRepeats(equals)) + .compose(delay(100)) + + /** + * Get the typer speed from state$ + * @const typer/speed$ + * @type {Stream} + */ + const speed$ = state$ + .map((state) => prop(speedName, state)) + .compose(dropRepeats(equals)) + .startWith("") + + /** + * Slowly type the search value, starting with 1 letter and incrementally getting longer until the full text is being output + * Only used when speed is explicitely set + * @const typer/typer$ + * @type {Stream} + */ + const typer$ = xs + .combine(speed$, value$) + .filter(([speed, _]) => speed == "" || speed == "yes" || !isNaN(speed)) + .map(([speed, value]) => { + const l = value.length + const range = Array(l) + .fill() + .map((_, index) => index + 1) + + const interval = isNaN(speed) ? 100 : max(min(speed, 5000), 50) + + return xs + .fromArray( + range.map((i) => + xs.of(value.substr(0, i)).compose(delay(interval * i)) + ) + ) + .compose(flattenConcurrently) + }) + .flatten() + + /** + * Output search value in one go if the typer isn't enabled by the speed value + * @const typer/typerNotSelected$ + * @type {Stream} + */ + const typerNotSelected$ = xs + .combine(speed$, value$) + .filter( + ([speed, _]) => + speed == undefined || (speed != "" && speed != "yes" && isNaN(speed)) + ) + .map(([_, value]) => value) + + return xs.merge(typer$, typerNotSelected$) +} From 1db302765e177cee1818991952ba7e5fd92bf7dd Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 3 Feb 2022 14:14:02 +0100 Subject: [PATCH 122/191] Add search query functionality in the disease WF validation happens when all genes are found, silently translate substitutions (wanted?) --- src/js/components/SignatureCheck.js | 29 +++++++++++++++++++++---- src/js/components/SignatureForm.js | 33 +++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/js/components/SignatureCheck.js b/src/js/components/SignatureCheck.js index 22b2ae28..4b499a7b 100644 --- a/src/js/components/SignatureCheck.js +++ b/src/js/components/SignatureCheck.js @@ -1,6 +1,6 @@ import xs from 'xstream'; import { p, div, br, label, input, code, table, tr, th, td, b, h2, button, thead, tbody, i, h, hr } from '@cycle/dom'; -import { clone, equals } from 'ramda'; +import { clone, equals, all } from 'ramda'; import sampleCombine from 'xstream/extra/sampleCombine' import {log, logThis} from '../utils/logger' import {ENTER_KEYCODE} from '../utils/keycodes.js' @@ -24,7 +24,13 @@ const stateTemplate = { } const checkLens = { - get: state => ({query: state.core.query, settings: state.settings}), + get: state => ({ + query: state.core.query, + settings: state.settings, + search: state.search, + searchAutoRun: state.searchAutoRun, + validated: state.core.validated + }), set: (state, childState) => ({...state, core : {...state.core, query: childState.query}}) }; @@ -128,14 +134,29 @@ function SignatureCheck(sources) { // Update and Collapse button updates the query and collapses the window const collapseUpdate$ = domSource$.select('.collapseUpdate').events('click'); - const collapseUpdateReducer$ = collapseUpdate$.compose(sampleCombine(data$)) + + // Auto start query if the entered search string is valid + // Only run once, even if query is changed and then reverted to original value + const searchAutoRun$ = data$ + .compose(sampleCombine(state$)) + .filter( + ([_, state]) => + state.searchAutoRun == "" || state.searchAutoRun == "yes" // autorun enabled? + ) + .filter(([data, _]) => all((x) => x.found ?? x.inL1000)(data)) // all entered data is valid? + //.filter(([_, state]) => state.query == state.search) + .filter(([_, state]) => state.validated == false) // not yet validated? + .mapTo(true) + .compose(dropRepeats(equals)) + + const collapseUpdateReducer$ = xs.merge(collapseUpdate$,searchAutoRun$).compose(sampleCombine(data$)) .map(([collapse, data]) => prevState => { return ({...prevState, query : data.map(x => (x.found ?? x.inL1000) ? x.symbol : '').join(" ").replace(/\s\s+/g, ' ').trim()}); }); // The result of this component is an event when valid // XXX: stays true the whole cycle, so maybe tackle this as well!!!! - const validated$ = collapseUpdate$.map(update => true) + const validated$ = xs.merge(collapseUpdate$,searchAutoRun$).map(update => true) return { log: xs.merge( diff --git a/src/js/components/SignatureForm.js b/src/js/components/SignatureForm.js index c2c945cf..97a9d870 100644 --- a/src/js/components/SignatureForm.js +++ b/src/js/components/SignatureForm.js @@ -9,6 +9,7 @@ import { SignatureCheck, checkLens } from '../components/SignatureCheck' import dropRepeats from 'xstream/extra/dropRepeats' import { loggerFactory } from '../utils/logger' import { dirtyUiReducer } from '../utils/ui' +import { typer } from '../utils/searchUtils' // Granular access to global state and parts of settings const formLens = { @@ -20,8 +21,21 @@ const formLens = { common: state.settings.common, }, ui: state.ui?.form ?? {}, + search: state.routerInformation.params?.signature, + searchAutoRun: state.routerInformation.params?.autorun, + searchTyper: state.routerInformation.params?.typer, + }), + set: (state, childState) => ({ + ...state, + form: {...childState.core }, + routerInformation: { + ...state.routerInformation, + pageState: { + ...state.routerInformation.pageState, + signature: childState.core.query, + } + } }), - set: (state, childState) => ({ ...state, form: {...childState.core } }), } function model(newQuery$, state$, sources, signatureCheckSink, actions$) { @@ -89,10 +103,22 @@ function model(newQuery$, state$, sources, signatureCheckSink, actions$) { // When update is clicked, update the query. Onionify does the rest const childReducer$ = signatureCheckSink.onion + // Auto start query + // Only run once, even if query is changed and then reverted to original value + const searchAutoRun$ = state$ + .filter( + (state) => state.searchAutoRun == "" || state.searchAutoRun == "yes" + ) + .filter((state) => state.search == state.core.query) + .filter((state) => state.core.validated == true) + .mapTo(true) + .compose(dropRepeats(equals)) + // When GO clicked or enter -> send updated 'value' to sink // Maybe catch when no valid query? const query$ = xs.merge( actions$.update$, + searchAutoRun$, // Ghost mode sources.onion.state$.map(state => state.core.ghost).filter(ghost => ghost).compose(dropRepeats()) ) @@ -174,11 +200,14 @@ function SignatureForm(sources) { const signatureCheckHTTP$ = signatureCheckSink.HTTP; const signatureCheckReducer$ = signatureCheckSink.onion; + const typer$ = typer(state$, 'search', 'searchTyper') + // Update in query, or simply ENTER const newQuery$ = xs.merge( domSource$.select('.Query').events('input').map(ev => ev.target.value), // Ghost - state$.map(state => state.core.query).compose(dropRepeats()) + state$.map(state => state.core.query).compose(dropRepeats()), + typer$, ) const actions$ = intent(domSource$) From 74e66d2c50024f88c4c1238466014c422b581dc1 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 3 Feb 2022 15:09:57 +0100 Subject: [PATCH 123/191] Add search query functionality for the correlation WF Add typers & autorun code in form changed lenses in SignatureCheck to set correct IDs --- src/js/components/CorrelationForm.js | 50 +++++++++++++++++++++++++--- src/js/components/SignatureCheck.js | 16 +++++++-- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/js/components/CorrelationForm.js b/src/js/components/CorrelationForm.js index e540b7d6..b005ff23 100644 --- a/src/js/components/CorrelationForm.js +++ b/src/js/components/CorrelationForm.js @@ -8,6 +8,7 @@ import { ENTER_KEYCODE } from '../utils/keycodes.js' import { SignatureCheck, checkLens1, checkLens2 } from '../components/SignatureCheck' import dropRepeats from 'xstream/extra/dropRepeats' import { loggerFactory } from '../utils/logger' +import { typer } from '../utils/searchUtils' const stateTemplate = { form: { @@ -21,8 +22,29 @@ const stateTemplate = { // Granular access to global state and parts of settings const formLens = { - get: state => ({ core: state.form, settings: { form: state.settings.form, api: state.settings.api } }), - set: (state, childState) => ({ ...state, form: childState.core }) + get: state => ({ + core: state.form, + settings: { + form: state.settings.form, + api: state.settings.api + }, + search1: state.routerInformation.params?.signature1, + search2: state.routerInformation.params?.signature2, + searchAutoRun: state.routerInformation.params?.autorun, + searchTyper: state.routerInformation.params?.typer, + }), + set: (state, childState) => ({ + ...state, + form: childState.core, + routerInformation: { + ...state.routerInformation, + pageState: { + ...state.routerInformation.pageState, + signature1: childState.core.query1, + signature2: childState.core.query2, + } + } + }) }; function CorrelationForm(sources) { @@ -79,15 +101,20 @@ function CorrelationForm(sources) { }); // Update in query, or simply ENTER + const typer1$ = typer(state$, 'search1', 'searchTyper') const newQuery1$ = xs.merge( sources.DOM.select('.Query1').events('input').map(ev => ev.target.value), // Ghost - state$.map(state => state.core.query1).compose(dropRepeats()) + state$.map(state => state.core.query1).compose(dropRepeats()), + typer1$, ) + + const typer2$ = typer(state$, 'search2', 'searchTyper') const newQuery2$ = xs.merge( sources.DOM.select('.Query2').events('input').map(ev => ev.target.value), // Ghost - state$.map(state => state.core.query2).compose(dropRepeats()) + state$.map(state => state.core.query2).compose(dropRepeats()), + typer2$, ) // Updated state is propagated and picked up by the necessary components @@ -187,12 +214,25 @@ function CorrelationForm(sources) { const childReducer1$ = signatureCheck1.onion const childReducer2$ = signatureCheck2.onion + // Auto start query + // Only run once, even if query is changed and then reverted to original value + const searchAutoRun$ = state$ + .filter( + (state) => state.searchAutoRun == "" || state.searchAutoRun == "yes" + ) + .filter((state) => state.search1 == state.core.query1 && state.search2 == state.core.query2) + .filter((state) => state.core.validated1 == true && state.core.validated2 == true) + .mapTo(true) + .compose(dropRepeats(equals)) + + // When GO clicked or enter -> send updated 'value' to sink // Maybe catch when no valid query? const query$ = xs.merge( update$, // Ghost mode - sources.onion.state$.map(state => state.core.ghost).filter(ghost => ghost).compose(dropRepeats()) + sources.onion.state$.map(state => state.core.ghost).filter(ghost => ghost).compose(dropRepeats()), + searchAutoRun$, ) .compose(sampleCombine(state$)) .map(([update, state]) => ({query1: state.core.query1, query2: state.core.query2})) diff --git a/src/js/components/SignatureCheck.js b/src/js/components/SignatureCheck.js index 4b499a7b..fc7be84a 100644 --- a/src/js/components/SignatureCheck.js +++ b/src/js/components/SignatureCheck.js @@ -35,12 +35,24 @@ const checkLens = { }; const checkLens1 = { - get: state => ({query: state.core.query1, settings: state.settings}), + get: state => ({ + query: state.core.query1, + settings: state.settings, + search: state.search1, + searchAutoRun: state.searchAutoRun, + validated: state.core.validated1 + }), set: (state, childState) => ({...state, core : {...state.core, query1: childState.query}}) }; const checkLens2 = { - get: state => ({query: state.core.query2, settings: state.settings}), + get: state => ({ + query: state.core.query2, + settings: state.settings, + search: state.search2, + searchAutoRun: state.searchAutoRun, + validated: state.core.validated2 + }), set: (state, childState) => ({...state, core : {...state.core, query2: childState.query}}) }; From 37c750a18152805f84b2cfffbb07e58ed9a5b809 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 1 Feb 2022 15:08:24 +0100 Subject: [PATCH 124/191] refactor modal to use clipboard and disable not available exports --- src/js/components/Exporter.js | 86 ++++++++++++++++++++------------ src/js/pages/genericTreatment.js | 17 +------ src/sass/_exporter.scss | 4 +- 3 files changed, 59 insertions(+), 48 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 62abbb33..8335425f 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -2,11 +2,12 @@ import xs from "xstream" import { div, i, ul, li, p, input, button, span } from "@cycle/dom" import { loggerFactory } from "../utils/logger" import delay from "xstream/extra/delay" +import sampleCombine from "xstream/extra/sampleCombine" function intent(domSource$) { - const exportLinkTrigger$ = domSource$.select(".export-link").events("click") - const exportSignatureTrigger$ = domSource$.select(".export-signature").events("click") - const exportPdfTrigger$ = domSource$.select(".export-pdf").events("click") + const exportLinkTrigger$ = domSource$.select(".export-clipboard-link").events("click") + const exportSignatureTrigger$ = domSource$.select(".export-clipboard-signature").events("click") + // const exportPdfTrigger$ = domSource$.select(".export-pdf").events("click") const modalTrigger$ = domSource$.select(".modal-open-btn").events("click") const modalCloseTrigger$ = domSource$.select(".export-close").events("click") @@ -14,22 +15,33 @@ function intent(domSource$) { return { exportLinkTrigger$: exportLinkTrigger$, exportSignatureTrigger$: exportSignatureTrigger$, - exportPdfTrigger$: exportPdfTrigger$, + // exportPdfTrigger$: exportPdfTrigger$, modalTrigger$: modalTrigger$, modalCloseTrigger$: modalCloseTrigger$, } } -function model(actions) { +function model(actions, state$) { const openModal$ = actions.modalTrigger$ .map(_ => ({ el: '#modal-exporter', state: 'open' })) const closeModal$ = actions.modalCloseTrigger$ .map(_ => ({ el: '#modal-exporter', state: 'close' })) + + const clipboardLink$ = actions.exportLinkTrigger$ + .compose(sampleCombine(state$.map((state) => state.routerInformation.pageStateURL))) + .map(([_, url]) => url) + .remember() + + const clipboardSignature$ = actions.exportSignatureTrigger$ + .compose(sampleCombine(state$.map((state) => state.form.signature.output))) + .map(([_, signature]) => signature) + .remember() return { reducers$: xs.empty(), modal$: xs.merge(openModal$, closeModal$), + clipboard$: xs.merge(clipboardLink$, clipboardSignature$), } } @@ -42,59 +54,70 @@ function view(state$) { div(".fixed-action-btn", [ span(".btn-floating .btn-large", i(".large .material-icons", "share")), ul([ - li(span(".btn-floating .export-link", i(".material-icons", "link"))), - li(span(".btn-floating .export-signature"/* + (signature ? "" : " .disabled")*/, i(".material-icons", "content_copy"))), - li(span(".btn-floating .export-pdf", i(".material-icons", "picture_as_pdf"))), + li(span(".btn-floating .export-clipboard-link", i(".material-icons", "link"))), + li(span(".btn-floating .export-clipboard-signature", i(".material-icons", "content_copy"))), + // li(span(".btn-floating .export-file-report", i(".material-icons", "picture_as_pdf"))), li(span(".btn-floating .modal-open-btn", i(".material-icons", "open_with"))), ]) ])) .startWith(div()) - const modal$ = signaturePresent$ - .map((signature) => - div([ + const modal$ = xs + .combine( + signaturePresent$, + state$.map((state) => state.routerInformation.pageStateURL), + ) + .map(([signature, url]) => { + const signatureAvailable = signature ? "" : " .disabled" + const plotsAvailable = " .disabled" + const topTableAvailable = " .disabled" + const bottomTableAvailable = " .disabled" + const reportAvailable = " .disabled" + + return div([ div("#modal-exporter.modal", [ div(".modal-content", [ - div(".row", + div(".row .title", p(".col .s12", "Export to clipboard or file") ), div(".row", [ - span(".col .s4", "Create link to this page's state"), - span(".btn .col .s1 .offset-s1", i(".material-icons", "content_copy")), - span(".btn .col .s1 .offset-s1", i(".material-icons", "file_download")), + span(".col .s6 .push-s1", "Create link to this page's state"), + span(".btn .col .s1 .offset-s1 .export-clipboard-link", i(".material-icons", "content_copy")), + span(".btn .col .s1 .offset-s1 export-file-link", i(".material-icons", "file_download")), ]), div(".row", [ - span(".col .s4", "Copy signature"), - span(".btn .col .s1 .offset-s1" + (signature ? "" : " .disabled"), i(".material-icons", "content_copy")), - span(".btn .col .s1 .offset-s1" + (signature ? "" : " .disabled"), i(".material-icons", "file_download")), + span(".col .s6 .push-s1", "Copy signature"), + span(".btn .col .s1 .offset-s1 .export-clipboard-signature" + signatureAvailable, i(".material-icons", "content_copy")), + span(".btn .col .s1 .offset-s1 .export-file-signature" + signatureAvailable, i(".material-icons", "file_download")), ]), div(".row", [ - span(".col .s4", "Copy binned plots"), - span(".btn .col .s1 .offset-s1", i(".material-icons", "content_copy")), - span(".btn .col .s1 .offset-s1", i(".material-icons", "file_download")), + span(".col .s6 .push-s1", "Copy binned plots"), + span(".btn .col .s1 .offset-s1 .export-clipboard-plots" + plotsAvailable, i(".material-icons", "content_copy")), + span(".btn .col .s1 .offset-s1 .export-file-plots" + plotsAvailable, i(".material-icons", "file_download")), ]), div(".row", [ - span(".col .s4", "Copy top table"), - span(".btn .col .s1 .offset-s1", i(".material-icons", "content_copy")), - span(".btn .col .s1 .offset-s1", i(".material-icons", "file_download")), + span(".col .s6 .push-s1", "Copy top table"), + span(".btn .col .s1 .offset-s1 .export-clipboard-toptable" + topTableAvailable, i(".material-icons", "content_copy")), + span(".btn .col .s1 .offset-s1 .export-file-toptable" + topTableAvailable, i(".material-icons", "file_download")), ]), div(".row", [ - span(".col .s4", "Copy bottom table"), - span(".btn .col .s1 .offset-s1", i(".material-icons", "content_copy")), - span(".btn .col .s1 .offset-s1", i(".material-icons", "file_download")), + span(".col .s6 .push-s1", "Copy bottom table"), + span(".btn .col .s1 .offset-s1 .export-clipboard-bottomtable" + bottomTableAvailable, i(".material-icons", "content_copy")), + span(".btn .col .s1 .offset-s1 .export-file-bottomtable" + bottomTableAvailable, i(".material-icons", "file_download")), ]), div(".row", [ - span(".col .s4", "Export report"), + span(".col .s6 .push-s1", "Export report"), // span(".btn .col .s1 .offset-s1", i(".material-icons", "content_copy")), - span(".btn .col .s1 .offset-s3 .disabled", i(".material-icons", "file_download")), + span(".btn .col .s1 .offset-s3 .export-file-report" + reportAvailable, i(".material-icons", "file_download")), ]), ]), div(".modal-footer", [ button(".export-close .col .s8 .push-s2 .btn", "Close"), + div(".col .s12 .blue.lighten-3", {style: {wordWrap: "break-word"}}, url), ]), ]), ]) - ) + }) .startWith(div()) const vdom$ = xs.combine( @@ -123,7 +146,7 @@ function Exporter(sources) { const vdom$ = view(state$) - const model_ = model(actions) + const model_ = model(actions, state$) const fabInit$ = xs.of({ state: "init", @@ -140,6 +163,7 @@ function Exporter(sources) { onion: model_.reducers$, fab: fabInit$, modal: model_.modal$, + clipboard: model_.clipboard$, } } diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index f59c7be5..6b3da100 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -289,9 +289,8 @@ export default function GenericTreatmentWorkflow(sources) { tailTable.DOM, displayPlots$, exporter.DOM, - state$.map((state) => state.routerInformation.pageStateURL) ) - .map(([formDOM, filter, plots, headTable, tailTable, displayPlots, exporter, url]) => + .map(([formDOM, filter, plots, headTable, tailTable, displayPlots, exporter]) => div(workflowMainDivClass /* something like ".row .genetic" */ , { style: { margin: "0px 0px 0px 0px" } }, [ formDOM, div(".col .s10 .offset-s1", pageStyle, [ @@ -303,22 +302,10 @@ export default function GenericTreatmentWorkflow(sources) { div(".row", []), div(".row", [displayPlots === "after tables" ? plots : div()]), ]), - div(".col .s10 .offset-s1", [ - div(".col .s12 .blue.lighten-3", {style: {wordWrap: "break-word"}}, url), - div([ - button(".clipboard .col .s4 .offset-s4 .btn .grey", "Copy to clipboard"), - ]) - ]), exporter, ]) ) - const clipboardTrigger$ = sources.DOM.select(".clipboard").events("click").remember() - const clipboard$ = clipboardTrigger$ - .compose(sampleCombine(state$.map((state) => state.routerInformation.pageStateURL))) - .map(([_, url]) => url) - .remember() - return { log: xs.merge( logger(state$, "state$"), @@ -353,6 +340,6 @@ export default function GenericTreatmentWorkflow(sources) { modal: xs.merge(TreatmentFormSink.modal, exporter.modal), ac: TreatmentFormSink.ac, fab: exporter.fab, - clipboard: clipboard$, + clipboard: exporter.clipboard, } } diff --git a/src/sass/_exporter.scss b/src/sass/_exporter.scss index 5a165ccf..6f154157 100644 --- a/src/sass/_exporter.scss +++ b/src/sass/_exporter.scss @@ -13,10 +13,10 @@ background-color: color('red', 'base'); } > ul { - span.export-link { + span.export-clipboard-link { background-color: color('red', 'base') } - span.export-signature { + span.export-clipboard-signature { background-color: color('yellow', 'darken-1') } span.export-pdf { From 15ede360ad1a8541aee89890b1db86c21c91d1b4 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 1 Feb 2022 16:08:58 +0100 Subject: [PATCH 125/191] add download of url and signature pass url & signature to modal creation so we can add the required download functionality --- src/js/components/Exporter.js | 39 ++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 8335425f..9bac0446 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -1,5 +1,5 @@ import xs from "xstream" -import { div, i, ul, li, p, input, button, span } from "@cycle/dom" +import { div, i, ul, li, p, input, button, span, a } from "@cycle/dom" import { loggerFactory } from "../utils/logger" import delay from "xstream/extra/delay" import sampleCombine from "xstream/extra/sampleCombine" @@ -33,7 +33,7 @@ function model(actions, state$) { .map(([_, url]) => url) .remember() - const clipboardSignature$ = actions.exportSignatureTrigger$ + const clipboardSignature$ = actions.exportSignatureTrigger$ .compose(sampleCombine(state$.map((state) => state.form.signature.output))) .map(([_, signature]) => signature) .remember() @@ -62,18 +62,25 @@ function view(state$) { ])) .startWith(div()) + const url$ = state$.map((state) => state.routerInformation.pageStateURL) + const signature$ = state$.map((state) => state.form.signature.output) + const modal$ = xs .combine( signaturePresent$, - state$.map((state) => state.routerInformation.pageStateURL), + url$, + signature$, ) - .map(([signature, url]) => { - const signatureAvailable = signature ? "" : " .disabled" + .map(([signaturePresent, url, signature]) => { + const signatureAvailable = signaturePresent ? "" : " .disabled" const plotsAvailable = " .disabled" const topTableAvailable = " .disabled" const bottomTableAvailable = " .disabled" const reportAvailable = " .disabled" + const urlFile = "text/plain;charset=utf-8," + url + const signatureFile = "text/plain;charset=utf-8," + signature + return div([ div("#modal-exporter.modal", [ div(".modal-content", [ @@ -83,12 +90,30 @@ function view(state$) { div(".row", [ span(".col .s6 .push-s1", "Create link to this page's state"), span(".btn .col .s1 .offset-s1 .export-clipboard-link", i(".material-icons", "content_copy")), - span(".btn .col .s1 .offset-s1 export-file-link", i(".material-icons", "file_download")), + // span(".btn .col .s1 .offset-s1 .export-file-link", i(".material-icons", "file_download")), + a(".btn .col .s1 .offset-s1", + { + props: { + href: "data:" + urlFile, + download: "url.txt", + }, + }, + i(".material-icons", "file_download"), + ), ]), div(".row", [ span(".col .s6 .push-s1", "Copy signature"), span(".btn .col .s1 .offset-s1 .export-clipboard-signature" + signatureAvailable, i(".material-icons", "content_copy")), - span(".btn .col .s1 .offset-s1 .export-file-signature" + signatureAvailable, i(".material-icons", "file_download")), + // span(".btn .col .s1 .offset-s1 .export-file-signature" + signatureAvailable, i(".material-icons", "file_download")), + a(".btn .col .s1 .offset-s1" + signatureAvailable, + { + props: { + href: "data:" + signatureFile, + download: "signature.txt", + }, + }, + i(".material-icons", "file_download"), + ), ]), div(".row", [ span(".col .s6 .push-s1", "Copy binned plots"), From 0f8a644c5f28242a32516a443a67b9e51deeb2ee Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 1 Feb 2022 17:44:56 +0100 Subject: [PATCH 126/191] add file exports in modal --- src/js/components/Exporter.js | 116 ++++++++++++++++++++++++++-------- 1 file changed, 90 insertions(+), 26 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 9bac0446..f4697fc1 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -3,6 +3,7 @@ import { div, i, ul, li, p, input, button, span, a } from "@cycle/dom" import { loggerFactory } from "../utils/logger" import delay from "xstream/extra/delay" import sampleCombine from "xstream/extra/sampleCombine" +import { convertToCSV } from "../utils/export" function intent(domSource$) { const exportLinkTrigger$ = domSource$.select(".export-clipboard-link").events("click") @@ -38,18 +39,48 @@ function model(actions, state$) { .map(([_, signature]) => signature) .remember() + function notEmpty(data) { + return data != undefined && data != "" + } + + const signaturePresent$ = state$.map((state) => notEmpty(state.form.signature.output)).startWith(false) + const headTablePresent$ = state$.map((state) => notEmpty(state.headTable.data)).startWith(false) + const tailTablePresent$ = state$.map((state) => notEmpty(state.tailTable.data)).startWith(false) + + const url$ = state$.map((state) => state.routerInformation.pageStateURL).startWith("") + const signature$ = state$.map((state) => state.form.signature.output).startWith("") + + const headTableCsv$ = state$.map((state) => state.headTable.data) + .filter((data) => notEmpty(data)) + .map((data) => convertToCSV(data)) + .startWith("") + + const tailTableCsv$ = state$.map((state) => state.tailTable.data) + .filter((data) => notEmpty(data)) + .map((data) => convertToCSV(data)) + .startWith("") + return { reducers$: xs.empty(), modal$: xs.merge(openModal$, closeModal$), clipboard$: xs.merge(clipboardLink$, clipboardSignature$), + dataPresent: { + signaturePresent$: signaturePresent$, + headTablePresent$: headTablePresent$, + tailTablePresent$: tailTablePresent$, + }, + exportData: { + url$: url$, + signature$: signature$, + headTableCsv$: headTableCsv$, + tailTableCsv$: tailTableCsv$, + } } } -function view(state$) { +function view(state$, dataPresent, exportData) { - const signaturePresent$ = state$.map((state) => state.form.signature.output != undefined && state.form.signature.output != "") - - const fab$ = signaturePresent$ + const fab$ = dataPresent.signaturePresent$ .map((signature) => div(".fixed-action-btn", [ span(".btn-floating .btn-large", i(".large .material-icons", "share")), @@ -62,24 +93,30 @@ function view(state$) { ])) .startWith(div()) - const url$ = state$.map((state) => state.routerInformation.pageStateURL) - const signature$ = state$.map((state) => state.form.signature.output) - const modal$ = xs .combine( - signaturePresent$, - url$, - signature$, + dataPresent.signaturePresent$, + dataPresent.headTablePresent$, + dataPresent.tailTablePresent$, + exportData.url$, + exportData.signature$, + exportData.headTableCsv$, + exportData.tailTableCsv$, ) - .map(([signaturePresent, url, signature]) => { + .map(([signaturePresent, headTablePresent, tailTablePresent, url, signature, headTableCsv, tailTableCsv]) => { const signatureAvailable = signaturePresent ? "" : " .disabled" - const plotsAvailable = " .disabled" - const topTableAvailable = " .disabled" - const bottomTableAvailable = " .disabled" + const plotsAvailable = ""//" .disabled" + const headTableAvailable = headTablePresent ? "" : " .disabled" + const tailTableAvailable = tailTablePresent ? "" : " .disabled" const reportAvailable = " .disabled" - const urlFile = "text/plain;charset=utf-8," + url - const signatureFile = "text/plain;charset=utf-8," + signature + const urlFile = "data:text/plain;charset=utf-8," + url + const signatureFile = "data:text/plain;charset=utf-8," + signature + const plotsFile = "data:image/png;base64,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" + + const headTableCsvFile = "data:text/tsv;charset=utf-8," + encodeURIComponent(headTableCsv) + const tailTableCsvFile = "data:text/tsv;charset=utf-8," + encodeURIComponent(tailTableCsv) + return div([ div("#modal-exporter.modal", [ @@ -94,7 +131,7 @@ function view(state$) { a(".btn .col .s1 .offset-s1", { props: { - href: "data:" + urlFile, + href: urlFile, download: "url.txt", }, }, @@ -108,7 +145,7 @@ function view(state$) { a(".btn .col .s1 .offset-s1" + signatureAvailable, { props: { - href: "data:" + signatureFile, + href: signatureFile, download: "signature.txt", }, }, @@ -118,17 +155,44 @@ function view(state$) { div(".row", [ span(".col .s6 .push-s1", "Copy binned plots"), span(".btn .col .s1 .offset-s1 .export-clipboard-plots" + plotsAvailable, i(".material-icons", "content_copy")), - span(".btn .col .s1 .offset-s1 .export-file-plots" + plotsAvailable, i(".material-icons", "file_download")), + // span(".btn .col .s1 .offset-s1 .export-file-plots" + plotsAvailable, i(".material-icons", "file_download")), + a(".btn .col .s1 .offset-s1" + plotsAvailable, + { + props: { + href: plotsFile, + download: "plot.png", + }, + }, + i(".material-icons", "file_download"), + ), ]), div(".row", [ span(".col .s6 .push-s1", "Copy top table"), - span(".btn .col .s1 .offset-s1 .export-clipboard-toptable" + topTableAvailable, i(".material-icons", "content_copy")), - span(".btn .col .s1 .offset-s1 .export-file-toptable" + topTableAvailable, i(".material-icons", "file_download")), + span(".btn .col .s1 .offset-s1 .export-clipboard-toptable" + headTableAvailable, i(".material-icons", "content_copy")), + // span(".btn .col .s1 .offset-s1 .export-file-toptable" + headTableAvailable, i(".material-icons", "file_download")), + a(".btn .col .s1 .offset-s1" + headTableAvailable, + { + props: { + href: headTableCsvFile, + download: "table.tsv", + }, + }, + i(".material-icons", "file_download"), + ), ]), div(".row", [ span(".col .s6 .push-s1", "Copy bottom table"), - span(".btn .col .s1 .offset-s1 .export-clipboard-bottomtable" + bottomTableAvailable, i(".material-icons", "content_copy")), - span(".btn .col .s1 .offset-s1 .export-file-bottomtable" + bottomTableAvailable, i(".material-icons", "file_download")), + span(".btn .col .s1 .offset-s1 .export-clipboard-bottomtable" + tailTableAvailable, i(".material-icons", "content_copy")), + // span(".btn .col .s1 .offset-s1 .export-file-bottomtable" + bottomTableAvailable, i(".material-icons", "file_download")), + a(".btn .col .s1 .offset-s1" + tailTableAvailable, + { + props: { + href: tailTableCsvFile, + download: "table.tsv", + }, + }, + i(".material-icons", "file_download"), + ), ]), div(".row", [ span(".col .s6 .push-s1", "Export report"), @@ -143,7 +207,7 @@ function view(state$) { ]), ]) }) - .startWith(div()) + .startWith(div("#modal-exporter.modal", "empty")) const vdom$ = xs.combine( fab$, @@ -169,10 +233,10 @@ function Exporter(sources) { const actions = intent(sources.DOM) - const vdom$ = view(state$) - const model_ = model(actions, state$) + const vdom$ = view(state$, model_.dataPresent, model_.exportData) + const fabInit$ = xs.of({ state: "init", element: ".fixed-action-btn", From 61601dbc0b8dce88dae1d14cc9aaf0bb7e8f5b9b Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 1 Feb 2022 18:01:56 +0100 Subject: [PATCH 127/191] Add placeholders for exporting plots --- src/js/components/Exporter.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index f4697fc1..7cbe49c2 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -44,11 +44,13 @@ function model(actions, state$) { } const signaturePresent$ = state$.map((state) => notEmpty(state.form.signature.output)).startWith(false) + const plotsPresent$ = xs.of(true) const headTablePresent$ = state$.map((state) => notEmpty(state.headTable.data)).startWith(false) const tailTablePresent$ = state$.map((state) => notEmpty(state.tailTable.data)).startWith(false) const url$ = state$.map((state) => state.routerInformation.pageStateURL).startWith("") const signature$ = state$.map((state) => state.form.signature.output).startWith("") + const plots$ = xs.of("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") const headTableCsv$ = state$.map((state) => state.headTable.data) .filter((data) => notEmpty(data)) @@ -66,12 +68,14 @@ function model(actions, state$) { clipboard$: xs.merge(clipboardLink$, clipboardSignature$), dataPresent: { signaturePresent$: signaturePresent$, + plotsPresent$: plotsPresent$, headTablePresent$: headTablePresent$, tailTablePresent$: tailTablePresent$, }, exportData: { url$: url$, signature$: signature$, + plots$: plots$, headTableCsv$: headTableCsv$, tailTableCsv$: tailTableCsv$, } @@ -96,28 +100,28 @@ function view(state$, dataPresent, exportData) { const modal$ = xs .combine( dataPresent.signaturePresent$, + dataPresent.plotsPresent$, dataPresent.headTablePresent$, dataPresent.tailTablePresent$, exportData.url$, exportData.signature$, + exportData.plots$, exportData.headTableCsv$, exportData.tailTableCsv$, ) - .map(([signaturePresent, headTablePresent, tailTablePresent, url, signature, headTableCsv, tailTableCsv]) => { + .map(([signaturePresent, plotsPresent, headTablePresent, tailTablePresent, url, signature, plots, headTableCsv, tailTableCsv]) => { const signatureAvailable = signaturePresent ? "" : " .disabled" - const plotsAvailable = ""//" .disabled" + const plotsAvailable = plotsPresent ? "" : " .disabled" const headTableAvailable = headTablePresent ? "" : " .disabled" const tailTableAvailable = tailTablePresent ? "" : " .disabled" const reportAvailable = " .disabled" const urlFile = "data:text/plain;charset=utf-8," + url const signatureFile = "data:text/plain;charset=utf-8," + signature - const plotsFile = "data:image/png;base64,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" - + const plotsFile = "data:image/png;base64," + plots const headTableCsvFile = "data:text/tsv;charset=utf-8," + encodeURIComponent(headTableCsv) const tailTableCsvFile = "data:text/tsv;charset=utf-8," + encodeURIComponent(tailTableCsv) - return div([ div("#modal-exporter.modal", [ div(".modal-content", [ From b3bb2f93e42137cef674dffa63b5dc1789a36f9b Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 1 Feb 2022 18:28:58 +0100 Subject: [PATCH 128/191] Add extra support for exporting to clipboard --- src/js/components/Exporter.js | 54 ++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 7cbe49c2..9b76bcd1 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -8,7 +8,9 @@ import { convertToCSV } from "../utils/export" function intent(domSource$) { const exportLinkTrigger$ = domSource$.select(".export-clipboard-link").events("click") const exportSignatureTrigger$ = domSource$.select(".export-clipboard-signature").events("click") - // const exportPdfTrigger$ = domSource$.select(".export-pdf").events("click") + const exportPlotsTrigger$ = domSource$.select(".export-clipboard-plots").events("click") + const exportHeadTableTrigger$ = domSource$.select(".export-clipboard-headTable").events("click") + const exportTailTableTrigger$ = domSource$.select(".export-clipboard-tailTable").events("click") const modalTrigger$ = domSource$.select(".modal-open-btn").events("click") const modalCloseTrigger$ = domSource$.select(".export-close").events("click") @@ -16,7 +18,9 @@ function intent(domSource$) { return { exportLinkTrigger$: exportLinkTrigger$, exportSignatureTrigger$: exportSignatureTrigger$, - // exportPdfTrigger$: exportPdfTrigger$, + exportPlotsTrigger$: exportPlotsTrigger$, + exportHeadTableTrigger$: exportHeadTableTrigger$, + exportTailTableTrigger$: exportTailTableTrigger$, modalTrigger$: modalTrigger$, modalCloseTrigger$: modalCloseTrigger$, } @@ -29,16 +33,6 @@ function model(actions, state$) { const closeModal$ = actions.modalCloseTrigger$ .map(_ => ({ el: '#modal-exporter', state: 'close' })) - const clipboardLink$ = actions.exportLinkTrigger$ - .compose(sampleCombine(state$.map((state) => state.routerInformation.pageStateURL))) - .map(([_, url]) => url) - .remember() - - const clipboardSignature$ = actions.exportSignatureTrigger$ - .compose(sampleCombine(state$.map((state) => state.form.signature.output))) - .map(([_, signature]) => signature) - .remember() - function notEmpty(data) { return data != undefined && data != "" } @@ -61,11 +55,37 @@ function model(actions, state$) { .filter((data) => notEmpty(data)) .map((data) => convertToCSV(data)) .startWith("") + + const clipboardLink$ = actions.exportLinkTrigger$ + .compose(sampleCombine(url$)) + .map(([_, url]) => url) + .remember() + + const clipboardSignature$ = actions.exportSignatureTrigger$ + .compose(sampleCombine(signature$)) + .map(([_, signature]) => signature) + .remember() + + const clipboardPlots$ = actions.exportPlotsTrigger$ + .compose(sampleCombine(plots$)) + .map(([_, plots]) => "data:image/png;base64,"+plots) + .remember().debug("clipboardPlots$") + + const clipboardHeadTable$ = actions.exportHeadTableTrigger$ + .compose(sampleCombine(headTableCsv$)) + .map(([_, table]) => table) + .remember() + + const clipboardTailTable$ = actions.exportTailTableTrigger$ + .compose(sampleCombine(tailTableCsv$)) + .map(([_, table]) => table) + .remember() + return { reducers$: xs.empty(), modal$: xs.merge(openModal$, closeModal$), - clipboard$: xs.merge(clipboardLink$, clipboardSignature$), + clipboard$: xs.merge(clipboardLink$, clipboardSignature$, clipboardPlots$, clipboardHeadTable$, clipboardTailTable$), dataPresent: { signaturePresent$: signaturePresent$, plotsPresent$: plotsPresent$, @@ -172,8 +192,8 @@ function view(state$, dataPresent, exportData) { ]), div(".row", [ span(".col .s6 .push-s1", "Copy top table"), - span(".btn .col .s1 .offset-s1 .export-clipboard-toptable" + headTableAvailable, i(".material-icons", "content_copy")), - // span(".btn .col .s1 .offset-s1 .export-file-toptable" + headTableAvailable, i(".material-icons", "file_download")), + span(".btn .col .s1 .offset-s1 .export-clipboard-headTable" + headTableAvailable, i(".material-icons", "content_copy")), + // span(".btn .col .s1 .offset-s1 .export-file-headTable" + headTableAvailable, i(".material-icons", "file_download")), a(".btn .col .s1 .offset-s1" + headTableAvailable, { props: { @@ -186,8 +206,8 @@ function view(state$, dataPresent, exportData) { ]), div(".row", [ span(".col .s6 .push-s1", "Copy bottom table"), - span(".btn .col .s1 .offset-s1 .export-clipboard-bottomtable" + tailTableAvailable, i(".material-icons", "content_copy")), - // span(".btn .col .s1 .offset-s1 .export-file-bottomtable" + bottomTableAvailable, i(".material-icons", "file_download")), + span(".btn .col .s1 .offset-s1 .export-clipboard-tailTable" + tailTableAvailable, i(".material-icons", "content_copy")), + // span(".btn .col .s1 .offset-s1 .export-file-tailTable" + bottomTableAvailable, i(".material-icons", "file_download")), a(".btn .col .s1 .offset-s1" + tailTableAvailable, { props: { From 0efbe0e2302ee935deca5181f7ec683e50f0ee0e Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 2 Feb 2022 10:06:47 +0100 Subject: [PATCH 129/191] Provide way to paste images in clipboard copying of plots is with fixed data --- src/js/components/Exporter.js | 22 ++++++++++++++++++---- src/js/drivers/clipboardDriver.js | 13 ++++++++++++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 9b76bcd1..82d5f167 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -44,7 +44,7 @@ function model(actions, state$) { const url$ = state$.map((state) => state.routerInformation.pageStateURL).startWith("") const signature$ = state$.map((state) => state.form.signature.output).startWith("") - const plots$ = xs.of("iVBORw0KGgoAAAANSUhEUgAAAV0AAAFDCAYAAACZVN1cAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAuIwAALiMBeKU/dgAAActpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+QWRvYmUgSW1hZ2VSZWFkeTwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KKS7NPQAAMpBJREFUeAHtneuV21ayha1Z89+8ETRuBOJEIDgC80YgKAJxIhAUgekIhI7AVARGR2AqAqMjGDqCuXu3gV4kRJB1HgDx2GetEl51qup8ADcOQYr95gdj++9//5vDdQv70djF4vbMmG/evNlbnK/5oL4Vju9g76/56ZgI9ESA13J1Erus149YHriO67zkUm3ZBN5Yhg9BK+DXp5h9wAXJHF6tFtwSnd96BVAnERiOwF9IRRFuxJjrFa7/F2EergxluheBm6ILQctQ3JcBCvxfXHiVT54Bbgo+ZamPCLgS+IYOFN8Xw+uhdA0g//ETsIguT/y7AYbyb1xkO588EN3/+vRTHxGYAAEKcQmjEJe+ExP0VRsJgX8a6lgbfGK4rHyCQG+Hqs+nPPURgVACfGT2+tgM1zufHZe17SHCR6yrTYjAPwy1Vgafe7qMvb57slHu+RF4wJDew77A/gMRPsByTT6mc6ItonsYaDiVT576Ts+7v5oILJEAZ8GfYH9AeI+wArZZIoipjNnyTDfBYCi8P/Y4qG8Qz7Vv/Poi+823v/qJwAwJ8FsSexpeW1yqjYTATdFlnbWoFVjtQ3j5QUEa+mwKNWaIw7dcaiIgAucEKMAFDa8zTqDU7kjAJLqsD6K2wiKDpTCuhzae/AMugiI0UNMfNSZYz2ApTE0EhiKQINHDUMkC8/BR3A5GAT4GxlJ3DwJm0fWIrS4isEgCuPmn9cA5OVnX69zHbT6DHUt7RCEU33IsBS2hDonuEs6yxjgqAvU7sgRFUZBpXH8Hu1d7RuI85rvOew1kCnklulM4S6pxEQQgxo0Ic5nChp4V89nvjqZHD6CgJgIisDwCfFQBy2ElbMhWIFmyPOIasQiIgAicEIAQbmA7WAUbokl8T/hrVQREYMEEoLgJbAsrB1Bfie+CrzUNXQREoEUAoruCZbB9zwIs8W2x16YIiMDCCdQCzBnwoUcBzpln4ag1fBEQARE4JwBhTGA72BEWuzHm9jyjtkRABERABF4IQCD5+KGMrbyIV8FSYRYBERABEbhAAALJ2W8Biz37paAnF1JqlwiIgAiIAARyBcthFSxWo5DnoisCIiACInCFAIQyg8UUX8ZKr6Rc7CH9N+D61OMC4Sex/FAgrXfFXhwRkL9tWsQO3MTDGBKscwzrZp+WgxAoT7JUWKf9MMUfksE1lKH0HBbrV9N+ZTz9t2JQqJtEFyBwoVGkSlgfvxeMsGftCVub2Bdh/WLZIfYQYzgbkDZuEuA5ZythFW3sghxZfJ8x5mzsY0aNgzSJLjDjAquwiHVnt5y4X3EBckYapaH+BIEOMAluFKKDBaEY8bzRSi5j34wRM6hFFl/NenE2Fi+6uKg24PBb0JXp1/l/Yr3AMIYdSvjoV4Z6jYwAhbhsDNdIhfW7N1xjOYrYwkJv7Bwf3+nxRrPI9o9Fjvp80OvzzcG2YuaNGWswAEp0kQDfcb2HfYH9CbHjB1IFbANbXewxwE6IZI40CewRFtI4Pv4RzTwkyJT7SnSnfPZU+xIINCLMd2P8k+t72BaWDD14vjODZcj7L9hTYP5PGEMJu9uNJLB+7+4S3fqTZm+C/h0P/l2/6xkz1nfBtWNUBH5GNb/A/oRgHWCDCzAfDcBS1PABxh8+923v0JEz+dQ3wBT7vZli0TFrxgnnnbaChT6rcinrsZ4xuPTp9MUYEhz8s9NBB5ZAgDPPArbHtXUcasD16ydHvo+BOT+jbsaZfVu86PIM48JZY1HChhDeb8iTxn5hYAwZ4n6BqS2bAGeee9gO19hhKBS4/lLkKmB8HOLbvqIjv1o22E3Dt9CQfnq8AHr1xUnhfYSFvF26di4otp9h0QWXSTGGAoufYLxw+xoDQquNnAAnDu9h/LCKjx8y2KrvmnH9lcjB19CvAbl+Rl8+52Wc2TbNdGd7ajWwIQhAINKTPAnWaWzpy7/3/Su/dQkvN+EdNgqIY9Xs7GtZMykQ33fWy0lDhlr3fdWouCIgAjMmwJkmhQjGWWcO42zuCLtHK5A06Rs3cnDMu8ABbvuuU/FFQAQWRIDiB9vAGiEO1Cin7kOJL8d3dKrs3LlY0CWhoYqACAxNAHqTwoYU4d7FF+NJYCXMt7HvauhzoXwiIAILI0ChgXGmSGEMmS2i+83GHEmfiBE/v1lFt8MBhyS8fZ4gxRYBETgnANFJYTtYBeurMX5v4obYG9jRs/gD+q3PqWhLBERABAYgUIsXZ6e+AnZN9xhz29cwEDuBUUB9GmuT8PZ1chRXBETgOgEI0AqWwUpY7EZhTK9X4HcUcVk3bxo+TcLrh129REAEYhKAenEGSSGjKMVsjNnLIwfEzT0LlfDGvHgUSwREwJ8ABRK2hVWegnapG0Uu86+quyfjwhjftbGPHjV0o9URERCBoQlAlChoMcW3RLwk9jgonjAJb2ywiicCInAfAhC0FEbBjNEojlnskSAmhffgUSDr0Yw39glRPBEQgXACECeKb+UhbJe67LEz6rNexoPNWnij/eANQPFOswm/LBTBkwB/R/Xg2dfUDec4gSPP8crUYf5OFYZIYzv2zf/vNHH+xbnMECmH+f4oDbq+NP44zQZjL//eDP8XtfH6Yry3jtFYC3/Fr9fXgWNN37kHi24NaI/I776Lrh1DE/iKhPx1pmPsxDjPO8T8GDvuTON9w7h4Dg71ssSywnmpsBxVw3nNUdAWxp+EDGmfMb48JMBp31pXeM3xZypd2uiFN4boliAiwXW5LPr1/YqLfxMzhQQ3Gk0KAoW4rJeHMQhxgMBhGGftCVuc9R7P9gZsoLYC3X2EN4lZR8AQvusaJLoAkiHil++iase9CfwLFxxf3MEN5zhBkD+DAylAF4FnHCgbu6cI41ynqGMHc31bjy6vjTeW2I8bCsR0FV6+2+Cjhmg3AMSL0kL/csQ6ShUKEpvAJmLAmLEiljWbUA8YCQXlC+xPCB8/RNrBBn9tQaBKGPN+hlE8fRofU/yO+rc+nS/1QU0Z9j9eOnZlH28c5ZXjdzsk0b0b+skkXk2m0nkUSrH4COOf2znCClg65NAgcjnyUXyfAvL+wtoD+p919RXemDWcFRSwESq6ZUBude2PwDFi6EPEWArlRoCzRs6Cf4d48GtenAEnbiH8vCFy/OAvRe9/w3xnve9RL2fuUW7cnsLLGnKMYTTtTUglGAzvhn+ExFDfXgj8L180MSLXLxjGogCojYMAn1fucI6LIcrBNZAgzx721jMf681Qb5QbOOphHNdaPgzF6xajoJluDfHXW0l0fFACn3FeqlgZEeuIWNtY8RQnCgEKzheIDx8/9D775fUEWyMnn/X6NNZbolbGiNFSBKGQuzRyipXfJe93vkEz3SYaBpNjnS/MH5t9Wg5O4C9kzPHi2PWRGed4g7iMzQ9+1MZH4BElcfZ76LM0XAcp4u9hPq91XqOc8bJ/UEMdKwQoYRR0a2P+BPmP1g59+EUR3aaw+oQ0m1oOSAAXUjlEOpzjNfLwgl96SwCA1rS0XiEfH0Fq4oQunxCAN98yNFBX/1rw9jj+rsvnxv4PqK+44XPzMOpI4HSAufD+htw8R2oiIAJzIsCbE2wDy2F7GD8IG7Lx7XzaJ1PE51t235bFqA3JyfnoWMQuRm7FEAERGDkBCMMKlsIoxBTFIVqBJElfaBA7g7mKXjPuLEZdCLZpAjoso+SOUb9iiIAIDEgAIpHCdrCDg2D4uObotOpjaIi7hh19ikKfLEZNjOOYn/XqMUMM+IohAlMlABFIYFvYAdZHo9BkffBB3BXMt+4oNSF/AXNprLeXG1EfjBVTBESgRwIQgz4FuET86LM8ChiMsX1aFgMnErsK/y5GXsUQARGYEQEIyRrGWdwRFrPlfWBCgazVp2Wh9SAphd+V0yY0r/qLgAjMkEAtKFssK1isdkCgPma99xTetSMcivRqhpeMhiQCIhCLAEQig5WO4nLNPY9VWxMHyXiD8GnBNwEkdc1dNnVrKQIiIAKdBCAuKSyW+DJO0pnM4wDiZTDXxplnDOHdOybeegxRXURABJZIAOJC8T04iswldwreJiZDxPMV3lVIHci7glUwa+PYk5Cc6isCIrAwAhANCpyL0HQJUh4TXV1XV66u/byJhArvuit4x/4y5rgVSwREYAEEKFSwHMaZW0gr0TlI9E5xIxZvCK6tOI3hs46EZOHSMp881j5nP3iDqgh4be28EL8KP5BRDTFW8Cf7aBf5EDUvKQeug3JK48X1lKDeHezngLr5E4r8ZbBDQIzXrqipwAZ/mN2lfUb+3KVD2xd5S+yz/kBP/79GxpPDomBqlwlU2J22T2SsbcTewkJnJZcr197YBHYIOKkbI+rdwEKuL/aNdv0jVgFzbZuQ1xuSUeNcGOxD8l3r+waF8AKqYC4/j3Yt5pyP/YQ7bhlzgOC/Q7yPMWMqVu8EJvfzgPXrvACZkFnvB1z/jBHcUE+BIC4zXs4+U+Q/+CZHzi36/uLQ/yfkKx38Ta4U3QKeLoM3BZ6p0zNOQhJrbGDPWH/Giqc4gxL4jGshHzRjhGS45jYIU8B8J1lRxo06VqihhL2FWRsfdVB4j9YObT/kLbHP+pjhGbkS+Edt/HM9Elw70gectNTuftMzu+khh7ESoHhNrkFE9ig6gT15Fv8Jr4HCs+9rt1o4U+x4ft15e4UCvbvtdtUjw1HOmi2Nr/fc4ujiE/Q30lwSyfcigfXFvdo5BQIuM7RRjYeCB0tR1GfPwt5HFF7evKwiyHKZO+OKT8O4K/TLHfpukW/l4H/TVaJ7E1GvDodeoyt4nwRcZmh91uEdGwKUo/NPMBfRa/JR/Ipmw3eJGvgayBz775A7cezz6o6cO2zwUYWl8TEM/aM1iq41ebSkEw8UUyhjxpo41smVX06u4gsFQ4A4jjXMRwdiCe8e+f8NszYKIfuEtMyhM8dJRlEaRXcbJdIygnzGRXqMNVTE4oXzFCue4gxG4C9kygfL1nMiXIcVUqSwR5hriyW8OyT+6pD8LYQwd/A/c8WYD9jx69nO6xusL0p7wygoPsOCQXkHUbtM4FecqO3lQ/57wX6F3nuY9RNV/2TqGYPAM4Js6hdtjHijioHrkTrw0aOoRzDJPPq9dqlfCyV2vH3deXvlX77nos5XIYVV935CrvJ2Sdc9XkSXLnUBGVYpAmrnBArArs53xd0C/xQRaWrjJVChtD2uheN4SwyvDNdihihfPCIFT0yQe428fzjkDvrONPJtkesXY76gXMYcchMBEVgiAQovzKdlobyQlN8YcGl5SE4kOjgkCx5fSK3qKwIiMGMCEKIN7OggSI1rFooFgfZNMOOSM2SvhvipMQfdKq8k6iQCIiACFgIQmTXMVXjp7y2CrAv9V455S8t4unyQq4RZW9YVR/tFQAREIJgAlMhXeFchyZF3Y1XB2o/PZ71aPUZrusoriTqJgAiIgJUA1MjlLXgjXgdr/C4/BNo1wQzLI3y8hR59C0OOxiXrqln7RUAERCAKAahN1iiOw7IISY48K1g1RD7kSBzyVCHjUl8REAERMBGAKPkIb2YK3uGEnK6PGdYdoW7uRq7CQXjTmwHlIAIiIAKhBCBKuYMw0ZVv+72FkPWi/56BjK30HSPiJ8YcdPPO41uf+omACCyUAATHZUZIgTrAQp63Ugwp3taW+p4aJHAZW9DNxLdG9RMBEVggAYgThdSl7UIwIVHukKzyzYUcFHhrK3zzqJ8IiIAIOBGAKq1glVWdar+NU5KWs2O+rNXdvIk8LrPdxBxYjiIgAiIQQgDitK7F1LrgI4KQxwypNRH8Kt+xoa9Lntw3j/qJgAiIgDMBCFTmIIR03TsnOemA/qVDvuykq9OqQ57KKbCcRUAERCCUAASqcBBCuqa+OdE3cchVBeRxuZlkvnnUTwREQAScCUAEXZ/vVuzjnKjugL4uIp8F5GGdlhY0e/etT/1EQAQWTADK5Pp8N/fFhVyJRQlrnyogT+6QJ/HNo34zI4CLhi+GPexYX0AHLLOZDVPDGQEBXFcuIsXLMfEtG30LBjC2zCcPYq+M8emW++RQn5kRwIWQXbloypkNV8MZAQFcb7ypW5v3NYgEiTUJ/ELyWMW9GgF+lXBPAvVF2cxuu67P/J41Kvf8COBCW3ddbB37U18KiGcVRKb2ysN+7Gxsa9+xqN8MCOAi2RoulOMMhqohjIwArrvccO01LpVv+QiQNEEMyyIgT2WITxfvHL61qd+ICOAC2FsulBGVrFJmQgDX3QpmFSpeppnv0NG3YABjS3zyIHZujK9JjA/gufTBRVJaLpS5jFfjGBcBXHsby/VX+1S+1aP/2iFP7pMH8ROHHBufHOozAwK4SCS6MziPUx6C9RqsBS3zHatDniogx6Gu89ai8M2hfhMnYL0QJz5MlT9iArgGXWahle9QkMdlVu01E0WODGZpVx8x/MN3kOonAiIgArcIvHnz5gCfx1t+9fEHKFpm9D1zQ549djyf7eze8BJdhGMOS/sR40gtjvKZGQGceD1emNk5neJwcB26PA+tfMeIPDnM2lY+eRB8b0yw64qvmW4XGe0XARGIQgCz0AqBPhuDPUDUMqNv261o77iy7TvbLa/EPD3kG/80htanRgAXr2a6UztpM60X1+IKdoRZ2sEXA4JbZ6KlTw7Ed5m1J5dyaKZ7iYr2iYAIRCWA2e4RAQtj0LcQt9To23Yr2js6tt9RQDuOde6uZ+3fOh3OD1yc7Up0zyFpSwREoD8CO4fQmYPvq2v9gdpfrzuur1wUxetdXo7uDT50SY1+cpsLAdzJ9XhhLidzJuPANVnArG3lM2wEt+YoPeOvrQO4FF8z3UtUtE8ERKAvAr3PdlG4dSb6DuLpLOyYTR+QwzSbRvy0DVKi2yZi2CZIn5NlCD1pF3GZ9OkbpPhasJ6MybZGvzO3sT9ikOiena7uDQhKAnv5ZBRev8P+g+0Dhaa71/yPYPx8q1XC/ovRNly4vZ7/6DVCTwKFsd9DwHW0N+bYGP3abmV7R8d22rFfu68R4ImHHWFdLbvW/17HUOyLGHYV3ez3rQ/9b3FJfWOr37wJ4Nq59npqLk0udz4k0G9zGuTK+tEzPidhpuYTf/F9QPaWePECWo0NlKHul4vGt250rl4CdP9T+cZWv3kTwCWz675szo54X0OIYhX2tQ9txL91/TcDOYuvxws3aINaApd3N9x+xPHNDZ9ZHQaXFAN6uDGoh9rvhpsOL5DAzjhmXkNnomXsR7fS6Ov72rXGT0/rkOie0ri8nlze/d1eq993HWe+I535+DQ8DwL4sKtCN+t/Msg8UrDL3tgvNfq13cr2jo7ts5uGRLeDknaLgAj0TqAwZuh7JnrrnWxXmYeuA6396em2RPeUhtZFQASGJGCdiXo9Yqhn06afe/R5DFZ//c3Ci/WvGkeJbkNCSxEQgUEJOD5iSD2LK439fONbv3O8buqQ6DYktBQBEbgHgcKYdGP0a7uV7R0d26+i2HG8a3fZdaC1/zW+RLdFRpsiIAKDEiiN2Xyfu1rjp8Y62m5Ve0fHtkS3A4x2i4AIDEigfi7a53PXCsOx/E4C/8RO4jH0g7HPa2zNdI3E5CYCItAbgdIYOTX6td3K9o6O7dfZaMfx73Y7fJj2OlOX6H6HUTtEQAQGJlAa86VGv7abdTbqLLp1ItP3jZtvMEh026dH2yIgAkMTKI0JfUWx7/iVS/0SXSMtuYmACPRDoP7qmOW5Lp+7+ghvZaw8Mfq13Zxm0hLdNj5ti4AI3IOAk3C5FFiLuuXDtLcucU98q5P1a6srHpToXkOkYyIgAkMRKI2JEqNf280k6j3PpNcsSqLbPjXaFgERuAcBkyiisNSzuMrY72U2avRt3Ky1v8SW6DbYtBQBEbgnAatwJZ5FVsZ+qdHv1Q2PL46vG9dX1jws0b0OSUdFQAQGIFALl+XDtFu/4dxVbdl1INJ+y9fG+LvbEt1IwBVGBEQgnEBlCYHnrqnFz9PHN7Zptsvv6mqm63lm1E0ERCA6AesjBufEmEmXzp3cOphEFyHXEl03sPIWARHoj4BVuNL+Svgh8YxtvmFIdD0Jq5sIiEB0AmX0iOcBLc9dfZ8Zn2e6siXRvQJHh0RABEZJYO1ZlXUm7RPeGjuV6PrgVR8REIHoBByeu66iJz8JiA+7kpNN6+rB6ijRtZKSnwiIwNQJWIUx6XOgEt0+6Sq2CIhAHwRef5vWMbj1EYBjWDd3ia4bL3mLgAj0S+Cp3/B3j55IdO9+DlSACIjADAhUxjFIdI2g5CYCIrAcAmvXoeJDwMraRzNdK6me/fCJaQY7wJrG9U3PaRVeBJZEoDIOdmX083KT6Hphi9sJ4log4hfY6Y8oc/03HMuxVBMBEQgnUIWHCI8g0Q1nGBQBopohwPsrQT7BJ71yXIdEQAQmRECie/+TtTWUkBl85CICIjABAhLd+5+k00cKXdUkXQe0XwREYFoEJLrTOl+qVgREYOIEJLoTP4EqXwREYFoEJLrTOl+qVgREYOIEJLoTP4EqXwREYFoEJLrTOl+qVgREYOIEJLoTP4EqXwREYFoEJLrTOl+qVgREYOIEJLoTP4EqXwREYFoEJLrTOl+qVgREYOIEJLoTP4EqXwREYFoEJLrTOl+qVgREYOIE/jnx+lW+CIjAHQnUv4CX1iXs8WPehzuWM4nUEt1JnCYVKQLjIgCxXaOiPezhpDL+DCn/xtkG4ns82a/VEwJ6vHACQ6siIAK3CUBYE3iVsFPBxeZLe4d/S/is6m0tWgQkui0g2hQBEbhJIIfHj1e8+HOl2ZXjiz4k0V306dfgRcCLQGrotTH4LNJForvI065Bi0AQgUuPFYICLqmzRHdJZ1tjFQERsBA4WJx8fSS6vuTUTwREYFIE8I2KEgU/3yj6LxynX29NotsbWgUWAREYIYHtjZq2fX/dTaJ74wzosAiIwHwIQFD3GM3/wdozXs5wP+B4gWWvTf85ole8Ci4C8yKA79+mxhEdjX6Du9XCuz8dC/aVIYWcxroVR6J7i5COi4AI+BDw/TAq8Unm0ydUaH1yok+pxwue5NRNBBZKYNXzuC1fR2s/Gui5pLjhJbpxeSqaCMydwNo4wD4fL1TGGkbpJtEd5WlRUSIwWgKJsTLnxwt4LmqNbSxhUDfzOwCJ7qDnRclEYPIEEuMIfGa61tjOgm6sOcRtbexcSXSNpOQmAiLwQsAkLviQykcYEyPjo9FvjG4S3TGeFdUkAmMkUL/9v/brYk3Z35oVx2Vi9K+MfkO6razJNNO1kpKfCIiAaZYLTJUnqr7je5Zl6mauXaJr4iknERABELAKy8GT1srYrzL6jc4Nj130eGF0Z0UFicB4CaTG0nxFl3914majcN10Gt7BekP6QTPd4U+OMorAVAmYRBGDK10HiOfFVtHi32AbY7M8636pXaI7xtOnmkRgZAQcflvgGTPRo0f5VtGtPGL32sXhhvFSh0S319Oh4CIwGwIb40hKo1/bzSq6h3bHEWyvjDW81C7RNdKSmwgsnEBqHH9p9Gu7TVl00/ZgOraP3C/R7aCj3SIgAn8TqL+fy7/wa2mlxemCj+l5MR5d+Ma/kDLaLutM96V2iW407gokArMlsDGO7JvPNwscnhf7/qcLY/nebtZZuma63ojVUQSWRSAzDrc0+rXd0vaOju1Dx/577zaJLm5IL/Vrpnvv06X8IjBiAvUn89ZHC4XnUFJjv9LoN5gb+KyQzPJ1sdffAJboDnZ6lEgEJklga6z6uZnJGf1f3GrRMj3PRYfSJfZAvqZZLmqpmnokug0JLUVABM4I1IK4OdvZvbHvPnT1iDU+Rb26Guk+B1Nj2rLxk+g2JLQUARFoE6AgWt46s9+u3dm4nRr9fEXdGN7bbW3seWj8JLoNCS1FQATaBPL2jo5tr28t1LE2HTHbu8v2jpFsS3RHciJUhghMmgAeLVAMH4yD2Bn9ztzqHKaZNB4t7M86j2CjfvxiYfTX6aMRzXRHcPJUggiMkMDWWNNf8PMVxI0xx1ej39BuqTHh4dRPontKQ+siIAI/YAaXAoP1GwUFZnFHV2z1LNEqunvX+AP5r415ylM/ie4pDa2LgAiQwM4Bg4vvadgNNkyPFuC3P+04ovXUWMvh1E+ie0pD6yKwcAKYgWZA8NaI4fH0WaWxT+OWNSs3ll99ZtI3YsY6bH03UJ4mlOie0tC6CCyYQP2WP3dA4DXLRZ4EOayCNcpZLsaQGjk9t28aEl0jObmJwAIIbDFGy6fxRPEEMTl72+zAh3ksLeRDOkv8EJ+NsXPZ9pPotoloWwQWSKCefX5yGLpVOM9C1rPp7Gxn98a+PUvsdh38SGrMWLb9JLptItoWgWUSKByG/Rgwy+UM0foBmktNDuWHudY3Dutz77KdTaLbJqJtEVgYAYgIZ63WZ6ykk/Mfz5Yb+z1D2Euj79Bu1kcLF/+nnkR36NOlfCIwIgL1Y4XcoaTPEMPKwf/VFbkybFifGeevHce3khpLKi/5SXQvUdE+EVgOgT2Gan27zw+2dgFocmPfMX+AxiFYZ7rlpfFKdC9R0T4RWAABzDwpoNZnkySSYZZ79EHjOMv1+l9uPnW59sE4KLimmxRY7S/Fl+heoqJ9IjBzArV4fHQY5lOXiBhj5EY/uvFmMNZG0bW0zt+LkOha8MlHBGZEAIK7xnAKhyHx7X7m4H/m6jjLfYS4V2cBxrVhFd19V9kS3S4y2i8CMyQAAVxhWAXM9Ba5RpAHCmFex7EsXHwt8aL51O8OrNwkutHIK5AITJtAifLfOgyBv32wc/A/c4VQ5dhh/cbCXGa5/KrY8QzEyYZmuicwtCoCcyYAASwwPhfBDX2swFn11oFp7uB7D9eNMWlxzU+ie42OjonATAjUgvvecTibazM2Q6wdfKxvx0c9y431aIHMJLqGK0cuIjBlAp6C+xmCW/qOGzlT9HUR+dw310D9MmOeJ3CrrvlKdK/R0TERmDgBT8Hlc9w8cOiFQ//Pt4TKIVZ0VzBMEPRnY+Dilp9E9xYhHReBiRLwFNxvGG4WMmTkzdHf+uEZnxvzMcSYm/VZLsewvzUQie4tQjouAhMk4Cm4FMCg57jIu0aMTw7I+HW0o4P/PVy3xqSPlrH80xhMbiIgAhMgANFbocw97J1juRTcNORtfp27cMjLr1btHPwHd8WYNkhqnbWT+82mme5NRHIQgWkQqGeZJap1FVwOkDPcA1cCWo6+Ll9J2wbkGqprZkz0DH4SXSMsuYnA5AnUM7ISA3ERvWbcHyAY7Ovd6vwfHQL8GprTIZeXK8aUoGO0D9CaIjTTbUhoKQITJQBxyFH6b7AfPYZAwS08+r12qcXJJcYzOuevAca74jITL6zD0DNdKyn5icDICJyInc/jBI4mWHBrJHxb7SL4GYT+WPcd5QJsVygsMxbHr9hVRt8fJLpWUvITgRERgChwFpbDXMTudARRBBd1FAj69jTwjfXgxwrIuUaOTZ2nwrKPP2BJvla2u7oW00Kia8IkJxEYBwEIToJKCpjv7JbfUuBMc49lUEMtGQK8dwgS9FgB+VbIVcB+buXc4dgWY+KxWC0zBnpG3tLo++KmZ7outOQrAnckAGHJkf4ACxFcfi0shuCmqOMLzKUFfQcYiQpYW3CZnzPSL+Cz4UZoQ5wMMR6McXKj36ubRPcVhVZEYJwEKAKwCtV9glnf8rYH8w071hBcinZQQy1rBHAV7s8huZGTgnpJcE/HsjvdCFjPjX35rsGVg37wxghXbiIwOAEITQorkZgzSuvM61KdX7Ez6D8+NEFRzwrrFBoX8X+C4OZNDNdlndMiqA/wXbvGP/VH/wzbVtY7jOt42t+yrpmuhZJ8RGBAAnjhN2L7O9K+C0z9GcIQ+rb+pQTUtcJKCbOKEvvxOe6GKwEtR19rTtYY0nJjZ85yd0bfMzd9kHaGQxsicB8CtaBRnHKYVWCuFUtRoNiW15ysx04E9621T+0XJPi8ASHOR4ecBwffM1fkyrDDyn7vM8tlQokuKaiJwJ0I4IW+RuotjILr8pb9WsVPjOcrCu3AAYL7ATWEiOAKtRTteq5sP/mOuR5jfiV2+5CL71lfie4ZDm2IQP8E8AJPkIUim8FcZ47o0tk4u80hPLtOD8cDAYLL7+MWjuna7jl2WGee7LvlP56Nfa25HjG2yjOPZrq+4NRPBFwIQLwosimMS+uLG67mxtktv39bmXvccAwQXP4PrRAB/KHm5fJYgSJ/uDGki4frcbrUm18MZNypma4RlNxEwEoAL+I1fBNYCuP6O1hfjbPbLQSniJkgQHC/oY4spJY6d+EQgx/W5Q7+bVf2tT7aeQTrqh3AZVui60JLvosmUIsBRfS0pfUGlytYzMcFdejOxa84wscJx04PjwOBgsuvpoXWs0fZVhHkCDnD98qJsSbob51Rvzy+YcKQ9iaks29fDHSNvjnsZ98Y6icCCybwFWPn7LaKzaB+bVL0XB+BUJAouIeQmpA/R/9PDjH4WGHr4H/minwldljfiXxGrvwsgMfG4KJbn9QStbrcyTyGpi4iMDsCTxgRZ7ZlHyMLeG3GEtwU4/rdYWx8rLAGj6NDn1dXjHeDjd9ed1xf4RgT31ynoe/xeGGPAiS4p2dB6yJwncAjDhd9iS1TQ4AyLL5w3bHFEtwEeakNLi3zFUGMd4VEO4dkvNkdHfw7XQcV3frO8tBZjQ6IgAg0BChmFCG+2KtmZx9LvC4LxH3vETuW4FIAOVaXyRgfK5QeNTddtlixatEzcu2ajqHLQUUXxa5DC1Z/EZg5gW8YH1/ge7zQj32OFWKbMA/srUeeKIJb5+V4XWr4BjZbj5pfumDca6x8cujvnetSjqFF93ipCO0TgYUT4LPJPYyPEA5DsKjfdRbI5TK7bEqLJrioI0dQl1k2c2+aQjyXO4d+TzgnPDfR2tCiW0arXIFEYNoEOKPli5kz2sOQQ4HQUXQ+euZk3XyWGlwz6sgQy2XGyZKZu+KKT0POLfq9c+ibOfiaXN+YvCI6YdB7hPs5YkiFEoEpEKBYUahKWO+PDpDju4bX3ho7C5jLW/nTOBxDCtE7nu70Wa9r+cOxb+jXwxLk4zn40Zj3M8aaG33NbvcQ3RWq28Nc7jbmAclRBEZA4Ak1VLWVWB5iCBXieDeIXI7OrrPK03xfscFZZizBLRHPKn6sg89x11zxbWBQoq9Vd/jIZx1jvO16BxfdpgAA2GCdljT7tBSBiRAoT+qkCB3q7buL60ldL6t4naVYKWAPMN/2CPHJfDuf9kM9a2yXMBfB/Qv+SYgAIu8WMX6BWdv/Id/e6iw/ERCBhROAyKxgBSy0ZbFQohDWdPAoiELt3ZBv7ZhTYutNWx1FYIEEIDA57OgoNG139g8Su1P0iLWCHdpJDNvZaRzXdY+8HPfKNY/8RUAEFkgAYpHBKlhoKxEgmvAwFsxHcPPQ04i8O5hL24TmVH8REIGZE4CipLAYYktxymPiQjxfwS1C60DuDQfk0PahOdVfBERgxgQgJhnMZwZ5SYco2mlMXIjnK7hlaB3IncCOMGujb7TZfWj96i8CIjASAhQGGMWWIhmrFQgUVXAQbw07eBTIPkG1sL9H7s1ITrHKEAERGAMBiAhnbjvYERarMVZ0sUHMNcynzgP6BQkuzxVi8Cbi0ooxnGPVIAIicGcCUI0VLIOVLgpi9N3BL1jg2ogQcw07Gms4dWOf4HoQg7xcWgXn4LxtDtoWARGYEAGIwAZWwHzEC92utgOOpn3gQFxXwWsK5TjXoTVxXE1Ah2Vw3tC61V8ERGBgAhCIFYyC1ZfQUoMobNu+hobYOZN4tFiCmyA3Y7m03nj0xVlxRUAEPAhAFSiyKWwHO8D6bsyz8ij1ZhfGhfFm4dOO6BQ800QM1nBwLGB/c3ByEAERmCYBiAEFNoNRnFzFAV28G/MlfVFjbJjveI7oGyy4HBvilDCXVsG5l5uQhfXQv6drqUk+IjApAngBJyiYxpbC+IKmoCSwkB+aQXev9oheOX6wpfLqbeiEMadw28NcfrimiRzzJyILBLX+chjz88dzNiE/nsMgIW1xoouLJQOwDYwvjDk1vgCKvi6memaQIQfZLb3x2nk7Qgi9iy3HjGshx+IT1z1aTMHNkf+9Yw1bvEYOjn2iur+JGm3EwWrRoDC53BVHPKKLpT1jL+/iUS+qml2J2GMUmosgFrSTM7cCtsN5r7DsrUV4DT2huCizTNSSIdYXx8EG/Qi6Y65O9yWJ7g4UPnaSmM+B4B97bqPABV5i35xvVu0hT2GbN1he0729uzmFgGtgw1wwn8cJDPWIm0LGldDmKbhPyJ+G5o7RfxGii5O0Aqz/xAA2kRjRfoAZ7NYY8x8TGffcy+Ssdg+j0JZDDLZ+7eTIFTJh+Yx6GSO4eQputEcawQNAgKU806VwLKlxvHxxxmibGEEUI4jAV/Tm+dxDvI5BkRw6Q+BSuBewB5hv+4CaC9/Op/3qCcDudJ9hnTeqKI80DLlMLksRXRMMOYnAiAjcRWg5/kizW4pdCsE9xGBaC26JWC6PN5oaqhg1xIqxFNEdFfRYJ+9KnJjjjfKiuVKrDv1N4BsWJQ1Ctf971/D/Qtw2yFrAXMStXSjHQsE9tg/4bHsKLlNtY4m+T91dfRbxTJeDx4krsVjCh0G8uyexLnjEIrsKi5C3mAyjdk7gVWSxm0IbRaDOU9i3cI4TeBew0NdI1G8IBAjuBzDleEbXliS6K9AvYW9HdxbiFUTB5QzjEC/ki+iuEa+Ehcx+YpY0pVg8JzwfjVU4P+VYBgBRW6GWLexTYE0cZ4ax7QPjvHZHbRtsFDDX6+4z6sjRb5RtMaJL+icXWIbNOc3ceMHzYs9xsVVYRm9glyBoDuMLwfVFgC6za42YNgOrsEJjK/kPzsXLkutjbDinGerawULP5zfE4IdVFZZRWl3bF49gj6gj8+inLiIgAiLQDwEKGqyCxWgU7agNRbE+n1ZELUTBREAERCCEAFRsA4sltoyThtRzqS9i5jCfVlyKp30iIAIiMDgBKBhnjrHEloK4g61iDwQxCwb3aEXsWhRPBERABJwIQLhWsNhiS+FOnQoxONe1llj6tMKQQi4iIAIi0A8BqFYCy2H8vdqYra/ZLes9eBZa9ENRUUVABETgBgGI1ga29xSva904A13fSO91mHFhx2vJrxwrvJKqkwiIgAj4EoAgcZa4g1VXxMn3EMUw863tVj/G9i0M/Ypb8XVcBERABKIQgOBQaLewA6yvliNw9A/KCIBxYUVA4XkUkAoiAiIgAl0EIFBDCC11kGKYdNURur8eR8jNIgutQf1FQARE4DsCECfOBjewHayC9d1KJEi+KyTiDsTneI6eA2G/LGI5CiUCIrBkAhCUFYyitIMdYEM1im3aN3vk4Lh82xEde/kgr+9xK74IiMAICEBAVrAUlsP2sAo2dCuQMO0bB3KsYYeAwbHv7AR3UT940/dFpvgiQAIQCn4IRbFolgnWadwX+uMyCOHdHtGztx9FOq0KDLbY/uV0n+P6E/xH9RcfHOvvdI8muvUdadOZSQeWRKDCYPf4tafjmAaNazRBPbxGV5HqWrdihf4WbaSyzsI8Y6uA7YY4HzVj5gthEfU3eVHLqFqw6AIyL+A9LATyqKComCgEov++akhVuE536P8xJMbE+n5FvQWElq/NQRoYb5Eoh/nO5nnNbFFzgeVsWwzRLUFHgjvbSyR4YP/Ci+gQHCUgwIIEt5nVUmyrAGROXcF3jQ47WIgOsHY+TrjrteI0cE/nINEF7A3y/uaZW92WQeAJL6T0XkPFNZog95/3yj9AXs4O9zA+PhhUsMB2hbxb2CdYSOOsPEP9x5AgU+n7z8BC08D+6j5/AiGznxh0NjGCjCwGZ4V7WAmh4nLwVk+4dkj8EJj83xgD4yymhYruejGkNNCpElhNtfBW3d+wvacNPaM9raN+51BgX+jNdDGPE075cT1UdEvECIXPOtREoC8CVV+Be45LkS0bu/db7/pRQo56YnwYuajHCWB21kKf6aaI9vtZRG2IwDmBRwhGdr5ruK1aLCpk9P1Efahin5CohB24vLfIoobXBoY5NrawUIZ/IQaf3XLGvtgWJLqkhhNSYPGe62oi0CLAF9kaL7KqtX/QTVyjGRJ+GTRpdzLOYI+wEkaBrcCHy9G1mluOwh4iFMebCgW3ihBr0iGCRZejj3gnnDRMFX9GYFQvslpAdqgwdLZ2NsgLGxw3W1UbBZaiehyruKK2sxZZbHnjzTF2slcDgSii25DEyUqbdS0XTYCzt2qMBHq4Rg8YK4V18i2y2JLHV9h2rNfC5E+YBiACIjA9AhDaFSyHVbBYjbE206OhikVABESgJwIQxQS2gx1hMRtjrnoqW2FFQAREYFoEIIgb2D6mytaxSizX06KhakVABESgBwIQQ85qtzC+7Y/dGFOPEno4bwopAiIwMQIQwwzWx6yWwn2E5RNDonJFQAREIC4BCOEGVtSiiEUvLUdUPbeNe+oUTQREYCoEIIBDCC3Vm2KeTIWL6hQBERCBKAQofLAMxkcHfJvfd5PYRjlzCiICIjAJAlDUFYyz2R3sABuqSWx7uEKi/o+0HupTSBFYHAEoaoJB8+tXaW1vsRyq8b/t7mhz+Z92Q4Gz5pHoWknJTwR6IACBpbgmsEZkuez79yGQ4rv2jD05bC+x/Y5N1B0S3ag4FUwEvidQz1wTHGmsEdohZ7DfF/b3Hv4+Ame1ZZeD9sclINGNy3Ny0SAI6eSKHlfBK5RDEW1aghUa271mrX9n7/73GYcKmn6MphtSX0ckun2RHXlciG2OErewe7yVHTmd2Zb3iJHx8cF+tiOcwMAkuhM4SbFLhOCWiPkudlzFGyUBPj6gyOpZ7UhOT+jfSBvJMFSGlQAEN4OvBNcKbJp+FNoSRqGtsFQbEQGJ7ohOxkClZAPlUZrhCPBrXiVMM9rhmHtnkuh6o1NHEbgrgW/IXsI4m+VSbSIEJLoTOVEqc/EEGpEtQaKE0B4XT2SiACS6Ez1xAWUf0FfPdAMADtCVX+nieaKVXEpkQWEmTd9emMmJtA4DH6St4FvB9FUxK7R+/Z4QvqqtxFICCwhzbhLdOZ/djrFBeNc4tIc9dLhodzwCfCxwhFUtk7gCyBKbRHeJZx1jrme8G6wmC0UQc9gU1cNJwNH+CfqTGrUqAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIgAiIwagL/D3GJmp++OV9BAAAAAElFTkSuQmCC") + const plots$ = xs.of("iVBORw0KGgoAAAANSUhEUgAAAV0AAAFDCAMAAACuii1uAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAuIwAALiMBeKU/dgAAActpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+QWRvYmUgSW1hZ2VSZWFkeTwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KKS7NPQAAADNQTFRFAAAA////////////////////////////////////////////////////////////////t5XiggAAABB0Uk5TABAgMEBQYHCAkKCwwNDg8FTgqMgAABBNSURBVHja7Z3bgqQqDEVFEAER+P+vPS9nZlQIBBXLS/I43VOtq2IuO4Bd99eUD7G5odsaM+HW5qy1So1CdDcyiJncwp3DU8zbSQ38DnAleI097lu4r81m/LUfW/DixvUvhmfarGX/O7oevC61+j0enmvOSPYbujOSLgvPtlnxGyW1KK258HTzZriabg+Fhnnzi0N4gV0OeEjjnRm+ungWYH1piGDjZLemZcrNlb2xVUQuN7KOrNqEGJSymLbHCKK1O5mI0diSA0vidMS41Fk39ooCxNFgoXJObHoidLgs0o74No3EoyW+TStPORHftqU9kOYov50UInSyN/UjoTlpYJAMwY76i7Mc2KQc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") const headTableCsv$ = state$.map((state) => state.headTable.data) .filter((data) => notEmpty(data)) @@ -66,17 +66,31 @@ function model(actions, state$) { .map(([_, signature]) => signature) .remember() + // not yet functional, uses static content const clipboardPlots$ = actions.exportPlotsTrigger$ .compose(sampleCombine(plots$)) - .map(([_, plots]) => "data:image/png;base64,"+plots) + .map(([_, plots]) => { + + const byteCharacters = atob(plots); + const byteNumbers = new Array(byteCharacters.length); + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i); + } + const byteArray = new Uint8Array(byteNumbers); + + return { + type: "image/png", + data: new Blob([byteArray], {type: 'image/png'}), + } + }) .remember().debug("clipboardPlots$") - const clipboardHeadTable$ = actions.exportHeadTableTrigger$ + const clipboardHeadTable$ = actions.exportHeadTableTrigger$ .compose(sampleCombine(headTableCsv$)) .map(([_, table]) => table) .remember() - const clipboardTailTable$ = actions.exportTailTableTrigger$ + const clipboardTailTable$ = actions.exportTailTableTrigger$ .compose(sampleCombine(tailTableCsv$)) .map(([_, table]) => table) .remember() diff --git a/src/js/drivers/clipboardDriver.js b/src/js/drivers/clipboardDriver.js index af1b0751..8113c615 100644 --- a/src/js/drivers/clipboardDriver.js +++ b/src/js/drivers/clipboardDriver.js @@ -1,13 +1,24 @@ function clipboardDriver(stream$) { stream$.addListener({ next: message => { - // console.log("text that should be placed in clipboard: " + message) + + if (typeof message === 'object') { + console.log('object received for clipboard, type: ' + message.type) + navigator.clipboard.write([ + new ClipboardItem({ + [message.type]: message.data + }) + ]); + } + else { + console.log("text that should be placed in clipboard: " + message) navigator.clipboard.writeText(message).then(function() { /* clipboard successfully set */ }, function() { /* clipboard write failed */ console.warn("Writing to clipboard failed") }); + } }, error: e => console.error(e), complete: () => {} From 0ae1f254e32a1935a0fd4622d86530fb51012640 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 2 Feb 2022 10:52:18 +0100 Subject: [PATCH 130/191] change notEmpty to notEmptyOrUndefined and support arrays use ramda 'isEmpty' but also catch undefined. isEmpty returns false for undefined Add plotsPresent$ functionality --- src/js/components/Exporter.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 82d5f167..bdf6e0ed 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -1,5 +1,6 @@ import xs from "xstream" import { div, i, ul, li, p, input, button, span, a } from "@cycle/dom" +import { isEmpty } from "ramda" import { loggerFactory } from "../utils/logger" import delay from "xstream/extra/delay" import sampleCombine from "xstream/extra/sampleCombine" @@ -33,26 +34,26 @@ function model(actions, state$) { const closeModal$ = actions.modalCloseTrigger$ .map(_ => ({ el: '#modal-exporter', state: 'close' })) - function notEmpty(data) { - return data != undefined && data != "" + function notEmptyOrUndefined(data) { + return data != undefined && !isEmpty(data) } - const signaturePresent$ = state$.map((state) => notEmpty(state.form.signature.output)).startWith(false) - const plotsPresent$ = xs.of(true) - const headTablePresent$ = state$.map((state) => notEmpty(state.headTable.data)).startWith(false) - const tailTablePresent$ = state$.map((state) => notEmpty(state.tailTable.data)).startWith(false) + const signaturePresent$ = state$.map((state) => notEmptyOrUndefined(state.form.signature.output)).startWith(false) + const plotsPresent$ = state$.map((state) => notEmptyOrUndefined(state.plots.data)).startWith(false) + const headTablePresent$ = state$.map((state) => notEmptyOrUndefined(state.headTable.data)).startWith(false) + const tailTablePresent$ = state$.map((state) => notEmptyOrUndefined(state.tailTable.data)).startWith(false) const url$ = state$.map((state) => state.routerInformation.pageStateURL).startWith("") const signature$ = state$.map((state) => state.form.signature.output).startWith("") const plots$ = xs.of("iVBORw0KGgoAAAANSUhEUgAAAV0AAAFDCAMAAACuii1uAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAuIwAALiMBeKU/dgAAActpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+QWRvYmUgSW1hZ2VSZWFkeTwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KKS7NPQAAADNQTFRFAAAA////////////////////////////////////////////////////////////////t5XiggAAABB0Uk5TABAgMEBQYHCAkKCwwNDg8FTgqMgAABBNSURBVHja7Z3bgqQqDEVFEAER+P+vPS9nZlQIBBXLS/I43VOtq2IuO4Bd99eUD7G5odsaM+HW5qy1So1CdDcyiJncwp3DU8zbSQ38DnAleI097lu4r81m/LUfW/DixvUvhmfarGX/O7oevC61+j0enmvOSPYbujOSLgvPtlnxGyW1KK258HTzZriabg+Fhnnzi0N4gV0OeEjjnRm+ungWYH1piGDjZLemZcrNlb2xVUQuN7KOrNqEGJSymLbHCKK1O5mI0diSA0vidMS41Fk39ooCxNFgoXJObHoidLgs0o74No3EoyW+TStPORHftqU9kOYov50UInSyN/UjoTlpYJAMwY76i7Mc2KQc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") const headTableCsv$ = state$.map((state) => state.headTable.data) - .filter((data) => notEmpty(data)) + .filter((data) => notEmptyOrUndefined(data)) .map((data) => convertToCSV(data)) .startWith("") const tailTableCsv$ = state$.map((state) => state.tailTable.data) - .filter((data) => notEmpty(data)) + .filter((data) => notEmptyOrUndefined(data)) .map((data) => convertToCSV(data)) .startWith("") From 65c28442382504d43d3963b2eb9eec4681c7c310 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 2 Feb 2022 13:41:59 +0100 Subject: [PATCH 131/191] add basic code to get vega image and paste to clipboard add test button in FAB so it's easier to assign test functionality add stream return to vega driver builder --- src/js/components/Exporter.js | 67 ++++++++++++++++++++++++------- src/js/drivers/clipboardDriver.js | 17 ++++++-- src/js/drivers/makeVegaDriver.js | 3 +- 3 files changed, 67 insertions(+), 20 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index bdf6e0ed..49d64612 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -16,6 +16,8 @@ function intent(domSource$) { const modalTrigger$ = domSource$.select(".modal-open-btn").events("click") const modalCloseTrigger$ = domSource$.select(".export-close").events("click") + const testTrigger$ = domSource$.select(".test-btn").events("click") + return { exportLinkTrigger$: exportLinkTrigger$, exportSignatureTrigger$: exportSignatureTrigger$, @@ -24,10 +26,11 @@ function intent(domSource$) { exportTailTableTrigger$: exportTailTableTrigger$, modalTrigger$: modalTrigger$, modalCloseTrigger$: modalCloseTrigger$, + testTrigger$: testTrigger$, } } -function model(actions, state$) { +function model(actions, state$, vega$) { const openModal$ = actions.modalTrigger$ .map(_ => ({ el: '#modal-exporter', state: 'open' })) @@ -69,22 +72,30 @@ function model(actions, state$) { // not yet functional, uses static content const clipboardPlots$ = actions.exportPlotsTrigger$ - .compose(sampleCombine(plots$)) - .map(([_, plots]) => { - - const byteCharacters = atob(plots); - const byteNumbers = new Array(byteCharacters.length); - for (let i = 0; i < byteCharacters.length; i++) { - byteNumbers[i] = byteCharacters.charCodeAt(i); + .compose(sampleCombine(vega$)) + .map(([_, vega]) => { + return xs.fromPromise(vega.view.toImageURL('png')) + }) + .flatten() + .map((data) => { + // input data is "data:image/png;base64,abcdef0123456789..." + const parts = data.split(';base64,'); + const imageType = parts[0].split(':')[1]; + const decodedData = window.atob(parts[1]); + const uInt8Array = new Uint8Array(decodedData.length); + + for (let i = 0; i < decodedData.length; i++) { + uInt8Array[i] = decodedData.charCodeAt(i); } - const byteArray = new Uint8Array(byteNumbers); + + const blob = new Blob([uInt8Array], { type: imageType }) return { - type: "image/png", - data: new Blob([byteArray], {type: 'image/png'}), + type: imageType, + data: blob, } }) - .remember().debug("clipboardPlots$") + .remember() const clipboardHeadTable$ = actions.exportHeadTableTrigger$ .compose(sampleCombine(headTableCsv$)) @@ -96,11 +107,36 @@ function model(actions, state$) { .map(([_, table]) => table) .remember() - + const testAction$ = actions.testTrigger$ + .compose(sampleCombine(vega$)) + .map(([_, vega]) => { + return xs.fromPromise(vega.view.toImageURL('png')) + }) + .flatten() + .map((data) => { + // input data is "data:image/png;base64,abcdef0123456789..." + const parts = data.split(';base64,'); + const imageType = parts[0].split(':')[1]; + const decodedData = window.atob(parts[1]); + const uInt8Array = new Uint8Array(decodedData.length); + + for (let i = 0; i < decodedData.length; i++) { + uInt8Array[i] = decodedData.charCodeAt(i); + } + + const blob = new Blob([uInt8Array], { type: imageType }) + + return { + type: imageType, + data: blob, + } + }) + .remember() + return { reducers$: xs.empty(), modal$: xs.merge(openModal$, closeModal$), - clipboard$: xs.merge(clipboardLink$, clipboardSignature$, clipboardPlots$, clipboardHeadTable$, clipboardTailTable$), + clipboard$: xs.merge(clipboardLink$, clipboardSignature$, clipboardPlots$, clipboardHeadTable$, clipboardTailTable$, testAction$), dataPresent: { signaturePresent$: signaturePresent$, plotsPresent$: plotsPresent$, @@ -128,6 +164,7 @@ function view(state$, dataPresent, exportData) { li(span(".btn-floating .export-clipboard-signature", i(".material-icons", "content_copy"))), // li(span(".btn-floating .export-file-report", i(".material-icons", "picture_as_pdf"))), li(span(".btn-floating .modal-open-btn", i(".material-icons", "open_with"))), + li(span(".btn-floating .test-btn", i(".material-icons", "star"))), ]) ])) .startWith(div()) @@ -272,7 +309,7 @@ function Exporter(sources) { const actions = intent(sources.DOM) - const model_ = model(actions, state$) + const model_ = model(actions, state$, sources.vega) const vdom$ = view(state$, model_.dataPresent, model_.exportData) diff --git a/src/js/drivers/clipboardDriver.js b/src/js/drivers/clipboardDriver.js index 8113c615..6530d6c4 100644 --- a/src/js/drivers/clipboardDriver.js +++ b/src/js/drivers/clipboardDriver.js @@ -8,16 +8,25 @@ function clipboardDriver(stream$) { new ClipboardItem({ [message.type]: message.data }) - ]); + ]) + .then( + function() { + /* clipboard successfully set */ + }, function() { + /* clipboard write failed */ + console.warn("Writing to clipboard failed") + }); } else { console.log("text that should be placed in clipboard: " + message) - navigator.clipboard.writeText(message).then(function() { + navigator.clipboard.writeText(message) + .then( + function() { /* clipboard successfully set */ - }, function() { + }, function() { /* clipboard write failed */ console.warn("Writing to clipboard failed") - }); + }); } }, error: e => console.error(e), diff --git a/src/js/drivers/makeVegaDriver.js b/src/js/drivers/makeVegaDriver.js index ecaf7132..ad5c2baa 100644 --- a/src/js/drivers/makeVegaDriver.js +++ b/src/js/drivers/makeVegaDriver.js @@ -22,7 +22,8 @@ function makeVegaDriver() { console.error(m) } }) - + + return view$ } return vegaDriver From 3b6baab66d2edc9628f92d27817b6301149c25ee Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 2 Feb 2022 13:55:45 +0100 Subject: [PATCH 132/191] Move creation of plot data to separate stream remove static encoded image new plot string already contains "data:image/png;base64," so don't add it in the file download --- src/js/components/Exporter.js | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 49d64612..f5db4bd6 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -48,7 +48,7 @@ function model(actions, state$, vega$) { const url$ = state$.map((state) => state.routerInformation.pageStateURL).startWith("") const signature$ = state$.map((state) => state.form.signature.output).startWith("") - const plots$ = xs.of("iVBORw0KGgoAAAANSUhEUgAAAV0AAAFDCAMAAACuii1uAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAuIwAALiMBeKU/dgAAActpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+QWRvYmUgSW1hZ2VSZWFkeTwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KKS7NPQAAADNQTFRFAAAA////////////////////////////////////////////////////////////////t5XiggAAABB0Uk5TABAgMEBQYHCAkKCwwNDg8FTgqMgAABBNSURBVHja7Z3bgqQqDEVFEAER+P+vPS9nZlQIBBXLS/I43VOtq2IuO4Bd99eUD7G5odsaM+HW5qy1So1CdDcyiJncwp3DU8zbSQ38DnAleI097lu4r81m/LUfW/DixvUvhmfarGX/O7oevC61+j0enmvOSPYbujOSLgvPtlnxGyW1KK258HTzZriabg+Fhnnzi0N4gV0OeEjjnRm+ungWYH1piGDjZLemZcrNlb2xVUQuN7KOrNqEGJSymLbHCKK1O5mI0diSA0vidMS41Fk39ooCxNFgoXJObHoidLgs0o74No3EoyW+TStPORHftqU9kOYov50UInSyN/UjoTlpYJAMwY76i7Mc2KQc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") + const plots$ = vega$.map((vega) => xs.fromPromise(vega.view.toImageURL('png'))).flatten() const headTableCsv$ = state$.map((state) => state.headTable.data) .filter((data) => notEmptyOrUndefined(data)) @@ -72,12 +72,8 @@ function model(actions, state$, vega$) { // not yet functional, uses static content const clipboardPlots$ = actions.exportPlotsTrigger$ - .compose(sampleCombine(vega$)) - .map(([_, vega]) => { - return xs.fromPromise(vega.view.toImageURL('png')) - }) - .flatten() - .map((data) => { + .compose(sampleCombine(plots$)) + .map(([_, data]) => { // input data is "data:image/png;base64,abcdef0123456789..." const parts = data.split(';base64,'); const imageType = parts[0].split(':')[1]; @@ -108,12 +104,8 @@ function model(actions, state$, vega$) { .remember() const testAction$ = actions.testTrigger$ - .compose(sampleCombine(vega$)) - .map(([_, vega]) => { - return xs.fromPromise(vega.view.toImageURL('png')) - }) - .flatten() - .map((data) => { + .compose(sampleCombine(plots$)) + .map(([_, data]) => { // input data is "data:image/png;base64,abcdef0123456789..." const parts = data.split(';base64,'); const imageType = parts[0].split(':')[1]; @@ -190,7 +182,7 @@ function view(state$, dataPresent, exportData) { const urlFile = "data:text/plain;charset=utf-8," + url const signatureFile = "data:text/plain;charset=utf-8," + signature - const plotsFile = "data:image/png;base64," + plots + const plotsFile = plots const headTableCsvFile = "data:text/tsv;charset=utf-8," + encodeURIComponent(headTableCsv) const tailTableCsvFile = "data:text/tsv;charset=utf-8," + encodeURIComponent(tailTableCsv) From d5aea6959741d9e5cc04909f4343f5d6ac76ec62 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 2 Feb 2022 15:07:10 +0100 Subject: [PATCH 133/191] Select similarity plot for exports --- src/js/components/Exporter.js | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index f5db4bd6..58f0a649 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -48,7 +48,11 @@ function model(actions, state$, vega$) { const url$ = state$.map((state) => state.routerInformation.pageStateURL).startWith("") const signature$ = state$.map((state) => state.form.signature.output).startWith("") - const plots$ = vega$.map((vega) => xs.fromPromise(vega.view.toImageURL('png'))).flatten() + const similarityPlot$ = vega$ + .filter(vega => vega.el == '#simplot') + .map((vega) => xs.fromPromise(vega.view.toImageURL('png'))) + .flatten() + .startWith("") const headTableCsv$ = state$.map((state) => state.headTable.data) .filter((data) => notEmptyOrUndefined(data)) @@ -70,22 +74,18 @@ function model(actions, state$, vega$) { .map(([_, signature]) => signature) .remember() - // not yet functional, uses static content const clipboardPlots$ = actions.exportPlotsTrigger$ - .compose(sampleCombine(plots$)) + .compose(sampleCombine(similarityPlot$)) .map(([_, data]) => { // input data is "data:image/png;base64,abcdef0123456789..." const parts = data.split(';base64,'); const imageType = parts[0].split(':')[1]; const decodedData = window.atob(parts[1]); const uInt8Array = new Uint8Array(decodedData.length); - for (let i = 0; i < decodedData.length; i++) { uInt8Array[i] = decodedData.charCodeAt(i); } - const blob = new Blob([uInt8Array], { type: imageType }) - return { type: imageType, data: blob, @@ -104,20 +104,17 @@ function model(actions, state$, vega$) { .remember() const testAction$ = actions.testTrigger$ - .compose(sampleCombine(plots$)) + .compose(sampleCombine(similarityPlot$)) .map(([_, data]) => { // input data is "data:image/png;base64,abcdef0123456789..." const parts = data.split(';base64,'); const imageType = parts[0].split(':')[1]; const decodedData = window.atob(parts[1]); const uInt8Array = new Uint8Array(decodedData.length); - for (let i = 0; i < decodedData.length; i++) { uInt8Array[i] = decodedData.charCodeAt(i); } - const blob = new Blob([uInt8Array], { type: imageType }) - return { type: imageType, data: blob, @@ -138,7 +135,7 @@ function model(actions, state$, vega$) { exportData: { url$: url$, signature$: signature$, - plots$: plots$, + similarityPlot$: similarityPlot$, headTableCsv$: headTableCsv$, tailTableCsv$: tailTableCsv$, } @@ -169,11 +166,11 @@ function view(state$, dataPresent, exportData) { dataPresent.tailTablePresent$, exportData.url$, exportData.signature$, - exportData.plots$, + exportData.similarityPlot$, exportData.headTableCsv$, exportData.tailTableCsv$, ) - .map(([signaturePresent, plotsPresent, headTablePresent, tailTablePresent, url, signature, plots, headTableCsv, tailTableCsv]) => { + .map(([signaturePresent, plotsPresent, headTablePresent, tailTablePresent, url, signature, similarityPlot, headTableCsv, tailTableCsv]) => { const signatureAvailable = signaturePresent ? "" : " .disabled" const plotsAvailable = plotsPresent ? "" : " .disabled" const headTableAvailable = headTablePresent ? "" : " .disabled" @@ -182,7 +179,7 @@ function view(state$, dataPresent, exportData) { const urlFile = "data:text/plain;charset=utf-8," + url const signatureFile = "data:text/plain;charset=utf-8," + signature - const plotsFile = plots + const plotsFile = similarityPlot const headTableCsvFile = "data:text/tsv;charset=utf-8," + encodeURIComponent(headTableCsv) const tailTableCsvFile = "data:text/tsv;charset=utf-8," + encodeURIComponent(tailTableCsv) From d93cb96e2732c694348bfce96566ff3cbe7cc049 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 2 Feb 2022 16:02:02 +0100 Subject: [PATCH 134/191] Remove temporary test button and diverse stream.debug and console.log statements --- src/js/components/Exporter.js | 2 +- src/js/drivers/clipboardDriver.js | 4 ++-- src/js/index.js | 6 +----- src/js/main.js | 10 +--------- src/js/pages/genericTreatment.js | 2 +- 5 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 58f0a649..204bc9e3 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -153,7 +153,7 @@ function view(state$, dataPresent, exportData) { li(span(".btn-floating .export-clipboard-signature", i(".material-icons", "content_copy"))), // li(span(".btn-floating .export-file-report", i(".material-icons", "picture_as_pdf"))), li(span(".btn-floating .modal-open-btn", i(".material-icons", "open_with"))), - li(span(".btn-floating .test-btn", i(".material-icons", "star"))), + // li(span(".btn-floating .test-btn", i(".material-icons", "star"))), ]) ])) .startWith(div()) diff --git a/src/js/drivers/clipboardDriver.js b/src/js/drivers/clipboardDriver.js index 6530d6c4..de1fa69f 100644 --- a/src/js/drivers/clipboardDriver.js +++ b/src/js/drivers/clipboardDriver.js @@ -3,7 +3,7 @@ function clipboardDriver(stream$) { next: message => { if (typeof message === 'object') { - console.log('object received for clipboard, type: ' + message.type) + // console.log('object received for clipboard, type: ' + message.type) navigator.clipboard.write([ new ClipboardItem({ [message.type]: message.data @@ -18,7 +18,7 @@ function clipboardDriver(stream$) { }); } else { - console.log("text that should be placed in clipboard: " + message) + // console.log("text that should be placed in clipboard: " + message) navigator.clipboard.writeText(message) .then( function() { diff --git a/src/js/index.js b/src/js/index.js index 700a808a..b3bef475 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -57,10 +57,6 @@ import { export default function Index(sources) { const { router } = sources - console.log("sources:") - console.log(sources) - console.log("router:") - console.log(router) const logger = loggerFactory( "index", @@ -88,7 +84,7 @@ export default function Index(sources) { "/debug": Debug, "/admin": IsolatedAdminSettings, "*": Home, - })(sources).debug("page$") + })(sources) // TODO: Add a visual reference for ghost mode // const ghost$ = state$ diff --git a/src/js/main.js b/src/js/main.js index 4d81548e..97be4547 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -53,13 +53,5 @@ const drivers = { deployments: () => xs.fromPromise(fetch('/deployments.json').then(m => m.json())) }; -const customSwitchPath = (sourcePath, routes) => { - - console.log("sourcePath: " + sourcePath) - console.log("routes:") - console.log(routes) - return switchPath(sourcePath, routes) -} - -let StatifiedMain = onionify(storageify(routerify(Index, customSwitchPath, {omitHistory: false}), { key: 'ComPass' })); +let StatifiedMain = onionify(storageify(routerify(Index, switchPath, {omitHistory: false}), { key: 'ComPass' })); run(StatifiedMain, drivers); diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index 6b3da100..c8383f0c 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -43,7 +43,7 @@ export default function GenericTreatmentWorkflow(sources) { "settings.common.debug" ) - const state$ = sources.onion.state$.debug("state$") + const state$ = sources.onion.state$ // Scenario for ghost mode const scenarios$ = sources.onion.state$ From b921c67706bd17064a577862c28aa3f4cae5f88a Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 3 Feb 2022 16:46:05 +0100 Subject: [PATCH 135/191] Add exporter modal and FAB to disease and correlation WF Add optional chaining to input data that is not available in all WFs Add config object to exporter creation so settings can be passed, ie. currently only the plot name --- src/js/components/Exporter.js | 28 ++++++++++++++++------------ src/js/pages/correlation.js | 17 ++++++++++++++--- src/js/pages/disease.js | 15 +++++++++++++-- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 204bc9e3..43052516 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -1,6 +1,6 @@ import xs from "xstream" import { div, i, ul, li, p, input, button, span, a } from "@cycle/dom" -import { isEmpty } from "ramda" +import { isEmpty, mergeLeft } from "ramda" import { loggerFactory } from "../utils/logger" import delay from "xstream/extra/delay" import sampleCombine from "xstream/extra/sampleCombine" @@ -30,7 +30,7 @@ function intent(domSource$) { } } -function model(actions, state$, vega$) { +function model(actions, state$, vega$, config) { const openModal$ = actions.modalTrigger$ .map(_ => ({ el: '#modal-exporter', state: 'open' })) @@ -41,25 +41,25 @@ function model(actions, state$, vega$) { return data != undefined && !isEmpty(data) } - const signaturePresent$ = state$.map((state) => notEmptyOrUndefined(state.form.signature.output)).startWith(false) + const signaturePresent$ = state$.map((state) => notEmptyOrUndefined(state.form.signature?.output)).startWith(false) const plotsPresent$ = state$.map((state) => notEmptyOrUndefined(state.plots.data)).startWith(false) - const headTablePresent$ = state$.map((state) => notEmptyOrUndefined(state.headTable.data)).startWith(false) - const tailTablePresent$ = state$.map((state) => notEmptyOrUndefined(state.tailTable.data)).startWith(false) + const headTablePresent$ = state$.map((state) => notEmptyOrUndefined(state.headTable?.data)).startWith(false) + const tailTablePresent$ = state$.map((state) => notEmptyOrUndefined(state.tailTable?.data)).startWith(false) const url$ = state$.map((state) => state.routerInformation.pageStateURL).startWith("") - const signature$ = state$.map((state) => state.form.signature.output).startWith("") + const signature$ = state$.map((state) => state.form.signature?.output).startWith("") const similarityPlot$ = vega$ - .filter(vega => vega.el == '#simplot') + .filter(vega => vega.el == config.plot) .map((vega) => xs.fromPromise(vega.view.toImageURL('png'))) .flatten() .startWith("") - const headTableCsv$ = state$.map((state) => state.headTable.data) + const headTableCsv$ = state$.map((state) => state.headTable?.data) .filter((data) => notEmptyOrUndefined(data)) .map((data) => convertToCSV(data)) .startWith("") - const tailTableCsv$ = state$.map((state) => state.tailTable.data) + const tailTableCsv$ = state$.map((state) => state.tailTable?.data) .filter((data) => notEmptyOrUndefined(data)) .map((data) => convertToCSV(data)) .startWith("") @@ -285,8 +285,7 @@ function view(state$, dataPresent, exportData) { -function Exporter(sources) { - +function Exporter(sources, config) { const logger = loggerFactory( "exporter", @@ -294,11 +293,16 @@ function Exporter(sources) { "settings.common.debug" ) + const defaultConfig = { + plot: "#simplot", + } + const fullConfig = mergeLeft(config, defaultConfig) + const state$ = sources.onion.state$ const actions = intent(sources.DOM) - const model_ = model(actions, state$, sources.vega) + const model_ = model(actions, state$, sources.vega, fullConfig) const vdom$ = view(state$, model_.dataPresent, model_.exportData) diff --git a/src/js/pages/correlation.js b/src/js/pages/correlation.js index dfb15a70..e0f705d8 100644 --- a/src/js/pages/correlation.js +++ b/src/js/pages/correlation.js @@ -13,6 +13,7 @@ import { initSettings } from '../configuration.js' import { Filter, compoundFilterLens } from '../components/Filter' import { loggerFactory } from '../utils/logger' import { SampleTable, sampleTableLens } from '../components/SampleTable/SampleTable' +import { Exporter } from "../components/Exporter" // Support for ghost mode import { scenario } from '../scenarios/diseaseScenario' @@ -76,6 +77,8 @@ function CorrelationWorkflow(sources) { // const tailTable = isolate(tailTableContainer, { onion: tailTableLens }) // ({...sources, input: xs.combine(signature$, filter$).map(([s, f]) => ({ query: s, filter: f })).remember() }); + const exporter = Exporter(sources, {plot: "#corrplot"}) + const pageStyle = { style: { fontSize: '14px', @@ -93,6 +96,7 @@ function CorrelationWorkflow(sources) { // headTable.DOM, // tailTable.DOM, // feedback$ + exporter.DOM, ) .map(([ form, @@ -101,6 +105,7 @@ function CorrelationWorkflow(sources) { // headTable, // tailTable, // feedback + exporter, ]) => div('.row .correlation', { style: { margin: '0px 0px 0px 0px' } }, [ form, @@ -112,22 +117,25 @@ function CorrelationWorkflow(sources) { // div('.row', []), // div('.col .s12', [tailTable]), // div('.row', []) - ]) + ]), + exporter, ]) ); return { log: xs.merge( // logger(state$, 'state$'), + exporter.log, ), DOM: vdom$, onion: xs.merge( defaultReducer$, correlationForm.onion, // filterForm.onion, - correlationPlot.onion, + correlationPlot.onion, // headTable.onion, // tailTable.onion, + exporter.onion, scenarioReducer$ ), vega: xs.merge( @@ -139,7 +147,10 @@ function CorrelationWorkflow(sources) { // headTable.HTTP, // tailTable.HTTP ), - popup: scenarioPopup$ + popup: scenarioPopup$, + modal: exporter.modal, + fab: exporter.fab, + clipboard: exporter.clipboard, }; } diff --git a/src/js/pages/disease.js b/src/js/pages/disease.js index 1cd02ded..bff6a792 100644 --- a/src/js/pages/disease.js +++ b/src/js/pages/disease.js @@ -15,6 +15,7 @@ import { SampleTable, sampleTableLens, } from "../components/SampleTable/SampleTable" +import { Exporter } from "../components/Exporter" // Support for ghost mode import { scenario } from "../scenarios/diseaseScenario" @@ -189,6 +190,8 @@ function DiseaseWorkflow(sources) { .remember(), }) + const exporter = Exporter(sources) + /** * Style object used in div capsulating filter, displayPlots and tables * @const pageStyle @@ -212,6 +215,7 @@ function DiseaseWorkflow(sources) { headTable.DOM, tailTable.DOM, displayPlots$, + exporter.DOM, // feedback$ ) .map( @@ -221,7 +225,8 @@ function DiseaseWorkflow(sources) { plots, headTable, tailTable, - displayPlots, + displayPlots, + exporter, // feedback ]) => div(".row .disease", { style: { margin: "0px 0px 0px 0px" } }, [ @@ -236,6 +241,7 @@ function DiseaseWorkflow(sources) { div(".row", []), div(".row", [displayPlots === "after tables" ? plots : div()]), ]), + exporter, ]) ) @@ -244,7 +250,8 @@ function DiseaseWorkflow(sources) { logger(state$, "state$"), binnedPlots.log, filterForm.log, - signatureForm.log + signatureForm.log, + exporter.log, ), DOM: vdom$, onion: xs.merge( @@ -254,6 +261,7 @@ function DiseaseWorkflow(sources) { binnedPlots.onion, headTable.onion, tailTable.onion, + exporter.onion, scenarioReducer$, uiReducer$, ), @@ -266,6 +274,9 @@ function DiseaseWorkflow(sources) { tailTable.HTTP ), popup: scenarioPopup$, + modal: exporter.modal, + fab: exporter.fab, + clipboard: exporter.clipboard, } } From 0ef0b0465af69d340c9c79137f1198a3ae5bc09d Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 4 Feb 2022 10:20:48 +0100 Subject: [PATCH 136/191] Add plot name for exporter and merge with sources instead of separate variable Adding config into the sources follows our/cycle.js style much better --- src/js/components/Exporter.js | 13 +++++++------ src/js/pages/correlation.js | 8 +++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 43052516..273fff8f 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -49,7 +49,7 @@ function model(actions, state$, vega$, config) { const url$ = state$.map((state) => state.routerInformation.pageStateURL).startWith("") const signature$ = state$.map((state) => state.form.signature?.output).startWith("") const similarityPlot$ = vega$ - .filter(vega => vega.el == config.plot) + .filter(vega => vega.el == config.plotId) .map((vega) => xs.fromPromise(vega.view.toImageURL('png'))) .flatten() .startWith("") @@ -142,7 +142,7 @@ function model(actions, state$, vega$, config) { } } -function view(state$, dataPresent, exportData) { +function view(state$, dataPresent, exportData, config) { const fab$ = dataPresent.signaturePresent$ .map((signature) => @@ -218,7 +218,7 @@ function view(state$, dataPresent, exportData) { ), ]), div(".row", [ - span(".col .s6 .push-s1", "Copy binned plots"), + span(".col .s6 .push-s1", "Copy " + config.plotName + " plot"), span(".btn .col .s1 .offset-s1 .export-clipboard-plots" + plotsAvailable, i(".material-icons", "content_copy")), // span(".btn .col .s1 .offset-s1 .export-file-plots" + plotsAvailable, i(".material-icons", "file_download")), a(".btn .col .s1 .offset-s1" + plotsAvailable, @@ -294,9 +294,10 @@ function Exporter(sources, config) { ) const defaultConfig = { - plot: "#simplot", + plotId: "#simplot", + plotName: "binned similarity" } - const fullConfig = mergeLeft(config, defaultConfig) + const fullConfig = mergeLeft(sources.config, defaultConfig) const state$ = sources.onion.state$ @@ -304,7 +305,7 @@ function Exporter(sources, config) { const model_ = model(actions, state$, sources.vega, fullConfig) - const vdom$ = view(state$, model_.dataPresent, model_.exportData) + const vdom$ = view(state$, model_.dataPresent, model_.exportData, fullConfig) const fabInit$ = xs.of({ state: "init", diff --git a/src/js/pages/correlation.js b/src/js/pages/correlation.js index e0f705d8..92ee267d 100644 --- a/src/js/pages/correlation.js +++ b/src/js/pages/correlation.js @@ -77,7 +77,13 @@ function CorrelationWorkflow(sources) { // const tailTable = isolate(tailTableContainer, { onion: tailTableLens }) // ({...sources, input: xs.combine(signature$, filter$).map(([s, f]) => ({ query: s, filter: f })).remember() }); - const exporter = Exporter(sources, {plot: "#corrplot"}) + const exporter = Exporter({ + ...sources, + config: { + plotId: "#corrplot", + plotName: "correlation" + } + }) const pageStyle = { style: { From ae46eafe216d60de1f75a172798e25a7cd18d79e Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 4 Feb 2022 10:43:53 +0100 Subject: [PATCH 137/191] Move creation of export modal lines into separate function that is reused --- src/js/components/Exporter.js | 101 ++++++++-------------------------- 1 file changed, 24 insertions(+), 77 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 273fff8f..b3c49e27 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -171,11 +171,6 @@ function view(state$, dataPresent, exportData, config) { exportData.tailTableCsv$, ) .map(([signaturePresent, plotsPresent, headTablePresent, tailTablePresent, url, signature, similarityPlot, headTableCsv, tailTableCsv]) => { - const signatureAvailable = signaturePresent ? "" : " .disabled" - const plotsAvailable = plotsPresent ? "" : " .disabled" - const headTableAvailable = headTablePresent ? "" : " .disabled" - const tailTableAvailable = tailTablePresent ? "" : " .disabled" - const reportAvailable = " .disabled" const urlFile = "data:text/plain;charset=utf-8," + url const signatureFile = "data:text/plain;charset=utf-8," + signature @@ -183,86 +178,38 @@ function view(state$, dataPresent, exportData, config) { const headTableCsvFile = "data:text/tsv;charset=utf-8," + encodeURIComponent(headTableCsv) const tailTableCsvFile = "data:text/tsv;charset=utf-8," + encodeURIComponent(tailTableCsv) + const addExportDiv = (text, clipboardId, fileData, fileName, available) => { + const availableText = available ? "" : " .disabled" + + return div(".row", [ + span(".col .s6 .push-s1", text), + span(".btn .col .s1 .offset-s1 " + clipboardId + availableText, i(".material-icons", "content_copy")), + a(".btn .col .s1 .offset-s1" + availableText, + { + props: { + href: fileData, + download: fileName, + }, + }, + i(".material-icons", "file_download"), + ), + ]) + } + return div([ div("#modal-exporter.modal", [ div(".modal-content", [ div(".row .title", p(".col .s12", "Export to clipboard or file") ), - div(".row", [ - span(".col .s6 .push-s1", "Create link to this page's state"), - span(".btn .col .s1 .offset-s1 .export-clipboard-link", i(".material-icons", "content_copy")), - // span(".btn .col .s1 .offset-s1 .export-file-link", i(".material-icons", "file_download")), - a(".btn .col .s1 .offset-s1", - { - props: { - href: urlFile, - download: "url.txt", - }, - }, - i(".material-icons", "file_download"), - ), - ]), - div(".row", [ - span(".col .s6 .push-s1", "Copy signature"), - span(".btn .col .s1 .offset-s1 .export-clipboard-signature" + signatureAvailable, i(".material-icons", "content_copy")), - // span(".btn .col .s1 .offset-s1 .export-file-signature" + signatureAvailable, i(".material-icons", "file_download")), - a(".btn .col .s1 .offset-s1" + signatureAvailable, - { - props: { - href: signatureFile, - download: "signature.txt", - }, - }, - i(".material-icons", "file_download"), - ), - ]), - div(".row", [ - span(".col .s6 .push-s1", "Copy " + config.plotName + " plot"), - span(".btn .col .s1 .offset-s1 .export-clipboard-plots" + plotsAvailable, i(".material-icons", "content_copy")), - // span(".btn .col .s1 .offset-s1 .export-file-plots" + plotsAvailable, i(".material-icons", "file_download")), - a(".btn .col .s1 .offset-s1" + plotsAvailable, - { - props: { - href: plotsFile, - download: "plot.png", - }, - }, - i(".material-icons", "file_download"), - ), - ]), - div(".row", [ - span(".col .s6 .push-s1", "Copy top table"), - span(".btn .col .s1 .offset-s1 .export-clipboard-headTable" + headTableAvailable, i(".material-icons", "content_copy")), - // span(".btn .col .s1 .offset-s1 .export-file-headTable" + headTableAvailable, i(".material-icons", "file_download")), - a(".btn .col .s1 .offset-s1" + headTableAvailable, - { - props: { - href: headTableCsvFile, - download: "table.tsv", - }, - }, - i(".material-icons", "file_download"), - ), - ]), - div(".row", [ - span(".col .s6 .push-s1", "Copy bottom table"), - span(".btn .col .s1 .offset-s1 .export-clipboard-tailTable" + tailTableAvailable, i(".material-icons", "content_copy")), - // span(".btn .col .s1 .offset-s1 .export-file-tailTable" + bottomTableAvailable, i(".material-icons", "file_download")), - a(".btn .col .s1 .offset-s1" + tailTableAvailable, - { - props: { - href: tailTableCsvFile, - download: "table.tsv", - }, - }, - i(".material-icons", "file_download"), - ), - ]), + addExportDiv("Create link to this page's state", ".export-clipboard-link", urlFile, "url.txt", true), + addExportDiv("Copy signature", ".export-clipboard-signature", signatureFile, "signature.txt", signaturePresent), + addExportDiv("Copy " + config.plotName + " plot", ".export-clipboard-plots", plotsFile, "plot.png", plotsPresent), + addExportDiv("Copy top table", ".export-clipboard-headTable", headTableCsvFile, "table.tsv", headTablePresent), + addExportDiv("Copy bottom table", ".export-clipboard-tailTable", tailTableCsvFile, "table.tsv", tailTablePresent), div(".row", [ span(".col .s6 .push-s1", "Export report"), - // span(".btn .col .s1 .offset-s1", i(".material-icons", "content_copy")), - span(".btn .col .s1 .offset-s3 .export-file-report" + reportAvailable, i(".material-icons", "file_download")), + span(".btn .col .s1 .offset-s3 .export-file-report .disabled", i(".material-icons", "file_download")), ]), ]), div(".modal-footer", [ From fa90523da30cdeec7e35d5069ca3f40b9cbb2fb6 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 4 Feb 2022 11:26:56 +0100 Subject: [PATCH 138/191] Add config field in config for the signature button to be disabled or hidden Intended values are "", ".hide", ".disabled" --- src/js/components/Exporter.js | 9 +++++---- src/js/pages/correlation.js | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index b3c49e27..2721820f 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -150,7 +150,7 @@ function view(state$, dataPresent, exportData, config) { span(".btn-floating .btn-large", i(".large .material-icons", "share")), ul([ li(span(".btn-floating .export-clipboard-link", i(".material-icons", "link"))), - li(span(".btn-floating .export-clipboard-signature", i(".material-icons", "content_copy"))), + li(span(".btn-floating .export-clipboard-signature " + config.fabSignature, i(".material-icons", "content_copy"))), // li(span(".btn-floating .export-file-report", i(".material-icons", "picture_as_pdf"))), li(span(".btn-floating .modal-open-btn", i(".material-icons", "open_with"))), // li(span(".btn-floating .test-btn", i(".material-icons", "star"))), @@ -232,7 +232,7 @@ function view(state$, dataPresent, exportData, config) { -function Exporter(sources, config) { +function Exporter(sources) { const logger = loggerFactory( "exporter", @@ -241,8 +241,9 @@ function Exporter(sources, config) { ) const defaultConfig = { - plotId: "#simplot", - plotName: "binned similarity" + plotId: "#simplot", // id of the div passed to vega + plotName: "binned similarity", // part of the text to be displayed for plot copy/download + fabSignature: "", // part of FAB class name, set to "", ".hide" or ".disabled" } const fullConfig = mergeLeft(sources.config, defaultConfig) diff --git a/src/js/pages/correlation.js b/src/js/pages/correlation.js index 92ee267d..b805d9fa 100644 --- a/src/js/pages/correlation.js +++ b/src/js/pages/correlation.js @@ -81,7 +81,8 @@ function CorrelationWorkflow(sources) { ...sources, config: { plotId: "#corrplot", - plotName: "correlation" + plotName: "correlation", + fabSignature: ".disabled", } }) From 577d9c9f39d1e85131dfb147cfc290525c3cd896 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 4 Feb 2022 13:09:47 +0100 Subject: [PATCH 139/191] add some basic documentation --- src/js/components/Exporter.js | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 2721820f..22ff1b70 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -6,6 +6,16 @@ import delay from "xstream/extra/delay" import sampleCombine from "xstream/extra/sampleCombine" import { convertToCSV } from "../utils/export" +/** + * @module components/Exporter + */ + +/** + * Get triggers from button presses in the DOM + * @function intent + * @param {*} domSource$ + * @returns object containing trigger streams + */ function intent(domSource$) { const exportLinkTrigger$ = domSource$.select(".export-clipboard-link").events("click") const exportSignatureTrigger$ = domSource$.select(".export-clipboard-signature").events("click") @@ -30,6 +40,23 @@ function intent(domSource$) { } } +/** + * collect data to be made available for copy/download + * trigger modal to be opened or closed + * send data to clipboard when triggered + * + * @function model + * @param {Object} actions object of trigger streams + * @param {Stream} state$ full state + * @param {Stream} vega$ stream of vega objects, to be filtered + * @param {Object} config configuration object passed from workflow + * @returns Object with + * * reducers$ placeholder for reducers + * * modal$ data to be sent to the modal driver + * * clipboard$ data to be sent to the clipboard driver + * * dataPresent Object with booleans for what data is available + * * exportData Object with data + */ function model(actions, state$, vega$, config) { const openModal$ = actions.modalTrigger$ @@ -142,6 +169,14 @@ function model(actions, state$, vega$, config) { } } +/** + * @function view + * @param {Stream} state$ full state + * @param {Object} dataPresent object with booleans of what data is available + * @param {Object} exportData object with available data + * @param {Object} config configuration object passed from workflow + * @returns Vdom div object with nested children for FAB and modal + */ function view(state$, dataPresent, exportData, config) { const fab$ = dataPresent.signaturePresent$ From 1b2c38978f8e7b6fb8689b9518ba3ef734e4c44a Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 7 Feb 2022 08:50:16 +0100 Subject: [PATCH 140/191] Add file download data types in model instead of in view --- src/js/components/Exporter.js | 52 +++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 22ff1b70..5bc500c0 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -75,7 +75,8 @@ function model(actions, state$, vega$, config) { const url$ = state$.map((state) => state.routerInformation.pageStateURL).startWith("") const signature$ = state$.map((state) => state.form.signature?.output).startWith("") - const similarityPlot$ = vega$ + // result already contains 'data:image/png;base64,' + const plotFile$ = vega$ .filter(vega => vega.el == config.plotId) .map((vega) => xs.fromPromise(vega.view.toImageURL('png'))) .flatten() @@ -91,6 +92,11 @@ function model(actions, state$, vega$, config) { .map((data) => convertToCSV(data)) .startWith("") + const urlFile$ = url$.map(url => "data:text/plain;charset=utf-8," + url) + const signatureFile$ = signature$.map(signature => "data:text/plain;charset=utf-8," + signature) + const headTableCsvFile$ = headTableCsv$.map(headTableCsv => "data:text/tsv;charset=utf-8," + encodeURIComponent(headTableCsv)) + const tailTableCsvFile$ = tailTableCsv$.map(tailTableCsv => "data:text/tsv;charset=utf-8," + encodeURIComponent(tailTableCsv)) + const clipboardLink$ = actions.exportLinkTrigger$ .compose(sampleCombine(url$)) .map(([_, url]) => url) @@ -102,7 +108,7 @@ function model(actions, state$, vega$, config) { .remember() const clipboardPlots$ = actions.exportPlotsTrigger$ - .compose(sampleCombine(similarityPlot$)) + .compose(sampleCombine(plotFile$)) .map(([_, data]) => { // input data is "data:image/png;base64,abcdef0123456789..." const parts = data.split(';base64,'); @@ -131,7 +137,7 @@ function model(actions, state$, vega$, config) { .remember() const testAction$ = actions.testTrigger$ - .compose(sampleCombine(similarityPlot$)) + .compose(sampleCombine(plotFile$)) .map(([_, data]) => { // input data is "data:image/png;base64,abcdef0123456789..." const parts = data.split(';base64,'); @@ -161,10 +167,11 @@ function model(actions, state$, vega$, config) { }, exportData: { url$: url$, - signature$: signature$, - similarityPlot$: similarityPlot$, - headTableCsv$: headTableCsv$, - tailTableCsv$: tailTableCsv$, + urlFile$: urlFile$, + signatureFile$: signatureFile$, + plotFile$: plotFile$, + headTableCsvFile$: headTableCsvFile$, + tailTableCsvFile$: tailTableCsvFile$, } } } @@ -200,19 +207,24 @@ function view(state$, dataPresent, exportData, config) { dataPresent.headTablePresent$, dataPresent.tailTablePresent$, exportData.url$, - exportData.signature$, - exportData.similarityPlot$, - exportData.headTableCsv$, - exportData.tailTableCsv$, + exportData.urlFile$, + exportData.signatureFile$, + exportData.plotFile$, + exportData.headTableCsvFile$, + exportData.tailTableCsvFile$, ) - .map(([signaturePresent, plotsPresent, headTablePresent, tailTablePresent, url, signature, similarityPlot, headTableCsv, tailTableCsv]) => { - - const urlFile = "data:text/plain;charset=utf-8," + url - const signatureFile = "data:text/plain;charset=utf-8," + signature - const plotsFile = similarityPlot - const headTableCsvFile = "data:text/tsv;charset=utf-8," + encodeURIComponent(headTableCsv) - const tailTableCsvFile = "data:text/tsv;charset=utf-8," + encodeURIComponent(tailTableCsv) - + .map(([ + signaturePresent, + plotsPresent, + headTablePresent, + tailTablePresent, + url, + urlFile, + signatureFile, + plotFile, + headTableCsvFile, + tailTableCsvFile + ]) => { const addExportDiv = (text, clipboardId, fileData, fileName, available) => { const availableText = available ? "" : " .disabled" @@ -239,7 +251,7 @@ function view(state$, dataPresent, exportData, config) { ), addExportDiv("Create link to this page's state", ".export-clipboard-link", urlFile, "url.txt", true), addExportDiv("Copy signature", ".export-clipboard-signature", signatureFile, "signature.txt", signaturePresent), - addExportDiv("Copy " + config.plotName + " plot", ".export-clipboard-plots", plotsFile, "plot.png", plotsPresent), + addExportDiv("Copy " + config.plotName + " plot", ".export-clipboard-plots", plotFile, "plot.png", plotsPresent), addExportDiv("Copy top table", ".export-clipboard-headTable", headTableCsvFile, "table.tsv", headTablePresent), addExportDiv("Copy bottom table", ".export-clipboard-tailTable", tailTableCsvFile, "table.tsv", tailTablePresent), div(".row", [ From 6dd2d19f3663edc81c963a816beda86867e3f5ab Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 7 Feb 2022 13:48:44 +0100 Subject: [PATCH 141/191] Add wave effect on copy/download buttons This effect is also used in the table for the download buttons --- src/js/components/Exporter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 5bc500c0..974385fb 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -230,8 +230,8 @@ function view(state$, dataPresent, exportData, config) { return div(".row", [ span(".col .s6 .push-s1", text), - span(".btn .col .s1 .offset-s1 " + clipboardId + availableText, i(".material-icons", "content_copy")), - a(".btn .col .s1 .offset-s1" + availableText, + span(".btn .col .s1 .offset-s1 .waves-effect .waves-light " + clipboardId + availableText, i(".material-icons", "content_copy")), + a(".btn .col .s1 .offset-s1 .waves-effect .waves-light " + availableText, { props: { href: fileData, From 2c5e1edf57fb768a07faf8a529c7173046cfb1e6 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 7 Feb 2022 16:21:52 +0100 Subject: [PATCH 142/191] Fix downloads messing up the router. Has something to do with clicking a 'a' directly make the i child the full size of the a parent to capture the direct click --- src/js/components/Exporter.js | 4 ++-- src/sass/_exporter.scss | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 974385fb..c2b7311c 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -231,7 +231,7 @@ function view(state$, dataPresent, exportData, config) { return div(".row", [ span(".col .s6 .push-s1", text), span(".btn .col .s1 .offset-s1 .waves-effect .waves-light " + clipboardId + availableText, i(".material-icons", "content_copy")), - a(".btn .col .s1 .offset-s1 .waves-effect .waves-light " + availableText, + a(".btn .col .s1 .offset-s1 .waves-effect .waves-light .paddingfix " + availableText, { props: { href: fileData, @@ -239,7 +239,7 @@ function view(state$, dataPresent, exportData, config) { }, }, i(".material-icons", "file_download"), - ), + ) ]) } diff --git a/src/sass/_exporter.scss b/src/sass/_exporter.scss index 6f154157..99182688 100644 --- a/src/sass/_exporter.scss +++ b/src/sass/_exporter.scss @@ -32,6 +32,16 @@ div #modal-exporter { // background-color: green; + // clicking *directly* on the a download link makes the router(?) go nuts. make sure we always click the i + // so to recapitulate, changing the padding fixes the underlying issue. the padding in itself is ok + a.paddingfix { + padding-left: 0px; + padding-right: 0px; + i { + width: 100%; + } + } + div.modal-footer { > button.export-close{ background-color: color('blue-grey', 'base') From 5651aa8c8473fbd6e164ed5e219c12e5d788039b Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 8 Feb 2022 14:45:42 +0100 Subject: [PATCH 143/191] Add extra explanation of the workaround remove all padding of the a element instead of only left & right --- src/js/components/Exporter.js | 8 ++++++++ src/sass/_exporter.scss | 3 +-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index c2b7311c..32a81597 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -228,6 +228,14 @@ function view(state$, dataPresent, exportData, config) { const addExportDiv = (text, clipboardId, fileData, fileName, available) => { const availableText = available ? "" : " .disabled" + // Styling should prevent the user to click the 'a' directly; this causes the page div#root to be corrupted. + // Work around is to make the internal 'i' the full size of the 'a' thus "catching" the initial click. + // Exact reason is not 100% clear. Using preventDefault doesn't seem to work. + // + // At the time of writing, the impression is that it could have to do with the exporter or sub-parts not being isolated. + // Debugging suggest the @cycle/dom to be the culprit. + // Removing the 'div#Root' -> 'fromEvent.js:16' event listener prevents the page from misbehaving. + // Workaround is done in '.paddingfix' in the scss. return div(".row", [ span(".col .s6 .push-s1", text), span(".btn .col .s1 .offset-s1 .waves-effect .waves-light " + clipboardId + availableText, i(".material-icons", "content_copy")), diff --git a/src/sass/_exporter.scss b/src/sass/_exporter.scss index 99182688..adfdb89e 100644 --- a/src/sass/_exporter.scss +++ b/src/sass/_exporter.scss @@ -35,8 +35,7 @@ div #modal-exporter { // clicking *directly* on the a download link makes the router(?) go nuts. make sure we always click the i // so to recapitulate, changing the padding fixes the underlying issue. the padding in itself is ok a.paddingfix { - padding-left: 0px; - padding-right: 0px; + padding: 0px; i { width: 100%; } From 482d80a2f1db24bd89ef06271ddc01cb1a74b560 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 9 Feb 2022 11:43:57 +0100 Subject: [PATCH 144/191] use numTableHead and numTableTail parameters in search query to set default entries in table Sets the start value, the user can still change the value later, so functionally should and does act as an alternative default value from the settings --- src/js/components/Table.js | 61 ++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/src/js/components/Table.js b/src/js/components/Table.js index c2d38db6..39161eca 100644 --- a/src/js/components/Table.js +++ b/src/js/components/Table.js @@ -76,13 +76,24 @@ const headTableLens = { sourire: state.settings.sourire, filter: state.settings.filter, }, - ui: (state.ui??{}).headTable ?? {dirty: false}, // Get state.ui.headTable in a safe way or else get a default + ui: state.ui?.headTable ?? {dirty: false}, // Get state.ui.headTable in a safe way or else get a default + numEntries: state.routerInformation?.params?.numTableHead, }), - set: (state, childState) => ({ + set: (state, childState) => { + // don't add value of numEntres to the pageState if it is the default value + const numEntries = childState.core.numEntries == parseInt(childState.settings.table.count) ? undefined : childState.core.numEntries + return { ...state, headTable: childState.core, settings: { ...state.settings, headTable: childState.settings.table }, - }), + routerInformation: { + ...state.routerInformation, + pageState: { + ...state.routerInformation.pageState, + numTableHead: numEntries, + } + } + }}, } /** @@ -105,13 +116,24 @@ const tailTableLens = { sourire: state.settings.sourire, filter: state.settings.filter, }, - ui: (state.ui??{}).tailTable ?? {dirty: false}, // Get state.ui.tailTable in a safe way or else get a default + ui: state.ui?.tailTable ?? {dirty: false}, // Get state.ui.tailTable in a safe way or else get a default + numEntries: state.routerInformation?.params?.numTableTail, }), - set: (state, childState) => ({ + set: (state, childState) => { + // don't add value of numEntres to the pageState if it is the default value + const numEntries = childState.core.numEntries == parseInt(childState.settings.table.count) ? undefined : childState.core.numEntries + return { ...state, tailTable: childState.core, settings: { ...state.settings, tailTable: childState.settings.table }, - }), + routerInformation: { + ...state.routerInformation, + pageState: { + ...state.routerInformation.pageState, + numTableTail: numEntries, + } + } + }}, } /** @@ -134,7 +156,7 @@ const compoundContainerTableLens = { sourire: state.settings.sourire, filter: state.settings.filter, }, - ui: (state.ui??{}).compoundTable ?? {dirty: false}, // Get state.ui.compoundTable in a safe way or else get a default + ui: state.ui?.compoundTable ?? {dirty: false}, // Get state.ui.compoundTable in a safe way or else get a default }), set: (state, childState) => ({ ...state, @@ -336,11 +358,17 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { * second update 0 5 5 * * pairwise gives us previous and new value but need to make sure that if we only receive 1 value we do get an output, so use .startWith(0) + * + * If a value for the amount of entries was passed through the search query, use that as default, otherwise use the standard default setting * * @const makeTable/Table/defaultAmountToDisplay$ * @type {Stream} */ - const defaultAmountToDisplay$ = state$.map(state => parseInt(state.settings.table.count)) + const defaultAmountToDisplay$ = xs.combine( + state$.map(state => parseInt(state.settings.table.count)), + state$.map(state => state.numEntries), + ) + .map(([default_, searchQuery]) => (isNaN(searchQuery) ? default_ : searchQuery)) .compose(dropRepeats(equals)) .startWith(0) .compose(pairwise) @@ -867,6 +895,20 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { core: { ...prevState.core, expandOptions: yesorno }, })) + /** + * Reducer for passing the amount of entries shown in the table to the search query params object + * @const makeTable/Table/amountOfDisplayedLinesReducer$ + * @type {Reducer} + */ + const amountOfDisplayedLinesReducer$ = amountToDisplay$ + .map((new_) => (prevState) => { + const val = max(1, new_) + return { + ...prevState, + core: { ...prevState.core, numEntries: val} + } + }) + return { DOM: vdom$, HTTP: request$, @@ -875,7 +917,8 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { inputReducer$, requestReducer$, dataReducer$, - switchReducer$ + switchReducer$, + amountOfDisplayedLinesReducer$, ), log: xs.merge( logger(modifiedState$, "state$") From a0e8b16dd260e0683dc3809256931887a75dff3b Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 9 Feb 2022 13:13:50 +0100 Subject: [PATCH 145/191] Initialize FAB once vdom$ launches vdom Add extra 1ms delay so vdom$ can propagate to DOM before calling FAB init --- src/js/components/Exporter.js | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 32a81597..27854cbb 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -1,9 +1,10 @@ import xs from "xstream" import { div, i, ul, li, p, input, button, span, a } from "@cycle/dom" -import { isEmpty, mergeLeft } from "ramda" +import { isEmpty, mergeLeft, equals } from "ramda" import { loggerFactory } from "../utils/logger" import delay from "xstream/extra/delay" import sampleCombine from "xstream/extra/sampleCombine" +import dropRepeats from "xstream/extra/dropRepeats" import { convertToCSV } from "../utils/export" /** @@ -186,19 +187,18 @@ function model(actions, state$, vega$, config) { */ function view(state$, dataPresent, exportData, config) { - const fab$ = dataPresent.signaturePresent$ - .map((signature) => + const fab$ = xs + .of( div(".fixed-action-btn", [ - span(".btn-floating .btn-large", i(".large .material-icons", "share")), - ul([ - li(span(".btn-floating .export-clipboard-link", i(".material-icons", "link"))), - li(span(".btn-floating .export-clipboard-signature " + config.fabSignature, i(".material-icons", "content_copy"))), - // li(span(".btn-floating .export-file-report", i(".material-icons", "picture_as_pdf"))), - li(span(".btn-floating .modal-open-btn", i(".material-icons", "open_with"))), - // li(span(".btn-floating .test-btn", i(".material-icons", "star"))), - ]) - ])) - .startWith(div()) + span(".btn-floating .btn-large", i(".large .material-icons", "share")), + ul([ + li(span(".btn-floating .export-clipboard-link", i(".material-icons", "link"))), + li(span(".btn-floating .export-clipboard-signature " + config.fabSignature, i(".material-icons", "content_copy"))), + // li(span(".btn-floating .export-file-report", i(".material-icons", "picture_as_pdf"))), + li(span(".btn-floating .modal-open-btn", i(".material-icons", "open_with"))), + // li(span(".btn-floating .test-btn", i(".material-icons", "star"))), + ]) + ])) const modal$ = xs .combine( @@ -310,14 +310,16 @@ function Exporter(sources) { const vdom$ = view(state$, model_.dataPresent, model_.exportData, fullConfig) - const fabInit$ = xs.of({ + const fabInit$ = vdom$.mapTo({ state: "init", element: ".fixed-action-btn", options: { direction: "top", // hoverEnabled: false, } - }).compose(delay(1000)).remember() + }) + .compose(dropRepeats(equals)) // run just once + .compose(delay(1)) // let the vdom propagate first and next cycle initialize FAB return { log: xs.merge(logger(state$, "state$")), From 66e2c5747ff2e36a3273b61d809365f80728ea53 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 9 Feb 2022 13:45:31 +0100 Subject: [PATCH 146/191] Add unit test for search queries in filters Simple test with changed values, unchanged values & an invalid option Check both 'output' and 'filter_output' --- src/test/FilterTest.js | 60 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/test/FilterTest.js b/src/test/FilterTest.js index 810f534b..a12b77af 100644 --- a/src/test/FilterTest.js +++ b/src/test/FilterTest.js @@ -292,3 +292,63 @@ describe("uiReducer", function () { }) }) }) + +describe("Search query", function () { + it("Updates the output which should match that of the search query, except for invalid values which should be discarded", () => { + const possibleValues = { + dose: [1, 2, 3], + cell: ["cell1", "cell2", "cell3"], + trtType: ["a", "b", "c"], + } + const possibleValues$ = fromDiagram("-x").mapTo(possibleValues) + const search = xs.of({ + dose: [1, 2], + cell: ["cell1", "cell2", "cell3"], + trtType: ["a", "b", "d"] + }) + const search$ = fromDiagram("--x").mapTo(search) + + const reducers$ = model( + possibleValues$, + xs.empty(), + xs.empty(), + xs.empty(), + xs.empty(), + search$ + ) + + // Predefine filters in settings as possible filters will be updated there + const defaultFilter = {settings: {filter: {}}} + const state$ = reducers$.fold((state, reducer) => reducer(state), defaultFilter) + + // Outputs all valid values + let expectedOutput = [ + {}, + {}, + {}, + { dose: [2, 3], cell: ["cell1", "cell2", "cell3"], trtType: ["a", "b"] }, + ] + + let expectedFilterOutput = [ + {}, + {}, + {}, + { dose: [2, 3], cell: undefined, trtType: ["a", "b"] }, + ] + + state$ + .drop(1) // drop the first state as it is undefined + .addListener({ + next(state) { + assert.deepStrictEqual(state?.core?.output, expectedOutput.shift()) + assert.deepStrictEqual(state?.core?.filter_output, expectedFilterOutput.shift()) + }, + error(e) { + console.log(e) + }, + complete() { + console.log("done!") + }, + }) + }) +}) From fe820c30676170b3fd92a485b11a64ff1a30aa10 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 9 Feb 2022 13:52:56 +0100 Subject: [PATCH 147/191] Gskcmp 86 auto complete doesnt work with single result while still not complete (#114) * Update the AutoComplete even if there is only 1 result Prevents an incomplete entry getting no results However, now requires the user to type out the full or click the suggestion * Don't feed autofeed when the input matches the single AutoComplete data This restores the original intended functionality * Clear AutoComplete when input completely matches single entry When the user types the full name, close the AutoComplete instead of wanting the user to confirm their only option --- src/js/components/TreatmentCheck.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index 947bef45..788b87b5 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -226,11 +226,10 @@ function TreatmentCheck(sources) { })) // Feed the autocomplete driver - const ac$ = data$ - .filter((data) => data.length > 1) - .map((data) => ({ + const ac$ = data$.compose(sampleCombine(input$)) + .map(([data, input]) => ({ el: ".treatmentQuery", - data: data, + data: (data.length == 1 && data[0].trtId == input) ? [] : data, render: function (data) { return mergeAll( data.map((d) => ({ [d.trtId + " - " + d.trtName + " (" + d.count + ")"]: null })) From b16aa491d01e14644d99675d57c9c80b307e5799 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 9 Feb 2022 14:19:53 +0100 Subject: [PATCH 148/191] Truncate dose when text is too long When dose isn't a number or the dose unit is long, truncate the whole string --- src/js/components/SampleSelection.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/js/components/SampleSelection.js b/src/js/components/SampleSelection.js index 1ad1677a..e9b0288a 100644 --- a/src/js/components/SampleSelection.js +++ b/src/js/components/SampleSelection.js @@ -172,7 +172,15 @@ function SampleSelection(sources) { td(selectedClass(entry.use), ((_) => { const dose = entry.dose !== "N/A" ? entry.dose + " " + entry.dose_unit : entry.dose - return dose.length > 6 ? dose.substring(0, 6) + "..." : dose + const maxLength = 7 + if (dose.length <= maxLength) + return dose + else if (isNaN(entry.dose) || entry.dose_unit >= 3) + return dose.substring(0, maxLength-1) + "..." + // adding '...' is quite small on screen (in non-monospaced fonts), so we're ignoring that + else + return Number(entry.dose).toFixed(maxLength - 3 - entry.dose_unit?.length) + " " + entry.dose_unit + // -3 = '0.' and ' ' })() ), // td(selectedClass(entry.use), entry.batch), From 08395641a36d8991cf00ec445cf1a5dec0989369 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 9 Feb 2022 14:21:50 +0100 Subject: [PATCH 149/191] Version bump to 5.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ce61b0e3..6659160e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "LuciusWeb", - "version": "5.0.0", + "version": "5.0.1", "description": "Web interface for ComPass aka Lucius", "repository": { "type": "git", From f9ccfd5872be37ea5c91e94ff2c38b9d70c7c509 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 9 Feb 2022 15:10:07 +0100 Subject: [PATCH 150/191] Add autorun to search query by default However, only add '?' or autorun when a state is available, otherwise just link base link address --- src/js/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/js/index.js b/src/js/index.js index 700a808a..6e7945f4 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -440,12 +440,17 @@ export default function Index(sources) { // Don't output fields with undefined values const filteredState = pickBy(identity, state) const url = new URLSearchParams(filteredState) + const urlString = url.toString() + + const fullUrl = urlString.length > 0 + ? location.protocol + "//" + location.host + prevState.routerInformation.pathname + "?autorun&" + url.toString() + : location.protocol + "//" + location.host + prevState.routerInformation.pathname return { ...prevState, routerInformation: { ...prevState.routerInformation, - pageStateURL: location.protocol + "//" + location.host + prevState.routerInformation.pathname + "?" + url.toString(), + pageStateURL: fullUrl, } } From a3d1b243eabf68cc241cf45310c4fed087bc55ae Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 9 Feb 2022 15:56:06 +0100 Subject: [PATCH 151/191] Hide pdf report option for now it's never available, so don't show the button --- src/js/components/Exporter.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 27854cbb..1bdd5654 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -262,10 +262,10 @@ function view(state$, dataPresent, exportData, config) { addExportDiv("Copy " + config.plotName + " plot", ".export-clipboard-plots", plotFile, "plot.png", plotsPresent), addExportDiv("Copy top table", ".export-clipboard-headTable", headTableCsvFile, "table.tsv", headTablePresent), addExportDiv("Copy bottom table", ".export-clipboard-tailTable", tailTableCsvFile, "table.tsv", tailTablePresent), - div(".row", [ - span(".col .s6 .push-s1", "Export report"), - span(".btn .col .s1 .offset-s3 .export-file-report .disabled", i(".material-icons", "file_download")), - ]), + // div(".row", [ + // span(".col .s6 .push-s1", "Export report"), + // span(".btn .col .s1 .offset-s3 .export-file-report .disabled", i(".material-icons", "file_download")), + // ]), ]), div(".modal-footer", [ button(".export-close .col .s8 .push-s2 .btn", "Close"), From b177d614a95e56e055ce3ec4ff16f319b700c969 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 9 Feb 2022 16:37:14 +0100 Subject: [PATCH 152/191] Add option to update FAB when signature becomes available, add waves effect Added waves effect on FAB to indicate action --- src/js/components/Exporter.js | 37 ++++++++++++++----- .../drivers/makeFloatingActionButtonDriver.js | 14 ++++++- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 1bdd5654..01ed7e64 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -187,18 +187,23 @@ function model(actions, state$, vega$, config) { */ function view(state$, dataPresent, exportData, config) { - const fab$ = xs - .of( - div(".fixed-action-btn", [ + const fab$ = dataPresent.signaturePresent$ + .map((present) => { + + const extraSigClass = config.fabSignature != "update" + ? config.fabSignature + : present ? "" : ".disabled" + + return div(".fixed-action-btn", [ span(".btn-floating .btn-large", i(".large .material-icons", "share")), ul([ - li(span(".btn-floating .export-clipboard-link", i(".material-icons", "link"))), - li(span(".btn-floating .export-clipboard-signature " + config.fabSignature, i(".material-icons", "content_copy"))), + li(span(".btn-floating .export-clipboard-link .waves-effect.waves-light", i(".material-icons", "link"))), + li(span(".btn-floating .export-clipboard-signature .waves-effect.waves-light " + extraSigClass, i(".material-icons", "content_copy"))), // li(span(".btn-floating .export-file-report", i(".material-icons", "picture_as_pdf"))), - li(span(".btn-floating .modal-open-btn", i(".material-icons", "open_with"))), + li(span(".btn-floating .modal-open-btn .waves-effect.waves-light", i(".material-icons", "open_with"))), // li(span(".btn-floating .test-btn", i(".material-icons", "star"))), ]) - ])) + ])}) const modal$ = xs .combine( @@ -298,7 +303,8 @@ function Exporter(sources) { const defaultConfig = { plotId: "#simplot", // id of the div passed to vega plotName: "binned similarity", // part of the text to be displayed for plot copy/download - fabSignature: "", // part of FAB class name, set to "", ".hide" or ".disabled" + fabSignature: "update", // part of FAB class name, set to "", ".hide" or ".disabled". + //"update" sets ".disabled" when the signature is not available and updates the FAB when it becomes available } const fullConfig = mergeLeft(sources.config, defaultConfig) @@ -321,11 +327,24 @@ function Exporter(sources) { .compose(dropRepeats(equals)) // run just once .compose(delay(1)) // let the vdom propagate first and next cycle initialize FAB + const fabUpdate$ = model_.dataPresent.signaturePresent$ + .filter(_ => fullConfig.fabSignature == "update") + .compose(dropRepeats(equals)) + .mapTo({ + state: "update", + element: ".fixed-action-btn", + options: { + direction: "top", + // hoverEnabled: false, + } + }) + .compose(delay(1)) // let the vdom propagate first and next cycle update FAB + return { log: xs.merge(logger(state$, "state$")), DOM: vdom$, onion: model_.reducers$, - fab: fabInit$, + fab: xs.merge(fabInit$, fabUpdate$), modal: model_.modal$, clipboard: model_.clipboard$, } diff --git a/src/js/drivers/makeFloatingActionButtonDriver.js b/src/js/drivers/makeFloatingActionButtonDriver.js index 37a0be99..c8598f5b 100644 --- a/src/js/drivers/makeFloatingActionButtonDriver.js +++ b/src/js/drivers/makeFloatingActionButtonDriver.js @@ -26,7 +26,19 @@ function makeFloatingActionButtonDriver() { } fab.close() } - if (ev.state == 'open') { + else if (ev.state == 'update') { + const currentlyOpen = fab?.isOpen + const elem = document.querySelector(ev.element) + if (elem == undefined) + console.warn("fabDriver couldn't find element") + else { + fab.close() + fab = M.FloatingActionButton.init(elem, ev.options); + if (currentlyOpen && fab != undefined) + fab.open() + } + } + else if (ev.state == 'open') { if (fab == undefined) { From 05a786dab8cf9263b6b54349dab480ac2fb0f155 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 9 Feb 2022 16:44:14 +0100 Subject: [PATCH 153/191] Hide signature FAB shortcut for disease and correlation WF --- src/js/pages/correlation.js | 2 +- src/js/pages/disease.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/js/pages/correlation.js b/src/js/pages/correlation.js index b805d9fa..d6474f75 100644 --- a/src/js/pages/correlation.js +++ b/src/js/pages/correlation.js @@ -82,7 +82,7 @@ function CorrelationWorkflow(sources) { config: { plotId: "#corrplot", plotName: "correlation", - fabSignature: ".disabled", + fabSignature: ".hide", } }) diff --git a/src/js/pages/disease.js b/src/js/pages/disease.js index bff6a792..ec31fd41 100644 --- a/src/js/pages/disease.js +++ b/src/js/pages/disease.js @@ -190,7 +190,10 @@ function DiseaseWorkflow(sources) { .remember(), }) - const exporter = Exporter(sources) + const exporter = Exporter({ + ...sources, + config: { fabSignature: ".hide" } + }) /** * Style object used in div capsulating filter, displayPlots and tables From 3693a3d99a6ecdd9c636ea0621694deb1ae20c15 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 10 Feb 2022 12:34:22 +0100 Subject: [PATCH 154/191] WIP rewrite of clipboard to return permissions and results --- src/js/components/Exporter.js | 32 ++++++++----- src/js/drivers/clipboardDriver.js | 77 ++++++++++++++++++++++--------- 2 files changed, 77 insertions(+), 32 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 01ed7e64..73f41364 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -183,9 +183,10 @@ function model(actions, state$, vega$, config) { * @param {Object} dataPresent object with booleans of what data is available * @param {Object} exportData object with available data * @param {Object} config configuration object passed from workflow + * @param {Object} clipboard state of the clipboard driver which gives us our permissions and results * @returns Vdom div object with nested children for FAB and modal */ -function view(state$, dataPresent, exportData, config) { +function view(state$, dataPresent, exportData, config, clipboard) { const fab$ = dataPresent.signaturePresent$ .map((present) => { @@ -217,6 +218,8 @@ function view(state$, dataPresent, exportData, config) { exportData.plotFile$, exportData.headTableCsvFile$, exportData.tailTableCsvFile$, + clipboard.copyImagesPermission$, + clipboard.results$, ) .map(([ signaturePresent, @@ -228,10 +231,16 @@ function view(state$, dataPresent, exportData, config) { signatureFile, plotFile, headTableCsvFile, - tailTableCsvFile + tailTableCsvFile, + clipboardPermissions, + clipboardResult, ]) => { - const addExportDiv = (text, clipboardId, fileData, fileName, available) => { - const availableText = available ? "" : " .disabled" + + const copyImagesPermission = clipboardPermissions.state == "granted" + + const addExportDiv = (text, clipboardId, fileData, fileName, available, clipboardAllowed=true, downloadAllowed=true) => { + const availableClipboardText = available && clipboardAllowed ? "" : " .disabled" + const availableDownloadText = available && downloadAllowed ? "" : " .disabled" // Styling should prevent the user to click the 'a' directly; this causes the page div#root to be corrupted. // Work around is to make the internal 'i' the full size of the 'a' thus "catching" the initial click. @@ -243,8 +252,8 @@ function view(state$, dataPresent, exportData, config) { // Workaround is done in '.paddingfix' in the scss. return div(".row", [ span(".col .s6 .push-s1", text), - span(".btn .col .s1 .offset-s1 .waves-effect .waves-light " + clipboardId + availableText, i(".material-icons", "content_copy")), - a(".btn .col .s1 .offset-s1 .waves-effect .waves-light .paddingfix " + availableText, + span(".btn .col .s1 .offset-s1 .waves-effect .waves-light " + clipboardId + " " + availableClipboardText, i(".material-icons", "content_copy")), + a(".btn .col .s1 .offset-s1 .waves-effect .waves-light .paddingfix " + availableDownloadText, { props: { href: fileData, @@ -259,12 +268,13 @@ function view(state$, dataPresent, exportData, config) { return div([ div("#modal-exporter.modal", [ div(".modal-content", [ - div(".row .title", - p(".col .s12", "Export to clipboard or file") - ), + div(".row .title", [ + p(".col .s12", "Export to clipboard or file"), + p(".col .s12", "" + clipboardResult.text), + ]), addExportDiv("Create link to this page's state", ".export-clipboard-link", urlFile, "url.txt", true), addExportDiv("Copy signature", ".export-clipboard-signature", signatureFile, "signature.txt", signaturePresent), - addExportDiv("Copy " + config.plotName + " plot", ".export-clipboard-plots", plotFile, "plot.png", plotsPresent), + addExportDiv("Copy " + config.plotName + " plot", ".export-clipboard-plots", plotFile, "plot.png", plotsPresent, copyImagesPermission), addExportDiv("Copy top table", ".export-clipboard-headTable", headTableCsvFile, "table.tsv", headTablePresent), addExportDiv("Copy bottom table", ".export-clipboard-tailTable", tailTableCsvFile, "table.tsv", tailTablePresent), // div(".row", [ @@ -314,7 +324,7 @@ function Exporter(sources) { const model_ = model(actions, state$, sources.vega, fullConfig) - const vdom$ = view(state$, model_.dataPresent, model_.exportData, fullConfig) + const vdom$ = view(state$, model_.dataPresent, model_.exportData, fullConfig, sources.clipboard) const fabInit$ = vdom$.mapTo({ state: "init", diff --git a/src/js/drivers/clipboardDriver.js b/src/js/drivers/clipboardDriver.js index de1fa69f..38cf0d80 100644 --- a/src/js/drivers/clipboardDriver.js +++ b/src/js/drivers/clipboardDriver.js @@ -1,37 +1,72 @@ +import xs from "xstream" + function clipboardDriver(stream$) { + + const results$ = xs.createWithMemory().startWith({}) + + const reportSuccessfull = (functionName, sender) => () => { + console.log("clipboard " + functionName + " successful") + results$.shamefullySendNext({ + sender: sender, + state: "success", + text: "clipboard " + functionName + " successful" + }) + } + + const reportFailed = (functionName, sender) => () => { + console.warn("clipboard " + functionName + " failed") + results$.shamefullySendNext({ + sender: sender, + state: "success", + text: "clipboard " + functionName + " failed" + }) + } + stream$.addListener({ next: message => { - if (typeof message === 'object') { - // console.log('object received for clipboard, type: ' + message.type) - navigator.clipboard.write([ - new ClipboardItem({ - [message.type]: message.data - }) - ]) - .then( - function() { - /* clipboard successfully set */ - }, function() { - /* clipboard write failed */ - console.warn("Writing to clipboard failed") - }); + //console.log('object received for clipboard, type: ' + message.type) + try { + navigator.clipboard.write([ + new ClipboardItem({ + [message.type]: message.data + }) + ]) + .then( + reportSuccessfull("write", message.sender), + reportFailed("write", message.sender) + ) + } + catch{ + // mimic promise call + reportFailed("write try", message.sender)() + } } else { - // console.log("text that should be placed in clipboard: " + message) navigator.clipboard.writeText(message) .then( - function() { - /* clipboard successfully set */ - }, function() { - /* clipboard write failed */ - console.warn("Writing to clipboard failed") - }); + reportSuccessfull("writeText"), + reportFailed("writeText") + ) } }, error: e => console.error(e), complete: () => {} }) + + // Chrome by default allows clipboard write and implements the permissions API call + // Firefox by default doesn't allow the clipboard write and doesn't implement the permissions API call + // so we're kind of forced to assume that if the API call fails that we won't have the necessary permissions + const queryOpts = { name: 'clipboard-write', allowWithoutGesture: false }; + const copyImagesPermission$ = xs + .fromPromise( navigator.permissions.query(queryOpts) ) + .replaceError(_ => xs.of({state: "error"})) + .debug() + + return { + copyImagesPermission$: copyImagesPermission$, + results$: results$.debug("clipboard results$"), + } } export { clipboardDriver } \ No newline at end of file From dd2742abd7d3fe5643c7d452f517dec2e051b107 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 10 Feb 2022 14:22:11 +0100 Subject: [PATCH 155/191] Add sender in clipboard object add undefined type as meaning raw text to be copied split class identifiers of elements in fab and modal (url and signature) let modal use clipboard objects with sender --- src/js/components/Exporter.js | 41 +++++++++++++++++++++++++------ src/js/drivers/clipboardDriver.js | 11 +++++---- src/sass/_exporter.scss | 4 +-- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 73f41364..8faf3ae2 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -18,6 +18,9 @@ import { convertToCSV } from "../utils/export" * @returns object containing trigger streams */ function intent(domSource$) { + const exportLinkTriggerFab$ = domSource$.select(".export-clipboard-link-fab").events("click") + const exportSignatureTriggerFab$ = domSource$.select(".export-clipboard-signature-fab").events("click") + const exportLinkTrigger$ = domSource$.select(".export-clipboard-link").events("click") const exportSignatureTrigger$ = domSource$.select(".export-clipboard-signature").events("click") const exportPlotsTrigger$ = domSource$.select(".export-clipboard-plots").events("click") @@ -30,6 +33,8 @@ function intent(domSource$) { const testTrigger$ = domSource$.select(".test-btn").events("click") return { + exportLinkTriggerFab$: exportLinkTriggerFab$, + exportSignatureTriggerFab$: exportSignatureTriggerFab$, exportLinkTrigger$: exportLinkTrigger$, exportSignatureTrigger$: exportSignatureTrigger$, exportPlotsTrigger$: exportPlotsTrigger$, @@ -98,16 +103,32 @@ function model(actions, state$, vega$, config) { const headTableCsvFile$ = headTableCsv$.map(headTableCsv => "data:text/tsv;charset=utf-8," + encodeURIComponent(headTableCsv)) const tailTableCsvFile$ = tailTableCsv$.map(tailTableCsv => "data:text/tsv;charset=utf-8," + encodeURIComponent(tailTableCsv)) - const clipboardLink$ = actions.exportLinkTrigger$ + const clipboardLinkFab$ = actions.exportLinkTriggerFab$ .compose(sampleCombine(url$)) .map(([_, url]) => url) .remember() - const clipboardSignature$ = actions.exportSignatureTrigger$ + const clipboardSignatureFab$ = actions.exportSignatureTriggerFab$ .compose(sampleCombine(signature$)) .map(([_, signature]) => signature) .remember() + const clipboardLink$ = actions.exportLinkTrigger$ + .compose(sampleCombine(url$)) + .map(([_, url]) => ({ + sender: "url", + data: url, + })) + .remember() + + const clipboardSignature$ = actions.exportSignatureTrigger$ + .compose(sampleCombine(signature$)) + .map(([_, signature]) => ({ + sender: "signature", + data: signature, + })) + .remember() + const clipboardPlots$ = actions.exportPlotsTrigger$ .compose(sampleCombine(plotFile$)) .map(([_, data]) => { @@ -129,12 +150,18 @@ function model(actions, state$, vega$, config) { const clipboardHeadTable$ = actions.exportHeadTableTrigger$ .compose(sampleCombine(headTableCsv$)) - .map(([_, table]) => table) + .map(([_, table]) => ({ + sender: "headTable", + data: table, + })) .remember() const clipboardTailTable$ = actions.exportTailTableTrigger$ .compose(sampleCombine(tailTableCsv$)) - .map(([_, table]) => table) + .map(([_, table]) => ({ + sender: "tailTable", + data: table, + })) .remember() const testAction$ = actions.testTrigger$ @@ -159,7 +186,7 @@ function model(actions, state$, vega$, config) { return { reducers$: xs.empty(), modal$: xs.merge(openModal$, closeModal$), - clipboard$: xs.merge(clipboardLink$, clipboardSignature$, clipboardPlots$, clipboardHeadTable$, clipboardTailTable$, testAction$), + clipboard$: xs.merge(clipboardLinkFab$, clipboardSignatureFab$, clipboardLink$, clipboardSignature$, clipboardPlots$, clipboardHeadTable$, clipboardTailTable$, testAction$), dataPresent: { signaturePresent$: signaturePresent$, plotsPresent$: plotsPresent$, @@ -198,8 +225,8 @@ function view(state$, dataPresent, exportData, config, clipboard) { return div(".fixed-action-btn", [ span(".btn-floating .btn-large", i(".large .material-icons", "share")), ul([ - li(span(".btn-floating .export-clipboard-link .waves-effect.waves-light", i(".material-icons", "link"))), - li(span(".btn-floating .export-clipboard-signature .waves-effect.waves-light " + extraSigClass, i(".material-icons", "content_copy"))), + li(span(".btn-floating .export-clipboard-link-fab .waves-effect.waves-light", i(".material-icons", "link"))), + li(span(".btn-floating .export-clipboard-signature-fab .waves-effect.waves-light " + extraSigClass, i(".material-icons", "content_copy"))), // li(span(".btn-floating .export-file-report", i(".material-icons", "picture_as_pdf"))), li(span(".btn-floating .modal-open-btn .waves-effect.waves-light", i(".material-icons", "open_with"))), // li(span(".btn-floating .test-btn", i(".material-icons", "star"))), diff --git a/src/js/drivers/clipboardDriver.js b/src/js/drivers/clipboardDriver.js index 38cf0d80..9c16776e 100644 --- a/src/js/drivers/clipboardDriver.js +++ b/src/js/drivers/clipboardDriver.js @@ -17,14 +17,14 @@ function clipboardDriver(stream$) { console.warn("clipboard " + functionName + " failed") results$.shamefullySendNext({ sender: sender, - state: "success", + state: "failed", text: "clipboard " + functionName + " failed" }) } stream$.addListener({ next: message => { - if (typeof message === 'object') { + if (typeof message === 'object' && message.type != undefined) { //console.log('object received for clipboard, type: ' + message.type) try { navigator.clipboard.write([ @@ -43,10 +43,11 @@ function clipboardDriver(stream$) { } } else { - navigator.clipboard.writeText(message) + const data = typeof message === 'string' ? message : message.data + navigator.clipboard.writeText(data) .then( - reportSuccessfull("writeText"), - reportFailed("writeText") + reportSuccessfull("writeText", message.sender), + reportFailed("writeText", message.sender) ) } }, diff --git a/src/sass/_exporter.scss b/src/sass/_exporter.scss index adfdb89e..ca63252a 100644 --- a/src/sass/_exporter.scss +++ b/src/sass/_exporter.scss @@ -13,10 +13,10 @@ background-color: color('red', 'base'); } > ul { - span.export-clipboard-link { + span.export-clipboard-link-fab { background-color: color('red', 'base') } - span.export-clipboard-signature { + span.export-clipboard-signature-fab { background-color: color('yellow', 'darken-1') } span.export-pdf { From 856f19c80264eeca119991d7e473a1b0ef299d11 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 10 Feb 2022 15:25:00 +0100 Subject: [PATCH 156/191] Display clipboard results in modal For now just make button green or red for 2 seconds --- src/js/components/Exporter.js | 32 +++++++++++++++++++++---------- src/js/drivers/clipboardDriver.js | 2 +- src/sass/_exporter.scss | 12 +++++++++++- 3 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 8faf3ae2..84a655af 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -3,6 +3,7 @@ import { div, i, ul, li, p, input, button, span, a } from "@cycle/dom" import { isEmpty, mergeLeft, equals } from "ramda" import { loggerFactory } from "../utils/logger" import delay from "xstream/extra/delay" +import debounce from "xstream/extra/debounce" import sampleCombine from "xstream/extra/sampleCombine" import dropRepeats from "xstream/extra/dropRepeats" import { convertToCSV } from "../utils/export" @@ -142,6 +143,7 @@ function model(actions, state$, vega$, config) { } const blob = new Blob([uInt8Array], { type: imageType }) return { + sender: "plot", type: imageType, data: blob, } @@ -233,6 +235,12 @@ function view(state$, dataPresent, exportData, config, clipboard) { ]) ])}) + const clipboardResultAutoClear$ = xs + .merge( + clipboard.results$, + clipboard.results$.compose(debounce(2000)).mapTo({}) + ) + const modal$ = xs .combine( dataPresent.signaturePresent$, @@ -246,7 +254,7 @@ function view(state$, dataPresent, exportData, config, clipboard) { exportData.headTableCsvFile$, exportData.tailTableCsvFile$, clipboard.copyImagesPermission$, - clipboard.results$, + clipboardResultAutoClear$, ) .map(([ signaturePresent, @@ -265,10 +273,14 @@ function view(state$, dataPresent, exportData, config, clipboard) { const copyImagesPermission = clipboardPermissions.state == "granted" - const addExportDiv = (text, clipboardId, fileData, fileName, available, clipboardAllowed=true, downloadAllowed=true) => { + const addExportDiv = (identifier, text, clipboardId, fileData, fileName, available, clipboardAllowed=true, downloadAllowed=true) => { const availableClipboardText = available && clipboardAllowed ? "" : " .disabled" const availableDownloadText = available && downloadAllowed ? "" : " .disabled" + const clipboardBtnResult = identifier != clipboardResult?.sender + ? "" + : clipboardResult.state == "success" ? " .success" : " .failure" + // Styling should prevent the user to click the 'a' directly; this causes the page div#root to be corrupted. // Work around is to make the internal 'i' the full size of the 'a' thus "catching" the initial click. // Exact reason is not 100% clear. Using preventDefault doesn't seem to work. @@ -279,7 +291,7 @@ function view(state$, dataPresent, exportData, config, clipboard) { // Workaround is done in '.paddingfix' in the scss. return div(".row", [ span(".col .s6 .push-s1", text), - span(".btn .col .s1 .offset-s1 .waves-effect .waves-light " + clipboardId + " " + availableClipboardText, i(".material-icons", "content_copy")), + span(".btn .col .s1 .offset-s1 .waves-effect .waves-light " + clipboardId + " " + availableClipboardText + clipboardBtnResult, i(".material-icons", "content_copy")), a(".btn .col .s1 .offset-s1 .waves-effect .waves-light .paddingfix " + availableDownloadText, { props: { @@ -297,13 +309,13 @@ function view(state$, dataPresent, exportData, config, clipboard) { div(".modal-content", [ div(".row .title", [ p(".col .s12", "Export to clipboard or file"), - p(".col .s12", "" + clipboardResult.text), + //p(".col .s12", "" + clipboardResult.text), ]), - addExportDiv("Create link to this page's state", ".export-clipboard-link", urlFile, "url.txt", true), - addExportDiv("Copy signature", ".export-clipboard-signature", signatureFile, "signature.txt", signaturePresent), - addExportDiv("Copy " + config.plotName + " plot", ".export-clipboard-plots", plotFile, "plot.png", plotsPresent, copyImagesPermission), - addExportDiv("Copy top table", ".export-clipboard-headTable", headTableCsvFile, "table.tsv", headTablePresent), - addExportDiv("Copy bottom table", ".export-clipboard-tailTable", tailTableCsvFile, "table.tsv", tailTablePresent), + addExportDiv("url", "Create link to this page's state", ".export-clipboard-link", urlFile, "url.txt", true), + addExportDiv("signature", "Copy signature", ".export-clipboard-signature", signatureFile, "signature.txt", signaturePresent), + addExportDiv("plot", "Copy " + config.plotName + " plot", ".export-clipboard-plots", plotFile, "plot.png", plotsPresent, copyImagesPermission), + addExportDiv("headTable", "Copy top table", ".export-clipboard-headTable", headTableCsvFile, "table.tsv", headTablePresent), + addExportDiv("tailTable", "Copy bottom table", ".export-clipboard-tailTable", tailTableCsvFile, "table.tsv", tailTablePresent), // div(".row", [ // span(".col .s6 .push-s1", "Export report"), // span(".btn .col .s1 .offset-s3 .export-file-report .disabled", i(".material-icons", "file_download")), @@ -311,7 +323,7 @@ function view(state$, dataPresent, exportData, config, clipboard) { ]), div(".modal-footer", [ button(".export-close .col .s8 .push-s2 .btn", "Close"), - div(".col .s12 .blue.lighten-3", {style: {wordWrap: "break-word"}}, url), + //div(".col .s12 .blue.lighten-3", {style: {wordWrap: "break-word"}}, url), ]), ]), ]) diff --git a/src/js/drivers/clipboardDriver.js b/src/js/drivers/clipboardDriver.js index 9c16776e..9fdbf1c8 100644 --- a/src/js/drivers/clipboardDriver.js +++ b/src/js/drivers/clipboardDriver.js @@ -66,7 +66,7 @@ function clipboardDriver(stream$) { return { copyImagesPermission$: copyImagesPermission$, - results$: results$.debug("clipboard results$"), + results$: results$, } } diff --git a/src/sass/_exporter.scss b/src/sass/_exporter.scss index ca63252a..c777ed9e 100644 --- a/src/sass/_exporter.scss +++ b/src/sass/_exporter.scss @@ -30,7 +30,17 @@ } div #modal-exporter { - // background-color: green; + + div.title { + font-size: 1.5rem; + } + + span.success { + background-color: green; + } + span.failure { + background-color: red; + } // clicking *directly* on the a download link makes the router(?) go nuts. make sure we always click the i // so to recapitulate, changing the padding fixes the underlying issue. the padding in itself is ok From e59399d5c9af68439fc598423e33d1f21113f0c6 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 10 Feb 2022 17:30:16 +0100 Subject: [PATCH 157/191] Always require objects to be passed to clipboard driver Rewrite poc driver to not use shameful methods but instead create a nested provider & listener (thanks Toni!) --- src/js/components/Exporter.js | 11 ++- src/js/drivers/clipboardDriver.js | 129 +++++++++++++++--------------- src/js/main.js | 4 +- 3 files changed, 77 insertions(+), 67 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 84a655af..7ea75ba9 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -106,12 +106,18 @@ function model(actions, state$, vega$, config) { const clipboardLinkFab$ = actions.exportLinkTriggerFab$ .compose(sampleCombine(url$)) - .map(([_, url]) => url) + .map(([_, url]) => ({ + sender: "url-fab", + data: url, + })) .remember() const clipboardSignatureFab$ = actions.exportSignatureTriggerFab$ .compose(sampleCombine(signature$)) - .map(([_, signature]) => signature) + .map(([_, signature]) => ({ + sender: "signature-fab", + data: signature, + })) .remember() const clipboardLink$ = actions.exportLinkTrigger$ @@ -240,6 +246,7 @@ function view(state$, dataPresent, exportData, config, clipboard) { clipboard.results$, clipboard.results$.compose(debounce(2000)).mapTo({}) ) + .startWith({}) const modal$ = xs .combine( diff --git a/src/js/drivers/clipboardDriver.js b/src/js/drivers/clipboardDriver.js index 9fdbf1c8..6450f42b 100644 --- a/src/js/drivers/clipboardDriver.js +++ b/src/js/drivers/clipboardDriver.js @@ -1,73 +1,76 @@ import xs from "xstream" +import flattenConcurrently from "xstream/extra/flattenConcurrently" -function clipboardDriver(stream$) { - - const results$ = xs.createWithMemory().startWith({}) - - const reportSuccessfull = (functionName, sender) => () => { - console.log("clipboard " + functionName + " successful") - results$.shamefullySendNext({ - sender: sender, - state: "success", - text: "clipboard " + functionName + " successful" - }) - } - - const reportFailed = (functionName, sender) => () => { - console.warn("clipboard " + functionName + " failed") - results$.shamefullySendNext({ - sender: sender, - state: "failed", - text: "clipboard " + functionName + " failed" - }) - } - - stream$.addListener({ - next: message => { - if (typeof message === 'object' && message.type != undefined) { - //console.log('object received for clipboard, type: ' + message.type) - try { - navigator.clipboard.write([ - new ClipboardItem({ - [message.type]: message.data - }) - ]) - .then( - reportSuccessfull("write", message.sender), - reportFailed("write", message.sender) - ) - } - catch{ - // mimic promise call - reportFailed("write try", message.sender)() - } - } - else { - const data = typeof message === 'string' ? message : message.data - navigator.clipboard.writeText(data) - .then( - reportSuccessfull("writeText", message.sender), - reportFailed("writeText", message.sender) - ) - } - }, - error: e => console.error(e), - complete: () => {} - }) - +/** + * Pass message objects with members: + * - type: data type to set, use '' or undefined if raw text + * - data: data blob + * - sender: sender id, is added in return value + * @returns object with members: + * - state: 'success' or 'failure' + * - sender: sender id from message object + */ +function makeClipboardDriver() { // Chrome by default allows clipboard write and implements the permissions API call // Firefox by default doesn't allow the clipboard write and doesn't implement the permissions API call // so we're kind of forced to assume that if the API call fails that we won't have the necessary permissions - const queryOpts = { name: 'clipboard-write', allowWithoutGesture: false }; + const queryOpts = { name: "clipboard-write", allowWithoutGesture: false } const copyImagesPermission$ = xs - .fromPromise( navigator.permissions.query(queryOpts) ) - .replaceError(_ => xs.of({state: "error"})) - .debug() + .fromPromise(navigator.permissions.query(queryOpts)) + .replaceError((_) => xs.of({ state: "error" })) - return { - copyImagesPermission$: copyImagesPermission$, - results$: results$, + function clipboardDriver(stream$) { + const results$ = xs + .create({ + start: (listener) => { + stream$.addListener({ + next: (message) => { + const clipboard$ = + message.type != undefined && message.type != "" + ? (() => { + try { + return xs.fromPromise( + navigator.clipboard.write([ + new ClipboardItem({ + [message.type]: message.data, + }), + ]) + ) + } catch { + return xs.of({ + state: "failure", + sender: message.sender, + }) + } + })() + : xs.fromPromise(navigator.clipboard.writeText(message.data)) + const clipboardMapped$ = clipboard$ + .debug("clipboard$") + .map((res) => + res == undefined + ? { state: "success", sender: message.sender } + : res + ) + .replaceError((_) => + xs.of({ state: "failure", sender: message.sender }) + ) + listener.next(clipboardMapped$) + }, + error: (e) => { + console.log(e) + }, + }) + }, + stop: () => {}, + }) + + return { + copyImagesPermission$: copyImagesPermission$, + results$: results$.compose(flattenConcurrently), + } } + + return clipboardDriver } -export { clipboardDriver } \ No newline at end of file +export { makeClipboardDriver } diff --git a/src/js/main.js b/src/js/main.js index 97be4547..5446f568 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -25,7 +25,7 @@ import { makeModalDriver } from './drivers/makeModalDriver' import { makeAutocompleteDriver } from './drivers/makeAutocompleteDriver'; import { makeSidenavDriver } from './drivers/makeSidenavDriver'; import { makeFloatingActionButtonDriver } from './drivers/makeFloatingActionButtonDriver'; -import { clipboardDriver } from './drivers/clipboardDriver'; +import { makeClipboardDriver } from './drivers/clipboardDriver'; import '../sass/main.scss' import fromEvent from 'xstream/extra/fromEvent' @@ -49,7 +49,7 @@ const drivers = { ac: makeAutocompleteDriver(), sidenav: makeSidenavDriver(), fab: makeFloatingActionButtonDriver(), - clipboard: clipboardDriver, + clipboard: makeClipboardDriver(), deployments: () => xs.fromPromise(fetch('/deployments.json').then(m => m.json())) }; From ed912f0c9b0dc6ea0c63eb3e1071926cc3c0faad Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 10 Feb 2022 17:41:19 +0100 Subject: [PATCH 158/191] Give DOM more time before initializing FAB 1ms delay is not guaranteed to be enough. so far 50ms seems fine. --- src/js/components/Exporter.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 01ed7e64..f291f380 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -325,7 +325,7 @@ function Exporter(sources) { } }) .compose(dropRepeats(equals)) // run just once - .compose(delay(1)) // let the vdom propagate first and next cycle initialize FAB + .compose(delay(50)) // let the vdom propagate first and next cycle initialize FAB const fabUpdate$ = model_.dataPresent.signaturePresent$ .filter(_ => fullConfig.fabSignature == "update") @@ -338,7 +338,7 @@ function Exporter(sources) { // hoverEnabled: false, } }) - .compose(delay(1)) // let the vdom propagate first and next cycle update FAB + .compose(delay(50)) // let the vdom propagate first and next cycle update FAB return { log: xs.merge(logger(state$, "state$")), @@ -350,4 +350,4 @@ function Exporter(sources) { } } -export {Exporter} \ No newline at end of file +export {Exporter} From afb690dd1e71d94f8bc3e14ef8844c4b0c262506 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 10 Feb 2022 18:00:13 +0100 Subject: [PATCH 159/191] rename clipboardDriver -> makeClipboardDriver --- src/js/drivers/{clipboardDriver.js => makeClipboardDriver.js} | 0 src/js/main.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/js/drivers/{clipboardDriver.js => makeClipboardDriver.js} (100%) diff --git a/src/js/drivers/clipboardDriver.js b/src/js/drivers/makeClipboardDriver.js similarity index 100% rename from src/js/drivers/clipboardDriver.js rename to src/js/drivers/makeClipboardDriver.js diff --git a/src/js/main.js b/src/js/main.js index 5446f568..cf255792 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -25,7 +25,7 @@ import { makeModalDriver } from './drivers/makeModalDriver' import { makeAutocompleteDriver } from './drivers/makeAutocompleteDriver'; import { makeSidenavDriver } from './drivers/makeSidenavDriver'; import { makeFloatingActionButtonDriver } from './drivers/makeFloatingActionButtonDriver'; -import { makeClipboardDriver } from './drivers/clipboardDriver'; +import { makeClipboardDriver } from './drivers/makeClipboardDriver' import '../sass/main.scss' import fromEvent from 'xstream/extra/fromEvent' From cb5a07b8fcef42eda7047ac9dafde2d28a3071ee Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 11 Feb 2022 09:52:34 +0100 Subject: [PATCH 160/191] Add clipboard result to FAB --- src/js/components/Exporter.js | 38 ++++++++++++++++++++++++----------- src/sass/_exporter.scss | 20 ++++++++++++++++++ 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 7ea75ba9..f27050b4 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -223,30 +223,44 @@ function model(actions, state$, vega$, config) { */ function view(state$, dataPresent, exportData, config, clipboard) { - const fab$ = dataPresent.signaturePresent$ - .map((present) => { + const clipboardResultAutoClear$ = xs + .merge( + clipboard.results$, + clipboard.results$.compose(debounce(2000)).mapTo({}) + ) + .startWith({}) + + const fab$ = xs + .combine( + dataPresent.signaturePresent$, + clipboardResultAutoClear$, + ) + .map(([signaturePresent, clipboardResult]) => { + const clipboardUrlBtnResult = "url-fab" != clipboardResult?.sender + ? "" + : clipboardResult.state == "success" ? " .success" : " .failure" + + const clipboardSigBtnResult = "signature-fab" != clipboardResult?.sender + ? "" + : clipboardResult.state == "success" ? " .success" : " .failure" + + const extraSigClass = config.fabSignature != "update" ? config.fabSignature - : present ? "" : ".disabled" + : signaturePresent ? "" : " .disabled" return div(".fixed-action-btn", [ span(".btn-floating .btn-large", i(".large .material-icons", "share")), ul([ - li(span(".btn-floating .export-clipboard-link-fab .waves-effect.waves-light", i(".material-icons", "link"))), - li(span(".btn-floating .export-clipboard-signature-fab .waves-effect.waves-light " + extraSigClass, i(".material-icons", "content_copy"))), + li(span(".btn-floating .export-clipboard-link-fab .waves-effect.waves-light",span(".fab-wrap" + clipboardUrlBtnResult, i(".material-icons", "link")))), + li(span(".btn-floating .export-clipboard-signature-fab .waves-effect.waves-light" + extraSigClass, span(".fab-wrap" + clipboardSigBtnResult, i(".material-icons", "content_copy")))), // li(span(".btn-floating .export-file-report", i(".material-icons", "picture_as_pdf"))), - li(span(".btn-floating .modal-open-btn .waves-effect.waves-light", i(".material-icons", "open_with"))), + li(span(".btn-floating .modal-open-btn .waves-effect.waves-light", span(".test3", i(".material-icons", "open_with")))), // li(span(".btn-floating .test-btn", i(".material-icons", "star"))), ]) ])}) - const clipboardResultAutoClear$ = xs - .merge( - clipboard.results$, - clipboard.results$.compose(debounce(2000)).mapTo({}) - ) - .startWith({}) const modal$ = xs .combine( diff --git a/src/sass/_exporter.scss b/src/sass/_exporter.scss index c777ed9e..a2df2322 100644 --- a/src/sass/_exporter.scss +++ b/src/sass/_exporter.scss @@ -25,6 +25,26 @@ span.modal-open-btn { background-color: color('blue', 'base') } + + span.fab-wrap{ + display: block; + overflow: hidden; + position: relative; + width: 100%; + height: 100%; + background-color: transparent; + box-shadow: none; + color: #fff; + // line-height: $button-floating-large-size; + z-index: 1; + + &.success{ + background-color:green; + } + &.failure{ + background-color:red; + } + } } } From 47d747c623e58cd8d30aca5cdda27a3d4d127fc8 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 11 Feb 2022 11:18:44 +0100 Subject: [PATCH 161/191] keep FAB open when updating it FAB should be destroyed during update instead of closed, fixes most of behavior issues. Still does some animation but at least the overall state is correct now --- src/js/drivers/makeFloatingActionButtonDriver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/drivers/makeFloatingActionButtonDriver.js b/src/js/drivers/makeFloatingActionButtonDriver.js index c8598f5b..64c042c3 100644 --- a/src/js/drivers/makeFloatingActionButtonDriver.js +++ b/src/js/drivers/makeFloatingActionButtonDriver.js @@ -32,7 +32,7 @@ function makeFloatingActionButtonDriver() { if (elem == undefined) console.warn("fabDriver couldn't find element") else { - fab.close() + fab.destroy() fab = M.FloatingActionButton.init(elem, ev.options); if (currentlyOpen && fab != undefined) fab.open() From 106049befcddf10a7179b46b7c763139e21741f6 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 11 Feb 2022 13:44:04 +0100 Subject: [PATCH 162/191] Provide optional stream sink for clipboard driver Allows clipboard driver to be used without a result listener and instead provides a void sink for the result stream --- src/js/drivers/makeClipboardDriver.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/js/drivers/makeClipboardDriver.js b/src/js/drivers/makeClipboardDriver.js index 6450f42b..de517f4c 100644 --- a/src/js/drivers/makeClipboardDriver.js +++ b/src/js/drivers/makeClipboardDriver.js @@ -6,11 +6,13 @@ import flattenConcurrently from "xstream/extra/flattenConcurrently" * - type: data type to set, use '' or undefined if raw text * - data: data blob * - sender: sender id, is added in return value + * @param {Boolean} provideOwnSink when set to true, the driver adds a sink to the result output stream so that the clipboard + * functionality still works even if the output stream is not used * @returns object with members: * - state: 'success' or 'failure' * - sender: sender id from message object */ -function makeClipboardDriver() { +function makeClipboardDriver(provideOwnSink= false) { // Chrome by default allows clipboard write and implements the permissions API call // Firefox by default doesn't allow the clipboard write and doesn't implement the permissions API call // so we're kind of forced to assume that if the API call fails that we won't have the necessary permissions @@ -64,6 +66,13 @@ function makeClipboardDriver() { stop: () => {}, }) + if (provideOwnSink) { + results$.addListener({ + next: () => {}, + error: () => {}, + }) + } + return { copyImagesPermission$: copyImagesPermission$, results$: results$.compose(flattenConcurrently), From 52dad60a55e18d7ce413873ec0c7e1a406b76f80 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 15 Feb 2022 09:11:03 +0100 Subject: [PATCH 163/191] if search query, reload page without search query but store search query in router state This removes the ...?xxx from the browser URL which prevents users making changes to the page and believing the URL can be copied and to have a valid state However this introduces an additional issue that page refreshes or clicking on the same WF still starts the same search query as it stays in the router state Handle this by only converting the data when the router type is set to 'push'. When we push to the state, the router state is set to 'push' but when the user refreshes/clicks same WF link the type is undefined --- src/js/index.js | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/src/js/index.js b/src/js/index.js index b3bef475..b4e1e5c4 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -418,8 +418,14 @@ export default function Index(sources) { return result } - const params = new URLSearchParams(router.search) - const paramsObject = paramsToObject(params) + // we should not apply the same search query values when the page was reloaded + // if user refreshes or clicks same WF link, router.type is not set + // if we push new state, router.type == "push" + let paramsObject = {} + if (router?.type == "push") { + const params = new URLSearchParams(router.state) + paramsObject = paramsToObject(params) + } return { ...prevState, @@ -460,27 +466,13 @@ export default function Index(sources) { const history$ = sources.onion.state$.fold((acc, x) => acc.concat([x]), [{}]) - const historyDriver$ = xs.empty() - // const historyDriver$ = xs.periodic(30000).map(i => ({ - // type: 'replace', - // hash: "tailTable"+i, - // //pathname: "/genetic", - // // search: "", - // // state: undefined, - // }) - // // '/genetic#tailTable' - // ) - - // PushHistoryInput( - // type: 'push' - // pathname?: Pathname; - // search?: Search; - // state?: any; - // hash?: Hash; - // key?: LocationKey; - // ) - - // const historyDriver$ = xs.periodic(1000).map(i => '/genetic') + const historyDriver$ = router.history$ + .map((router) => router.search) + .filter((search) => search != undefined && search != "" && search != "?") + .map((search) => ({ + type: 'push', + state: search, + })) return { log: xs.merge( From 95f3d48553621b6a23dacce167598465919948ff Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 17 Feb 2022 13:48:52 +0100 Subject: [PATCH 164/191] Version bump to 5.1.0-alpha1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ce61b0e3..c02c0d78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "LuciusWeb", - "version": "5.0.0", + "version": "5.1.0-alpha1", "description": "Web interface for ComPass aka Lucius", "repository": { "type": "git", From 5dc3218cd35f3cb1711ae8096c0772fdfe52bf56 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 7 Mar 2022 10:47:38 +0100 Subject: [PATCH 165/191] Add filter in correlation WF (#126) --- src/js/pages/correlation.js | 36 +++++++++++++++++++++++++++--------- src/sass/_main_custom.scss | 7 +++++++ 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/js/pages/correlation.js b/src/js/pages/correlation.js index d6474f75..ebc3f3f5 100644 --- a/src/js/pages/correlation.js +++ b/src/js/pages/correlation.js @@ -10,7 +10,7 @@ import { CorrelationForm, formLens } from '../components/CorrelationForm' import { CorrelationPlot, correlationPlotsLens } from '../components/BinnedPlots/CorrelationPlot' import { makeTable, headTableLens, tailTableLens } from '../components/Table' import { initSettings } from '../configuration.js' -import { Filter, compoundFilterLens } from '../components/Filter' +import { Filter, filterLens } from '../components/Filter' import { loggerFactory } from '../utils/logger' import { SampleTable, sampleTableLens } from '../components/SampleTable/SampleTable' import { Exporter } from "../components/Exporter" @@ -42,9 +42,16 @@ function CorrelationWorkflow(sources) { const correlationForm = isolate(CorrelationForm, { onion: formLens })(sources) const queries$ = correlationForm.output + const doubleSignature$ = queries$ + .filter((queries) => (queries.query1 != undefined && queries.query1 != "")) + .filter((queries) => (queries.query2 != undefined && queries.query2 != "")) + .map((queries) => (queries.query1 + " + " + queries.query2)) // Filter Form - // const filterForm = isolate(Filter, { onion: compoundFilterLens })({...sources, input: queries$}) - // const filter$ = filterForm.output.remember() + const filterForm = isolate(Filter, { onion: filterLens })({ + ...sources, + input: doubleSignature$ + }) + const filter$ = filterForm.output.remember() // default Reducer, initialization const defaultReducer$ = xs.of(prevState => { @@ -64,8 +71,18 @@ function CorrelationWorkflow(sources) { }) // Binned Plots Component - const correlationPlot = isolate(CorrelationPlot, { onion: correlationPlotsLens }) - ({...sources, input: queries$.remember() }); + const correlationPlot = isolate(CorrelationPlot, { onion: correlationPlotsLens })({ + ...sources, + input: xs + .combine(queries$, filter$) + .map(([queries, filter]) => + ({ + ...queries, + filter: filter, + }) + ) + .remember() + }); // tables // const headTableContainer = makeTable(SampleTable, sampleTableLens) @@ -98,7 +115,7 @@ function CorrelationWorkflow(sources) { const vdom$ = xs.combine( correlationForm.DOM, - // filterForm.DOM, + filterForm.DOM, correlationPlot.DOM, // headTable.DOM, // tailTable.DOM, @@ -107,7 +124,7 @@ function CorrelationWorkflow(sources) { ) .map(([ form, - // filter, + filter, plot, // headTable, // tailTable, @@ -117,7 +134,7 @@ function CorrelationWorkflow(sources) { div('.row .correlation', { style: { margin: '0px 0px 0px 0px' } }, [ form, div('.col .s10 .offset-s1', pageStyle, [ - // div('.row', [filter]), + div('.row', [filter]), div('.row', [plot]), // div('.row', []), // div('.col .s12', [headTable]), @@ -138,7 +155,7 @@ function CorrelationWorkflow(sources) { onion: xs.merge( defaultReducer$, correlationForm.onion, - // filterForm.onion, + filterForm.onion, correlationPlot.onion, // headTable.onion, // tailTable.onion, @@ -150,6 +167,7 @@ function CorrelationWorkflow(sources) { ), HTTP: xs.merge( correlationForm.HTTP, + filterForm.HTTP, correlationPlot.HTTP, // headTable.HTTP, // tailTable.HTTP diff --git a/src/sass/_main_custom.scss b/src/sass/_main_custom.scss index 58711bdb..3a6f3a77 100644 --- a/src/sass/_main_custom.scss +++ b/src/sass/_main_custom.scss @@ -7,4 +7,11 @@ main div.correlation { .validation { background-color: color("blue", "lighten-3"); } + + & > div:first-child { + div.validation > div > div { + margin-bottom: 0px; + } + margin-bottom: 20px; + } } From 5bd23a28e410821ecf7397d602ca4dfa676e794c Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 7 Mar 2022 11:18:44 +0100 Subject: [PATCH 166/191] Gskcmp 20 sorting sample selection table (#121) * Start making SampleSelection sortable Clicking the header triggers sort selection and direction Sort selection and direction is displayed in the header No actual sorting is happening for now * Use button in header and add sort icon on hover Use button as it is provides mouse cursor changes Add sort icon when hovering over the title to indicate functionality * Sort data when header clicked match header id to entry field names * Prevent header updates while loading Prevents updates on loaded vdom stream which would otherwise replace the loading vdom move icon name logic to separate const to make code clearer * Fix header buttons in place Keep the text centered with the icons next to it Force text and icons on the same line on small screens * Gskcmp 86 auto complete doesnt work with single result while still not complete (#114) * Update the AutoComplete even if there is only 1 result Prevents an incomplete entry getting no results However, now requires the user to type out the full or click the suggestion * Don't feed autofeed when the input matches the single AutoComplete data This restores the original intended functionality * Clear AutoComplete when input completely matches single entry When the user types the full name, close the AutoComplete instead of wanting the user to confirm their only option * Truncate dose when text is too long When dose isn't a number or the dose unit is long, truncate the whole string * Version bump to 5.0.1 * Don't allow 'Use' column to be sorted * Sort dose & time numerically These examples are typical issues 2 < 10 0.02 < 0.4 * Fix truncation with long dose unit names add missing .length --- src/js/components/SampleSelection.js | 141 ++++++++++++++++++++++++--- src/js/components/TreatmentCheck.js | 7 +- 2 files changed, 131 insertions(+), 17 deletions(-) diff --git a/src/js/components/SampleSelection.js b/src/js/components/SampleSelection.js index a755ef3f..a4752abb 100644 --- a/src/js/components/SampleSelection.js +++ b/src/js/components/SampleSelection.js @@ -12,8 +12,9 @@ import { thead, tbody, p, + i, } from "@cycle/dom" -import { clone, equals, merge } from "ramda" +import { clone, equals, merge, sortWith, prop, ascend, descend } from "ramda" import xs from "xstream" import dropRepeats from "xstream/extra/dropRepeats" import debounce from 'xstream/extra/debounce' @@ -151,7 +152,34 @@ function SampleSelection(sources) { : {} const selectedClass = (selected) => selected ? ".sampleSelected" : ".sampleDeselected" - let rows = data.map((entry) => [ + + function propSort(prop, descend) { + return (a, b) => { + const aValue = a[prop] + const bValue = b[prop] + const multi = descend ? -1 : 1 + + if (isNaN(aValue) || isNaN(bValue)) + // works properly for integers but not for decimal numbers, so only use it as fallback + return aValue.localeCompare(bValue, undefined, {numeric: true}) + else + return (Number(aValue) - Number(bValue) > 0 ? 1 : -1) * multi + } + } + + const dataSortAscend = sortWith([ + ascend(prop(state.core.sort)), + ]); + const dataSortDescend = sortWith([ + descend(prop(state.core.sort)), + ]); + const sortedData = state.core.sort !== "" + ? state.core.sort == "dose" || state.core.sort == "time" + ? sortWith([propSort(state.core.sort, state.core.direction)])(data) + : state.core.direction ? dataSortDescend(data) : dataSortAscend(data) + : data + + let rows = sortedData.map((entry) => [ td(".selection", { props: { id: entry.id } }, [ label("", { props: { id: entry.id } }, [ input( @@ -178,7 +206,15 @@ function SampleSelection(sources) { td(selectedClass(entry.use), ((_) => { const dose = entry.dose !== "N/A" ? entry.dose + " " + entry.dose_unit : entry.dose - return dose.length > 6 ? dose.substring(0, 6) + "..." : dose + const maxLength = 7 + if (dose.length <= maxLength) + return dose + else if (isNaN(entry.dose) || entry.dose_unit.length >= 3) + return dose.substring(0, maxLength-1) + "..." + // adding '...' is quite small on screen (in non-monospaced fonts), so we're ignoring that + else + return Number(entry.dose).toFixed(maxLength - 3 - entry.dose_unit?.length) + " " + entry.dose_unit + // -3 = '0.' and ' ' })() ), // td(selectedClass(entry.use), entry.batch), @@ -186,17 +222,61 @@ function SampleSelection(sources) { td(selectedClass(entry.use), entry.time !== "N/A" ? entry.time + " " + entry.time_unit : entry.time), td(selectedClass(entry.use), entry.significantGenes), ]) + + const sortableHeaderEntry = (id, text, state, sortable=true) => + { + const currentSortId = state.core.sort + const sortDirection = state.core.direction + const hover = state.core.sortHover === id + const loaded = state.core.data.length > 0 + + const sortIcon = + id === currentSortId ? + sortDirection ? "arrow_upward" : "arrow_downward" : + hover ? "sort" : "" + + return th( + button( + ".btn-flat" + (loaded && sortable ? " .sortable" : ""), + { + style: { + whiteSpace: "nowrap", + "margin-bottom": "0px", + "margin-top": "0px", + "vertical-align": "middle", + }, + props: { + id: id, + } + }, + [ + span( + { + style: { + "vertical-align": "top", + fontSize: "1em", + fontWeight: "bold", + textTransform: "none", + paddingLeft: "1.5em", + }, + }, + text + ), + i(".material-icons", {style: {width: "1.5em"}}, sortIcon) + ] + ) + ) + } + const header = tr([ - th("Use?"), - th(safeModelToUi("id", state.settings.common.modelTranslations)), - th("Name"), - th("Sample"), - th("Cell"), - th("Dose"), - // th("Batch"), - // th("Year"), - th("Time"), - th("Sign. Genes"), + sortableHeaderEntry("use", "Use?", state, false), + sortableHeaderEntry("trt_id", safeModelToUi("id", state.settings.common.modelTranslations), state), + sortableHeaderEntry("trt_name", "Name", state), + sortableHeaderEntry("id", "Sample", state), + sortableHeaderEntry("cell", "Cell", state), + sortableHeaderEntry("dose", "Dose", state), + sortableHeaderEntry("time", "Time", state), + sortableHeaderEntry("significantGenes", "Sign. Genes", state), ]) let body = [] @@ -281,6 +361,20 @@ function SampleSelection(sources) { const a$ = xs.merge(aDown$, aUp$).compose(dropRepeats(equals)).startWith(false) + const sortClick$ = sources.DOM.select(".sortable") + .events("click") + .map((ev) => ev.ownerTarget.id) + .startWith("") + + const sortHover$ = sources.DOM.select(".sortable") + .events("mouseenter") + .map((ev) => ev.ownerTarget.id) + .startWith("") + + const sortLeave$ = sources.DOM.select(".sortable") + .events("mouseleave") + .mapTo("") + const selectReducer$ = useClick$ .compose(sampleCombine(a$)) .map(([id, a]) => (prevState) => { @@ -367,6 +461,25 @@ function SampleSelection(sources) { core: { ...prevState.core, request: req }, })) + const sortReducer$ = sortClick$.map((sort) => (prevState) => ({ + ...prevState, + core: { + ...prevState.core, + sort: sort, + direction: (sort != prevState.core.sort ? false : !prevState.core.direction) + } + })) + + const hoverReducer$ = xs.merge(sortHover$, sortLeave$) + .map((hover) => (prevState) => ({ + ...prevState, + core: { + ...prevState.core, + sortHover: hover, + } + })) + + const sampleSelection$ = xs .merge( sources.DOM.select(".doSelect").events("click"), @@ -398,6 +511,8 @@ function SampleSelection(sources) { selectReducer$, autoSelectReducer$, busyReducer$, + sortReducer$, + hoverReducer$, dirtyReducer$, ), output: sampleSelection$, diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index 7cf1b412..c66c488b 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -238,11 +238,10 @@ function TreatmentCheck(sources) { })) // Feed the autocomplete driver - const ac$ = data$ - .filter((data) => data.length > 1) - .map((data) => ({ + const ac$ = data$.compose(sampleCombine(input$)) + .map(([data, input]) => ({ el: ".treatmentQuery", - data: data, + data: (data.length == 1 && data[0].trtId == input) ? [] : data, render: function (data) { return mergeAll( data.map((d) => ({ [d.trtId + " - " + d.trtName + " (" + d.count + ")"]: null })) From 74c8d4af1907844ac1b54dffeb30b51379c3ea48 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 7 Mar 2022 13:48:53 +0100 Subject: [PATCH 167/191] Gskcmp 89 ghost mode leaves page in dirty state (#119) * Let ghost mode use router params functionality Change treatments scenario as proof of concept: - all selecting is done by setting the router params - set continue functions - remove old functionality * Update disease WF ghost mode Still a bit WIP. Setting of filters etc needs some attention, currently doesn't use the router params system * Basic update for ghost mode in correlation WF * Don't allow search queries when ghost mode is enabled Don't apply the state to the routerInformation.params Still clear the search string from the browser url * Move ghost mode strings & settings to deployments too Change scenario logic for disease and correlation WFs to same style as genericTreatment as it is now very similar --- deployments.json | 43 +++++++---- src/js/components/SignatureCheck.js | 27 +++++-- src/js/index.js | 22 +++++- src/js/pages/correlation.js | 23 +++--- src/js/pages/disease.js | 11 ++- src/js/scenarios/correlationScenario.js | 78 ++++++++++++++++++++ src/js/scenarios/diseaseScenario.js | 95 ++++++------------------- src/js/scenarios/treatmentScenario.js | 61 ++++++++-------- 8 files changed, 216 insertions(+), 144 deletions(-) create mode 100644 src/js/scenarios/correlationScenario.js diff --git a/deployments.json b/deployments.json index 225d840c..d8427629 100644 --- a/deployments.json +++ b/deployments.json @@ -22,22 +22,41 @@ }, "ghost": { "compound": { - "treatment": "BRD-K93645900", - "index": 0, - "sample": ["A375_6H_BRD_K93645900_10"], - "signature": "-CRCP" + "params": { + "typer": 500, + "treatment": "BRD-K93645900", + "samples": "A375_6H_BRD_K93645900_10", + "trtType": "trt_cp" + } }, "ligand": { - "treatment": "ANGPT1", - "index": 0, - "sample": ["HCC515_4H_CMAP_CYT_SRP3007_50"], - "signature": "FBXO11 SLC35 F5BAG4 RIF1" + "params": { + "typer": 500, + "treatment": "ANGPT1", + "samples": "HCC515_4H_CMAP_CYT_SRP3007_50", + "trtType": "trt_cp" + } }, "genetic": { - "treatment": "PARP2", - "index": 0, - "sample": ["DER001_HCC515_96H_X1_F1B6_DUO52HI53LO:B03"], - "signature": "-COPS7A -KEAP1 -CCNB1 -HMG20B -ELOVL6 -DERA LPL -PLEKHJ1 -TPI1 ST3GAL5 -HAT1 -MLF2 -FOXG1 -STRAP -MRPL4 -EDN1 -PON2 KMT2A FRMD4A -C16orf58" + "params": { + "typer": 500, + "treatment": "PARP2", + "samples": "DER001_HCC515_96H_X1_F1B6_DUO52HI53LO", + "trtType": "trt_cp" + } + }, + "disease": { + "params": { + "typer": 100, + "signature": "HSPA1A DNAJB1 DDIT4 -TSEN2" + } + }, + "correlation": { + "params": { + "typer": 100, + "signature1": "HSPA1A DNAJB1 DDIT4 -TSEN2", + "signature2": "DNAJB1 DDIT4 -TSEN2" + } } }, "modelTranslations": [ diff --git a/src/js/components/SignatureCheck.js b/src/js/components/SignatureCheck.js index fc7be84a..adfbeb2d 100644 --- a/src/js/components/SignatureCheck.js +++ b/src/js/components/SignatureCheck.js @@ -25,7 +25,8 @@ const stateTemplate = { const checkLens = { get: state => ({ - query: state.core.query, + query: state.core.query, + ghostUpdate: state.core.ghostUpdate, settings: state.settings, search: state.search, searchAutoRun: state.searchAutoRun, @@ -36,7 +37,8 @@ const checkLens = { const checkLens1 = { get: state => ({ - query: state.core.query1, + query: state.core.query1, + ghostUpdate: state.core.ghostUpdate1, settings: state.settings, search: state.search1, searchAutoRun: state.searchAutoRun, @@ -48,6 +50,7 @@ const checkLens1 = { const checkLens2 = { get: state => ({ query: state.core.query2, + ghostUpdate: state.core.ghostUpdate2, settings: state.settings, search: state.search2, searchAutoRun: state.searchAutoRun, @@ -161,14 +164,30 @@ function SignatureCheck(sources) { .mapTo(true) .compose(dropRepeats(equals)) - const collapseUpdateReducer$ = xs.merge(collapseUpdate$,searchAutoRun$).compose(sampleCombine(data$)) + const ghostUpdate$ = sources.onion.state$ + .map((state) => state.ghostUpdate) + .filter((ghost) => ghost) + .compose(dropRepeats()) + + const collapseUpdateReducer$ = xs + .merge( + collapseUpdate$, + searchAutoRun$, + ghostUpdate$, + ) + .compose(sampleCombine(data$)) .map(([collapse, data]) => prevState => { return ({...prevState, query : data.map(x => (x.found ?? x.inL1000) ? x.symbol : '').join(" ").replace(/\s\s+/g, ' ').trim()}); }); // The result of this component is an event when valid // XXX: stays true the whole cycle, so maybe tackle this as well!!!! - const validated$ = xs.merge(collapseUpdate$,searchAutoRun$).map(update => true) + const validated$ = xs + .merge( + collapseUpdate$, + searchAutoRun$, + ghostUpdate$, + ).mapTo(true) return { log: xs.merge( diff --git a/src/js/index.js b/src/js/index.js index 9b5332b9..2162a2e5 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -54,6 +54,7 @@ import { correlationSVG, settingsSVG, } from "./svg" +import sampleCombine from "xstream/extra/sampleCombine" export default function Index(sources) { const { router } = sources @@ -408,7 +409,12 @@ export default function Index(sources) { } ) - const routerReducer$ = router.history$.map((router) => (prevState) => { + const ghostModeEnabled$ = state$ + .map((state) => state.settings.common.ghostMode) + + const routerReducer$ = router.history$ + .compose(sampleCombine(ghostModeEnabled$)) + .map(([router, ghostMode]) => (prevState) => { function paramsToObject(entries) { const result = {} @@ -421,8 +427,9 @@ export default function Index(sources) { // we should not apply the same search query values when the page was reloaded // if user refreshes or clicks same WF link, router.type is not set // if we push new state, router.type == "push" + // Also, don't allow search queries when ghost mode is enabled let paramsObject = {} - if (router?.type == "push") { + if (router?.type == "push" && ghostMode != true) { const params = new URLSearchParams(router.state) paramsObject = paramsToObject(params) } @@ -437,6 +444,12 @@ export default function Index(sources) { } }) + const ghostModeWarning$ = router.history$ + .compose(sampleCombine(ghostModeEnabled$)) + .filter(([router, ghostMode]) => (router?.type == "push")) + .filter(([router, ghostMode]) => (ghostMode == true)) + .mapTo({ text: 'Search query ignored as Ghost Mode is enabled', duration: 10000 }) + const pageStateReducer$ = state$.map((state) => state.routerInformation.pageState).compose(dropRepeats(equals)) .map((state) => (prevState) => { // Don't output fields with undefined values @@ -506,7 +519,10 @@ export default function Index(sources) { prevent$, page$.map(prop("preventDefault")).filter(Boolean).flatten() ), - popup: page$.map(prop("popup")).filter(Boolean).flatten(), + popup: xs.merge( + ghostModeWarning$, + page$.map(prop("popup")).filter(Boolean).flatten(), + ), modal: page$.map(prop("modal")).filter(Boolean).flatten(), ac: page$.map(prop("ac")).filter(Boolean).flatten(), sidenav: sidenavEvent$, diff --git a/src/js/pages/correlation.js b/src/js/pages/correlation.js index ebc3f3f5..984cde5b 100644 --- a/src/js/pages/correlation.js +++ b/src/js/pages/correlation.js @@ -16,7 +16,7 @@ import { SampleTable, sampleTableLens } from '../components/SampleTable/SampleTa import { Exporter } from "../components/Exporter" // Support for ghost mode -import { scenario } from '../scenarios/diseaseScenario' +import { scenario } from '../scenarios/correlationScenario' import { runScenario } from '../utils/scenario' function CorrelationWorkflow(sources) { @@ -26,18 +26,15 @@ function CorrelationWorkflow(sources) { const state$ = sources.onion.state$ // Scenario for ghost mode - const scenarioReducer$ = - sources.onion.state$.take(1) - .filter(state => state.settings.common.ghostMode) - .mapTo(runScenario(scenario).scenarioReducer$) - .flatten() - .startWith(prevState => prevState) - const scenarioPopup$ = - sources.onion.state$.take(1) - .filter(state => state.settings.common.ghostMode) - .mapTo(runScenario(scenario).scenarioPopup$) - .flatten() - .startWith({ text: 'Welcome to Correlation Workflow', duration: 4000 }) + const scenarios$ = sources.onion.state$ + .take(1) + .filter((state) => state.settings.common.ghostMode) + .map(state => runScenario(scenario(state.settings.common.ghost.correlation), state$)) + const scenarioReducer$ = scenarios$.map(s => s.scenarioReducer$) + .flatten() + const scenarioPopup$ = scenarios$.map(s => s.scenarioPopup$) + .flatten() + .startWith({ text: "Welcome to Correlation Workflow", duration: 4000 }) const correlationForm = isolate(CorrelationForm, { onion: formLens })(sources) const queries$ = correlationForm.output diff --git a/src/js/pages/disease.js b/src/js/pages/disease.js index ec31fd41..44baeeac 100644 --- a/src/js/pages/disease.js +++ b/src/js/pages/disease.js @@ -31,16 +31,13 @@ function DiseaseWorkflow(sources) { const state$ = sources.onion.state$ // Scenario for ghost mode - const scenarioReducer$ = sources.onion.state$ + const scenarios$ = sources.onion.state$ .take(1) .filter((state) => state.settings.common.ghostMode) - .mapTo(runScenario(scenario).scenarioReducer$) + .map(state => runScenario(scenario(state.settings.common.ghost.disease), state$)) + const scenarioReducer$ = scenarios$.map(s => s.scenarioReducer$) .flatten() - .startWith((prevState) => prevState) - const scenarioPopup$ = sources.onion.state$ - .take(1) - .filter((state) => state.settings.common.ghostMode) - .mapTo(runScenario(scenario).scenarioPopup$) + const scenarioPopup$ = scenarios$.map(s => s.scenarioPopup$) .flatten() .startWith({ text: "Welcome to Disease Workflow", duration: 4000 }) diff --git a/src/js/scenarios/correlationScenario.js b/src/js/scenarios/correlationScenario.js new file mode 100644 index 00000000..f98fc2fa --- /dev/null +++ b/src/js/scenarios/correlationScenario.js @@ -0,0 +1,78 @@ + +export const scenario = config => [ + { //Form + delay: 500, + state: {}, + message: { + text: 'Please enter the gene symbols, or ENSMBL or ENTREZ ID or Probe set ID, from your gene signature', + duration: 8000 + } + }, + { // Use typer to write full query string + delay: 200, + state: { + routerInformation: { + params: config.params, + } + } + }, + { + delay: 700, + state: {}, + message: { + text: 'Downregulated genes are marked with "-"', + duration: 4000 + } + }, + { + delay: 4000, + state: {}, + message: { + text: 'The table provides feedback for the gene symbols in the signature', + duration: 4000 + } + }, + + { // Validate + delay: 2000, + continue: (s) => (s.form.query1 == config.params.signature1), + state: {}, + message: { + text: 'When ready, press the update/validate button, removing faulty or non-measured L1000 genes', + duration: 6000 + } + }, + { + delay: 1000, + state: { form: { ghostUpdate1: true } } + }, + { + delay: 1000, + continue: (s) => (s.form.query2 == config.params.signature2), + state: { form: { ghostUpdate2: true } } + }, + + // Start analysis + { + delay: 4000, + state: { + form: { ghost: true }, + }, + message: { + text: 'When ready, press play_arrow', + duration: 6000 + } + }, + + // Wait a bit for everything to load... + + { // output text + delay: 6000, + state: {}, + message: { + text: 'The correlation is vizualised for the entire database', + duration: 4000 + } + }, + +] diff --git a/src/js/scenarios/diseaseScenario.js b/src/js/scenarios/diseaseScenario.js index 1dd61243..abb13b8f 100644 --- a/src/js/scenarios/diseaseScenario.js +++ b/src/js/scenarios/diseaseScenario.js @@ -16,103 +16,52 @@ const filterCellDose = { trtType: ['trt_cp', 'trt_lig'] } -export const scenario = [ +export const scenario = config => [ { //Form delay: 500, - state: { form: { query: "-E", validated: false } }, + state: {}, message: { text: 'Please enter the gene symbols, or ENSMBL or ENTREZ ID or Probe set ID, from your gene signature', duration: 8000 } }, - { + { // Use typer to write full query string delay: 200, - state: { form: { query: "-EN", validated: false } }, - }, - { - delay: 100, - state: { form: { query: "-ENS", validated: false } }, + state: { + routerInformation: { + params: config.params, + } + } }, { - delay: 600, - state: { form: { query: "-ENSG00000013583", validated: false } }, + delay: 700, + state: {}, message: { text: 'Downregulated genes are marked with "-"', duration: 4000 } }, { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860", validated: false } }, + delay: 4000, + state: {}, message: { text: 'The table provides feedback for the gene symbols in the signature', duration: 4000 } }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6 PMAIP1", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6 PMAIP1 DDIT4", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6 PMAIP1 DDIT4 -RAB31", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS", validated: false } }, - }, { // Validate delay: 2000, - state: { form: { query: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS', validated: true } }, + continue: (s) => (s.form.query == config.params.signature), + state: {}, message: { text: 'When ready, press the update/validate button, removing faulty or non-measured L1000 genes', duration: 6000 } }, { - delay: 100, - state: { form: { validated: true } }, + delay: 1000, + state: { form: { ghostUpdate: true } } }, // Initiate Ghost, prepare tables to fit on the screen @@ -120,12 +69,12 @@ export const scenario = [ delay: 4000, state: { form: { ghost: true }, - filter: { - input: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS', - output: filterValues, - filter_output: filterValues, - state: {dose: false, cell: false, trtType: false}, - }, + // filter: { + // // input: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS', + // output: filterValues, + // filter_output: filterValues, + // state: {dose: false, cell: false, trtType: false}, + // }, }, message: { text: 'When ready, press play_arrow', diff --git a/src/js/scenarios/treatmentScenario.js b/src/js/scenarios/treatmentScenario.js index c85cf7c5..aa14eeed 100644 --- a/src/js/scenarios/treatmentScenario.js +++ b/src/js/scenarios/treatmentScenario.js @@ -41,21 +41,34 @@ export const scenario = config => state: { form: { check: { ghostinput: true } } }, }, ], - typer( - config.treatment, - { // Form input - state: { form: { check: { input: config.treatment, showSuggestions: true, validated: false } } }, + [ + { + delay: 500, + state: { + routerInformation: { + params: config.params, + } + } } - ), + ], [ - { // Form input - delay: 1500, - state: { form: { check: { input: config.treatment, showSuggestions: false, validated: true } } }, + { // Sample Selection + delay: 100, + state: {}, + message: { + text: 'Enter a treatment for which to search', + duration: 4000 + } }, { // GO - delay: 1000, - state: { form: { check: { ghostoutput: true } } }, + delay: 1000, + continue: (s) => (s.form.check.validated), + state: { form: { check: { ghostoutput: true } } }, + // message: { + // text: 'Press the \'GO\' button', + // duration: 4000 + // } }, { // Sample Selection @@ -69,31 +82,24 @@ export const scenario = config => }, { delay: 3000, - state: { form: { sampleSelection: { data: { index: config.index, value: { use: true } } } } }, + state: {}, message: { text: 'select or deselect the desired sample(s)', duration: 4000 } }, - // { - // delay: 1000, - // state: { form: { sampleSelection: { data: { index: 2, value: { use: false } } } } }, - // }, - // { - // delay: 1000, - // state: { form: { sampleSelection: { data: { index: 3, value: { use: false } } } } }, - // }, { // Signature creation delay: 500, - state: { form: { sampleSelection: { ghostoutput: true, output: config.sample } } }, + state: { form: { sampleSelection: { ghostoutput: true } } }, message: { text: 'Press Select', duration: 4000 } }, { - delay: 4000, + delay: 100, + continue: (s) => (notEmpty(s.form.signature.output)), state: {}, message: { text: 'A signed ranked gene signature is generated across the samples', @@ -112,18 +118,9 @@ export const scenario = config => { // Filter delay: 500, continue: (s) => (notEmpty(s.form.signature.output)), - state: { - filter: { - input: config.signature, - output: { trtType: [ "trt_cp" ] }, - filter_output: { trtType: [ "trt_cp" ] }, - ghost: { expand: false } - }, - headTable: { input: { query: config.signature } }, - tailTable: { input: { query: config.signature } }, - }, + state: {}, message: { - text: 'Set filter a filter', + text: 'Set a filter', duration: 7000 } }, From 350552cb88d6bfd12f6810c70515f5620a56b10d Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Mon, 7 Mar 2022 19:36:37 +0100 Subject: [PATCH 168/191] Update CHANGELOG.md --- CHANGELOG.md | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index beb09794..86128455 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,40 @@ # CHANGELOG -## Version 5.0.0-alpha4 +## Version 5.1.0 -## Functionality +### Functionality + +- URL Queries: ComPass accepts URL queries and runs the complete analysis automatically. For instance, the following URI: + + ``` + http://localhost:3000/disease?autorun&signature=HSPA1A+DNAJB1+DDIT4+-TSEN2&numTableHead=10 + ``` + + will run the Disease workflow using the signature `HSPA1A DNAJB1 DDIT4` and will show 10 entries for the top table. + +- Export: Add buttons to export data and plots to a file or copy it to the clipboard. A popup window is available with all the possible export features on the right bottom of the screen. + +- Correlation workflow has filters. + +- Sample tables can be sorted by clicking the columns header + +### Other + +- Ghost mode has been improved, it no longer requires explicit timings in the scenarios + +## Version 5.0.1 + +__TODO__ + +## Version 5.0.0 + +### Functionality - A new 'Genetic' workflow is created useful for searching for genetic perturbations. - Inverting the filter selection can be done using the `ALT` (Option on Mac) modifier key instead of the `a` key - Filter values are populated dynamically based on the data available through the API -## Other +### Other - The dependency stack has been cleaned and (partly) updated - Cycle dependencies have been updated to the latest versions except for `@cycle/state`. From 1f8897a1b275edbd78d1deaa3f8dc731fa33731d Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 8 Mar 2022 13:08:54 +0100 Subject: [PATCH 169/191] Add documentation for v5.0.1 --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86128455..be75fbfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,8 @@ ## Version 5.0.1 -__TODO__ +- The head and tail tables use pictures to display the treatment type +- Adapt the behaviour for the autocomplete when there is only one option to choose from and if the entered value matches the single autocomplete value ## Version 5.0.0 From 0b5ee0f9195325b4e7a0597f3e934570b7b148a3 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 10 Mar 2022 12:08:22 +0100 Subject: [PATCH 170/191] Delay loading vdom by 100ms (#128) State can glitch, especially when using the search queries. This causes loading vdom to be output and directly being overwritten by init vdom --- src/js/components/Table.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/js/components/Table.js b/src/js/components/Table.js index 39161eca..003c38c0 100644 --- a/src/js/components/Table.js +++ b/src/js/components/Table.js @@ -627,6 +627,7 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { ]) ) .remember() + .compose(delay(100)) /** * Full vdom content once request is received From 593a65fe65b477dbebe63e8ced40127aed6c50b0 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 10 Mar 2022 15:58:32 +0100 Subject: [PATCH 171/191] Remove debug statements from clipboard driver and scenario logic --- src/js/drivers/makeClipboardDriver.js | 1 - src/js/utils/scenario.js | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/js/drivers/makeClipboardDriver.js b/src/js/drivers/makeClipboardDriver.js index de517f4c..85f81327 100644 --- a/src/js/drivers/makeClipboardDriver.js +++ b/src/js/drivers/makeClipboardDriver.js @@ -47,7 +47,6 @@ function makeClipboardDriver(provideOwnSink= false) { })() : xs.fromPromise(navigator.clipboard.writeText(message.data)) const clipboardMapped$ = clipboard$ - .debug("clipboard$") .map((res) => res == undefined ? { state: "success", sender: message.sender } diff --git a/src/js/utils/scenario.js b/src/js/utils/scenario.js index 7aa9b47a..89b3b492 100644 --- a/src/js/utils/scenario.js +++ b/src/js/utils/scenario.js @@ -30,15 +30,12 @@ export const runScenario = (scenario, state$) => { .compose(debounce(equals)) .filter(a => a == true) .compose(dropRepeats(equals)) - .debug("trigger$") const triggerDelayed$ = trigger$ .compose(delay(10)) - .debug("triggerDelayed$") const triggerOutput$ = trigger$ .mapTo(step) .endWhen(triggerDelayed$) - .debug("triggerOutput$") return triggerOutput$.compose(delay(step.delay)) } From 743fe667fa1d9b9493ee1aa6a2dc48af4e3f497b Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 11 Mar 2022 08:09:28 +0100 Subject: [PATCH 172/191] Add TRT_OE as possible treatment to be displayed in the top tables (#129) Copied from trt_sh --- src/js/components/SampleTable/SampleInfo.js | 42 +++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/js/components/SampleTable/SampleInfo.js b/src/js/components/SampleTable/SampleInfo.js index a85759af..eb5e54ee 100644 --- a/src/js/components/SampleTable/SampleInfo.js +++ b/src/js/components/SampleTable/SampleInfo.js @@ -232,6 +232,31 @@ export function SampleInfo(sources) { div(".col .s8", { style: blur }, [sample.trt_name]) ]) ]), + trt_oe: div(".row", { style: { fontWeight: "small" } }, [ + div(".valign-wrapper", [ + div(".col .s2 .l1 .left-align", { style: { fontWeight: "bold" } }, [ + zhangRounded, + ]), + + div(".col .l2 .hide-on-med-and-down .truncate", [sample.id]), + div(".col .l1 .hide-on-med-and-down", [sample.cell]), + div(".col .l2 .hide-on-med-and-down .truncate", { style: blur }, [ sample.trt_id != "NA" ? sample.trt_id : "" ]), + div(".col .l3 .hide-on-med-and-down", { style: blur }, [sample.trt_name]), + + div(".col .s2 .offset-s5 .l1", { style: blur }, imgForTrtPart), + div(".col .s3 .l2 .center-align", { style: blur }, visualizeTextPart), + ]), + div(".hide-on-large-only", {style: {paddingTop: "10px"}}, [ + div(".col .s4 .m3 .offset-m1", {style: {whiteSpace: "nowrap"}}, ["Sample ID"]), + div(".col .s8 .truncate", [sample.id]), + div(".col .s4 .m3 .offset-m1", ["Cell"]), + div(".col .s8", [sample.cell]), + div(".col .s4 .m3 .offset-m1", {style: {whiteSpace: "nowrap"}}, ["Treatment ID"]), + div(".col .s8 .truncate", { style: blur }, [ sample.trt_id != "NA" ? sample.trt_id : "" ]), + div(".col .s4 .m3 .offset-m1", {style: {whiteSpace: "nowrap"}}, ["Treatment Name"]), + div(".col .s8", { style: blur }, [sample.trt_name]) + ]) + ]), trt_lig: div(".row", { style: { fontWeight: "small" } }, [ div(".valign-wrapper", [ div(".col .s2 .l1 .left-align", { style: { fontWeight: "bold" } }, [ @@ -400,6 +425,23 @@ export function SampleInfo(sources) { ) ), ]), + trt_oe: div([ + div(".row", [ + div(".col .s12 .m6 .l4", { style: { margin: "15px 0px 0px 0px" } }, samplePart), + div(".col .s12 .m6 .l4", { style: { margin: "15px 0px 0px 0px" } }, treatmentPart), + div(".col .s12 .m12 .l2 .push-l2 .hide-on-med-and-down .center-align", + { style: merge(blur, { height: "100%", "margin-top": "30px"}) }, + visualizeTextPart + ), + ]), + div( + ".row", + { style: { margin: "15px 0px 0px 0px" } }, + [p(".col .s12.filterHeader", hStyle, "Filter Info:")].concat( + _filters.map((x) => p(pStyle, entrySmall(x.key, x.value))) + ) + ), + ]), trt_lig: div([ div(".row", [ div(".col .s12 .m6 .l4", { style: { margin: "15px 0px 0px 0px" } }, samplePart), From 46ec2eff891a972d8b59e7682c2fcdadd3013bd9 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 11 Mar 2022 08:23:51 +0100 Subject: [PATCH 173/191] Add placeholder for this version in the changelog file --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index be75fbfe..67959399 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## Version 5.2.0 + +### Functionality + +### Other + ## Version 5.1.0 ### Functionality From 92127764183c1c38c32c37ce37323316205b9b5a Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 14 Mar 2022 15:04:52 +0100 Subject: [PATCH 174/191] Fix mistake in sample selection column truncation code (#133) --- CHANGELOG.md | 4 ++++ package.json | 2 +- src/js/components/SampleSelection.js | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index beb09794..134d5ef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## Version 5.0.2 + +- Fix mistake in sample selection column truncation code + ## Version 5.0.0-alpha4 ## Functionality diff --git a/package.json b/package.json index 6659160e..b4e0740e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "LuciusWeb", - "version": "5.0.1", + "version": "5.0.2", "description": "Web interface for ComPass aka Lucius", "repository": { "type": "git", diff --git a/src/js/components/SampleSelection.js b/src/js/components/SampleSelection.js index e9b0288a..56db3462 100644 --- a/src/js/components/SampleSelection.js +++ b/src/js/components/SampleSelection.js @@ -175,7 +175,7 @@ function SampleSelection(sources) { const maxLength = 7 if (dose.length <= maxLength) return dose - else if (isNaN(entry.dose) || entry.dose_unit >= 3) + else if (isNaN(entry.dose) || entry.dose_unit?.length >= 3) return dose.substring(0, maxLength-1) + "..." // adding '...' is quite small on screen (in non-monospaced fonts), so we're ignoring that else From e8df3fd9dc061c6a7c9f2fc55bc79eaba24d77a3 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 15 Mar 2022 08:23:17 +0100 Subject: [PATCH 175/191] Allow filter values API call coming in later than the input trigger (#134) In disease & correlation WFs the input trigger comes instantaneously but the API for filter values is still under its way. This means that a regular samplecombine doesn't output the values first combine input trigger and filter values and then samplecombine the search values --- src/js/components/Filter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/components/Filter.js b/src/js/components/Filter.js index 5f17c35a..6f936fe2 100644 --- a/src/js/components/Filter.js +++ b/src/js/components/Filter.js @@ -374,8 +374,8 @@ export function model( * @const model/searchReducer$ * @type {Reducer} */ - const searchReducer$ = input$.compose(sampleCombine(xs.combine(possibleValues$, search$))) - .map(([_, [possibleValues, search]]) => { + const searchReducer$ = xs.combine(input$, possibleValues$).compose(sampleCombine(search$)) + .map(([[_, possibleValues], search]) => { const matchedFilters = (searchValue, possibleValues) => { const values = searchValue.split(',') From dd1ef05cb4e6124ec92602d9cf78eb87497a0fea Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 15 Mar 2022 08:27:34 +0100 Subject: [PATCH 176/191] Gskcmp 99 improve input validation (#131) * Improve sturdiness against invalid input data Very selective changes to prevent undefined data output caused by errors and instead pass empty data These were highlighted by testing with invalid input strings * Add changelog entry --- CHANGELOG.md | 2 ++ src/js/components/BinnedPlots/BinnedPlots.js | 2 +- src/js/components/BinnedPlots/CorrelationPlot.js | 2 +- src/js/components/Table.js | 4 +++- src/js/components/TreatmentCheck.js | 5 ++++- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67959399..cdb9cdd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ### Other +- Improve sturdiness against invalid input + ## Version 5.1.0 ### Functionality diff --git a/src/js/components/BinnedPlots/BinnedPlots.js b/src/js/components/BinnedPlots/BinnedPlots.js index 5ad8b358..b918a0e8 100644 --- a/src/js/components/BinnedPlots/BinnedPlots.js +++ b/src/js/components/BinnedPlots/BinnedPlots.js @@ -41,7 +41,7 @@ const isEmptyState = (state) => { // For this component, when is a API result empty? const isEmptyData = (data) => { - return data.length == 0 + return data == undefined || data.length == 0 } const makeVega = (elementID) => { diff --git a/src/js/components/BinnedPlots/CorrelationPlot.js b/src/js/components/BinnedPlots/CorrelationPlot.js index e93a5596..00b032ec 100644 --- a/src/js/components/BinnedPlots/CorrelationPlot.js +++ b/src/js/components/BinnedPlots/CorrelationPlot.js @@ -34,7 +34,7 @@ const isEmptyState = (state) => { // For this component, when is a API result empty? const isEmptyData = (data) => { - return data.length == 0 + return data == undefined || data.length == 0 } const makeVega = (elementID) => { diff --git a/src/js/components/Table.js b/src/js/components/Table.js index 003c38c0..733f6dd4 100644 --- a/src/js/components/Table.js +++ b/src/js/components/Table.js @@ -462,7 +462,9 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { * @const makeTable/Table/data$ * @type {Stream} */ - const data$ = validResponse$.map((result) => result.body.result.data) + const data$ = validResponse$ + .map((result) => result.body.result.data) + .map((data) => data ?? []) /** * Stream of table data converted into TSV format diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index c66c488b..191bc591 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -129,7 +129,10 @@ function TreatmentCheck(sources) { .map((response$) => response$.replaceError(() => xs.of([]))) .flatten() - const data$ = response$.map((res) => res.body.result.data).remember() + const data$ = response$ + .map((res) => res.body.result.data) + .map((data) => data ?? []) + .remember() const initVdom$ = emptyState$.mapTo(div()) From bd9564c07d4739619fa8fecfc3cfe255b0429f8f Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 16 Mar 2022 07:49:28 +0100 Subject: [PATCH 177/191] Catch error thrown in permission query under Firefox (#136) Firefox doesn't implement 'clipboard-write' but also doesn't allow querying it Catch the exception that gets thrown. --- src/js/drivers/makeClipboardDriver.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/js/drivers/makeClipboardDriver.js b/src/js/drivers/makeClipboardDriver.js index 85f81327..eb3273b1 100644 --- a/src/js/drivers/makeClipboardDriver.js +++ b/src/js/drivers/makeClipboardDriver.js @@ -17,8 +17,12 @@ function makeClipboardDriver(provideOwnSink= false) { // Firefox by default doesn't allow the clipboard write and doesn't implement the permissions API call // so we're kind of forced to assume that if the API call fails that we won't have the necessary permissions const queryOpts = { name: "clipboard-write", allowWithoutGesture: false } - const copyImagesPermission$ = xs - .fromPromise(navigator.permissions.query(queryOpts)) + const copyImagesPermission$ = xs + .fromPromise( + navigator.permissions.query(queryOpts) + .then((_) => { return _ }) + .catch((_) => { return { state: "error" } }) + ) .replaceError((_) => xs.of({ state: "error" })) function clipboardDriver(stream$) { From 62871f6224321e52e66166eaf74419eb10558709 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 17 Mar 2022 08:23:23 +0100 Subject: [PATCH 178/191] Gskcmp 98 dynamic filter keys (#130) * Make filter vdom generation dynamic Use data from settings to call function that generates single group vdom tree Use modelTranslations to go from base data value to value to be displayed to the user Sort fields by translated name * Move collapsableFilter to own code block This is now in line with other sub-blocks Update documentation of params in togglebleFilter * make filter groups DOM click listeners dynamic * make filter value actions dom listeners dynamic too * Use ramda keys instead of Object.keys * adapt logic to be dynamic to filter names and amount of them * Make lenses dynamic Fix searchReducer$ mergeAll call * Remove default values from deployments.json and default reducer These had to be statically defined and didn't serve a function anymore * Fix unit test to reflect new behavior of the default reducer Check for empty class instead of 3 fixed keys * Add entry in changelog * Add documentation for deployments in the changelog Add a section `Deployment changes` Add comment: Requires extra translations for each filter type in `deployments.json` --- CHANGELOG.md | 5 + deployments.json | 18 +- src/js/components/Filter.js | 336 ++++++++++++++++-------------------- src/test/FilterTest.js | 2 +- 4 files changed, 171 insertions(+), 190 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdb9cdd8..8d97f8fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,13 @@ ### Other +- Filter functionality made dynamic so that new filter groups can be added as needed - Improve sturdiness against invalid input +### Deployment changes + +- Requires extra translations for each filter type in `deployments.json` + ## Version 5.1.0 ### Functionality diff --git a/deployments.json b/deployments.json index d8427629..0a8d53a2 100644 --- a/deployments.json +++ b/deployments.json @@ -7,9 +7,6 @@ "services": { "filter": { "values": { - "dose": ["sample_entry"], - "cell": ["sample_entry"], - "trtType": ["sample_entry"] } }, "common": { @@ -69,6 +66,21 @@ "ui": "Secondary ID", "model": "jnjb", "comment": "" + }, + { + "ui": "Cell", + "model": "cell", + "comment": "Filter group name" + }, + { + "ui": "Dose", + "model": "dose", + "comment": "Filter group name" + }, + { + "ui": "Type", + "model": "trtType", + "comment": "Filter group name" } ] }, diff --git a/src/js/components/Filter.js b/src/js/components/Filter.js index 6f936fe2..916fd393 100644 --- a/src/js/components/Filter.js +++ b/src/js/components/Filter.js @@ -11,9 +11,20 @@ import { assocPath, equals, mergeAll, + lensProp, + view as viewR, + ascend, + sortWith, + none, + any, + values, + identity, + replace, } from "ramda" import { FetchFilters } from "./FetchFilters" import debounce from 'xstream/extra/debounce' +import flattenConcurrently from 'xstream/extra/flattenConcurrently' +import { safeModelToUi } from '../modelTranslations' /** * @module components/Filter @@ -27,32 +38,53 @@ import debounce from 'xstream/extra/debounce' * @type {Lens} */ export const filterLens = { - get: (state) => ({ - core: state.filter, - settings: { filter: state.settings.filter, api: state.settings.api }, - search: { - dose: state.routerInformation.params?.dose, - cell: state.routerInformation.params?.cell, - trtType: state.routerInformation.params?.trtType, + get: (state) => { + + // Get keys starting with 'filter_' but that is not only 'filter_' + const keys_ = keys(state.routerInformation.params) + .filter((key) => key.startsWith("filter_") && key != "filter_") + const searchArr = keys_.map((key) => { + const newKey = replace(/^filter_/, "", key) + return { + [newKey]: state.routerInformation.params[key] + } + }) + const searchObj = mergeAll(searchArr) + + return { + core: state.filter, + settings: { filter: state.settings.filter, api: state.settings.api, modelTranslations: state.settings.common.modelTranslations}, + search: searchObj, } - }), - set: (state, childState) => ({ - ...state, - filter: childState.core, - settings: { - ...state.settings, - filter: childState.settings.filter, - }, - routerInformation: { - ...state.routerInformation, - pageState: { - ...state.routerInformation.pageState, - dose: childState.core.filter_output?.dose?.join(), - cell: childState.core.filter_output?.cell?.join(), - trtType: childState.core.filter_output?.trtType?.join(), + }, + set: (state, childState) => { + + const filter_outputs = childState.core.filter_output + const filterStates = keys(filter_outputs) + .map((key) => ({ ["filter_" + key] : filter_outputs[key]?.join() })) + const mergedFilterState = mergeAll(filterStates) + // mergedFilterState is an object with e.g. + // { + // filter_dose: "123", + // filter_cell: "abc,def", + // } + + return { + ...state, + filter: childState.core, + settings: { + ...state.settings, + filter: childState.settings.filter, + }, + routerInformation: { + ...state.routerInformation, + pageState: { + ...state.routerInformation.pageState, + ...mergedFilterState, + } } } - }), + }, } /** @@ -81,56 +113,27 @@ const isEmptyState = (state) => { * Filter intent, convert events on the dom to actions * @function intent * @param {Stream} domSource$ events from the dom + * @param {Stream} filterNames$ array of top-level filter names * @returns {Stream} object with: * - filterValuesAction$ stream of object where key is filter group (top level) and value is which option is being clicked/modified * - modifier$: stream of boolean of modifier key being pressed or not * - filterAction$: stream of object where key is filter group (top level) and value is boolean of the group being clicked open or not */ -function intent(domSource$) { +function intent(domSource$, filterNames$) { // const expandAnyGhost$ = ghostChanges$.map(state => state.core.ghost.expand).startWith(false) - const showDoseUI$ = domSource$ - .select(".dose") - .events("click") - .fold((x, _) => ({ dose: !x.dose }), { dose: false }) - .startWith({ dose: false }) - - const showDose$ = xs - .merge( - showDoseUI$ - // showAnyGhost$ + const filterAction$ = filterNames$ + .map((names) => xs.fromArray(names.map( + (name) => domSource$ + .select("." + name) + .events("click") + .fold((x, _) => ({ [name]: !x[name] }), { [name]: false }) + .startWith({ [name]: false }) + )) ) - .remember() - - const showCellUI$ = domSource$ - .select(".cell") - .events("click") - .fold((x, _) => ({ cell: !x.cell }), { cell: false }) - .startWith({ cell: false }) - - const showCell$ = xs - .merge( - showCellUI$ - // showAnyGhost$ - ) - .remember() - - const showTypeUI$ = domSource$ - .select(".type") - .events("click") - .fold((x, _) => ({ trtType: !x.trtType }), { trtType: false }) - .startWith({ trtType: false }) - - const showType$ = xs - .merge( - showTypeUI$ - // showAnyGhost$ - ) - .remember() - - const filterAction$ = xs - .combine(showDose$, showCell$, showType$) - .map(mergeAll) + .compose(flattenConcurrently) + .compose(flattenConcurrently) + .fold((acc, new_) => ({...acc, ...new_}), ({})) // Toggles for filter options // const toggledGhost$ = @@ -139,35 +142,21 @@ function intent(domSource$) { // .map(state => state.core.ghost.deselect) // .compose(dropRepeats(equals)) - const doseToggled$ = domSource$ - .select(".dose-options") - .events("click") - .map(function (ev) { - ev.preventDefault() - return ev - }) - .map((ev) => ev.ownerTarget.id) - .map((value) => ({ dose: value })) - - const cellToggled$ = domSource$ - .select(".cell-options") - .events("click") - .map(function (ev) { - ev.preventDefault() - return ev - }) - .map((ev) => ev.ownerTarget.id) - .map((value) => ({ cell: value })) - - const typeToggled$ = domSource$ - .select(".type-options") - .events("click") - .map(function (ev) { - ev.preventDefault() - return ev - }) - .map((ev) => ev.ownerTarget.id) - .map((value) => ({ trtType: value })) + const filterValueAction$ = filterNames$ + .map((names) => xs.fromArray(names.map( + (name) => domSource$ + .select("." + name + "-options") + .events("click") + .map(function (ev) { + ev.preventDefault() + return ev + }) + .map((ev) => ev.ownerTarget.id) + .map((value) => ({ [name]: value })) + )) + ) + .compose(flattenConcurrently) + .compose(flattenConcurrently) const modDown$ = domSource$ .select("document") @@ -190,15 +179,8 @@ function intent(domSource$) { .compose(dropRepeats(equals)) .remember() - const action$ = xs.merge( - doseToggled$, - cellToggled$, - typeToggled$, - // toggledGhost$ - ) - return { - filterValuesAction$: action$, + filterValuesAction$: filterValueAction$, modifier$: modifier$, filterAction$: filterAction$, } @@ -235,7 +217,7 @@ export function model( core: { output: {}, filter_output: {}, - state: {dose: false, cell: false, trtType: false}, + state: {}, dirty: false, }, })) @@ -363,7 +345,7 @@ export function model( * @type {Reducer} */ const outputReducer$ = filterAction$ - .filter((state) => !state.dose && !state.cell && !state.trtType) + .filter((state) => none(identity, values(state))) // check all values are false .map(_ => (prevState) => ({ ...prevState, core: { ...prevState.core, filter_output: minimizeFilterOutput(prevState) }, @@ -381,17 +363,15 @@ export function model( const values = searchValue.split(',') return values.filter(v => possibleValues.includes(v)) } - - const matchedDoses = search.dose == undefined ? undefined : matchedFilters(search.dose, possibleValues.dose) - const matchedCells = search.cell == undefined ? undefined : matchedFilters(search.cell, possibleValues.cell) - const matchedTypes = search.trtType == undefined ? undefined : matchedFilters(search.trtType, possibleValues.trtType) - return { - dose: matchedDoses, - cell: matchedCells, - trtType: matchedTypes, - } + + return mergeAll( + keys(possibleValues) + .map((key) => ( + { [key] : search[key] == undefined ? undefined : matchedFilters(search[key], possibleValues[key]) } + )) + ) }) - .filter((output) => (output.dose != undefined || output.cell != undefined || output.trtType != undefined)) // Only set filter if filter values are set + .filter((output) => any((value) => (value != undefined), values(output))) // any value not undefined? .compose(dropRepeats(equals)) // only do this once. Changes in the WF should not be overwritten .map((output) => (prevState) => { const filter_output = minimizeFilterOutput({ @@ -493,9 +473,10 @@ function view(state$) { /** * A table of check boxes that is only shown if needed - * @param {*} toggle Visible or not? - * @param {*} options Array of possible options - * @param {*} selection Array of selections (multiple) + * @param {String} filter Filter name as specified by the API + * @param {Boolean} toggle Visible or not? + * @param {Array} possibleOptions Array of possible options + * @param {Array} selectedOptions Array of selections (multiple) */ const togglableFilter = (filter, toggle, possibleOptions, selectedOptions) => toggle @@ -517,78 +498,57 @@ function view(state$) { ), ]), ]) - : div(".cell .col .s10 .offset-s1", [""]) - + : div(".col .s10 .offset-s1", [""]) - const loadedVdom$ = modifiedState$.map((state) => { - const possibleDoses = state.settings.filter.values.dose || [ "Populating filter dialog...", ] - const possibleCells = state.settings.filter.values.cell || [ "Populating filter dialog...", ] - const possibleTypes = state.settings.filter.values.trtType || [ "Populating filter dialog...", ] - - const selectedDoses = - state.core.output.dose == undefined - ? possibleDoses - : state.core.output.dose - const selectedCells = - state.core.output.cell == undefined - ? possibleCells - : state.core.output.cell - const selectedTypes = - state.core.output.trtType == undefined - ? possibleTypes - : state.core.output.trtType - - return div([ - // DEBUG -- debugging purposes, remove when no longer necessary !!! - // div('.col .s12', [ div('', [ code('', JSON.stringify(fvs)) ] ) ]), - // div('.col .s12', [ div('', [ code('', JSON.stringify(state.settings.filter.values)) ] ) ]), - // div('.col .s12', [ div('', [ code('', JSON.stringify(state.core.output)) ] ) ]), - div(".col .s12", [ - div(".chip .dose .col .s12", [ - span(".dose .blue-grey-text", [ - noFilter(selectedDoses, possibleDoses) - ? "No Dose Filter" - : "Doses: " + selectedDoses.join(", "), - ]), - ]), - togglableFilter( - "dose", - state.core.state.dose, - possibleDoses, - selectedDoses - ), - ]), - div(".col .s12", [ - div(".chip .cell .col .s12", [ - span(".cell .blue-grey-text", [ - noFilter(selectedCells, possibleCells) - ? "No Cell Filter" - : "Cells: " + selectedCells.join(", "), - ]), - ]), - togglableFilter( - "cell", - state.core.state.cell, - possibleCells, - selectedCells - ), - ]), - div(".col .s12", [ - div(".chip .type .col .s12", [ - span(".type .blue-grey-text", [ - noFilter(selectedTypes, possibleTypes) - ? "No Type Filter" - : "Types: " + selectedTypes.join(", "), - ]), + /** + * Top level of a filter group. + * First part contains the filter name and summary of the selected filters + * Second part, if toggled open, lists all filter values with selection boxes + * @param {Array} selectedValues All filter values that are currently selected + * @param {Array} possibleValues All possible filter values in this group + * @param {String} propName Filter name as specified by the API + * @param {String} domText Filter name as it should be displayed on the DOM + * @param {Boolean} toggle Filter is currently open or not + * @returns vdom div containing sub vdom elements + */ + const collapsableFilter = (selectedValues, possibleValues, propName, domText, toggle) => + div(".col .s12", [ + div(".chip " + "." + propName + ".col .s12", [ + span("." + propName + " .blue-grey-text", [ + noFilter(selectedValues, possibleValues) + ? "No " + domText + " Filter" + : domText + ": " + selectedValues.join(", "), ]), - togglableFilter( - "type", - state.core.state.trtType, - possibleTypes, - selectedTypes - ), ]), + togglableFilter( + propName, + toggle, + possibleValues, + selectedValues + ), ]) + + const loadedVdom$ = modifiedState$.map((state) => { + + const propToFilterSection = (propName) => { + const lens = lensProp(propName) + const possibleValues = viewR(lens, state.settings.filter.values) || [ "Populating filter dialog...", ] + const selectedValues = viewR(lens, state.core.output) ?? possibleValues + const toggle = viewR(lens, state.core.state) + const domText = safeModelToUi(propName, state.settings.modelTranslations) + + return collapsableFilter(selectedValues, possibleValues, propName, domText, toggle) + } + + const filterGroups = keys(state.settings.filter.values) + // sort by name that will be shown on the UI + const sortedFilterGroups = sortWith([ + ascend(a => safeModelToUi(a, state.settings.modelTranslations)) + ])(filterGroups) + const domArray = sortedFilterGroups + .map(f => propToFilterSection(f)) + + return div(domArray) }) return xs.merge( @@ -627,11 +587,15 @@ function Filter(sources) { // The debounce is required, else it simply does not work const state$ = sources.onion.state$.compose(debounce(100)) + const filterNames$ = state$ + .map(state => keys(state.settings.filter.values)) + .compose(dropRepeats(equals)) + const input$ = sources.input const filterQuery = FetchFilters(sources) - const actions = intent(sources.DOM) + const actions = intent(sources.DOM, filterNames$) const vdom$ = view(state$) diff --git a/src/test/FilterTest.js b/src/test/FilterTest.js index a12b77af..e8225297 100644 --- a/src/test/FilterTest.js +++ b/src/test/FilterTest.js @@ -21,7 +21,7 @@ describe("defaultReducer", function () { const newState = f(state) assert.deepStrictEqual(newState.core.output, {}) assert.deepStrictEqual(newState.core.filter_output, {}) - assert.deepStrictEqual(newState.core.state, {dose: false, cell: false, trtType: false}) + assert.deepStrictEqual(newState.core.state, {}) }, error(e) { done(e) From d2f79bc9a83d68e3e04b7e73eb75002b18e2844f Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 17 Mar 2022 08:25:19 +0100 Subject: [PATCH 179/191] Gskcmp 100 restart api (#132) * Committing proof of concept Able to send commands to delete and start context (without external data) Able to send hard coded config file to initialize the api * more or less working Should make the commands configurable Each step works but all together somehow it returns 'classPath not found' * Simply pass File objects to cycle.js HTTP driver / superagent library Not well documented but works perfectly fine and better, with even less code than the alternatives so far * Add settings section and start using Intent Model View use api address from deployment instead of hard coded * move view code to view function and refactor to more logical names for their current function disable step 2 & 4 buttons if no file was selected * rename reload to init add link to SJS remove test/trigger 5 button * Add changelog entry * Add timeout functionality on init page status query Capture the invalid stream and push that as a failure string --- CHANGELOG.md | 2 + deployments.json | 4 + src/js/index.js | 2 + src/js/pages/admin.js | 86 ------------ src/js/pages/init.js | 306 ++++++++++++++++++++++++++++++++++++++++++ src/sass/_init.scss | 12 ++ src/sass/main.scss | 1 + 7 files changed, 327 insertions(+), 86 deletions(-) delete mode 100644 src/js/pages/admin.js create mode 100644 src/js/pages/init.js create mode 100644 src/sass/_init.scss diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d97f8fe..7c1d46f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Functionality +- Add browser access to restarting the API + ### Other - Filter functionality made dynamic so that new filter groups can be added as needed diff --git a/deployments.json b/deployments.json index 0a8d53a2..6b971863 100644 --- a/deployments.json +++ b/deployments.json @@ -90,6 +90,10 @@ "api": { "url": "http://localhost:8090/jobs?context=luciusapi&appName=luciusapi&sync=true&timeout=30" }, + "init": { + "url": "http://localhost:8090/", + "contextOptions": "context-factory=spark.jobserver.context.SessionContextFactory&spark.scheduler.mode=FAIR&spark.jobserver.context-creation-timeout=60&spark.memory.fraction=0.7&spark.dynamicAllocation.enabled=false&spark.executor.instances=6&spark.executor.cores=4&spark.executor.memory=4g&spark.yarn.executor.memoryOverhead=2g&spark.yarn.am.memory=4G&spark.driver.memory=4G" + }, "sourire": { "url": "http://localhost:9999/molecule/" }, diff --git a/src/js/index.js b/src/js/index.js index 2162a2e5..aa9fc0ad 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -40,6 +40,7 @@ import Debug from "./pages/debug" import Home from "./pages/home" import { IsolatedSettings } from "./pages/settings" import { IsolatedAdminSettings } from "./pages/adminSettings" +import Init from "./pages/init" // Utilities import { initSettings } from "./configuration.js" @@ -84,6 +85,7 @@ export default function Index(sources) { "/correlation": CorrelationWorkflow, "/debug": Debug, "/admin": IsolatedAdminSettings, + "/init": Init, "*": Home, })(sources) diff --git a/src/js/pages/admin.js b/src/js/pages/admin.js deleted file mode 100644 index 70d2ce57..00000000 --- a/src/js/pages/admin.js +++ /dev/null @@ -1,86 +0,0 @@ -import xs from "xstream" -import { div, p } from "@cycle/dom" -import { merge, prop, equals } from "ramda" - -import { initSettings } from "../configuration" -import debounce from "xstream/extra/debounce" -import dropRepeats from "xstream/extra/dropRepeats" - -const defaultState = { - green: { - url: "http://localhost:8090", - }, - blue: { - url: "localhost:8081", - }, -} - -/** - * This component should become an API managemt page used to trigger initialization - * and other operational aspects of managing LuciusAPI. - */ -function Admin(sources) { - const state$ = sources.onion.state$ - .compose(dropRepeats((x, y) => equals(x.core, y.core))) - .startWith({ core: defaultState, settings: initSettings }) - // .map(state => merge(state, state.settings.admin, state.settings.api)) - - const request$ = state$.map((state) => ({ - url: state.core.green.url + "/jobs", - method: "GET", - send: {}, - category: "status", - })) - - const response$$ = sources.HTTP.select("status") - - const invalidResponse$ = response$$ - .map( - (response$) => - response$ - .filter((response) => false) // ignore regular events - .replaceError((error) => xs.of(error)) // emit error - ) - .flatten() - - /** - * Parse the successful results only. - * - * We add a little wait time (`debounce`) in order for the jobserver - * to be up-to-date with the actual jobs. Otherwize, we measure the - * wrong job times. - */ - const validResponse$ = response$$ - .map((response$) => response$.replaceError((error) => xs.empty())) - .flatten() - .compose(debounce(500)) - - const vdom$ = state$.map((state) => - div(".container", [ - div([p("Green - Alright, let's get rolling..." + state.extra)]), - div([p("Blue - Alright, let's get rolling...")]), - ]) - ) - - // This is needed in order to get the onion stream active! - const defaultReducer$ = xs.of((prevState) => { - if (typeof prevState === "undefined") { - return defaultState - } else { - return prevState - } - }) - - const responseReducer$ = validResponse$.map((response) => (prevState) => ({ - ...prevState, - core: { ...prevState.core, state: 1 }, - })) - - return { - DOM: vdom$, - HTTP: request$, - onion: xs.merge(defaultReducer$, responseReducer$), - } -} - -export default Admin diff --git a/src/js/pages/init.js b/src/js/pages/init.js new file mode 100644 index 00000000..5401f0fd --- /dev/null +++ b/src/js/pages/init.js @@ -0,0 +1,306 @@ +import xs from "xstream" +import { div, p, span, button, textarea, input, a } from "@cycle/dom" +import isolate from "@cycle/isolate" +import { merge, prop, equals } from "ramda" + +import { initSettings } from "../configuration" +import { SettingsEditor } from "../components/SettingsEditor" +import debounce from "xstream/extra/debounce" +import dropRepeats from "xstream/extra/dropRepeats" +import sampleCombine from "xstream/extra/sampleCombine" + +const defaultState = { + green: { + url: "http://localhost:8090", + }, + blue: { + url: "localhost:8081", + }, +} + +const settingsConfig = [ + { + group: "init", + title: "API Settings", + settings: [ + { + field: "url", + class: ".input-field", + type: "text", + title: "Spark JobServer URL", + props: {}, + }, + { + field: "contextOptions", + class: ".input-field", + type: "text", + title: "Context options", + props: {}, + }, + ], + }, +] + +function intent(domSource$) { + const trigger1$ = domSource$.select(".trigger1").events("click").remember() + const trigger2$ = domSource$.select(".trigger2").events("click").remember() + const trigger3$ = domSource$.select(".trigger3").events("click").remember() + const trigger4$ = domSource$.select(".trigger4").events("click").remember() + // const trigger5$ = domSource$.select(".trigger5").events("click").remember() + + const loadJar$ = domSource$.select(".jarFile").events("change").remember().debug("loadJar$") + const loadConfig$ = domSource$.select(".configFile").events("change").remember().debug("loadConfig$") + + return { + trigger1$: trigger1$, + trigger2$: trigger2$, + trigger3$: trigger3$, + trigger4$: trigger4$, + // trigger5$: trigger5$, + + loadJar$: loadJar$, + loadConfig$: loadConfig$, + } +} + +function model(actions, state$) { + const jarFile$ = actions.loadJar$ + .map((_) => { + const input = document.getElementById("jarFile") + const file = input.files[0] + return file + }) + .startWith(undefined) + + const configFile$ = actions.loadConfig$ + .map((_) => { + const input = document.getElementById("configFile") + const file = input.files[0] + return file + }) + .startWith(undefined) + + // Deleting previous context... + // curl -X DELETE localhost:8090/contexts/luciusapi + const requestTrigger1$ = actions.trigger1$.compose(sampleCombine(state$)) + .map(([_, state]) => ({ + url: state.settings.init.url + "contexts/luciusapi", + method: "DELETE", + send: {}, + category: "init", + })).debug("requestTrigger1$") + + // Uploading assembly jar... + // curl -H Content-Type: application/java-archive --data-binary @target/scala-2.11/LuciusAPI-assembly-5.0.1.jar localhost:8090/binaries/luciusapi + const requestTrigger2$ = actions.trigger2$.compose(sampleCombine(xs.combine(state$, jarFile$))) + .map(([_, [state, jar]]) => ({ + url: state.settings.init.url + "binaries/luciusapi", + method: "POST", + type: "application/java-archive", + send: jar, + category: "init", + })) + + // Starting new context... + // curl -d localhost:8090/contexts/luciusapi?context-factory=spark.jobserver.context.SessionContextFactory&spark.scheduler.mode=FAIR&spark.jobserver.context-creation-timeout=60&spark.memory.fraction=0.7&spark.dynamicAllocation.enabled=false&spark.executor.instances=6&spark.executor.cores=4&spark.executor.memory=4g&spark.yarn.executor.memoryOverhead=2g&spark.yarn.am.memory=4G&spark.driver.memory=4G + const requestTrigger3$ = actions.trigger3$.compose(sampleCombine(state$)) + .map(([_, state]) => ({ + url: state.settings.init.url + "contexts/luciusapi?" + state.settings.init.contextOptions, + method: "POST", + send: {}, + category: "init", + })) + + // Initializing API... + // curl --data-binary @utils/../../config/spark_config.conf localhost:8090/jobs?context=luciusapi&appName=luciusapi&classPath=com.dataintuitive.luciusapi.initialize + const requestTrigger4$ = actions.trigger4$.compose(sampleCombine(xs.combine(state$, configFile$))) + .map(([_, [state, config]]) => ({ + url: state.settings.init.url + "jobs?context=luciusapi&appName=luciusapi&classPath=com.dataintuitive.luciusapi.initialize", + method: "POST", + send: config, + category: "init", + })) + + // const requestTrigger5$ = actions.trigger5$.compose(sampleCombine(state$)) + // .map(([_, state]) => ({ + // url: state.settings.init.url + "jobs/9ab0a4bb-0e62-49f4-8654-db47e701c59c", + // method: "DELETE", + // send: {}, + // category: "init", + // })) + + return { + jarFile$: jarFile$, + configFile$: configFile$, + requests$: xs.merge( + requestTrigger1$, + requestTrigger2$, + requestTrigger3$, + requestTrigger4$, + // requestTrigger5$, + ) + } +} + +function view(requests$, responses$, statusDisplay$, settingsDOM$, jarFile$, configFile$, apiUrl$) { + + const requestsText$ = requests$ + .map((obj) => ( + "--> " + obj.method + ": " + obj.url + )) + + const responsesText$ = responses$ + .map((t) => "<-- " + (t != undefined ? t.body.status + ": " + (t.body.result ?? t.body.duration ?? "") : "response missing")) + + const initText$ = xs + .merge( + requestsText$, + responsesText$ + ) + .fold((acc, t) => acc + t + "\r\n", "") + + const vdom$ = xs.combine(apiUrl$, statusDisplay$, settingsDOM$, initText$, jarFile$, configFile$) + .map(([sjsLink, statusDisplay, settings, initText, jarFile, configFile]) => + div(".container .init", [ + div(".row .s12", a({props: {href: sjsLink, target: "_blank"}}, "Spark overview page")), + div([p("Server poll status: ", [ + span("SJS status query: "), + span(".status-" + statusDisplay, + statusDisplay == "loading" + ? "no reply received yet" + : statusDisplay == "valid" + ? "reply successfully received" + : "status query failed" + ) + ])]), + settings, + div(".row .s12"), + div(".row .s12", [ + span(".col .s1", "Step 1"), + span(".col .s3 .offset-s1", "Delete previous context"), + button(".trigger1 .col .s2 .offset-s5 .btn .grey", "Start"), + ]), + div(".row .s12", [ + span(".col .s1", "Step 2"), + span(".col .s3 .offset-s1", "Upload assembly jar"), + input(".col .s3 .offset-s1 .jarFile", { props: {type: "file", name: "jarFile", id: "jarFile", accept: "application/java-archive"} }), + button(".trigger2 .col .s2 .offset-s1 .btn .grey" + (jarFile == undefined ? " .disabled" : ""), "Start"), + ]), + div(".row .s12", [ + span(".col .s1", "Step 3"), + span(".col .s3 .offset-s1", "Start new context"), + button(".trigger3 .col .s2 .offset-s5 .btn .grey", "Start"), + ]), + div(".row .s12", [ + span(".col .s1", "Step 4"), + span(".col .s3 .offset-s1", "Initialize API"), + input(".col .s3 .offset-s1 .configFile", { props: {type: "file", name: "configFile", id: "configFile", accept: "application/json, .conf"} }), + button(".trigger4 .col .s2 .offset-s1 .btn .grey" + (configFile == undefined ? " .disabled" : ""), "Start"), + ]), + div(".row .s12", textarea({ props: { value: initText, readOnly: true}, style: { height: "400px" } })), + // div(".row .s12", [ + // button(".trigger5 .col .s2 .btn .grey", "Test button"), + // ]), + div(".row .s12", [""]), + ]) + ) + return vdom$ +} + + +/** + * This component should become an API managemt page used to trigger initialization + * and other operational aspects of managing LuciusAPI. + */ +function Init(sources) { + const state$ = sources.onion.state$ + .compose(dropRepeats(equals)) + .startWith({ core: defaultState, settings: initSettings }) + // .map(state => merge(state, state.settings.admin, state.settings.api)) + + const apiUrl$ = state$.map((state) => state.settings.init?.url).compose(debounce(1000)) + + const statusRequest$ = apiUrl$.map((url) => ({ + url: url + "jobs", + method: "GET", + send: {}, + category: "status", + })) + + const statusResponse$$ = sources.HTTP.select("status") + + const invalidStatusResponse$ = statusResponse$$ + .map( + (response$) => + response$ + .filter((response) => false) // ignore regular events + .replaceError((error) => xs.of(error)) // emit error + ) + .flatten() + + /** + * Parse the successful results only. + * + * We add a little wait time (`debounce`) in order for the jobserver + * to be up-to-date with the actual jobs. Otherwize, we measure the + * wrong job times. + */ + const validStatusResponse$ = statusResponse$$ + .map((response$) => response$.replaceError((error) => xs.empty())) + .flatten() + .compose(debounce(500)) + + const statusDisplay$ = xs + .merge( + validStatusResponse$.mapTo("valid"), + invalidStatusResponse$.mapTo("invalid") + ) + .startWith("loading") + + const Settings = isolate(SettingsEditor, "settings")({ + ...sources, + settings$: xs.of(settingsConfig) + }) + + const response$$ = sources.HTTP.select("init") + + const responses$ = response$$ + .map( + (response$) => + response$.debug("response$") + .replaceError((error) => xs.of(error.response)) // emit error + ) + .flatten() + .debug("responses$") + + const actions = intent(sources.DOM) + + const model_ = model(actions, state$) + + const vdom$ = view(model_.requests$, responses$, statusDisplay$, Settings.DOM, model_.jarFile$, model_.configFile$, apiUrl$) + + + // This is needed in order to get the onion stream active! + const defaultReducer$ = xs.of((prevState) => { + if (typeof prevState === "undefined") { + return defaultState + } else { + return prevState + } + }) + + // const responseReducer$ = validResponse$.map((response) => (prevState) => ({ + // ...prevState, + // core: { ...prevState.core, state: 1 }, + // })) + + const settingsReducer$ = Settings.onion.compose(debounce(200)) + + return { + DOM: vdom$, + HTTP: xs.merge(statusRequest$, model_.requests$), + onion: xs.merge(defaultReducer$, /*responseReducer$,*/ settingsReducer$), + } +} + +export default Init diff --git a/src/sass/_init.scss b/src/sass/_init.scss new file mode 100644 index 00000000..61caf9ed --- /dev/null +++ b/src/sass/_init.scss @@ -0,0 +1,12 @@ +// Styling of the status query +main div.init { + span.status-loading { + color: orange; + } + span.status-valid { + color: green; + } + span.status-invalid { + color: red; + } +} diff --git a/src/sass/main.scss b/src/sass/main.scss index 93c960b7..0295100c 100644 --- a/src/sass/main.scss +++ b/src/sass/main.scss @@ -179,6 +179,7 @@ div.fixed-action-btn { } @import 'exporter'; +@import 'init'; /* home svg styling and hover */ From 487ada484be79390ba4d6037981647cb78e7e14c Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 17 Mar 2022 08:30:39 +0100 Subject: [PATCH 180/191] Gskcmp 38 add functionality to create a pdf and txt report from the workflow output (#135) * WIP add markdown report export * Add reports for different workflows Create streams with headers added to the data * Add markdown styling for the URL, add rounding to zhang score * Move filter to Md to utils/export.js * Add table for selected and deselected samples * Don't display the 'deselected samples' table when there are no deselected samples * Add export md buttons on exporter modal Create stream to enable/disable the buttons when not all data is available yet * fix encoding of url and md file * Add entry in changelog * Add documentation for deployments in the changelog Add a section `Deployment changes` Add comment: Requires extra translations for each filter type in `deployments.json` * Revert "Add documentation for deployments in the changelog" This reverts commit 180bfd36421024f6c911fdb17a23bd72f3b7e89a. --- CHANGELOG.md | 1 + src/js/components/Exporter.js | 159 +++++++++++++++++++++++++++---- src/js/pages/compound.js | 1 + src/js/pages/correlation.js | 1 + src/js/pages/disease.js | 2 +- src/js/pages/genericTreatment.js | 5 +- src/js/pages/genetic.js | 1 + src/js/pages/ligand.js | 1 + src/js/utils/export.js | 104 +++++++++++++++++++- 9 files changed, 255 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c1d46f4..8aaf6a43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Functionality +- Add exporting of report in Markdown format - Add browser access to restarting the API ### Other diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index e64728c9..6faa7a51 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -1,12 +1,13 @@ import xs from "xstream" import { div, i, ul, li, p, input, button, span, a } from "@cycle/dom" -import { isEmpty, mergeLeft, equals } from "ramda" +import { isEmpty, mergeLeft, equals, keys, toUpper, all, identity } from "ramda" import { loggerFactory } from "../utils/logger" import delay from "xstream/extra/delay" import debounce from "xstream/extra/debounce" import sampleCombine from "xstream/extra/sampleCombine" import dropRepeats from "xstream/extra/dropRepeats" -import { convertToCSV } from "../utils/export" +import { convertToCSV, convertTableToMd, convertFilterToMd, convertSelectedSamplesToMd } from "../utils/export" +import { map } from "jquery" /** * @module components/Exporter @@ -27,6 +28,7 @@ function intent(domSource$) { const exportPlotsTrigger$ = domSource$.select(".export-clipboard-plots").events("click") const exportHeadTableTrigger$ = domSource$.select(".export-clipboard-headTable").events("click") const exportTailTableTrigger$ = domSource$.select(".export-clipboard-tailTable").events("click") + const exportMdReportTrigger$ = domSource$.select(".export-clipboard-mdReport").events("click") const modalTrigger$ = domSource$.select(".modal-open-btn").events("click") const modalCloseTrigger$ = domSource$.select(".export-close").events("click") @@ -41,6 +43,7 @@ function intent(domSource$) { exportPlotsTrigger$: exportPlotsTrigger$, exportHeadTableTrigger$: exportHeadTableTrigger$, exportTailTableTrigger$: exportTailTableTrigger$, + exportMdReportTrigger$: exportMdReportTrigger$, modalTrigger$: modalTrigger$, modalCloseTrigger$: modalCloseTrigger$, testTrigger$: testTrigger$, @@ -80,6 +83,18 @@ function model(actions, state$, vega$, config) { const headTablePresent$ = state$.map((state) => notEmptyOrUndefined(state.headTable?.data)).startWith(false) const tailTablePresent$ = state$.map((state) => notEmptyOrUndefined(state.tailTable?.data)).startWith(false) + const genericMdDataPresent$ = xs.combine(signaturePresent$, plotsPresent$, headTablePresent$, tailTablePresent$).map((arr) => all(identity)(arr)) + const diseaseMdDataPresent$ = xs.combine(plotsPresent$, headTablePresent$, tailTablePresent$).map((arr) => all(identity)(arr)) + const correlationMdDataPresent$ = plotsPresent$ + + const mdPresentSelector = { + "": genericMdDataPresent$, // generic treatment + "DISEASE": diseaseMdDataPresent$, + "CORRELATION": correlationMdDataPresent$, + } + + const mdReportPresent$ = (mdPresentSelector[toUpper(config.workflowName)] ?? mdPresentSelector[""]) + const url$ = state$.map((state) => state.routerInformation.pageStateURL).startWith("") const signature$ = state$.map((state) => state.form.signature?.output).startWith("") // result already contains 'data:image/png;base64,' @@ -99,10 +114,104 @@ function model(actions, state$, vega$, config) { .map((data) => convertToCSV(data)) .startWith("") + const urlMd$ = url$.map((url) => "["+url+"]("+url+")").startWith("") + + const treatmentQueryMd$ = state$.map((state) => state.form.check?.output) // generic treatment WF + const signatureQueryMd$ = state$.map((state) => state.form.query) // disease WF + const signatureQuery1Md$ = state$.map((state) => state.form.query1) // correlation WF + const signatureQuery2Md$ = state$.map((state) => state.form.query2) // correlation WF + + // present in generic treatment WFs + const samplesMd$ = state$.map((state) => convertSelectedSamplesToMd(state.form.sampleSelection?.data)).startWith("") + const signatureMd$ = signature$ + + const filterMd$ = state$.map((state) => convertFilterToMd(state.filter.filter_output, state.settings.filter.values)) + .startWith("") + + const plotMd$ = plotFile$ + .filter((data) => data != "") + .map((data) => "![](" + data + ")") + .startWith("") + + const headTableMd$ = state$.map((state) => state.headTable?.data) + .filter((data) => notEmptyOrUndefined(data)) + .map((data) => convertTableToMd(data)) + .startWith("") + + const tailTableMd$ = state$.map((state) => state.tailTable?.data) + .filter((data) => notEmptyOrUndefined(data)) + .map((data) => convertTableToMd(data)) + .startWith("") + + // data streams with headers added + const addHeaderL2 = (h) => (s) => ("\n" + "## " + h + "\n\n" + s) + const urlMdWH$ = urlMd$.map(addHeaderL2("Search query URL")) + const treatmentQueryMdWH$ = treatmentQueryMd$.map(addHeaderL2("Treatment query")) + const signatureQueryMdWH$ = signatureQueryMd$.map(addHeaderL2("Signature query")) + const signatureQuery1MdWH$ = signatureQuery1Md$.map(addHeaderL2("Signature query 1")) + const signatureQuery2MdWH$ = signatureQuery2Md$.map(addHeaderL2("Signature query 2")) + const samplesMdWH$ = samplesMd$.map(addHeaderL2("Selected samples")) + const signatureMdWH$ = signatureMd$.map(addHeaderL2("Signature")) + const filterMdWH$ = filterMd$.map(addHeaderL2("Filter")) + const plotMdWH$ = plotMd$.map(addHeaderL2("Plot")) + const headTableMdWH$ = headTableMd$.map(addHeaderL2("Top table")) + const tailTableMdWH$ = tailTableMd$.map(addHeaderL2("Bottom table")) + + const WFTitleMd$ = xs.of("# " + config.workflowName + " Workflow report") + + const genericMd$ = xs.combine(WFTitleMd$, urlMdWH$, treatmentQueryMdWH$, samplesMdWH$, signatureMdWH$, filterMdWH$, plotMdWH$, headTableMdWH$, tailTableMdWH$) + .map(([WFTitle, url, treatmentQuery, samples, signature, filter, plot, head, tail]) => + [ + WFTitle, + url, + treatmentQuery, + samples, + signature, + filter, + plot, + head, + tail + ].join("\n") + ) + + const DiseaseMd$ = xs.combine(WFTitleMd$, urlMdWH$, signatureQueryMdWH$, filterMdWH$, plotMdWH$, headTableMdWH$, tailTableMdWH$) + .map(([WFTitle, url, signatureQuery, filter, plot, head, tail]) => + [ + WFTitle, + url, + signatureQuery, + filter, + plot, + head, + tail + ].join("\n") + ) + + const CorrelationMd$ = xs.combine(WFTitleMd$, urlMdWH$, signatureQuery1MdWH$, signatureQuery2MdWH$, filterMdWH$, plotMdWH$) + .map(([WFTitle, url, signatureQuery1, signatureQuery2, filter, plot, head, tail]) => + [ + WFTitle, + url, + signatureQuery1, + signatureQuery2, + filter, + plot, + ].join("\n") + ) + + const MdSelector = { + "": genericMd$, // generic treatment + "DISEASE": DiseaseMd$, + "CORRELATION": CorrelationMd$, + } + + const selectedMd$ = MdSelector[toUpper(config.workflowName)] ?? MdSelector[""] + const urlFile$ = url$.map(url => "data:text/plain;charset=utf-8," + url) const signatureFile$ = signature$.map(signature => "data:text/plain;charset=utf-8," + signature) const headTableCsvFile$ = headTableCsv$.map(headTableCsv => "data:text/tsv;charset=utf-8," + encodeURIComponent(headTableCsv)) const tailTableCsvFile$ = tailTableCsv$.map(tailTableCsv => "data:text/tsv;charset=utf-8," + encodeURIComponent(tailTableCsv)) + const mdReportFile$ = selectedMd$.map(md => "data:text/plain;charset=utf-8," + encodeURIComponent(md)) const clipboardLinkFab$ = actions.exportLinkTriggerFab$ .compose(sampleCombine(url$)) @@ -172,21 +281,21 @@ function model(actions, state$, vega$, config) { })) .remember() + const clipboardMdReport$ = actions.exportMdReportTrigger$ + .compose(sampleCombine(selectedMd$)) + .map(([_, md]) => ({ + sender: "mdReport", + data: md, + })) + .remember() + const testAction$ = actions.testTrigger$ - .compose(sampleCombine(plotFile$)) - .map(([_, data]) => { - // input data is "data:image/png;base64,abcdef0123456789..." - const parts = data.split(';base64,'); - const imageType = parts[0].split(':')[1]; - const decodedData = window.atob(parts[1]); - const uInt8Array = new Uint8Array(decodedData.length); - for (let i = 0; i < decodedData.length; i++) { - uInt8Array[i] = decodedData.charCodeAt(i); - } - const blob = new Blob([uInt8Array], { type: imageType }) + .compose(sampleCombine(selectedMd$)) + .map(([_, md]) => { + return { - type: imageType, - data: blob, + sender: 'test-fab', + data: md, } }) .remember() @@ -194,12 +303,23 @@ function model(actions, state$, vega$, config) { return { reducers$: xs.empty(), modal$: xs.merge(openModal$, closeModal$), - clipboard$: xs.merge(clipboardLinkFab$, clipboardSignatureFab$, clipboardLink$, clipboardSignature$, clipboardPlots$, clipboardHeadTable$, clipboardTailTable$, testAction$), + clipboard$: xs.merge( + clipboardLinkFab$, + clipboardSignatureFab$, + clipboardLink$, + clipboardSignature$, + clipboardPlots$, + clipboardHeadTable$, + clipboardTailTable$, + clipboardMdReport$, + testAction$, + ), dataPresent: { signaturePresent$: signaturePresent$, plotsPresent$: plotsPresent$, headTablePresent$: headTablePresent$, tailTablePresent$: tailTablePresent$, + mdReportPresent$: mdReportPresent$, }, exportData: { url$: url$, @@ -208,6 +328,7 @@ function model(actions, state$, vega$, config) { plotFile$: plotFile$, headTableCsvFile$: headTableCsvFile$, tailTableCsvFile$: tailTableCsvFile$, + mdReportFile$: mdReportFile$, } } } @@ -268,12 +389,14 @@ function view(state$, dataPresent, exportData, config, clipboard) { dataPresent.plotsPresent$, dataPresent.headTablePresent$, dataPresent.tailTablePresent$, + dataPresent.mdReportPresent$, exportData.url$, exportData.urlFile$, exportData.signatureFile$, exportData.plotFile$, exportData.headTableCsvFile$, exportData.tailTableCsvFile$, + exportData.mdReportFile$, clipboard.copyImagesPermission$, clipboardResultAutoClear$, ) @@ -282,12 +405,14 @@ function view(state$, dataPresent, exportData, config, clipboard) { plotsPresent, headTablePresent, tailTablePresent, + mdReportPresent, url, urlFile, signatureFile, plotFile, headTableCsvFile, tailTableCsvFile, + mdReportFile, clipboardPermissions, clipboardResult, ]) => { @@ -337,6 +462,7 @@ function view(state$, dataPresent, exportData, config, clipboard) { addExportDiv("plot", "Copy " + config.plotName + " plot", ".export-clipboard-plots", plotFile, "plot.png", plotsPresent, copyImagesPermission), addExportDiv("headTable", "Copy top table", ".export-clipboard-headTable", headTableCsvFile, "table.tsv", headTablePresent), addExportDiv("tailTable", "Copy bottom table", ".export-clipboard-tailTable", tailTableCsvFile, "table.tsv", tailTablePresent), + addExportDiv("mdReport", "Copy MarkDown report", ".export-clipboard-mdReport", mdReportFile, "report.md", mdReportPresent), // div(".row", [ // span(".col .s6 .push-s1", "Export report"), // span(".btn .col .s1 .offset-s3 .export-file-report .disabled", i(".material-icons", "file_download")), @@ -375,6 +501,7 @@ function Exporter(sources) { plotName: "binned similarity", // part of the text to be displayed for plot copy/download fabSignature: "update", // part of FAB class name, set to "", ".hide" or ".disabled". //"update" sets ".disabled" when the signature is not available and updates the FAB when it becomes available + workflowName: "", // Both the name to add in the MarkDown report and select how the content in combined } const fullConfig = mergeLeft(sources.config, defaultConfig) diff --git a/src/js/pages/compound.js b/src/js/pages/compound.js index c75c3be8..070b8829 100644 --- a/src/js/pages/compound.js +++ b/src/js/pages/compound.js @@ -11,6 +11,7 @@ export default function CompoundWorkflow(sources) { mainDivClass: ".row .compound", loggerName: "compound", ghostModeScenarioSelector: ((state) => state.settings.common.ghost.compound), + workflowName: "Compound" }, } diff --git a/src/js/pages/correlation.js b/src/js/pages/correlation.js index 984cde5b..0dc5db0d 100644 --- a/src/js/pages/correlation.js +++ b/src/js/pages/correlation.js @@ -97,6 +97,7 @@ function CorrelationWorkflow(sources) { plotId: "#corrplot", plotName: "correlation", fabSignature: ".hide", + workflowName: "Correlation" } }) diff --git a/src/js/pages/disease.js b/src/js/pages/disease.js index 44baeeac..2eb82404 100644 --- a/src/js/pages/disease.js +++ b/src/js/pages/disease.js @@ -189,7 +189,7 @@ function DiseaseWorkflow(sources) { const exporter = Exporter({ ...sources, - config: { fabSignature: ".hide" } + config: { fabSignature: ".hide", workflowName: "Disease" } }) /** diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index aab6c3b4..b16e88bd 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -263,7 +263,10 @@ export default function GenericTreatmentWorkflow(sources) { .remember(), }) - const exporter = Exporter(sources) + const exporter = Exporter({ + ...sources, + config: { workflowName: workflow.workflowName }, + }) /** * Style object used in div capsulating filter, displayPlots and tables diff --git a/src/js/pages/genetic.js b/src/js/pages/genetic.js index b40b14c1..213f53e9 100644 --- a/src/js/pages/genetic.js +++ b/src/js/pages/genetic.js @@ -11,6 +11,7 @@ export default function GeneticWorkflow(sources) { mainDivClass: ".row .genetic", loggerName: "genetic", ghostModeScenarioSelector: ((state) => state.settings.common.ghost.genetic), + workflowName: "Genetic" }, } diff --git a/src/js/pages/ligand.js b/src/js/pages/ligand.js index ffadd920..fc3b6d68 100644 --- a/src/js/pages/ligand.js +++ b/src/js/pages/ligand.js @@ -11,6 +11,7 @@ export default function LigandWorkflow(sources) { mainDivClass: ".row .ligand", loggerName: "ligand", ghostModeScenarioSelector: ((state) => state.settings.common.ghost.ligand), + workflowName: "Ligand" }, } diff --git a/src/js/utils/export.js b/src/js/utils/export.js index 2940aebc..943ecd7a 100644 --- a/src/js/utils/export.js +++ b/src/js/utils/export.js @@ -1,4 +1,4 @@ -import { keys, values } from 'ramda' +import { keys, values, filter } from 'ramda' const convertToSafeString = (s) => { const quotesRequired = (s.indexOf(';') > -1) || (s.indexOf(',') > -1) @@ -47,4 +47,104 @@ function convertToCSV_internal(objArray) { return csv; } -export { convertToCSV } \ No newline at end of file +function convertTableToMd(objArray) { + + function convertToSafeString(s) { + return s.replace('|', '\|') + } + + function toTableLine(arr) { + return '| ' + arr.join(' | ') + ' |' + } + + const header = ["Zhang Score", "Sample ID", "Cell", "Treatment ID", "Treatment Name", "Treatment Type"] + const headerMd = toTableLine(header) + const headerMdSubLine = toTableLine(header.map(s => '---')) + + const md = [headerMd, headerMdSubLine] + .concat( + objArray.map(row => ( + toTableLine( + [parseFloat(row.zhang).toFixed(3), row.id, row.cell, row.trt_id, row.trt_name, row.trt] + .map(s => convertToSafeString(s)) + ) + ) + )) + .join('\n') + + return md +} + +function convertFilterToMd(selectedFilters, availableFilters) { + if (selectedFilters == undefined || selectedFilters?.length == 0) { + return "No filters applied" + } + else { + const allGroupNames = keys(availableFilters) + const allFilters = allGroupNames.map((group) => { + if (selectedFilters[group] != undefined) + { + return "\n### " + group + "\n\n" + selectedFilters[group].map((f) => "- " + f).join("\n") + } + else + { + return "\n### " + group + "\n\nNo filters selected" + } + + }) + return allFilters.join("\n") + } +} + +function convertSelectedSamplesToMd(samplesArr) { + if (samplesArr == undefined || samplesArr.length == 0) + return "No samples available" + + const isSelected = (sample) => sample.use + const isNotSelected = (sample) => !sample.use + + const selectedArr = filter(isSelected, samplesArr) + const notSelectedArr = filter(isNotSelected, samplesArr) + + function convertToSafeString(s) { + return s.replace('|', '\|') + } + + function toTableLine(arr) { + return '| ' + arr.join(' | ') + ' |' + } + + const header = ["ID", "Name", "Sample", "Cell", "Dose", "Time", "Sign. Genes"] + const headerMd = toTableLine(header) + const headerMdSubLine = toTableLine(header.map(s => '---')) + + const selectedMd = [headerMd, headerMdSubLine] + .concat( + selectedArr.map(row => ( + toTableLine( + [row.trt_id, row.trt_name, row.id, row.cell, row.dose + " " + row.dose_unit, row.time + " " + row.time_unit, row.significantGenes.toString()] + .map(s => convertToSafeString(s)) + ) + ) + )) + .join('\n') + + const notSelectedMd = [headerMd, headerMdSubLine] + .concat( + notSelectedArr.map(row => ( + toTableLine( + [row.trt_id, row.trt_name, row.id, row.cell, row.dose + " " + row.dose_unit, row.time + " " + row.time_unit, row.significantGenes.toString()] + .map(s => convertToSafeString(s)) + ) + ) + )) + .join('\n') + + const md = notSelectedArr.length > 0 + ? ["", "### Selected samples", "", selectedMd, "", "### Deselected samples", "", notSelectedMd].join("\n") + : ["", "### Selected samples", "", selectedMd].join("\n") + + return md +} + +export { convertToCSV, convertTableToMd, convertFilterToMd, convertSelectedSamplesToMd } From 04d4dd94415fae219dfd8fbcfe31dc8215d999dc Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Mon, 28 Mar 2022 14:52:34 +0200 Subject: [PATCH 181/191] Develop 5.1 (#143) * attempt at working with the history driver * WIP allow a search query to fill in the treatment to solve: validation & optional 'go' * Add validation for treatment type when copy/pasting or using search query When there is new input and is validated by the API, validate the output auto complete only works when there are more than 1 option * Add parameter to automatically start search Still need to fix edge conditions when the search field is changed later Remove temporary debug outputs * only trigger autorun once When the query is changed and then changed back to the original value, autorun should not trigger * Simplify single autorun logic * Support a search query string to automatically select specific samples at load Only perform the action once We should add a check whether all specified samples were in fact found/selected * Add filter search query handling * Fix Filter testbench filter model has a new input stream, add xs.empty() for each test * Fix page failure when no filter search query present Added missing object selector * combine the page state to create a sharable URL * Pass strings in the page state instead of arrays so page state is uniform Moving burden of knowing what format the data is in to the one where it is generated * temporarily display generated URL on the bottom of the page Add server name, port, protocol in the URL. Haven't found it in the router though. * Create placeholder for clipboardDriver and add functionality to genericTreatment page * Add clipboard driver functionality * Restyle temporary vdom search query divs Just making it looking a little bit less bad * Add POC floating action button buttons/icons are still from example somehow materialize css doesn't get applied * Improve POC * WIP Work on getting modal to display Text for modal is hidden and opens when fab is pressed, however often/always? requires two presses initially Modal is shown but only inline with current position of div instead of floating over rest of the page Closing modal disables scrolling the page too * Open & close of modal somewhere triggers to the modal are getting delayed between the stream in index.js & the driver * Use span instead of a to prevent errors in history driver errors caused the modal not opening due delayed streams Add same styling for fab 'a' for 'span' * Move modal styling to own sass file, move sass files to separate folder * Add trigger placeholders for fab restructure code to pass triggers to the model * Create first draft of what the export menu could possibly look like Add some text and buttons with options that are at least a bit topical * Disable signature export buttons when no signature present does not yet work for FAB, probably code loses track as the classes change * add option in ghost scenario runner to wait for a condition that must be matched This allows the scenario to be more reactive to backend delays instead of having to wait fixed times * Add option in genericTreatments to use the typer for the treatment search value add typer as parameter, if empty or "yes" it uses a default speed of 100ms minimum value 50ms, maximum value 5000ms * Add search query functionality in the disease WF validation happens when all genes are found, silently translate substitutions (wanted?) * Add search query functionality for the correlation WF Add typers & autorun code in form changed lenses in SignatureCheck to set correct IDs * refactor modal to use clipboard and disable not available exports * add download of url and signature pass url & signature to modal creation so we can add the required download functionality * add file exports in modal * Add placeholders for exporting plots * Add extra support for exporting to clipboard * Provide way to paste images in clipboard copying of plots is with fixed data * change notEmpty to notEmptyOrUndefined and support arrays use ramda 'isEmpty' but also catch undefined. isEmpty returns false for undefined Add plotsPresent$ functionality * add basic code to get vega image and paste to clipboard add test button in FAB so it's easier to assign test functionality add stream return to vega driver builder * Move creation of plot data to separate stream remove static encoded image new plot string already contains "data:image/png;base64," so don't add it in the file download * Select similarity plot for exports * Remove temporary test button and diverse stream.debug and console.log statements * Add exporter modal and FAB to disease and correlation WF Add optional chaining to input data that is not available in all WFs Add config object to exporter creation so settings can be passed, ie. currently only the plot name * Add plot name for exporter and merge with sources instead of separate variable Adding config into the sources follows our/cycle.js style much better * Move creation of export modal lines into separate function that is reused * Add config field in config for the signature button to be disabled or hidden Intended values are "", ".hide", ".disabled" * add some basic documentation * Add file download data types in model instead of in view * Add wave effect on copy/download buttons This effect is also used in the table for the download buttons * Fix downloads messing up the router. Has something to do with clicking a 'a' directly make the i child the full size of the a parent to capture the direct click * Add extra explanation of the workaround remove all padding of the a element instead of only left & right * use numTableHead and numTableTail parameters in search query to set default entries in table Sets the start value, the user can still change the value later, so functionally should and does act as an alternative default value from the settings * Initialize FAB once vdom$ launches vdom Add extra 1ms delay so vdom$ can propagate to DOM before calling FAB init * Add unit test for search queries in filters Simple test with changed values, unchanged values & an invalid option Check both 'output' and 'filter_output' * Add autorun to search query by default However, only add '?' or autorun when a state is available, otherwise just link base link address * Hide pdf report option for now it's never available, so don't show the button * Add option to update FAB when signature becomes available, add waves effect Added waves effect on FAB to indicate action * Hide signature FAB shortcut for disease and correlation WF * WIP rewrite of clipboard to return permissions and results * Add sender in clipboard object add undefined type as meaning raw text to be copied split class identifiers of elements in fab and modal (url and signature) let modal use clipboard objects with sender * Display clipboard results in modal For now just make button green or red for 2 seconds * Always require objects to be passed to clipboard driver Rewrite poc driver to not use shameful methods but instead create a nested provider & listener (thanks Toni!) * Give DOM more time before initializing FAB 1ms delay is not guaranteed to be enough. so far 50ms seems fine. * rename clipboardDriver -> makeClipboardDriver * Add clipboard result to FAB * keep FAB open when updating it FAB should be destroyed during update instead of closed, fixes most of behavior issues. Still does some animation but at least the overall state is correct now * Provide optional stream sink for clipboard driver Allows clipboard driver to be used without a result listener and instead provides a void sink for the result stream * if search query, reload page without search query but store search query in router state This removes the ...?xxx from the browser URL which prevents users making changes to the page and believing the URL can be copied and to have a valid state However this introduces an additional issue that page refreshes or clicking on the same WF still starts the same search query as it stays in the router state Handle this by only converting the data when the router type is set to 'push'. When we push to the state, the router state is set to 'push' but when the user refreshes/clicks same WF link the type is undefined * Version bump to 5.1.0-alpha1 * Add filter in correlation WF (#126) * Gskcmp 20 sorting sample selection table (#121) * Start making SampleSelection sortable Clicking the header triggers sort selection and direction Sort selection and direction is displayed in the header No actual sorting is happening for now * Use button in header and add sort icon on hover Use button as it is provides mouse cursor changes Add sort icon when hovering over the title to indicate functionality * Sort data when header clicked match header id to entry field names * Prevent header updates while loading Prevents updates on loaded vdom stream which would otherwise replace the loading vdom move icon name logic to separate const to make code clearer * Fix header buttons in place Keep the text centered with the icons next to it Force text and icons on the same line on small screens * Gskcmp 86 auto complete doesnt work with single result while still not complete (#114) * Update the AutoComplete even if there is only 1 result Prevents an incomplete entry getting no results However, now requires the user to type out the full or click the suggestion * Don't feed autofeed when the input matches the single AutoComplete data This restores the original intended functionality * Clear AutoComplete when input completely matches single entry When the user types the full name, close the AutoComplete instead of wanting the user to confirm their only option * Truncate dose when text is too long When dose isn't a number or the dose unit is long, truncate the whole string * Version bump to 5.0.1 * Don't allow 'Use' column to be sorted * Sort dose & time numerically These examples are typical issues 2 < 10 0.02 < 0.4 * Fix truncation with long dose unit names add missing .length * Gskcmp 89 ghost mode leaves page in dirty state (#119) * Let ghost mode use router params functionality Change treatments scenario as proof of concept: - all selecting is done by setting the router params - set continue functions - remove old functionality * Update disease WF ghost mode Still a bit WIP. Setting of filters etc needs some attention, currently doesn't use the router params system * Basic update for ghost mode in correlation WF * Don't allow search queries when ghost mode is enabled Don't apply the state to the routerInformation.params Still clear the search string from the browser url * Move ghost mode strings & settings to deployments too Change scenario logic for disease and correlation WFs to same style as genericTreatment as it is now very similar * Update CHANGELOG.md * Add documentation for v5.0.1 * Delay loading vdom by 100ms (#128) State can glitch, especially when using the search queries. This causes loading vdom to be output and directly being overwritten by init vdom * Remove debug statements from clipboard driver and scenario logic * Add TRT_OE as possible treatment to be displayed in the top tables (#129) Copied from trt_sh * Allow filter values API call coming in later than the input trigger (#134) In disease & correlation WFs the input trigger comes instantaneously but the API for filter values is still under its way. This means that a regular samplecombine doesn't output the values first combine input trigger and filter values and then samplecombine the search values * Catch error thrown in permission query under Firefox (#136) Firefox doesn't implement 'clipboard-write' but also doesn't allow querying it Catch the exception that gets thrown. Co-authored-by: Toni Verbeiren --- CHANGELOG.md | 33 +- deployments.json | 43 +- package.json | 2 +- src/js/components/CorrelationForm.js | 50 ++- src/js/components/Exporter.js | 423 ++++++++++++++++++ src/js/components/Filter.js | 61 ++- src/js/components/SampleSelection.js | 174 ++++++- src/js/components/SampleTable/SampleInfo.js | 42 ++ src/js/components/SignatureCheck.js | 64 ++- src/js/components/SignatureForm.js | 33 +- src/js/components/Table.js | 62 ++- src/js/components/TreatmentCheck.js | 48 +- src/js/drivers/makeClipboardDriver.js | 88 ++++ .../drivers/makeFloatingActionButtonDriver.js | 75 ++++ src/js/drivers/makeVegaDriver.js | 3 +- src/js/index.js | 79 +++- src/js/main.js | 10 +- src/js/pages/correlation.js | 83 ++-- src/js/pages/disease.js | 29 +- src/js/pages/genericTreatment.js | 37 +- src/js/scenarios/correlationScenario.js | 78 ++++ src/js/scenarios/diseaseScenario.js | 95 +--- src/js/scenarios/treatmentScenario.js | 69 +-- src/js/utils/scenario.js | 29 +- src/js/utils/searchUtils.js | 78 ++++ src/{js => sass}/_compass_svg.scss | 0 src/sass/_exporter.scss | 79 ++++ src/{js => sass}/_main_custom.scss | 7 + src/{js => sass}/_variables.scss | 0 src/{js => sass}/main.scss | 6 + src/test/FilterTest.js | 68 ++- 31 files changed, 1733 insertions(+), 215 deletions(-) create mode 100644 src/js/components/Exporter.js create mode 100644 src/js/drivers/makeClipboardDriver.js create mode 100644 src/js/drivers/makeFloatingActionButtonDriver.js create mode 100644 src/js/scenarios/correlationScenario.js create mode 100644 src/js/utils/searchUtils.js rename src/{js => sass}/_compass_svg.scss (100%) create mode 100644 src/sass/_exporter.scss rename src/{js => sass}/_main_custom.scss (61%) rename src/{js => sass}/_variables.scss (100%) rename src/{js => sass}/main.scss (98%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 134d5ef2..92b07b78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,45 @@ # CHANGELOG +## Version 5.1.0 + +### Functionality + +- URL Queries: ComPass accepts URL queries and runs the complete analysis automatically. For instance, the following URI: + + ``` + http://localhost:3000/disease?autorun&signature=HSPA1A+DNAJB1+DDIT4+-TSEN2&numTableHead=10 + ``` + + will run the Disease workflow using the signature `HSPA1A DNAJB1 DDIT4` and will show 10 entries for the top table. + +- Export: Add buttons to export data and plots to a file or copy it to the clipboard. A popup window is available with all the possible export features on the right bottom of the screen. + +- Correlation workflow has filters. + +- Sample tables can be sorted by clicking the columns header + +### Other + +- Ghost mode has been improved, it no longer requires explicit timings in the scenarios + ## Version 5.0.2 - Fix mistake in sample selection column truncation code -## Version 5.0.0-alpha4 +## Version 5.0.1 + +- The head and tail tables use pictures to display the treatment type +- Adapt the behaviour for the autocomplete when there is only one option to choose from and if the entered value matches the single autocomplete value + +## Version 5.0.0 -## Functionality +### Functionality - A new 'Genetic' workflow is created useful for searching for genetic perturbations. - Inverting the filter selection can be done using the `ALT` (Option on Mac) modifier key instead of the `a` key - Filter values are populated dynamically based on the data available through the API -## Other +### Other - The dependency stack has been cleaned and (partly) updated - Cycle dependencies have been updated to the latest versions except for `@cycle/state`. diff --git a/deployments.json b/deployments.json index 225d840c..d8427629 100644 --- a/deployments.json +++ b/deployments.json @@ -22,22 +22,41 @@ }, "ghost": { "compound": { - "treatment": "BRD-K93645900", - "index": 0, - "sample": ["A375_6H_BRD_K93645900_10"], - "signature": "-CRCP" + "params": { + "typer": 500, + "treatment": "BRD-K93645900", + "samples": "A375_6H_BRD_K93645900_10", + "trtType": "trt_cp" + } }, "ligand": { - "treatment": "ANGPT1", - "index": 0, - "sample": ["HCC515_4H_CMAP_CYT_SRP3007_50"], - "signature": "FBXO11 SLC35 F5BAG4 RIF1" + "params": { + "typer": 500, + "treatment": "ANGPT1", + "samples": "HCC515_4H_CMAP_CYT_SRP3007_50", + "trtType": "trt_cp" + } }, "genetic": { - "treatment": "PARP2", - "index": 0, - "sample": ["DER001_HCC515_96H_X1_F1B6_DUO52HI53LO:B03"], - "signature": "-COPS7A -KEAP1 -CCNB1 -HMG20B -ELOVL6 -DERA LPL -PLEKHJ1 -TPI1 ST3GAL5 -HAT1 -MLF2 -FOXG1 -STRAP -MRPL4 -EDN1 -PON2 KMT2A FRMD4A -C16orf58" + "params": { + "typer": 500, + "treatment": "PARP2", + "samples": "DER001_HCC515_96H_X1_F1B6_DUO52HI53LO", + "trtType": "trt_cp" + } + }, + "disease": { + "params": { + "typer": 100, + "signature": "HSPA1A DNAJB1 DDIT4 -TSEN2" + } + }, + "correlation": { + "params": { + "typer": 100, + "signature1": "HSPA1A DNAJB1 DDIT4 -TSEN2", + "signature2": "DNAJB1 DDIT4 -TSEN2" + } } }, "modelTranslations": [ diff --git a/package.json b/package.json index b4e0740e..22efa6d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "LuciusWeb", - "version": "5.0.2", + "version": "5.1.0", "description": "Web interface for ComPass aka Lucius", "repository": { "type": "git", diff --git a/src/js/components/CorrelationForm.js b/src/js/components/CorrelationForm.js index e540b7d6..b005ff23 100644 --- a/src/js/components/CorrelationForm.js +++ b/src/js/components/CorrelationForm.js @@ -8,6 +8,7 @@ import { ENTER_KEYCODE } from '../utils/keycodes.js' import { SignatureCheck, checkLens1, checkLens2 } from '../components/SignatureCheck' import dropRepeats from 'xstream/extra/dropRepeats' import { loggerFactory } from '../utils/logger' +import { typer } from '../utils/searchUtils' const stateTemplate = { form: { @@ -21,8 +22,29 @@ const stateTemplate = { // Granular access to global state and parts of settings const formLens = { - get: state => ({ core: state.form, settings: { form: state.settings.form, api: state.settings.api } }), - set: (state, childState) => ({ ...state, form: childState.core }) + get: state => ({ + core: state.form, + settings: { + form: state.settings.form, + api: state.settings.api + }, + search1: state.routerInformation.params?.signature1, + search2: state.routerInformation.params?.signature2, + searchAutoRun: state.routerInformation.params?.autorun, + searchTyper: state.routerInformation.params?.typer, + }), + set: (state, childState) => ({ + ...state, + form: childState.core, + routerInformation: { + ...state.routerInformation, + pageState: { + ...state.routerInformation.pageState, + signature1: childState.core.query1, + signature2: childState.core.query2, + } + } + }) }; function CorrelationForm(sources) { @@ -79,15 +101,20 @@ function CorrelationForm(sources) { }); // Update in query, or simply ENTER + const typer1$ = typer(state$, 'search1', 'searchTyper') const newQuery1$ = xs.merge( sources.DOM.select('.Query1').events('input').map(ev => ev.target.value), // Ghost - state$.map(state => state.core.query1).compose(dropRepeats()) + state$.map(state => state.core.query1).compose(dropRepeats()), + typer1$, ) + + const typer2$ = typer(state$, 'search2', 'searchTyper') const newQuery2$ = xs.merge( sources.DOM.select('.Query2').events('input').map(ev => ev.target.value), // Ghost - state$.map(state => state.core.query2).compose(dropRepeats()) + state$.map(state => state.core.query2).compose(dropRepeats()), + typer2$, ) // Updated state is propagated and picked up by the necessary components @@ -187,12 +214,25 @@ function CorrelationForm(sources) { const childReducer1$ = signatureCheck1.onion const childReducer2$ = signatureCheck2.onion + // Auto start query + // Only run once, even if query is changed and then reverted to original value + const searchAutoRun$ = state$ + .filter( + (state) => state.searchAutoRun == "" || state.searchAutoRun == "yes" + ) + .filter((state) => state.search1 == state.core.query1 && state.search2 == state.core.query2) + .filter((state) => state.core.validated1 == true && state.core.validated2 == true) + .mapTo(true) + .compose(dropRepeats(equals)) + + // When GO clicked or enter -> send updated 'value' to sink // Maybe catch when no valid query? const query$ = xs.merge( update$, // Ghost mode - sources.onion.state$.map(state => state.core.ghost).filter(ghost => ghost).compose(dropRepeats()) + sources.onion.state$.map(state => state.core.ghost).filter(ghost => ghost).compose(dropRepeats()), + searchAutoRun$, ) .compose(sampleCombine(state$)) .map(([update, state]) => ({query1: state.core.query1, query2: state.core.query2})) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js new file mode 100644 index 00000000..e64728c9 --- /dev/null +++ b/src/js/components/Exporter.js @@ -0,0 +1,423 @@ +import xs from "xstream" +import { div, i, ul, li, p, input, button, span, a } from "@cycle/dom" +import { isEmpty, mergeLeft, equals } from "ramda" +import { loggerFactory } from "../utils/logger" +import delay from "xstream/extra/delay" +import debounce from "xstream/extra/debounce" +import sampleCombine from "xstream/extra/sampleCombine" +import dropRepeats from "xstream/extra/dropRepeats" +import { convertToCSV } from "../utils/export" + +/** + * @module components/Exporter + */ + +/** + * Get triggers from button presses in the DOM + * @function intent + * @param {*} domSource$ + * @returns object containing trigger streams + */ +function intent(domSource$) { + const exportLinkTriggerFab$ = domSource$.select(".export-clipboard-link-fab").events("click") + const exportSignatureTriggerFab$ = domSource$.select(".export-clipboard-signature-fab").events("click") + + const exportLinkTrigger$ = domSource$.select(".export-clipboard-link").events("click") + const exportSignatureTrigger$ = domSource$.select(".export-clipboard-signature").events("click") + const exportPlotsTrigger$ = domSource$.select(".export-clipboard-plots").events("click") + const exportHeadTableTrigger$ = domSource$.select(".export-clipboard-headTable").events("click") + const exportTailTableTrigger$ = domSource$.select(".export-clipboard-tailTable").events("click") + + const modalTrigger$ = domSource$.select(".modal-open-btn").events("click") + const modalCloseTrigger$ = domSource$.select(".export-close").events("click") + + const testTrigger$ = domSource$.select(".test-btn").events("click") + + return { + exportLinkTriggerFab$: exportLinkTriggerFab$, + exportSignatureTriggerFab$: exportSignatureTriggerFab$, + exportLinkTrigger$: exportLinkTrigger$, + exportSignatureTrigger$: exportSignatureTrigger$, + exportPlotsTrigger$: exportPlotsTrigger$, + exportHeadTableTrigger$: exportHeadTableTrigger$, + exportTailTableTrigger$: exportTailTableTrigger$, + modalTrigger$: modalTrigger$, + modalCloseTrigger$: modalCloseTrigger$, + testTrigger$: testTrigger$, + } +} + +/** + * collect data to be made available for copy/download + * trigger modal to be opened or closed + * send data to clipboard when triggered + * + * @function model + * @param {Object} actions object of trigger streams + * @param {Stream} state$ full state + * @param {Stream} vega$ stream of vega objects, to be filtered + * @param {Object} config configuration object passed from workflow + * @returns Object with + * * reducers$ placeholder for reducers + * * modal$ data to be sent to the modal driver + * * clipboard$ data to be sent to the clipboard driver + * * dataPresent Object with booleans for what data is available + * * exportData Object with data + */ +function model(actions, state$, vega$, config) { + + const openModal$ = actions.modalTrigger$ + .map(_ => ({ el: '#modal-exporter', state: 'open' })) + const closeModal$ = actions.modalCloseTrigger$ + .map(_ => ({ el: '#modal-exporter', state: 'close' })) + + function notEmptyOrUndefined(data) { + return data != undefined && !isEmpty(data) + } + + const signaturePresent$ = state$.map((state) => notEmptyOrUndefined(state.form.signature?.output)).startWith(false) + const plotsPresent$ = state$.map((state) => notEmptyOrUndefined(state.plots.data)).startWith(false) + const headTablePresent$ = state$.map((state) => notEmptyOrUndefined(state.headTable?.data)).startWith(false) + const tailTablePresent$ = state$.map((state) => notEmptyOrUndefined(state.tailTable?.data)).startWith(false) + + const url$ = state$.map((state) => state.routerInformation.pageStateURL).startWith("") + const signature$ = state$.map((state) => state.form.signature?.output).startWith("") + // result already contains 'data:image/png;base64,' + const plotFile$ = vega$ + .filter(vega => vega.el == config.plotId) + .map((vega) => xs.fromPromise(vega.view.toImageURL('png'))) + .flatten() + .startWith("") + + const headTableCsv$ = state$.map((state) => state.headTable?.data) + .filter((data) => notEmptyOrUndefined(data)) + .map((data) => convertToCSV(data)) + .startWith("") + + const tailTableCsv$ = state$.map((state) => state.tailTable?.data) + .filter((data) => notEmptyOrUndefined(data)) + .map((data) => convertToCSV(data)) + .startWith("") + + const urlFile$ = url$.map(url => "data:text/plain;charset=utf-8," + url) + const signatureFile$ = signature$.map(signature => "data:text/plain;charset=utf-8," + signature) + const headTableCsvFile$ = headTableCsv$.map(headTableCsv => "data:text/tsv;charset=utf-8," + encodeURIComponent(headTableCsv)) + const tailTableCsvFile$ = tailTableCsv$.map(tailTableCsv => "data:text/tsv;charset=utf-8," + encodeURIComponent(tailTableCsv)) + + const clipboardLinkFab$ = actions.exportLinkTriggerFab$ + .compose(sampleCombine(url$)) + .map(([_, url]) => ({ + sender: "url-fab", + data: url, + })) + .remember() + + const clipboardSignatureFab$ = actions.exportSignatureTriggerFab$ + .compose(sampleCombine(signature$)) + .map(([_, signature]) => ({ + sender: "signature-fab", + data: signature, + })) + .remember() + + const clipboardLink$ = actions.exportLinkTrigger$ + .compose(sampleCombine(url$)) + .map(([_, url]) => ({ + sender: "url", + data: url, + })) + .remember() + + const clipboardSignature$ = actions.exportSignatureTrigger$ + .compose(sampleCombine(signature$)) + .map(([_, signature]) => ({ + sender: "signature", + data: signature, + })) + .remember() + + const clipboardPlots$ = actions.exportPlotsTrigger$ + .compose(sampleCombine(plotFile$)) + .map(([_, data]) => { + // input data is "data:image/png;base64,abcdef0123456789..." + const parts = data.split(';base64,'); + const imageType = parts[0].split(':')[1]; + const decodedData = window.atob(parts[1]); + const uInt8Array = new Uint8Array(decodedData.length); + for (let i = 0; i < decodedData.length; i++) { + uInt8Array[i] = decodedData.charCodeAt(i); + } + const blob = new Blob([uInt8Array], { type: imageType }) + return { + sender: "plot", + type: imageType, + data: blob, + } + }) + .remember() + + const clipboardHeadTable$ = actions.exportHeadTableTrigger$ + .compose(sampleCombine(headTableCsv$)) + .map(([_, table]) => ({ + sender: "headTable", + data: table, + })) + .remember() + + const clipboardTailTable$ = actions.exportTailTableTrigger$ + .compose(sampleCombine(tailTableCsv$)) + .map(([_, table]) => ({ + sender: "tailTable", + data: table, + })) + .remember() + + const testAction$ = actions.testTrigger$ + .compose(sampleCombine(plotFile$)) + .map(([_, data]) => { + // input data is "data:image/png;base64,abcdef0123456789..." + const parts = data.split(';base64,'); + const imageType = parts[0].split(':')[1]; + const decodedData = window.atob(parts[1]); + const uInt8Array = new Uint8Array(decodedData.length); + for (let i = 0; i < decodedData.length; i++) { + uInt8Array[i] = decodedData.charCodeAt(i); + } + const blob = new Blob([uInt8Array], { type: imageType }) + return { + type: imageType, + data: blob, + } + }) + .remember() + + return { + reducers$: xs.empty(), + modal$: xs.merge(openModal$, closeModal$), + clipboard$: xs.merge(clipboardLinkFab$, clipboardSignatureFab$, clipboardLink$, clipboardSignature$, clipboardPlots$, clipboardHeadTable$, clipboardTailTable$, testAction$), + dataPresent: { + signaturePresent$: signaturePresent$, + plotsPresent$: plotsPresent$, + headTablePresent$: headTablePresent$, + tailTablePresent$: tailTablePresent$, + }, + exportData: { + url$: url$, + urlFile$: urlFile$, + signatureFile$: signatureFile$, + plotFile$: plotFile$, + headTableCsvFile$: headTableCsvFile$, + tailTableCsvFile$: tailTableCsvFile$, + } + } +} + +/** + * @function view + * @param {Stream} state$ full state + * @param {Object} dataPresent object with booleans of what data is available + * @param {Object} exportData object with available data + * @param {Object} config configuration object passed from workflow + * @param {Object} clipboard state of the clipboard driver which gives us our permissions and results + * @returns Vdom div object with nested children for FAB and modal + */ +function view(state$, dataPresent, exportData, config, clipboard) { + + const clipboardResultAutoClear$ = xs + .merge( + clipboard.results$, + clipboard.results$.compose(debounce(2000)).mapTo({}) + ) + .startWith({}) + + const fab$ = xs + .combine( + dataPresent.signaturePresent$, + clipboardResultAutoClear$, + ) + .map(([signaturePresent, clipboardResult]) => { + + const clipboardUrlBtnResult = "url-fab" != clipboardResult?.sender + ? "" + : clipboardResult.state == "success" ? " .success" : " .failure" + + const clipboardSigBtnResult = "signature-fab" != clipboardResult?.sender + ? "" + : clipboardResult.state == "success" ? " .success" : " .failure" + + + const extraSigClass = config.fabSignature != "update" + ? config.fabSignature + : signaturePresent ? "" : " .disabled" + + return div(".fixed-action-btn", [ + span(".btn-floating .btn-large", i(".large .material-icons", "share")), + ul([ + li(span(".btn-floating .export-clipboard-link-fab .waves-effect.waves-light",span(".fab-wrap" + clipboardUrlBtnResult, i(".material-icons", "link")))), + li(span(".btn-floating .export-clipboard-signature-fab .waves-effect.waves-light" + extraSigClass, span(".fab-wrap" + clipboardSigBtnResult, i(".material-icons", "content_copy")))), + // li(span(".btn-floating .export-file-report", i(".material-icons", "picture_as_pdf"))), + li(span(".btn-floating .modal-open-btn .waves-effect.waves-light", span(".test3", i(".material-icons", "open_with")))), + // li(span(".btn-floating .test-btn", i(".material-icons", "star"))), + ]) + ])}) + + + const modal$ = xs + .combine( + dataPresent.signaturePresent$, + dataPresent.plotsPresent$, + dataPresent.headTablePresent$, + dataPresent.tailTablePresent$, + exportData.url$, + exportData.urlFile$, + exportData.signatureFile$, + exportData.plotFile$, + exportData.headTableCsvFile$, + exportData.tailTableCsvFile$, + clipboard.copyImagesPermission$, + clipboardResultAutoClear$, + ) + .map(([ + signaturePresent, + plotsPresent, + headTablePresent, + tailTablePresent, + url, + urlFile, + signatureFile, + plotFile, + headTableCsvFile, + tailTableCsvFile, + clipboardPermissions, + clipboardResult, + ]) => { + + const copyImagesPermission = clipboardPermissions.state == "granted" + + const addExportDiv = (identifier, text, clipboardId, fileData, fileName, available, clipboardAllowed=true, downloadAllowed=true) => { + const availableClipboardText = available && clipboardAllowed ? "" : " .disabled" + const availableDownloadText = available && downloadAllowed ? "" : " .disabled" + + const clipboardBtnResult = identifier != clipboardResult?.sender + ? "" + : clipboardResult.state == "success" ? " .success" : " .failure" + + // Styling should prevent the user to click the 'a' directly; this causes the page div#root to be corrupted. + // Work around is to make the internal 'i' the full size of the 'a' thus "catching" the initial click. + // Exact reason is not 100% clear. Using preventDefault doesn't seem to work. + // + // At the time of writing, the impression is that it could have to do with the exporter or sub-parts not being isolated. + // Debugging suggest the @cycle/dom to be the culprit. + // Removing the 'div#Root' -> 'fromEvent.js:16' event listener prevents the page from misbehaving. + // Workaround is done in '.paddingfix' in the scss. + return div(".row", [ + span(".col .s6 .push-s1", text), + span(".btn .col .s1 .offset-s1 .waves-effect .waves-light " + clipboardId + " " + availableClipboardText + clipboardBtnResult, i(".material-icons", "content_copy")), + a(".btn .col .s1 .offset-s1 .waves-effect .waves-light .paddingfix " + availableDownloadText, + { + props: { + href: fileData, + download: fileName, + }, + }, + i(".material-icons", "file_download"), + ) + ]) + } + + return div([ + div("#modal-exporter.modal", [ + div(".modal-content", [ + div(".row .title", [ + p(".col .s12", "Export to clipboard or file"), + //p(".col .s12", "" + clipboardResult.text), + ]), + addExportDiv("url", "Create link to this page's state", ".export-clipboard-link", urlFile, "url.txt", true), + addExportDiv("signature", "Copy signature", ".export-clipboard-signature", signatureFile, "signature.txt", signaturePresent), + addExportDiv("plot", "Copy " + config.plotName + " plot", ".export-clipboard-plots", plotFile, "plot.png", plotsPresent, copyImagesPermission), + addExportDiv("headTable", "Copy top table", ".export-clipboard-headTable", headTableCsvFile, "table.tsv", headTablePresent), + addExportDiv("tailTable", "Copy bottom table", ".export-clipboard-tailTable", tailTableCsvFile, "table.tsv", tailTablePresent), + // div(".row", [ + // span(".col .s6 .push-s1", "Export report"), + // span(".btn .col .s1 .offset-s3 .export-file-report .disabled", i(".material-icons", "file_download")), + // ]), + ]), + div(".modal-footer", [ + button(".export-close .col .s8 .push-s2 .btn", "Close"), + //div(".col .s12 .blue.lighten-3", {style: {wordWrap: "break-word"}}, url), + ]), + ]), + ]) + }) + .startWith(div("#modal-exporter.modal", "empty")) + + const vdom$ = xs.combine( + fab$, + modal$ + ) + .map(([fab, modal]) => (div([fab, modal]))) + + return vdom$ +} + + + +function Exporter(sources) { + + const logger = loggerFactory( + "exporter", + sources.onion.state$, + "settings.common.debug" + ) + + const defaultConfig = { + plotId: "#simplot", // id of the div passed to vega + plotName: "binned similarity", // part of the text to be displayed for plot copy/download + fabSignature: "update", // part of FAB class name, set to "", ".hide" or ".disabled". + //"update" sets ".disabled" when the signature is not available and updates the FAB when it becomes available + } + const fullConfig = mergeLeft(sources.config, defaultConfig) + + const state$ = sources.onion.state$ + + const actions = intent(sources.DOM) + + const model_ = model(actions, state$, sources.vega, fullConfig) + + const vdom$ = view(state$, model_.dataPresent, model_.exportData, fullConfig, sources.clipboard) + + const fabInit$ = vdom$.mapTo({ + state: "init", + element: ".fixed-action-btn", + options: { + direction: "top", + // hoverEnabled: false, + } + }) + .compose(dropRepeats(equals)) // run just once + .compose(delay(50)) // let the vdom propagate first and next cycle initialize FAB + + const fabUpdate$ = model_.dataPresent.signaturePresent$ + .filter(_ => fullConfig.fabSignature == "update") + .compose(dropRepeats(equals)) + .mapTo({ + state: "update", + element: ".fixed-action-btn", + options: { + direction: "top", + // hoverEnabled: false, + } + }) + .compose(delay(50)) // let the vdom propagate first and next cycle update FAB + + return { + log: xs.merge(logger(state$, "state$")), + DOM: vdom$, + onion: model_.reducers$, + fab: xs.merge(fabInit$, fabUpdate$), + modal: model_.modal$, + clipboard: model_.clipboard$, + } +} + +export {Exporter} diff --git a/src/js/components/Filter.js b/src/js/components/Filter.js index a2910e10..6f936fe2 100644 --- a/src/js/components/Filter.js +++ b/src/js/components/Filter.js @@ -30,6 +30,11 @@ export const filterLens = { get: (state) => ({ core: state.filter, settings: { filter: state.settings.filter, api: state.settings.api }, + search: { + dose: state.routerInformation.params?.dose, + cell: state.routerInformation.params?.cell, + trtType: state.routerInformation.params?.trtType, + } }), set: (state, childState) => ({ ...state, @@ -38,6 +43,15 @@ export const filterLens = { ...state.settings, filter: childState.settings.filter, }, + routerInformation: { + ...state.routerInformation, + pageState: { + ...state.routerInformation.pageState, + dose: childState.core.filter_output?.dose?.join(), + cell: childState.core.filter_output?.cell?.join(), + trtType: childState.core.filter_output?.trtType?.join(), + } + } }), } @@ -199,7 +213,7 @@ function intent(domSource$) { * @param {Stream} filterValuesAction$ object where key is filter group (top level; dose, cell, type) and value is which option is being clicked/modified * @param {Stream} modifier$ boolean of modifier key being pressed or not * @param {Stream} filterAction$ object where key is filter group (top level; dose, cell, type) and value is boolean of the group being clicked open or not - * @param {Stream} state$ readback of full state object used for comparing committed state vs current state, if not identical means ui is dirty + * @param {Stream} search$ search query values for filter settings * @returns {Stream} reducers */ export function model( @@ -208,6 +222,7 @@ export function model( filterValuesAction$, modifier$, filterAction$, + search$, ) { /** @@ -354,6 +369,48 @@ export function model( core: { ...prevState.core, filter_output: minimizeFilterOutput(prevState) }, })) + /** + * Set filter values during page load when search query contains filter values + * @const model/searchReducer$ + * @type {Reducer} + */ + const searchReducer$ = xs.combine(input$, possibleValues$).compose(sampleCombine(search$)) + .map(([[_, possibleValues], search]) => { + + const matchedFilters = (searchValue, possibleValues) => { + const values = searchValue.split(',') + return values.filter(v => possibleValues.includes(v)) + } + + const matchedDoses = search.dose == undefined ? undefined : matchedFilters(search.dose, possibleValues.dose) + const matchedCells = search.cell == undefined ? undefined : matchedFilters(search.cell, possibleValues.cell) + const matchedTypes = search.trtType == undefined ? undefined : matchedFilters(search.trtType, possibleValues.trtType) + return { + dose: matchedDoses, + cell: matchedCells, + trtType: matchedTypes, + } + }) + .filter((output) => (output.dose != undefined || output.cell != undefined || output.trtType != undefined)) // Only set filter if filter values are set + .compose(dropRepeats(equals)) // only do this once. Changes in the WF should not be overwritten + .map((output) => (prevState) => { + const filter_output = minimizeFilterOutput({ + ...prevState, + core: { + ...prevState.core, + output: output, + } + }) + return { + ...prevState, + core: { + ...prevState.core, + output: output, + filter_output: filter_output, + } + } + }) + /** * Dirty state reducer, custom version than in ui.js as more logic is required and otherwise need to loop state$ back into model * Uses value change or opening/closing of the filters and compares current state with committed state @@ -377,6 +434,7 @@ export function model( possibleValuesReducer$, filterReducer$, toggleReducer$, + searchReducer$, outputReducer$, dirtyReducer$, ) @@ -583,6 +641,7 @@ function Filter(sources) { actions.filterValuesAction$, actions.modifier$, actions.filterAction$, + state$.map((state) => (state.search)), ) const outputTrigger$ = diff --git a/src/js/components/SampleSelection.js b/src/js/components/SampleSelection.js index 56db3462..6dde3b9f 100644 --- a/src/js/components/SampleSelection.js +++ b/src/js/components/SampleSelection.js @@ -12,8 +12,9 @@ import { thead, tbody, p, + i, } from "@cycle/dom" -import { clone, equals, merge } from "ramda" +import { clone, equals, merge, sortWith, prop, ascend, descend } from "ramda" import xs from "xstream" import dropRepeats from "xstream/extra/dropRepeats" import debounce from 'xstream/extra/debounce' @@ -35,11 +36,17 @@ const sampleSelectionLens = { core: typeof state.form !== "undefined" ? state.form.sampleSelection : {}, settings: state.settings, ui: (state.ui??{}).sampleSelection ?? {dirty: false}, // Get state.ui.sampleSelection in a safe way or else get a default + search: state.params?.samples, + searchAutoRun: state.params?.autorun, }), // get: state => ({core: state.form.sampleSelection, settings: state.settings}), set: (state, childState) => ({ ...state, form: { ...state.form, sampleSelection: childState.core}, + pageState: { + ...state.pageState, + samples: childState.core.output?.join(), + } }), } @@ -145,7 +152,34 @@ function SampleSelection(sources) { : {} const selectedClass = (selected) => selected ? ".sampleSelected" : ".sampleDeselected" - let rows = data.map((entry) => [ + + function propSort(prop, descend) { + return (a, b) => { + const aValue = a[prop] + const bValue = b[prop] + const multi = descend ? -1 : 1 + + if (isNaN(aValue) || isNaN(bValue)) + // works properly for integers but not for decimal numbers, so only use it as fallback + return aValue.localeCompare(bValue, undefined, {numeric: true}) + else + return (Number(aValue) - Number(bValue) > 0 ? 1 : -1) * multi + } + } + + const dataSortAscend = sortWith([ + ascend(prop(state.core.sort)), + ]); + const dataSortDescend = sortWith([ + descend(prop(state.core.sort)), + ]); + const sortedData = state.core.sort !== "" + ? state.core.sort == "dose" || state.core.sort == "time" + ? sortWith([propSort(state.core.sort, state.core.direction)])(data) + : state.core.direction ? dataSortDescend(data) : dataSortAscend(data) + : data + + let rows = sortedData.map((entry) => [ td(".selection", { props: { id: entry.id } }, [ label("", { props: { id: entry.id } }, [ input( @@ -188,17 +222,61 @@ function SampleSelection(sources) { td(selectedClass(entry.use), entry.time !== "N/A" ? entry.time + " " + entry.time_unit : entry.time), td(selectedClass(entry.use), entry.significantGenes), ]) + + const sortableHeaderEntry = (id, text, state, sortable=true) => + { + const currentSortId = state.core.sort + const sortDirection = state.core.direction + const hover = state.core.sortHover === id + const loaded = state.core.data.length > 0 + + const sortIcon = + id === currentSortId ? + sortDirection ? "arrow_upward" : "arrow_downward" : + hover ? "sort" : "" + + return th( + button( + ".btn-flat" + (loaded && sortable ? " .sortable" : ""), + { + style: { + whiteSpace: "nowrap", + "margin-bottom": "0px", + "margin-top": "0px", + "vertical-align": "middle", + }, + props: { + id: id, + } + }, + [ + span( + { + style: { + "vertical-align": "top", + fontSize: "1em", + fontWeight: "bold", + textTransform: "none", + paddingLeft: "1.5em", + }, + }, + text + ), + i(".material-icons", {style: {width: "1.5em"}}, sortIcon) + ] + ) + ) + } + const header = tr([ - th("Use?"), - th(safeModelToUi("id", state.settings.common.modelTranslations)), - th("Name"), - th("Sample"), - th("Cell"), - th("Dose"), - // th("Batch"), - // th("Year"), - th("Time"), - th("Sign. Genes"), + sortableHeaderEntry("use", "Use?", state, false), + sortableHeaderEntry("trt_id", safeModelToUi("id", state.settings.common.modelTranslations), state), + sortableHeaderEntry("trt_name", "Name", state), + sortableHeaderEntry("id", "Sample", state), + sortableHeaderEntry("cell", "Cell", state), + sortableHeaderEntry("dose", "Dose", state), + sortableHeaderEntry("time", "Time", state), + sortableHeaderEntry("significantGenes", "Sign. Genes", state), ]) let body = [] @@ -283,6 +361,20 @@ function SampleSelection(sources) { const a$ = xs.merge(aDown$, aUp$).compose(dropRepeats(equals)).startWith(false) + const sortClick$ = sources.DOM.select(".sortable") + .events("click") + .map((ev) => ev.ownerTarget.id) + .startWith("") + + const sortHover$ = sources.DOM.select(".sortable") + .events("mouseenter") + .map((ev) => ev.ownerTarget.id) + .startWith("") + + const sortLeave$ = sources.DOM.select(".sortable") + .events("mouseleave") + .mapTo("") + const selectReducer$ = useClick$ .compose(sampleCombine(a$)) .map(([id, a]) => (prevState) => { @@ -323,6 +415,39 @@ function SampleSelection(sources) { } }) + const autoSelect$ = data$.compose(sampleCombine(state$)) + .filter(([_, state]) => (state.search != undefined)) + .mapTo(true) + .compose(dropRepeats(equals)) + + const autoSelectReducer$ = autoSelect$ + .map((_) => (prevState) => { + const samplesToUse = prevState.search.split(",") + + const newData = prevState.core.data.map((el) => { + // One sample object + var newEl = clone(el) + const use = samplesToUse.includes(el.id)//id === el.id + newEl.use = use + // console.log(el) + // console.log(newEl) + return newEl + }) + return { + ...prevState, + core: { + ...prevState.core, + data: newData, + output: newData.filter((x) => x.use).map((x) => x.id), + }, + } + }) + + const autoRun$ = autoSelectReducer$.compose(sampleCombine(state$)) + .filter(([_, state]) => state.searchAutoRun == "" || state.searchAutoRun == "yes") + .mapTo(true) + .compose(dropRepeats(equals)) + const defaultReducer$ = xs.of((prevState) => ({ ...prevState, core: { input: "", data: [] }, @@ -336,6 +461,25 @@ function SampleSelection(sources) { core: { ...prevState.core, request: req }, })) + const sortReducer$ = sortClick$.map((sort) => (prevState) => ({ + ...prevState, + core: { + ...prevState.core, + sort: sort, + direction: (sort != prevState.core.sort ? false : !prevState.core.direction) + } + })) + + const hoverReducer$ = xs.merge(sortHover$, sortLeave$) + .map((hover) => (prevState) => ({ + ...prevState, + core: { + ...prevState.core, + sortHover: hover, + } + })) + + const sampleSelection$ = xs .merge( sources.DOM.select(".doSelect").events("click"), @@ -343,7 +487,8 @@ function SampleSelection(sources) { sources.onion.state$ .map((state) => state.core.ghostoutput) .filter((ghost) => ghost) - .compose(dropRepeats()) + .compose(dropRepeats()), + autoRun$, ) .compose(sampleCombine(state$)) .map(([ev, state]) => state.core.output) @@ -364,7 +509,10 @@ function SampleSelection(sources) { requestReducer$, dataReducer$, selectReducer$, + autoSelectReducer$, busyReducer$, + sortReducer$, + hoverReducer$, dirtyReducer$, ), output: sampleSelection$, diff --git a/src/js/components/SampleTable/SampleInfo.js b/src/js/components/SampleTable/SampleInfo.js index a85759af..eb5e54ee 100644 --- a/src/js/components/SampleTable/SampleInfo.js +++ b/src/js/components/SampleTable/SampleInfo.js @@ -232,6 +232,31 @@ export function SampleInfo(sources) { div(".col .s8", { style: blur }, [sample.trt_name]) ]) ]), + trt_oe: div(".row", { style: { fontWeight: "small" } }, [ + div(".valign-wrapper", [ + div(".col .s2 .l1 .left-align", { style: { fontWeight: "bold" } }, [ + zhangRounded, + ]), + + div(".col .l2 .hide-on-med-and-down .truncate", [sample.id]), + div(".col .l1 .hide-on-med-and-down", [sample.cell]), + div(".col .l2 .hide-on-med-and-down .truncate", { style: blur }, [ sample.trt_id != "NA" ? sample.trt_id : "" ]), + div(".col .l3 .hide-on-med-and-down", { style: blur }, [sample.trt_name]), + + div(".col .s2 .offset-s5 .l1", { style: blur }, imgForTrtPart), + div(".col .s3 .l2 .center-align", { style: blur }, visualizeTextPart), + ]), + div(".hide-on-large-only", {style: {paddingTop: "10px"}}, [ + div(".col .s4 .m3 .offset-m1", {style: {whiteSpace: "nowrap"}}, ["Sample ID"]), + div(".col .s8 .truncate", [sample.id]), + div(".col .s4 .m3 .offset-m1", ["Cell"]), + div(".col .s8", [sample.cell]), + div(".col .s4 .m3 .offset-m1", {style: {whiteSpace: "nowrap"}}, ["Treatment ID"]), + div(".col .s8 .truncate", { style: blur }, [ sample.trt_id != "NA" ? sample.trt_id : "" ]), + div(".col .s4 .m3 .offset-m1", {style: {whiteSpace: "nowrap"}}, ["Treatment Name"]), + div(".col .s8", { style: blur }, [sample.trt_name]) + ]) + ]), trt_lig: div(".row", { style: { fontWeight: "small" } }, [ div(".valign-wrapper", [ div(".col .s2 .l1 .left-align", { style: { fontWeight: "bold" } }, [ @@ -400,6 +425,23 @@ export function SampleInfo(sources) { ) ), ]), + trt_oe: div([ + div(".row", [ + div(".col .s12 .m6 .l4", { style: { margin: "15px 0px 0px 0px" } }, samplePart), + div(".col .s12 .m6 .l4", { style: { margin: "15px 0px 0px 0px" } }, treatmentPart), + div(".col .s12 .m12 .l2 .push-l2 .hide-on-med-and-down .center-align", + { style: merge(blur, { height: "100%", "margin-top": "30px"}) }, + visualizeTextPart + ), + ]), + div( + ".row", + { style: { margin: "15px 0px 0px 0px" } }, + [p(".col .s12.filterHeader", hStyle, "Filter Info:")].concat( + _filters.map((x) => p(pStyle, entrySmall(x.key, x.value))) + ) + ), + ]), trt_lig: div([ div(".row", [ div(".col .s12 .m6 .l4", { style: { margin: "15px 0px 0px 0px" } }, samplePart), diff --git a/src/js/components/SignatureCheck.js b/src/js/components/SignatureCheck.js index 22b2ae28..adfbeb2d 100644 --- a/src/js/components/SignatureCheck.js +++ b/src/js/components/SignatureCheck.js @@ -1,6 +1,6 @@ import xs from 'xstream'; import { p, div, br, label, input, code, table, tr, th, td, b, h2, button, thead, tbody, i, h, hr } from '@cycle/dom'; -import { clone, equals } from 'ramda'; +import { clone, equals, all } from 'ramda'; import sampleCombine from 'xstream/extra/sampleCombine' import {log, logThis} from '../utils/logger' import {ENTER_KEYCODE} from '../utils/keycodes.js' @@ -24,17 +24,38 @@ const stateTemplate = { } const checkLens = { - get: state => ({query: state.core.query, settings: state.settings}), + get: state => ({ + query: state.core.query, + ghostUpdate: state.core.ghostUpdate, + settings: state.settings, + search: state.search, + searchAutoRun: state.searchAutoRun, + validated: state.core.validated + }), set: (state, childState) => ({...state, core : {...state.core, query: childState.query}}) }; const checkLens1 = { - get: state => ({query: state.core.query1, settings: state.settings}), + get: state => ({ + query: state.core.query1, + ghostUpdate: state.core.ghostUpdate1, + settings: state.settings, + search: state.search1, + searchAutoRun: state.searchAutoRun, + validated: state.core.validated1 + }), set: (state, childState) => ({...state, core : {...state.core, query1: childState.query}}) }; const checkLens2 = { - get: state => ({query: state.core.query2, settings: state.settings}), + get: state => ({ + query: state.core.query2, + ghostUpdate: state.core.ghostUpdate2, + settings: state.settings, + search: state.search2, + searchAutoRun: state.searchAutoRun, + validated: state.core.validated2 + }), set: (state, childState) => ({...state, core : {...state.core, query2: childState.query}}) }; @@ -128,14 +149,45 @@ function SignatureCheck(sources) { // Update and Collapse button updates the query and collapses the window const collapseUpdate$ = domSource$.select('.collapseUpdate').events('click'); - const collapseUpdateReducer$ = collapseUpdate$.compose(sampleCombine(data$)) + + // Auto start query if the entered search string is valid + // Only run once, even if query is changed and then reverted to original value + const searchAutoRun$ = data$ + .compose(sampleCombine(state$)) + .filter( + ([_, state]) => + state.searchAutoRun == "" || state.searchAutoRun == "yes" // autorun enabled? + ) + .filter(([data, _]) => all((x) => x.found ?? x.inL1000)(data)) // all entered data is valid? + //.filter(([_, state]) => state.query == state.search) + .filter(([_, state]) => state.validated == false) // not yet validated? + .mapTo(true) + .compose(dropRepeats(equals)) + + const ghostUpdate$ = sources.onion.state$ + .map((state) => state.ghostUpdate) + .filter((ghost) => ghost) + .compose(dropRepeats()) + + const collapseUpdateReducer$ = xs + .merge( + collapseUpdate$, + searchAutoRun$, + ghostUpdate$, + ) + .compose(sampleCombine(data$)) .map(([collapse, data]) => prevState => { return ({...prevState, query : data.map(x => (x.found ?? x.inL1000) ? x.symbol : '').join(" ").replace(/\s\s+/g, ' ').trim()}); }); // The result of this component is an event when valid // XXX: stays true the whole cycle, so maybe tackle this as well!!!! - const validated$ = collapseUpdate$.map(update => true) + const validated$ = xs + .merge( + collapseUpdate$, + searchAutoRun$, + ghostUpdate$, + ).mapTo(true) return { log: xs.merge( diff --git a/src/js/components/SignatureForm.js b/src/js/components/SignatureForm.js index c2c945cf..97a9d870 100644 --- a/src/js/components/SignatureForm.js +++ b/src/js/components/SignatureForm.js @@ -9,6 +9,7 @@ import { SignatureCheck, checkLens } from '../components/SignatureCheck' import dropRepeats from 'xstream/extra/dropRepeats' import { loggerFactory } from '../utils/logger' import { dirtyUiReducer } from '../utils/ui' +import { typer } from '../utils/searchUtils' // Granular access to global state and parts of settings const formLens = { @@ -20,8 +21,21 @@ const formLens = { common: state.settings.common, }, ui: state.ui?.form ?? {}, + search: state.routerInformation.params?.signature, + searchAutoRun: state.routerInformation.params?.autorun, + searchTyper: state.routerInformation.params?.typer, + }), + set: (state, childState) => ({ + ...state, + form: {...childState.core }, + routerInformation: { + ...state.routerInformation, + pageState: { + ...state.routerInformation.pageState, + signature: childState.core.query, + } + } }), - set: (state, childState) => ({ ...state, form: {...childState.core } }), } function model(newQuery$, state$, sources, signatureCheckSink, actions$) { @@ -89,10 +103,22 @@ function model(newQuery$, state$, sources, signatureCheckSink, actions$) { // When update is clicked, update the query. Onionify does the rest const childReducer$ = signatureCheckSink.onion + // Auto start query + // Only run once, even if query is changed and then reverted to original value + const searchAutoRun$ = state$ + .filter( + (state) => state.searchAutoRun == "" || state.searchAutoRun == "yes" + ) + .filter((state) => state.search == state.core.query) + .filter((state) => state.core.validated == true) + .mapTo(true) + .compose(dropRepeats(equals)) + // When GO clicked or enter -> send updated 'value' to sink // Maybe catch when no valid query? const query$ = xs.merge( actions$.update$, + searchAutoRun$, // Ghost mode sources.onion.state$.map(state => state.core.ghost).filter(ghost => ghost).compose(dropRepeats()) ) @@ -174,11 +200,14 @@ function SignatureForm(sources) { const signatureCheckHTTP$ = signatureCheckSink.HTTP; const signatureCheckReducer$ = signatureCheckSink.onion; + const typer$ = typer(state$, 'search', 'searchTyper') + // Update in query, or simply ENTER const newQuery$ = xs.merge( domSource$.select('.Query').events('input').map(ev => ev.target.value), // Ghost - state$.map(state => state.core.query).compose(dropRepeats()) + state$.map(state => state.core.query).compose(dropRepeats()), + typer$, ) const actions$ = intent(domSource$) diff --git a/src/js/components/Table.js b/src/js/components/Table.js index c2d38db6..003c38c0 100644 --- a/src/js/components/Table.js +++ b/src/js/components/Table.js @@ -76,13 +76,24 @@ const headTableLens = { sourire: state.settings.sourire, filter: state.settings.filter, }, - ui: (state.ui??{}).headTable ?? {dirty: false}, // Get state.ui.headTable in a safe way or else get a default + ui: state.ui?.headTable ?? {dirty: false}, // Get state.ui.headTable in a safe way or else get a default + numEntries: state.routerInformation?.params?.numTableHead, }), - set: (state, childState) => ({ + set: (state, childState) => { + // don't add value of numEntres to the pageState if it is the default value + const numEntries = childState.core.numEntries == parseInt(childState.settings.table.count) ? undefined : childState.core.numEntries + return { ...state, headTable: childState.core, settings: { ...state.settings, headTable: childState.settings.table }, - }), + routerInformation: { + ...state.routerInformation, + pageState: { + ...state.routerInformation.pageState, + numTableHead: numEntries, + } + } + }}, } /** @@ -105,13 +116,24 @@ const tailTableLens = { sourire: state.settings.sourire, filter: state.settings.filter, }, - ui: (state.ui??{}).tailTable ?? {dirty: false}, // Get state.ui.tailTable in a safe way or else get a default + ui: state.ui?.tailTable ?? {dirty: false}, // Get state.ui.tailTable in a safe way or else get a default + numEntries: state.routerInformation?.params?.numTableTail, }), - set: (state, childState) => ({ + set: (state, childState) => { + // don't add value of numEntres to the pageState if it is the default value + const numEntries = childState.core.numEntries == parseInt(childState.settings.table.count) ? undefined : childState.core.numEntries + return { ...state, tailTable: childState.core, settings: { ...state.settings, tailTable: childState.settings.table }, - }), + routerInformation: { + ...state.routerInformation, + pageState: { + ...state.routerInformation.pageState, + numTableTail: numEntries, + } + } + }}, } /** @@ -134,7 +156,7 @@ const compoundContainerTableLens = { sourire: state.settings.sourire, filter: state.settings.filter, }, - ui: (state.ui??{}).compoundTable ?? {dirty: false}, // Get state.ui.compoundTable in a safe way or else get a default + ui: state.ui?.compoundTable ?? {dirty: false}, // Get state.ui.compoundTable in a safe way or else get a default }), set: (state, childState) => ({ ...state, @@ -336,11 +358,17 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { * second update 0 5 5 * * pairwise gives us previous and new value but need to make sure that if we only receive 1 value we do get an output, so use .startWith(0) + * + * If a value for the amount of entries was passed through the search query, use that as default, otherwise use the standard default setting * * @const makeTable/Table/defaultAmountToDisplay$ * @type {Stream} */ - const defaultAmountToDisplay$ = state$.map(state => parseInt(state.settings.table.count)) + const defaultAmountToDisplay$ = xs.combine( + state$.map(state => parseInt(state.settings.table.count)), + state$.map(state => state.numEntries), + ) + .map(([default_, searchQuery]) => (isNaN(searchQuery) ? default_ : searchQuery)) .compose(dropRepeats(equals)) .startWith(0) .compose(pairwise) @@ -599,6 +627,7 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { ]) ) .remember() + .compose(delay(100)) /** * Full vdom content once request is received @@ -867,6 +896,20 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { core: { ...prevState.core, expandOptions: yesorno }, })) + /** + * Reducer for passing the amount of entries shown in the table to the search query params object + * @const makeTable/Table/amountOfDisplayedLinesReducer$ + * @type {Reducer} + */ + const amountOfDisplayedLinesReducer$ = amountToDisplay$ + .map((new_) => (prevState) => { + const val = max(1, new_) + return { + ...prevState, + core: { ...prevState.core, numEntries: val} + } + }) + return { DOM: vdom$, HTTP: request$, @@ -875,7 +918,8 @@ function makeTable(tableComponent, tableLens, scope = "scope1") { inputReducer$, requestReducer$, dataReducer$, - switchReducer$ + switchReducer$, + amountOfDisplayedLinesReducer$, ), log: xs.merge( logger(modifiedState$, "state$") diff --git a/src/js/components/TreatmentCheck.js b/src/js/components/TreatmentCheck.js index 788b87b5..c66c488b 100644 --- a/src/js/components/TreatmentCheck.js +++ b/src/js/components/TreatmentCheck.js @@ -6,15 +6,23 @@ import dropRepeats from "xstream/extra/dropRepeats" import debounce from "xstream/extra/debounce" import { loggerFactory } from "../utils/logger" import { dirtyUiReducer } from "../utils/ui" +import { typer } from "../utils/searchUtils" const checkLens = { get: (state) => ({ core: typeof state.form !== "undefined" ? state.form.check : {}, settings: state.settings, + search: state.params?.treatment, + searchAutoRun: state.params?.autorun, + searchTyper: state.params?.typer, }), set: (state, childState) => ({ ...state, form: { ...state.form, check: childState.core }, + pageState: { + ...state.pageState, + treatment: childState.core.input, + } }), } @@ -49,6 +57,9 @@ function TreatmentCheck(sources) { const acInput$ = sources.ac + // Get input from search query, either slowly typed or in one go + const typer$ = typer(state$, "search", "searchTyper") + const input$ = xs.merge( sources.DOM.select(".treatmentQuery") .events("input") @@ -58,7 +69,8 @@ function TreatmentCheck(sources) { state$ .filter((state) => typeof state.core.ghostinput !== "undefined") .map((state) => state.core.input) - .compose(dropRepeats()) + .compose(dropRepeats()), + typer$, ) // When the component should not be shown, including empty signature @@ -266,12 +278,45 @@ function TreatmentCheck(sources) { } }) + /** + * When there is new input and is validated by the API, validate the output + * auto complete only works when there are more than 1 option + * @const TreatmentCheck/fullInputValidationReducer$ + * @type {Reducer} + */ + const fullInputValidationReducer$ = data$.compose(sampleCombine(input$)) + .filter(([data, input]) => data.length == 1 && + data[0].trtId == input + ) + .map(([data, input]) => (prevState) => ({ + ...prevState, + core: { + ...prevState.core, + input: input, + showSuggestions: false, + validated: true, + output: input, + }, + })) + // GO!!! const run$ = sources.DOM.select(".treatmentCheck").events("click") + // Auto start query + // Only run once, even if query is changed and then reverted to original value + const searchAutoRun$ = state$ + .filter( + (state) => state.searchAutoRun == "" || state.searchAutoRun == "yes" + ) + .filter((state) => state.search == state.core.input) + .filter((state) => state.core.validated == true) + .mapTo(true) + .compose(dropRepeats(equals)) + const query$ = xs .merge( run$, + searchAutoRun$, // Ghost mode sources.onion.state$ .map((state) => state.core.ghostoutput) @@ -299,6 +344,7 @@ function TreatmentCheck(sources) { requestReducer$, setDefaultReducer$, autocompleteReducer$, + fullInputValidationReducer$, dirtyReducer$, ), output: query$, diff --git a/src/js/drivers/makeClipboardDriver.js b/src/js/drivers/makeClipboardDriver.js new file mode 100644 index 00000000..eb3273b1 --- /dev/null +++ b/src/js/drivers/makeClipboardDriver.js @@ -0,0 +1,88 @@ +import xs from "xstream" +import flattenConcurrently from "xstream/extra/flattenConcurrently" + +/** + * Pass message objects with members: + * - type: data type to set, use '' or undefined if raw text + * - data: data blob + * - sender: sender id, is added in return value + * @param {Boolean} provideOwnSink when set to true, the driver adds a sink to the result output stream so that the clipboard + * functionality still works even if the output stream is not used + * @returns object with members: + * - state: 'success' or 'failure' + * - sender: sender id from message object + */ +function makeClipboardDriver(provideOwnSink= false) { + // Chrome by default allows clipboard write and implements the permissions API call + // Firefox by default doesn't allow the clipboard write and doesn't implement the permissions API call + // so we're kind of forced to assume that if the API call fails that we won't have the necessary permissions + const queryOpts = { name: "clipboard-write", allowWithoutGesture: false } + const copyImagesPermission$ = xs + .fromPromise( + navigator.permissions.query(queryOpts) + .then((_) => { return _ }) + .catch((_) => { return { state: "error" } }) + ) + .replaceError((_) => xs.of({ state: "error" })) + + function clipboardDriver(stream$) { + const results$ = xs + .create({ + start: (listener) => { + stream$.addListener({ + next: (message) => { + const clipboard$ = + message.type != undefined && message.type != "" + ? (() => { + try { + return xs.fromPromise( + navigator.clipboard.write([ + new ClipboardItem({ + [message.type]: message.data, + }), + ]) + ) + } catch { + return xs.of({ + state: "failure", + sender: message.sender, + }) + } + })() + : xs.fromPromise(navigator.clipboard.writeText(message.data)) + const clipboardMapped$ = clipboard$ + .map((res) => + res == undefined + ? { state: "success", sender: message.sender } + : res + ) + .replaceError((_) => + xs.of({ state: "failure", sender: message.sender }) + ) + listener.next(clipboardMapped$) + }, + error: (e) => { + console.log(e) + }, + }) + }, + stop: () => {}, + }) + + if (provideOwnSink) { + results$.addListener({ + next: () => {}, + error: () => {}, + }) + } + + return { + copyImagesPermission$: copyImagesPermission$, + results$: results$.compose(flattenConcurrently), + } + } + + return clipboardDriver +} + +export { makeClipboardDriver } diff --git a/src/js/drivers/makeFloatingActionButtonDriver.js b/src/js/drivers/makeFloatingActionButtonDriver.js new file mode 100644 index 00000000..64c042c3 --- /dev/null +++ b/src/js/drivers/makeFloatingActionButtonDriver.js @@ -0,0 +1,75 @@ +import * as M from 'materialize-css' + +function makeFloatingActionButtonDriver() { + + var fab = undefined + + // options are: + // Name Type Default Description + // direction String 'top' Direction FAB menu opens. Can be 'top', 'right', 'buttom', 'left' + // hoverEnabled Boolean true If true, FAB menu will open on hover instead of click + // toolbarEnabled Boolean false Enable transit the FAB into a toolbar on click + + function fabDriver(in$) { + + in$.addListener({ + next: (ev) => { + // console.log("FAB" + ev.state) + if (ev.state == 'init') { + if (fab == undefined) { + + const elem = document.querySelector(ev.element) + if (elem == undefined) + console.warn("fabDriver couldn't find element") + else + fab = M.FloatingActionButton.init(elem, ev.options); + } + fab.close() + } + else if (ev.state == 'update') { + const currentlyOpen = fab?.isOpen + const elem = document.querySelector(ev.element) + if (elem == undefined) + console.warn("fabDriver couldn't find element") + else { + fab.destroy() + fab = M.FloatingActionButton.init(elem, ev.options); + if (currentlyOpen && fab != undefined) + fab.open() + } + } + else if (ev.state == 'open') { + + if (fab == undefined) { + + const elem = document.querySelector(ev.element) + fab = M.FloatingActionButton.init(elem, ev.options); + + + // We handle opening of the sidenav ourselves. + // Unless also implemented, this removes swipe-closing support though, which is one option to close the sidenav on mobile. + // Opening sidenav creates an overlay which gets an on-click listener, which is used to close the sidenav + // This is not removed by this call. + // sidenav._removeEventHandlers() + } + + fab.open() + } + else if (ev.state == 'close') { + // can be used to e.g. add a button in the sidenav that closes the sidenav + if (fab != undefined) + fab.close() + } + }, + error: (e) => { + console.error(e) + } + }) + + } + + return fabDriver + +} + +export { makeFloatingActionButtonDriver }; \ No newline at end of file diff --git a/src/js/drivers/makeVegaDriver.js b/src/js/drivers/makeVegaDriver.js index ecaf7132..ad5c2baa 100644 --- a/src/js/drivers/makeVegaDriver.js +++ b/src/js/drivers/makeVegaDriver.js @@ -22,7 +22,8 @@ function makeVegaDriver() { console.error(m) } }) - + + return view$ } return vegaDriver diff --git a/src/js/index.js b/src/js/index.js index e8e63852..2162a2e5 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -22,7 +22,7 @@ import { span, img, } from "@cycle/dom" -import { merge, prop, mergeDeepLeft, mergeDeepRight } from "ramda" +import { merge, prop, mergeDeepLeft, mergeDeepRight, equals, pickBy, identity } from "ramda" import * as R from "ramda" // Workflows @@ -54,6 +54,7 @@ import { correlationSVG, settingsSVG, } from "./svg" +import sampleCombine from "xstream/extra/sampleCombine" export default function Index(sources) { const { router } = sources @@ -408,13 +409,68 @@ export default function Index(sources) { } ) - const routerReducer$ = router.history$.map((router) => (prevState) => { + const ghostModeEnabled$ = state$ + .map((state) => state.settings.common.ghostMode) + + const routerReducer$ = router.history$ + .compose(sampleCombine(ghostModeEnabled$)) + .map(([router, ghostMode]) => (prevState) => { + + function paramsToObject(entries) { + const result = {} + for(const [key, value] of entries) { // each 'entry' is a [key, value] tupple + result[key] = value + } + return result + } + + // we should not apply the same search query values when the page was reloaded + // if user refreshes or clicks same WF link, router.type is not set + // if we push new state, router.type == "push" + // Also, don't allow search queries when ghost mode is enabled + let paramsObject = {} + if (router?.type == "push" && ghostMode != true) { + const params = new URLSearchParams(router.state) + paramsObject = paramsToObject(params) + } + return { ...prevState, - routerInformation: router, + routerInformation: { + ...router, + params: paramsObject, + pageState: {} + } } }) + const ghostModeWarning$ = router.history$ + .compose(sampleCombine(ghostModeEnabled$)) + .filter(([router, ghostMode]) => (router?.type == "push")) + .filter(([router, ghostMode]) => (ghostMode == true)) + .mapTo({ text: 'Search query ignored as Ghost Mode is enabled', duration: 10000 }) + + const pageStateReducer$ = state$.map((state) => state.routerInformation.pageState).compose(dropRepeats(equals)) + .map((state) => (prevState) => { + // Don't output fields with undefined values + const filteredState = pickBy(identity, state) + const url = new URLSearchParams(filteredState) + const urlString = url.toString() + + const fullUrl = urlString.length > 0 + ? location.protocol + "//" + location.host + prevState.routerInformation.pathname + "?autorun&" + url.toString() + : location.protocol + "//" + location.host + prevState.routerInformation.pathname + + return { + ...prevState, + routerInformation: { + ...prevState.routerInformation, + pageStateURL: fullUrl, + } + } + + }) + // Capture link targets and send to router driver const router$ = sources.DOM.select("a") .events("click") @@ -428,6 +484,14 @@ export default function Index(sources) { const history$ = sources.onion.state$.fold((acc, x) => acc.concat([x]), [{}]) + const historyDriver$ = router.history$ + .map((router) => router.search) + .filter((search) => search != undefined && search != "" && search != "?") + .map((search) => ({ + type: 'push', + state: search, + })) + return { log: xs.merge( // logger(page$, 'page$', '>> ', ' > ', ''), @@ -441,6 +505,7 @@ export default function Index(sources) { defaultReducer$, deploymentsReducer$, routerReducer$, + pageStateReducer$, page$.map(prop("onion")).filter(Boolean).flatten() ), DOM: vdom$, @@ -454,16 +519,22 @@ export default function Index(sources) { prevent$, page$.map(prop("preventDefault")).filter(Boolean).flatten() ), - popup: page$.map(prop("popup")).filter(Boolean).flatten(), + popup: xs.merge( + ghostModeWarning$, + page$.map(prop("popup")).filter(Boolean).flatten(), + ), modal: page$.map(prop("modal")).filter(Boolean).flatten(), ac: page$.map(prop("ac")).filter(Boolean).flatten(), sidenav: sidenavEvent$, + fab: page$.map(prop("fab")).filter(Boolean).flatten(), storage: page$.map(prop("storage")).filter(Boolean).flatten(), deployments: page$ .map(prop("deployments")) .filter(Boolean) .flatten() .debug("deployments"), + history: historyDriver$, + clipboard: page$.map(prop("clipboard")).filter(Boolean).flatten(), } } diff --git a/src/js/main.js b/src/js/main.js index 389adb8a..cf255792 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -24,7 +24,9 @@ import switchPath from 'switch-path' import { makeModalDriver } from './drivers/makeModalDriver' import { makeAutocompleteDriver } from './drivers/makeAutocompleteDriver'; import { makeSidenavDriver } from './drivers/makeSidenavDriver'; -import './main.scss' +import { makeFloatingActionButtonDriver } from './drivers/makeFloatingActionButtonDriver'; +import { makeClipboardDriver } from './drivers/makeClipboardDriver' +import '../sass/main.scss' import fromEvent from 'xstream/extra/fromEvent' import xs from 'xstream' @@ -36,7 +38,7 @@ const drivers = { vega: makeVegaDriver(), HTTP: makeHTTPDriver(), // router: makeRouterDriver(captureClicks(makeHistoryDriver()), switchPath), - history: makeHistoryDriver(), + history: makeHistoryDriver({forceRefresh: false}), preventDefault: preventDefaultDriver, alert: alertDriver, storage: storageDriver, @@ -46,8 +48,10 @@ const drivers = { modal: makeModalDriver(), ac: makeAutocompleteDriver(), sidenav: makeSidenavDriver(), + fab: makeFloatingActionButtonDriver(), + clipboard: makeClipboardDriver(), deployments: () => xs.fromPromise(fetch('/deployments.json').then(m => m.json())) }; -let StatifiedMain = onionify(storageify(routerify(Index, switchPath), { key: 'ComPass' })); +let StatifiedMain = onionify(storageify(routerify(Index, switchPath, {omitHistory: false}), { key: 'ComPass' })); run(StatifiedMain, drivers); diff --git a/src/js/pages/correlation.js b/src/js/pages/correlation.js index dfb15a70..984cde5b 100644 --- a/src/js/pages/correlation.js +++ b/src/js/pages/correlation.js @@ -10,12 +10,13 @@ import { CorrelationForm, formLens } from '../components/CorrelationForm' import { CorrelationPlot, correlationPlotsLens } from '../components/BinnedPlots/CorrelationPlot' import { makeTable, headTableLens, tailTableLens } from '../components/Table' import { initSettings } from '../configuration.js' -import { Filter, compoundFilterLens } from '../components/Filter' +import { Filter, filterLens } from '../components/Filter' import { loggerFactory } from '../utils/logger' import { SampleTable, sampleTableLens } from '../components/SampleTable/SampleTable' +import { Exporter } from "../components/Exporter" // Support for ghost mode -import { scenario } from '../scenarios/diseaseScenario' +import { scenario } from '../scenarios/correlationScenario' import { runScenario } from '../utils/scenario' function CorrelationWorkflow(sources) { @@ -25,25 +26,29 @@ function CorrelationWorkflow(sources) { const state$ = sources.onion.state$ // Scenario for ghost mode - const scenarioReducer$ = - sources.onion.state$.take(1) - .filter(state => state.settings.common.ghostMode) - .mapTo(runScenario(scenario).scenarioReducer$) - .flatten() - .startWith(prevState => prevState) - const scenarioPopup$ = - sources.onion.state$.take(1) - .filter(state => state.settings.common.ghostMode) - .mapTo(runScenario(scenario).scenarioPopup$) - .flatten() - .startWith({ text: 'Welcome to Correlation Workflow', duration: 4000 }) + const scenarios$ = sources.onion.state$ + .take(1) + .filter((state) => state.settings.common.ghostMode) + .map(state => runScenario(scenario(state.settings.common.ghost.correlation), state$)) + const scenarioReducer$ = scenarios$.map(s => s.scenarioReducer$) + .flatten() + const scenarioPopup$ = scenarios$.map(s => s.scenarioPopup$) + .flatten() + .startWith({ text: "Welcome to Correlation Workflow", duration: 4000 }) const correlationForm = isolate(CorrelationForm, { onion: formLens })(sources) const queries$ = correlationForm.output + const doubleSignature$ = queries$ + .filter((queries) => (queries.query1 != undefined && queries.query1 != "")) + .filter((queries) => (queries.query2 != undefined && queries.query2 != "")) + .map((queries) => (queries.query1 + " + " + queries.query2)) // Filter Form - // const filterForm = isolate(Filter, { onion: compoundFilterLens })({...sources, input: queries$}) - // const filter$ = filterForm.output.remember() + const filterForm = isolate(Filter, { onion: filterLens })({ + ...sources, + input: doubleSignature$ + }) + const filter$ = filterForm.output.remember() // default Reducer, initialization const defaultReducer$ = xs.of(prevState => { @@ -63,8 +68,18 @@ function CorrelationWorkflow(sources) { }) // Binned Plots Component - const correlationPlot = isolate(CorrelationPlot, { onion: correlationPlotsLens }) - ({...sources, input: queries$.remember() }); + const correlationPlot = isolate(CorrelationPlot, { onion: correlationPlotsLens })({ + ...sources, + input: xs + .combine(queries$, filter$) + .map(([queries, filter]) => + ({ + ...queries, + filter: filter, + }) + ) + .remember() + }); // tables // const headTableContainer = makeTable(SampleTable, sampleTableLens) @@ -76,6 +91,15 @@ function CorrelationWorkflow(sources) { // const tailTable = isolate(tailTableContainer, { onion: tailTableLens }) // ({...sources, input: xs.combine(signature$, filter$).map(([s, f]) => ({ query: s, filter: f })).remember() }); + const exporter = Exporter({ + ...sources, + config: { + plotId: "#corrplot", + plotName: "correlation", + fabSignature: ".hide", + } + }) + const pageStyle = { style: { fontSize: '14px', @@ -88,46 +112,51 @@ function CorrelationWorkflow(sources) { const vdom$ = xs.combine( correlationForm.DOM, - // filterForm.DOM, + filterForm.DOM, correlationPlot.DOM, // headTable.DOM, // tailTable.DOM, // feedback$ + exporter.DOM, ) .map(([ form, - // filter, + filter, plot, // headTable, // tailTable, // feedback + exporter, ]) => div('.row .correlation', { style: { margin: '0px 0px 0px 0px' } }, [ form, div('.col .s10 .offset-s1', pageStyle, [ - // div('.row', [filter]), + div('.row', [filter]), div('.row', [plot]), // div('.row', []), // div('.col .s12', [headTable]), // div('.row', []), // div('.col .s12', [tailTable]), // div('.row', []) - ]) + ]), + exporter, ]) ); return { log: xs.merge( // logger(state$, 'state$'), + exporter.log, ), DOM: vdom$, onion: xs.merge( defaultReducer$, correlationForm.onion, - // filterForm.onion, - correlationPlot.onion, + filterForm.onion, + correlationPlot.onion, // headTable.onion, // tailTable.onion, + exporter.onion, scenarioReducer$ ), vega: xs.merge( @@ -135,11 +164,15 @@ function CorrelationWorkflow(sources) { ), HTTP: xs.merge( correlationForm.HTTP, + filterForm.HTTP, correlationPlot.HTTP, // headTable.HTTP, // tailTable.HTTP ), - popup: scenarioPopup$ + popup: scenarioPopup$, + modal: exporter.modal, + fab: exporter.fab, + clipboard: exporter.clipboard, }; } diff --git a/src/js/pages/disease.js b/src/js/pages/disease.js index 1cd02ded..44baeeac 100644 --- a/src/js/pages/disease.js +++ b/src/js/pages/disease.js @@ -15,6 +15,7 @@ import { SampleTable, sampleTableLens, } from "../components/SampleTable/SampleTable" +import { Exporter } from "../components/Exporter" // Support for ghost mode import { scenario } from "../scenarios/diseaseScenario" @@ -30,16 +31,13 @@ function DiseaseWorkflow(sources) { const state$ = sources.onion.state$ // Scenario for ghost mode - const scenarioReducer$ = sources.onion.state$ + const scenarios$ = sources.onion.state$ .take(1) .filter((state) => state.settings.common.ghostMode) - .mapTo(runScenario(scenario).scenarioReducer$) + .map(state => runScenario(scenario(state.settings.common.ghost.disease), state$)) + const scenarioReducer$ = scenarios$.map(s => s.scenarioReducer$) .flatten() - .startWith((prevState) => prevState) - const scenarioPopup$ = sources.onion.state$ - .take(1) - .filter((state) => state.settings.common.ghostMode) - .mapTo(runScenario(scenario).scenarioPopup$) + const scenarioPopup$ = scenarios$.map(s => s.scenarioPopup$) .flatten() .startWith({ text: "Welcome to Disease Workflow", duration: 4000 }) @@ -189,6 +187,11 @@ function DiseaseWorkflow(sources) { .remember(), }) + const exporter = Exporter({ + ...sources, + config: { fabSignature: ".hide" } + }) + /** * Style object used in div capsulating filter, displayPlots and tables * @const pageStyle @@ -212,6 +215,7 @@ function DiseaseWorkflow(sources) { headTable.DOM, tailTable.DOM, displayPlots$, + exporter.DOM, // feedback$ ) .map( @@ -221,7 +225,8 @@ function DiseaseWorkflow(sources) { plots, headTable, tailTable, - displayPlots, + displayPlots, + exporter, // feedback ]) => div(".row .disease", { style: { margin: "0px 0px 0px 0px" } }, [ @@ -236,6 +241,7 @@ function DiseaseWorkflow(sources) { div(".row", []), div(".row", [displayPlots === "after tables" ? plots : div()]), ]), + exporter, ]) ) @@ -244,7 +250,8 @@ function DiseaseWorkflow(sources) { logger(state$, "state$"), binnedPlots.log, filterForm.log, - signatureForm.log + signatureForm.log, + exporter.log, ), DOM: vdom$, onion: xs.merge( @@ -254,6 +261,7 @@ function DiseaseWorkflow(sources) { binnedPlots.onion, headTable.onion, tailTable.onion, + exporter.onion, scenarioReducer$, uiReducer$, ), @@ -266,6 +274,9 @@ function DiseaseWorkflow(sources) { tailTable.HTTP ), popup: scenarioPopup$, + modal: exporter.modal, + fab: exporter.fab, + clipboard: exporter.clipboard, } } diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index 880cf8e3..aab6c3b4 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -1,4 +1,4 @@ -import { div } from "@cycle/dom" +import { div, button } from "@cycle/dom" import xs from "xstream" import isolate from "@cycle/isolate" import { TreatmentForm, treatmentLikeFilter } from "../components/TreatmentForm" @@ -11,6 +11,7 @@ import { SampleTable, sampleTableLens, } from "../components/SampleTable/SampleTable" +import { Exporter } from "../components/Exporter" // Support for ghost mode import { scenario } from "../scenarios/treatmentScenario" @@ -18,6 +19,7 @@ import { runScenario } from "../utils/scenario" import dropRepeats from "xstream/extra/dropRepeats" import debounce from "xstream/extra/debounce" +import sampleCombine from "xstream/extra/sampleCombine" import { equals } from "ramda" @@ -47,7 +49,7 @@ export default function GenericTreatmentWorkflow(sources) { const scenarios$ = sources.onion.state$ .take(1) .filter((state) => state.settings.common.ghostMode) - .map(state => runScenario(scenario( workflowGhostModeScenarioSelector(state) ))) + .map(state => runScenario(scenario( workflowGhostModeScenarioSelector(state) ), state$)) const scenarioReducer$ = scenarios$.map(s => s.scenarioReducer$) .flatten() const scenarioPopup$ = scenarios$.map(s => s.scenarioPopup$) @@ -71,8 +73,19 @@ export default function GenericTreatmentWorkflow(sources) { treatmentLike: workflowTreatmentType, }, ui: state.ui?.form ?? {}, + params: state.routerInformation.params, }), - set: (state, childState) => ({ ...state, form: childState.form }), + set: (state, childState) => ({ + ...state, + form: childState.form, + routerInformation: { + ...state.routerInformation, + pageState: { + ...state.routerInformation.pageState, + ...childState.pageState, + } + } + }), } /** @@ -250,6 +263,8 @@ export default function GenericTreatmentWorkflow(sources) { .remember(), }) + const exporter = Exporter(sources) + /** * Style object used in div capsulating filter, displayPlots and tables * @const pageStyle @@ -273,8 +288,9 @@ export default function GenericTreatmentWorkflow(sources) { headTable.DOM, tailTable.DOM, displayPlots$, + exporter.DOM, ) - .map(([formDOM, filter, plots, headTable, tailTable, displayPlots]) => + .map(([formDOM, filter, plots, headTable, tailTable, displayPlots, exporter]) => div(workflowMainDivClass /* something like ".row .genetic" */ , { style: { margin: "0px 0px 0px 0px" } }, [ formDOM, div(".col .s10 .offset-s1", pageStyle, [ @@ -282,10 +298,11 @@ export default function GenericTreatmentWorkflow(sources) { div(".row", [displayPlots === "before tables" ? plots : div()]), div(".col .s12", [headTable]), div(".row", []), - div(".col .s12", [tailTable]), + div(".col .s12", {props: {id: "tailTable"}}, [tailTable]), div(".row", []), div(".row", [displayPlots === "after tables" ? plots : div()]), ]), + exporter, ]) ) @@ -296,9 +313,10 @@ export default function GenericTreatmentWorkflow(sources) { filterForm.log, binnedPlots.log, headTable.log, - tailTable.log + tailTable.log, + exporter.log, ), - DOM: vdom$.startWith(div()), + DOM: vdom$,//.startWith(div()), onion: xs.merge( defaultReducer$, TreatmentFormSink.onion, @@ -306,6 +324,7 @@ export default function GenericTreatmentWorkflow(sources) { filterForm.onion, headTable.onion, tailTable.onion, + exporter.onion, scenarioReducer$, uiReducer$, ), @@ -318,7 +337,9 @@ export default function GenericTreatmentWorkflow(sources) { ), vega: binnedPlots.vega, popup: scenarioPopup$, - modal: xs.merge(TreatmentFormSink.modal), + modal: xs.merge(TreatmentFormSink.modal, exporter.modal), ac: TreatmentFormSink.ac, + fab: exporter.fab, + clipboard: exporter.clipboard, } } diff --git a/src/js/scenarios/correlationScenario.js b/src/js/scenarios/correlationScenario.js new file mode 100644 index 00000000..f98fc2fa --- /dev/null +++ b/src/js/scenarios/correlationScenario.js @@ -0,0 +1,78 @@ + +export const scenario = config => [ + { //Form + delay: 500, + state: {}, + message: { + text: 'Please enter the gene symbols, or ENSMBL or ENTREZ ID or Probe set ID, from your gene signature', + duration: 8000 + } + }, + { // Use typer to write full query string + delay: 200, + state: { + routerInformation: { + params: config.params, + } + } + }, + { + delay: 700, + state: {}, + message: { + text: 'Downregulated genes are marked with "-"', + duration: 4000 + } + }, + { + delay: 4000, + state: {}, + message: { + text: 'The table provides feedback for the gene symbols in the signature', + duration: 4000 + } + }, + + { // Validate + delay: 2000, + continue: (s) => (s.form.query1 == config.params.signature1), + state: {}, + message: { + text: 'When ready, press the update/validate button, removing faulty or non-measured L1000 genes', + duration: 6000 + } + }, + { + delay: 1000, + state: { form: { ghostUpdate1: true } } + }, + { + delay: 1000, + continue: (s) => (s.form.query2 == config.params.signature2), + state: { form: { ghostUpdate2: true } } + }, + + // Start analysis + { + delay: 4000, + state: { + form: { ghost: true }, + }, + message: { + text: 'When ready, press play_arrow', + duration: 6000 + } + }, + + // Wait a bit for everything to load... + + { // output text + delay: 6000, + state: {}, + message: { + text: 'The correlation is vizualised for the entire database', + duration: 4000 + } + }, + +] diff --git a/src/js/scenarios/diseaseScenario.js b/src/js/scenarios/diseaseScenario.js index 1dd61243..abb13b8f 100644 --- a/src/js/scenarios/diseaseScenario.js +++ b/src/js/scenarios/diseaseScenario.js @@ -16,103 +16,52 @@ const filterCellDose = { trtType: ['trt_cp', 'trt_lig'] } -export const scenario = [ +export const scenario = config => [ { //Form delay: 500, - state: { form: { query: "-E", validated: false } }, + state: {}, message: { text: 'Please enter the gene symbols, or ENSMBL or ENTREZ ID or Probe set ID, from your gene signature', duration: 8000 } }, - { + { // Use typer to write full query string delay: 200, - state: { form: { query: "-EN", validated: false } }, - }, - { - delay: 100, - state: { form: { query: "-ENS", validated: false } }, + state: { + routerInformation: { + params: config.params, + } + } }, { - delay: 600, - state: { form: { query: "-ENSG00000013583", validated: false } }, + delay: 700, + state: {}, message: { text: 'Downregulated genes are marked with "-"', duration: 4000 } }, { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860", validated: false } }, + delay: 4000, + state: {}, message: { text: 'The table provides feedback for the gene symbols in the signature', duration: 4000 } }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6 PMAIP1", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6 PMAIP1 DDIT4", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6 PMAIP1 DDIT4 -RAB31", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5", validated: false } }, - }, - { - delay: 1000, - state: { form: { query: "-ENSG00000013583 DDX10 -217933_s_at -GLRX 4860 -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS", validated: false } }, - }, { // Validate delay: 2000, - state: { form: { query: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS', validated: true } }, + continue: (s) => (s.form.query == config.params.signature), + state: {}, message: { text: 'When ready, press the update/validate button, removing faulty or non-measured L1000 genes', duration: 6000 } }, { - delay: 100, - state: { form: { validated: true } }, + delay: 1000, + state: { form: { ghostUpdate: true } } }, // Initiate Ghost, prepare tables to fit on the screen @@ -120,12 +69,12 @@ export const scenario = [ delay: 4000, state: { form: { ghost: true }, - filter: { - input: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS', - output: filterValues, - filter_output: filterValues, - state: {dose: false, cell: false, trtType: false}, - }, + // filter: { + // // input: '-HEBP1 DDX10 -LAP3 -GLRX NP -SLC2A6 PMAIP1 DDIT4 -RAB31 FYN HSD17B10 KLHL21 MMP1 MAPKAPK5 EPRS', + // output: filterValues, + // filter_output: filterValues, + // state: {dose: false, cell: false, trtType: false}, + // }, }, message: { text: 'When ready, press play_arrow', diff --git a/src/js/scenarios/treatmentScenario.js b/src/js/scenarios/treatmentScenario.js index 53b9c053..aa14eeed 100644 --- a/src/js/scenarios/treatmentScenario.js +++ b/src/js/scenarios/treatmentScenario.js @@ -19,6 +19,10 @@ const typer = (text, json) => { ) } +function notEmpty(data) { + return data != undefined && data != "" +} + /** * Definition of the scenario * Configuration is provided by means of `config` object: @@ -37,25 +41,39 @@ export const scenario = config => state: { form: { check: { ghostinput: true } } }, }, ], - typer( - config.treatment, - { // Form input - state: { form: { check: { input: config.treatment, showSuggestions: true, validated: false } } }, + [ + { + delay: 500, + state: { + routerInformation: { + params: config.params, + } + } } - ), + ], [ - { // Form input - delay: 1500, - state: { form: { check: { input: config.treatment, showSuggestions: false, validated: true } } }, + { // Sample Selection + delay: 100, + state: {}, + message: { + text: 'Enter a treatment for which to search', + duration: 4000 + } }, { // GO - delay: 1000, - state: { form: { check: { ghostoutput: true } } }, + delay: 1000, + continue: (s) => (s.form.check.validated), + state: { form: { check: { ghostoutput: true } } }, + // message: { + // text: 'Press the \'GO\' button', + // duration: 4000 + // } }, { // Sample Selection delay: 500, + continue: (s) => (notEmpty(s.form.sampleSelection.output)), state: {}, message: { text: 'All samples that correspond to the selected compound are tabulated', @@ -64,31 +82,24 @@ export const scenario = config => }, { delay: 3000, - state: { form: { sampleSelection: { data: { index: config.index, value: { use: true } } } } }, + state: {}, message: { text: 'select or deselect the desired sample(s)', duration: 4000 } }, - // { - // delay: 1000, - // state: { form: { sampleSelection: { data: { index: 2, value: { use: false } } } } }, - // }, - // { - // delay: 1000, - // state: { form: { sampleSelection: { data: { index: 3, value: { use: false } } } } }, - // }, { // Signature creation delay: 500, - state: { form: { sampleSelection: { ghostoutput: true, output: config.sample } } }, + state: { form: { sampleSelection: { ghostoutput: true } } }, message: { text: 'Press Select', duration: 4000 } }, { - delay: 4000, + delay: 100, + continue: (s) => (notEmpty(s.form.signature.output)), state: {}, message: { text: 'A signed ranked gene signature is generated across the samples', @@ -105,19 +116,11 @@ export const scenario = config => }, { // Filter - delay: 7000, - state: { - filter: { - input: config.signature, - output: { trtType: [ "trt_cp" ] }, - filter_output: { trtType: [ "trt_cp" ] }, - ghost: { expand: false } - }, - headTable: { input: { query: config.signature } }, - tailTable: { input: { query: config.signature } }, - }, + delay: 500, + continue: (s) => (notEmpty(s.form.signature.output)), + state: {}, message: { - text: 'Set filter a filter', + text: 'Set a filter', duration: 7000 } }, diff --git a/src/js/utils/scenario.js b/src/js/utils/scenario.js index 922c957c..89b3b492 100644 --- a/src/js/utils/scenario.js +++ b/src/js/utils/scenario.js @@ -1,7 +1,9 @@ import xs from "xstream" -import { mergeDeepRight, mergeDeepWithKey } from "ramda" +import { mergeDeepRight, mergeDeepWithKey, equals } from "ramda" import delay from "xstream/extra/delay" import concat from "xstream/extra/concat" +import dropRepeats from "xstream/extra/dropRepeats" +import debounce from "xstream/extra/debounce" /** * We expect the following input for the right-hand side: @@ -19,10 +21,27 @@ const updateData = (k, l, r) => * The hardest part is providing the new state and merging that to the previous one. * Especially for Arrays, because we don't want to provide the full array in every step. */ -export const runScenario = (scenario) => { - const scenarioStreamArray = scenario.map((step) => - xs.of(step).compose(delay(step.delay)) - ) +export const runScenario = (scenario, state$) => { + + const scenarioStreamArray = scenario.map((step) => { + if (step.continue != undefined) { + const trigger$ = state$ + .map((s) => step.continue(s)) + .compose(debounce(equals)) + .filter(a => a == true) + .compose(dropRepeats(equals)) + const triggerDelayed$ = trigger$ + .compose(delay(10)) + + const triggerOutput$ = trigger$ + .mapTo(step) + .endWhen(triggerDelayed$) + + return triggerOutput$.compose(delay(step.delay)) + } + else + return xs.of(step).compose(delay(step.delay)) + }) const reducerStreamArray = scenarioStreamArray.map((step$) => step$.map( diff --git a/src/js/utils/searchUtils.js b/src/js/utils/searchUtils.js new file mode 100644 index 00000000..0b1ac0d6 --- /dev/null +++ b/src/js/utils/searchUtils.js @@ -0,0 +1,78 @@ +import xs from "xstream" +import flattenConcurrently from "xstream/extra/flattenConcurrently" +import delay from "xstream/extra/delay" +import dropRepeats from "xstream/extra/dropRepeats" +import { prop, equals, min, max } from "ramda" + +/** + * Create stream with text to be used as input + * @function typer + * @param {Stream} state$ full state onion of the component + * @param {String} valueName name of the property in the state to be used; search value + * @param {String} speedName name of the property in the state to be used; enables slow typing + * @returns Stream + */ +export function typer(state$, valueName, speedName) { + /** + * Get the search value from state$ + * @const typer/value$ + * @type {Stream} + */ + const value$ = state$ + .map((state) => prop(valueName, state)) + .filter((value) => value !== undefined) + .compose(dropRepeats(equals)) + .compose(delay(100)) + + /** + * Get the typer speed from state$ + * @const typer/speed$ + * @type {Stream} + */ + const speed$ = state$ + .map((state) => prop(speedName, state)) + .compose(dropRepeats(equals)) + .startWith("") + + /** + * Slowly type the search value, starting with 1 letter and incrementally getting longer until the full text is being output + * Only used when speed is explicitely set + * @const typer/typer$ + * @type {Stream} + */ + const typer$ = xs + .combine(speed$, value$) + .filter(([speed, _]) => speed == "" || speed == "yes" || !isNaN(speed)) + .map(([speed, value]) => { + const l = value.length + const range = Array(l) + .fill() + .map((_, index) => index + 1) + + const interval = isNaN(speed) ? 100 : max(min(speed, 5000), 50) + + return xs + .fromArray( + range.map((i) => + xs.of(value.substr(0, i)).compose(delay(interval * i)) + ) + ) + .compose(flattenConcurrently) + }) + .flatten() + + /** + * Output search value in one go if the typer isn't enabled by the speed value + * @const typer/typerNotSelected$ + * @type {Stream} + */ + const typerNotSelected$ = xs + .combine(speed$, value$) + .filter( + ([speed, _]) => + speed == undefined || (speed != "" && speed != "yes" && isNaN(speed)) + ) + .map(([_, value]) => value) + + return xs.merge(typer$, typerNotSelected$) +} diff --git a/src/js/_compass_svg.scss b/src/sass/_compass_svg.scss similarity index 100% rename from src/js/_compass_svg.scss rename to src/sass/_compass_svg.scss diff --git a/src/sass/_exporter.scss b/src/sass/_exporter.scss new file mode 100644 index 00000000..a2df2322 --- /dev/null +++ b/src/sass/_exporter.scss @@ -0,0 +1,79 @@ +// style copy from materialize but add usage of 'span' instead of 'a' +.fixed-action-btn { + ul { + span.btn-floating { + opacity: 0; + } + } +} + +// own styling +.fixed-action-btn { + > span.btn-floating { + background-color: color('red', 'base'); + } + > ul { + span.export-clipboard-link-fab { + background-color: color('red', 'base') + } + span.export-clipboard-signature-fab { + background-color: color('yellow', 'darken-1') + } + span.export-pdf { + background-color: color('green', 'base') + } + span.modal-open-btn { + background-color: color('blue', 'base') + } + + span.fab-wrap{ + display: block; + overflow: hidden; + position: relative; + width: 100%; + height: 100%; + background-color: transparent; + box-shadow: none; + color: #fff; + // line-height: $button-floating-large-size; + z-index: 1; + + &.success{ + background-color:green; + } + &.failure{ + background-color:red; + } + } + } + +} + +div #modal-exporter { + + div.title { + font-size: 1.5rem; + } + + span.success { + background-color: green; + } + span.failure { + background-color: red; + } + + // clicking *directly* on the a download link makes the router(?) go nuts. make sure we always click the i + // so to recapitulate, changing the padding fixes the underlying issue. the padding in itself is ok + a.paddingfix { + padding: 0px; + i { + width: 100%; + } + } + + div.modal-footer { + > button.export-close{ + background-color: color('blue-grey', 'base') + } + } +} \ No newline at end of file diff --git a/src/js/_main_custom.scss b/src/sass/_main_custom.scss similarity index 61% rename from src/js/_main_custom.scss rename to src/sass/_main_custom.scss index 58711bdb..3a6f3a77 100644 --- a/src/js/_main_custom.scss +++ b/src/sass/_main_custom.scss @@ -7,4 +7,11 @@ main div.correlation { .validation { background-color: color("blue", "lighten-3"); } + + & > div:first-child { + div.validation > div > div { + margin-bottom: 0px; + } + margin-bottom: 20px; + } } diff --git a/src/js/_variables.scss b/src/sass/_variables.scss similarity index 100% rename from src/js/_variables.scss rename to src/sass/_variables.scss diff --git a/src/js/main.scss b/src/sass/main.scss similarity index 98% rename from src/js/main.scss rename to src/sass/main.scss index 42209a0a..93c960b7 100644 --- a/src/js/main.scss +++ b/src/sass/main.scss @@ -174,6 +174,12 @@ img.trt_img { color: $color-footer-text; } +div.fixed-action-btn { + bottom: 120px; +} + +@import 'exporter'; + /* home svg styling and hover */ a:hover #border { diff --git a/src/test/FilterTest.js b/src/test/FilterTest.js index f090f9b1..a12b77af 100644 --- a/src/test/FilterTest.js +++ b/src/test/FilterTest.js @@ -12,6 +12,7 @@ describe("defaultReducer", function () { xs.empty(), xs.empty(), xs.empty(), + xs.empty(), xs.empty() ) @@ -38,6 +39,7 @@ describe("defaultReducer", function () { xs.empty(), xs.empty(), xs.empty(), + xs.empty(), xs.empty() ) @@ -73,6 +75,7 @@ describe("possibleValuesReducer", function () { xs.empty(), xs.empty(), xs.empty(), + xs.empty(), xs.empty() ) @@ -112,6 +115,7 @@ describe("inputReducer", function () { input$, xs.empty(), xs.empty(), + xs.empty(), xs.empty() ) @@ -169,6 +173,7 @@ describe("toggleReducer with and without modifier", function () { input$, filterValuesAction$, modifier$, + xs.empty(), xs.empty() ) @@ -247,7 +252,8 @@ describe("uiReducer", function () { input$, filterValuesAction$, modifierFalse$, - openFilter$ + openFilter$, + xs.empty() ) // Predefine filters in settings as possible filters will be updated there @@ -286,3 +292,63 @@ describe("uiReducer", function () { }) }) }) + +describe("Search query", function () { + it("Updates the output which should match that of the search query, except for invalid values which should be discarded", () => { + const possibleValues = { + dose: [1, 2, 3], + cell: ["cell1", "cell2", "cell3"], + trtType: ["a", "b", "c"], + } + const possibleValues$ = fromDiagram("-x").mapTo(possibleValues) + const search = xs.of({ + dose: [1, 2], + cell: ["cell1", "cell2", "cell3"], + trtType: ["a", "b", "d"] + }) + const search$ = fromDiagram("--x").mapTo(search) + + const reducers$ = model( + possibleValues$, + xs.empty(), + xs.empty(), + xs.empty(), + xs.empty(), + search$ + ) + + // Predefine filters in settings as possible filters will be updated there + const defaultFilter = {settings: {filter: {}}} + const state$ = reducers$.fold((state, reducer) => reducer(state), defaultFilter) + + // Outputs all valid values + let expectedOutput = [ + {}, + {}, + {}, + { dose: [2, 3], cell: ["cell1", "cell2", "cell3"], trtType: ["a", "b"] }, + ] + + let expectedFilterOutput = [ + {}, + {}, + {}, + { dose: [2, 3], cell: undefined, trtType: ["a", "b"] }, + ] + + state$ + .drop(1) // drop the first state as it is undefined + .addListener({ + next(state) { + assert.deepStrictEqual(state?.core?.output, expectedOutput.shift()) + assert.deepStrictEqual(state?.core?.filter_output, expectedFilterOutput.shift()) + }, + error(e) { + console.log(e) + }, + complete() { + console.log("done!") + }, + }) + }) +}) From 1034f39a6c73044982885b9b43004f2fd32c888c Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 30 Mar 2022 14:01:25 +0200 Subject: [PATCH 182/191] Add extra comment about moving .scss files in changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92b07b78..b0d49fc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ ### Other - Ghost mode has been improved, it no longer requires explicit timings in the scenarios +- Moved scss files from `src/js` to `src/sass`, so any alternative versions of e.g. `_variables.scss` or `_main.custom.scss` should now be placed in the new folder ## Version 5.0.2 From 5f6688f2081a889d9d99078498054d74bf4d869c Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 7 Apr 2022 11:35:44 +0200 Subject: [PATCH 183/191] Fix Exporter in generic WF (#141) Exporter encounters issues when taking 'toUpperCase' from an undefined variable, so pass an empty string instead of an undefined --- src/js/pages/genericTreatment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index b16e88bd..ead98fb5 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -265,7 +265,7 @@ export default function GenericTreatmentWorkflow(sources) { const exporter = Exporter({ ...sources, - config: { workflowName: workflow.workflowName }, + config: { workflowName: workflow.workflowName ?? "" }, }) /** From 10162d06598a9efb78d71e0178ccda6b8caf6b47 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 7 Apr 2022 11:38:53 +0200 Subject: [PATCH 184/191] Display tooltip for clipboard and download buttons in export menu (#140) --- CHANGELOG.md | 1 + src/js/components/Exporter.js | 44 +++++++++++++++++------------- src/sass/_exporter.scss | 50 +++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aaf6a43..f239510f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Add exporting of report in Markdown format - Add browser access to restarting the API +- Display tooltip for clipboard and download buttons in export menu ### Other diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 6faa7a51..2fc458b5 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -437,19 +437,29 @@ function view(state$, dataPresent, exportData, config, clipboard) { // Workaround is done in '.paddingfix' in the scss. return div(".row", [ span(".col .s6 .push-s1", text), - span(".btn .col .s1 .offset-s1 .waves-effect .waves-light " + clipboardId + " " + availableClipboardText + clipboardBtnResult, i(".material-icons", "content_copy")), - a(".btn .col .s1 .offset-s1 .waves-effect .waves-light .paddingfix " + availableDownloadText, - { - props: { - href: fileData, - download: fileName, + div(".col .s1 .offset-s1 .tooltip", [ + span(".btn .waves-effect .waves-light " + clipboardId + " " + availableClipboardText + clipboardBtnResult, i(".material-icons", "content_copy")), + span(".tooltiptext", "Copy to clipboard") + ]), + div(".col .s1 .offset-s1 .tooltip", [ + a(".btn .waves-effect .waves-light .paddingfix " + availableDownloadText, + { + props: { + href: fileData, + download: fileName, + }, }, - }, - i(".material-icons", "file_download"), - ) + i(".material-icons", "file_download"), + ), + span(".tooltiptext", "Download as file") + ]), ]) } + function capitalizeFirstLetter(string) { + return string.charAt(0).toUpperCase() + string.slice(1); + } + return div([ div("#modal-exporter.modal", [ div(".modal-content", [ @@ -457,16 +467,12 @@ function view(state$, dataPresent, exportData, config, clipboard) { p(".col .s12", "Export to clipboard or file"), //p(".col .s12", "" + clipboardResult.text), ]), - addExportDiv("url", "Create link to this page's state", ".export-clipboard-link", urlFile, "url.txt", true), - addExportDiv("signature", "Copy signature", ".export-clipboard-signature", signatureFile, "signature.txt", signaturePresent), - addExportDiv("plot", "Copy " + config.plotName + " plot", ".export-clipboard-plots", plotFile, "plot.png", plotsPresent, copyImagesPermission), - addExportDiv("headTable", "Copy top table", ".export-clipboard-headTable", headTableCsvFile, "table.tsv", headTablePresent), - addExportDiv("tailTable", "Copy bottom table", ".export-clipboard-tailTable", tailTableCsvFile, "table.tsv", tailTablePresent), - addExportDiv("mdReport", "Copy MarkDown report", ".export-clipboard-mdReport", mdReportFile, "report.md", mdReportPresent), - // div(".row", [ - // span(".col .s6 .push-s1", "Export report"), - // span(".btn .col .s1 .offset-s3 .export-file-report .disabled", i(".material-icons", "file_download")), - // ]), + addExportDiv("url", "Url to this page's state", ".export-clipboard-link", urlFile, "url.txt", true), + addExportDiv("signature", "Signature", ".export-clipboard-signature", signatureFile, "signature.txt", signaturePresent), + addExportDiv("plot", capitalizeFirstLetter(config.plotName) + " plot", ".export-clipboard-plots", plotFile, "plot.png", plotsPresent, copyImagesPermission), + addExportDiv("headTable", "Top table", ".export-clipboard-headTable", headTableCsvFile, "table.tsv", headTablePresent), + addExportDiv("tailTable", "Bottom table", ".export-clipboard-tailTable", tailTableCsvFile, "table.tsv", tailTablePresent), + addExportDiv("mdReport", "Markdown report", ".export-clipboard-mdReport", mdReportFile, "report.md", mdReportPresent), ]), div(".modal-footer", [ button(".export-close .col .s8 .push-s2 .btn", "Close"), diff --git a/src/sass/_exporter.scss b/src/sass/_exporter.scss index a2df2322..5ca5c686 100644 --- a/src/sass/_exporter.scss +++ b/src/sass/_exporter.scss @@ -76,4 +76,54 @@ div #modal-exporter { background-color: color('blue-grey', 'base') } } + + // clipboard and download button tooltips + $color-tooltip-background: color('blue-grey', 'base') !default; + $color-tooltip-text: white !default; + .tooltip { + position: relative; + display: inline-block; + + // expand & center button in middle of div, so we can align the tooltip to the center too + .btn { + margin: 0 -16px; + justify-content: center; + display: flex; + } + + .tooltiptext { + visibility: hidden; + width: 150px; + background-color: $color-tooltip-background; + color: $color-tooltip-text; + text-align: center; + padding: 5px 0; + border-radius: 6px; + + /* Position the tooltip text - see examples below! */ + position: absolute; + z-index: 1; + + bottom: 130%; + left: 50%; + margin-left: -75px; + } + + &:hover .tooltiptext { + visibility: visible; + } + + // Arrow pointing down + .tooltiptext::after { + content: " "; + position: absolute; + top: 100%; /* At the bottom of the tooltip */ + left: 50%; + margin-left: -10px; + border-width: 10px; + border-style: solid; + border-color: $color-tooltip-background transparent transparent transparent; + } + } + } \ No newline at end of file From f08b609c939de4a44f7b4114fe78178194c974d6 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Thu, 7 Apr 2022 13:07:50 +0200 Subject: [PATCH 185/191] Gskcmp 102 all a clicks get send to the router (#139) Add filter on "do-not-route" before sending a clicks to the router * Remove no longer valid comment * Add do-not-route in init tab * Add changelog entry * Remove unused and commented a.paddingfix Cleans up stylesheet from code that should not be needed anymore --- CHANGELOG.md | 1 + src/js/components/Exporter.js | 10 +--------- src/js/index.js | 1 + src/js/pages/init.js | 2 +- src/sass/_exporter.scss | 9 --------- 5 files changed, 4 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f239510f..5247de96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Filter functionality made dynamic so that new filter groups can be added as needed - Improve sturdiness against invalid input +- Filter a-link references on 'do-not-route' before sending to the router ### Deployment changes diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index 2fc458b5..cbffffc1 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -427,14 +427,6 @@ function view(state$, dataPresent, exportData, config, clipboard) { ? "" : clipboardResult.state == "success" ? " .success" : " .failure" - // Styling should prevent the user to click the 'a' directly; this causes the page div#root to be corrupted. - // Work around is to make the internal 'i' the full size of the 'a' thus "catching" the initial click. - // Exact reason is not 100% clear. Using preventDefault doesn't seem to work. - // - // At the time of writing, the impression is that it could have to do with the exporter or sub-parts not being isolated. - // Debugging suggest the @cycle/dom to be the culprit. - // Removing the 'div#Root' -> 'fromEvent.js:16' event listener prevents the page from misbehaving. - // Workaround is done in '.paddingfix' in the scss. return div(".row", [ span(".col .s6 .push-s1", text), div(".col .s1 .offset-s1 .tooltip", [ @@ -442,7 +434,7 @@ function view(state$, dataPresent, exportData, config, clipboard) { span(".tooltiptext", "Copy to clipboard") ]), div(".col .s1 .offset-s1 .tooltip", [ - a(".btn .waves-effect .waves-light .paddingfix " + availableDownloadText, + a(".btn .waves-effect .waves-light .do-not-route " + availableDownloadText, { props: { href: fileData, diff --git a/src/js/index.js b/src/js/index.js index aa9fc0ad..6e08f2b6 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -476,6 +476,7 @@ export default function Index(sources) { // Capture link targets and send to router driver const router$ = sources.DOM.select("a") .events("click") + .filter((ev) => !ev.ownerTarget.classList.contains("do-not-route")) .map((ev) => ev.target.pathname) .remember() diff --git a/src/js/pages/init.js b/src/js/pages/init.js index 5401f0fd..f4e51ed2 100644 --- a/src/js/pages/init.js +++ b/src/js/pages/init.js @@ -162,7 +162,7 @@ function view(requests$, responses$, statusDisplay$, settingsDOM$, jarFile$, con const vdom$ = xs.combine(apiUrl$, statusDisplay$, settingsDOM$, initText$, jarFile$, configFile$) .map(([sjsLink, statusDisplay, settings, initText, jarFile, configFile]) => div(".container .init", [ - div(".row .s12", a({props: {href: sjsLink, target: "_blank"}}, "Spark overview page")), + div(".row .s12", a(".do-not-route", {props: {href: sjsLink, target: "_blank"}}, "Spark overview page")), div([p("Server poll status: ", [ span("SJS status query: "), span(".status-" + statusDisplay, diff --git a/src/sass/_exporter.scss b/src/sass/_exporter.scss index 5ca5c686..703e40d5 100644 --- a/src/sass/_exporter.scss +++ b/src/sass/_exporter.scss @@ -62,15 +62,6 @@ div #modal-exporter { background-color: red; } - // clicking *directly* on the a download link makes the router(?) go nuts. make sure we always click the i - // so to recapitulate, changing the padding fixes the underlying issue. the padding in itself is ok - a.paddingfix { - padding: 0px; - i { - width: 100%; - } - } - div.modal-footer { > button.export-close{ background-color: color('blue-grey', 'base') From 9cebc973f1e4654f95536922aead09ccba40c748 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 15 Apr 2022 11:39:54 +0200 Subject: [PATCH 186/191] Bump version number to 5.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 22efa6d4..902fde25 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "LuciusWeb", - "version": "5.1.0", + "version": "5.2.0", "description": "Web interface for ComPass aka Lucius", "repository": { "type": "git", From 1f93001d1a9e22460ad6be109acbc501d209ee9f Mon Sep 17 00:00:00 2001 From: Toni Verbeiren Date: Thu, 21 Apr 2022 09:05:04 +0200 Subject: [PATCH 187/191] Fix version in site title --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index eafbff84..06f4388e 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - ComPass v4 + ComPass v5 From 8a8af725c475173b91e46c92e0e86967aebb0096 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 22 Apr 2022 08:47:03 +0200 Subject: [PATCH 188/191] Use 'DOMContentLoaded' event as trigger to initialize FAB (#145) also handle rare condition of fab not being initialized when trying to update it --- src/js/components/Exporter.js | 5 ++--- src/js/drivers/makeFloatingActionButtonDriver.js | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/js/components/Exporter.js b/src/js/components/Exporter.js index cbffffc1..3d7e937b 100644 --- a/src/js/components/Exporter.js +++ b/src/js/components/Exporter.js @@ -511,7 +511,8 @@ function Exporter(sources) { const vdom$ = view(state$, model_.dataPresent, model_.exportData, fullConfig, sources.clipboard) - const fabInit$ = vdom$.mapTo({ + const fabInit$ = sources.DOM.select('document').events('DOMContentLoaded') + .mapTo({ state: "init", element: ".fixed-action-btn", options: { @@ -519,8 +520,6 @@ function Exporter(sources) { // hoverEnabled: false, } }) - .compose(dropRepeats(equals)) // run just once - .compose(delay(50)) // let the vdom propagate first and next cycle initialize FAB const fabUpdate$ = model_.dataPresent.signaturePresent$ .filter(_ => fullConfig.fabSignature == "update") diff --git a/src/js/drivers/makeFloatingActionButtonDriver.js b/src/js/drivers/makeFloatingActionButtonDriver.js index 64c042c3..4d6fa948 100644 --- a/src/js/drivers/makeFloatingActionButtonDriver.js +++ b/src/js/drivers/makeFloatingActionButtonDriver.js @@ -32,7 +32,7 @@ function makeFloatingActionButtonDriver() { if (elem == undefined) console.warn("fabDriver couldn't find element") else { - fab.destroy() + fab?.destroy() fab = M.FloatingActionButton.init(elem, ev.options); if (currentlyOpen && fab != undefined) fab.open() From 14035a904545a947727f683938e2a2ba22e2bb38 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Wed, 11 May 2022 16:25:33 +0200 Subject: [PATCH 189/191] Gskcmp 101 ability to filter sample tables (#148) Add Filters component to the Sample Selection tables Use value & filter parts depending on usability Use Materialize CSS NoUiSlider and create a driver to initialize & update the sliders --- src/js/components/SampleSelection.js | 262 ++++-- src/js/components/SampleSelectionFilters.js | 835 ++++++++++++++++++++ src/js/components/TreatmentForm.js | 1 + src/js/drivers/makeSliderDriver.js | 44 ++ src/js/index.js | 1 + src/js/main.js | 3 + src/js/pages/genericTreatment.js | 1 + src/sass/_sample_selection_filters.scss | 11 + src/sass/main.scss | 1 + src/test/SampleSelectionTest.js | 253 ++++++ 10 files changed, 1361 insertions(+), 51 deletions(-) create mode 100644 src/js/components/SampleSelectionFilters.js create mode 100644 src/js/drivers/makeSliderDriver.js create mode 100644 src/sass/_sample_selection_filters.scss create mode 100644 src/test/SampleSelectionTest.js diff --git a/src/js/components/SampleSelection.js b/src/js/components/SampleSelection.js index a4752abb..53256b0a 100644 --- a/src/js/components/SampleSelection.js +++ b/src/js/components/SampleSelection.js @@ -14,12 +14,14 @@ import { p, i, } from "@cycle/dom" -import { clone, equals, merge, sortWith, prop, ascend, descend } from "ramda" +import { clone, equals, merge, sortWith, prop, ascend, descend, keys, length, includes, anyPass, allPass, filter } from "ramda" import xs from "xstream" +import isolate from "@cycle/isolate" import dropRepeats from "xstream/extra/dropRepeats" import debounce from 'xstream/extra/debounce' import { loggerFactory } from "../utils/logger" import { TreatmentAnnotation } from "./TreatmentAnnotation" +import { SampleSelectionFilters, SampleSelectionFiltersLens } from "./SampleSelectionFilters" import { safeModelToUi } from "../modelTranslations" import { dirtyUiReducer, dirtyWrapperStream, busyUiReducer } from "../utils/ui" @@ -50,6 +52,154 @@ const sampleSelectionLens = { }), } +/** + * Create function to check value against a single criterion + * + * { type: 'range', min: 1, max: 2, unit: 'um' }, + * { type: 'value', value: 1, unit: 'um' }, + * + * @param {Object} criterion + * @returns function (sampleEntry) => boolean + */ +const createFilterCheck = (filterKey) => (criterion) => { + if (criterion.type == 'range') { + const minCheck = criterion.min == undefined + ? () => true + : (value) => Number(value[filterKey]) >= Number(criterion.min) + const maxCheck = criterion.max == undefined + ? () => true + : (value) => Number(value[filterKey]) <= Number(criterion.max) + const unitCheck = criterion.unit == undefined || criterion.unit == '' + ? () => true + : (value) => value[filterKey + "_unit"] == criterion.unit + + return allPass([minCheck, maxCheck, unitCheck]) + } + else if (criterion.type == 'value') { + const valueCheck = criterion.value == undefined + ? () => true + : (value) => value[filterKey] == criterion.value + const unitCheck = criterion.unit == undefined || criterion.unit == '' + ? () => true + : (value) => value[filterKey + "_unit"] == criterion.unit + + return allPass([valueCheck, unitCheck]) + } + + console.warn("Criterion type " + criterion.type + " unknown.") + return (value) => false +} + +/** + * Filter an array of sample data by the specified criteria + * + * Criteria object has entries for each property to filter by. + * Each entry has an array of object with, each should have a type field + * type, string: + * - 'range' + * - 'value' + * Depending on the type, the following fields can be added + * - range: + * - min + * - max + * - unit + * - value: + * - value + * - unit + * + * In case of multiple properties to be filtered, use AND logic + * In case of multiple limits/values for a property, use OR logic + * + * { + * dose: [ + * { type: 'range', min: 1, max: 2, unit: 'um' }, + * { type: 'value', value: 1, unit: 'um' }, + * ... + * ] + * } + * + * @param {Array} data Data to filter + * @param {Object} criteria + * @returns Array of filtered data + */ +export const filterData = (data, criteria) => { + // we need at least 1 data entry to be able to filter + // not just as concept but also for our code to work + if (length(data) == 0) + return data + + const filterKeys = keys(criteria) + const dataKeys = keys(data[0]) + const criteriaArray = [] + + for (const filterKey of filterKeys) { + if (!includes(filterKey, dataKeys)) { + console.warn("Not using '" + filterKey + "' to filter data as it is not found in the data set.") + continue + } + + if (!Array.isArray(criteria[filterKey])) { + console.warn("Not using '" + filterKey+ "' to filter data as it is the wrong data type: " + (typeof criteria[filterKey]) + ".") + continue + } + + const check = anyPass(criteria[filterKey].map(createFilterCheck(filterKey))) + criteriaArray.push(check) + } + + const filteredData = filter(allPass(criteriaArray), data) + return filteredData +} + +/** + * update hide field to earmark the field as filtered + * @param {Array} data Data to filter + * @param {Object} criteria + * @returns Array of data with the hide field updated + */ +const hideFilteredData = (data, criteria) => { + const filteredData = filterData(data, criteria) + return data.map((v) => ({ ...v, hide: !includes(v, filteredData) }) ) +} + +/** + * Sorts an array of sample data by the specified property and direction + * Dose and time are sorted numerically instead of alphabetically, otherwise 3 > 10 + * @param {Array} data Data to be sorted + * @param {String} sortBy Property name to sort by + * @param {Boolean} direction True = descending, False = ascending + * @returns Array of sorted data + */ +export const sortData = (data, sortBy, direction) => { + + function propSort(prop, descend) { + return (a, b) => { + const aValue = a[prop] + const bValue = b[prop] + const multi = descend ? -1 : 1 + + if (isNaN(aValue) || isNaN(bValue)) + // works properly for integers but not for decimal numbers, so only use it as fallback + return aValue.localeCompare(bValue, undefined, {numeric: true}) + else + return (Number(aValue) - Number(bValue) > 0 ? 1 : -1) * multi + } + } + + const dataSortAscend = sortWith([ + ascend(prop(sortBy)), + ]); + const dataSortDescend = sortWith([ + descend(prop(sortBy)), + ]); + + return sortBy !== "" + ? sortBy == "dose" || sortBy == "time" + ? sortWith([propSort(sortBy, direction)])(data) + : direction ? dataSortDescend(data) : dataSortAscend(data) + : data +} + /** * Based on a (list of) treatment(s), get the samples that correspond to it and allow users to select them. * @@ -65,7 +215,10 @@ function SampleSelection(sources) { "settings.form.debug" ) - const state$ = sources.onion.state$ + // Add .drop(0) to force creation of a separate state$ stream + // This seems to help with the synchronisation when there are lots of updates + // To be confirmed over time whether this actually always works + const state$ = sources.onion.state$.drop(0) const input$ = sources.input // .startWith("BRD-K28907958") // REMOVE ME !!! @@ -90,13 +243,13 @@ function SampleSelection(sources) { const emptyState$ = state$ // .filter(state => state.core.input == null || state.core.input == '') .filter((state) => isEmptyState(state)) - .compose(dropRepeats((x, y) => equals(x, y))) + .compose(dropRepeats(equals)) // When the state is cycled because of an internal update const modifiedState$ = state$ // .filter(state => state.core.input != '') .filter((state) => !isEmptyState(state)) - .compose(dropRepeats((x, y) => equals(x, y))) + .compose(dropRepeats(equals)) const newInput$ = xs .combine(input$, state$) @@ -142,9 +295,11 @@ function SampleSelection(sources) { .map((json) => json.result.data) .remember() + const sampleFilters = isolate(SampleSelectionFilters, { onion: SampleSelectionFiltersLens })(sources) + // Helper function for rendering the table, based on the state const makeTable = (state, annotation, initialization) => { - const data = state.core.data + const data = state.core.usageData const blurStyle = state.settings.common.blur ? { style: { filter: "blur(" + state.settings.common.amountBlur + "px)" }, @@ -152,32 +307,9 @@ function SampleSelection(sources) { : {} const selectedClass = (selected) => selected ? ".sampleSelected" : ".sampleDeselected" - - function propSort(prop, descend) { - return (a, b) => { - const aValue = a[prop] - const bValue = b[prop] - const multi = descend ? -1 : 1 - - if (isNaN(aValue) || isNaN(bValue)) - // works properly for integers but not for decimal numbers, so only use it as fallback - return aValue.localeCompare(bValue, undefined, {numeric: true}) - else - return (Number(aValue) - Number(bValue) > 0 ? 1 : -1) * multi - } - } - - const dataSortAscend = sortWith([ - ascend(prop(state.core.sort)), - ]); - const dataSortDescend = sortWith([ - descend(prop(state.core.sort)), - ]); - const sortedData = state.core.sort !== "" - ? state.core.sort == "dose" || state.core.sort == "time" - ? sortWith([propSort(state.core.sort, state.core.direction)])(data) - : state.core.direction ? dataSortDescend(data) : dataSortAscend(data) - : data + + const filteredData = filter((v) => !v.hide, data) + const sortedData = sortData(filteredData, state.core.sort, state.core.direction) let rows = sortedData.map((entry) => [ td(".selection", { props: { id: entry.id } }, [ @@ -301,13 +433,20 @@ function SampleSelection(sources) { ]) } + const makeFiltersAndTable = (state, annotation, initialization, filtersDom, filtersObject) => { + return div([ + filtersDom, + makeTable(state, annotation, initialization, filtersObject) + ]) + } + const initVdom$ = emptyState$.mapTo(div()) const loadingVdom$ = request$ - .compose(sampleCombine(loadingState$)) - .map(([_, state]) => + .compose(sampleCombine(loadingState$, sampleFilters.DOM)) + .map(([_, state, filtersDom]) => // Use the same makeTable function, pass a initialization=true parameter and a body DOM with preloading - makeTable( + makeFiltersAndTable( state, div(".col.s10.offset-s1.l10.offset-l1", [ div( @@ -316,31 +455,49 @@ function SampleSelection(sources) { [div(".indeterminate", { style: { "background-color": "white" } })] ), ]), - true + true, + filtersDom ) ) .remember() const loadedVdom$ = xs - .combine(modifiedState$, treatmentAnnotations.DOM) - .filter(([state, _]) => state.core.busy == false) - .map(([state, annotation]) => makeTable(state, annotation, false)) + .combine(modifiedState$, treatmentAnnotations.DOM, sampleFilters.DOM) + .filter(([state, _1, _2]) => state.core.busy == false) + .map(([state, annotation, filtersDom]) => makeFiltersAndTable(state, annotation, false, filtersDom)) // Wrap component vdom with an extra div that handles being dirty const vdom$ = dirtyWrapperStream( state$, xs.merge(initVdom$, loadingVdom$, loadedVdom$)) const dataReducer$ = data$.map((data) => (prevState) => { - const newData = data.map((el) => merge(el, { use: true })) + const newData = data.map((el) => merge(el, { use: true, hide: false })) + const hiddenData = hideFilteredData(newData, prevState.core.filtersObject) return { ...prevState, core: { ...prevState.core, - data: newData, - output: newData.filter((x) => x.use).map((x) => x.id), + data: data, + usageData: hiddenData, + output: hiddenData.filter((x) => x.use && !x.hide).map((x) => x.id), }, } }) + const filtersObjectReducer$ = sampleFilters.output + .compose(dropRepeats(equals)) + .map((filtersObject) => (prevState) => { + const hiddenData = hideFilteredData(prevState.core.usageData, filtersObject) + return { + ...prevState, + core: { + ...prevState.core, + filtersObject: filtersObject, + usageData: hiddenData, + output: hiddenData.filter((x) => x.use && !x.hide).map((x) => x.id), + } + } + }) + const useClick$ = sources.DOM.select(".selection") .events("click", { preventDefault: true }) .map((ev) => ev.ownerTarget.id) @@ -380,7 +537,7 @@ function SampleSelection(sources) { .map(([id, a]) => (prevState) => { // a = false is the usual behavior if (!a) { - const newData = prevState.core.data.map((el) => { + const newData = prevState.core.usageData.map((el) => { // One sample object var newEl = clone(el) const switchUse = id === el.id @@ -393,12 +550,12 @@ function SampleSelection(sources) { ...prevState, core: { ...prevState.core, - data: newData, - output: newData.filter((x) => x.use).map((x) => x.id), + usageData: newData, + output: newData.filter((x) => x.use && !x.hide).map((x) => x.id), }, } } else { - const newData = prevState.core.data.map((el) => { + const newData = prevState.core.usageData.map((el) => { // One sample object var newEl = clone(el) newEl.use = !el.use @@ -408,8 +565,8 @@ function SampleSelection(sources) { ...prevState, core: { ...prevState.core, - data: newData, - output: newData.filter((x) => x.use).map((x) => x.id), + usageData: newData, + output: newData.filter((x) => x.use && !x.hide).map((x) => x.id), }, } } @@ -424,7 +581,7 @@ function SampleSelection(sources) { .map((_) => (prevState) => { const samplesToUse = prevState.search.split(",") - const newData = prevState.core.data.map((el) => { + const newData = prevState.core.usageData.map((el) => { // One sample object var newEl = clone(el) const use = samplesToUse.includes(el.id)//id === el.id @@ -437,8 +594,8 @@ function SampleSelection(sources) { ...prevState, core: { ...prevState.core, - data: newData, - output: newData.filter((x) => x.use).map((x) => x.id), + usageData: newData, + output: newData.filter((x) => x.use && !x.hide).map((x) => x.id), }, } }) @@ -450,7 +607,7 @@ function SampleSelection(sources) { const defaultReducer$ = xs.of((prevState) => ({ ...prevState, - core: { input: "", data: [] }, + core: { input: "", data: [], usageData: [], filtersObject: {} }, })) const inputReducer$ = input$.map((i) => (prevState) => ({ ...prevState, @@ -508,15 +665,18 @@ function SampleSelection(sources) { inputReducer$, requestReducer$, dataReducer$, + filtersObjectReducer$, selectReducer$, autoSelectReducer$, busyReducer$, sortReducer$, hoverReducer$, dirtyReducer$, + sampleFilters.onion, ), output: sampleSelection$, modal: treatmentAnnotations.modal, + slider: sampleFilters.slider, } } diff --git a/src/js/components/SampleSelectionFilters.js b/src/js/components/SampleSelectionFilters.js new file mode 100644 index 00000000..1467d6ed --- /dev/null +++ b/src/js/components/SampleSelectionFilters.js @@ -0,0 +1,835 @@ +import xs from "xstream" +import { div, label, input, button, span, i } from "@cycle/dom" +import { pick, mix } from 'cycle-onionify'; +import { + prop, + keys, + length, + includes, + filter, + uniq, + apply, + countBy, + identity, + none, + all, + toPairs, + fromPairs, + flatten, + equals, + lensProp, + set, + view as viewR, + find, + whereEq, + sum, + any, + reject, +} from "ramda" +import dropRepeats from "xstream/extra/dropRepeats"; +import flattenConcurrently from 'xstream/extra/flattenConcurrently' +import SampleCombine from 'xstream/extra/sampleCombine' +import delay from "xstream/extra/delay" +import debounce from "xstream/extra/debounce" +import pairwise from "xstream/extra/pairwise" + +/** + * @module components/SampleSelectionFilters + */ + +const SampleSelectionFiltersLens = { + get: (state) => ({ + core: { + data: state.core.data, + filterData: state.core?.sampleSelectionFilters?.filterData, + filterInfo: state.core?.sampleSelectionFilters?.filterInfo, + stateData: state.core?.sampleSelectionFilters?.stateData, + } + }), + set: (state, childState) => ({ + ...state, + core: { + ...state.core, + sampleSelectionFilters: { + filterData: childState.core?.filterData, + filterInfo: childState.core?.filterInfo, + stateData: childState.core?.stateData, + } + }, + }), +} + +// serialize fields into a string that's deserializable again +// might need to adjust delimiter and check if certain characters need to be escaped +const serialize = (key, unit, value) => { + const str = [key, unit, value].join("___") + return str +} + +const deserialize = (str) => { + const arr = str.split("___") + return arr +} + +function SingleSampleSelectionFilter(key, filterInfo$, filterData$, stateData$, filterConfig) { + + const thisFilterInfo$ = filterInfo$.map((info) => info[key]) + const thisFilterData$ = filterData$.map((data) => data[key]) + const thisFilterConfig = filterConfig[key] + + const valueElements = (key, unitInfo, filterData) => { + + const list = toPairs(unitInfo.values).map(([value, amount]) => { + + var use = false + const matcher = whereEq({ type: 'value', unit: unitInfo.unit, value: value }) + if (filterData?.length > 0) { + const data = find( matcher )(filterData) + use = data?.use + } + + return div(".collection-item", { props: { id: serialize(key, unitInfo.unit, value) }}, [ + label("", [ + input( + ".selection-cb", + { props: { type: "checkbox", checked: use } }, + "tt" + ), + span(value), + span(" "), + span("(" + amount + ")"), + ]), + ]) + }) + + return div(".sampleSelectionFilter-" + key + "-checkboxes", [ + div(".collection .selection", list) + ]) + } + + const sliderElements = (key, unitInfo, filterData) => { + const slider2Exists = any(whereEq( { type: 'range', unit: unitInfo.unit, id:1 } ), filterData ?? []) + const sliderDiv = div(".sampleSelectionFilter-" + key + "-sliders .row", [ + div(".col.s11 .sampleSelectionFilterSlider", { props: { id: serialize(key, unitInfo.unit, '-slider-')}}), + div(".col.s1", + unitInfo.allowDoubleRange + ? [button(".btn-flat .sampleSelectionRangeSwitch", + { props: { id: serialize(key, unitInfo.unit, '-rangeswitch-')}}, + i( + ".material-icons .grey-text", + slider2Exists ? "remove" : "add" + ) + )] + : [] + ) + ]) + + return unitInfo.hasRange ? sliderDiv : div() + } + + const createFilter = (key, info, filterData, stateData, filterConfig) => { + + const mappedInfo = flatten(info.values.map((unitInfo) => { + + const showValues = filterConfig?.values != 'hide' + const hasRange = any(whereEq( { type: 'range', unit: unitInfo.unit } ), filterData ?? []) + const showRange = filterConfig?.range != 'hide' && hasRange + + const unitValuePairs = toPairs(unitInfo.values) + const firstDataChunk = { ...unitInfo, values: fromPairs(unitValuePairs.filter((_, i) => i < unitValuePairs.length / 2)) } + const secondDataChunk = { ...unitInfo, values: fromPairs(unitValuePairs.filter((_, i) => i >= unitValuePairs.length / 2)) } + + const thisStateData = stateData[serialize(key, unitInfo.unit, "")]?.state // open (true) or closed (false) + // any filter value or range set? if so set class so css coloring can be set + const filterDeselectedValues = filter(whereEq( { type: 'value', unit: unitInfo.unit, use: false } ), filterData ?? []) + const filterRange = find(whereEq( { type: 'range', unit: unitInfo.unit, id: 0 } ), filterData ?? []) + const filterRange2 = find(whereEq( { type: 'range', unit: unitInfo.unit, id: 1 } ), filterData ?? []) + + const hasFilterRange = + filterRange == undefined + ? false // no range filters + : filterRange2 == undefined // 1 or 2 range filters + ? filterRange.min != unitInfo.min || filterRange.max != unitInfo.max + : filterRange.min != unitInfo.min || filterRange.max != filterRange2.min || filterRange2.max != unitInfo.max + + const activeFilterClass = + (filterDeselectedValues.length > 0 + ? " .activeValueFilter" + : "" + ) + + (hasFilterRange + ? " .activeRangeFilter" + : "" + ) + + const filterElements = [] + .concat( + showValues + ? [ + div(".col.l6.s12", valueElements(key, firstDataChunk, filterData)), + div(".col.l6.s12", valueElements(key, secondDataChunk, filterData)) + ] + : [] + ) + .concat( + showRange + ? [div(".col .s12", sliderElements(key, unitInfo, filterData))] + : [] + ) + + return showValues || showRange + ? thisStateData + ? [ + div(".col .s12.m4.l2", div(".chip .sampleSelectionFilterHeader" + activeFilterClass, { props: { id: serialize(key, unitInfo.unit, "-header-") } } , [span(key), span(" "), span(unitInfo.unit)])), + div(".col .s12", filterElements) + ] + : [ + div(".col .s12.m4.l2", div(".chip .sampleSelectionFilterHeader" + activeFilterClass, { props: { id: serialize(key, unitInfo.unit, "-header-") } } , [span(key), span(" "), span(unitInfo.unit)])), + ] + : [] + })) + + return mappedInfo + } + + const vdom$ = xs + .combine( + thisFilterInfo$, + thisFilterData$, + stateData$, + ) + .map(([info, filterData, stateData]) => createFilter(key, info, filterData, stateData, thisFilterConfig)) + + const createSliderDriverObject = (key, unitInfo, minValue, maxValue, minValue2, maxValue2) => { + const scale = (v) => (Number(v) - Number(unitInfo.min)) / (Number(unitInfo.max) - Number(unitInfo.min)) + const rescale = (v) => { + if (v == unitInfo.min) + return 'min' + else if (v == unitInfo.max) + return 'max' + + const scaled = scale(v) * 100 + return scaled.toString() + "%" + } + + const rangeValues = keys(unitInfo.values).sort((a, b) => Number(a) - Number(b)) // All values numerically sorted + const rangeNumberValues = rangeValues.map((v) => Number(v)) // All values as numbers + const scaledRange = fromPairs(rangeNumberValues.map((v) => [rescale(v), v])) // All values as dict: scaled -> value + const formatLookupDict = fromPairs(rangeValues.map((v) => [Number(v), v])) // All values as dict: Number -> original string + + const showPipValues = rangeNumberValues.reduce((acc, v, index, arr) => + (index == 0) || ((scale(v) - scale(acc.slice(-1)[0]) >= 0.1) && (scale(v) <= 0.9)) || (index == length(arr) - 1) + ? acc.concat(v) + : acc, + []) // Only values that are either first, far enough from others, or last => Spacy + const formatLookupDictPips = fromPairs(rangeValues.map((v) => + [ + Number(v), + includes(Number(v), showPipValues) ? v : "" + ])) // All values as dict: Number -> original string (spacy) or empty string (non-spacy) + + // Formatter to convert all values as original strings, provide fallback + const formatLookup = { to: (v) => formatLookupDict[v] ?? v.toFixed(2), from: (v) => v } + // Formatter to convert spacy values as original strings, provide fallback + const formatLookupPips = { to: (v) => formatLookupDictPips[v] ?? v.toFixed(2), from: (v) => v } + + const doubleRange = minValue2 != undefined && maxValue2 != undefined + const minValueWDefault = minValue ?? unitInfo.minValue + const maxValueWDefault = maxValue ?? unitInfo.maxValue + const minValueWDefault2 = minValue2 ?? unitInfo.maxValue + const maxValueWDefault2 = maxValue2 ?? unitInfo.maxValue + + const startRange = doubleRange ? [minValueWDefault, maxValueWDefault, minValueWDefault2, maxValueWDefault2] : [minValueWDefault, maxValueWDefault] + const connect = doubleRange ? [false, true, false, true, false] : true + + return { + id: serialize(key, unitInfo.unit, '-slider-'), + object: { + start: startRange, + snap: true, + connect: connect, + orientation: 'horizontal', + range: scaledRange, + format: formatLookup, + pips: { + mode: 'values', + values: rangeNumberValues, + density: 100, + stepped: true, + format: formatLookupPips, + } + } + } + } + + const sliderInfo$ = thisFilterInfo$ + .map((info) => xs.fromArray(info.values)) + .compose(flattenConcurrently) + .filter((unitInfo) => unitInfo.hasRange) + const sliderData$ = thisFilterData$ + .map((dataArr) => find(whereEq( { type: "range", id: 0 } ), dataArr ?? [])) + const sliderData2$ = thisFilterData$ + .map((dataArr) => find(whereEq( { type: "range", id: 1 } ), dataArr ?? [])) + const sliderObject$ = xs + .combine(sliderInfo$, sliderData$, sliderData2$) + .map(([unitInfo, unitData, unitData2]) => createSliderDriverObject(key, unitInfo, unitData?.min, unitData?.max, unitData2?.min, unitData2?.max)) + + const slider$ = stateData$ + .compose(debounce(10)) + .compose(dropRepeats(equals)) + .compose(SampleCombine(sliderObject$)) + .map(([states, sliderObject]) => { + const [sliderKey, sliderUnit, _] = deserialize(sliderObject.id) + const filterId = serialize(sliderKey, sliderUnit, '') + return { + ...sliderObject, + shown: states[filterId]?.state ?? false + } + }) + + return { + DOM: vdom$, + slider: slider$, + } +} + + + +// const composedFilterInfo = { +// 'field1': { +// hasUnits: false, +// hasRange: false, +// min: 0, +// max: 0, +// values: [ +// { unit: '', values: {'a': 3, 'b': 2, 'c': 2, 'd': 3}, hasRange: false, min: 0, max: 0, amount: 10 }, +// ] +// }, +// 'field2': { +// hasUnits: true, +// hasRange: true, // only true if all units have 'hasRange' == true +// min: 0.001, // minimum of all values +// max: 100, +// values: [ +// { unit: 'unit1', values: {0.001: 2, 0.01: 2, 0.1: 3, 1: 3}, hasRange: true, min: 0.001, max: 1, amount: 10 }, +// { unit: 'unit2', values: {10: 5, 100: 5}, hasRange: true, min: 10, max: 100, amount: 10 }, +// ] +// } +// } + +/** + * Create an object that specifies what filters can be created + * @param {Object} data Master data that needs to be analyzed on how it can be filtered + * @returns {Object} + */ +const composeFilterInfo = (data) => { + // we need at least 1 data entry to be able to compose the data types that will be present in the data + if (length(data) == 0) return {} + + const getValueStruct = (unit, arr) => { + const counts = countBy(identity, arr) + const isAllNumbers = none(isNaN, arr) + const allIntegers = any(Number.isInteger, arr) + const minValue = isAllNumbers ? apply(Math.min, arr) : 0 + const maxValue = isAllNumbers ? apply(Math.max, arr) : 0 + const valueOptions = length(keys(counts)) + return { + unit: unit, + values: counts, + hasRange: isAllNumbers && valueOptions >= 3, + allowDoubleRange: isAllNumbers && valueOptions >= 5, + allIntegers: allIntegers, + min: minValue, + max: maxValue, + amount: length(arr), + valueOptions: valueOptions, + } + } + + const dataKeys = keys(data[0]).filter((v) => v != 'use') + + const mappedDataArr = dataKeys.map((dataKey) => { + if (includes(dataKey + "_unit", dataKeys)) { + const units = uniq(data.map(prop(dataKey + "_unit"))) + + const unitValuesArr = units.map((unit) => { + const unitFilter = (entry) => prop(dataKey + "_unit", entry) == unit + const filteredData = filter(unitFilter, data) + const arr = filteredData.map(prop(dataKey)) + + return getValueStruct(unit, arr) + }) + + const allHasRange = all(prop("hasRange"), unitValuesArr) + const allMinValue = allHasRange + ? apply(Math.min, unitValuesArr.map(prop("min"))) + : 0 + const allMaxValue = allHasRange + ? apply(Math.max, unitValuesArr.map(prop("max"))) + : 0 + const valueOptions = sum(unitValuesArr.map(prop("valueOptions"))) + + return [ + dataKey, { + hasUnits: true, + hasRange: allHasRange, + min: allMinValue, + max: allMaxValue, + values: unitValuesArr, + valueOptions: valueOptions, + }, + ] + } else { + const arr = data.map(prop(dataKey)) + const values = getValueStruct("", arr) + + return [ + dataKey, { + hasUnits: false, + hasRange: values.hasRange, + min: values.min, + max: values.max, + values: [values], + valueOptions: values.valueOptions, + }, + ] + } + }) + + const result = fromPairs(mappedDataArr.filter(([k, v]) => v.valueOptions > 1)) + return result +} + + +function intent(domSource$) { + + const useValueClick$ = domSource$.select(".collection-item") + .events("click", { preventDefault: true }) + .map((ev) => ({id: ev.ownerTarget.id, modifier: ev.altKey})) + + const headerClick$ = domSource$.select(".sampleSelectionFilterHeader") + .events("click") + .map((ev) => ev.ownerTarget.id) + + const switchRangeClick$ = domSource$.select(".sampleSelectionRangeSwitch") + .events("click") + .map((ev) => ev.ownerTarget.id) + + return { + useValueClick$: useValueClick$, + headerClick$: headerClick$, + switchRangeClick$: switchRangeClick$, + } +} + +function model(state$, intents, sliderEvents$) { + const filterInfo$ = state$ + .map((state) => composeFilterInfo(state.core.data)) + .compose(dropRepeats(equals)) + .remember() + + const defaultReducer$ = xs.of((prevState) => ({ + ...prevState, + core: { + filterData: {}, + filterInfo: {}, + state: {}, + } + })) + + /** + * Convert incoming filter information (base information) to filter data (how filters are configured) + * @const modelInitialFilterData$ + * @type {Stream} + */ + const initialFilterData$ = filterInfo$.map((filterInfo) => { + + const filterDataPairs = toPairs(filterInfo).map(([key, value]) => { + const nestedValues = value.values.map((valuesPerUnit) => keys(valuesPerUnit.values).map((v) => ({ type: 'value', value: v, unit: valuesPerUnit.unit, use: true }))) + const nestedRange = value.values.map((valuesPerUnit) => valuesPerUnit.hasRange + ? [{ type: 'range', min: valuesPerUnit.min, max: valuesPerUnit.max, unit: valuesPerUnit.unit, id: 0 }] + : []) + const flattenedValues = flatten(nestedValues.concat(nestedRange)) // tag on nestedRange, either empty array or array with single object + + return [key, flattenedValues] + }) + + const filterData = fromPairs(filterDataPairs) + return filterData + }) + + /** + * Toggle single or all values because user clicked a value without or with the modifier key + * @const model/updateFilterData + * @param {String} key identifier for what values to update + * @param {String} unit identifier for what values to update + * @param {String} value identifier for what single value to update or ignored in case modifier is used + * @param {Boolean} modifier modifier key being pressed or not (toggle single or all values) + * @param {Object} prevFilterData data to be updated + * @returns {Object} updated data + */ + const updateFilterData = (key, unit, value, modifier, prevFilterData) => { + const keyLens = lensProp(key) + // if modifier key is pressed, toggle all data points that match this key & unit, otherwise toggle single data point matching key, unit and value + const matcher = modifier + ? whereEq({ type: 'value', unit: unit }) + : whereEq({ type: 'value', unit: unit, value: value }) + const toggleUse = (v) => ({ ...v, use: !v.use }) + const matchToggle = (v) => matcher(v) ? toggleUse(v) : v + + const currentFilterDataKey = viewR(keyLens, prevFilterData) + const currentFilterDataOnlyKeyUpdatedValue = currentFilterDataKey.map((data) => matchToggle(data)) + const updatedFilterData = set(keyLens, currentFilterDataOnlyKeyUpdatedValue, prevFilterData) + return updatedFilterData + } + + /** + * Update range min & max values + * @const model/updateSliderFilterData + * @param {String} key identifier for what range to update + * @param {String} unit identifier for what range to update + * @param {Array} sliderValue array of the slider values, either 2 or 4 values + * @param {String} sliderHandle id of the DOM slider + * @param {Object} prevFilterData data to be updated + * @returns {Object} updated data + */ + const updateSliderFilterData = (key, unit, sliderValue, sliderHandle, prevFilterData) => { + const keyLens = lensProp(key) + const matcher = whereEq({ type: 'range', unit: unit, id: 0 }) + const changeRanges = (v) => ({ ...v, min: sliderValue[0], max: sliderValue[1] }) + const matchUpdate = (v) => matcher(v) ? changeRanges(v) : v + + // 2nd slider for this key/unit + const matcher2 = whereEq({ type: 'range', unit: unit, id: 1 }) + const changeRanges2 = (v) => ({ ...v, min: sliderValue[2], max: sliderValue[3] }) + const matchUpdate2 = (v) => matcher2(v) ? changeRanges2(v) : v + + const currentFilterDataKey = viewR(keyLens, prevFilterData) + const currentFilterDataOnlyKeyUpdatedValue = currentFilterDataKey.map((data) => matchUpdate(data)) + const currentFilterDataOnlyKeyUpdatedValue2 = currentFilterDataOnlyKeyUpdatedValue.map((data) => matchUpdate2(data)) + const updatedFilterData = set(keyLens, currentFilterDataOnlyKeyUpdatedValue2, prevFilterData) + + return updatedFilterData + } + + /** + * Store incoming (master) data + * @const model/filterInfoReducer$ + * @type {Reducer} + */ + const filterInfoReducer$ = filterInfo$.map((filterInfo) => (prevState) => ({ + ...prevState, + core: { + ...prevState.core, + filterInfo: filterInfo, + } + })) + + /** + * Store new filterData when new data arrives + * @const model/initFilterDataReducer$ + * @type {Reducer} + */ + const initFilterDataReducer$ = initialFilterData$.map((filterData) => (prevState) => ({ + ...prevState, + core: { + ...prevState.core, + filterData: filterData, + } + })) + + /** + * Create new stateData (filter open/closed, 1/2 slider mode) from updated (master) filter data + * @const model/initStateDataReducer$ + * @type {Reducer} + */ + const initStateDataReducer$ = initialFilterData$.map((filterData) => (prevState) => { + const newStateDataHeaders = uniq(flatten(toPairs(filterData).map(([key, value]) => value.map((v) => serialize(key, v.unit, ""))))) + // if already exists, use old state value, otherwise set to false (closed) + const updatedStateData = fromPairs(newStateDataHeaders.map((h) => [h, + { + state: prevState.core.stateData[h]?.state ?? false, + mode: prevState.core.stateData[h]?.mode ?? 1, + } + ])) + return { + ...prevState, + core: { + ...prevState.core, + stateData: updatedStateData, + } + } + }) + + /** + * Get event from DOM and update the filter state being open or closed + * @const model/stateDataReducer$ + * @type {Reducer} + */ + const stateDataReducer$ = intents.headerClick$.map((id) => (prevState) => { + const [key, unit, _] = deserialize(id) + const filterId = serialize(key, unit, '') + return { + ...prevState, + core: { + ...prevState.core, + stateData: { + ...prevState.core.stateData, + [filterId]: { + state: !prevState.core.stateData[filterId].state, + mode: prevState.core.stateData[filterId].mode, + } + } + } + }}) + + /** + * Get event from DOM and update filter range mode in stateData + * @const model/stateDataSliderReducer$ + * @type {Reducer} + */ + const stateDataSliderReducer$ = intents.switchRangeClick$.map((id) => (prevState) => { + const [key, unit, _] = deserialize(id) + const filterId = serialize(key, unit, '') + return { + ...prevState, + core: { + ...prevState.core, + stateData: { + ...prevState.core.stateData, + [filterId]: { + state: prevState.core.stateData[filterId].state, + mode: prevState.core.stateData[filterId].mode == 1 ? 2 : 1 + } + } + } + }}) + + const stateData$ = state$.map((state) => state.core.stateData) + const filterData$ = state$.map((state) => state.core.filterData) + + /** + * Get event from DOM and update the filterData with the updated values + * @const model/filterDataReducer$ + * @type {Reducer} + */ + const filterDataReducer$ = intents.useValueClick$.map((ev) => (prevState) => { + const [key, unit, value] = deserialize(ev.id) + const filterData = updateFilterData(key, unit, value, ev.modifier, prevState.core.filterData) + return { + ...prevState, + core: { + ...prevState.core, + filterData: filterData, + } + } + }) + + /** + * Get event from DOM and update the filterData with the updated values + * @const model/sliderFilterDataReducer$ + * @type {Reducer} + */ + const sliderFilterDataReducer$ = sliderEvents$.map((ev) => (prevState) => { + const [key, unit, _] = deserialize(ev.id) + const filterData = updateSliderFilterData(key, unit, ev.value, ev.handle, prevState.core.filterData) + return { + ...prevState, + core: { + ...prevState.core, + filterData: filterData, + } + } + }) + + /** + * Convert stateData object to stream of changes in the mode values + * Stream consists of key, value pairs + * @const model/sliderSwitchRange$ + * @type {Stream} + */ + const sliderSwitchRange$ = stateData$ + .map((v) => toPairs(v)) + .map((arr) => arr.map(([k, v]) => [k, v.mode])) + .map((arr) => fromPairs(arr)) + .compose(pairwise) + .map(([a, b]) => filter(([k, v]) => v != a[k], toPairs(b))) + .map((a) => xs.fromArray(a)) + .compose(flattenConcurrently) + + /** + * Starting from an id/mode-value pair, change between 1 or 2 sliders in filterData + * @const model/sliderSwitchRangeReducer$ + * @type {Reducer} + */ + const sliderSwitchRangeReducer$ = sliderSwitchRange$.map(([id, mode]) => (prevState) => { + const [key, unit, _] = deserialize(id) + const prevFilterData = prevState.core.filterData + + const keyLens = lensProp(key) + const currentFilterDataKey = viewR(keyLens, prevFilterData) + + const unitInfo = find(whereEq( { unit: unit } ), prevState.core.filterInfo[key].values) + const rangeValues = keys(unitInfo.values).sort((a, b) => Number(a) - Number(b)) // All values numerically sorted + const middleValue1 = Number(rangeValues[~~(unitInfo.valueOptions/4)]) + const middleValue2 = Number(rangeValues[~~(unitInfo.valueOptions*3/4)]) + + const rangesArr = !unitInfo.hasRange + ? [] + : mode == 2 && unitInfo.allowDoubleRange + ? [ + { type: 'range', min: unitInfo.min, max: middleValue1, unit: unit, id: 0 }, + { type: 'range', min: middleValue2, max: unitInfo.max, unit: unit, id: 1 }, + ] + : [ { type: 'range', min: unitInfo.min, max: unitInfo.max, unit: unit, id: 0 } ] + + const thisFilterDataWithoutRanges = reject(whereEq( { type: 'range', unit: unit } ), currentFilterDataKey) + const currentFilterDataKeyUpdatedValue = thisFilterDataWithoutRanges.concat(rangesArr) + + const updatedFilterData = set(keyLens, currentFilterDataKeyUpdatedValue, prevFilterData) + + return { + ...prevState, + core: { + ...prevState.core, + filterData: updatedFilterData, + } + } + }) + + /** + * Remove data without actual used filters + * + * Removes data with either all values are selected or ranges are full range + * @param {Array} keyValuePairs + * @param {Object} info + * @returns {Array} array of key/value pairs + */ + const filterUnits = (keyValuePairs, info) => { + function filterData(data, info) { + if (info == undefined) + return data + + const filteredUnitData = info.values.map((unitInfo) => { + const unitData = filter(whereEq( { unit: unitInfo.unit } ), data) // get data from only matching unit, still contains both 'value' and 'range' types + const unitValues = filter(whereEq( { type: 'value' } ), unitData).map((v) => v.value) // only keep 'value' type and map to only value type so we have a flat list of values + const unitInfoValues = keys(unitInfo.values) // get flat list of values from unitInfo + const noChangeInValues = equals(unitValues, unitInfoValues) + + const range = find(whereEq( { type: 'range', id: 0 } ), unitData) + const range2 = find(whereEq( { type: 'range', id: 1 } ), unitData) + + const useRange = range == undefined + ? false // no range + : range2 == undefined + ? range.min != unitInfo.min || range.max != unitInfo.max // single range mode + : range.min != unitInfo.min || range.max != range2.min || range2.max != unitInfo.max // double range mode + + return [] + .concat(filter((v) => v.type == 'value' && !noChangeInValues, unitData)) + .concat(filter((v) => v.type == 'range' && useRange, unitData)) + }) + + return flatten(filteredUnitData) + } + const filteredData = keyValuePairs.map(([key, value]) => [ key, filterData(value, info[key]) ]) + return filteredData + } + + /** + * Minimize filters to actually set/changed filters + * @const model/filterOutputs$ + * @type {Stream} + */ + const filterOutput$ = xs + .combine(filterData$, filterInfo$) + .map(([dataObject, info]) => [toPairs(dataObject), info]) // split object to an array of key/value pairs + .map(([arr, info]) => [arr.map(([key, values]) => [ key, filter( (v) => (v?.type != 'value') || v?.use , values) ]), info]) // don't use 'type' == 'value' when 'use' is false + .map(([arr, info]) => [filterUnits(arr, info), info]) // filter data per key & unit if either it has all values selected or range is full range + .map(([arr, info]) => [filter(([key, values]) => length(values) != 0, arr), info]) // remove a key if there is no more data left + .map(([dataPairs, info]) => fromPairs(dataPairs)) // rebuild to an object + + return { + filterInfo$: filterInfo$, + filterData$: filterData$, + filterOutput$: filterOutput$, + stateData$: stateData$, + onion: xs.merge( + defaultReducer$, + filterInfoReducer$, + initFilterDataReducer$, + initStateDataReducer$, + stateDataReducer$, + stateDataSliderReducer$, + filterDataReducer$, + sliderFilterDataReducer$, + sliderSwitchRangeReducer$, + ), + } +} + +function view(childrenSinks$) { + + const composedChildrenSinks$ = childrenSinks$.compose(pick('DOM')).compose(mix(xs.combine)) + + const vdom$ = composedChildrenSinks$ + .map((arr) => flatten([].concat(arr))) + .map((arr) => + div( + ".sampleSelectionFilters .col .s10 .offset-s1", + div(".row", + div(div(".col .s12", arr)) + ) + ) + ) + .startWith(div()) + + return vdom$ +} + +function SampleSelectionFilters(sources) { + const state$ = sources.onion.state$ + const sliderEvents$ = sources.slider + + const intent_ = intent(sources.DOM) + const model_ = model(state$, intent_, sliderEvents$) + + const filterConfig = { + significantGenes: { + values: 'hide', + range: 'show', + }, + inchikey: { + values: 'hide', + range : 'hide', + }, + smiles: { + values: 'hide', + range: 'hide', + }, + id: { + values: 'hide', + range: 'hide', + } + } + + const childrenSinks$ = model_.filterInfo$ + .map((filterInfo) => + keys(filterInfo).map((key) => + SingleSampleSelectionFilter(key, model_.filterInfo$, model_.filterData$, model_.stateData$, filterConfig))) + .remember() + + const vdom$ = view(childrenSinks$) + + const sliderDriver$ = childrenSinks$.compose(pick('slider')).compose(mix(xs.merge)) + + return { + log: xs.empty(), + DOM: vdom$, + onion: model_.onion, + slider: sliderDriver$.compose(delay(10)), + output: model_.filterOutput$, + } +} + +export { SampleSelectionFilters, SampleSelectionFiltersLens } diff --git a/src/js/components/TreatmentForm.js b/src/js/components/TreatmentForm.js index 5c4c7020..2173e325 100644 --- a/src/js/components/TreatmentForm.js +++ b/src/js/components/TreatmentForm.js @@ -73,6 +73,7 @@ function TreatmentForm(sources) { output: signature$, modal: xs.merge(SignatureGeneratorSink.modal, SampleSelectionSink.modal), ac: TreatmentCheckSink.ac, + slider: SampleSelectionSink.slider, } } diff --git a/src/js/drivers/makeSliderDriver.js b/src/js/drivers/makeSliderDriver.js new file mode 100644 index 00000000..fa03d886 --- /dev/null +++ b/src/js/drivers/makeSliderDriver.js @@ -0,0 +1,44 @@ +import * as noUiSlider from 'materialize-css/extras/noUiSlider/nouislider' +import xs from 'xstream' + + +function makeSliderDriver() { + + function sliderDriver(in$) { + + const producer = { + listener: 0, + start: (listener) => { this.listener = listener }, + next: (obj) => { this.listener.next(obj) }, + stop: () => { }, + } + + const returnStream$ = xs.create(producer) + + const sliderCallback = (id) => (value, handle) => { + producer.next( {id: id, value: value, handle: handle} ) + } + + in$.addListener({ + next: (slider) => { + var sliderElement = document.getElementById(slider.id) + + if (sliderElement?.noUiSlider != undefined) { + sliderElement.noUiSlider.destroy() + } + if (slider.shown) { + noUiSlider.create(sliderElement, slider.object) + sliderElement.noUiSlider.on('update', sliderCallback(slider.id)) + } + }, + error: (e) => { + console.error(e) + } + }) + return returnStream$ + } + + return sliderDriver +} + +export { makeSliderDriver }; \ No newline at end of file diff --git a/src/js/index.js b/src/js/index.js index 6e08f2b6..fbbe20b0 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -529,6 +529,7 @@ export default function Index(sources) { modal: page$.map(prop("modal")).filter(Boolean).flatten(), ac: page$.map(prop("ac")).filter(Boolean).flatten(), sidenav: sidenavEvent$, + slider: page$.map(prop("slider")).filter(Boolean).flatten(), fab: page$.map(prop("fab")).filter(Boolean).flatten(), storage: page$.map(prop("storage")).filter(Boolean).flatten(), deployments: page$ diff --git a/src/js/main.js b/src/js/main.js index cf255792..35258fd4 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -1,4 +1,5 @@ import 'materialize-css/dist/css/materialize.css'; +import 'materialize-css/extras/noUiSlider/nouislider.css' require('../../favicon.ico') @@ -24,6 +25,7 @@ import switchPath from 'switch-path' import { makeModalDriver } from './drivers/makeModalDriver' import { makeAutocompleteDriver } from './drivers/makeAutocompleteDriver'; import { makeSidenavDriver } from './drivers/makeSidenavDriver'; +import { makeSliderDriver } from './drivers/makeSliderDriver' import { makeFloatingActionButtonDriver } from './drivers/makeFloatingActionButtonDriver'; import { makeClipboardDriver } from './drivers/makeClipboardDriver' import '../sass/main.scss' @@ -48,6 +50,7 @@ const drivers = { modal: makeModalDriver(), ac: makeAutocompleteDriver(), sidenav: makeSidenavDriver(), + slider: makeSliderDriver(), fab: makeFloatingActionButtonDriver(), clipboard: makeClipboardDriver(), deployments: () => xs.fromPromise(fetch('/deployments.json').then(m => m.json())) diff --git a/src/js/pages/genericTreatment.js b/src/js/pages/genericTreatment.js index ead98fb5..678d7614 100644 --- a/src/js/pages/genericTreatment.js +++ b/src/js/pages/genericTreatment.js @@ -344,5 +344,6 @@ export default function GenericTreatmentWorkflow(sources) { ac: TreatmentFormSink.ac, fab: exporter.fab, clipboard: exporter.clipboard, + slider: TreatmentFormSink.slider, } } diff --git a/src/sass/_sample_selection_filters.scss b/src/sass/_sample_selection_filters.scss new file mode 100644 index 00000000..ff456eea --- /dev/null +++ b/src/sass/_sample_selection_filters.scss @@ -0,0 +1,11 @@ +div.col.sampleSelectionFilterSlider { + margin: 20px 0 60px 0; // Needs extra margin, not only for visuals but also because it's somewhat overbearing and captures clicks for other elements + padding: 0; // fix .col padding which messes up the internal slider calculations +} + +div.chip.sampleSelectionFilterHeader { + width: 100%; + &.activeValueFilter, &.activeRangeFilter { + background-color: color('yellow', 'lighten-2'); + } +} diff --git a/src/sass/main.scss b/src/sass/main.scss index 0295100c..5a96a066 100644 --- a/src/sass/main.scss +++ b/src/sass/main.scss @@ -179,6 +179,7 @@ div.fixed-action-btn { } @import 'exporter'; +@import 'sample_selection_filters'; @import 'init'; /* home svg styling and hover */ diff --git a/src/test/SampleSelectionTest.js b/src/test/SampleSelectionTest.js new file mode 100644 index 00000000..a6344ca8 --- /dev/null +++ b/src/test/SampleSelectionTest.js @@ -0,0 +1,253 @@ +import "mocha" +import * as assert from "assert" +import { filterData, sortData } from "../js/components/SampleSelection.js" + +/** + * Tests to verify filtering + */ + +const filterTestData = [ + { field1: 1, field2: 100, field2_unit: "abc", field3: 1, field3_unit: "foo" }, + { field1: 2, field2: 101, field2_unit: "abc", field3: 1, field3_unit: "foo" }, + { field1: 3, field2: 102, field2_unit: "abc", field3: 1, field3_unit: "foo" }, + { field1: 4, field2: 103, field2_unit: "abc", field3: 1, field3_unit: "foo" }, + { field1: 5, field2: 104, field2_unit: "abc", field3: 1, field3_unit: "foo" }, + { field1: 6, field2: 105, field2_unit: "abc", field3: 2, field3_unit: "foo" }, + { field1: 7, field2: 106, field2_unit: "abc", field3: 2, field3_unit: "foo" }, + { field1: 8, field2: 107, field2_unit: "abc", field3: 2, field3_unit: "foo" }, + { field1: 9, field2: 108, field2_unit: "abc", field3: 2, field3_unit: "foo" }, + { field1: 10, field2: 109, field2_unit: "abc", field3: 2, field3_unit: "foo" }, + { field1: 1, field2: 110, field2_unit: "def", field3: 3, field3_unit: "foo" }, + { field1: 2, field2: 111, field2_unit: "def", field3: 3, field3_unit: "foo" }, + { field1: 3, field2: 112, field2_unit: "def", field3: 3, field3_unit: "foo" }, + { field1: 4, field2: 113, field2_unit: "def", field3: 3, field3_unit: "foo" }, + { field1: 5, field2: 114, field2_unit: "def", field3: 3, field3_unit: "foo" }, + { field1: 6, field2: 115, field2_unit: "def", field3: 4, field3_unit: "foo" }, + { field1: 7, field2: 116, field2_unit: "def", field3: 4, field3_unit: "foo" }, + { field1: 8, field2: 117, field2_unit: "def", field3: 4, field3_unit: "foo" }, + { field1: 9, field2: 118, field2_unit: "def", field3: 4, field3_unit: "foo" }, + { field1: 10, field2: 119, field2_unit: "def", field3: 4, field3_unit: "foo" }, +] + +describe("Simple value filter", function() { + it("filters entries without unit set", () => { + const filter1 = { + field1: [ + { type: 'value', value: 1 }, + { type: 'value', value: 2 }, + { type: 'value', value: 3 }, + ] + } + const filterOutput1 = filterData(filterTestData, filter1) + assert.equal(filterOutput1.length, 6) + + const filter2 = { + field3: [ + { type: 'value', value: 1 }, + { type: 'value', value: 2 }, + { type: 'value', value: 3 }, + ] + } + const filterOutput2 = filterData(filterTestData, filter2) + assert.equal(filterOutput2.length, 15) + }) + + it("filters entries with unit set", () => { + const filter = { + field2: [ + { type: 'value', value: 100, unit: 'abc' }, + { type: 'value', value: 101, unit: 'abc' }, + { type: 'value', value: 102, unit: 'abc' }, + { type: 'value', value: 103, unit: 'abc' }, + { type: 'value', value: 104, unit: 'def' }, + { type: 'value', value: 105, unit: 'def' }, + { type: 'value', value: 110, unit: 'def' }, + { type: 'value', value: 111, unit: 'def' }, + ] + } + const filterOutput = filterData(filterTestData, filter) + assert.equal(filterOutput.length, 6) + }) +}) + +describe("Simple range filter", function() { + it("filters entries without unit set", () => { + const filter1 = { + field1: [ + { type: 'range', min: 1, max: 3 }, + ] + } + const filterOutput1 = filterData(filterTestData, filter1) + assert.equal(filterOutput1.length, 6) + + const filter2 = { + field3: [ + { type: 'range', min: 1, max: 3 }, + ] + } + const filterOutput2 = filterData(filterTestData, filter2) + assert.equal(filterOutput2.length, 15) + + const filter3 = { + field2: [ + { type: 'range', min: 105, max: 115}, + ] + } + const filterOutput3 = filterData(filterTestData, filter3) + assert.equal(filterOutput3.length, 11) + }) + + it("filter entries without max value set", () => { + const filter = { + field2: [ + { type: 'range', min: 105 } + ] + } + const filterOutput = filterData(filterTestData, filter) + assert.equal(filterOutput.length, 15) + }) + + it("filter entries without min value set", () => { + const filter = { + field2: [ + { type: 'range', max: 105 } + ] + } + const filterOutput = filterData(filterTestData, filter) + assert.equal(filterOutput.length, 6) + }) + + it("filter entries with unit set", () => { + const filter = { + field2: [ + { type: 'range', min: 105, max: 115, unit: "abc" }, + ] + } + const filterOutput = filterData(filterTestData, filter) + assert.equal(filterOutput.length, 5) + }) +}) + +describe("Combined range filter", function() { + it("filters entries without unit set", () => { + const filter1 = { + field1: [ + { type: 'range', min: 1, max: 3 }, + { type: 'range', min: 7, max: 9 }, + ] + } + const filterOutput1 = filterData(filterTestData, filter1) + assert.equal(filterOutput1.length, 12) + + const filter2 = { + field2: [ + { type: 'range', min: 105, max: 110 }, + { type: 'range', min: 115, max: 117 }, + ] + } + const filterOutput2 = filterData(filterTestData, filter2) + assert.equal(filterOutput2.length, 9) + }) + +}) + +describe("Filter can combine range and value", function() { + it("filters correctly", () => { + const filter = { + field2: [ + { type: 'range', min: 105, max: 115 }, + { type: 'value', value: 117 }, + { type: 'value', value: 118 }, + ] + } + const filterOutput = filterData(filterTestData, filter) + assert.equal(filterOutput.length, 13) + }) +}) + +describe("Filter on two fields at once", function() { + it("filters correctly", () => { + const filter = { + field1: [ + { type: 'range', min: 3, max: 8 } + ], + field2: [ + { type: 'range', min: 100, max: 115 } + ] + } + const filterOutput = filterData(filterTestData, filter) + assert.equal(filterOutput.length, 10) + }) +}) + +/** + * Tests to verify sorting + */ + + +// unsorted data +const sortTestData = [ + { dose: '10', field1: '10', field2: 'a' }, + { dose: '2', field1: '2', field2: 'b' }, + { dose: '1', field1: '1', field2: 'd' }, + { dose: '1', field1: '1', field2: 'c' }, + { dose: '0.1', field1: '0.1', field2: 'e' }, +] + +describe("Sorting alphabetically", function() { + it("sorts text ascending", () => { + const sorted = sortData(sortTestData, 'field2', false) + assert.equal(sorted[0].field2, 'a') + assert.equal(sorted[1].field2, 'b') + assert.equal(sorted[2].field2, 'c') + assert.equal(sorted[3].field2, 'd') + assert.equal(sorted[4].field2, 'e') + }) + + it("sorts text descending", () => { + const sorted = sortData(sortTestData, 'field2', true) + assert.equal(sorted[0].field2, 'e') + assert.equal(sorted[1].field2, 'd') + assert.equal(sorted[2].field2, 'c') + assert.equal(sorted[3].field2, 'b') + assert.equal(sorted[4].field2, 'a') + }) + + it("sorts number strings ascending", () => { + const sorted = sortData(sortTestData, 'field1', false) + assert.equal(sorted[0].field1, '0.1') + assert.equal(sorted[1].field1, '1') + assert.equal(sorted[2].field1, '1') + assert.equal(sorted[3].field1, '10') + assert.equal(sorted[4].field1, '2') + }) + + it("sorts number strings descending", () => { + const sorted = sortData(sortTestData, 'field1', true) + assert.equal(sorted[0].field1, '2') + assert.equal(sorted[1].field1, '10') + assert.equal(sorted[2].field1, '1') + assert.equal(sorted[3].field1, '1') + assert.equal(sorted[4].field1, '0.1') + }) +}) + +describe("sort numbers numerically when the property is dose or time", function() { + it("sorts numbers ascending", () => { + const sorted = sortData(sortTestData, 'dose', false) + assert.equal(sorted[0].field1, '0.1') + assert.equal(sorted[1].field1, '1') + assert.equal(sorted[2].field1, '1') + assert.equal(sorted[3].field1, '2') + assert.equal(sorted[4].field1, '10') + }) + + it("sorts numbers descending", () => { + const sorted = sortData(sortTestData, 'dose', true) + assert.equal(sorted[0].field1, '10') + assert.equal(sorted[1].field1, '2') + assert.equal(sorted[2].field1, '1') + assert.equal(sorted[3].field1, '1') + assert.equal(sorted[4].field1, '0.1') + }) +}) From f2ebd77edc2030a534af71f61d22fecd492e9471 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Fri, 13 May 2022 11:28:35 +0200 Subject: [PATCH 190/191] Bump version to 5.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 902fde25..f6cd28c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "LuciusWeb", - "version": "5.2.0", + "version": "5.3.0", "description": "Web interface for ComPass aka Lucius", "repository": { "type": "git", From 41aa4846daaec47e766e3326fff58a7710b0f0c9 Mon Sep 17 00:00:00 2001 From: Hendrik Cannoodt Date: Tue, 31 May 2022 17:24:59 +0200 Subject: [PATCH 191/191] Add changelog entry --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5247de96..0480ded7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## Version 5.3.0 + +### Functionality + +- Add filters to the sample selection component + ## Version 5.2.0 ### Functionality