1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2026-03-24 23:16:18 +08:00

Compare commits

..

39 Commits

Author SHA1 Message Date
lana-k
c2c376219f fix lint 2026-02-28 17:59:39 +01:00
lana-k
0199415dde 0.29.0 2026-02-28 17:44:17 +01:00
lana-k
534b186d76 tests for graph editor 2026-02-28 16:53:43 +01:00
lana-k
4f6efb5bda test for DataView 2026-02-26 22:32:24 +01:00
lana-k
2c0b8f9124 enable webgl in chromium 2026-02-25 22:58:28 +01:00
lana-k
5265f5493e splitpanes test 2026-02-24 21:28:11 +01:00
lana-k
c0e59f6fb8 update run result test 2026-02-24 21:02:06 +01:00
lana-k
7471744633 add tests for Value Viewer 2026-02-24 20:53:03 +01:00
lana-k
e6e5efa8c6 tests for graphHelper 2026-02-22 22:30:18 +01:00
lana-k
57c36b3900 tests for migration 2026-02-07 22:08:39 +01:00
lana-k
1e8c1761e6 #133 highlight nodes and edges 2026-02-07 21:18:49 +01:00
lana-k
dd30e17ff5 #133 Add node data viewer 2026-01-25 22:15:48 +01:00
lana-k
4e5adc147f #132 node opacity 2026-01-22 22:25:36 +01:00
lana-k
7edc196a02 change repo structure 2026-01-15 21:53:12 +01:00
lana-k
85b5a200e2 fix tinycolor2 bundle 2025-12-27 21:11:01 +01:00
lana-k
a0ef93921f #131 fix label color 2025-12-26 20:56:08 +01:00
lana-k
859cd2ccfc #129 fix icon 2025-12-25 12:29:28 +01:00
lana-k
a59946c09d remove karma config 2025-12-24 22:14:48 +01:00
lana-k
7b06b3d9c8 uninstall mesa 2025-12-24 22:06:39 +01:00
lana-k
ced933f497 revert firefox base and env 2025-12-24 21:59:23 +01:00
lana-k
cda368f109 xvfb 2025-12-24 21:49:53 +01:00
lana-k
df67466c2f firefox base 2025-12-24 21:41:29 +01:00
lana-k
528549ae5a LIBGL_ALWAYS_SOFTWARE 2025-12-24 21:36:47 +01:00
lana-k
20f4dcc645 fix package names 2025-12-24 17:51:40 +01:00
lana-k
b8353ef0ce install mesa 2025-12-24 17:49:07 +01:00
lana-k
7975f419c9 anoter settings 2025-12-24 17:40:18 +01:00
lana-k
72aa0dd80b another settings 2025-12-24 17:30:05 +01:00
lana-k
e000ee71fc ensure webgl is enabled infirefox 2025-12-24 17:25:25 +01:00
lana-k
b6a12668d3 #43 fix lint errors 2025-12-24 16:17:49 +01:00
lana-k
713f5ac768 #43 graph 0.28.0 2025-12-23 21:41:17 +01:00
lana-k
5492609c3a update readme 2025-12-23 21:28:32 +01:00
lana-k
8bfd0f5944 tests 2025-12-23 21:15:44 +01:00
lana-k
a8006bcf52 tests 2025-12-17 21:26:57 +01:00
lana-k
1463f93bb0 tests for layouts 2025-12-13 17:57:48 +01:00
lana-k
5108495430 test for canvas 2025-12-13 13:00:13 +01:00
lana-k
d28968e539 tests 2025-12-07 19:56:16 +01:00
lana-k
68221cba6d tests 2025-11-15 14:29:41 +01:00
lana-k
65c1c18fcb tests 2025-11-08 22:23:38 +01:00
lana-k
d7db6a0f5d fix tests 2025-11-02 12:31:32 +01:00
89 changed files with 6767 additions and 517 deletions

View File

@@ -23,7 +23,10 @@ jobs:
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
sudo add-apt-repository -y ppa:mozillateam/ppa sudo add-apt-repository -y ppa:mozillateam/ppa
sudo apt-get update sudo apt-get update
sudo apt-get install -y chromium-browser firefox-esr sudo apt-get install -y \
chromium-browser \
firefox-esr \
xvfb
- name: Update npm - name: Update npm
run: npm install -g npm@10 run: npm install -g npm@10
@@ -35,4 +38,4 @@ jobs:
run: npm run lint -- --no-fix run: npm run lint -- --no-fix
- name: Run karma tests - name: Run karma tests
run: npm run test run: xvfb-run -a npm test

View File

@@ -9,7 +9,7 @@ of SQLite databases, CSV, JSON or NDJSON files.
With sqliteviz you can: With sqliteviz you can:
- run SQL queries against a SQLite database and create [Plotly][11] charts and pivot tables based on the result sets - run SQL queries against a SQLite database and create [Plotly][11] charts, graphs and pivot tables based on the result sets
- import a CSV/JSON/NDJSON file into a SQLite database and visualize imported data - import a CSV/JSON/NDJSON file into a SQLite database and visualize imported data
- export result set to CSV file - export result set to CSV file
- manage inquiries and run them against different databases - manage inquiries and run them against different databases
@@ -33,7 +33,9 @@ It's a kind of middleground between [Plotly Falcon][1] and [Redash][2].
## Components ## Components
It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4] and [Vue-Codemirror][8] in [Vue.js][5]. CSV parsing is performed with [Papa Parse][9]. It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4]
and [Vue-Codemirror][8] in [Vue.js][5]. CSV parsing is performed with [Papa Parse][9].
Graphs are visualized with [Sigma.js][13] and [Graphology][14].
[1]: https://github.com/plotly/falcon [1]: https://github.com/plotly/falcon
[2]: https://github.com/getredash/redash [2]: https://github.com/getredash/redash
@@ -47,3 +49,5 @@ It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4]
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries [10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
[11]: https://github.com/plotly/plotly.js [11]: https://github.com/plotly/plotly.js
[12]: https://github.com/nicolaskruchten/pivottable [12]: https://github.com/nicolaskruchten/pivottable
[13]: https://www.sigmajs.org/
[14]: https://graphology.github.io/

View File

@@ -92,11 +92,23 @@ module.exports = function (config) {
'dom.w3c_touch_events.enabled': 1, 'dom.w3c_touch_events.enabled': 1,
'dom.events.asyncClipboard.clipboardItem': true 'dom.events.asyncClipboard.clipboardItem': true
} }
},
ChromiumHeadlessWebGL: {
base: 'ChromiumHeadless',
flags: [
'--headless=new',
'--use-angle=swiftshader',
'--use-gl=angle',
'--enable-webgl',
'--ignore-gpu-blocklist',
'--disable-gpu-sandbox',
'--no-sandbox'
]
} }
}, },
// start these browsers // start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['ChromiumHeadless', 'FirefoxHeadlessTouch'], browsers: ['ChromiumHeadlessWebGL', 'FirefoxHeadlessTouch'],
// Continuous Integration mode // Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits // if true, Karma captures browsers, runs the tests and exits

18
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "sqliteviz", "name": "sqliteviz",
"version": "0.27.1", "version": "0.28.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sqliteviz", "name": "sqliteviz",
"version": "0.27.1", "version": "0.28.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@sigma/export-image": "^3.0.0", "@sigma/export-image": "^3.0.0",
@@ -32,6 +32,7 @@
"sigma": "^3.0.1", "sigma": "^3.0.1",
"sql.js": "file:./lib/sql-js", "sql.js": "file:./lib/sql-js",
"tiny-emitter": "^2.1.0", "tiny-emitter": "^2.1.0",
"tinycolor2": "^1.4.2",
"veaury": "^2.5.1", "veaury": "^2.5.1",
"vue": "^3.5.11", "vue": "^3.5.11",
"vue-final-modal": "^4.5.5", "vue-final-modal": "^4.5.5",
@@ -13341,6 +13342,11 @@
"react": "^0.14.0 || ^15.0.0" "react": "^0.14.0 || ^15.0.0"
} }
}, },
"node_modules/react-chart-editor/node_modules/tinycolor2": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
},
"node_modules/react-color": { "node_modules/react-color": {
"version": "2.19.3", "version": "2.19.3",
"license": "MIT", "license": "MIT",
@@ -15514,8 +15520,12 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/tinycolor2": { "node_modules/tinycolor2": {
"version": "1.6.0", "version": "1.4.2",
"license": "MIT" "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
"integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==",
"engines": {
"node": "*"
}
}, },
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.12", "version": "0.2.12",

View File

@@ -1,6 +1,6 @@
{ {
"name": "sqliteviz", "name": "sqliteviz",
"version": "0.27.1", "version": "0.29.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": true, "private": true,
"type": "module", "type": "module",
@@ -36,6 +36,7 @@
"sigma": "^3.0.1", "sigma": "^3.0.1",
"sql.js": "file:./lib/sql-js", "sql.js": "file:./lib/sql-js",
"tiny-emitter": "^2.1.0", "tiny-emitter": "^2.1.0",
"tinycolor2": "^1.4.2",
"veaury": "^2.5.1", "veaury": "^2.5.1",
"vue": "^3.5.11", "vue": "^3.5.11",
"vue-final-modal": "^4.5.5", "vue-final-modal": "^4.5.5",

View File

@@ -27,7 +27,7 @@
<script> <script>
import CloseIcon from '@/components/svg/close' import CloseIcon from '@/components/svg/close'
import { version } from '../../../package.json' import { version } from '../../package.json'
export default { export default {
name: 'AppDiagnosticInfo', name: 'AppDiagnosticInfo',

View File

@@ -120,8 +120,8 @@ export default {
this.resizeObserver.observe(this.$refs.chartContainer) this.resizeObserver.observe(this.$refs.chartContainer)
if (this.dataSources) { if (this.dataSources) {
dereference.default(this.state.data, this.dataSources) dereference.default(this.state.data, this.dataSources)
this.updatePlotly()
} }
this.handleResize()
}, },
activated() { activated() {
this.useResizeHandler = true this.useResizeHandler = true
@@ -134,6 +134,10 @@ export default {
}, },
methods: { methods: {
async handleResize() { async handleResize() {
// Call updatePlotly twice because there is a small gap (for scrolling?)
// on right and bottom of the plot.
// After the second call it's good.
this.updatePlotly()
this.updatePlotly() this.updatePlotly()
}, },
onRender() { onRender() {

View File

@@ -23,7 +23,7 @@
<script> <script>
import tooltipMixin from '@/tooltipMixin' import tooltipMixin from '@/tooltipMixin'
import LoadingIndicator from '@/components/LoadingIndicator' import LoadingIndicator from '@/components/Common/LoadingIndicator'
export default { export default {
name: 'SideBarButton', name: 'SideBarButton',

View File

@@ -46,7 +46,7 @@
</template> </template>
<script> <script>
import LoadingIndicator from '@/components/LoadingIndicator' import LoadingIndicator from '@/components/Common/LoadingIndicator'
import CloseIcon from '@/components/svg/close' import CloseIcon from '@/components/svg/close'
export default { export default {

View File

@@ -18,7 +18,7 @@
</template> </template>
<script> <script>
import LoadingIndicator from '@/components/LoadingIndicator' import LoadingIndicator from '@/components/Common/LoadingIndicator'
export default { export default {
name: 'Logs', name: 'Logs',

View File

@@ -13,16 +13,16 @@
:style="movableSplitterStyle" :style="movableSplitterStyle"
/> />
<div <div
v-show="!before.hidden"
ref="left" ref="left"
class="splitpanes-pane" class="splitpanes-pane"
:size="paneBefore.size"
max-size="30"
:style="styles.before" :style="styles.before"
> >
<slot name="left-pane" /> <slot name="left-pane" />
</div> </div>
<!-- Splitter start--> <!-- Splitter start-->
<div <div
v-show="!before.hidden && !after.hidden"
class="splitpanes-splitter" class="splitpanes-splitter"
@mousedown="bindEvents" @mousedown="bindEvents"
@touchstart="bindEvents" @touchstart="bindEvents"
@@ -64,7 +64,12 @@
</div> </div>
</div> </div>
<!-- splitter end --> <!-- splitter end -->
<div ref="right" class="splitpanes-pane" :style="styles.after"> <div
v-show="!after.hidden"
ref="right"
class="splitpanes-pane"
:style="styles.after"
>
<slot name="right-pane" /> <slot name="right-pane" />
</div> </div>
</div> </div>
@@ -114,10 +119,12 @@ export default {
styles() { styles() {
return { return {
before: { before: {
[this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%` [this.horizontal ? 'height' : 'width']:
`${this.after.hidden ? 100 : this.paneBefore.size}%`
}, },
after: { after: {
[this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%` [this.horizontal ? 'height' : 'width']:
`${this.before.hidden ? 100 : this.paneAfter.size}%`
} }
} }
}, },

View File

@@ -102,11 +102,11 @@
<script> <script>
import csv from '@/lib/csv' import csv from '@/lib/csv'
import CloseIcon from '@/components/svg/close' import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField' import TextField from '@/components/Common/TextField'
import DelimiterSelector from './DelimiterSelector' import DelimiterSelector from './DelimiterSelector'
import CheckBox from '@/components/CheckBox' import CheckBox from '@/components/Common/CheckBox'
import SqlTable from '@/components/SqlTable' import SqlTable from '@/components/SqlTable'
import Logs from '@/components/Logs' import Logs from '@/components/Common/Logs'
import time from '@/lib/utils/time' import time from '@/lib/utils/time'
import fIo from '@/lib/utils/fileIo' import fIo from '@/lib/utils/fileIo'
import events from '@/lib/utils/events' import events from '@/lib/utils/events'

View File

@@ -7,15 +7,18 @@
v-model:exportToPngEnabled="exportToPngEnabled" v-model:exportToPngEnabled="exportToPngEnabled"
v-model:exportToSvgEnabled="exportToSvgEnabled" v-model:exportToSvgEnabled="exportToSvgEnabled"
v-model:exportToHtmlEnabled="exportToHtmlEnabled" v-model:exportToHtmlEnabled="exportToHtmlEnabled"
v-model:exportToClipboardEnabled="exportToClipboardEnabled"
:initOptions="initOptionsByMode[mode]" :initOptions="initOptionsByMode[mode]"
:data-sources="dataSource" :data-sources="dataSource"
:showViewSettings="showViewSettings" :showViewSettings="showViewSettings"
:showValueViewer="viewValuePanelVisible"
@loading-image-completed="loadingImage = false" @loading-image-completed="loadingImage = false"
@update="$emit('update')" @update="$emit('update')"
/> />
</div> </div>
<side-tool-bar panel="dataView" @switch-to="$emit('switchTo', $event)"> <side-tool-bar panel="dataView" @switch-to="$emit('switchTo', $event)">
<icon-button <icon-button
ref="chartBtn"
:active="mode === 'chart'" :active="mode === 'chart'"
tooltip="Switch to chart" tooltip="Switch to chart"
tooltipPosition="top-left" tooltipPosition="top-left"
@@ -33,6 +36,7 @@
<pivot-icon /> <pivot-icon />
</icon-button> </icon-button>
<icon-button <icon-button
ref="graphBtn"
:active="mode === 'graph'" :active="mode === 'graph'"
tooltip="Switch to graph" tooltip="Switch to graph"
tooltipPosition="top-left" tooltipPosition="top-left"
@@ -53,9 +57,21 @@
<settings-icon /> <settings-icon />
</icon-button> </icon-button>
<icon-button
v-if="mode === 'graph'"
ref="viewNodeOrEdgeBtn"
tooltip="View node or edge details"
tooltipPosition="top-left"
:active="viewValuePanelVisible"
@click="viewValuePanelVisible = !viewValuePanelVisible"
>
<view-cell-value-icon />
</icon-button>
<div class="side-tool-bar-divider" /> <div class="side-tool-bar-divider" />
<icon-button <icon-button
ref="pngExportBtn"
:disabled="!exportToPngEnabled || loadingImage" :disabled="!exportToPngEnabled || loadingImage"
:loading="loadingImage" :loading="loadingImage"
tooltip="Save as PNG image" tooltip="Save as PNG image"
@@ -85,6 +101,7 @@
</icon-button> </icon-button>
<icon-button <icon-button
ref="copyToClipboardBtn" ref="copyToClipboardBtn"
:disabled="!exportToClipboardEnabled"
:loading="copyingImage" :loading="copyingImage"
tooltip="Copy visualisation to clipboard" tooltip="Copy visualisation to clipboard"
tooltipPosition="top-left" tooltipPosition="top-left"
@@ -108,11 +125,11 @@
</template> </template>
<script> <script>
import Chart from './Chart/index.vue' import Chart from '@/components/Chart.vue'
import Pivot from './Pivot/index.vue' import Pivot from '@/components/Pivot'
import Graph from './Graph/index.vue' import Graph from '@/components/Graph/index.vue'
import SideToolBar from '../SideToolBar' import SideToolBar from '@/components/SideToolBar'
import IconButton from '@/components/IconButton' import IconButton from '@/components/Common/IconButton'
import ChartIcon from '@/components/svg/chart' import ChartIcon from '@/components/svg/chart'
import PivotIcon from '@/components/svg/pivot' import PivotIcon from '@/components/svg/pivot'
import GraphIcon from '@/components/svg/graph.vue' import GraphIcon from '@/components/svg/graph.vue'
@@ -121,8 +138,9 @@ import HtmlIcon from '@/components/svg/html'
import ExportToSvgIcon from '@/components/svg/exportToSvg' import ExportToSvgIcon from '@/components/svg/exportToSvg'
import PngIcon from '@/components/svg/png' import PngIcon from '@/components/svg/png'
import ClipboardIcon from '@/components/svg/clipboard' import ClipboardIcon from '@/components/svg/clipboard'
import ViewCellValueIcon from '@/components/svg/viewCellValue'
import cIo from '@/lib/utils/clipboardIo' import cIo from '@/lib/utils/clipboardIo'
import loadingDialog from '@/components/LoadingDialog.vue' import loadingDialog from '@/components/Common/LoadingDialog.vue'
import time from '@/lib/utils/time' import time from '@/lib/utils/time'
import events from '@/lib/utils/events' import events from '@/lib/utils/events'
@@ -139,6 +157,7 @@ export default {
GraphIcon, GraphIcon,
SettingsIcon, SettingsIcon,
ExportToSvgIcon, ExportToSvgIcon,
ViewCellValueIcon,
PngIcon, PngIcon,
HtmlIcon, HtmlIcon,
ClipboardIcon, ClipboardIcon,
@@ -156,6 +175,7 @@ export default {
exportToPngEnabled: true, exportToPngEnabled: true,
exportToSvgEnabled: true, exportToSvgEnabled: true,
exportToHtmlEnabled: true, exportToHtmlEnabled: true,
exportToClipboardEnabled: true,
loadingImage: false, loadingImage: false,
copyingImage: false, copyingImage: false,
preparingCopy: false, preparingCopy: false,
@@ -166,7 +186,8 @@ export default {
graph: this.initMode === 'graph' ? this.initOptions : null graph: this.initMode === 'graph' ? this.initOptions : null
}, },
showLoadingDialog: false, showLoadingDialog: false,
showViewSettings: true showViewSettings: true,
viewValuePanelVisible: false
} }
}, },
computed: { computed: {
@@ -178,6 +199,7 @@ export default {
mode(newMode, oldMode) { mode(newMode, oldMode) {
this.$emit('update') this.$emit('update')
this.exportToPngEnabled = true this.exportToPngEnabled = true
this.exportToClipboardEnabled = true
this.initOptionsByMode[oldMode] = this.getOptionsForSave() this.initOptionsByMode[oldMode] = this.getOptionsForSave()
} }
}, },

View File

@@ -1,12 +1,15 @@
<template> <template>
<Field label="Scaling ratio"> <Field label="Scaling ratio" fieldContainerClassName="test_fa2_scaling">
<NumericInput <NumericInput
:value="modelValue.scalingRatio" :value="modelValue.scalingRatio"
@update="update('scalingRatio', $event)" @update="update('scalingRatio', $event)"
/> />
</Field> </Field>
<Field label="Prevent overlapping"> <Field
label="Prevent overlapping"
fieldContainerClassName="test_fa2_adjustSizes"
>
<RadioBlocks <RadioBlocks
:options="booleanOptions" :options="booleanOptions"
:activeOption="modelValue.adjustSizes" :activeOption="modelValue.adjustSizes"
@@ -14,7 +17,10 @@
/> />
</Field> </Field>
<Field label="Barnes-Hut optimize"> <Field
label="Barnes-Hut optimize"
fieldContainerClassName="test_fa2_barnes_hut"
>
<RadioBlocks <RadioBlocks
:options="booleanOptions" :options="booleanOptions"
:activeOption="modelValue.barnesHutOptimize" :activeOption="modelValue.barnesHutOptimize"
@@ -22,14 +28,21 @@
/> />
</Field> </Field>
<Field v-show="modelValue.barnesHutOptimize" label="Barnes-Hut Theta"> <Field
v-show="modelValue.barnesHutOptimize"
label="Barnes-Hut Theta"
fieldContainerClassName="test_fa2_barnes_theta"
>
<NumericInput <NumericInput
:value="modelValue.barnesHutTheta" :value="modelValue.barnesHutTheta"
@update="update('barnesHutTheta', $event)" @update="update('barnesHutTheta', $event)"
/> />
</Field> </Field>
<Field label="Strong gravity mode"> <Field
label="Strong gravity mode"
fieldContainerClassName="test_fa2_strong_gravity"
>
<RadioBlocks <RadioBlocks
:options="booleanOptions" :options="booleanOptions"
:activeOption="modelValue.strongGravityMode" :activeOption="modelValue.strongGravityMode"
@@ -37,7 +50,10 @@
/> />
</Field> </Field>
<Field label="Noack's LinLog model"> <Field
label="Noack's LinLog model"
fieldContainerClassName="test_fa2_lin_log"
>
<RadioBlocks <RadioBlocks
:options="booleanOptions" :options="booleanOptions"
:activeOption="modelValue.linLogMode" :activeOption="modelValue.linLogMode"
@@ -45,7 +61,10 @@
/> />
</Field> </Field>
<Field label="Out bound attraction distribution"> <Field
label="Outbound attraction distribution"
fieldContainerClassName="test_fa2_outbound_attraction"
>
<RadioBlocks <RadioBlocks
:options="booleanOptions" :options="booleanOptions"
:activeOption="modelValue.outboundAttractionDistribution" :activeOption="modelValue.outboundAttractionDistribution"
@@ -53,7 +72,7 @@
/> />
</Field> </Field>
<Field label="Slow down"> <Field label="Slow down" fieldContainerClassName="test_fa2_slow_down">
<NumericInput <NumericInput
:value="modelValue.slowDown" :value="modelValue.slowDown"
:min="0" :min="0"
@@ -65,10 +84,15 @@
<Dropdown <Dropdown
:options="keyOptions" :options="keyOptions"
:value="modelValue.weightSource" :value="modelValue.weightSource"
className="test_fa2_weight_source"
@change="update('weightSource', $event)" @change="update('weightSource', $event)"
/> />
</Field> </Field>
<Field v-show="modelValue.weightSource" label="Edge weight influence"> <Field
v-show="modelValue.weightSource"
label="Edge weight influence"
fieldContainerClassName="test_fa2_weight_influence"
>
<NumericInput <NumericInput
:value="modelValue.edgeWeightInfluence" :value="modelValue.edgeWeightInfluence"
@update="update('edgeWeightInfluence', $event)" @update="update('edgeWeightInfluence', $event)"

View File

@@ -40,7 +40,6 @@
import { applyPureReactInVue } from 'veaury' import { applyPureReactInVue } from 'veaury'
import Field from 'react-chart-editor/lib/components/fields/Field' import Field from 'react-chart-editor/lib/components/fields/Field'
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput' import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import Multiselect from 'vue-multiselect' import Multiselect from 'vue-multiselect'
import 'react-chart-editor/lib/react-chart-editor.css' import 'react-chart-editor/lib/react-chart-editor.css'
@@ -48,7 +47,6 @@ export default {
components: { components: {
Field: applyPureReactInVue(Field), Field: applyPureReactInVue(Field),
NumericInput: applyPureReactInVue(NumericInput), NumericInput: applyPureReactInVue(NumericInput),
Dropdown: applyPureReactInVue(Dropdown),
Multiselect Multiselect
}, },
props: { props: {

View File

@@ -1,18 +1,21 @@
<template> <template>
<Field label="Color"> <Field label="Color" fieldContainerClassName="test_edge_color">
<RadioBlocks <RadioBlocks
:options="edgeColorTypeOptions" :options="edgeColorTypeOptions"
:activeOption="modelValue.type" :activeOption="modelValue.type"
@option-change="updateColorType" @option-change="updateColorType"
/> />
<Field v-if="modelValue.type === 'constant'"> <Field
v-if="modelValue.type === 'constant'"
fieldContainerClassName="test_edge_color_value"
>
<ColorPicker <ColorPicker
:selectedColor="modelValue.value" :selectedColor="modelValue.value"
@color-change="updateSettings('value', $event)" @color-change="updateSettings('value', $event)"
/> />
</Field> </Field>
<template v-else> <template v-else>
<Field> <Field fieldContainerClassName="test_edge_color_value">
<Dropdown <Dropdown
v-if="modelValue.type === 'variable'" v-if="modelValue.type === 'variable'"
:options="keyOptions" :options="keyOptions"
@@ -21,7 +24,7 @@
/> />
</Field> </Field>
<Field> <Field fieldContainerClassName="test_edge_color_mapping_mode">
<RadioBlocks <RadioBlocks
:options="colorSourceUsageOptions" :options="colorSourceUsageOptions"
:activeOption="modelValue.sourceUsage" :activeOption="modelValue.sourceUsage"
@@ -42,6 +45,7 @@
<Field <Field
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'" v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
label="Color as" label="Color as"
fieldContainerClassName="test_edge_color_as"
> >
<RadioBlocks <RadioBlocks
:options="сolorAsOptions" :options="сolorAsOptions"
@@ -53,6 +57,7 @@
<Field <Field
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'" v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
label="Colorscale direction" label="Colorscale direction"
fieldContainerClassName="test_edge_color_colorscale_direction"
> >
<RadioBlocks <RadioBlocks
:options="сolorscaleDirections" :options="сolorscaleDirections"

View File

@@ -1,12 +1,12 @@
<template> <template>
<Field label="Size"> <Field label="Size" fieldContainerClassName="test_edge_size">
<RadioBlocks <RadioBlocks
:options="edgeSizeTypeOptions" :options="edgeSizeTypeOptions"
:activeOption="modelValue.type" :activeOption="modelValue.type"
@option-change="updateSizeType" @option-change="updateSizeType"
/> />
<Field> <Field fieldContainerClassName="test_edge_size_value">
<NumericInput <NumericInput
v-if="modelValue.type === 'constant'" v-if="modelValue.type === 'constant'"
:value="modelValue.value" :value="modelValue.value"
@@ -23,14 +23,14 @@
</Field> </Field>
<template v-if="modelValue.type !== 'constant'"> <template v-if="modelValue.type !== 'constant'">
<Field label="Size scale"> <Field label="Size scale" fieldContainerClassName="test_edge_size_scale">
<NumericInput <NumericInput
:value="modelValue.scale" :value="modelValue.scale"
@update="updateSettings('scale', $event)" @update="updateSettings('scale', $event)"
/> />
</Field> </Field>
<Field label="Minimum size"> <Field label="Minimum size" fieldContainerClassName="test_edge_size_min">
<NumericInput <NumericInput
:value="modelValue.min" :value="modelValue.min"
@update="updateSettings('min', $event)" @update="updateSettings('min', $event)"

View File

@@ -1,5 +1,8 @@
<template> <template>
<Field label="Initial iterations"> <Field
label="Initial iterations"
fieldContainerClassName="test_fa2_iteration_amount"
>
<NumericInput <NumericInput
:value="modelValue.initialIterationsAmount" :value="modelValue.initialIterationsAmount"
:min="1" :min="1"
@@ -7,7 +10,7 @@
/> />
</Field> </Field>
<Field label="Gravity"> <Field label="Gravity" fieldContainerClassName="test_fa2_gravity">
<NumericInput <NumericInput
:value="modelValue.gravity" :value="modelValue.gravity"
@update="update('gravity', $event)" @update="update('gravity', $event)"

View File

@@ -11,10 +11,11 @@
documentation</a documentation</a
>. >.
</Field> </Field>
<Field label="Object type"> <Field ref="objectTypeField" label="Object type">
<Dropdown <Dropdown
:options="keysOptions" :options="keysOptions"
:value="settings.structure.objectType" :value="settings.structure.objectType"
className="test_object_type_select"
@change="updateStructure('objectType', $event)" @change="updateStructure('objectType', $event)"
/> />
<Field> <Field>
@@ -27,6 +28,7 @@
<Dropdown <Dropdown
:options="keysOptions" :options="keysOptions"
:value="settings.structure.nodeId" :value="settings.structure.nodeId"
className="test_node_id_select"
@change="updateStructure('nodeId', $event)" @change="updateStructure('nodeId', $event)"
/> />
<Field> A field keeping unique node identifier. </Field> <Field> A field keeping unique node identifier. </Field>
@@ -36,6 +38,7 @@
<Dropdown <Dropdown
:options="keysOptions" :options="keysOptions"
:value="settings.structure.edgeSource" :value="settings.structure.edgeSource"
className="test_edge_source_select"
@change="updateStructure('edgeSource', $event)" @change="updateStructure('edgeSource', $event)"
/> />
<Field> <Field>
@@ -47,6 +50,7 @@
<Dropdown <Dropdown
:options="keysOptions" :options="keysOptions"
:value="settings.structure.edgeTarget" :value="settings.structure.edgeTarget"
className="test_edge_target_select"
@change="updateStructure('edgeTarget', $event)" @change="updateStructure('edgeTarget', $event)"
/> />
<Field> <Field>
@@ -63,6 +67,15 @@
@color-change="settings.style.backgroundColor = $event" @color-change="settings.style.backgroundColor = $event"
/> />
</Field> </Field>
<Field label="Highlight mode">
<Dropdown
:options="highlightModeOptions"
:value="settings.style.highlightMode"
className="test_highlight_mode_select"
@change="updateHighlightNodeMode"
/>
</Field>
</Fold> </Fold>
</Panel> </Panel>
<Panel group="Style" name="Nodes"> <Panel group="Style" name="Nodes">
@@ -71,6 +84,7 @@
<Dropdown <Dropdown
:options="keysOptions" :options="keysOptions"
:value="settings.style.nodes.label.source" :value="settings.style.nodes.label.source"
className="test_label_select"
@change="updateNodes('label.source', $event)" @change="updateNodes('label.source', $event)"
/> />
</Field> </Field>
@@ -97,7 +111,10 @@
<Panel group="Style" name="Edges"> <Panel group="Style" name="Edges">
<Fold name="Edges"> <Fold name="Edges">
<Field label="Direction"> <Field
label="Direction"
fieldContainerClassName="test_edge_direction"
>
<RadioBlocks <RadioBlocks
:options="visibilityOptions" :options="visibilityOptions"
:activeOption="settings.style.edges.showDirection" :activeOption="settings.style.edges.showDirection"
@@ -109,6 +126,7 @@
<Dropdown <Dropdown
:options="keysOptions" :options="keysOptions"
:value="settings.style.edges.label.source" :value="settings.style.edges.label.source"
className="test_edge_label_select"
@change="updateEdges('label.source', $event)" @change="updateEdges('label.source', $event)"
/> />
</Field> </Field>
@@ -139,6 +157,8 @@
<Dropdown <Dropdown
:options="layoutOptions" :options="layoutOptions"
:value="settings.layout.type" :value="settings.layout.type"
:clearable="false"
className="test_layout_algorithm_select"
@change="updateLayout($event)" @change="updateLayout($event)"
/> />
</Field> </Field>
@@ -159,10 +179,18 @@
/> />
</Fold> </Fold>
<div class="force-atlas-buttons"> <div class="force-atlas-buttons">
<Button variant="secondary" @click="resetFA2LayoutSettings"> <Button
variant="secondary"
class="test_fa2_reset"
@click="resetFA2LayoutSettings"
>
Reset Reset
</Button> </Button>
<Button variant="primary" @click="toggleFA2Layout"> <Button
variant="primary"
class="test_fa2_toggle"
@click="toggleFA2Layout"
>
<template #node:icon> <template #node:icon>
<div <div
:style="{ :style="{
@@ -183,6 +211,7 @@
<div <div
ref="graph" ref="graph"
class="test_graph_output"
:style="{ :style="{
height: '100%', height: '100%',
width: '100%', width: '100%',
@@ -205,10 +234,11 @@ import Button from 'react-chart-editor/lib/components/widgets/Button'
import Field from 'react-chart-editor/lib/components/fields/Field' import Field from 'react-chart-editor/lib/components/fields/Field'
import RandomLayoutSettings from '@/components/Graph/RandomLayoutSettings.vue' import RandomLayoutSettings from '@/components/Graph/RandomLayoutSettings.vue'
import ForceAtlasLayoutSettings from '@/components/Graph/ForceAtlasLayoutSettings.vue' import ForceAtlasLayoutSettings from '@/components/Graph/ForceAtlasLayoutSettings.vue'
// eslint-disable-next-line max-len
import AdvancedForceAtlasLayoutSettings from '@/components/Graph/AdvancedForceAtlasLayoutSettings.vue' import AdvancedForceAtlasLayoutSettings from '@/components/Graph/AdvancedForceAtlasLayoutSettings.vue'
import CirclePackLayoutSettings from '@/components/Graph/CirclePackLayoutSettings.vue' import CirclePackLayoutSettings from '@/components/Graph/CirclePackLayoutSettings.vue'
import FA2Layout from 'graphology-layout-forceatlas2/worker' import FA2Layout from 'graphology-layout-forceatlas2/worker'
import forceAtlas2 from 'graphology-layout-forceatlas2' import * as forceAtlas2 from 'graphology-layout-forceatlas2'
import RunIcon from '@/components/svg/run.vue' import RunIcon from '@/components/svg/run.vue'
import StopIcon from '@/components/svg/stop.vue' import StopIcon from '@/components/svg/stop.vue'
import { downloadAsPNG, drawOnCanvas } from '@sigma/export-image' import { downloadAsPNG, drawOnCanvas } from '@sigma/export-image'
@@ -216,7 +246,9 @@ import {
buildNodes, buildNodes,
buildEdges, buildEdges,
updateNodes, updateNodes,
updateEdges updateEdges,
reduceNodes,
reduceEdges
} from '@/lib/graphHelper' } from '@/lib/graphHelper'
import Graph from 'graphology' import Graph from 'graphology'
import { circular, random, circlepack } from 'graphology-layout' import { circular, random, circlepack } from 'graphology-layout'
@@ -256,7 +288,7 @@ export default {
initOptions: Object, initOptions: Object,
showViewSettings: Boolean showViewSettings: Boolean
}, },
emits: ['update'], emits: ['update', 'selectItem', 'clearSelection'],
data() { data() {
return { return {
graph: new Graph({ multi: true, allowSelfLoops: true }), graph: new Graph({ multi: true, allowSelfLoops: true }),
@@ -279,7 +311,10 @@ export default {
circlepack: CirclePackLayoutSettings, circlepack: CirclePackLayoutSettings,
forceAtlas2: ForceAtlasLayoutSettings forceAtlas2: ForceAtlasLayoutSettings
}), }),
selectedNodeId: undefined,
hoveredNodeId: undefined,
selectedEdgeId: undefined,
hoveredEdgeId: undefined,
settings: this.initOptions settings: this.initOptions
? JSON.parse(JSON.stringify(this.initOptions)) ? JSON.parse(JSON.stringify(this.initOptions))
: { : {
@@ -291,6 +326,7 @@ export default {
}, },
style: { style: {
backgroundColor: 'white', backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: { nodes: {
size: { size: {
type: 'constant', type: 'constant',
@@ -298,7 +334,8 @@ export default {
}, },
color: { color: {
type: 'constant', type: 'constant',
value: '#1F77B4' value: '#1F77B4',
opacity: 100
}, },
label: { label: {
source: null, source: null,
@@ -330,7 +367,15 @@ export default {
random: null, random: null,
circlepack: null, circlepack: null,
forceAtlas2: null forceAtlas2: null
} },
highlightModeOptions: markRaw([
{ label: 'Node alone', value: 'node_alone' },
{ label: 'Node and neighbors', value: 'node_and_neighbors' },
{
label: 'Include edges between neighbors',
value: 'include_neighbor_edges'
}
])
} }
}, },
computed: { computed: {
@@ -338,11 +383,10 @@ export default {
if (!this.dataSources) { if (!this.dataSources) {
return [] return []
} }
const firstColumnName = Object.keys(this.dataSources)[0]
try { try {
return ( return (
this.dataSources[Object.keys(this.dataSources)[0] || 'doc'].map( this.dataSources[firstColumnName].map(json => JSON.parse(json)) || []
json => JSON.parse(json)
) || []
) )
} catch { } catch {
return [] return []
@@ -358,6 +402,46 @@ export default {
}, new Set()) }, new Set())
return Array.from(keySet) return Array.from(keySet)
},
neighborsOfSelectedNode() {
if (this.settings.style.highlightMode === 'node_alone') {
return undefined
}
return this.selectedNodeId
? new Set(this.graph.neighbors(this.selectedNodeId))
: undefined
},
neighborsOfHoveredNode() {
if (this.settings.style.highlightMode === 'node_alone') {
return undefined
}
return this.hoveredNodeId
? new Set(this.graph.neighbors(this.hoveredNodeId))
: undefined
},
hoveredEdgeExtremities() {
return this.hoveredEdgeId
? this.graph.extremities(this.hoveredEdgeId)
: []
},
selectedEdgeExtremities() {
return this.selectedEdgeId
? this.graph.extremities(this.selectedEdgeId)
: []
},
interactionState() {
return {
selectedNodeId: this.selectedNodeId,
hoveredNodeId: this.hoveredNodeId,
selectedEdgeId: this.selectedEdgeId,
hoveredEdgeId: this.hoveredEdgeId,
neighborsOfSelectedNode: this.neighborsOfSelectedNode,
neighborsOfHoveredNode: this.neighborsOfHoveredNode,
hoveredEdgeExtremities: this.hoveredEdgeExtremities,
selectedEdgeExtremities: this.selectedEdgeExtremities
}
} }
}, },
watch: { watch: {
@@ -402,6 +486,7 @@ export default {
}, },
methods: { methods: {
buildGraph() { buildGraph() {
this.clearSelection()
if (this.renderer) { if (this.renderer) {
this.renderer.kill() this.renderer.kill()
} }
@@ -419,12 +504,85 @@ export default {
renderEdgeLabels: true, renderEdgeLabels: true,
allowInvalidContainer: true, allowInvalidContainer: true,
labelColor: { attribute: 'labelColor', color: '#444444' }, labelColor: { attribute: 'labelColor', color: '#444444' },
edgeLabelColor: { attribute: 'labelColor', color: '#a2b1c6' } edgeLabelColor: { attribute: 'labelColor', color: '#a2b1c6' },
enableEdgeEvents: true,
zIndex: true,
nodeReducer: (node, data) =>
reduceNodes(node, data, this.interactionState, this.settings),
edgeReducer: (edge, data) =>
reduceEdges(
edge,
data,
this.interactionState,
this.settings,
this.graph
)
}) })
this.renderer.on('clickNode', ({ node }) => {
this.selectedNodeId = node
this.selectedEdgeId = undefined
this.$emit('selectItem', this.graph.getNodeAttributes(node).data)
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('clickEdge', ({ edge }) => {
this.selectedEdgeId = edge
this.selectedNodeId = undefined
this.$emit('selectItem', this.graph.getEdgeAttributes(edge).data)
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('clickStage', () => {
this.clearSelection()
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('enterNode', ({ node }) => {
this.hoveredNodeId = node
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('enterEdge', ({ edge }) => {
this.hoveredEdgeId = edge
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('leaveNode', () => {
this.hoveredNodeId = undefined
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('leaveEdge', () => {
this.hoveredEdgeId = undefined
this.renderer.refresh({
skipIndexation: true
})
})
if (this.settings.layout.type === 'forceAtlas2') { if (this.settings.layout.type === 'forceAtlas2') {
this.autoRunFA2Layout() this.autoRunFA2Layout()
} }
}, },
clearSelection() {
this.selectedNodeId = undefined
this.selectedEdgeId = undefined
this.$emit('clearSelection')
},
updateHighlightNodeMode(mode) {
this.settings.style.highlightMode = mode
if (this.renderer) {
this.renderer.refresh({
skipIndexation: true
})
}
},
updateStructure(attributeName, value) { updateStructure(attributeName, value) {
this.settings.structure[attributeName] = value this.settings.structure[attributeName] = value
}, },
@@ -491,7 +649,7 @@ export default {
if (layoutType === 'random') { if (layoutType === 'random') {
random.assign(this.graph, { random.assign(this.graph, {
rng: seedrandom(this.settings.layout.options.seedValue || 1) rng: seedrandom(this.settings.layout.options.seedValue)
}) })
return return
} }
@@ -524,7 +682,7 @@ export default {
this.settings.layout.options.hierarchyAttributes?.map( this.settings.layout.options.hierarchyAttributes?.map(
(_, index) => 'hierarchyAttribute' + index (_, index) => 'hierarchyAttribute' + index
) || [], ) || [],
rng: seedrandom(this.settings.layout.options.seedValue || 1) rng: seedrandom(this.settings.layout.options.seedValue)
}) })
return return
} }
@@ -574,10 +732,6 @@ export default {
} }
}, },
autoRunFA2Layout() { autoRunFA2Layout() {
if (this.fa2Layout.isRunning()) {
this.stopFA2Layout()
}
let iteration = 1 let iteration = 1
this.checkIteration = () => { this.checkIteration = () => {
if ( if (
@@ -592,7 +746,7 @@ export default {
this.fa2Layout.start() this.fa2Layout.start()
}, },
setRecommendedFA2Settings() { setRecommendedFA2Settings() {
const sensibleSettings = forceAtlas2.inferSettings(this.graph) const sensibleSettings = forceAtlas2.default.inferSettings(this.graph)
this.settings.layout.options = { this.settings.layout.options = {
initialIterationsAmount: 50, initialIterationsAmount: 50,
adjustSizes: false, adjustSizes: false,

View File

@@ -1,18 +1,21 @@
<template> <template>
<Field label="Color"> <Field label="Color" fieldContainerClassName="test_node_color">
<RadioBlocks <RadioBlocks
:options="nodeColorTypeOptions" :options="nodeColorTypeOptions"
:activeOption="modelValue.type" :activeOption="modelValue.type"
@option-change="updateColorType" @option-change="updateColorType"
/> />
<Field v-if="modelValue.type === 'constant'"> <Field
v-if="modelValue.type === 'constant'"
fieldContainerClassName="test_node_color_value"
>
<ColorPicker <ColorPicker
:selectedColor="modelValue.value" :selectedColor="modelValue.value"
@color-change="updateSettings('value', $event)" @color-change="updateSettings('value', $event)"
/> />
</Field> </Field>
<template v-else> <template v-else>
<Field> <Field fieldContainerClassName="test_node_color_value">
<Dropdown <Dropdown
v-if="modelValue.type === 'variable'" v-if="modelValue.type === 'variable'"
:options="keyOptions" :options="keyOptions"
@@ -23,11 +26,15 @@
v-if="modelValue.type === 'calculated'" v-if="modelValue.type === 'calculated'"
:options="nodeCalculatedColorMethodOptions" :options="nodeCalculatedColorMethodOptions"
:value="modelValue.method" :value="modelValue.method"
:clearable="false"
@change="updateSettings('method', $event)" @change="updateSettings('method', $event)"
/> />
</Field> </Field>
<Field v-if="modelValue.type === 'variable'"> <Field
v-if="modelValue.type === 'variable'"
fieldContainerClassName="test_node_color_mapping_mode"
>
<RadioBlocks <RadioBlocks
:options="colorSourceUsageOptions" :options="colorSourceUsageOptions"
:activeOption="modelValue.sourceUsage" :activeOption="modelValue.sourceUsage"
@@ -35,7 +42,12 @@
/> />
</Field> </Field>
<Field v-if="modelValue.sourceUsage === 'map_to'"> <Field
v-if="
modelValue.sourceUsage === 'map_to' ||
modelValue.type === 'calculated'
"
>
<ColorscalePicker <ColorscalePicker
:selected="modelValue.colorscale" :selected="modelValue.colorscale"
className="colorscale-picker" className="colorscale-picker"
@@ -45,9 +57,22 @@
</template> </template>
</Field> </Field>
<Field label="Opacity" fieldContainerClassName="test_node_opacity">
<NumericInput
:value="modelValue.opacity"
:showSlider="true"
:integerOnly="true"
:max="100"
:min="0"
units="%"
@update="updateSettings('opacity', $event)"
/>
</Field>
<Field <Field
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'" v-if="modelValue.type === 'map_to' || modelValue.type === 'calculated'"
label="Color as" label="Color as"
fieldContainerClassName="test_node_color_as"
> >
<RadioBlocks <RadioBlocks
:options="сolorAsOptions" :options="сolorAsOptions"
@@ -57,8 +82,11 @@
</Field> </Field>
<Field <Field
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'" v-if="
modelValue.sourceUsage === 'map_to' || modelValue.type === 'calculated'
"
label="Colorscale direction" label="Colorscale direction"
fieldContainerClassName="test_node_color_colorscale_direction"
> >
<RadioBlocks <RadioBlocks
:options="сolorscaleDirections" :options="сolorscaleDirections"
@@ -71,6 +99,7 @@
<script> <script>
import { markRaw } from 'vue' import { markRaw } from 'vue'
import { applyPureReactInVue } from 'veaury' import { applyPureReactInVue } from 'veaury'
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown' import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks' import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
import ColorscalePicker from 'react-chart-editor/lib/components/widgets/ColorscalePicker' import ColorscalePicker from 'react-chart-editor/lib/components/widgets/ColorscalePicker'
@@ -80,6 +109,7 @@ import 'react-chart-editor/lib/react-chart-editor.css'
export default { export default {
components: { components: {
NumericInput: applyPureReactInVue(NumericInput),
Dropdown: applyPureReactInVue(Dropdown), Dropdown: applyPureReactInVue(Dropdown),
RadioBlocks: applyPureReactInVue(RadioBlocks), RadioBlocks: applyPureReactInVue(RadioBlocks),
Field: applyPureReactInVue(Field), Field: applyPureReactInVue(Field),
@@ -116,20 +146,21 @@ export default {
{ label: 'Map to', value: 'map_to' } { label: 'Map to', value: 'map_to' }
]), ]),
defaultColorSettings: { defaultColorSettings: {
constant: { value: '#1F77B4' }, constant: { value: '#1F77B4', opacity: 100 },
variable: { variable: {
source: null, source: null,
sourceUsage: 'map_to', sourceUsage: 'map_to',
colorscale: null, colorscale: null,
mode: 'categorical', mode: 'categorical',
colorscaleDirection: 'normal' colorscaleDirection: 'normal',
opacity: 100
}, },
calculated: { calculated: {
method: 'degree', method: 'degree',
sourceUsage: 'map_to',
colorscale: null, colorscale: null,
mode: 'continious', mode: 'continious',
colorscaleDirection: 'normal' colorscaleDirection: 'normal',
opacity: 100
} }
} }
} }

View File

@@ -1,16 +1,17 @@
<template> <template>
<Field label="Size"> <Field label="Size" fieldContainerClassName="test_node_size">
<RadioBlocks <RadioBlocks
:options="nodeSizeTypeOptions" :options="nodeSizeTypeOptions"
:activeOption="modelValue.type" :activeOption="modelValue.type"
@option-change="updateSizeType" @option-change="updateSizeType"
/> />
<Field> <Field fieldContainerClassName="test_node_size_value">
<NumericInput <NumericInput
v-if="modelValue.type === 'constant'" v-if="modelValue.type === 'constant'"
:value="modelValue.value" :value="modelValue.value"
:min="1" :min="1"
class="test_node_size_value"
@update="updateSettings('value', $event)" @update="updateSettings('value', $event)"
/> />
<Dropdown <Dropdown
@@ -23,20 +24,21 @@
v-if="modelValue.type === 'calculated'" v-if="modelValue.type === 'calculated'"
:options="nodeCalculatedSizeMethodOptions" :options="nodeCalculatedSizeMethodOptions"
:value="modelValue.method" :value="modelValue.method"
:clearable="false"
@change="updateSettings('method', $event)" @change="updateSettings('method', $event)"
/> />
</Field> </Field>
</Field> </Field>
<template v-if="modelValue.type !== 'constant'"> <template v-if="modelValue.type !== 'constant'">
<Field label="Size scale"> <Field label="Size scale" fieldContainerClassName="test_node_size_scale">
<NumericInput <NumericInput
:value="modelValue.scale" :value="modelValue.scale"
@update="updateSettings('scale', $event)" @update="updateSettings('scale', $event)"
/> />
</Field> </Field>
<Field label="Size mode"> <Field label="Size mode" fieldContainerClassName="test_node_size_mode">
<RadioBlocks <RadioBlocks
:options="nodeSizeModeOptions" :options="nodeSizeModeOptions"
:activeOption="modelValue.mode" :activeOption="modelValue.mode"
@@ -44,7 +46,7 @@
/> />
</Field> </Field>
<Field label="Minimum size"> <Field label="Minimum size" fieldContainerClassName="test_node_size_min">
<NumericInput <NumericInput
:value="modelValue.min" :value="modelValue.min"
@update="updateSettings('min', $event)" @update="updateSettings('min', $event)"

View File

@@ -0,0 +1,146 @@
<template>
<div ref="graphContainer" class="graph-container">
<div v-show="!dataSources" class="warning data-view-warning no-data">
There is no data to build a graph. Run your SQL query and make sure the
result is not empty.
</div>
<div
v-show="!dataSourceIsValid"
class="warning data-view-warning invalid-data"
>
Result set is invalid for graph visualisation. Learn more in
<a href="https://sqliteviz.com/docs/graph/" target="_blank">
documentation</a
>.
</div>
<splitpanes
:before="{ size: 70, max: 100 }"
:after="{ size: 30, max: 50, hidden: !showValueViewer }"
:default="{ before: 70, after: 30 }"
class="graph"
:style="{
height:
!dataSources || !dataSourceIsValid ? 'calc(100% - 40px)' : '100%'
}"
>
<template #left-pane>
<div ref="graphEditorContainer" :style="{ height: '100%' }">
<GraphEditor
ref="graphEditor"
:dataSources="dataSources"
:initOptions="initOptions"
:showViewSettings="showViewSettings"
@update="$emit('update')"
@select-item="selectedItem = $event"
@clear-selection="selectedItem = null"
/>
</div>
</template>
<template v-if="showValueViewer" #right-pane>
<value-viewer
:empty="!selectedItem"
emptyMessage="No node or edge selected to view"
:value="JSON.stringify(selectedItem)"
defaultFormat="json"
/>
</template>
</splitpanes>
</div>
</template>
<script>
import 'react-chart-editor/lib/react-chart-editor.css'
import GraphEditor from '@/components/Graph/GraphEditor.vue'
import { dataSourceIsValid } from '@/lib/graphHelper'
import ValueViewer from '@/components/ValueViewer.vue'
import Splitpanes from '@/components/Common/Splitpanes'
export default {
name: 'Graph',
components: { GraphEditor, ValueViewer, Splitpanes },
props: {
dataSources: Object,
initOptions: Object,
exportToPngEnabled: Boolean,
exportToSvgEnabled: Boolean,
exportToHtmlEnabled: Boolean,
showViewSettings: Boolean,
showValueViewer: Boolean
},
emits: [
'update:exportToSvgEnabled',
'update:exportToHtmlEnabled',
'update:exportToPngEnabled',
'update:exportToClipboardEnabled',
'update',
'loadingImageCompleted'
],
data() {
return {
resizeObserver: null,
selectedItem: null
}
},
computed: {
dataSourceIsValid() {
return !this.dataSources || dataSourceIsValid(this.dataSources)
}
},
watch: {
async showViewSettings() {
await this.$nextTick()
this.handleResize()
},
dataSources() {
this.$emit('update:exportToPngEnabled', !!this.dataSources)
this.$emit('update:exportToClipboardEnabled', !!this.dataSources)
}
},
created() {
this.$emit('update:exportToSvgEnabled', false)
this.$emit('update:exportToHtmlEnabled', false)
this.$emit('update:exportToPngEnabled', !!this.dataSources)
this.$emit('update:exportToClipboardEnabled', !!this.dataSources)
},
mounted() {
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.graphEditorContainer)
},
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.graphEditorContainer)
},
methods: {
getOptionsForSave() {
return this.$refs.graphEditor.settings
},
async saveAsPng() {
await this.$refs.graphEditor.saveAsPng()
this.$emit('loadingImageCompleted')
},
prepareCopy() {
return this.$refs.graphEditor.prepareCopy()
},
async handleResize() {
const renderer = this.$refs.graphEditor?.renderer
if (renderer) {
renderer.refresh()
renderer.getCamera().animatedReset({ duration: 600 })
}
}
}
}
</script>
<style scoped>
.graph-container {
height: 100%;
}
.graph {
min-height: 242px;
}
:deep(.editor_controls .sidebar__item:before) {
width: 0;
}
</style>

View File

@@ -87,7 +87,7 @@
</template> </template>
<script> <script>
import TextField from '@/components/TextField' import TextField from '@/components/Common/TextField'
import CloseIcon from '@/components/svg/close' import CloseIcon from '@/components/svg/close'
import storedInquiries from '@/lib/storedInquiries' import storedInquiries from '@/lib/storedInquiries'
import AppDiagnosticInfo from './AppDiagnosticInfo' import AppDiagnosticInfo from './AppDiagnosticInfo'

View File

@@ -31,9 +31,9 @@ import fIo from '@/lib/utils/fileIo'
import $ from 'jquery' import $ from 'jquery'
import 'pivottable' import 'pivottable'
import 'pivottable/dist/pivot.css' import 'pivottable/dist/pivot.css'
import PivotUi from './PivotUi' import PivotUi from './PivotUi/index.vue'
import pivotHelper from './pivotHelper' import pivotHelper from './pivotHelper'
import Chart from '@/views/MainView/Workspace/Tabs/Tab/DataView/Chart' import Chart from '@/components/Chart'
import chartHelper from '@/lib/chartHelper' import chartHelper from '@/lib/chartHelper'
import events from '@/lib/utils/events' import events from '@/lib/utils/events'
import plotly from 'plotly.js' import plotly from 'plotly.js'
@@ -227,12 +227,12 @@ export default {
async prepareCopy() { async prepareCopy() {
if (this.viewCustomChart) { if (this.viewCustomChart) {
return await this.$refs.customChart.prepareCopy() return this.$refs.customChart.prepareCopy()
} }
if (this.viewStandartChart) { if (this.viewStandartChart) {
return await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png') return chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
} }
return await pivotHelper.getPivotCanvas(this.$refs.pivotOutput) return pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
}, },
async saveAsSvg() { async saveAsSvg() {

View File

@@ -40,7 +40,7 @@
</template> </template>
<script> <script>
import IconButton from '@/components/IconButton' import IconButton from '@/components/Common/IconButton'
import ArrowIcon from '@/components/svg/arrow' import ArrowIcon from '@/components/svg/arrow'
import EdgeArrowIcon from '@/components/svg/edgeArrow' import EdgeArrowIcon from '@/components/svg/edgeArrow'

View File

@@ -1,40 +1,66 @@
<template> <template>
<div ref="runResultPanel" class="run-result-panel"> <div ref="runResultPanel" class="run-result-panel">
<component <splitpanes
:is="viewValuePanelVisible ? 'splitpanes' : 'div'"
:before="{ size: 50, max: 100 }" :before="{ size: 50, max: 100 }"
:after="{ size: 50, max: 100 }" :after="{ size: 50, max: 100, hidden: !viewValuePanelVisible }"
:default="{ before: 50, after: 50 }" :default="{ before: 50, after: 50 }"
class="run-result-panel-content" class="run-result-panel-content"
> >
<template #left-pane> <template #left-pane>
<div <div class="result-set-container">
:id="'run-result-left-pane-' + tab.id" <div
class="result-set-container" v-show="result === null && !isGettingResults && !error"
/> class="table-preview result-before"
</template> >
<div Run your query and get results here
:id="'run-result-result-set-' + tab.id"
class="result-set-container"
/>
<template v-if="viewValuePanelVisible" #right-pane>
<div class="value-viewer-container">
<value-viewer
v-show="selectedCell"
:cellValue="
selectedCell
? result.values[result.columns[selectedCell.dataset.col]][
selectedCell.dataset.row
]
: ''
"
/>
<div v-show="!selectedCell" class="table-preview">
No cell selected to view
</div> </div>
<div v-if="isGettingResults" class="table-preview result-in-progress">
<loading-indicator :size="30" />
Fetching results...
</div>
<div
v-show="result === undefined && !isGettingResults && !error"
class="table-preview result-empty"
>
No rows retrieved according to your query
</div>
<logs v-if="error" :messages="[error]" />
<sql-table
v-if="result && !viewRecord"
:data-set="result"
:time="time"
:pageSize="pageSize"
:page="defaultPage"
:selectedCellCoordinates="defaultSelectedCell"
class="straight"
@update-selected-cell="onUpdateSelectedCell"
/>
<record
v-if="result && viewRecord"
ref="recordView"
:data-set="result"
:time="time"
:selectedColumnIndex="selectedCell ? +selectedCell.dataset.col : 0"
:rowIndex="selectedCell ? +selectedCell.dataset.row : 0"
@update-selected-cell="onUpdateSelectedCell"
/>
</div> </div>
</template> </template>
</component> <template v-if="viewValuePanelVisible" #right-pane>
<value-viewer
:empty="!selectedCell"
emptyMessage="No cell selected to view"
:value="
selectedCell
? result.values[result.columns[selectedCell.dataset.col]][
selectedCell.dataset.row
]
: ''
"
/>
</template>
</splitpanes>
<side-tool-bar panel="table" @switch-to="$emit('switchTo', $event)"> <side-tool-bar panel="table" @switch-to="$emit('switchTo', $event)">
<icon-button <icon-button
@@ -89,69 +115,27 @@
@action="copyToClipboard" @action="copyToClipboard"
@cancel="cancelCopy" @cancel="cancelCopy"
/> />
<teleport defer :to="resultSetTeleportTarget" :disabled="!enableTeleport">
<div>
<div
v-show="result === null && !isGettingResults && !error"
class="table-preview result-before"
>
Run your query and get results here
</div>
<div v-if="isGettingResults" class="table-preview result-in-progress">
<loading-indicator :size="30" />
Fetching results...
</div>
<div
v-show="result === undefined && !isGettingResults && !error"
class="table-preview result-empty"
>
No rows retrieved according to your query
</div>
<logs v-if="error" :messages="[error]" />
<sql-table
v-if="result && !viewRecord"
:data-set="result"
:time="time"
:pageSize="pageSize"
:page="defaultPage"
:selectedCellCoordinates="defaultSelectedCell"
class="straight"
@update-selected-cell="onUpdateSelectedCell"
/>
<record
v-if="result && viewRecord"
ref="recordView"
:data-set="result"
:time="time"
:selectedColumnIndex="selectedCell ? +selectedCell.dataset.col : 0"
:rowIndex="selectedCell ? +selectedCell.dataset.row : 0"
@update-selected-cell="onUpdateSelectedCell"
/>
</div>
</teleport>
</div> </div>
</template> </template>
<script> <script>
import Logs from '@/components/Logs' import Logs from '@/components/Common/Logs'
import SqlTable from '@/components/SqlTable/index.vue' import SqlTable from '@/components/SqlTable'
import LoadingIndicator from '@/components/LoadingIndicator' import LoadingIndicator from '@/components/Common/LoadingIndicator'
import SideToolBar from '../SideToolBar' import SideToolBar from '@/components/SideToolBar'
import Splitpanes from '@/components/Splitpanes' import Splitpanes from '@/components/Common/Splitpanes'
import ExportToCsvIcon from '@/components/svg/exportToCsv' import ExportToCsvIcon from '@/components/svg/exportToCsv'
import ClipboardIcon from '@/components/svg/clipboard' import ClipboardIcon from '@/components/svg/clipboard'
import ViewCellValueIcon from '@/components/svg/viewCellValue' import ViewCellValueIcon from '@/components/svg/viewCellValue'
import RowIcon from '@/components/svg/row' import RowIcon from '@/components/svg/row'
import IconButton from '@/components/IconButton' import IconButton from '@/components/Common/IconButton'
import csv from '@/lib/csv' import csv from '@/lib/csv'
import fIo from '@/lib/utils/fileIo' import fIo from '@/lib/utils/fileIo'
import cIo from '@/lib/utils/clipboardIo' import cIo from '@/lib/utils/clipboardIo'
import time from '@/lib/utils/time' import time from '@/lib/utils/time'
import loadingDialog from '@/components/LoadingDialog' import loadingDialog from '@/components/Common/LoadingDialog'
import events from '@/lib/utils/events' import events from '@/lib/utils/events'
import ValueViewer from './ValueViewer' import ValueViewer from '@/components/ValueViewer'
import Record from './Record/index.vue' import Record from './Record/index.vue'
export default { export default {
@@ -172,7 +156,6 @@ export default {
Splitpanes Splitpanes
}, },
props: { props: {
tab: Object,
result: Object, result: Object,
isGettingResults: Boolean, isGettingResults: Boolean,
error: Object, error: Object,
@@ -194,20 +177,6 @@ export default {
showLoadingDialog: false showLoadingDialog: false
} }
}, },
computed: {
resultSetTeleportTarget() {
if (!this.enableTeleport) {
return undefined
}
const base = `#${
this.viewValuePanelVisible
? 'run-result-left-pane'
: 'run-result-result-set'
}`
const tabIdPostfix = `-${this.tab.id}`
return base + tabIdPostfix
}
},
watch: { watch: {
result() { result() {
this.defaultSelectedCell = null this.defaultSelectedCell = null
@@ -332,19 +301,12 @@ export default {
width: 0; width: 0;
} }
.result-set-container, .result-set-container {
.result-set-container > div {
position: relative; position: relative;
height: 100%; height: 100%;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
} }
.value-viewer-container {
height: 100%;
width: 100%;
background-color: var(--color-white);
position: relative;
}
.table-preview { .table-preview {
position: absolute; position: absolute;

View File

@@ -35,7 +35,7 @@
import fIo from '@/lib/utils/fileIo' import fIo from '@/lib/utils/fileIo'
import events from '@/lib/utils/events' import events from '@/lib/utils/events'
import TableDescription from './TableDescription' import TableDescription from './TableDescription'
import TextField from '@/components/TextField' import TextField from '@/components/Common/TextField'
import TreeChevron from '@/components/svg/treeChevron' import TreeChevron from '@/components/svg/treeChevron'
import DbUploader from '@/components/DbUploader' import DbUploader from '@/components/DbUploader'
import ExportIcon from '@/components/svg/export' import ExportIcon from '@/components/svg/export'

View File

@@ -37,7 +37,7 @@
</template> </template>
<script> <script>
import IconButton from '@/components/IconButton' import IconButton from '@/components/Common/IconButton'
import TableIcon from '@/components/svg/table' import TableIcon from '@/components/svg/table'
import SqlEditorIcon from '@/components/svg/sqlEditor' import SqlEditorIcon from '@/components/svg/sqlEditor'
import DataViewIcon from '@/components/svg/dataView' import DataViewIcon from '@/components/svg/dataView'

View File

@@ -34,7 +34,7 @@ import 'codemirror/theme/neo.css'
import 'codemirror/addon/hint/show-hint.css' import 'codemirror/addon/hint/show-hint.css'
import 'codemirror/addon/display/autorefresh.js' import 'codemirror/addon/display/autorefresh.js'
import SideToolBar from '../SideToolBar' import SideToolBar from '../SideToolBar'
import IconButton from '@/components/IconButton' import IconButton from '@/components/Common/IconButton'
import RunIcon from '@/components/svg/run' import RunIcon from '@/components/svg/run'
export default { export default {

View File

@@ -69,7 +69,7 @@
</template> </template>
<script> <script>
import Pager from './Pager.vue' import Pager from '@/components/Common/Pager.vue'
export default { export default {
name: 'SqlTable', name: 'SqlTable',

View File

@@ -37,7 +37,6 @@
:disabled="!enableTeleport" :disabled="!enableTeleport"
> >
<run-result <run-result
:tab="tab"
:result="tab.result" :result="tab.result"
:isGettingResults="tab.isGettingResults" :isGettingResults="tab.isGettingResults"
:error="tab.error" :error="tab.error"
@@ -64,10 +63,10 @@
</template> </template>
<script> <script>
import Splitpanes from '@/components/Splitpanes' import Splitpanes from '@/components/Common/Splitpanes'
import SqlEditor from './SqlEditor' import SqlEditor from '@/components/SqlEditor'
import DataView from './DataView' import DataView from '@/components/DataView'
import RunResult from './RunResult' import RunResult from '@/components/RunResult'
import { nextTick, computed } from 'vue' import { nextTick, computed } from 'vue'
import events from '@/lib/utils/events' import events from '@/lib/utils/events'

View File

@@ -1,48 +1,56 @@
<template> <template>
<div class="value-viewer"> <div class="value-viewer">
<div class="value-viewer-toolbar"> <template v-if="!empty">
<button <div class="value-viewer-toolbar">
v-for="format in formats" <button
:key="format.value" v-for="format in formats"
type="button" :key="format.value"
:aria-selected="currentFormat === format.value" type="button"
:class="format.value" :aria-selected="currentFormat === format.value"
@click="currentFormat = format.value" :class="format.value"
> @click="currentFormat = format.value"
{{ format.text }} >
</button> {{ format.text }}
</button>
<button type="button" class="copy" @click="copyToClipboard">Copy</button> <button type="button" class="copy" @click="copyToClipboard">
<button Copy
type="button" </button>
class="line-wrap" <button
:aria-selected="lineWrapping === true" type="button"
@click="lineWrapping = !lineWrapping" class="line-wrap"
> :aria-selected="lineWrapping === true"
Line wrap @click="lineWrapping = !lineWrapping"
</button> >
</div> Line wrap
<div class="value-body"> </button>
<codemirror </div>
v-if="currentFormat === 'json' && formattedJson" <div class="value-body">
:value="formattedJson" <codemirror
:options="cmOptions" v-if="currentFormat === 'json' && formattedJson"
class="json-value original-style" :value="formattedJson"
/> :options="cmOptions"
<pre class="json-value original-style"
v-if="currentFormat === 'text'" />
:class="[ <pre
'text-value', v-if="currentFormat === 'text'"
{ 'meta-value': isNull || isBlob }, :class="[
{ 'line-wrap': lineWrapping } 'text-value',
]" { 'meta-value': isNull || isBlob },
>{{ cellText }}</pre { 'line-wrap': lineWrapping }
> ]"
<logs >{{ cellText }}</pre
v-if="messages && messages.length > 0" >
:messages="messages" <logs
class="messages" v-if="messages && messages.length > 0"
/> :messages="messages"
class="messages"
/>
</div>
</template>
<div v-show="empty" class="empty-message">
{{ emptyMessage }}
</div> </div>
</div> </div>
</template> </template>
@@ -57,15 +65,22 @@ import 'codemirror/addon/fold/foldgutter.css'
import 'codemirror/addon/fold/brace-fold.js' import 'codemirror/addon/fold/brace-fold.js'
import 'codemirror/theme/neo.css' import 'codemirror/theme/neo.css'
import cIo from '@/lib/utils/clipboardIo' import cIo from '@/lib/utils/clipboardIo'
import Logs from '@/components/Logs' import Logs from '@/components/Common/Logs'
export default { export default {
name: 'ValueViewer',
components: { components: {
Codemirror, Codemirror,
Logs Logs
}, },
props: { props: {
cellValue: [String, Number, Uint8Array] value: [String, Number, Uint8Array],
empty: Boolean,
emptyMessage: String,
defaultFormat: {
type: String,
default: 'text'
}
}, },
data() { data() {
return { return {
@@ -73,7 +88,7 @@ export default {
{ text: 'Text', value: 'text' }, { text: 'Text', value: 'text' },
{ text: 'JSON', value: 'json' } { text: 'JSON', value: 'json' }
], ],
currentFormat: 'text', currentFormat: this.defaultFormat,
lineWrapping: false, lineWrapping: false,
formattedJson: '', formattedJson: '',
messages: [] messages: []
@@ -94,13 +109,13 @@ export default {
} }
}, },
isBlob() { isBlob() {
return this.cellValue && ArrayBuffer.isView(this.cellValue) return this.value && ArrayBuffer.isView(this.value)
}, },
isNull() { isNull() {
return this.cellValue === null return this.value === null
}, },
cellText() { cellText() {
const value = this.cellValue const value = this.value
if (this.isNull) { if (this.isNull) {
return 'NULL' return 'NULL'
} }
@@ -111,17 +126,23 @@ export default {
} }
}, },
watch: { watch: {
currentFormat() { currentFormat: {
this.messages = [] immediate: true,
this.formattedJson = '' handler() {
if (this.currentFormat === 'json') { this.messages = []
this.formatJson(this.cellValue) this.formattedJson = ''
if (this.currentFormat === 'json') {
this.formatJson(this.value)
}
} }
}, },
cellValue() { value: {
this.messages = [] immediate: true,
if (this.currentFormat === 'json') { handler() {
this.formatJson(this.cellValue) this.messages = []
if (this.currentFormat === 'json') {
this.formatJson(this.value)
}
} }
} }
}, },
@@ -141,7 +162,7 @@ export default {
}, },
copyToClipboard() { copyToClipboard() {
cIo.copyText( cIo.copyText(
this.currentFormat === 'json' ? this.formattedJson : this.cellValue, this.currentFormat === 'json' ? this.formattedJson : this.value,
'The value is copied to clipboard.' 'The value is copied to clipboard.'
) )
} }
@@ -153,8 +174,10 @@ export default {
.value-viewer { .value-viewer {
background-color: var(--color-white); background-color: var(--color-white);
height: 100%; height: 100%;
width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative;
} }
.value-viewer-toolbar { .value-viewer-toolbar {
display: flex; display: flex;
@@ -219,4 +242,14 @@ export default {
width: 1px; width: 1px;
background: var(--color-text-base); background: var(--color-text-base);
} }
.empty-message {
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--color-text-base);
font-size: 13px;
text-align: center;
}
</style> </style>

View File

@@ -7,27 +7,33 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="M5 4C5 5.10457 4.10457 6 3 6C1.89543 6 1 5.10457 1 4C1 2.89543 1.89543 2 3 2C4.10457 2 5 2.89543 5 4Z" d="M5 4C5 5.10457 4.10457 6 3 6C1.89543 6 1 5.10457 1 4C1 2.89543
1.89543 2 3 2C4.10457 2 5 2.89543 5 4Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
<path <path
d="M17 7.5C17 8.88071 15.8807 10 14.5 10C13.1193 10 12 8.88071 12 7.5C12 6.11929 13.1193 5 14.5 5C15.8807 5 17 6.11929 17 7.5Z" d="M17 7.5C17 8.88071 15.8807 10 14.5 10C13.1193 10 12 8.88071 12
7.5C12 6.11929 13.1193 5 14.5 5C15.8807 5 17 6.11929 17 7.5Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
<path <path
d="M8 13.5C8 14.8807 6.88071 16 5.5 16C4.11929 16 3 14.8807 3 13.5C3 12.1193 4.11929 11 5.5 11C6.88071 11 8 12.1193 8 13.5Z" d="M8 13.5C8 14.8807 6.88071 16 5.5 16C4.11929 16 3 14.8807 3 13.5C3
12.1193 4.11929 11 5.5 11C6.88071 11 8 12.1193 8 13.5Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
<path <path
d="M2.93128 5.31436L3.90527 5.08778L5.48693 11.8867L4.51294 12.1133L2.93128 5.31436Z" d="M2.93128 5.31436L3.90527 5.08778L5.48693 11.8867L4.51294
12.1133L2.93128 5.31436Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
<path <path
d="M12.9447 7.79159L13.5548 8.58392L7.30516 13.3962L6.69507 12.6038L12.9447 7.79159Z" d="M12.9447 7.79159L13.5548 8.58392L7.30516 13.3962L6.69507
12.6038L12.9447 7.79159Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
<path <path
d="M14.1316 6.51712L3.13166 3.51723L2.86844 4.48202L13.8684 7.48191L14.1316 6.51712Z" d="M14.1316 6.51712L3.13166 3.51723L2.86844 4.48202L13.8684
7.48191L14.1316 6.51712Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
</svg> </svg>

File diff suppressed because one or more lines are too long

View File

@@ -7,7 +7,8 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="M3 4C3 3.44772 3.44772 3 4 3H14C14.5523 3 15 3.44772 15 4V14C15 14.5523 14.5523 15 14 15H4C3.44772 15 3 14.5523 3 14V4Z" d="M3 4C3 3.44772 3.44772 3 4 3H14C14.5523 3 15 3.44772 15 4V14C15
14.5523 14.5523 15 14 15H4C3.44772 15 3 14.5523 3 14V4Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
</svg> </svg>

View File

@@ -47,8 +47,7 @@ export function buildNodes(graph, dataSources, options) {
nodes.forEach(node => { nodes.forEach(node => {
if (node[nodeId]) { if (node[nodeId]) {
graph.addNode(node[nodeId], { graph.addNode(node[nodeId], {
data: node, data: node
labelColor: options.style.nodes.label.color
}) })
} }
}) })
@@ -56,7 +55,7 @@ export function buildNodes(graph, dataSources, options) {
} }
export function buildEdges(graph, dataSources, options) { export function buildEdges(graph, dataSources, options) {
const docColumn = Object.keys(dataSources)[0] || 'doc' const docColumn = Object.keys(dataSources)[0]
const { objectType, edgeSource, edgeTarget } = options.structure const { objectType, edgeSource, edgeTarget } = options.structure
if (objectType && edgeSource && edgeTarget) { if (objectType && edgeSource && edgeTarget) {
@@ -69,8 +68,7 @@ export function buildEdges(graph, dataSources, options) {
const target = edge[edgeTarget] const target = edge[edgeTarget]
if (graph.hasNode(source) && graph.hasNode(target)) { if (graph.hasNode(source) && graph.hasNode(target)) {
graph.addEdge(source, target, { graph.addEdge(source, target, {
data: edge, data: edge
labelColor: options.style.edges.label.color
}) })
} }
}) })
@@ -129,6 +127,119 @@ export function updateEdges(graph, attributeUpdates) {
}) })
} }
export function reduceNodes(nodeId, nodeData, interactionState, settings) {
const {
selectedNodeId,
hoveredNodeId,
selectedEdgeId,
hoveredEdgeId,
neighborsOfSelectedNode,
neighborsOfHoveredNode,
selectedEdgeExtremities,
hoveredEdgeExtremities
} = interactionState
const res = { ...nodeData }
if (selectedNodeId || hoveredNodeId || hoveredEdgeId || selectedEdgeId) {
res.zIndex = 2
res.highlighted = nodeId === selectedNodeId || nodeId === hoveredNodeId
const isInHoveredFamily =
nodeId === hoveredNodeId ||
neighborsOfHoveredNode?.has(nodeId) ||
hoveredEdgeExtremities.includes(nodeId)
const isInSelectedFamily =
nodeId === selectedNodeId ||
neighborsOfSelectedNode?.has(nodeId) ||
selectedEdgeExtremities.includes(nodeId)
if (isInSelectedFamily || isInHoveredFamily) {
res.forceLabel = true
} else {
res.color = getDiminishedColor(
nodeData.color,
settings.style.backgroundColor
)
res.label = ''
res.zIndex = 1
}
}
return res
}
export function reduceEdges(
edgeId,
edgeData,
interactionState,
settings,
graph
) {
const {
selectedNodeId,
hoveredNodeId,
selectedEdgeId,
hoveredEdgeId,
neighborsOfSelectedNode,
neighborsOfHoveredNode
} = interactionState
const res = { ...edgeData }
if (hoveredEdgeId || selectedEdgeId || selectedNodeId || hoveredNodeId) {
const extremities = graph.extremities(edgeId)
res.zIndex = 2
const isHighlighted = hoveredEdgeId === edgeId || selectedEdgeId === edgeId
let isVisible
if (settings.style.highlightMode === 'node_alone') {
isVisible = isHighlighted
} else if (settings.style.highlightMode === 'node_and_neighbors') {
isVisible =
isHighlighted ||
(selectedNodeId && extremities.includes(selectedNodeId)) ||
(hoveredNodeId && extremities.includes(hoveredNodeId))
} else {
isVisible =
isHighlighted ||
(selectedNodeId &&
extremities.every(
n => n === selectedNodeId || neighborsOfSelectedNode.has(n)
)) ||
(hoveredNodeId &&
extremities.every(
n => n === hoveredNodeId || neighborsOfHoveredNode.has(n)
))
}
if (isHighlighted) {
res.size = res.size * 2
res.forceLabel = true
} else if (!isVisible) {
res.color = getDiminishedColor(
edgeData.color,
settings.style.backgroundColor
)
res.zIndex = 1
res.label = ''
}
}
return res
}
export function getDiminishedColor(color, bgColor) {
const colorObj = tinycolor(color)
const colorOpacity = colorObj.getAlpha()
colorObj.setAlpha(0.25 * colorOpacity)
const fg = colorObj.toRgb()
const bg = tinycolor(bgColor).toRgb()
const r = Math.round(fg.r * fg.a + bg.r * (1 - fg.a))
const g = Math.round(fg.g * fg.a + bg.g * (1 - fg.a))
const b = Math.round(fg.b * fg.a + bg.b * (1 - fg.a))
return tinycolor({ r, g, b, a: 1 }).toHexString()
}
function getUpdateLabelMethod(labelSettings) { function getUpdateLabelMethod(labelSettings) {
const { source, color } = labelSettings const { source, color } = labelSettings
return attributes => { return attributes => {
@@ -163,9 +274,14 @@ function getUpdateSizeMethod(graph, sizeSettings) {
} }
} }
function getDirectVariableColorUpdateMethod(source) { function getDirectVariableColorUpdateMethod(source, opacity = 100) {
return attributes => return attributes => {
(attributes.color = tinycolor(attributes.data[source]).toHexString()) const color = tinycolor(attributes.data[source])
const colorOpacity = color.getAlpha()
attributes.color = color
.setAlpha((opacity / 100) * colorOpacity)
.toHex8String()
}
} }
function getUpdateNodeColorMethod(graph, colorSettings) { function getUpdateNodeColorMethod(graph, colorSettings) {
@@ -177,10 +293,16 @@ function getUpdateNodeColorMethod(graph, colorSettings) {
colorscale, colorscale,
colorscaleDirection, colorscaleDirection,
mode, mode,
method method,
opacity
} = colorSettings } = colorSettings
if (type === 'constant') { if (type === 'constant') {
return attributes => (attributes.color = value) const color = tinycolor(value)
const colorOpacity = color.getAlpha()
return attributes =>
(attributes.color = color
.setAlpha((opacity / 100) * colorOpacity)
.toHex8String())
} else if (type === 'variable') { } else if (type === 'variable') {
return sourceUsage === 'map_to' return sourceUsage === 'map_to'
? getColorMethod( ? getColorMethod(
@@ -189,9 +311,10 @@ function getUpdateNodeColorMethod(graph, colorSettings) {
(nodeId, attributes) => attributes.data[source], (nodeId, attributes) => attributes.data[source],
colorscale, colorscale,
colorscaleDirection, colorscaleDirection,
getNodeValueScale getNodeValueScale,
opacity
) )
: getDirectVariableColorUpdateMethod(source) : getDirectVariableColorUpdateMethod(source, opacity)
} else { } else {
return getColorMethod( return getColorMethod(
graph, graph,
@@ -199,7 +322,8 @@ function getUpdateNodeColorMethod(graph, colorSettings) {
nodeId => graph[method](nodeId), nodeId => graph[method](nodeId),
colorscale, colorscale,
colorscaleDirection, colorscaleDirection,
getNodeValueScale getNodeValueScale,
opacity
) )
} }
} }
@@ -246,8 +370,10 @@ function getColorMethod(
sourceGetter, sourceGetter,
selectedColorscale, selectedColorscale,
colorscaleDirection, colorscaleDirection,
valueScaleGetter valueScaleGetter,
opacity = 100
) { ) {
const opacityFactor = opacity / 100
const valueScale = valueScaleGetter(graph, sourceGetter) const valueScale = valueScaleGetter(graph, sourceGetter)
let colorscale = selectedColorscale || DEFAULT_SCALE let colorscale = selectedColorscale || DEFAULT_SCALE
if (colorscaleDirection === 'reversed') { if (colorscaleDirection === 'reversed') {
@@ -261,10 +387,11 @@ function getColorMethod(
colorscale[index % colorscale.length] colorscale[index % colorscale.length]
]) ])
) )
return (attributes, nodeId) => { return (attributes, nodeId) => {
const category = sourceGetter(nodeId, attributes) const category = sourceGetter(nodeId, attributes)
attributes.color = colorMap[category] attributes.color = tinycolor(colorMap[category])
.setAlpha(opacityFactor)
.toHex8String()
} }
} else { } else {
const min = valueScale[0] const min = valueScale[0]
@@ -277,20 +404,25 @@ function getColorMethod(
const value = sourceGetter(nodeId, attributes) const value = sourceGetter(nodeId, attributes)
const normalizedValue = (value - min) / (max - min) const normalizedValue = (value - min) / (max - min)
if (isNaN(normalizedValue)) { if (isNaN(normalizedValue)) {
attributes.color = tinycolor('#000000')
.setAlpha(opacityFactor)
.toHex8String()
return return
} }
const exactMatch = normalizedColorscale.find( const exactMatch = normalizedColorscale.find(
([value]) => value === normalizedValue ([value]) => value === normalizedValue
) )
if (exactMatch) { if (exactMatch) {
attributes.color = tinycolor(exactMatch[1]).toHexString() attributes.color = tinycolor(exactMatch[1])
.setAlpha(opacityFactor)
.toHex8String()
return return
} }
const rightColorIndex = normalizedColorscale.findIndex( const rightColorIndex = normalizedColorscale.findIndex(
([value]) => value >= normalizedValue ([value]) => value > normalizedValue
) )
const leftColorIndex = (rightColorIndex || 1) - 1 const leftColorIndex = rightColorIndex - 1
const right = normalizedColorscale[rightColorIndex] const right = normalizedColorscale[rightColorIndex]
const left = normalizedColorscale[leftColorIndex] const left = normalizedColorscale[leftColorIndex]
const interpolationFactor = const interpolationFactor =
@@ -307,7 +439,9 @@ function getColorMethod(
r: r0 + interpolationFactor * (r1 - r0), r: r0 + interpolationFactor * (r1 - r0),
g: g0 + interpolationFactor * (g1 - g0), g: g0 + interpolationFactor * (g1 - g0),
b: b0 + interpolationFactor * (b1 - b0) b: b0 + interpolationFactor * (b1 - b0)
}).toHexString() })
.setAlpha(opacityFactor)
.toHex8String()
} }
} }
} }
@@ -327,18 +461,3 @@ function getEdgeValueScale(graph, sourceGetter) {
}, new Set()) }, new Set())
return Array.from(scaleSet).sort((a, b) => a - b) return Array.from(scaleSet).sort((a, b) => a - b)
} }
export function getOptionsFromDataSources(dataSources) {
if (!dataSources) {
return []
}
return Object.keys(dataSources).map(name => ({
value: name,
label: name
}))
}
export default {
getOptionsFromDataSources
}

View File

@@ -1,12 +1,22 @@
export default { export default {
_migrate(installedVersion, inquiries) { _migrate(installedVersion, inquiries) {
if (installedVersion === 1) { if (installedVersion < 2) {
inquiries.forEach(inquire => { inquiries.forEach(inquiry => {
inquire.viewType = 'chart' inquiry.viewType = 'chart'
inquire.viewOptions = inquire.chart inquiry.viewOptions = inquiry.chart
delete inquire.chart delete inquiry.chart
}) })
return inquiries
} }
if (installedVersion < 3) {
inquiries.forEach(inquiry => {
if (inquiry.viewType === 'graph') {
inquiry.viewOptions.style.nodes.color.opacity = 100
inquiry.viewOptions.style.highlightMode = 'node_and_neighbors'
}
})
}
return inquiries
} }
} }

View File

@@ -7,7 +7,7 @@ const migrate = migration._migrate
const myInquiriesKey = 'myInquiries' const myInquiriesKey = 'myInquiries'
export default { export default {
version: 2, version: 3,
myInquiriesKey, myInquiriesKey,
getStoredInquiries() { getStoredInquiries() {
let myInquiries = JSON.parse(localStorage.getItem(myInquiriesKey)) let myInquiries = JSON.parse(localStorage.getItem(myInquiriesKey))
@@ -21,7 +21,13 @@ export default {
return [] return []
} }
return (myInquiries && myInquiries.inquiries) || [] if (myInquiries.version === 2) {
myInquiries = migrate(2, myInquiries.inquiries)
this.updateStorage(myInquiries)
return myInquiries
}
return myInquiries.inquiries || []
}, },
duplicateInquiry(baseInquiry) { duplicateInquiry(baseInquiry) {
@@ -63,6 +69,8 @@ export default {
// Turn data into array if they are not // Turn data into array if they are not
inquiryList = !Array.isArray(inquiries) ? [inquiries] : inquiries inquiryList = !Array.isArray(inquiries) ? [inquiries] : inquiries
inquiryList = migrate(1, inquiryList) inquiryList = migrate(1, inquiryList)
} else if (inquiries.version === 2) {
inquiryList = migrate(2, inquiries.inquiries)
} else { } else {
inquiryList = inquiries.inquiries || [] inquiryList = inquiries.inquiries || []
} }
@@ -82,11 +90,11 @@ export default {
importInquiries() { importInquiries() {
return fu.importFile().then(str => { return fu.importFile().then(str => {
const inquires = this.deserialiseInquiries(str) const inquiries = this.deserialiseInquiries(str)
events.send('inquiry.import', inquires.length) events.send('inquiry.import', inquiries.length)
return inquires return inquiries
}) })
}, },
export(inquiryList, fileName) { export(inquiryList, fileName) {
@@ -102,6 +110,8 @@ export default {
if (!data.version) { if (!data.version) {
return data.length > 0 ? migrate(1, data) : [] return data.length > 0 ? migrate(1, data) : []
} else if (data.version === 2) {
return migrate(2, data.inquiries)
} else { } else {
return data.inquiries return data.inquiries
} }

View File

@@ -1,6 +1,6 @@
import { createRouter, createWebHashHistory } from 'vue-router' import { createRouter, createWebHashHistory } from 'vue-router'
import Workspace from '@/views/MainView/Workspace' import Workspace from '@/views/Workspace'
import Inquiries from '@/views/MainView/Inquiries' import Inquiries from '@/views/Inquiries'
import Welcome from '@/views/Welcome' import Welcome from '@/views/Welcome'
import MainView from '@/views/MainView' import MainView from '@/views/MainView'
import LoadView from '@/views/LoadView' import LoadView from '@/views/LoadView'

View File

@@ -184,14 +184,14 @@
</template> </template>
<script> <script>
import RenameIcon from './svg/rename' import RenameIcon from '@/components/svg/rename'
import CopyIcon from './svg/copy' import CopyIcon from '@/components/svg/copy'
import ExportIcon from '@/components/svg/export' import ExportIcon from '@/components/svg/export'
import DeleteIcon from './svg/delete' import DeleteIcon from '@/components/svg/delete'
import CloseIcon from '@/components/svg/close' import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField' import TextField from '@/components/Common/TextField'
import CheckBox from '@/components/CheckBox' import CheckBox from '@/components/Common/CheckBox'
import LoadingIndicator from '@/components/LoadingIndicator' import LoadingIndicator from '@/components/Common/LoadingIndicator'
import tooltipMixin from '@/tooltipMixin' import tooltipMixin from '@/tooltipMixin'
import storedInquiries from '@/lib/storedInquiries' import storedInquiries from '@/lib/storedInquiries'
import eventBus from '@/lib/eventBus' import eventBus from '@/lib/eventBus'

View File

@@ -15,7 +15,7 @@
<script> <script>
import fu from '@/lib/utils/fileIo' import fu from '@/lib/utils/fileIo'
import database from '@/lib/database' import database from '@/lib/database'
import Logs from '@/components/Logs' import Logs from '@/components/Common/Logs'
import events from '@/lib/utils/events' import events from '@/lib/utils/events'
export default { export default {

View File

@@ -10,7 +10,7 @@
</template> </template>
<script> <script>
import MainMenu from './MainMenu' import MainMenu from '@/components/MainMenu'
import '@/assets/styles/scrollbars.css' import '@/assets/styles/scrollbars.css'
export default { export default {

View File

@@ -1,114 +0,0 @@
<template>
<div ref="graphContainer" class="graph-container">
<div v-show="!dataSources" class="warning data-view-warning">
There is no data to build a graph. Run your SQL query and make sure the
result is not empty.
</div>
<div v-show="!dataSourceIsValid" class="warning data-view-warning">
Result set is invalid for graph visualisation. Learn more in
<a href="https://sqliteviz.com/docs/graph/" target="_blank">
documentation</a
>.
</div>
<div
class="graph"
:style="{
height:
!dataSources || !dataSourceIsValid ? 'calc(100% - 40px)' : '100%'
}"
>
<GraphEditor
ref="graphEditor"
:dataSources="dataSources"
:initOptions="initOptions"
:showViewSettings="showViewSettings"
@update="$emit('update')"
/>
</div>
</div>
</template>
<script>
import 'react-chart-editor/lib/react-chart-editor.css'
import GraphEditor from './GraphEditor.vue'
import { dataSourceIsValid } from '@/lib/graphHelper'
export default {
name: 'Graph',
components: { GraphEditor },
props: {
dataSources: Object,
initOptions: Object,
exportToPngEnabled: Boolean,
exportToSvgEnabled: Boolean,
exportToHtmlEnabled: Boolean,
showViewSettings: Boolean
},
emits: [
'update:exportToSvgEnabled',
'update:exportToHtmlEnabled',
'update',
'loadingImageCompleted'
],
data() {
return {
resizeObserver: null
}
},
created() {
this.$emit('update:exportToSvgEnabled', false)
this.$emit('update:exportToHtmlEnabled', false)
},
mounted() {
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.graphContainer)
},
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.graphContainer)
},
watch: {
async showViewSettings() {
await this.$nextTick()
this.handleResize()
}
},
computed: {
dataSourceIsValid() {
return !this.dataSources || dataSourceIsValid(this.dataSources)
}
},
methods: {
getOptionsForSave() {
return this.$refs.graphEditor.settings
},
async saveAsPng() {
await this.$refs.graphEditor.saveAsPng()
this.$emit('loadingImageCompleted')
},
async prepareCopy() {
return await this.$refs.graphEditor.prepareCopy()
},
async handleResize() {
const renderer = this.$refs.graphEditor.renderer
if (renderer) {
renderer.refresh()
renderer.getCamera().animatedReset({ duration: 600 })
}
}
}
}
</script>
<style scoped>
.graph-container {
height: 100%;
}
.graph {
min-height: 242px;
}
:deep(.editor_controls .sidebar__item:before) {
width: 0;
}
</style>

View File

@@ -17,9 +17,9 @@
</template> </template>
<script> <script>
import Splitpanes from '@/components/Splitpanes' import Splitpanes from '@/components/Common/Splitpanes'
import Schema from './Schema' import Schema from '@/components/Schema'
import Tabs from './Tabs' import Tabs from '@/components/Tabs'
import events from '@/lib/utils/events' import events from '@/lib/utils/events'
export default { export default {

View File

@@ -1,7 +1,7 @@
import { expect } from 'chai' import { expect } from 'chai'
import sinon from 'sinon' import sinon from 'sinon'
import { mount, flushPromises } from '@vue/test-utils' import { mount, flushPromises } from '@vue/test-utils'
import Chart from '@/views/MainView/Workspace/Tabs/Tab/DataView/Chart/index.vue' import Chart from '@/components/Chart.vue'
import chartHelper from '@/lib/chartHelper' import chartHelper from '@/lib/chartHelper'
import * as dereference from 'react-chart-editor/lib/lib/dereference' import * as dereference from 'react-chart-editor/lib/lib/dereference'
import fIo from '@/lib/utils/fileIo' import fIo from '@/lib/utils/fileIo'
@@ -15,7 +15,6 @@ describe('Chart.vue', () => {
}) })
it('getOptionsForSave called with proper arguments', () => { it('getOptionsForSave called with proper arguments', () => {
// mount the component
const wrapper = mount(Chart, { const wrapper = mount(Chart, {
global: { global: {
mocks: { $store } mocks: { $store }
@@ -30,7 +29,6 @@ describe('Chart.vue', () => {
}) })
it('emits update when plotly updates', async () => { it('emits update when plotly updates', async () => {
// mount the component
const wrapper = mount(Chart, { const wrapper = mount(Chart, {
global: { global: {
mocks: { $store } mocks: { $store }
@@ -48,7 +46,6 @@ describe('Chart.vue', () => {
points: [80] points: [80]
} }
// mount the component
const wrapper = mount(Chart, { const wrapper = mount(Chart, {
props: { props: {
dataSources, dataSources,
@@ -131,6 +128,8 @@ describe('Chart.vue', () => {
expect(plot.scrollWidth).not.to.equal(initialPlotWidth) expect(plot.scrollWidth).not.to.equal(initialPlotWidth)
expect(plot.scrollHeight).not.to.equal(initialPlotHeight) expect(plot.scrollHeight).not.to.equal(initialPlotHeight)
container.style.width = 'unset'
container.style.height = 'unset'
wrapper.unmount() wrapper.unmount()
}) })
@@ -187,7 +186,8 @@ describe('Chart.vue', () => {
const wrapper = mount(Chart, { const wrapper = mount(Chart, {
attachTo: document.body, attachTo: document.body,
props: { props: {
dataSources dataSources,
showViewSettings: true
}, },
global: { global: {
mocks: { $store } mocks: { $store }
@@ -207,4 +207,41 @@ describe('Chart.vue', () => {
expect(wrapper.find('.Select__menu').text()).to.contain('name' + 'points') expect(wrapper.find('.Select__menu').text()).to.contain('name' + 'points')
wrapper.unmount() wrapper.unmount()
}) })
it('hides and shows controls depending on showViewSettings and resizes the plot', async () => {
const wrapper = mount(Chart, {
attachTo: document.body,
props: {
dataSources: null,
showViewSettings: false
},
global: {
mocks: { $store }
}
})
// don't call flushPromises here, otherwize resize observer will be call to often
// which causes ResizeObserver loop completed with undelivered notifications.
await nextTick()
const plot = wrapper.find('.svg-container').wrapperElement
await flushPromises()
const initialPlotWidth = plot.scrollWidth
const initialPlotHeight = plot.scrollHeight
expect(wrapper.find('.plotly_editor .editor_controls').exists()).to.equal(
false
)
await wrapper.setProps({ showViewSettings: true })
await flushPromises()
expect(plot.scrollWidth).not.to.equal(initialPlotWidth)
expect(plot.scrollHeight).to.equal(initialPlotHeight)
expect(wrapper.find('.plotly_editor .editor_controls').exists()).to.equal(
true
)
wrapper.unmount()
})
}) })

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai' import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils' import { shallowMount } from '@vue/test-utils'
import CheckBox from '@/components/CheckBox' import CheckBox from '@/components/Common/CheckBox'
describe('CheckBox', () => { describe('CheckBox', () => {
it('unchecked by default', () => { it('unchecked by default', () => {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai' import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils' import { shallowMount } from '@vue/test-utils'
import LoadingIndicator from '@/components/LoadingIndicator' import LoadingIndicator from '@/components/Common/LoadingIndicator'
describe('LoadingIndicator.vue', () => { describe('LoadingIndicator.vue', () => {
it('Calculates animation class', async () => { it('Calculates animation class', async () => {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai' import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils' import { shallowMount } from '@vue/test-utils'
import Logs from '@/components/Logs' import Logs from '@/components/Common/Logs'
import { nextTick } from 'vue' import { nextTick } from 'vue'
let place let place

View File

@@ -1,7 +1,7 @@
import { expect } from 'chai' import { expect } from 'chai'
import sinon from 'sinon' import sinon from 'sinon'
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import Pager from '@/components/SqlTable/Pager' import Pager from '@/components/Common/Pager'
describe('Pager.vue', () => { describe('Pager.vue', () => {
afterEach(() => { afterEach(() => {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai' import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils' import { shallowMount } from '@vue/test-utils'
import Splitpanes from '@/components/Splitpanes' import Splitpanes from '@/components/Common/Splitpanes'
import { nextTick } from 'vue' import { nextTick } from 'vue'
describe('Splitpanes.vue', () => { describe('Splitpanes.vue', () => {
@@ -49,6 +49,41 @@ describe('Splitpanes.vue', () => {
).to.equal('40%') ).to.equal('40%')
}) })
it('renders correctly with hidden panels', async () => {
// mount the component
const wrapper = shallowMount(Splitpanes, {
attachTo: document.body,
slots: {
leftPane: '<div />',
rightPane: '<div />'
},
props: {
before: { size: 60, max: 100, hidden: true },
after: { size: 40, max: 100 },
horizontal: true
}
})
expect(wrapper.findAll('.splitpanes-pane')[0].isVisible()).to.equal(false)
expect(wrapper.find('.splitpanes-splitter').isVisible()).to.equal(false)
expect(
wrapper.findAll('.splitpanes-pane')[1].element.style.height
).to.equal('100%')
await wrapper.setProps({
before: { size: 60, max: 100 },
after: { size: 40, max: 100, hidden: true }
})
expect(wrapper.findAll('.splitpanes-pane')[1].isVisible()).to.equal(false)
expect(wrapper.find('.splitpanes-splitter').isVisible()).to.equal(false)
expect(
wrapper.findAll('.splitpanes-pane')[0].element.style.height
).to.equal('100%')
wrapper.unmount()
})
it('toggles correctly - no maximized initially', async () => { it('toggles correctly - no maximized initially', async () => {
// mount the component // mount the component
const wrapper = shallowMount(Splitpanes, { const wrapper = shallowMount(Splitpanes, {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai' import { expect } from 'chai'
import sinon from 'sinon' import sinon from 'sinon'
import splitter from '@/components/Splitpanes/splitter' import splitter from '@/components/Common/Splitpanes/splitter'
describe('splitter.js', () => { describe('splitter.js', () => {
afterEach(() => { afterEach(() => {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai' import { expect } from 'chai'
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import DataView from '@/views/MainView/Workspace/Tabs/Tab/DataView' import DataView from '@/components/DataView.vue'
import sinon from 'sinon' import sinon from 'sinon'
import { nextTick } from 'vue' import { nextTick } from 'vue'
import cIo from '@/lib/utils/clipboardIo' import cIo from '@/lib/utils/clipboardIo'
@@ -15,7 +15,7 @@ describe('DataView.vue', () => {
it('emits update on mode changing', async () => { it('emits update on mode changing', async () => {
const wrapper = mount(DataView, { const wrapper = mount(DataView, {
global: { global: {
stubs: { chart: true } mocks: { $store }
} }
}) })
@@ -29,7 +29,10 @@ describe('DataView.vue', () => {
it('method getOptionsForSave calls the same method of the current view component', async () => { it('method getOptionsForSave calls the same method of the current view component', async () => {
const wrapper = mount(DataView, { const wrapper = mount(DataView, {
global: { global: {
mocks: { $store } mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
} }
}) })
@@ -53,13 +56,28 @@ describe('DataView.vue', () => {
expect(wrapper.vm.getOptionsForSave()).to.eql({ expect(wrapper.vm.getOptionsForSave()).to.eql({
here_are: 'pivot_settings' here_are: 'pivot_settings'
}) })
const graphBtn = wrapper.findComponent({ ref: 'graphBtn' })
await graphBtn.trigger('click')
const graph = wrapper.findComponent({ name: 'graph' }).vm
sinon
.stub(graph, 'getOptionsForSave')
.returns({ here_are: 'graph_settings' })
expect(wrapper.vm.getOptionsForSave()).to.eql({
here_are: 'graph_settings'
})
wrapper.unmount() wrapper.unmount()
}) })
it('method saveAsSvg calls the same method of the current view component', async () => { it('method saveAsSvg calls the same method of the current view component', async () => {
const wrapper = mount(DataView, { const wrapper = mount(DataView, {
global: { global: {
mocks: { $store } mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
} }
}) })
@@ -87,13 +105,22 @@ describe('DataView.vue', () => {
// Export to svg // Export to svg
await svgBtn.trigger('click') await svgBtn.trigger('click')
expect(pivot.saveAsSvg.calledOnce).to.equal(true) expect(pivot.saveAsSvg.calledOnce).to.equal(true)
// Switch to graph - svg disabled
const graphBtn = wrapper.findComponent({ ref: 'graphBtn' })
await graphBtn.trigger('click')
expect(svgBtn.attributes('disabled')).to.not.equal(undefined)
wrapper.unmount() wrapper.unmount()
}) })
it('method saveAsHtml calls the same method of the current view component', async () => { it('method saveAsHtml calls the same method of the current view component', async () => {
const wrapper = mount(DataView, { const wrapper = mount(DataView, {
global: { global: {
mocks: { $store } mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
} }
}) })
@@ -114,9 +141,76 @@ describe('DataView.vue', () => {
const pivot = wrapper.findComponent({ name: 'pivot' }).vm const pivot = wrapper.findComponent({ name: 'pivot' }).vm
sinon.spy(pivot, 'saveAsHtml') sinon.spy(pivot, 'saveAsHtml')
// Export to svg // Export to html
await htmlBtn.trigger('click') await htmlBtn.trigger('click')
expect(pivot.saveAsHtml.calledOnce).to.equal(true) expect(pivot.saveAsHtml.calledOnce).to.equal(true)
// Switch to graph - htmlBtn disabled
const graphBtn = wrapper.findComponent({ ref: 'graphBtn' })
await graphBtn.trigger('click')
expect(htmlBtn.attributes('disabled')).to.not.equal(undefined)
wrapper.unmount()
})
it('method saveAsPng calls the same method of the current view component', async () => {
const clock = sinon.useFakeTimers()
const wrapper = mount(DataView, {
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
// Find chart and stub the method
const chart = wrapper.findComponent({ name: 'Chart' }).vm
sinon.stub(chart, 'saveAsPng').callsFake(() => {
chart.$emit('loadingImageCompleted')
})
// Export to png
const pngBtn = wrapper.findComponent({ ref: 'pngExportBtn' })
await pngBtn.trigger('click')
await clock.tick(0)
expect(chart.saveAsPng.calledOnce).to.equal(true)
// Switch to pivot
const pivotBtn = wrapper.findComponent({ ref: 'pivotBtn' })
await pivotBtn.trigger('click')
// Find pivot and stub the method
const pivot = wrapper.findComponent({ name: 'pivot' }).vm
sinon.stub(pivot, 'saveAsPng').callsFake(() => {
pivot.$emit('loadingImageCompleted')
})
// Export to png
await pngBtn.trigger('click')
await clock.tick(0)
expect(pivot.saveAsPng.calledOnce).to.equal(true)
// Switch to graph
const graphBtn = wrapper.findComponent({ ref: 'graphBtn' })
await graphBtn.trigger('click')
// Save as png is disabled because there is no data
expect(pngBtn.attributes('disabled')).to.not.equal(undefined)
await wrapper.setProps({ dataSource: { doc: [] } })
// Find graph and stub the method
const graph = wrapper.findComponent({ name: 'graph' }).vm
sinon.stub(graph, 'saveAsPng').callsFake(() => {
graph.$emit('loadingImageCompleted')
})
// Export to png
await pngBtn.trigger('click')
await clock.tick(0)
expect(graph.saveAsPng.calledOnce).to.equal(true)
wrapper.unmount() wrapper.unmount()
}) })
@@ -276,4 +370,100 @@ describe('DataView.vue', () => {
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(false) expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(false)
wrapper.unmount() wrapper.unmount()
}) })
it('saves visualisation options and restores them when switch between modes', async () => {
const wrapper = mount(DataView, {
props: {
initMode: 'graph',
initOptions: { test_options: 'graph_options_from_inquiery' }
},
global: {
mocks: { $store },
stubs: {
chart: true,
graph: true,
pivot: true
}
}
})
const getOptionsForSaveStub = sinon.stub(wrapper.vm, 'getOptionsForSave')
expect(
wrapper.findComponent({ name: 'graph' }).props('initOptions')
).to.eql({ test_options: 'graph_options_from_inquiery' })
getOptionsForSaveStub.returns({ test_options: 'latest_graph_options' })
const chartBtn = wrapper.findComponent({ ref: 'chartBtn' })
await chartBtn.trigger('click')
getOptionsForSaveStub.returns({ test_options: 'chart_settings' })
const pivotBtn = wrapper.findComponent({ ref: 'pivotBtn' })
await pivotBtn.trigger('click')
getOptionsForSaveStub.returns({ test_options: 'pivot_settings' })
await chartBtn.trigger('click')
expect(
wrapper.findComponent({ name: 'chart' }).props('initOptions')
).to.eql({ test_options: 'chart_settings' })
await pivotBtn.trigger('click')
expect(
wrapper.findComponent({ name: 'pivot' }).props('initOptions')
).to.eql({ test_options: 'pivot_settings' })
const graphBtn = wrapper.findComponent({ ref: 'graphBtn' })
await graphBtn.trigger('click')
expect(
wrapper.findComponent({ name: 'graph' }).props('initOptions')
).to.eql({ test_options: 'latest_graph_options' })
})
it('switches visibility of node or edge in graph mode', async () => {
const wrapper = mount(DataView, {
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
// viewNodeOrEdgeBtn is not disaplyed in chart mode
expect(
wrapper.findComponent({ ref: 'viewNodeOrEdgeBtn' }).exists()
).to.equal(false)
// Switch to pivot
const pivotBtn = wrapper.findComponent({ ref: 'pivotBtn' })
await pivotBtn.trigger('click')
// viewNodeOrEdgeBtn is not disaplyed in pivot mode
expect(
wrapper.findComponent({ ref: 'viewNodeOrEdgeBtn' }).exists()
).to.equal(false)
// Switch to graph
const graphBtn = wrapper.findComponent({ ref: 'graphBtn' })
await graphBtn.trigger('click')
// viewNodeOrEdgeBtn is disaplyed in graph mode
const viewNodeOrEdgeBtn = wrapper.findComponent({
ref: 'viewNodeOrEdgeBtn'
})
expect(viewNodeOrEdgeBtn.exists()).to.equal(true)
// by default node viewer is hidden
expect(wrapper.findComponent({ name: 'value-viewer' }).exists()).to.equal(
false
)
// Click to show node viewer
await viewNodeOrEdgeBtn.trigger('click')
expect(wrapper.findComponent({ name: 'value-viewer' }).exists()).to.equal(
true
)
wrapper.unmount()
})
}) })

View File

@@ -0,0 +1,825 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { mount, flushPromises } from '@vue/test-utils'
import Graph from '@/components/Graph/index.vue'
import { nextTick } from 'vue'
function getPixels(canvas) {
const context = canvas.getContext('webgl2')
const width = context.canvas.width
const height = context.canvas.height
// Create arrays to hold the pixel data
const pixels = new Uint8Array(width * height * 4)
// Read pixels from canvas
context.readPixels(
0,
0,
width,
height,
context.RGBA,
context.UNSIGNED_BYTE,
pixels
)
return pixels.join(' ')
}
describe('Graph.vue', () => {
const $store = { state: { isWorkspaceVisible: true } }
afterEach(() => {
sinon.restore()
})
it('shows message when no data', () => {
const wrapper = mount(Graph, {
global: {
stubs: {
GraphEditor: true
}
},
attachTo: document.body
})
expect(wrapper.find('.no-data').isVisible()).to.equal(true)
expect(wrapper.find('.invalid-data').isVisible()).to.equal(false)
wrapper.unmount()
})
it('shows message when data is invalid', () => {
const wrapper = mount(Graph, {
props: {
dataSources: {
column1: ['value1', 'value2']
}
},
global: {
stubs: {
GraphEditor: true
}
},
attachTo: document.body
})
expect(wrapper.find('.no-data').isVisible()).to.equal(false)
expect(wrapper.find('.invalid-data').isVisible()).to.equal(true)
wrapper.unmount()
})
it('emits update when graph editor updates', async () => {
const wrapper = mount(Graph, {
global: {
stubs: {
GraphEditor: true
}
}
})
wrapper.findComponent({ ref: 'graphEditor' }).vm.$emit('update')
expect(wrapper.emitted('update')).to.have.lengthOf(1)
})
it('the graph resizes when the container resizes', async () => {
const wrapper = mount(Graph, {
attachTo: document.body,
props: {
dataSources: {
doc: [
'{"object_type":0,"node_id":"Gryffindor"}',
'{"object_type":0,"node_id":"Hufflepuff"}'
]
},
initOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
nodes: {
size: {
type: 'constant',
value: 10
},
color: {
type: 'constant',
value: '#1F77B4'
},
label: {
source: null,
color: '#444444'
}
},
edges: {
showDirection: true,
size: {
type: 'constant',
value: 2
},
color: {
type: 'constant',
value: '#a2b1c6'
},
label: {
source: null,
color: '#a2b1c6'
}
}
},
layout: {
type: 'circular',
options: null
}
}
},
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
const container =
wrapper.find('.graph-container').wrapperElement.parentElement
const canvas = wrapper.find('canvas.sigma-nodes').wrapperElement
const initialContainerWidth = container.scrollWidth
const initialContainerHeight = container.scrollHeight
const initialCanvasWidth = canvas.scrollWidth
const initialCanvasHeight = canvas.scrollHeight
const newContainerWidth = initialContainerWidth * 2 || 1000
const newContainerHeight = initialContainerHeight * 2 || 2000
container.style.width = `${newContainerWidth}px`
container.style.height = `${newContainerHeight}px`
await flushPromises()
expect(canvas.scrollWidth).not.to.equal(initialCanvasWidth)
expect(canvas.scrollHeight).not.to.equal(initialCanvasHeight)
wrapper.unmount()
})
it('the graph resizes when node viewer visibillity togglles', async () => {
const wrapper = mount(Graph, {
attachTo: document.body,
props: {
dataSources: {
doc: [
'{"object_type":0,"node_id":"Gryffindor"}',
'{"object_type":0,"node_id":"Hufflepuff"}'
]
},
initOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
nodes: {
size: {
type: 'constant',
value: 10
},
color: {
type: 'constant',
value: '#1F77B4'
},
label: {
source: null,
color: '#444444'
}
},
edges: {
showDirection: true,
size: {
type: 'constant',
value: 2
},
color: {
type: 'constant',
value: '#a2b1c6'
},
label: {
source: null,
color: '#a2b1c6'
}
}
},
layout: {
type: 'circular',
options: null
}
},
showValueViewer: false
},
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
const canvas = wrapper.find('canvas.sigma-nodes').wrapperElement
const initialCanvasWidth = canvas.scrollWidth
await wrapper.setProps({ showValueViewer: true })
await flushPromises()
expect(canvas.scrollWidth).not.to.equal(initialCanvasWidth)
await wrapper.setProps({ showValueViewer: false })
await flushPromises()
expect(canvas.scrollWidth).to.equal(initialCanvasWidth)
wrapper.unmount()
})
it('the graph resizes when the split pane resizes', async () => {
const wrapper = mount(Graph, {
attachTo: document.body,
props: {
dataSources: {
doc: [
'{"object_type":0,"node_id":"Gryffindor"}',
'{"object_type":0,"node_id":"Hufflepuff"}'
]
},
initOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
nodes: {
size: {
type: 'constant',
value: 10
},
color: {
type: 'constant',
value: '#1F77B4'
},
label: {
source: null,
color: '#444444'
}
},
edges: {
showDirection: true,
size: {
type: 'constant',
value: 2
},
color: {
type: 'constant',
value: '#a2b1c6'
},
label: {
source: null,
color: '#a2b1c6'
}
}
},
layout: {
type: 'circular',
options: null
}
},
showValueViewer: true
},
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
const container =
wrapper.find('.graph-container').wrapperElement.parentElement
const canvas = wrapper.find('canvas.sigma-nodes').wrapperElement
const initialContainerWidth = container.scrollWidth
const initialCanvasWidth = canvas.scrollWidth
await wrapper.find('.splitpanes-splitter').trigger('mousedown')
document.dispatchEvent(
new MouseEvent('mousemove', {
clientX: initialContainerWidth / 2,
clientY: 80
})
)
document.dispatchEvent(new MouseEvent('mouseup'))
await nextTick()
await nextTick()
await flushPromises()
expect(canvas.scrollWidth).not.to.equal(initialCanvasWidth)
wrapper.unmount()
})
it('opens and closes node viewer', async () => {
const wrapper = mount(Graph, {
props: {
dataSources: {
doc: [
'{"object_type":0,"node_id":"Gryffindor"}',
'{"object_type":0,"node_id":"Hufflepuff"}'
]
},
initOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
nodes: {
size: {
type: 'constant',
value: 10
},
color: {
type: 'constant',
value: '#1F77B4'
},
label: {
source: null,
color: '#444444'
}
},
edges: {
showDirection: true,
size: {
type: 'constant',
value: 2
},
color: {
type: 'constant',
value: '#a2b1c6'
},
label: {
source: null,
color: '#a2b1c6'
}
}
},
layout: {
type: 'circular',
options: null
}
},
showValueViewer: false
},
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
expect(wrapper.text()).not.contain('No node or edge selected to view')
await wrapper.setProps({ showValueViewer: true })
expect(wrapper.text()).contains('No node or edge selected to view')
})
it('passes selected item to node viewer', async () => {
const wrapper = mount(Graph, {
attachTo: document.body,
props: {
dataSources: {
doc: [
'{"object_type":0,"node_id":"Gryffindor"}',
'{"object_type":0,"node_id":"Hufflepuff"}'
]
},
initOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
nodes: {
size: {
type: 'constant',
value: 10
},
color: {
type: 'constant',
value: '#1F77B4'
},
label: {
source: null,
color: '#444444'
}
},
edges: {
showDirection: true,
size: {
type: 'constant',
value: 2
},
color: {
type: 'constant',
value: '#a2b1c6'
},
label: {
source: null,
color: '#a2b1c6'
}
}
},
layout: {
type: 'circular',
options: null
}
},
showValueViewer: true
},
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
expect(wrapper.find('.value-viewer .value-body').exists()).to.equal(false)
await wrapper
.findComponent({ ref: 'graphEditor' })
.vm.$emit('selectItem', { object_type: 0, node_id: 'Gryffindor' })
expect(wrapper.find('.value-viewer .value-body').text()).contains(
'"object_type": 0,'
)
expect(wrapper.find('.value-viewer .value-body').text()).contains(
'"node_id": "Gryffindor"'
)
await wrapper
.findComponent({ ref: 'graphEditor' })
.vm.$emit('clearSelection')
expect(wrapper.find('.value-viewer .value-body').exists()).to.equal(false)
wrapper.unmount()
})
it('nodes and edges are rendered', async () => {
const wrapper = mount(Graph, {
attachTo: document.body,
props: {
showViewSettings: true,
dataSources: {
doc: [
'{"object_type": 0, "node_id": 1}',
'{"object_type": 0, "node_id": 2}',
'{"object_type": 1, "source": 1, "target": 2}'
]
}
},
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
const container =
wrapper.find('.graph-container').wrapperElement.parentElement
container.style.height = '400px'
await wrapper
.find('.test_object_type_select.dropdown-container .Select__indicator')
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
let options = wrapper.findAll('.Select__menu .Select__option')
await options[0].trigger('click')
const nodeCanvasPixelsBefore = getPixels(
wrapper.find('.test_graph_output canvas.sigma-nodes').wrapperElement
)
const edgeCanvasPixelsBefore = getPixels(
wrapper.find('.test_graph_output canvas.sigma-edges').wrapperElement
)
await wrapper
.find('.test_node_id_select.dropdown-container .Select__indicator')
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
options = wrapper.findAll('.Select__menu .Select__option')
await options[1].trigger('click')
await wrapper
.find('.test_edge_source_select.dropdown-container .Select__indicator')
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
options = wrapper.findAll('.Select__menu .Select__option')
await options[2].trigger('click')
await wrapper
.find('.test_edge_target_select.dropdown-container .Select__indicator')
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
options = wrapper.findAll('.Select__menu .Select__option')
await options[3].trigger('click')
const nodeCanvasPixelsAfter = getPixels(
wrapper.find('.test_graph_output canvas.sigma-nodes').wrapperElement
)
const edgeCanvasPixelsAfter = getPixels(
wrapper.find('.test_graph_output canvas.sigma-edges').wrapperElement
)
expect(nodeCanvasPixelsBefore).not.equal(nodeCanvasPixelsAfter)
expect(edgeCanvasPixelsBefore).not.equal(edgeCanvasPixelsAfter)
wrapper.unmount()
})
it('rerenders when dataSource changes but does not rerender if dataSources is empty', async () => {
const wrapper = mount(Graph, {
attachTo: document.body,
props: {
showViewSettings: true,
dataSources: {
doc: [
'{"object_type": 0, "node_id": 1}',
'{"object_type": 0, "node_id": 2}',
'{"object_type": 1, "source": 1, "target": 2}'
]
}
},
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
const container =
wrapper.find('.graph-container').wrapperElement.parentElement
container.style.height = '400px'
await wrapper
.find('.test_object_type_select.dropdown-container .Select__indicator')
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
let options = wrapper.findAll('.Select__menu .Select__option')
await options[0].trigger('click')
await wrapper
.find('.test_node_id_select.dropdown-container .Select__indicator')
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
options = wrapper.findAll('.Select__menu .Select__option')
await options[1].trigger('click')
await wrapper
.find('.test_edge_source_select.dropdown-container .Select__indicator')
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
options = wrapper.findAll('.Select__menu .Select__option')
await options[2].trigger('click')
await wrapper
.find('.test_edge_target_select.dropdown-container .Select__indicator')
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
options = wrapper.findAll('.Select__menu .Select__option')
await options[3].trigger('click')
const nodeCanvasPixelsBefore = getPixels(
wrapper.find('.test_graph_output canvas.sigma-nodes').wrapperElement
)
const edgeCanvasPixelsBefore = getPixels(
wrapper.find('.test_graph_output canvas.sigma-edges').wrapperElement
)
await wrapper.setProps({
dataSources: {
doc: [
'{"object_type": 0, "node_id": 1}',
'{"object_type": 0, "node_id": 2}',
'{"object_type": 0, "node_id": 3}',
'{"object_type": 1, "source": 1, "target": 2}',
'{"object_type": 1, "source": 1, "target": 3}'
]
}
})
const nodeCanvasPixelsAfter = getPixels(
wrapper.find('.test_graph_output canvas.sigma-nodes').wrapperElement
)
const edgeCanvasPixelsAfter = getPixels(
wrapper.find('.test_graph_output canvas.sigma-edges').wrapperElement
)
expect(nodeCanvasPixelsBefore).not.equal(nodeCanvasPixelsAfter)
expect(edgeCanvasPixelsBefore).not.equal(edgeCanvasPixelsAfter)
await wrapper.setProps({
dataSources: null
})
const nodeCanvasPixelsAfterEmtyData = getPixels(
wrapper.find('.test_graph_output canvas.sigma-nodes').wrapperElement
)
const edgeCanvasPixelsAfterEmtyData = getPixels(
wrapper.find('.test_graph_output canvas.sigma-edges').wrapperElement
)
expect(nodeCanvasPixelsAfterEmtyData).equal(nodeCanvasPixelsAfter)
expect(edgeCanvasPixelsAfterEmtyData).equal(edgeCanvasPixelsAfter)
wrapper.unmount()
})
it('saveAsPng', async () => {
const wrapper = mount(Graph, {
props: {
dataSources: {
doc: [
'{"object_type":0,"node_id":"Gryffindor"}',
'{"object_type":0,"node_id":"Hufflepuff"}'
]
},
initOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
nodes: {
size: {
type: 'constant',
value: 10
},
color: {
type: 'constant',
value: '#1F77B4'
},
label: {
source: null,
color: '#444444'
}
},
edges: {
showDirection: true,
size: {
type: 'constant',
value: 2
},
color: {
type: 'constant',
value: '#a2b1c6'
},
label: {
source: null,
color: '#a2b1c6'
}
}
},
layout: {
type: 'circular',
options: null
}
}
},
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
sinon.stub(wrapper.vm.$refs.graphEditor, 'saveAsPng')
await wrapper.vm.saveAsPng()
expect(wrapper.emitted().loadingImageCompleted.length).to.equal(1)
})
it('hides and shows controls depending on showViewSettings and resizes the graph', async () => {
const wrapper = mount(Graph, {
attachTo: document.body,
props: {
dataSources: {
doc: [
'{"object_type":0,"node_id":"Gryffindor"}',
'{"object_type":0,"node_id":"Hufflepuff"}'
]
},
initOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
nodes: {
size: {
type: 'constant',
value: 10
},
color: {
type: 'constant',
value: '#1F77B4'
},
label: {
source: null,
color: '#444444'
}
},
edges: {
showDirection: true,
size: {
type: 'constant',
value: 2
},
color: {
type: 'constant',
value: '#a2b1c6'
},
label: {
source: null,
color: '#a2b1c6'
}
}
},
layout: {
type: 'circular',
options: null
}
}
},
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
const canvas = wrapper.find('canvas.sigma-nodes').wrapperElement
const initialPlotWidth = canvas.scrollWidth
const initialPlotHeight = canvas.scrollHeight
expect(
wrapper.find('.plotly_editor .editor_controls').isVisible()
).to.equal(false)
await wrapper.setProps({ showViewSettings: true })
await flushPromises()
expect(canvas.scrollWidth).not.to.equal(initialPlotWidth)
expect(canvas.scrollHeight).to.equal(initialPlotHeight)
expect(
wrapper.find('.plotly_editor .editor_controls').isVisible()
).to.equal(true)
wrapper.unmount()
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ import { expect } from 'chai'
import sinon from 'sinon' import sinon from 'sinon'
import { mount, shallowMount } from '@vue/test-utils' import { mount, shallowMount } from '@vue/test-utils'
import { createStore } from 'vuex' import { createStore } from 'vuex'
import MainMenu from '@/views/MainView/MainMenu' import MainMenu from '@/components/MainMenu'
import storedInquiries from '@/lib/storedInquiries' import storedInquiries from '@/lib/storedInquiries'
import { nextTick } from 'vue' import { nextTick } from 'vue'
import eventBus from '@/lib/eventBus' import eventBus from '@/lib/eventBus'

View File

@@ -1,11 +1,11 @@
import { expect } from 'chai' import { expect } from 'chai'
import { mount, flushPromises } from '@vue/test-utils' import { mount, flushPromises } from '@vue/test-utils'
import Pivot from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot' import Pivot from '@/components/Pivot/index.vue'
import chartHelper from '@/lib/chartHelper' import chartHelper from '@/lib/chartHelper'
import fIo from '@/lib/utils/fileIo' import fIo from '@/lib/utils/fileIo'
import $ from 'jquery' import $ from 'jquery'
import sinon from 'sinon' import sinon from 'sinon'
import pivotHelper from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/pivotHelper' import pivotHelper from '@/components/Pivot/pivotHelper'
describe('Pivot.vue', () => { describe('Pivot.vue', () => {
let container let container
@@ -586,4 +586,48 @@ describe('Pivot.vue', () => {
wrapper.unmount() wrapper.unmount()
}) })
it('hides or shows controlls depending on showViewSettings and resizes standart chart', async () => {
const wrapper = mount(Pivot, {
global: {
mocks: { $store: { state: { isWorkspaceVisible: true } } }
},
props: {
dataSources: {
item: ['foo', 'bar', 'bar', 'bar'],
year: [2021, 2021, 2020, 2020]
},
initOptions: {
rows: ['item'],
cols: ['year'],
colOrder: 'key_a_to_z',
rowOrder: 'key_a_to_z',
aggregatorName: 'Count',
vals: [],
renderer: $.pivotUtilities.renderers['Bar Chart'],
rendererName: 'Bar Chart'
}
},
attachTo: container
})
expect(wrapper.find('.pivot-ui').isVisible()).to.equal(false)
await flushPromises()
const plot = wrapper.find('.svg-container').wrapperElement
const initialPlotHeight = plot.scrollHeight
await wrapper.setProps({ showViewSettings: true })
expect(wrapper.find('.pivot-ui').isVisible()).to.equal(true)
await flushPromises()
const plotAfterResize = wrapper.find('.svg-container').wrapperElement
expect(plotAfterResize.scrollWidth.scrollHeight).not.to.equal(
initialPlotHeight
)
wrapper.unmount()
})
}) })

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai' import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils' import { shallowMount } from '@vue/test-utils'
import PivotSortBtn from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/PivotUi/PivotSortBtn' import PivotSortBtn from '@/components/Pivot/PivotUi/PivotSortBtn'
describe('PivotSortBtn.vue', () => { describe('PivotSortBtn.vue', () => {
it('switches order', async () => { it('switches order', async () => {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai' import { expect } from 'chai'
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import PivotUi from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/PivotUi' import PivotUi from '@/components/Pivot/PivotUi'
describe('PivotUi.vue', () => { describe('PivotUi.vue', () => {
it('returns value when settings changed', async () => { it('returns value when settings changed', async () => {

View File

@@ -3,7 +3,7 @@ import {
_getDataSources, _getDataSources,
getPivotCanvas, getPivotCanvas,
getPivotHtml getPivotHtml
} from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/pivotHelper' } from '@/components/Pivot/pivotHelper'
describe('pivotHelper.js', () => { describe('pivotHelper.js', () => {
it('_getDataSources returns data sources', () => { it('_getDataSources returns data sources', () => {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai' import { expect } from 'chai'
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import Record from '@/views/MainView/Workspace/Tabs/Tab/RunResult/Record' import Record from '@/components/RunResult/Record'
describe('Record.vue', () => { describe('Record.vue', () => {
it('shows record with selected cell', async () => { it('shows record with selected cell', async () => {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai' import { expect } from 'chai'
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import RunResult from '@/views/MainView/Workspace/Tabs/Tab/RunResult' import RunResult from '@/components/RunResult'
import csv from '@/lib/csv' import csv from '@/lib/csv'
import sinon from 'sinon' import sinon from 'sinon'
import { nextTick } from 'vue' import { nextTick } from 'vue'
@@ -18,7 +18,6 @@ describe('RunResult.vue', () => {
sinon.spy(window, 'alert') sinon.spy(window, 'alert')
const wrapper = mount(RunResult, { const wrapper = mount(RunResult, {
props: { props: {
tab: { id: 1 },
result: { result: {
columns: ['id', 'name'], columns: ['id', 'name'],
values: { values: {
@@ -54,7 +53,6 @@ describe('RunResult.vue', () => {
const wrapper = mount(RunResult, { const wrapper = mount(RunResult, {
attachTo: document.body, attachTo: document.body,
props: { props: {
tab: { id: 1 },
result: { result: {
columns: ['id', 'name'], columns: ['id', 'name'],
values: { values: {
@@ -114,7 +112,6 @@ describe('RunResult.vue', () => {
const wrapper = mount(RunResult, { const wrapper = mount(RunResult, {
attachTo: document.body, attachTo: document.body,
props: { props: {
tab: { id: 1 },
result: { result: {
columns: ['id', 'name'], columns: ['id', 'name'],
values: { values: {
@@ -154,7 +151,6 @@ describe('RunResult.vue', () => {
const wrapper = mount(RunResult, { const wrapper = mount(RunResult, {
attachTo: document.body, attachTo: document.body,
props: { props: {
tab: { id: 1 },
result: { result: {
columns: ['id', 'name'], columns: ['id', 'name'],
values: { values: {
@@ -196,7 +192,6 @@ describe('RunResult.vue', () => {
it('shows value of selected cell - result set', async () => { it('shows value of selected cell - result set', async () => {
const wrapper = mount(RunResult, { const wrapper = mount(RunResult, {
props: { props: {
tab: { id: 1 },
result: { result: {
columns: ['id', 'name'], columns: ['id', 'name'],
values: { values: {
@@ -250,9 +245,9 @@ describe('RunResult.vue', () => {
// Click on 'bar' cell again // Click on 'bar' cell again
await rows[1].findAll('td')[1].trigger('click') await rows[1].findAll('td')[1].trigger('click')
expect( expect(wrapper.find('.value-viewer').text()).to.equals(
wrapper.find('.value-viewer-container .table-preview').text() 'No cell selected to view'
).to.equals('No cell selected to view') )
wrapper.unmount() wrapper.unmount()
}) })
@@ -260,7 +255,6 @@ describe('RunResult.vue', () => {
const wrapper = mount(RunResult, { const wrapper = mount(RunResult, {
attachTo: document.body, attachTo: document.body,
props: { props: {
tab: { id: 1 },
result: { result: {
columns: ['id', 'name'], columns: ['id', 'name'],
values: { values: {
@@ -323,9 +317,9 @@ describe('RunResult.vue', () => {
// Click on 'foo' cell again // Click on 'foo' cell again
await rows[1].find('td').trigger('click') await rows[1].find('td').trigger('click')
expect( expect(wrapper.find('.value-viewer').text()).to.equals(
wrapper.find('.value-viewer-container .table-preview').text() 'No cell selected to view'
).to.equals('No cell selected to view') )
wrapper.unmount() wrapper.unmount()
}) })
@@ -333,7 +327,6 @@ describe('RunResult.vue', () => {
const wrapper = mount(RunResult, { const wrapper = mount(RunResult, {
attachTo: document.body, attachTo: document.body,
props: { props: {
tab: { id: 1 },
result: { result: {
columns: ['id', 'name'], columns: ['id', 'name'],
values: { values: {

View File

@@ -4,8 +4,8 @@ import { mount } from '@vue/test-utils'
import { createStore } from 'vuex' import { createStore } from 'vuex'
import actions from '@/store/actions' import actions from '@/store/actions'
import mutations from '@/store/mutations' import mutations from '@/store/mutations'
import Schema from '@/views/MainView/Workspace/Schema' import Schema from '@/components/Schema'
import TableDescription from '@/views/MainView/Workspace/Schema/TableDescription' import TableDescription from '@/components/Schema/TableDescription'
import database from '@/lib/database' import database from '@/lib/database'
import fIo from '@/lib/utils/fileIo' import fIo from '@/lib/utils/fileIo'
import csv from '@/lib/csv' import csv from '@/lib/csv'

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai' import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils' import { shallowMount } from '@vue/test-utils'
import TableDescription from '@/views/MainView/Workspace/Schema/TableDescription' import TableDescription from '@/components/Schema/TableDescription'
describe('TableDescription.vue', () => { describe('TableDescription.vue', () => {
it('Initially the columns are hidden and table name is rendered', () => { it('Initially the columns are hidden and table name is rendered', () => {

View File

@@ -1,7 +1,7 @@
import { expect } from 'chai' import { expect } from 'chai'
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import { createStore } from 'vuex' import { createStore } from 'vuex'
import SqlEditor from '@/views/MainView/Workspace/Tabs/Tab/SqlEditor' import SqlEditor from '@/components/SqlEditor'
import { nextTick } from 'vue' import { nextTick } from 'vue'
describe('SqlEditor.vue', () => { describe('SqlEditor.vue', () => {

View File

@@ -1,9 +1,7 @@
import { expect } from 'chai' import { expect } from 'chai'
import sinon from 'sinon' import sinon from 'sinon'
import state from '@/store/state' import state from '@/store/state'
import showHint, { import showHint, { getHints } from '@/components/SqlEditor/hint'
getHints
} from '@/views/MainView/Workspace/Tabs/Tab/SqlEditor/hint'
import CM from 'codemirror' import CM from 'codemirror'
describe('hint.js', () => { describe('hint.js', () => {

View File

@@ -3,7 +3,7 @@ import sinon from 'sinon'
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import mutations from '@/store/mutations' import mutations from '@/store/mutations'
import { createStore } from 'vuex' import { createStore } from 'vuex'
import Tab from '@/views/MainView/Workspace/Tabs/Tab' import Tab from '@/components/Tab'
import { nextTick } from 'vue' import { nextTick } from 'vue'
let place let place

View File

@@ -3,7 +3,7 @@ import sinon from 'sinon'
import { shallowMount, mount } from '@vue/test-utils' import { shallowMount, mount } from '@vue/test-utils'
import mutations from '@/store/mutations' import mutations from '@/store/mutations'
import { createStore } from 'vuex' import { createStore } from 'vuex'
import Tabs from '@/views/MainView/Workspace/Tabs' import Tabs from '@/components/Tabs'
import eventBus from '@/lib/eventBus' import eventBus from '@/lib/eventBus'
import { nextTick } from 'vue' import { nextTick } from 'vue'
import cIo from '@/lib/utils/clipboardIo' import cIo from '@/lib/utils/clipboardIo'

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai' import { expect } from 'chai'
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import ValueViewer from '@/views/MainView/Workspace/Tabs/Tab/RunResult/ValueViewer.vue' import ValueViewer from '@/components/ValueViewer.vue'
import sinon from 'sinon' import sinon from 'sinon'
describe('ValueViewer.vue', () => { describe('ValueViewer.vue', () => {
@@ -11,28 +11,45 @@ describe('ValueViewer.vue', () => {
it('shows value in text mode', () => { it('shows value in text mode', () => {
const wrapper = mount(ValueViewer, { const wrapper = mount(ValueViewer, {
props: { props: {
cellValue: 'foo' value: 'foo'
} }
}) })
expect(wrapper.find('.value-body').text()).to.equals('foo') expect(wrapper.find('.value-body').text()).to.equals('foo')
expect(wrapper.find('button.text').attributes('aria-selected')).to.equal(
'true'
)
})
it('shows meta values', async () => {
const wrapper = mount(ValueViewer, {
props: {
value: new Uint8Array()
}
})
expect(wrapper.find('.value-body').text()).to.equals('BLOB')
await wrapper.setProps({ value: null })
expect(wrapper.find('.value-body').text()).to.equals('NULL')
}) })
it('shows error in json mode if the value is not json', async () => { it('shows error in json mode if the value is not json', async () => {
const wrapper = mount(ValueViewer, { const wrapper = mount(ValueViewer, {
props: { props: {
cellValue: 'foo' value: 'foo',
defaultFormat: 'json'
} }
}) })
await wrapper.find('button.json').trigger('click')
expect(wrapper.find('.value-body').text()).to.equals("Can't parse JSON.") expect(wrapper.find('.value-body').text()).to.equals("Can't parse JSON.")
expect(wrapper.find('button[aria-selected="true"]').text()).contains('JSON')
}) })
it('copy to clipboard', async () => { it('copy to clipboard', async () => {
sinon.stub(window.navigator.clipboard, 'writeText').resolves() sinon.stub(window.navigator.clipboard, 'writeText').resolves()
const wrapper = mount(ValueViewer, { const wrapper = mount(ValueViewer, {
props: { props: {
cellValue: 'foo' value: 'foo'
} }
}) })
@@ -41,13 +58,20 @@ describe('ValueViewer.vue', () => {
expect(window.navigator.clipboard.writeText.calledOnceWith('foo')).to.equal( expect(window.navigator.clipboard.writeText.calledOnceWith('foo')).to.equal(
true true
) )
await wrapper.setProps({ value: '{"foo": "bar"}' })
await wrapper.find('button.json').trigger('click')
await wrapper.find('button.copy').trigger('click')
expect(window.navigator.clipboard.writeText.args[1][0]).to.equal(
'{\n "foo": "bar"\n}'
)
}) })
it('wraps lines', async () => { it('wraps lines', async () => {
const wrapper = mount(ValueViewer, { const wrapper = mount(ValueViewer, {
attachTo: document.body, attachTo: document.body,
props: { props: {
cellValue: 'foo' value: 'foo'
} }
}) })
@@ -55,7 +79,7 @@ describe('ValueViewer.vue', () => {
const valueBody = wrapper.find('.value-body').wrapperElement const valueBody = wrapper.find('.value-body').wrapperElement
expect(valueBody.scrollWidth).to.equal(valueBody.clientWidth) expect(valueBody.scrollWidth).to.equal(valueBody.clientWidth)
await wrapper.setProps({ cellValue: 'foo'.repeat(100) }) await wrapper.setProps({ value: 'foo'.repeat(100) })
expect(valueBody.scrollWidth).not.to.equal(valueBody.clientWidth) expect(valueBody.scrollWidth).not.to.equal(valueBody.clientWidth)
await wrapper.find('button.line-wrap').trigger('click') await wrapper.find('button.line-wrap').trigger('click')
@@ -67,7 +91,7 @@ describe('ValueViewer.vue', () => {
const wrapper = mount(ValueViewer, { const wrapper = mount(ValueViewer, {
attachTo: document.body, attachTo: document.body,
props: { props: {
cellValue: '{"foo": "foofoofoofoofoofoofoofoofoofoo"}' value: '{"foo": "foofoofoofoofoofoofoofoofoofoo"}'
} }
}) })
@@ -83,4 +107,15 @@ describe('ValueViewer.vue', () => {
expect(codeMirrorScroll.scrollWidth).to.equal(codeMirrorScroll.clientWidth) expect(codeMirrorScroll.scrollWidth).to.equal(codeMirrorScroll.clientWidth)
wrapper.unmount() wrapper.unmount()
}) })
it('shows empty message if empty is true', () => {
const wrapper = mount(ValueViewer, {
props: {
empty: true,
emptyMessage: 'I am empty'
}
})
expect(wrapper.find('.value-viewer').text()).to.equals('I am empty')
})
}) })

File diff suppressed because it is too large Load Diff

View File

@@ -39,4 +39,101 @@ describe('_migrations.js', () => {
} }
]) ])
}) })
it('migrates from version 2 to the current', () => {
const oldInquiries = [
{
id: '123',
name: 'foo',
query: 'SELECT * FROM foo',
viewType: 'chart',
viewOptions: { here_are: 'foo chart settings' },
createdAt: '2021-05-06T11:05:50.877Z'
},
{
id: '456',
name: 'bar',
query: 'SELECT * FROM bar',
viewType: 'graph',
viewOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
nodes: {
size: { type: 'constant', value: 10 },
color: {
type: 'calculated',
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'reversed'
},
label: { source: 'label', color: '#444444' }
},
edges: {
showDirection: true,
size: { type: 'constant', value: 2 },
color: { type: 'constant', value: '#a2b1c6' },
label: { source: null, color: '#a2b1c6' }
}
},
layout: { type: 'circular', options: null }
},
createdAt: '2021-05-07T11:05:50.877Z'
}
]
expect(migrations._migrate(2, oldInquiries)).to.eql([
{
id: '123',
name: 'foo',
query: 'SELECT * FROM foo',
viewType: 'chart',
viewOptions: { here_are: 'foo chart settings' },
createdAt: '2021-05-06T11:05:50.877Z'
},
{
id: '456',
name: 'bar',
query: 'SELECT * FROM bar',
viewType: 'graph',
viewOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: {
size: { type: 'constant', value: 10 },
color: {
type: 'calculated',
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'reversed',
opacity: 100
},
label: { source: 'label', color: '#444444' }
},
edges: {
showDirection: true,
size: { type: 'constant', value: 2 },
color: { type: 'constant', value: '#a2b1c6' },
label: { source: null, color: '#a2b1c6' }
}
},
layout: { type: 'circular', options: null }
},
createdAt: '2021-05-07T11:05:50.877Z'
}
])
})
}) })

View File

@@ -18,7 +18,7 @@ describe('storedInquiries.js', () => {
expect(inquiries).to.eql([]) expect(inquiries).to.eql([])
}) })
it('getStoredInquiries migrate and returns inquiries of v1', () => { it('getStoredInquiries migrates and returns inquiries of v1', () => {
localStorage.setItem( localStorage.setItem(
'myQueries', 'myQueries',
JSON.stringify([ JSON.stringify([
@@ -55,6 +55,114 @@ describe('storedInquiries.js', () => {
]) ])
}) })
it('getStoredInquiries migrates and returns inquiries of v2', () => {
localStorage.setItem(
'myInquiries',
JSON.stringify({
version: 2,
inquiries: [
{
id: 'Xh1Hc9v7P3mRPZVM59QiC',
query: 'SELECT * from doc',
viewType: 'graph',
viewOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
nodes: {
size: { type: 'constant', value: 10 },
color: {
type: 'calculated',
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'reversed'
},
label: { source: 'label', color: '#444444' }
},
edges: {
showDirection: true,
size: { type: 'constant', value: 2 },
color: { type: 'constant', value: '#a2b1c6' },
label: { source: null, color: '#a2b1c6' }
}
},
layout: { type: 'circular', options: null }
},
name: 'student graph',
updatedAt: '2026-01-19T21:49:40.708Z',
createdAt: '2026-01-19T21:46:13.899Z'
},
{
id: 'Yh1Hc9v7P3mRPZVM59QiD',
query: 'SELECT * from test',
viewType: 'chart',
viewOptions: 'some chart view options',
name: 'student chart',
updatedAt: '2026-01-19T21:49:40.708Z',
createdAt: '2026-01-19T21:46:13.899Z'
}
]
})
)
const inquiries = storedInquiries.getStoredInquiries()
expect(inquiries).to.eql([
{
id: 'Xh1Hc9v7P3mRPZVM59QiC',
query: 'SELECT * from doc',
viewType: 'graph',
viewOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: {
size: { type: 'constant', value: 10 },
color: {
type: 'calculated',
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'reversed',
opacity: 100
},
label: { source: 'label', color: '#444444' }
},
edges: {
showDirection: true,
size: { type: 'constant', value: 2 },
color: { type: 'constant', value: '#a2b1c6' },
label: { source: null, color: '#a2b1c6' }
}
},
layout: { type: 'circular', options: null }
},
name: 'student graph',
updatedAt: '2026-01-19T21:49:40.708Z',
createdAt: '2026-01-19T21:46:13.899Z'
},
{
id: 'Yh1Hc9v7P3mRPZVM59QiD',
query: 'SELECT * from test',
viewType: 'chart',
viewOptions: 'some chart view options',
name: 'student chart',
updatedAt: '2026-01-19T21:49:40.708Z',
createdAt: '2026-01-19T21:46:13.899Z'
}
])
})
it('updateStorage and getStoredInquiries', () => { it('updateStorage and getStoredInquiries', () => {
const data = [{ id: 1 }, { id: 2 }] const data = [{ id: 1 }, { id: 2 }]
storedInquiries.updateStorage(data) storedInquiries.updateStorage(data)
@@ -136,7 +244,7 @@ describe('storedInquiries.js', () => {
const str = storedInquiries.serialiseInquiries(inquiryList) const str = storedInquiries.serialiseInquiries(inquiryList)
const parsedJson = JSON.parse(str) const parsedJson = JSON.parse(str)
expect(parsedJson.version).to.equal(2) expect(parsedJson.version).to.equal(3)
expect(parsedJson.inquiries).to.have.lengthOf(2) expect(parsedJson.inquiries).to.have.lengthOf(2)
expect(parsedJson.inquiries[1]).to.eql(inquiryList[1]) expect(parsedJson.inquiries[1]).to.eql(inquiryList[1])
expect(parsedJson.inquiries[0]).to.eql({ expect(parsedJson.inquiries[0]).to.eql({
@@ -149,7 +257,7 @@ describe('storedInquiries.js', () => {
}) })
}) })
it('deserialiseInquiries migrates inquiries', () => { it('deserialiseInquiries migrates inquiries of v1', () => {
const str = `[ const str = `[
{ {
"id": 1, "id": 1,
@@ -188,7 +296,110 @@ describe('storedInquiries.js', () => {
]) ])
}) })
it('deserialiseInquiries return array for one inquiry of v1', () => { it('deserialiseInquiries migrates inquiries of v2', () => {
const str = `{
"version": 2,
"inquiries": [
{
"id": 1,
"name": "foo",
"query": "select * from foo",
"viewType": "chart",
"viewOptions": [],
"createdAt": "2020-11-03T14:17:49.524Z"
},
{
"id": "Xh1Hc9v7P3mRPZVM59QiC",
"query": "SELECT * from doc",
"viewType": "graph",
"viewOptions": {
"structure": {
"nodeId": "node_id",
"objectType": "object_type",
"edgeSource": "source",
"edgeTarget": "target"
},
"style": {
"backgroundColor": "white",
"nodes": {
"size": { "type": "constant", "value": 10 },
"color": {
"type": "calculated",
"method": "degree",
"colorscale": null,
"mode": "continious",
"colorscaleDirection": "reversed"
},
"label": { "source": "label", "color": "#444444" }
},
"edges": {
"showDirection": true,
"size": { "type": "constant", "value": 2 },
"color": { "type": "constant", "value": "#a2b1c6" },
"label": { "source": null, "color": "#a2b1c6" }
}
},
"layout": { "type": "circular", "options": null }
},
"name": "student graph",
"createdAt": "2026-01-19T21:46:13.899Z"
}
]
}
`
const inquiry = storedInquiries.deserialiseInquiries(str)
expect(inquiry).to.eql([
{
id: 1,
name: 'foo',
query: 'select * from foo',
viewType: 'chart',
viewOptions: [],
createdAt: '2020-11-03T14:17:49.524Z'
},
{
id: 'Xh1Hc9v7P3mRPZVM59QiC',
query: 'SELECT * from doc',
viewType: 'graph',
viewOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: {
size: { type: 'constant', value: 10 },
color: {
type: 'calculated',
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'reversed',
opacity: 100
},
label: { source: 'label', color: '#444444' }
},
edges: {
showDirection: true,
size: { type: 'constant', value: 2 },
color: { type: 'constant', value: '#a2b1c6' },
label: { source: null, color: '#a2b1c6' }
}
},
layout: { type: 'circular', options: null }
},
name: 'student graph',
createdAt: '2026-01-19T21:46:13.899Z'
}
])
})
it('deserialiseInquiries returns array for one inquiry of v1', () => {
const str = ` const str = `
{ {
"id": 1, "id": 1,
@@ -215,7 +426,7 @@ describe('storedInquiries.js', () => {
it('deserialiseInquiries generates new id to avoid duplication', () => { it('deserialiseInquiries generates new id to avoid duplication', () => {
storedInquiries.updateStorage([{ id: 1 }]) storedInquiries.updateStorage([{ id: 1 }])
const str = `{ const str = `{
"version": 2, "version": 3,
"inquiries": [ "inquiries": [
{ {
"id": 1, "id": 1,
@@ -275,7 +486,7 @@ describe('storedInquiries.js', () => {
it('importInquiries', async () => { it('importInquiries', async () => {
const str = `{ const str = `{
"version": 2, "version": 3,
"inquiries": [{ "inquiries": [{
"id": 1, "id": 1,
"name": "foo", "name": "foo",
@@ -300,7 +511,7 @@ describe('storedInquiries.js', () => {
]) ])
}) })
it('readPredefinedInquiries old', async () => { it('readPredefinedInquiries v1', async () => {
const str = `[ const str = `[
{ {
"id": 1, "id": 1,
@@ -325,18 +536,55 @@ describe('storedInquiries.js', () => {
]) ])
}) })
it('readPredefinedInquiries', async () => { it('readPredefinedInquiries v2', async () => {
const str = `{ const str = `{
"version": 2, "version": 2,
"inquiries": [ "inquiries": [
{ {
"id": 1, "id": 1,
"name": "foo", "name": "foo",
"query": "select * from foo", "query": "select * from foo",
"viewType": "chart", "viewType": "chart",
"viewOptions": [], "viewOptions": [],
"createdAt": "2020-11-03T14:17:49.524Z" "createdAt": "2020-11-03T14:17:49.524Z"
}] },
{
"id": "Xh1Hc9v7P3mRPZVM59QiC",
"query": "SELECT * from doc",
"viewType": "graph",
"viewOptions": {
"structure": {
"nodeId": "node_id",
"objectType": "object_type",
"edgeSource": "source",
"edgeTarget": "target"
},
"style": {
"backgroundColor": "white",
"nodes": {
"size": { "type": "constant", "value": 10 },
"color": {
"type": "calculated",
"method": "degree",
"colorscale": null,
"mode": "continious",
"colorscaleDirection": "reversed"
},
"label": { "source": "label", "color": "#444444" }
},
"edges": {
"showDirection": true,
"size": { "type": "constant", "value": 2 },
"color": { "type": "constant", "value": "#a2b1c6" },
"label": { "source": null, "color": "#a2b1c6" }
}
},
"layout": { "type": "circular", "options": null }
},
"name": "student graph",
"createdAt": "2026-01-19T21:46:13.899Z"
}
]
} }
` `
sinon.stub(fu, 'readFile').returns(Promise.resolve(new Response(str))) sinon.stub(fu, 'readFile').returns(Promise.resolve(new Response(str)))
@@ -350,6 +598,90 @@ describe('storedInquiries.js', () => {
viewType: 'chart', viewType: 'chart',
viewOptions: [], viewOptions: [],
createdAt: '2020-11-03T14:17:49.524Z' createdAt: '2020-11-03T14:17:49.524Z'
},
{
id: 'Xh1Hc9v7P3mRPZVM59QiC',
query: 'SELECT * from doc',
viewType: 'graph',
viewOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: {
size: { type: 'constant', value: 10 },
color: {
type: 'calculated',
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'reversed',
opacity: 100
},
label: { source: 'label', color: '#444444' }
},
edges: {
showDirection: true,
size: { type: 'constant', value: 2 },
color: { type: 'constant', value: '#a2b1c6' },
label: { source: null, color: '#a2b1c6' }
}
},
layout: { type: 'circular', options: null }
},
name: 'student graph',
createdAt: '2026-01-19T21:46:13.899Z'
}
])
})
it('readPredefinedInquiries', async () => {
const str = `{
"version": 3,
"inquiries": [
{
"id": 1,
"name": "foo",
"query": "select * from foo",
"viewType": "chart",
"viewOptions": [],
"createdAt": "2020-11-03T14:17:49.524Z"
},
{
"id": 2,
"name": "boo",
"query": "select * from boo",
"viewType": "graph",
"viewOptions": {},
"createdAt": "2020-11-03T14:17:49.524Z"
}
]
}
`
sinon.stub(fu, 'readFile').returns(Promise.resolve(new Response(str)))
const inquiries = await storedInquiries.readPredefinedInquiries()
expect(fu.readFile.calledOnceWith('./inquiries.json')).to.equal(true)
expect(inquiries).to.eql([
{
id: 1,
name: 'foo',
query: 'select * from foo',
viewType: 'chart',
viewOptions: [],
createdAt: '2020-11-03T14:17:49.524Z'
},
{
id: 2,
name: 'boo',
query: 'select * from boo',
viewType: 'graph',
viewOptions: {},
createdAt: '2020-11-03T14:17:49.524Z'
} }
]) ])
}) })

20
tests/testUtils.js Normal file
View File

@@ -0,0 +1,20 @@
export function waitCondition(condition, timeoutMs = 5000) {
return new Promise((resolve, reject) => {
if (condition()) {
resolve()
return
}
const start = new Date().getTime()
const interval = setInterval(() => {
if (condition()) {
clearInterval(interval)
resolve()
} else {
if (new Date().getTime() - start > timeoutMs) {
clearInterval(interval)
reject()
}
}
}, 500)
})
}

View File

@@ -2,7 +2,7 @@ import { expect } from 'chai'
import sinon from 'sinon' import sinon from 'sinon'
import { mount, shallowMount } from '@vue/test-utils' import { mount, shallowMount } from '@vue/test-utils'
import { createStore } from 'vuex' import { createStore } from 'vuex'
import Inquiries from '@/views/MainView/Inquiries' import Inquiries from '@/views/Inquiries'
import storedInquiries from '@/lib/storedInquiries' import storedInquiries from '@/lib/storedInquiries'
import mutations from '@/store/mutations' import mutations from '@/store/mutations'
import actions from '@/store/actions' import actions from '@/store/actions'

View File

@@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils'
import actions from '@/store/actions' import actions from '@/store/actions'
import mutations from '@/store/mutations' import mutations from '@/store/mutations'
import { createStore } from 'vuex' import { createStore } from 'vuex'
import Workspace from '@/views/MainView/Workspace' import Workspace from '@/views/Workspace'
describe('Workspace.vue', () => { describe('Workspace.vue', () => {
it('Creates a tab with example if schema is empty', () => { it('Creates a tab with example if schema is empty', () => {