mirror of
https://github.com/lana-k/sqliteviz.git
synced 2026-03-26 16:06:17 +08:00
Compare commits
1 Commits
0.29.0
...
1601514cca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1601514cca |
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -11,7 +11,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: Run tests
|
name: Run tests
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
@@ -21,12 +21,8 @@ jobs:
|
|||||||
- name: Install browsers
|
- name: Install browsers
|
||||||
run: |
|
run: |
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
sudo add-apt-repository -y ppa:mozillateam/ppa
|
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y \
|
sudo apt-get install -y chromium-browser firefox
|
||||||
chromium-browser \
|
|
||||||
firefox-esr \
|
|
||||||
xvfb
|
|
||||||
|
|
||||||
- name: Update npm
|
- name: Update npm
|
||||||
run: npm install -g npm@10
|
run: npm install -g npm@10
|
||||||
@@ -38,4 +34,4 @@ jobs:
|
|||||||
run: npm run lint -- --no-fix
|
run: npm run lint -- --no-fix
|
||||||
|
|
||||||
- name: Run karma tests
|
- name: Run karma tests
|
||||||
run: xvfb-run -a npm test
|
run: npm run test
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# docker build -t sqliteviz/test -f Dockerfile.test .
|
# docker build -t sqliteviz/test -f Dockerfile.test .
|
||||||
#
|
#
|
||||||
|
|
||||||
FROM node:12.22-bullseye
|
FROM node:12.22-buster
|
||||||
|
|
||||||
RUN set -ex; \
|
RUN set -ex; \
|
||||||
apt update; \
|
apt update; \
|
||||||
|
|||||||
@@ -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, graphs and pivot tables based on the result sets
|
- run SQL queries against a SQLite database and create [Plotly][11] charts 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,9 +33,7 @@ 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]
|
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].
|
||||||
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
|
||||||
@@ -49,5 +47,3 @@ Graphs are visualized with [Sigma.js][13] and [Graphology][14].
|
|||||||
[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/
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@\/*": ["./src/*"]
|
"@*": ["./src/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,23 +92,11 @@ 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: ['ChromiumHeadlessWebGL', 'FirefoxHeadlessTouch'],
|
browsers: ['ChromiumHeadless', '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
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from pathlib import Path
|
|||||||
from urllib import request
|
from urllib import request
|
||||||
|
|
||||||
|
|
||||||
amalgamation_url = 'https://sqlite.org/2025/sqlite-amalgamation-3500300.zip'
|
amalgamation_url = 'https://sqlite.org/2023/sqlite-amalgamation-3410000.zip'
|
||||||
|
|
||||||
# Extension-functions
|
# Extension-functions
|
||||||
# ===================
|
# ===================
|
||||||
@@ -22,15 +22,15 @@ contrib_functions_url = 'https://sqlite.org/contrib/download/extension-functions
|
|||||||
extension_urls = (
|
extension_urls = (
|
||||||
# Miscellaneous extensions
|
# Miscellaneous extensions
|
||||||
# ========================
|
# ========================
|
||||||
('https://sqlite.org/src/raw/e212edb2?at=series.c', 'sqlite3_series_init'),
|
('https://sqlite.org/src/raw/8d79354f?at=series.c', 'sqlite3_series_init'),
|
||||||
('https://sqlite.org/src/raw/5559daf1?at=closure.c', 'sqlite3_closure_init'),
|
('https://sqlite.org/src/raw/dbfd8543?at=closure.c', 'sqlite3_closure_init'),
|
||||||
('https://sqlite.org/src/raw/5bb2264c?at=uuid.c', 'sqlite3_uuid_init'),
|
('https://sqlite.org/src/raw/5bb2264c?at=uuid.c', 'sqlite3_uuid_init'),
|
||||||
('https://sqlite.org/src/raw/388e7f23?at=regexp.c', 'sqlite3_regexp_init'),
|
('https://sqlite.org/src/raw/5853b0e5?at=regexp.c', 'sqlite3_regexp_init'),
|
||||||
('https://sqlite.org/src/raw/72e05a21?at=percentile.c', 'sqlite3_percentile_init'),
|
('https://sqlite.org/src/raw/b9086e22?at=percentile.c', 'sqlite3_percentile_init'),
|
||||||
('https://sqlite.org/src/raw/228d47e9?at=decimal.c', 'sqlite3_decimal_init'),
|
('https://sqlite.org/src/raw/09f967dc?at=decimal.c', 'sqlite3_decimal_init'),
|
||||||
# Third-party extension
|
# Third-party extension
|
||||||
# =====================
|
# =====================
|
||||||
('https://github.com/jakethaw/pivot_vtab/raw/e7705f34/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
|
('https://github.com/jakethaw/pivot_vtab/raw/9323ef93/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
|
||||||
('https://github.com/nalgeon/sqlean/raw/95e8d21a/src/pearson.c', 'sqlite3_pearson_init'),
|
('https://github.com/nalgeon/sqlean/raw/95e8d21a/src/pearson.c', 'sqlite3_pearson_init'),
|
||||||
# Third-party extension with own dependencies
|
# Third-party extension with own dependencies
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
|||||||
2
lib/sql-js/dist/sql-wasm.js
vendored
2
lib/sql-js/dist/sql-wasm.js
vendored
File diff suppressed because one or more lines are too long
BIN
lib/sql-js/dist/sql-wasm.wasm
vendored
BIN
lib/sql-js/dist/sql-wasm.wasm
vendored
Binary file not shown.
24089
package-lock.json
generated
24089
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sqliteviz",
|
"name": "sqliteviz",
|
||||||
"version": "0.29.0",
|
"version": "0.26.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -13,7 +13,6 @@
|
|||||||
"format": "prettier . --write"
|
"format": "prettier . --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sigma/export-image": "^3.0.0",
|
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"codemirror": "^5.65.18",
|
"codemirror": "^5.65.18",
|
||||||
"codemirror-editor-vue3": "^2.8.0",
|
"codemirror-editor-vue3": "^2.8.0",
|
||||||
@@ -36,7 +35,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
50
src/App.vue
50
src/App.vue
@@ -1,13 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<router-view />
|
<router-view />
|
||||||
|
<modals-container />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import storedInquiries from '@/lib/storedInquiries'
|
import storedInquiries from '@/lib/storedInquiries'
|
||||||
|
import { ModalsContainer } from 'vue-final-modal'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: { ModalsContainer },
|
||||||
computed: {
|
computed: {
|
||||||
inquiries() {
|
inquiries() {
|
||||||
return this.$store.state.inquiries
|
return this.$store.state.inquiries
|
||||||
@@ -23,16 +26,53 @@ export default {
|
|||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
|
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
|
||||||
addEventListener('storage', event => {
|
|
||||||
if (event.key === storedInquiries.myInquiriesKey) {
|
|
||||||
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url('@/assets/fonts/OpenSans-Regular.woff2');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url('@/assets/fonts/OpenSans-SemiBold.woff2');
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url('@/assets/fonts/OpenSans-Bold.woff2');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url('@/assets/fonts/OpenSans-Italic.woff2');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url('@/assets/fonts/OpenSans-SemiBoldItalic.woff2');
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url('@/assets/fonts/OpenSans-BoldItalic.woff2');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
#app,
|
#app,
|
||||||
.dialog,
|
.dialog,
|
||||||
input,
|
input,
|
||||||
|
|||||||
@@ -4,10 +4,3 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-view-warning {
|
|
||||||
height: 40px;
|
|
||||||
line-height: 40px;
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
src: url('@/assets/fonts/OpenSans-Regular.woff2');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
src: url('@/assets/fonts/OpenSans-SemiBold.woff2');
|
|
||||||
font-weight: 600;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
src: url('@/assets/fonts/OpenSans-Bold.woff2');
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
src: url('@/assets/fonts/OpenSans-Italic.woff2');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
src: url('@/assets/fonts/OpenSans-SemiBoldItalic.woff2');
|
|
||||||
font-weight: 600;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
src: url('@/assets/fonts/OpenSans-BoldItalic.woff2');
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--color-accent-shade);
|
|
||||||
}
|
|
||||||
@@ -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/Common/TextField'
|
import TextField from '@/components/TextField'
|
||||||
import DelimiterSelector from './DelimiterSelector'
|
import DelimiterSelector from './DelimiterSelector'
|
||||||
import CheckBox from '@/components/Common/CheckBox'
|
import CheckBox from '@/components/CheckBox'
|
||||||
import SqlTable from '@/components/SqlTable'
|
import SqlTable from '@/components/SqlTable'
|
||||||
import Logs from '@/components/Common/Logs'
|
import Logs from '@/components/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'
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Field label="Scaling ratio" fieldContainerClassName="test_fa2_scaling">
|
|
||||||
<NumericInput
|
|
||||||
:value="modelValue.scalingRatio"
|
|
||||||
@update="update('scalingRatio', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
label="Prevent overlapping"
|
|
||||||
fieldContainerClassName="test_fa2_adjustSizes"
|
|
||||||
>
|
|
||||||
<RadioBlocks
|
|
||||||
:options="booleanOptions"
|
|
||||||
:activeOption="modelValue.adjustSizes"
|
|
||||||
@option-change="update('adjustSizes', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
label="Barnes-Hut optimize"
|
|
||||||
fieldContainerClassName="test_fa2_barnes_hut"
|
|
||||||
>
|
|
||||||
<RadioBlocks
|
|
||||||
:options="booleanOptions"
|
|
||||||
:activeOption="modelValue.barnesHutOptimize"
|
|
||||||
@option-change="update('barnesHutOptimize', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
v-show="modelValue.barnesHutOptimize"
|
|
||||||
label="Barnes-Hut Theta"
|
|
||||||
fieldContainerClassName="test_fa2_barnes_theta"
|
|
||||||
>
|
|
||||||
<NumericInput
|
|
||||||
:value="modelValue.barnesHutTheta"
|
|
||||||
@update="update('barnesHutTheta', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
label="Strong gravity mode"
|
|
||||||
fieldContainerClassName="test_fa2_strong_gravity"
|
|
||||||
>
|
|
||||||
<RadioBlocks
|
|
||||||
:options="booleanOptions"
|
|
||||||
:activeOption="modelValue.strongGravityMode"
|
|
||||||
@option-change="update('strongGravityMode', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
label="Noack's LinLog model"
|
|
||||||
fieldContainerClassName="test_fa2_lin_log"
|
|
||||||
>
|
|
||||||
<RadioBlocks
|
|
||||||
:options="booleanOptions"
|
|
||||||
:activeOption="modelValue.linLogMode"
|
|
||||||
@option-change="update('linLogMode', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
label="Outbound attraction distribution"
|
|
||||||
fieldContainerClassName="test_fa2_outbound_attraction"
|
|
||||||
>
|
|
||||||
<RadioBlocks
|
|
||||||
:options="booleanOptions"
|
|
||||||
:activeOption="modelValue.outboundAttractionDistribution"
|
|
||||||
@option-change="update('outboundAttractionDistribution', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Slow down" fieldContainerClassName="test_fa2_slow_down">
|
|
||||||
<NumericInput
|
|
||||||
:value="modelValue.slowDown"
|
|
||||||
:min="0"
|
|
||||||
@update="update('slowDown', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Edge weight">
|
|
||||||
<Dropdown
|
|
||||||
:options="keyOptions"
|
|
||||||
:value="modelValue.weightSource"
|
|
||||||
className="test_fa2_weight_source"
|
|
||||||
@change="update('weightSource', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field
|
|
||||||
v-show="modelValue.weightSource"
|
|
||||||
label="Edge weight influence"
|
|
||||||
fieldContainerClassName="test_fa2_weight_influence"
|
|
||||||
>
|
|
||||||
<NumericInput
|
|
||||||
:value="modelValue.edgeWeightInfluence"
|
|
||||||
@update="update('edgeWeightInfluence', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { markRaw } from 'vue'
|
|
||||||
import { applyPureReactInVue } from 'veaury'
|
|
||||||
import Field from 'react-chart-editor/lib/components/fields/Field'
|
|
||||||
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
|
||||||
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
|
|
||||||
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
|
|
||||||
import 'react-chart-editor/lib/react-chart-editor.css'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Field: applyPureReactInVue(Field),
|
|
||||||
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
|
||||||
Dropdown: applyPureReactInVue(Dropdown),
|
|
||||||
NumericInput: applyPureReactInVue(NumericInput)
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
modelValue: Object,
|
|
||||||
keyOptions: Array
|
|
||||||
},
|
|
||||||
emits: ['update:modelValue'],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
booleanOptions: markRaw([
|
|
||||||
{ label: 'Yes', value: true },
|
|
||||||
{ label: 'No', value: false }
|
|
||||||
])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
update(attributeName, value) {
|
|
||||||
this.$emit('update:modelValue', {
|
|
||||||
...this.modelValue,
|
|
||||||
[attributeName]: value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
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'
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
Field: applyPureReactInVue(Field),
|
Field: applyPureReactInVue(Field),
|
||||||
NumericInput: applyPureReactInVue(NumericInput),
|
NumericInput: applyPureReactInVue(NumericInput),
|
||||||
|
Dropdown: applyPureReactInVue(Dropdown),
|
||||||
Multiselect
|
Multiselect
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Field label="Color" fieldContainerClassName="test_edge_color">
|
|
||||||
<RadioBlocks
|
|
||||||
:options="edgeColorTypeOptions"
|
|
||||||
:activeOption="modelValue.type"
|
|
||||||
@option-change="updateColorType"
|
|
||||||
/>
|
|
||||||
<Field
|
|
||||||
v-if="modelValue.type === 'constant'"
|
|
||||||
fieldContainerClassName="test_edge_color_value"
|
|
||||||
>
|
|
||||||
<ColorPicker
|
|
||||||
:selectedColor="modelValue.value"
|
|
||||||
@color-change="updateSettings('value', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<template v-else>
|
|
||||||
<Field fieldContainerClassName="test_edge_color_value">
|
|
||||||
<Dropdown
|
|
||||||
v-if="modelValue.type === 'variable'"
|
|
||||||
:options="keyOptions"
|
|
||||||
:value="modelValue.source"
|
|
||||||
@change="updateSettings('source', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field fieldContainerClassName="test_edge_color_mapping_mode">
|
|
||||||
<RadioBlocks
|
|
||||||
:options="colorSourceUsageOptions"
|
|
||||||
:activeOption="modelValue.sourceUsage"
|
|
||||||
@option-change="updateSettings('sourceUsage', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field v-if="modelValue.sourceUsage === 'map_to'">
|
|
||||||
<ColorscalePicker
|
|
||||||
:selected="modelValue.colorscale"
|
|
||||||
className="colorscale-picker"
|
|
||||||
@colorscale-change="updateSettings('colorscale', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</template>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
|
|
||||||
label="Color as"
|
|
||||||
fieldContainerClassName="test_edge_color_as"
|
|
||||||
>
|
|
||||||
<RadioBlocks
|
|
||||||
:options="сolorAsOptions"
|
|
||||||
:activeOption="modelValue.mode"
|
|
||||||
@option-change="updateSettings('mode', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
|
|
||||||
label="Colorscale direction"
|
|
||||||
fieldContainerClassName="test_edge_color_colorscale_direction"
|
|
||||||
>
|
|
||||||
<RadioBlocks
|
|
||||||
:options="сolorscaleDirections"
|
|
||||||
:activeOption="modelValue.colorscaleDirection"
|
|
||||||
@option-change="updateSettings('colorscaleDirection', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { markRaw } from 'vue'
|
|
||||||
import { applyPureReactInVue } from 'veaury'
|
|
||||||
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
|
|
||||||
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
|
||||||
import ColorscalePicker from 'react-chart-editor/lib/components/widgets/ColorscalePicker'
|
|
||||||
import ColorPicker from 'react-chart-editor/lib/components/widgets/ColorPicker'
|
|
||||||
import Field from 'react-chart-editor/lib/components/fields/Field'
|
|
||||||
import 'react-chart-editor/lib/react-chart-editor.css'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Dropdown: applyPureReactInVue(Dropdown),
|
|
||||||
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
|
||||||
Field: applyPureReactInVue(Field),
|
|
||||||
ColorscalePicker: applyPureReactInVue(ColorscalePicker),
|
|
||||||
ColorPicker: applyPureReactInVue(ColorPicker)
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
modelValue: Object,
|
|
||||||
keyOptions: Array
|
|
||||||
},
|
|
||||||
emits: ['update:modelValue'],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
edgeColorTypeOptions: markRaw([
|
|
||||||
{ label: 'Constant', value: 'constant' },
|
|
||||||
{ label: 'Variable', value: 'variable' }
|
|
||||||
]),
|
|
||||||
сolorAsOptions: markRaw([
|
|
||||||
{ label: 'Continious', value: 'continious' },
|
|
||||||
{ label: 'Categorical', value: 'categorical' }
|
|
||||||
]),
|
|
||||||
сolorscaleDirections: markRaw([
|
|
||||||
{ label: 'Normal', value: 'normal' },
|
|
||||||
{ label: 'Recersed', value: 'reversed' }
|
|
||||||
]),
|
|
||||||
colorSourceUsageOptions: markRaw([
|
|
||||||
{ label: 'Direct', value: 'direct' },
|
|
||||||
{ label: 'Map to', value: 'map_to' }
|
|
||||||
]),
|
|
||||||
defaultColorSettings: {
|
|
||||||
constant: { value: '#1F77B4' },
|
|
||||||
variable: {
|
|
||||||
source: null,
|
|
||||||
sourceUsage: 'map_to',
|
|
||||||
colorscale: null,
|
|
||||||
mode: 'categorical',
|
|
||||||
colorscaleDirection: 'normal'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
updateColorType(newColorType) {
|
|
||||||
const currentColorType = this.modelValue.type
|
|
||||||
this.defaultColorSettings[currentColorType] = this.modelValue
|
|
||||||
|
|
||||||
this.$emit('update:modelValue', {
|
|
||||||
type: newColorType,
|
|
||||||
...this.defaultColorSettings[newColorType]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updateSettings(attributeName, value) {
|
|
||||||
this.$emit('update:modelValue', {
|
|
||||||
...this.modelValue,
|
|
||||||
[attributeName]: value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
:deep(.customPickerContainer) {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Field label="Size" fieldContainerClassName="test_edge_size">
|
|
||||||
<RadioBlocks
|
|
||||||
:options="edgeSizeTypeOptions"
|
|
||||||
:activeOption="modelValue.type"
|
|
||||||
@option-change="updateSizeType"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field fieldContainerClassName="test_edge_size_value">
|
|
||||||
<NumericInput
|
|
||||||
v-if="modelValue.type === 'constant'"
|
|
||||||
:value="modelValue.value"
|
|
||||||
:min="1"
|
|
||||||
@update="updateSettings('value', $event)"
|
|
||||||
/>
|
|
||||||
<Dropdown
|
|
||||||
v-if="modelValue.type === 'variable'"
|
|
||||||
:options="keyOptions"
|
|
||||||
:value="modelValue.source"
|
|
||||||
@change="updateSettings('source', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<template v-if="modelValue.type !== 'constant'">
|
|
||||||
<Field label="Size scale" fieldContainerClassName="test_edge_size_scale">
|
|
||||||
<NumericInput
|
|
||||||
:value="modelValue.scale"
|
|
||||||
@update="updateSettings('scale', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Minimum size" fieldContainerClassName="test_edge_size_min">
|
|
||||||
<NumericInput
|
|
||||||
:value="modelValue.min"
|
|
||||||
@update="updateSettings('min', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { markRaw } from 'vue'
|
|
||||||
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 RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
|
||||||
import Field from 'react-chart-editor/lib/components/fields/Field'
|
|
||||||
import 'react-chart-editor/lib/react-chart-editor.css'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Dropdown: applyPureReactInVue(Dropdown),
|
|
||||||
NumericInput: applyPureReactInVue(NumericInput),
|
|
||||||
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
|
||||||
Field: applyPureReactInVue(Field)
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
modelValue: Object,
|
|
||||||
keyOptions: Array
|
|
||||||
},
|
|
||||||
emits: ['update:modelValue'],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
edgeSizeTypeOptions: markRaw([
|
|
||||||
{ label: 'Constant', value: 'constant' },
|
|
||||||
{ label: 'Variable', value: 'variable' }
|
|
||||||
]),
|
|
||||||
defaultSizeSettings: {
|
|
||||||
constant: { value: 4 },
|
|
||||||
variable: { source: null, scale: 1, min: 1 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateSizeType(newSizeType) {
|
|
||||||
const currentSizeType = this.modelValue.type
|
|
||||||
this.defaultSizeSettings[currentSizeType] = this.modelValue
|
|
||||||
|
|
||||||
this.$emit('update:modelValue', {
|
|
||||||
type: newSizeType,
|
|
||||||
...this.defaultSizeSettings[newSizeType]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updateSettings(attributeName, value) {
|
|
||||||
this.$emit('update:modelValue', {
|
|
||||||
...this.modelValue,
|
|
||||||
[attributeName]: value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,32 +1,103 @@
|
|||||||
<template>
|
<template>
|
||||||
<Field
|
<Field label="Adjust sizes">
|
||||||
label="Initial iterations"
|
<RadioBlocks
|
||||||
fieldContainerClassName="test_fa2_iteration_amount"
|
:options="booleanOptions"
|
||||||
>
|
:activeOption="modelValue.adjustSizes"
|
||||||
<NumericInput
|
@option-change="update('adjustSizes', $event)"
|
||||||
:value="modelValue.initialIterationsAmount"
|
|
||||||
:min="1"
|
|
||||||
@update="update('initialIterationsAmount', $event)"
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Gravity" fieldContainerClassName="test_fa2_gravity">
|
<Field label="Barnes-Hut optimize">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="booleanOptions"
|
||||||
|
:activeOption="modelValue.barnesHutOptimize"
|
||||||
|
@option-change="update('barnesHutOptimize', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field v-show="modelValue.barnesHutOptimize" label="Barnes-Hut Theta">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.barnesHutTheta"
|
||||||
|
@update="update('barnesHutTheta', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Gravity">
|
||||||
<NumericInput
|
<NumericInput
|
||||||
:value="modelValue.gravity"
|
:value="modelValue.gravity"
|
||||||
@update="update('gravity', $event)"
|
@update="update('gravity', $event)"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Strong gravity mode">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="booleanOptions"
|
||||||
|
:activeOption="modelValue.strongGravityMode"
|
||||||
|
@option-change="update('strongGravityMode', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Noack's LinLog model">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="booleanOptions"
|
||||||
|
:activeOption="modelValue.linLogMode"
|
||||||
|
@option-change="update('linLogMode', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Out bound attraction distribution">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="booleanOptions"
|
||||||
|
:activeOption="modelValue.outboundAttractionDistribution"
|
||||||
|
@option-change="update('outboundAttractionDistribution', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Scaling ratio">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.scalingRatio"
|
||||||
|
@update="update('scalingRatio', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Slow down">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.slowDown"
|
||||||
|
:min="1"
|
||||||
|
@update="update('slowDown', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Edge weight influence">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.edgeWeightInfluence"
|
||||||
|
@update="update('edgeWeightInfluence', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Edge weight">
|
||||||
|
<Dropdown
|
||||||
|
:options="keyOptions"
|
||||||
|
:value="modelValue.weightSource"
|
||||||
|
@change="update('weightSource', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { markRaw } from 'vue'
|
||||||
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 RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
||||||
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 'react-chart-editor/lib/react-chart-editor.css'
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Field: applyPureReactInVue(Field),
|
Field: applyPureReactInVue(Field),
|
||||||
|
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
||||||
|
Dropdown: applyPureReactInVue(Dropdown),
|
||||||
NumericInput: applyPureReactInVue(NumericInput)
|
NumericInput: applyPureReactInVue(NumericInput)
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
@@ -34,6 +105,14 @@ export default {
|
|||||||
keyOptions: Array
|
keyOptions: Array
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
booleanOptions: markRaw([
|
||||||
|
{ label: 'Yes', value: true },
|
||||||
|
{ label: 'No', value: false }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
update(attributeName, value) {
|
update(attributeName, value) {
|
||||||
this.$emit('update:modelValue', {
|
this.$emit('update:modelValue', {
|
||||||
|
|||||||
@@ -1,812 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div :class="['plotly_editor', { with_controls: showViewSettings }]">
|
|
||||||
<GraphEditorControls v-show="showViewSettings">
|
|
||||||
<PanelMenuWrapper>
|
|
||||||
<Panel group="Structure" name="Graph">
|
|
||||||
<Fold name="Graph">
|
|
||||||
<Field>
|
|
||||||
Map your result set records to node and edge properties required
|
|
||||||
to build a graph. Learn more about result set requirements in the
|
|
||||||
<a href="https://sqliteviz.com/docs/graph/" target="_blank">
|
|
||||||
documentation</a
|
|
||||||
>.
|
|
||||||
</Field>
|
|
||||||
<Field ref="objectTypeField" label="Object type">
|
|
||||||
<Dropdown
|
|
||||||
:options="keysOptions"
|
|
||||||
:value="settings.structure.objectType"
|
|
||||||
className="test_object_type_select"
|
|
||||||
@change="updateStructure('objectType', $event)"
|
|
||||||
/>
|
|
||||||
<Field>
|
|
||||||
A field indicating if the record is node (value 0) or edge
|
|
||||||
(value 1).
|
|
||||||
</Field>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Node Id">
|
|
||||||
<Dropdown
|
|
||||||
:options="keysOptions"
|
|
||||||
:value="settings.structure.nodeId"
|
|
||||||
className="test_node_id_select"
|
|
||||||
@change="updateStructure('nodeId', $event)"
|
|
||||||
/>
|
|
||||||
<Field> A field keeping unique node identifier. </Field>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Edge source">
|
|
||||||
<Dropdown
|
|
||||||
:options="keysOptions"
|
|
||||||
:value="settings.structure.edgeSource"
|
|
||||||
className="test_edge_source_select"
|
|
||||||
@change="updateStructure('edgeSource', $event)"
|
|
||||||
/>
|
|
||||||
<Field>
|
|
||||||
A field keeping a node identifier where the edge starts.
|
|
||||||
</Field>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Edge target">
|
|
||||||
<Dropdown
|
|
||||||
:options="keysOptions"
|
|
||||||
:value="settings.structure.edgeTarget"
|
|
||||||
className="test_edge_target_select"
|
|
||||||
@change="updateStructure('edgeTarget', $event)"
|
|
||||||
/>
|
|
||||||
<Field>
|
|
||||||
A field keeping a node identifier where the edge ends.
|
|
||||||
</Field>
|
|
||||||
</Field>
|
|
||||||
</Fold>
|
|
||||||
</Panel>
|
|
||||||
<Panel group="Style" name="General">
|
|
||||||
<Fold name="General">
|
|
||||||
<Field label="Background color">
|
|
||||||
<ColorPicker
|
|
||||||
:selectedColor="settings.style.backgroundColor"
|
|
||||||
@color-change="settings.style.backgroundColor = $event"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Highlight mode">
|
|
||||||
<Dropdown
|
|
||||||
:options="highlightModeOptions"
|
|
||||||
:value="settings.style.highlightMode"
|
|
||||||
className="test_highlight_mode_select"
|
|
||||||
@change="updateHighlightNodeMode"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Fold>
|
|
||||||
</Panel>
|
|
||||||
<Panel group="Style" name="Nodes">
|
|
||||||
<Fold name="Nodes">
|
|
||||||
<Field label="Label">
|
|
||||||
<Dropdown
|
|
||||||
:options="keysOptions"
|
|
||||||
:value="settings.style.nodes.label.source"
|
|
||||||
className="test_label_select"
|
|
||||||
@change="updateNodes('label.source', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Label color">
|
|
||||||
<ColorPicker
|
|
||||||
:selectedColor="settings.style.nodes.label.color"
|
|
||||||
@color-change="updateNodes('label.color', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<NodeSizeSettings
|
|
||||||
v-model="settings.style.nodes.size"
|
|
||||||
:keyOptions="keysOptions"
|
|
||||||
@update:model-value="updateNodes('size', $event)"
|
|
||||||
/>
|
|
||||||
<NodeColorSettings
|
|
||||||
v-model="settings.style.nodes.color"
|
|
||||||
:keyOptions="keysOptions"
|
|
||||||
@update:model-value="updateNodes('color', $event)"
|
|
||||||
/>
|
|
||||||
</Fold>
|
|
||||||
</Panel>
|
|
||||||
|
|
||||||
<Panel group="Style" name="Edges">
|
|
||||||
<Fold name="Edges">
|
|
||||||
<Field
|
|
||||||
label="Direction"
|
|
||||||
fieldContainerClassName="test_edge_direction"
|
|
||||||
>
|
|
||||||
<RadioBlocks
|
|
||||||
:options="visibilityOptions"
|
|
||||||
:activeOption="settings.style.edges.showDirection"
|
|
||||||
@option-change="updateEdges('showDirection', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Label">
|
|
||||||
<Dropdown
|
|
||||||
:options="keysOptions"
|
|
||||||
:value="settings.style.edges.label.source"
|
|
||||||
className="test_edge_label_select"
|
|
||||||
@change="updateEdges('label.source', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Label color">
|
|
||||||
<ColorPicker
|
|
||||||
:selectedColor="settings.style.edges.label.color"
|
|
||||||
@color-change="updateEdges('label.color', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<EdgeSizeSettings
|
|
||||||
v-model="settings.style.edges.size"
|
|
||||||
:keyOptions="keysOptions"
|
|
||||||
@update:model-value="updateEdges('size', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EdgeColorSettings
|
|
||||||
v-model="settings.style.edges.color"
|
|
||||||
:keyOptions="keysOptions"
|
|
||||||
@update:model-value="updateEdges('color', $event)"
|
|
||||||
/>
|
|
||||||
</Fold>
|
|
||||||
</Panel>
|
|
||||||
<Panel group="Style" name="Layout">
|
|
||||||
<Fold name="Layout">
|
|
||||||
<Field label="Algorithm">
|
|
||||||
<Dropdown
|
|
||||||
:options="layoutOptions"
|
|
||||||
:value="settings.layout.type"
|
|
||||||
:clearable="false"
|
|
||||||
className="test_layout_algorithm_select"
|
|
||||||
@change="updateLayout($event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<component
|
|
||||||
:is="layoutSettingsComponentMap[settings.layout.type]"
|
|
||||||
v-if="settings.layout.type !== 'circular'"
|
|
||||||
v-model="settings.layout.options"
|
|
||||||
:keyOptions="keysOptions"
|
|
||||||
@update:model-value="updateLayout(settings.layout.type)"
|
|
||||||
/>
|
|
||||||
</Fold>
|
|
||||||
<template v-if="settings.layout.type === 'forceAtlas2'">
|
|
||||||
<Fold name="Advanced layout settings">
|
|
||||||
<AdvancedForceAtlasLayoutSettings
|
|
||||||
v-model="settings.layout.options"
|
|
||||||
:keyOptions="keysOptions"
|
|
||||||
@update:model-value="updateLayout(settings.layout.type)"
|
|
||||||
/>
|
|
||||||
</Fold>
|
|
||||||
<div class="force-atlas-buttons">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
class="test_fa2_reset"
|
|
||||||
@click="resetFA2LayoutSettings"
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
class="test_fa2_toggle"
|
|
||||||
@click="toggleFA2Layout"
|
|
||||||
>
|
|
||||||
<template #node:icon>
|
|
||||||
<div
|
|
||||||
:style="{
|
|
||||||
padding: '0 3px'
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<RunIcon v-if="!fa2Running" />
|
|
||||||
<StopIcon v-else />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
{{ fa2Running ? 'Stop' : 'Start' }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Panel>
|
|
||||||
</PanelMenuWrapper>
|
|
||||||
</GraphEditorControls>
|
|
||||||
|
|
||||||
<div
|
|
||||||
ref="graph"
|
|
||||||
class="test_graph_output"
|
|
||||||
:style="{
|
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
backgroundColor: settings.style.backgroundColor
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { markRaw } from 'vue'
|
|
||||||
import { applyPureReactInVue } from 'veaury'
|
|
||||||
import GraphEditorControls from '@/lib/GraphEditorControls.jsx'
|
|
||||||
import { PanelMenuWrapper, Panel, Fold, Section } from 'react-chart-editor'
|
|
||||||
import 'react-chart-editor/lib/react-chart-editor.css'
|
|
||||||
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
|
|
||||||
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
|
||||||
import ColorPicker from 'react-chart-editor/lib/components/widgets/ColorPicker'
|
|
||||||
import Button from 'react-chart-editor/lib/components/widgets/Button'
|
|
||||||
import Field from 'react-chart-editor/lib/components/fields/Field'
|
|
||||||
import RandomLayoutSettings from '@/components/Graph/RandomLayoutSettings.vue'
|
|
||||||
import ForceAtlasLayoutSettings from '@/components/Graph/ForceAtlasLayoutSettings.vue'
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
import AdvancedForceAtlasLayoutSettings from '@/components/Graph/AdvancedForceAtlasLayoutSettings.vue'
|
|
||||||
import CirclePackLayoutSettings from '@/components/Graph/CirclePackLayoutSettings.vue'
|
|
||||||
import FA2Layout from 'graphology-layout-forceatlas2/worker'
|
|
||||||
import * as forceAtlas2 from 'graphology-layout-forceatlas2'
|
|
||||||
import RunIcon from '@/components/svg/run.vue'
|
|
||||||
import StopIcon from '@/components/svg/stop.vue'
|
|
||||||
import { downloadAsPNG, drawOnCanvas } from '@sigma/export-image'
|
|
||||||
import {
|
|
||||||
buildNodes,
|
|
||||||
buildEdges,
|
|
||||||
updateNodes,
|
|
||||||
updateEdges,
|
|
||||||
reduceNodes,
|
|
||||||
reduceEdges
|
|
||||||
} from '@/lib/graphHelper'
|
|
||||||
import Graph from 'graphology'
|
|
||||||
import { circular, random, circlepack } from 'graphology-layout'
|
|
||||||
import Sigma from 'sigma'
|
|
||||||
import seedrandom from 'seedrandom'
|
|
||||||
import NodeColorSettings from '@/components/Graph/NodeColorSettings.vue'
|
|
||||||
import NodeSizeSettings from '@/components/Graph/NodeSizeSettings.vue'
|
|
||||||
import EdgeSizeSettings from '@/components/Graph/EdgeSizeSettings.vue'
|
|
||||||
import EdgeColorSettings from '@/components/Graph/EdgeColorSettings.vue'
|
|
||||||
import events from '@/lib/utils/events'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
GraphEditorControls: applyPureReactInVue(GraphEditorControls),
|
|
||||||
PanelMenuWrapper: applyPureReactInVue(PanelMenuWrapper),
|
|
||||||
Panel: applyPureReactInVue(Panel),
|
|
||||||
PanelSection: applyPureReactInVue(Section),
|
|
||||||
Dropdown: applyPureReactInVue(Dropdown),
|
|
||||||
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
|
||||||
Field: applyPureReactInVue(Field),
|
|
||||||
Fold: applyPureReactInVue(Fold),
|
|
||||||
Button: applyPureReactInVue(Button),
|
|
||||||
ColorPicker: applyPureReactInVue(ColorPicker),
|
|
||||||
RunIcon,
|
|
||||||
StopIcon,
|
|
||||||
RandomLayoutSettings,
|
|
||||||
CirclePackLayoutSettings,
|
|
||||||
NodeColorSettings,
|
|
||||||
NodeSizeSettings,
|
|
||||||
EdgeSizeSettings,
|
|
||||||
EdgeColorSettings,
|
|
||||||
AdvancedForceAtlasLayoutSettings
|
|
||||||
},
|
|
||||||
inject: ['tabLayout'],
|
|
||||||
props: {
|
|
||||||
dataSources: Object,
|
|
||||||
initOptions: Object,
|
|
||||||
showViewSettings: Boolean
|
|
||||||
},
|
|
||||||
emits: ['update', 'selectItem', 'clearSelection'],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
graph: new Graph({ multi: true, allowSelfLoops: true }),
|
|
||||||
renderer: null,
|
|
||||||
fa2Layout: null,
|
|
||||||
fa2Running: false,
|
|
||||||
checkIteration: null,
|
|
||||||
visibilityOptions: markRaw([
|
|
||||||
{ label: 'Show', value: true },
|
|
||||||
{ label: 'Hide', value: false }
|
|
||||||
]),
|
|
||||||
layoutOptions: markRaw([
|
|
||||||
{ label: 'Circular', value: 'circular' },
|
|
||||||
{ label: 'Random', value: 'random' },
|
|
||||||
{ label: 'Circle pack', value: 'circlepack' },
|
|
||||||
{ label: 'ForceAtlas2', value: 'forceAtlas2' }
|
|
||||||
]),
|
|
||||||
layoutSettingsComponentMap: markRaw({
|
|
||||||
random: RandomLayoutSettings,
|
|
||||||
circlepack: CirclePackLayoutSettings,
|
|
||||||
forceAtlas2: ForceAtlasLayoutSettings
|
|
||||||
}),
|
|
||||||
selectedNodeId: undefined,
|
|
||||||
hoveredNodeId: undefined,
|
|
||||||
selectedEdgeId: undefined,
|
|
||||||
hoveredEdgeId: undefined,
|
|
||||||
settings: this.initOptions
|
|
||||||
? JSON.parse(JSON.stringify(this.initOptions))
|
|
||||||
: {
|
|
||||||
structure: {
|
|
||||||
nodeId: null,
|
|
||||||
objectType: null,
|
|
||||||
edgeSource: null,
|
|
||||||
edgeTarget: null
|
|
||||||
},
|
|
||||||
style: {
|
|
||||||
backgroundColor: 'white',
|
|
||||||
highlightMode: 'node_and_neighbors',
|
|
||||||
nodes: {
|
|
||||||
size: {
|
|
||||||
type: 'constant',
|
|
||||||
value: 10
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: 'constant',
|
|
||||||
value: '#1F77B4',
|
|
||||||
opacity: 100
|
|
||||||
},
|
|
||||||
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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
layoutOptionsArchive: {
|
|
||||||
random: null,
|
|
||||||
circlepack: 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: {
|
|
||||||
records() {
|
|
||||||
if (!this.dataSources) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const firstColumnName = Object.keys(this.dataSources)[0]
|
|
||||||
try {
|
|
||||||
return (
|
|
||||||
this.dataSources[firstColumnName].map(json => JSON.parse(json)) || []
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
keysOptions() {
|
|
||||||
if (!this.dataSources) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
const keySet = this.records.reduce((result, currentRecord) => {
|
|
||||||
Object.keys(currentRecord).forEach(key => result.add(key))
|
|
||||||
return result
|
|
||||||
}, new Set())
|
|
||||||
|
|
||||||
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: {
|
|
||||||
dataSources() {
|
|
||||||
if (this.dataSources) {
|
|
||||||
this.buildGraph()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
deep: true,
|
|
||||||
handler() {
|
|
||||||
this.$emit('update')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'settings.structure': {
|
|
||||||
deep: true,
|
|
||||||
handler() {
|
|
||||||
this.buildGraph()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'settings.layout.type': {
|
|
||||||
immediate: true,
|
|
||||||
handler() {
|
|
||||||
events.send('viz_graph.render', null, {
|
|
||||||
layout: this.settings.layout.type
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tabLayout: {
|
|
||||||
deep: true,
|
|
||||||
handler() {
|
|
||||||
if (this.tabLayout.dataView !== 'hidden' && this.renderer) {
|
|
||||||
this.renderer.scheduleRender()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (this.dataSources) {
|
|
||||||
this.buildGraph()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
buildGraph() {
|
|
||||||
this.clearSelection()
|
|
||||||
if (this.renderer) {
|
|
||||||
this.renderer.kill()
|
|
||||||
}
|
|
||||||
this.graph.clear()
|
|
||||||
|
|
||||||
buildNodes(this.graph, this.dataSources, this.settings)
|
|
||||||
buildEdges(this.graph, this.dataSources, this.settings)
|
|
||||||
|
|
||||||
// Apply visual settings
|
|
||||||
updateNodes(this.graph, this.settings.style.nodes)
|
|
||||||
updateEdges(this.graph, this.settings.style.edges)
|
|
||||||
|
|
||||||
this.updateLayout(this.settings.layout.type)
|
|
||||||
this.renderer = new Sigma(this.graph, this.$refs.graph, {
|
|
||||||
renderEdgeLabels: true,
|
|
||||||
allowInvalidContainer: true,
|
|
||||||
labelColor: { attribute: 'labelColor', color: '#444444' },
|
|
||||||
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') {
|
|
||||||
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) {
|
|
||||||
this.settings.structure[attributeName] = value
|
|
||||||
},
|
|
||||||
updateNodes(attributeName, value) {
|
|
||||||
const attributePath = attributeName.split('.')
|
|
||||||
attributePath.reduce((result, current, index) => {
|
|
||||||
if (index === attributePath.length - 1) {
|
|
||||||
return (result[current] = value)
|
|
||||||
} else {
|
|
||||||
return result[current]
|
|
||||||
}
|
|
||||||
}, this.settings.style.nodes)
|
|
||||||
|
|
||||||
updateNodes(this.graph, {
|
|
||||||
[attributePath[0]]: this.settings.style.nodes[attributePath[0]]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updateEdges(attributeName, value) {
|
|
||||||
const attributePath = attributeName.split('.')
|
|
||||||
attributePath.reduce((result, current, index) => {
|
|
||||||
if (index === attributePath.length - 1) {
|
|
||||||
return (result[current] = value)
|
|
||||||
} else {
|
|
||||||
return result[current]
|
|
||||||
}
|
|
||||||
}, this.settings.style.edges)
|
|
||||||
|
|
||||||
updateEdges(this.graph, {
|
|
||||||
[attributePath[0]]: this.settings.style.edges[attributePath[0]]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updateLayout(layoutType) {
|
|
||||||
const prevLayout = this.settings.layout.type
|
|
||||||
|
|
||||||
// Change layout type? - restore layout settings or set default settings
|
|
||||||
if (layoutType !== prevLayout) {
|
|
||||||
this.layoutOptionsArchive[prevLayout] = this.settings.layout.options
|
|
||||||
this.settings.layout.options = this.layoutOptionsArchive[layoutType]
|
|
||||||
|
|
||||||
if (!this.settings.layout.options) {
|
|
||||||
if (layoutType === 'forceAtlas2') {
|
|
||||||
this.setRecommendedFA2Settings()
|
|
||||||
} else if (['random', 'circlepack'].includes(layoutType)) {
|
|
||||||
this.settings.layout.options = {
|
|
||||||
seedValue: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.settings.layout.type = layoutType
|
|
||||||
}
|
|
||||||
|
|
||||||
// In any case kill FA2 if it exists
|
|
||||||
if (this.fa2Layout) {
|
|
||||||
if (this.fa2Layout.isRunning()) {
|
|
||||||
this.stopFA2Layout()
|
|
||||||
}
|
|
||||||
this.fa2Layout.kill()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layoutType === 'circular') {
|
|
||||||
circular.assign(this.graph)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layoutType === 'random') {
|
|
||||||
random.assign(this.graph, {
|
|
||||||
rng: seedrandom(this.settings.layout.options.seedValue)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layoutType === 'circlepack') {
|
|
||||||
this.graph.forEachNode(nodeId => {
|
|
||||||
this.graph.updateNode(nodeId, attributes => {
|
|
||||||
const newAttributes = { ...attributes }
|
|
||||||
// Delete old hierarchy attributes
|
|
||||||
Object.keys(newAttributes)
|
|
||||||
.filter(key => key.startsWith('hierarchyAttribute'))
|
|
||||||
.forEach(
|
|
||||||
hierarchyAttributeKey =>
|
|
||||||
delete newAttributes[hierarchyAttributeKey]
|
|
||||||
)
|
|
||||||
// Set new hierarchy attributes
|
|
||||||
this.settings.layout.options.hierarchyAttributes?.forEach(
|
|
||||||
(hierarchyAttribute, index) => {
|
|
||||||
newAttributes['hierarchyAttribute' + index] =
|
|
||||||
attributes.data[hierarchyAttribute]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return newAttributes
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
circlepack.assign(this.graph, {
|
|
||||||
hierarchyAttributes:
|
|
||||||
this.settings.layout.options.hierarchyAttributes?.map(
|
|
||||||
(_, index) => 'hierarchyAttribute' + index
|
|
||||||
) || [],
|
|
||||||
rng: seedrandom(this.settings.layout.options.seedValue)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layoutType === 'forceAtlas2') {
|
|
||||||
if (
|
|
||||||
!this.graph.someNode(
|
|
||||||
(nodeKey, attributes) =>
|
|
||||||
typeof attributes.x === 'number' &&
|
|
||||||
typeof attributes.y === 'number'
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
circular.assign(this.graph)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fa2Layout = markRaw(
|
|
||||||
new FA2Layout(this.graph, {
|
|
||||||
getEdgeWeight: (_, attr) =>
|
|
||||||
this.settings.layout.options.weightSource
|
|
||||||
? attr.data[this.settings.layout.options.weightSource]
|
|
||||||
: 1,
|
|
||||||
settings: this.settings.layout.options
|
|
||||||
})
|
|
||||||
)
|
|
||||||
if (layoutType !== prevLayout) {
|
|
||||||
this.autoRunFA2Layout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toggleFA2Layout() {
|
|
||||||
if (this.fa2Layout.isRunning()) {
|
|
||||||
this.stopFA2Layout()
|
|
||||||
} else {
|
|
||||||
this.fa2Running = true
|
|
||||||
this.fa2Layout.start()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
stopFA2Layout() {
|
|
||||||
this.fa2Running = false
|
|
||||||
this.fa2Layout.stop()
|
|
||||||
if (this.checkIteration) {
|
|
||||||
this.fa2Layout.worker.removeEventListener(
|
|
||||||
'message',
|
|
||||||
this.checkIteration
|
|
||||||
)
|
|
||||||
this.checkIteration = null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
autoRunFA2Layout() {
|
|
||||||
let iteration = 1
|
|
||||||
this.checkIteration = () => {
|
|
||||||
if (
|
|
||||||
iteration === this.settings.layout.options.initialIterationsAmount
|
|
||||||
) {
|
|
||||||
this.stopFA2Layout()
|
|
||||||
}
|
|
||||||
iteration++
|
|
||||||
}
|
|
||||||
this.fa2Layout.worker.addEventListener('message', this.checkIteration)
|
|
||||||
this.fa2Running = true
|
|
||||||
this.fa2Layout.start()
|
|
||||||
},
|
|
||||||
setRecommendedFA2Settings() {
|
|
||||||
const sensibleSettings = forceAtlas2.default.inferSettings(this.graph)
|
|
||||||
this.settings.layout.options = {
|
|
||||||
initialIterationsAmount: 50,
|
|
||||||
adjustSizes: false,
|
|
||||||
barnesHutOptimize: false,
|
|
||||||
barnesHutTheta: 0.5,
|
|
||||||
edgeWeightInfluence: 0,
|
|
||||||
gravity: 1,
|
|
||||||
linLogMode: false,
|
|
||||||
outboundAttractionDistribution: false,
|
|
||||||
scalingRatio: 1,
|
|
||||||
slowDown: 1,
|
|
||||||
strongGravityMode: false,
|
|
||||||
...sensibleSettings
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
[Infinity, -Infinity].includes(this.settings.layout.options.slowDown)
|
|
||||||
) {
|
|
||||||
this.settings.layout.options.slowDown = 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resetFA2LayoutSettings() {
|
|
||||||
if (this.initOptions?.layout.type === 'forceAtlas2') {
|
|
||||||
this.settings.layout = JSON.parse(
|
|
||||||
JSON.stringify(this.initOptions.layout)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.setRecommendedFA2Settings()
|
|
||||||
}
|
|
||||||
this.updateLayout(this.settings.layout.type)
|
|
||||||
},
|
|
||||||
saveAsPng() {
|
|
||||||
return downloadAsPNG(this.renderer, {
|
|
||||||
backgroundColor: this.settings.style.backgroundColor
|
|
||||||
})
|
|
||||||
},
|
|
||||||
prepareCopy() {
|
|
||||||
return drawOnCanvas(this.renderer, {
|
|
||||||
backgroundColor: this.settings.style.backgroundColor
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.plotly_editor.with_controls > div {
|
|
||||||
display: flex !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.customPickerContainer) {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
.force-atlas-buttons {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.force-atlas-buttons :deep(button) {
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-basis: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Field label="Color" fieldContainerClassName="test_node_color">
|
|
||||||
<RadioBlocks
|
|
||||||
:options="nodeColorTypeOptions"
|
|
||||||
:activeOption="modelValue.type"
|
|
||||||
@option-change="updateColorType"
|
|
||||||
/>
|
|
||||||
<Field
|
|
||||||
v-if="modelValue.type === 'constant'"
|
|
||||||
fieldContainerClassName="test_node_color_value"
|
|
||||||
>
|
|
||||||
<ColorPicker
|
|
||||||
:selectedColor="modelValue.value"
|
|
||||||
@color-change="updateSettings('value', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<template v-else>
|
|
||||||
<Field fieldContainerClassName="test_node_color_value">
|
|
||||||
<Dropdown
|
|
||||||
v-if="modelValue.type === 'variable'"
|
|
||||||
:options="keyOptions"
|
|
||||||
:value="modelValue.source"
|
|
||||||
@change="updateSettings('source', $event)"
|
|
||||||
/>
|
|
||||||
<Dropdown
|
|
||||||
v-if="modelValue.type === 'calculated'"
|
|
||||||
:options="nodeCalculatedColorMethodOptions"
|
|
||||||
:value="modelValue.method"
|
|
||||||
:clearable="false"
|
|
||||||
@change="updateSettings('method', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
v-if="modelValue.type === 'variable'"
|
|
||||||
fieldContainerClassName="test_node_color_mapping_mode"
|
|
||||||
>
|
|
||||||
<RadioBlocks
|
|
||||||
:options="colorSourceUsageOptions"
|
|
||||||
:activeOption="modelValue.sourceUsage"
|
|
||||||
@option-change="updateSettings('sourceUsage', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
v-if="
|
|
||||||
modelValue.sourceUsage === 'map_to' ||
|
|
||||||
modelValue.type === 'calculated'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<ColorscalePicker
|
|
||||||
:selected="modelValue.colorscale"
|
|
||||||
className="colorscale-picker"
|
|
||||||
@colorscale-change="updateSettings('colorscale', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</template>
|
|
||||||
</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
|
|
||||||
v-if="modelValue.type === 'map_to' || modelValue.type === 'calculated'"
|
|
||||||
label="Color as"
|
|
||||||
fieldContainerClassName="test_node_color_as"
|
|
||||||
>
|
|
||||||
<RadioBlocks
|
|
||||||
:options="сolorAsOptions"
|
|
||||||
:activeOption="modelValue.mode"
|
|
||||||
@option-change="updateSettings('mode', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
v-if="
|
|
||||||
modelValue.sourceUsage === 'map_to' || modelValue.type === 'calculated'
|
|
||||||
"
|
|
||||||
label="Colorscale direction"
|
|
||||||
fieldContainerClassName="test_node_color_colorscale_direction"
|
|
||||||
>
|
|
||||||
<RadioBlocks
|
|
||||||
:options="сolorscaleDirections"
|
|
||||||
:activeOption="modelValue.colorscaleDirection"
|
|
||||||
@option-change="updateSettings('colorscaleDirection', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { markRaw } from 'vue'
|
|
||||||
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 RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
|
||||||
import ColorscalePicker from 'react-chart-editor/lib/components/widgets/ColorscalePicker'
|
|
||||||
import ColorPicker from 'react-chart-editor/lib/components/widgets/ColorPicker'
|
|
||||||
import Field from 'react-chart-editor/lib/components/fields/Field'
|
|
||||||
import 'react-chart-editor/lib/react-chart-editor.css'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
NumericInput: applyPureReactInVue(NumericInput),
|
|
||||||
Dropdown: applyPureReactInVue(Dropdown),
|
|
||||||
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
|
||||||
Field: applyPureReactInVue(Field),
|
|
||||||
ColorscalePicker: applyPureReactInVue(ColorscalePicker),
|
|
||||||
ColorPicker: applyPureReactInVue(ColorPicker)
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
modelValue: Object,
|
|
||||||
keyOptions: Array
|
|
||||||
},
|
|
||||||
emits: ['update:modelValue'],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
nodeColorTypeOptions: markRaw([
|
|
||||||
{ label: 'Constant', value: 'constant' },
|
|
||||||
{ label: 'Variable', value: 'variable' },
|
|
||||||
{ label: 'Calculated', value: 'calculated' }
|
|
||||||
]),
|
|
||||||
nodeCalculatedColorMethodOptions: markRaw([
|
|
||||||
{ label: 'Degree', value: 'degree' },
|
|
||||||
{ label: 'In degree', value: 'inDegree' },
|
|
||||||
{ label: 'Out degree', value: 'outDegree' }
|
|
||||||
]),
|
|
||||||
сolorAsOptions: markRaw([
|
|
||||||
{ label: 'Continious', value: 'continious' },
|
|
||||||
{ label: 'Categorical', value: 'categorical' }
|
|
||||||
]),
|
|
||||||
сolorscaleDirections: markRaw([
|
|
||||||
{ label: 'Normal', value: 'normal' },
|
|
||||||
{ label: 'Recersed', value: 'reversed' }
|
|
||||||
]),
|
|
||||||
colorSourceUsageOptions: markRaw([
|
|
||||||
{ label: 'Direct', value: 'direct' },
|
|
||||||
{ label: 'Map to', value: 'map_to' }
|
|
||||||
]),
|
|
||||||
defaultColorSettings: {
|
|
||||||
constant: { value: '#1F77B4', opacity: 100 },
|
|
||||||
variable: {
|
|
||||||
source: null,
|
|
||||||
sourceUsage: 'map_to',
|
|
||||||
colorscale: null,
|
|
||||||
mode: 'categorical',
|
|
||||||
colorscaleDirection: 'normal',
|
|
||||||
opacity: 100
|
|
||||||
},
|
|
||||||
calculated: {
|
|
||||||
method: 'degree',
|
|
||||||
colorscale: null,
|
|
||||||
mode: 'continious',
|
|
||||||
colorscaleDirection: 'normal',
|
|
||||||
opacity: 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateColorType(newColorType) {
|
|
||||||
const currentColorType = this.modelValue.type
|
|
||||||
this.defaultColorSettings[currentColorType] = this.modelValue
|
|
||||||
|
|
||||||
this.$emit('update:modelValue', {
|
|
||||||
type: newColorType,
|
|
||||||
...this.defaultColorSettings[newColorType]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updateSettings(attributeName, value) {
|
|
||||||
this.$emit('update:modelValue', {
|
|
||||||
...this.modelValue,
|
|
||||||
[attributeName]: value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
:deep(.customPickerContainer) {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Field label="Size" fieldContainerClassName="test_node_size">
|
|
||||||
<RadioBlocks
|
|
||||||
:options="nodeSizeTypeOptions"
|
|
||||||
:activeOption="modelValue.type"
|
|
||||||
@option-change="updateSizeType"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Field fieldContainerClassName="test_node_size_value">
|
|
||||||
<NumericInput
|
|
||||||
v-if="modelValue.type === 'constant'"
|
|
||||||
:value="modelValue.value"
|
|
||||||
:min="1"
|
|
||||||
class="test_node_size_value"
|
|
||||||
@update="updateSettings('value', $event)"
|
|
||||||
/>
|
|
||||||
<Dropdown
|
|
||||||
v-if="modelValue.type === 'variable'"
|
|
||||||
:options="keyOptions"
|
|
||||||
:value="modelValue.source"
|
|
||||||
@change="updateSettings('source', $event)"
|
|
||||||
/>
|
|
||||||
<Dropdown
|
|
||||||
v-if="modelValue.type === 'calculated'"
|
|
||||||
:options="nodeCalculatedSizeMethodOptions"
|
|
||||||
:value="modelValue.method"
|
|
||||||
:clearable="false"
|
|
||||||
@change="updateSettings('method', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<template v-if="modelValue.type !== 'constant'">
|
|
||||||
<Field label="Size scale" fieldContainerClassName="test_node_size_scale">
|
|
||||||
<NumericInput
|
|
||||||
:value="modelValue.scale"
|
|
||||||
@update="updateSettings('scale', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Size mode" fieldContainerClassName="test_node_size_mode">
|
|
||||||
<RadioBlocks
|
|
||||||
:options="nodeSizeModeOptions"
|
|
||||||
:activeOption="modelValue.mode"
|
|
||||||
@option-change="updateSettings('mode', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Minimum size" fieldContainerClassName="test_node_size_min">
|
|
||||||
<NumericInput
|
|
||||||
:value="modelValue.min"
|
|
||||||
@update="updateSettings('min', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { markRaw } from 'vue'
|
|
||||||
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 RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
|
||||||
import Field from 'react-chart-editor/lib/components/fields/Field'
|
|
||||||
import 'react-chart-editor/lib/react-chart-editor.css'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
Dropdown: applyPureReactInVue(Dropdown),
|
|
||||||
NumericInput: applyPureReactInVue(NumericInput),
|
|
||||||
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
|
||||||
Field: applyPureReactInVue(Field)
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
modelValue: Object,
|
|
||||||
keyOptions: Array
|
|
||||||
},
|
|
||||||
emits: ['update:modelValue'],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
nodeSizeTypeOptions: markRaw([
|
|
||||||
{ label: 'Constant', value: 'constant' },
|
|
||||||
{ label: 'Variable', value: 'variable' },
|
|
||||||
{ label: 'Calculated', value: 'calculated' }
|
|
||||||
]),
|
|
||||||
nodeCalculatedSizeMethodOptions: markRaw([
|
|
||||||
{ label: 'Degree', value: 'degree' },
|
|
||||||
{ label: 'In degree', value: 'inDegree' },
|
|
||||||
{ label: 'Out degree', value: 'outDegree' }
|
|
||||||
]),
|
|
||||||
nodeSizeModeOptions: markRaw([
|
|
||||||
{ label: 'Area', value: 'area' },
|
|
||||||
{ label: 'Diameter', value: 'diameter' }
|
|
||||||
]),
|
|
||||||
defaultSizeSettings: {
|
|
||||||
constant: { value: 4 },
|
|
||||||
variable: { source: null, scale: 1, mode: 'diameter', min: 1 },
|
|
||||||
calculated: { method: 'degree', scale: 1, mode: 'diameter', min: 1 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateSizeType(newSizeType) {
|
|
||||||
const currentSizeType = this.modelValue.type
|
|
||||||
this.defaultSizeSettings[currentSizeType] = this.modelValue
|
|
||||||
|
|
||||||
this.$emit('update:modelValue', {
|
|
||||||
type: newSizeType,
|
|
||||||
...this.defaultSizeSettings[newSizeType]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updateSettings(attributeName, value) {
|
|
||||||
this.$emit('update:modelValue', {
|
|
||||||
...this.modelValue,
|
|
||||||
[attributeName]: value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import tooltipMixin from '@/tooltipMixin'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
import LoadingIndicator from '@/components/Common/LoadingIndicator'
|
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SideBarButton',
|
name: 'SideBarButton',
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<modal
|
<modal
|
||||||
v-model="show"
|
:modalId="name"
|
||||||
class="dialog"
|
class="dialog"
|
||||||
:clickToClose="false"
|
:clickToClose="false"
|
||||||
:contentTransition="{ name: 'loading-dialog' }"
|
:contentTransition="{ name: 'loading-dialog' }"
|
||||||
:overlayTransition="{ name: 'loading-dialog' }"
|
:overlayTransition="{ name: 'loading-dialog' }"
|
||||||
@update:model-value="$emit('update:modelValue', $event)"
|
|
||||||
>
|
>
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
<close-icon :disabled="loading" @click="cancel" />
|
<close-icon :disabled="loading" @click="$emit('cancel')" />
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
<div v-if="loading" class="loading-dialog-body">
|
<div v-if="loading" class="loading-dialog-body">
|
||||||
@@ -29,7 +28,7 @@
|
|||||||
class="secondary"
|
class="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@click="cancel"
|
@click="$emit('cancel')"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -46,40 +45,31 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import LoadingIndicator from '@/components/Common/LoadingIndicator'
|
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||||
import CloseIcon from '@/components/svg/close'
|
import CloseIcon from '@/components/svg/close'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'LoadingDialog',
|
name: 'LoadingDialog',
|
||||||
components: { LoadingIndicator, CloseIcon },
|
components: { LoadingIndicator, CloseIcon },
|
||||||
props: {
|
props: {
|
||||||
modelValue: Boolean,
|
|
||||||
loadingMsg: String,
|
loadingMsg: String,
|
||||||
successMsg: String,
|
successMsg: String,
|
||||||
actionBtnName: String,
|
actionBtnName: String,
|
||||||
|
name: String,
|
||||||
title: String,
|
title: String,
|
||||||
loading: Boolean
|
loading: Boolean
|
||||||
},
|
},
|
||||||
emits: ['cancel', 'action', 'update:modelValue'],
|
emits: ['cancel', 'action'],
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
show: this.modelValue
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
watch: {
|
||||||
modelValue() {
|
|
||||||
this.show = this.modelValue
|
|
||||||
},
|
|
||||||
loading() {
|
loading() {
|
||||||
if (this.loading) {
|
if (this.loading) {
|
||||||
this.$emit('update:modelValue', true)
|
this.$modal.show(this.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
cancel() {
|
cancel() {
|
||||||
this.$emit('cancel')
|
this.$emit('cancel')
|
||||||
this.$emit('update:modelValue', false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import LoadingIndicator from '@/components/Common/LoadingIndicator'
|
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Logs',
|
name: 'Logs',
|
||||||
@@ -1,300 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="pivot-ui">
|
|
||||||
<div class="row">
|
|
||||||
<label>Columns</label>
|
|
||||||
<multiselect
|
|
||||||
v-model="cols"
|
|
||||||
class="sqliteviz-select cols"
|
|
||||||
:options="colsToSelect"
|
|
||||||
:disabled="colsToSelect.length === 0"
|
|
||||||
:multiple="true"
|
|
||||||
:hideSelected="true"
|
|
||||||
:closeOnSelect="true"
|
|
||||||
:showLabels="false"
|
|
||||||
:max="colsToSelect.length"
|
|
||||||
openDirection="bottom"
|
|
||||||
placeholder=""
|
|
||||||
>
|
|
||||||
<template #maxElements>
|
|
||||||
<span class="no-results">No Results</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #placeholder>Choose columns</template>
|
|
||||||
|
|
||||||
<template #noResult>
|
|
||||||
<span class="no-results">No Results</span>
|
|
||||||
</template>
|
|
||||||
</multiselect>
|
|
||||||
<pivot-sort-btn v-model="colOrder" class="sort-btn" direction="col" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<label>Rows</label>
|
|
||||||
<multiselect
|
|
||||||
v-model="rows"
|
|
||||||
class="sqliteviz-select rows"
|
|
||||||
:options="rowsToSelect"
|
|
||||||
:disabled="rowsToSelect.length === 0"
|
|
||||||
:multiple="true"
|
|
||||||
:hideSelected="true"
|
|
||||||
:closeOnSelect="true"
|
|
||||||
:showLabels="false"
|
|
||||||
:max="rowsToSelect.length"
|
|
||||||
:optionHeight="29"
|
|
||||||
openDirection="bottom"
|
|
||||||
placeholder=""
|
|
||||||
>
|
|
||||||
<template #maxElements>
|
|
||||||
<span class="no-results">No Results</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #placeholder>Choose rows</template>
|
|
||||||
|
|
||||||
<template #noResult>
|
|
||||||
<span class="no-results">No Results</span>
|
|
||||||
</template>
|
|
||||||
</multiselect>
|
|
||||||
<pivot-sort-btn v-model="rowOrder" class="sort-btn" direction="row" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row aggregator">
|
|
||||||
<label>Aggregator</label>
|
|
||||||
<multiselect
|
|
||||||
v-model="aggregator"
|
|
||||||
class="sqliteviz-select short aggregator"
|
|
||||||
:options="aggregators"
|
|
||||||
label="name"
|
|
||||||
trackBy="name"
|
|
||||||
:closeOnSelect="true"
|
|
||||||
:showLabels="false"
|
|
||||||
:hideSelected="true"
|
|
||||||
:optionHeight="29"
|
|
||||||
openDirection="bottom"
|
|
||||||
placeholder="Choose a function"
|
|
||||||
>
|
|
||||||
<template #noResult>
|
|
||||||
<span class="no-results">No Results</span>
|
|
||||||
</template>
|
|
||||||
</multiselect>
|
|
||||||
|
|
||||||
<multiselect
|
|
||||||
v-show="valCount > 0"
|
|
||||||
v-model="val1"
|
|
||||||
class="sqliteviz-select aggr-arg"
|
|
||||||
:options="keyNames"
|
|
||||||
:disabled="keyNames.length === 0"
|
|
||||||
:closeOnSelect="true"
|
|
||||||
:showLabels="false"
|
|
||||||
:hideSelected="true"
|
|
||||||
:optionHeight="29"
|
|
||||||
openDirection="bottom"
|
|
||||||
placeholder="Choose an argument"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<multiselect
|
|
||||||
v-show="valCount > 1"
|
|
||||||
v-model="val2"
|
|
||||||
class="sqliteviz-select aggr-arg"
|
|
||||||
:options="keyNames"
|
|
||||||
:disabled="keyNames.length === 0"
|
|
||||||
:closeOnSelect="true"
|
|
||||||
:showLabels="false"
|
|
||||||
:hideSelected="true"
|
|
||||||
:optionHeight="29"
|
|
||||||
openDirection="bottom"
|
|
||||||
placeholder="Choose a second argument"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<label>View</label>
|
|
||||||
<multiselect
|
|
||||||
v-model="renderer"
|
|
||||||
class="sqliteviz-select short renderer"
|
|
||||||
:options="renderers"
|
|
||||||
label="name"
|
|
||||||
trackBy="name"
|
|
||||||
:closeOnSelect="true"
|
|
||||||
:allowEmpty="false"
|
|
||||||
:showLabels="false"
|
|
||||||
:hideSelected="true"
|
|
||||||
:optionHeight="29"
|
|
||||||
openDirection="bottom"
|
|
||||||
placeholder="Choose a view"
|
|
||||||
>
|
|
||||||
<template #noResult>
|
|
||||||
<span class="no-results">No Results</span>
|
|
||||||
</template>
|
|
||||||
</multiselect>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import $ from 'jquery'
|
|
||||||
import Multiselect from 'vue-multiselect'
|
|
||||||
import PivotSortBtn from './PivotSortBtn'
|
|
||||||
import {
|
|
||||||
renderers,
|
|
||||||
aggregators,
|
|
||||||
zeroValAggregators,
|
|
||||||
twoValAggregators
|
|
||||||
} from '../pivotHelper'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'PivotUi',
|
|
||||||
components: {
|
|
||||||
Multiselect,
|
|
||||||
PivotSortBtn
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
keyNames: Array,
|
|
||||||
modelValue: Object
|
|
||||||
},
|
|
||||||
emits: ['update:modelValue', 'update'],
|
|
||||||
data() {
|
|
||||||
const aggregatorName =
|
|
||||||
(this.modelValue && this.modelValue.aggregatorName) || 'Count'
|
|
||||||
const rendererName =
|
|
||||||
(this.modelValue && this.modelValue.rendererName) || 'Table'
|
|
||||||
return {
|
|
||||||
renderer: {
|
|
||||||
name: rendererName,
|
|
||||||
fun: $.pivotUtilities.renderers[rendererName]
|
|
||||||
},
|
|
||||||
aggregator: {
|
|
||||||
name: aggregatorName,
|
|
||||||
fun: $.pivotUtilities.aggregators[aggregatorName]
|
|
||||||
},
|
|
||||||
rows: (this.modelValue && this.modelValue.rows) || [],
|
|
||||||
cols: (this.modelValue && this.modelValue.cols) || [],
|
|
||||||
val1:
|
|
||||||
(this.modelValue && this.modelValue.vals && this.modelValue.vals[0]) ||
|
|
||||||
'',
|
|
||||||
val2:
|
|
||||||
(this.modelValue && this.modelValue.vals && this.modelValue.vals[1]) ||
|
|
||||||
'',
|
|
||||||
colOrder: (this.modelValue && this.modelValue.colOrder) || 'key_a_to_z',
|
|
||||||
rowOrder: (this.modelValue && this.modelValue.rowOrder) || 'key_a_to_z'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
valCount() {
|
|
||||||
if (zeroValAggregators.includes(this.aggregator.name)) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if (twoValAggregators.includes(this.aggregator.name)) {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1
|
|
||||||
},
|
|
||||||
renderers() {
|
|
||||||
return renderers
|
|
||||||
},
|
|
||||||
aggregators() {
|
|
||||||
return aggregators
|
|
||||||
},
|
|
||||||
rowsToSelect() {
|
|
||||||
return this.keyNames.filter(key => !this.cols.includes(key))
|
|
||||||
},
|
|
||||||
colsToSelect() {
|
|
||||||
return this.keyNames.filter(key => !this.rows.includes(key))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
renderer() {
|
|
||||||
this.returnValue()
|
|
||||||
},
|
|
||||||
aggregator() {
|
|
||||||
this.returnValue()
|
|
||||||
},
|
|
||||||
rows() {
|
|
||||||
this.returnValue()
|
|
||||||
},
|
|
||||||
cols() {
|
|
||||||
this.returnValue()
|
|
||||||
},
|
|
||||||
val1() {
|
|
||||||
this.returnValue()
|
|
||||||
},
|
|
||||||
val2() {
|
|
||||||
this.returnValue()
|
|
||||||
},
|
|
||||||
colOrder() {
|
|
||||||
this.returnValue()
|
|
||||||
},
|
|
||||||
rowOrder() {
|
|
||||||
this.returnValue()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
returnValue() {
|
|
||||||
const vals = []
|
|
||||||
for (let i = 1; i <= this.valCount; i++) {
|
|
||||||
vals.push(this[`val${i}`])
|
|
||||||
}
|
|
||||||
this.$emit('update')
|
|
||||||
this.$emit('update:modelValue', {
|
|
||||||
rows: this.rows,
|
|
||||||
cols: this.cols,
|
|
||||||
colOrder: this.colOrder,
|
|
||||||
rowOrder: this.rowOrder,
|
|
||||||
aggregator: this.aggregator.fun(vals),
|
|
||||||
aggregatorName: this.aggregator.name,
|
|
||||||
renderer: this.renderer.fun,
|
|
||||||
rendererName: this.renderer.name,
|
|
||||||
vals
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.pivot-ui {
|
|
||||||
padding: 12px 24px;
|
|
||||||
color: var(--color-text-base);
|
|
||||||
font-size: 12px;
|
|
||||||
border-bottom: 1px solid var(--color-border-light);
|
|
||||||
background-color: var(--color-bg-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pivot-ui .row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 12px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pivot-ui .row label {
|
|
||||||
width: 76px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pivot-ui .row .sqliteviz-select.short {
|
|
||||||
width: 220px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pivot-ui .row .aggr-arg {
|
|
||||||
margin-left: 12px;
|
|
||||||
max-width: 220px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pivot-ui .row .sort-btn {
|
|
||||||
margin-left: 12px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switcher {
|
|
||||||
display: block;
|
|
||||||
width: min-content;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin: auto;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.switcher:hover {
|
|
||||||
color: var(--color-accent);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -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,12 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- splitter end -->
|
<!-- splitter end -->
|
||||||
<div
|
<div ref="right" class="splitpanes-pane" :style="styles.after">
|
||||||
v-show="!after.hidden"
|
|
||||||
ref="right"
|
|
||||||
class="splitpanes-pane"
|
|
||||||
:style="styles.after"
|
|
||||||
>
|
|
||||||
<slot name="right-pane" />
|
<slot name="right-pane" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,12 +114,10 @@ export default {
|
|||||||
styles() {
|
styles() {
|
||||||
return {
|
return {
|
||||||
before: {
|
before: {
|
||||||
[this.horizontal ? 'height' : 'width']:
|
[this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%`
|
||||||
`${this.after.hidden ? 100 : this.paneBefore.size}%`
|
|
||||||
},
|
},
|
||||||
after: {
|
after: {
|
||||||
[this.horizontal ? 'height' : 'width']:
|
[this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%`
|
||||||
`${this.before.hidden ? 100 : this.paneAfter.size}%`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Pager from '@/components/Common/Pager.vue'
|
import Pager from './Pager.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SqlTable',
|
name: 'SqlTable',
|
||||||
@@ -7,33 +7,27 @@
|
|||||||
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
|
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"
|
||||||
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
|
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"
|
||||||
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
|
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"
|
||||||
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
|
d="M2.93128 5.31436L3.90527 5.08778L5.48693 11.8867L4.51294 12.1133L2.93128 5.31436Z"
|
||||||
12.1133L2.93128 5.31436Z"
|
|
||||||
fill="#A2B1C6"
|
fill="#A2B1C6"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M12.9447 7.79159L13.5548 8.58392L7.30516 13.3962L6.69507
|
d="M12.9447 7.79159L13.5548 8.58392L7.30516 13.3962L6.69507 12.6038L12.9447 7.79159Z"
|
||||||
12.6038L12.9447 7.79159Z"
|
|
||||||
fill="#A2B1C6"
|
fill="#A2B1C6"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M14.1316 6.51712L3.13166 3.51723L2.86844 4.48202L13.8684
|
d="M14.1316 6.51712L3.13166 3.51723L2.86844 4.48202L13.8684 7.48191L14.1316 6.51712Z"
|
||||||
7.48191L14.1316 6.51712Z"
|
|
||||||
fill="#A2B1C6"
|
fill="#A2B1C6"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 18 18"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<g clip-path="url(#clip0_2236_2707)">
|
|
||||||
<path
|
|
||||||
d="M8.11441 0.0274963C7.73011 0.0274918 7.35916 0.168422 7.0718
|
|
||||||
0.423594C6.78445 0.678766 6.60065 1.03046 6.55522 1.41207L7.89167
|
|
||||||
1.57122C7.89815 1.51669 7.9244 1.46644 7.96546 1.42997C8.00651 1.39351
|
|
||||||
8.05951 1.37337 8.11441 1.37337V0.0274963ZM9.88559
|
|
||||||
0.0274963H8.11441V1.37337H9.88559V0.0274963ZM11.4448
|
|
||||||
1.41207C11.3994 1.03046 11.2156 0.678766 10.9282 0.423594C10.6408
|
|
||||||
0.168422 10.2699 0.0274918 9.88559 0.0274963V1.37337C9.94049 1.37337
|
|
||||||
9.99349 1.39351 10.0345 1.42997C10.0756 1.46644 10.1018 1.51669
|
|
||||||
10.1083 1.57122L11.4448 1.41207ZM11.5815 2.56055L11.4448
|
|
||||||
1.41207L10.1083 1.57122L10.2449 2.7197L11.5815 2.56055ZM14.349
|
|
||||||
3.0888L13.282 3.5464L13.8123 4.78326L14.8793 4.32566L14.349
|
|
||||||
3.0888ZM16.3276 3.74682C16.1355 3.41401 15.8279 3.16322 15.4633
|
|
||||||
3.04195C15.0986 2.92067 14.7021 2.93735 14.349 3.0888L14.8793
|
|
||||||
4.32566C14.9298 4.304 14.9865 4.30172 15.0386 4.31904C15.0907
|
|
||||||
4.33636 15.1346 4.3722 15.1621 4.41976L16.3276 3.74682ZM17.2132
|
|
||||||
5.28067L16.3276 3.74682L15.1621 4.41976L16.0477 5.95361L17.2132
|
|
||||||
5.28067ZM16.7937 7.32326C17.1015 7.09311 17.3142 6.75809 17.3915
|
|
||||||
6.38165C17.4688 6.00521 17.4054 5.61348 17.2132 5.28067L16.0477
|
|
||||||
5.95361C16.0751 6.00117 16.0842 6.05714 16.0731 6.11093C16.0621
|
|
||||||
6.16471 16.0317 6.21257 15.9877 6.24544L16.7937 7.32326ZM15.7861
|
|
||||||
8.07673L16.7937 7.32326L15.9877 6.24544L14.9801 6.99891L15.7861
|
|
||||||
8.07673ZM16.7937 10.6767L15.7861 9.92327L14.9802 11.0011L15.9877
|
|
||||||
11.7546L16.7937 10.6767ZM17.2132 12.7193C17.4054 12.3865 17.4688
|
|
||||||
11.9948 17.3915 11.6183C17.3142 11.2419 17.1015 10.9069 16.7937
|
|
||||||
10.6767L15.9877 11.7546C16.0317 11.7874 16.0621 11.8353 16.0731
|
|
||||||
11.8891C16.0842 11.9429 16.0751 11.9988 16.0477 12.0464L17.2132
|
|
||||||
12.7193ZM16.3276 14.2532L17.2132 12.7193L16.0477 12.0464L15.1621
|
|
||||||
13.5802L16.3276 14.2532ZM14.349 14.9112C14.7021 15.0626 15.0986
|
|
||||||
15.0793 15.4633 14.958C15.8279 14.8368 16.1355 14.586 16.3276
|
|
||||||
14.2532L15.1621 13.5802C15.1346 13.6278 15.0907 13.6636 15.0386
|
|
||||||
13.6809C14.9865 13.6982 14.9298 13.6959 14.8793 13.6742L14.349
|
|
||||||
14.9112ZM13.282 14.4536L14.349 14.9112L14.8793 13.6742L13.8125
|
|
||||||
13.2167L13.282 14.4536ZM11.4448 16.5879L11.5814 15.4394L10.245
|
|
||||||
15.2803L10.1083 16.4288L11.4448 16.5879ZM9.88559 17.9725C10.2699
|
|
||||||
17.9725 10.6408 17.8316 10.9282 17.5764C11.2156 17.3212 11.3994
|
|
||||||
16.9695 11.4448 16.5879L10.1083 16.4288C10.1018 16.4833 10.0756
|
|
||||||
16.5336 10.0345 16.57C9.99349 16.6065 9.94049 16.6266 9.88559
|
|
||||||
16.6266V17.9725ZM8.11441
|
|
||||||
17.9725H9.88559V16.6266H8.11441V17.9725ZM6.55522
|
|
||||||
16.5879C6.60065 16.9695 6.78445 17.3212 7.0718
|
|
||||||
17.5764C7.35916 17.8316 7.73011 17.9725 8.11441
|
|
||||||
17.9725V16.6266C8.05951 16.6266 8.00651 16.6065
|
|
||||||
7.96546 16.57C7.9244 16.5336 7.89815 16.4833 7.89167
|
|
||||||
16.4288L6.55522 16.5879ZM6.44172 15.6342L6.55522 16.5879L7.89167
|
|
||||||
16.4288L7.77817 15.475L6.44172 15.6342ZM4.00097 13.2967L3.12066
|
|
||||||
13.6742L3.65104 14.9112L4.53136 14.5337L4.00097 13.2967ZM3.12066
|
|
||||||
13.6742C3.07021 13.6959 3.01346 13.6982 2.96138 13.6809C2.90929
|
|
||||||
13.6636 2.86536 13.6278 2.83791 13.5802L1.67238 14.2532C1.86453
|
|
||||||
14.586 2.17205 14.8368 2.53672 14.958C2.90138 15.0793 3.29785
|
|
||||||
15.0626 3.65104 14.9112L3.12066 13.6742ZM2.83791 13.5802L1.95233
|
|
||||||
12.0464L0.786798 12.7193L1.67238 14.2532L2.83791 13.5802ZM1.95233
|
|
||||||
12.0464C1.92487 11.9988 1.91582 11.9429 1.92688 11.8891C1.93794
|
|
||||||
11.8353 1.96834 11.7874 2.01233 11.7546L1.20626 10.6767C0.898499
|
|
||||||
10.9069 0.685823 11.2419 0.608517 11.6183C0.531211 11.9948 0.594643
|
|
||||||
12.3865 0.786798 12.7193L1.95233 12.0464ZM2.01233 11.7546L2.70007
|
|
||||||
11.2403L1.89389 10.1625L1.20626 10.6767L2.01233 11.7546ZM1.20626
|
|
||||||
7.32326L1.894 7.83761L2.70007 6.75979L2.01233 6.24544L1.20626
|
|
||||||
7.32326ZM0.786798 5.28067C0.594643 5.61348 0.531211 6.00521
|
|
||||||
0.608517 6.38165C0.685823 6.75809 0.898499 7.09311 1.20626
|
|
||||||
7.32326L2.01233 6.24544C1.96834 6.21257 1.93794 6.16471 1.92688
|
|
||||||
6.11093C1.91582 6.05714 1.92487 6.00117 1.95233 5.95361L0.786798
|
|
||||||
5.28067ZM1.67238 3.74682L0.786798 5.28067L1.95233 5.95361L2.83791
|
|
||||||
4.41976L1.67238 3.74682ZM3.65104 3.0888C3.29785 2.93735 2.90138
|
|
||||||
2.92067 2.53672 3.04195C2.17205 3.16322 1.86453 3.41401 1.67238
|
|
||||||
3.74682L2.83791 4.41976C2.86536 4.37223 2.90929 4.33641 2.96138
|
|
||||||
4.31909C3.01346 4.30176 3.07021 4.30414 3.12066 4.32577L3.65104
|
|
||||||
3.0888ZM4.53136 3.46632L3.65104 3.0888L3.12066 4.32577L4.00097
|
|
||||||
4.70329L4.53136 3.46632ZM6.55522 1.41207L6.44172 2.36584L7.77817
|
|
||||||
2.52499L7.89167 1.57122L6.55522 1.41207ZM6.3585 4.5022C7.04018
|
|
||||||
4.11795 7.6696 3.4366 7.77817 2.52499L6.44172 2.36584C6.39887 2.72575
|
|
||||||
6.13116 3.08544 5.69756 3.32994L6.3585 4.5022ZM4.00097 4.70329C4.81792
|
|
||||||
5.05367 5.68948 4.87938 6.3585 4.5022L5.69756 3.32994C5.28213 3.56412
|
|
||||||
4.85852 3.60663 4.53136 3.46632L4.00097 4.70329ZM3.72866 9C3.72866
|
|
||||||
8.20369 3.44288 7.3153 2.70007 6.75979L1.894 7.83761C2.19884 8.06562
|
|
||||||
2.38278 8.48834 2.38278 9H3.72866ZM6.3585 13.4978C5.68948 13.1206
|
|
||||||
4.81792 12.9463 4.00097 13.2967L4.53136 14.5337C4.85852 14.3934
|
|
||||||
5.28213 14.436 5.69756 14.6703L6.3585 13.4978ZM2.70007
|
|
||||||
11.2403C3.44299 10.6848 3.72866 9.79631 3.72866 9H2.38278C2.38278
|
|
||||||
9.51177 2.19873 9.9346 1.89389 10.1625L2.70007 11.2403ZM11.5243
|
|
||||||
13.4358C10.9034 13.8057 10.3448 14.4432 10.245 15.2803L11.5814
|
|
||||||
15.4394C11.6183 15.1293 11.8415 14.8134 12.2132 14.5919L11.5243
|
|
||||||
13.4358ZM13.8125 13.2167C13.0169 12.8756 12.1673 13.0527 11.5243
|
|
||||||
13.4358L12.2132 14.5919C12.5925 14.3659 12.9839 14.3258 13.282
|
|
||||||
14.4536L13.8125 13.2167ZM7.77817 15.475C7.6696 14.5635 7.04018
|
|
||||||
13.8822 6.3585 13.4978L5.69756 14.6703C6.13116 14.9147 6.39887
|
|
||||||
15.2742 6.44172 15.6342L7.77817 15.475ZM14.047 9C14.047 9.71656
|
|
||||||
14.316 10.5045 14.9802 11.0011L15.7861 9.92327C15.547 9.74449 15.3929
|
|
||||||
9.41139 15.3929 9H14.047ZM14.9801 6.99891C14.3159 7.49553 14.047
|
|
||||||
8.28332 14.047 9H15.3929C15.3929 8.58861 15.547 8.2555 15.7861
|
|
||||||
8.07673L14.9801 6.99891ZM11.5243 4.56422C12.1673 4.94734 13.0168
|
|
||||||
5.12444 13.8123 4.78326L13.282 3.5464C12.9839 3.67426 12.5925
|
|
||||||
3.63399 12.2132 3.408L11.5243 4.56422ZM10.2449 2.7197C10.3446
|
|
||||||
3.55683 10.9034 4.19433 11.5243 4.56422L12.2132 3.408C11.8415
|
|
||||||
3.1866 11.6184 2.87077 11.5815 2.56055L10.2449 2.7197Z"
|
|
||||||
fill="#A2B1C6"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M11.0935 9C11.0935 7.84373 10.1562 6.90642 8.99988 6.90642C7.84361
|
|
||||||
6.90642 6.90629 7.84373 6.90629 9C6.90629 10.1563 7.84361 11.0936
|
|
||||||
8.99988 11.0936C10.1562 11.0936 11.0935 10.1563 11.0935 9ZM12.2898
|
|
||||||
9C12.2898 10.817 10.8169 12.2899 8.99988 12.2899C7.18289 12.2899
|
|
||||||
5.70996 10.817 5.70996 9C5.70996 7.18301 7.18289 5.71008 8.99988
|
|
||||||
5.71008C10.8169 5.71008 12.2898 7.18301 12.2898 9Z"
|
|
||||||
fill="#A2B1C6"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_2236_2707">
|
|
||||||
<rect width="18" height="18" fill="white" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'SettingsIcon'
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -7,8 +7,7 @@
|
|||||||
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
|
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"
|
||||||
14.5523 14.5523 15 14 15H4C3.44772 15 3 14.5523 3 14V4Z"
|
|
||||||
fill="#A2B1C6"
|
fill="#A2B1C6"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { nanoid } from 'nanoid'
|
||||||
import { COLOR_PICKER_CONSTANTS } from 'react-colorscales'
|
import { COLOR_PICKER_CONSTANTS } from 'react-colorscales'
|
||||||
import tinycolor from 'tinycolor2'
|
import tinycolor from 'tinycolor2'
|
||||||
|
|
||||||
@@ -5,57 +6,25 @@ const TYPE_NODE = 0
|
|||||||
const TYPE_EDGE = 1
|
const TYPE_EDGE = 1
|
||||||
const DEFAULT_SCALE = COLOR_PICKER_CONSTANTS.DEFAULT_SCALE
|
const DEFAULT_SCALE = COLOR_PICKER_CONSTANTS.DEFAULT_SCALE
|
||||||
|
|
||||||
export function dataSourceIsValid(dataSources) {
|
|
||||||
const docColumn = Object.keys(dataSources)[0]
|
|
||||||
if (!docColumn) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const records = dataSources[docColumn].slice(0, 10)
|
|
||||||
records.forEach(record => {
|
|
||||||
const parsedRec = JSON.parse(record)
|
|
||||||
if (Object.keys(parsedRec).length < 2) {
|
|
||||||
throw new Error('The records must have at least 2 keys')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const firstRecord = JSON.parse(records[0])
|
|
||||||
if (
|
|
||||||
!Object.keys(firstRecord).some(key => {
|
|
||||||
return records
|
|
||||||
.map(record => JSON.parse(record)[key])
|
|
||||||
.every(value => value === 0 || value === 1)
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
'There must be a common key used as object type: 0 - node, 1 - edge'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (err) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildNodes(graph, dataSources, options) {
|
export function buildNodes(graph, dataSources, options) {
|
||||||
const docColumn = Object.keys(dataSources)[0]
|
const docColumn = Object.keys(dataSources)[0] || 'doc'
|
||||||
const { objectType, nodeId } = options.structure
|
const { objectType, nodeId } = options.structure
|
||||||
|
|
||||||
if (objectType && nodeId) {
|
if (objectType && nodeId) {
|
||||||
const nodes = dataSources[docColumn]
|
const nodes = dataSources[docColumn]
|
||||||
.map(json => JSON.parse(json))
|
.map(json => JSON.parse(json))
|
||||||
.filter(item => item[objectType] === TYPE_NODE)
|
.filter(item => item[objectType] === TYPE_NODE)
|
||||||
|
|
||||||
nodes.forEach(node => {
|
nodes.forEach(node => {
|
||||||
if (node[nodeId]) {
|
graph.addNode(node[nodeId], {
|
||||||
graph.addNode(node[nodeId], {
|
data: node
|
||||||
data: node
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildEdges(graph, dataSources, options) {
|
export function buildEdges(graph, dataSources, options) {
|
||||||
const docColumn = Object.keys(dataSources)[0]
|
const docColumn = Object.keys(dataSources)[0] || 'doc'
|
||||||
const { objectType, edgeSource, edgeTarget } = options.structure
|
const { objectType, edgeSource, edgeTarget } = options.structure
|
||||||
|
|
||||||
if (objectType && edgeSource && edgeTarget) {
|
if (objectType && edgeSource && edgeTarget) {
|
||||||
@@ -119,133 +88,19 @@ export function updateEdges(graph, attributeUpdates) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
graph.forEachEdge((edgeId, attributes, source, target) => {
|
graph.forEachEdge((edgeId, attributes, source, target) => {
|
||||||
graph.updateEdgeWithKey(edgeId, source, target, attr => {
|
graph.updateEdge(source, target, attributes => {
|
||||||
const newAttributes = { ...attr }
|
const newAttributes = { ...attributes }
|
||||||
changeMethods.forEach(method => method(newAttributes, edgeId))
|
changeMethods.forEach(method => method(newAttributes, edgeId))
|
||||||
return newAttributes
|
return newAttributes
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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 } = labelSettings
|
||||||
return attributes => {
|
return attributes => {
|
||||||
const label = attributes.data[source] ?? ''
|
const label = attributes.data[source] ?? ''
|
||||||
attributes.label = label.toString()
|
attributes.label = label.toString()
|
||||||
attributes.labelColor = color
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,34 +109,16 @@ function getUpdateSizeMethod(graph, sizeSettings) {
|
|||||||
if (type === 'constant') {
|
if (type === 'constant') {
|
||||||
return attributes => (attributes.size = value)
|
return attributes => (attributes.size = value)
|
||||||
} else if (type === 'variable') {
|
} else if (type === 'variable') {
|
||||||
return attributes => {
|
return getVariabledSizeMethod(mode, source, scale, min)
|
||||||
attributes.size = getVariabledSize(
|
|
||||||
mode,
|
|
||||||
attributes.data[source],
|
|
||||||
scale,
|
|
||||||
min
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return (attributes, nodeId) => {
|
return (attributes, nodeId) =>
|
||||||
attributes.size = getVariabledSize(
|
(attributes.size = Math.max(graph[method](nodeId) * scale, min))
|
||||||
mode,
|
|
||||||
graph[method](nodeId),
|
|
||||||
scale,
|
|
||||||
min
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDirectVariableColorUpdateMethod(source, opacity = 100) {
|
function getDirectVariableColorUpdateMethod(source) {
|
||||||
return attributes => {
|
return attributes =>
|
||||||
const color = tinycolor(attributes.data[source])
|
(attributes.color = tinycolor(attributes.data[source]).toHexString())
|
||||||
const colorOpacity = color.getAlpha()
|
|
||||||
attributes.color = color
|
|
||||||
.setAlpha((opacity / 100) * colorOpacity)
|
|
||||||
.toHex8String()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUpdateNodeColorMethod(graph, colorSettings) {
|
function getUpdateNodeColorMethod(graph, colorSettings) {
|
||||||
@@ -293,16 +130,10 @@ function getUpdateNodeColorMethod(graph, colorSettings) {
|
|||||||
colorscale,
|
colorscale,
|
||||||
colorscaleDirection,
|
colorscaleDirection,
|
||||||
mode,
|
mode,
|
||||||
method,
|
method
|
||||||
opacity
|
|
||||||
} = colorSettings
|
} = colorSettings
|
||||||
if (type === 'constant') {
|
if (type === 'constant') {
|
||||||
const color = tinycolor(value)
|
return attributes => (attributes.color = 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(
|
||||||
@@ -311,10 +142,9 @@ function getUpdateNodeColorMethod(graph, colorSettings) {
|
|||||||
(nodeId, attributes) => attributes.data[source],
|
(nodeId, attributes) => attributes.data[source],
|
||||||
colorscale,
|
colorscale,
|
||||||
colorscaleDirection,
|
colorscaleDirection,
|
||||||
getNodeValueScale,
|
getNodeValueScale
|
||||||
opacity
|
|
||||||
)
|
)
|
||||||
: getDirectVariableColorUpdateMethod(source, opacity)
|
: getDirectVariableColorUpdateMethod(source)
|
||||||
} else {
|
} else {
|
||||||
return getColorMethod(
|
return getColorMethod(
|
||||||
graph,
|
graph,
|
||||||
@@ -322,8 +152,7 @@ function getUpdateNodeColorMethod(graph, colorSettings) {
|
|||||||
nodeId => graph[method](nodeId),
|
nodeId => graph[method](nodeId),
|
||||||
colorscale,
|
colorscale,
|
||||||
colorscaleDirection,
|
colorscaleDirection,
|
||||||
getNodeValueScale,
|
getNodeValueScale
|
||||||
opacity
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,13 +183,22 @@ function getUpdateEdgeColorMethod(graph, colorSettings) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVariabledSize(mode, value, scale, min) {
|
function getVariabledSizeMethod(mode, source, scale, min) {
|
||||||
if (mode === 'diameter') {
|
if (mode === 'diameter') {
|
||||||
return Math.max((value / 2) * scale, min / 2)
|
return attributes =>
|
||||||
|
(attributes.size = Math.max(
|
||||||
|
(attributes.data[source] / 2) * scale,
|
||||||
|
min / 2
|
||||||
|
))
|
||||||
} else if (mode === 'area') {
|
} else if (mode === 'area') {
|
||||||
return Math.max(Math.sqrt((value / 2) * scale), min / 2)
|
return attributes =>
|
||||||
|
(attributes.size = Math.max(
|
||||||
|
Math.sqrt((attributes.data[source] / 2) * scale),
|
||||||
|
min / 2
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
return Math.max(value * scale, min)
|
return attributes =>
|
||||||
|
(attributes.size = Math.max(attributes.data[source] * scale, min))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,10 +208,8 @@ 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') {
|
||||||
@@ -387,11 +223,10 @@ 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 = tinycolor(colorMap[category])
|
attributes.color = colorMap[category]
|
||||||
.setAlpha(opacityFactor)
|
|
||||||
.toHex8String()
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const min = valueScale[0]
|
const min = valueScale[0]
|
||||||
@@ -404,25 +239,20 @@ 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])
|
attributes.color = tinycolor(exactMatch[1]).toHexString()
|
||||||
.setAlpha(opacityFactor)
|
|
||||||
.toHex8String()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const rightColorIndex = normalizedColorscale.findIndex(
|
const rightColorIndex = normalizedColorscale.findIndex(
|
||||||
([value]) => value > normalizedValue
|
([value]) => value >= normalizedValue
|
||||||
)
|
)
|
||||||
const leftColorIndex = rightColorIndex - 1
|
const leftColorIndex = (rightColorIndex || 1) - 1
|
||||||
const right = normalizedColorscale[rightColorIndex]
|
const right = normalizedColorscale[rightColorIndex]
|
||||||
const left = normalizedColorscale[leftColorIndex]
|
const left = normalizedColorscale[leftColorIndex]
|
||||||
const interpolationFactor =
|
const interpolationFactor =
|
||||||
@@ -439,9 +269,7 @@ 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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -461,3 +289,76 @@ 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 function getOptionsForSave(state, dataSources) {
|
||||||
|
// we don't need to save the data, only settings
|
||||||
|
// so we modify state.data using dereference
|
||||||
|
const stateCopy = JSON.parse(JSON.stringify(state))
|
||||||
|
const emptySources = {}
|
||||||
|
for (const key in dataSources) {
|
||||||
|
emptySources[key] = []
|
||||||
|
}
|
||||||
|
dereference.default(stateCopy.data, emptySources)
|
||||||
|
return stateCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getImageDataUrl(element, type) {
|
||||||
|
const chartElement = element.querySelector('.js-plotly-plot')
|
||||||
|
return await plotly.toImage(chartElement, {
|
||||||
|
format: type,
|
||||||
|
width: null,
|
||||||
|
height: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChartData(element) {
|
||||||
|
const chartElement = element.querySelector('.js-plotly-plot')
|
||||||
|
return {
|
||||||
|
data: chartElement.data,
|
||||||
|
layout: chartElement.layout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHtml(options) {
|
||||||
|
const chartId = nanoid()
|
||||||
|
return `
|
||||||
|
<script src="https://cdn.plot.ly/plotly-latest.js" charset="UTF-8"></script>
|
||||||
|
<div id="${chartId}"></div>
|
||||||
|
<script>
|
||||||
|
const el = document.getElementById("${chartId}")
|
||||||
|
|
||||||
|
let timeout
|
||||||
|
function debounceResize() {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
var r = el.getBoundingClientRect()
|
||||||
|
Plotly.relayout(el, {width: r.width, height: r.height})
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(debounceResize)
|
||||||
|
resizeObserver.observe(el)
|
||||||
|
|
||||||
|
Plotly.newPlot(el, ${JSON.stringify(options.data)}, ${JSON.stringify(options.layout)})
|
||||||
|
</script>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getOptionsFromDataSources,
|
||||||
|
getOptionsForSave,
|
||||||
|
getImageDataUrl,
|
||||||
|
getHtml,
|
||||||
|
getChartData
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +1,12 @@
|
|||||||
export default {
|
export default {
|
||||||
_migrate(installedVersion, inquiries) {
|
_migrate(installedVersion, inquiries) {
|
||||||
if (installedVersion < 2) {
|
if (installedVersion === 1) {
|
||||||
inquiries.forEach(inquiry => {
|
inquiries.forEach(inquire => {
|
||||||
inquiry.viewType = 'chart'
|
inquire.viewType = 'chart'
|
||||||
inquiry.viewOptions = inquiry.chart
|
inquire.viewOptions = inquire.chart
|
||||||
delete inquiry.chart
|
delete inquire.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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ import events from '@/lib/utils/events'
|
|||||||
import migration from './_migrations'
|
import migration from './_migrations'
|
||||||
|
|
||||||
const migrate = migration._migrate
|
const migrate = migration._migrate
|
||||||
const myInquiriesKey = 'myInquiries'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
version: 3,
|
version: 2,
|
||||||
myInquiriesKey,
|
|
||||||
getStoredInquiries() {
|
getStoredInquiries() {
|
||||||
let myInquiries = JSON.parse(localStorage.getItem(myInquiriesKey))
|
let myInquiries = JSON.parse(localStorage.getItem('myInquiries'))
|
||||||
if (!myInquiries) {
|
if (!myInquiries) {
|
||||||
const oldInquiries = localStorage.getItem('myQueries')
|
const oldInquiries = localStorage.getItem('myQueries')
|
||||||
if (oldInquiries) {
|
if (oldInquiries) {
|
||||||
@@ -21,21 +19,14 @@ export default {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
if (myInquiries.version === 2) {
|
return (myInquiries && myInquiries.inquiries) || []
|
||||||
myInquiries = migrate(2, myInquiries.inquiries)
|
|
||||||
this.updateStorage(myInquiries)
|
|
||||||
return myInquiries
|
|
||||||
}
|
|
||||||
|
|
||||||
return myInquiries.inquiries || []
|
|
||||||
},
|
},
|
||||||
|
|
||||||
duplicateInquiry(baseInquiry) {
|
duplicateInquiry(baseInquiry) {
|
||||||
const newInquiry = JSON.parse(JSON.stringify(baseInquiry))
|
const newInquiry = JSON.parse(JSON.stringify(baseInquiry))
|
||||||
newInquiry.name = newInquiry.name + ' Copy'
|
newInquiry.name = newInquiry.name + ' Copy'
|
||||||
newInquiry.id = nanoid()
|
newInquiry.id = nanoid()
|
||||||
newInquiry.createdAt = new Date().toJSON()
|
newInquiry.createdAt = new Date()
|
||||||
newInquiry.updatedAt = new Date().toJSON()
|
|
||||||
delete newInquiry.isPredefined
|
delete newInquiry.isPredefined
|
||||||
|
|
||||||
return newInquiry
|
return newInquiry
|
||||||
@@ -47,7 +38,7 @@ export default {
|
|||||||
|
|
||||||
updateStorage(inquiries) {
|
updateStorage(inquiries) {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
myInquiriesKey,
|
'myInquiries',
|
||||||
JSON.stringify({ version: this.version, inquiries })
|
JSON.stringify({ version: this.version, inquiries })
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -69,8 +60,6 @@ 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 || []
|
||||||
}
|
}
|
||||||
@@ -90,11 +79,11 @@ export default {
|
|||||||
|
|
||||||
importInquiries() {
|
importInquiries() {
|
||||||
return fu.importFile().then(str => {
|
return fu.importFile().then(str => {
|
||||||
const inquiries = this.deserialiseInquiries(str)
|
const inquires = this.deserialiseInquiries(str)
|
||||||
|
|
||||||
events.send('inquiry.import', inquiries.length)
|
events.send('inquiry.import', inquires.length)
|
||||||
|
|
||||||
return inquiries
|
return inquires
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
export(inquiryList, fileName) {
|
export(inquiryList, fileName) {
|
||||||
@@ -110,8 +99,6 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export default class Tab {
|
|||||||
|
|
||||||
this.isSaved = !!inquiry.id
|
this.isSaved = !!inquiry.id
|
||||||
this.state = state
|
this.state = state
|
||||||
this.updatedAt = inquiry.updatedAt
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute() {
|
async execute() {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import store from '@/store'
|
|||||||
import { createVfm, VueFinalModal, useVfm } from 'vue-final-modal'
|
import { createVfm, VueFinalModal, useVfm } from 'vue-final-modal'
|
||||||
|
|
||||||
import '@/assets/styles/variables.css'
|
import '@/assets/styles/variables.css'
|
||||||
import '@/assets/styles/typography.css'
|
|
||||||
import '@/assets/styles/buttons.css'
|
import '@/assets/styles/buttons.css'
|
||||||
import '@/assets/styles/tables.css'
|
import '@/assets/styles/tables.css'
|
||||||
import '@/assets/styles/dialogs.css'
|
import '@/assets/styles/dialogs.css'
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
import Workspace from '@/views/Workspace'
|
import Workspace from '@/views/MainView/Workspace'
|
||||||
import Inquiries from '@/views/Inquiries'
|
import Inquiries from '@/views/MainView/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'
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
import database from '@/lib/database'
|
import database from '@/lib/database'
|
||||||
|
|
||||||
export const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'Welcome',
|
name: 'Welcome',
|
||||||
|
|||||||
@@ -17,33 +17,28 @@ export default {
|
|||||||
},
|
},
|
||||||
async saveInquiry({ state }, { inquiryTab, newName }) {
|
async saveInquiry({ state }, { inquiryTab, newName }) {
|
||||||
const value = {
|
const value = {
|
||||||
id: inquiryTab.isPredefined || newName ? nanoid() : inquiryTab.id,
|
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
|
||||||
query: inquiryTab.query,
|
query: inquiryTab.query,
|
||||||
viewType: inquiryTab.dataView.mode,
|
viewType: inquiryTab.dataView.mode,
|
||||||
viewOptions: inquiryTab.dataView.getOptionsForSave(),
|
viewOptions: inquiryTab.dataView.getOptionsForSave(),
|
||||||
name: newName || inquiryTab.name,
|
name: newName || inquiryTab.name
|
||||||
updatedAt: new Date().toJSON()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get inquiries from local storage
|
// Get inquiries from local storage
|
||||||
const myInquiries = state.inquiries
|
const myInquiries = state.inquiries
|
||||||
let inquiryIndex
|
|
||||||
// Set createdAt
|
// Set createdAt
|
||||||
if (newName) {
|
if (newName) {
|
||||||
value.createdAt = new Date().toJSON()
|
value.createdAt = new Date()
|
||||||
} else {
|
} else {
|
||||||
inquiryIndex = myInquiries.findIndex(
|
var inquiryIndex = myInquiries.findIndex(
|
||||||
oldInquiry => oldInquiry.id === inquiryTab.id
|
oldInquiry => oldInquiry.id === inquiryTab.id
|
||||||
)
|
)
|
||||||
|
value.createdAt = myInquiries[inquiryIndex].createdAt
|
||||||
value.createdAt =
|
|
||||||
inquiryIndex !== -1
|
|
||||||
? myInquiries[inquiryIndex].createdAt
|
|
||||||
: new Date().toJSON()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert in inquiries list
|
// Insert in inquiries list
|
||||||
if (newName || inquiryIndex === -1) {
|
if (newName) {
|
||||||
myInquiries.push(value)
|
myInquiries.push(value)
|
||||||
} else {
|
} else {
|
||||||
myInquiries.splice(inquiryIndex, 1, value)
|
myInquiries.splice(inquiryIndex, 1, value)
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateTab(state, { tab, newValues }) {
|
updateTab(state, { tab, newValues }) {
|
||||||
const { name, id, query, viewType, viewOptions, isSaved, updatedAt } =
|
const { name, id, query, viewType, viewOptions, isSaved } = newValues
|
||||||
newValues
|
|
||||||
const oldId = tab.id
|
const oldId = tab.id
|
||||||
|
|
||||||
if (id && state.currentTabId === oldId) {
|
if (id && state.currentTabId === oldId) {
|
||||||
@@ -37,9 +36,6 @@ export default {
|
|||||||
// Saved inquiry is not predefined
|
// Saved inquiry is not predefined
|
||||||
delete tab.isPredefined
|
delete tab.isPredefined
|
||||||
}
|
}
|
||||||
if (updatedAt) {
|
|
||||||
tab.updatedAt = updatedAt
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteTab(state, tab) {
|
deleteTab(state, tab) {
|
||||||
|
|||||||
@@ -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/Common/Logs'
|
import Logs from '@/components/Logs'
|
||||||
import events from '@/lib/utils/events'
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -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',
|
||||||
@@ -184,14 +184,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import RenameIcon from '@/components/svg/rename'
|
import RenameIcon from './svg/rename'
|
||||||
import CopyIcon from '@/components/svg/copy'
|
import CopyIcon from './svg/copy'
|
||||||
import ExportIcon from '@/components/svg/export'
|
import ExportIcon from '@/components/svg/export'
|
||||||
import DeleteIcon from '@/components/svg/delete'
|
import DeleteIcon from './svg/delete'
|
||||||
import CloseIcon from '@/components/svg/close'
|
import CloseIcon from '@/components/svg/close'
|
||||||
import TextField from '@/components/Common/TextField'
|
import TextField from '@/components/TextField'
|
||||||
import CheckBox from '@/components/Common/CheckBox'
|
import CheckBox from '@/components/CheckBox'
|
||||||
import LoadingIndicator from '@/components/Common/LoadingIndicator'
|
import LoadingIndicator from '@/components/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'
|
||||||
@@ -10,22 +10,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="nav-buttons">
|
<div id="nav-buttons">
|
||||||
<button
|
<button
|
||||||
v-show="currentInquiryTab && $route.path === '/workspace'"
|
v-show="currentInquiry && $route.path === '/workspace'"
|
||||||
id="save-btn"
|
id="save-btn"
|
||||||
class="primary"
|
class="primary"
|
||||||
:disabled="isSaved"
|
:disabled="isSaved"
|
||||||
@click="onSave(false)"
|
@click="checkInquiryBeforeSave"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
v-show="currentInquiryTab && $route.path === '/workspace'"
|
|
||||||
id="save-as-btn"
|
|
||||||
class="primary"
|
|
||||||
@click="onSaveAs"
|
|
||||||
>
|
|
||||||
Save as
|
|
||||||
</button>
|
|
||||||
<button id="create-btn" class="primary" @click="createNewInquiry">
|
<button id="create-btn" class="primary" @click="createNewInquiry">
|
||||||
Create
|
Create
|
||||||
</button>
|
</button>
|
||||||
@@ -53,41 +45,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="dialog-buttons-container">
|
<div class="dialog-buttons-container">
|
||||||
<button class="secondary" @click="cancelSave">Cancel</button>
|
<button class="secondary" @click="cancelSave">Cancel</button>
|
||||||
<button class="primary" @click="validateSaveFormAndSaveInquiry">
|
<button class="primary" @click="saveInquiry">Save</button>
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</modal>
|
|
||||||
|
|
||||||
<!-- Inquiery saving conflict dialog -->
|
|
||||||
<modal
|
|
||||||
modalId="inquiry-conflict"
|
|
||||||
class="dialog"
|
|
||||||
contentStyle="width: 560px;"
|
|
||||||
>
|
|
||||||
<div class="dialog-header">
|
|
||||||
Inquiry saving conflict
|
|
||||||
<close-icon @click="cancelSave" />
|
|
||||||
</div>
|
|
||||||
<div class="dialog-body">
|
|
||||||
<div id="save-note">
|
|
||||||
<img src="~@/assets/images/info.svg" />
|
|
||||||
This inquiry has been modified in the mean time. This can happen if an
|
|
||||||
inquiry is saved in another window or browser tab. Do you want to
|
|
||||||
overwrite that changes or save the current state as a new inquiry?
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="dialog-buttons-container">
|
|
||||||
<button class="secondary" @click="cancelSave">Cancel</button>
|
|
||||||
<button class="primary" @click="onSave(true)">Overwrite</button>
|
|
||||||
<button class="primary" @click="onSaveAs">Save as new</button>
|
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import TextField from '@/components/Common/TextField'
|
import TextField from '@/components/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'
|
||||||
@@ -108,28 +73,25 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
inquiries() {
|
currentInquiry() {
|
||||||
return this.$store.state.inquiries
|
|
||||||
},
|
|
||||||
currentInquiryTab() {
|
|
||||||
return this.$store.state.currentTab
|
return this.$store.state.currentTab
|
||||||
},
|
},
|
||||||
isSaved() {
|
isSaved() {
|
||||||
return this.currentInquiryTab && this.currentInquiryTab.isSaved
|
return this.currentInquiry && this.currentInquiry.isSaved
|
||||||
},
|
},
|
||||||
isPredefined() {
|
isPredefined() {
|
||||||
return this.currentInquiryTab && this.currentInquiryTab.isPredefined
|
return this.currentInquiry && this.currentInquiry.isPredefined
|
||||||
},
|
},
|
||||||
runDisabled() {
|
runDisabled() {
|
||||||
return (
|
return (
|
||||||
this.currentInquiryTab &&
|
this.currentInquiry &&
|
||||||
(!this.$store.state.db || !this.currentInquiryTab.query)
|
(!this.$store.state.db || !this.currentInquiry.query)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
eventBus.$on('createNewInquiry', this.createNewInquiry)
|
eventBus.$on('createNewInquiry', this.createNewInquiry)
|
||||||
eventBus.$on('saveInquiry', this.onSave)
|
eventBus.$on('saveInquiry', this.checkInquiryBeforeSave)
|
||||||
document.addEventListener('keydown', this._keyListener)
|
document.addEventListener('keydown', this._keyListener)
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
@@ -147,84 +109,63 @@ export default {
|
|||||||
events.send('inquiry.create', null, { auto: false })
|
events.send('inquiry.create', null, { auto: false })
|
||||||
},
|
},
|
||||||
cancelSave() {
|
cancelSave() {
|
||||||
this.errorMsg = null
|
|
||||||
this.name = ''
|
|
||||||
this.$modal.hide('save')
|
this.$modal.hide('save')
|
||||||
this.$modal.hide('inquiry-conflict')
|
|
||||||
eventBus.$off('inquirySaved')
|
eventBus.$off('inquirySaved')
|
||||||
},
|
},
|
||||||
onSave(skipConcurrentEditingCheck = false) {
|
checkInquiryBeforeSave() {
|
||||||
if (storedInquiries.isTabNeedName(this.currentInquiryTab)) {
|
|
||||||
this.openSaveModal()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!skipConcurrentEditingCheck) {
|
|
||||||
const inquiryInStore = this.inquiries.find(
|
|
||||||
inquiry => inquiry.id === this.currentInquiryTab.id
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
inquiryInStore &&
|
|
||||||
inquiryInStore.updatedAt !== this.currentInquiryTab.updatedAt
|
|
||||||
) {
|
|
||||||
this.$modal.show('inquiry-conflict')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.saveInquiry()
|
|
||||||
},
|
|
||||||
onSaveAs() {
|
|
||||||
this.openSaveModal()
|
|
||||||
},
|
|
||||||
openSaveModal() {
|
|
||||||
this.$modal.hide('inquiry-conflict')
|
|
||||||
this.errorMsg = null
|
this.errorMsg = null
|
||||||
this.name = ''
|
this.name = ''
|
||||||
this.$modal.show('save')
|
|
||||||
|
if (storedInquiries.isTabNeedName(this.currentInquiry)) {
|
||||||
|
this.$modal.show('save')
|
||||||
|
} else {
|
||||||
|
this.saveInquiry()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
validateSaveFormAndSaveInquiry() {
|
async saveInquiry() {
|
||||||
if (!this.name) {
|
const isNeedName = storedInquiries.isTabNeedName(this.currentInquiry)
|
||||||
|
if (isNeedName && !this.name) {
|
||||||
this.errorMsg = "Inquiry name can't be empty"
|
this.errorMsg = "Inquiry name can't be empty"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.saveInquiry()
|
const dataSet = this.currentInquiry.result
|
||||||
},
|
const tabView = this.currentInquiry.view
|
||||||
async saveInquiry() {
|
|
||||||
const eventName =
|
|
||||||
this.currentInquiryTab.name && this.name
|
|
||||||
? 'inquiry.saveAs'
|
|
||||||
: 'inquiry.save'
|
|
||||||
|
|
||||||
// Save inquiry
|
// Save inquiry
|
||||||
const value = await this.$store.dispatch('saveInquiry', {
|
const value = await this.$store.dispatch('saveInquiry', {
|
||||||
inquiryTab: this.currentInquiryTab,
|
inquiryTab: this.currentInquiry,
|
||||||
newName: this.name
|
newName: this.name
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update tab in store
|
// Update tab in store
|
||||||
this.$store.commit('updateTab', {
|
this.$store.commit('updateTab', {
|
||||||
tab: this.currentInquiryTab,
|
tab: this.currentInquiry,
|
||||||
newValues: {
|
newValues: {
|
||||||
name: value.name,
|
name: value.name,
|
||||||
id: value.id,
|
id: value.id,
|
||||||
query: value.query,
|
query: value.query,
|
||||||
viewType: value.viewType,
|
viewType: value.viewType,
|
||||||
viewOptions: value.viewOptions,
|
viewOptions: value.viewOptions,
|
||||||
isSaved: true,
|
isSaved: true
|
||||||
updatedAt: value.updatedAt
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Hide dialogs
|
// Restore data:
|
||||||
|
// e.g. if we save predefined inquiry the tab will be created again
|
||||||
|
// (because of new id) and
|
||||||
|
// it will be without sql result and has default view - table.
|
||||||
|
// That's why we need to restore data and view
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.currentInquiry.result = dataSet
|
||||||
|
this.currentInquiry.view = tabView
|
||||||
|
})
|
||||||
|
|
||||||
|
// Hide dialog
|
||||||
this.$modal.hide('save')
|
this.$modal.hide('save')
|
||||||
this.$modal.hide('inquiry-conflict')
|
|
||||||
this.errorMsg = null
|
|
||||||
this.name = ''
|
|
||||||
|
|
||||||
// Signal about saving
|
// Signal about saving
|
||||||
eventBus.$emit('inquirySaved')
|
eventBus.$emit('inquirySaved')
|
||||||
events.send(eventName)
|
events.send('inquiry.save')
|
||||||
},
|
},
|
||||||
_keyListener(e) {
|
_keyListener(e) {
|
||||||
if (this.$route.path === '/workspace') {
|
if (this.$route.path === '/workspace') {
|
||||||
@@ -232,25 +173,19 @@ export default {
|
|||||||
if ((e.key === 'r' || e.key === 'Enter') && (e.ctrlKey || e.metaKey)) {
|
if ((e.key === 'r' || e.key === 'Enter') && (e.ctrlKey || e.metaKey)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!this.runDisabled) {
|
if (!this.runDisabled) {
|
||||||
this.currentInquiryTab.execute()
|
this.currentInquiry.execute()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save inquiry Ctrl+S
|
// Save inquiry Ctrl+S
|
||||||
if (e.key === 's' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!this.isSaved) {
|
if (!this.isSaved) {
|
||||||
this.onSave()
|
this.checkInquiryBeforeSave()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Save inquiry as Ctrl+Shift+S
|
|
||||||
if (e.key === 'S' && (e.ctrlKey || e.metaKey) && e.shiftKey) {
|
|
||||||
e.preventDefault()
|
|
||||||
this.onSaveAs()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// New (blank) inquiry Ctrl+B
|
// New (blank) inquiry Ctrl+B
|
||||||
if (e.key === 'b' && (e.ctrlKey || e.metaKey)) {
|
if (e.key === 'b' && (e.ctrlKey || e.metaKey)) {
|
||||||
@@ -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/Common/TextField'
|
import TextField from '@/components/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'
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="chartContainer" class="chart-container">
|
<div ref="chartContainer" class="chart-container">
|
||||||
<div v-show="!dataSources" class="warning data-view-warning">
|
<div v-show="!dataSources" class="warning chart-warning">
|
||||||
There is no data to build a chart. Run your SQL query and make sure the
|
There is no data to build a chart. Run your SQL query and make sure the
|
||||||
result is not empty.
|
result is not empty.
|
||||||
</div>
|
</div>
|
||||||
@@ -20,7 +20,6 @@
|
|||||||
:useResizeHandler="useResizeHandler"
|
:useResizeHandler="useResizeHandler"
|
||||||
:debug="true"
|
:debug="true"
|
||||||
:advancedTraceTypeSelector="true"
|
:advancedTraceTypeSelector="true"
|
||||||
:hideControls="!showViewSettings"
|
|
||||||
@update="update"
|
@update="update"
|
||||||
@render="onRender"
|
@render="onRender"
|
||||||
/>
|
/>
|
||||||
@@ -46,17 +45,11 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
dataSources: Object,
|
dataSources: Object,
|
||||||
initOptions: Object,
|
initOptions: Object,
|
||||||
exportToPngEnabled: Boolean,
|
importToPngEnabled: Boolean,
|
||||||
exportToSvgEnabled: Boolean,
|
importToSvgEnabled: Boolean,
|
||||||
forPivot: Boolean,
|
forPivot: Boolean
|
||||||
showViewSettings: Boolean
|
|
||||||
},
|
},
|
||||||
emits: [
|
emits: ['update:importToSvgEnabled', 'update', 'loadingImageCompleted'],
|
||||||
'update:exportToSvgEnabled',
|
|
||||||
'update:exportToHtmlEnabled',
|
|
||||||
'update',
|
|
||||||
'loadingImageCompleted'
|
|
||||||
],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
plotly,
|
plotly,
|
||||||
@@ -87,9 +80,6 @@ export default {
|
|||||||
dereference.default(this.state.data, this.dataSources)
|
dereference.default(this.state.data, this.dataSources)
|
||||||
this.updatePlotly()
|
this.updatePlotly()
|
||||||
}
|
}
|
||||||
},
|
|
||||||
showViewSettings() {
|
|
||||||
this.handleResize()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -112,16 +102,15 @@ export default {
|
|||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
this.$emit('update:exportToSvgEnabled', true)
|
this.$emit('update:importToSvgEnabled', true)
|
||||||
this.$emit('update:exportToHtmlEnabled', true)
|
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||||
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,10 +123,6 @@ 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() {
|
||||||
@@ -176,8 +161,11 @@ export default {
|
|||||||
'text/html'
|
'text/html'
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
prepareCopy(type = 'png') {
|
async prepareCopy(type = 'png') {
|
||||||
return chartHelper.getImageDataUrl(this.$refs.plotlyEditor.$el, type)
|
return await chartHelper.getImageDataUrl(
|
||||||
|
this.$refs.plotlyEditor.$el,
|
||||||
|
type
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,6 +176,13 @@ export default {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-warning {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
.chart {
|
.chart {
|
||||||
min-height: 242px;
|
min-height: 242px;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,734 @@
|
|||||||
|
<template>
|
||||||
|
<div class="plotly_editor">
|
||||||
|
<GraphEditorControls>
|
||||||
|
<PanelMenuWrapper>
|
||||||
|
<Panel group="Structure" name="Graph">
|
||||||
|
<Fold name="Graph">
|
||||||
|
<Field>Choose keys explanation...</Field>
|
||||||
|
<Field label="Object type">
|
||||||
|
<Dropdown
|
||||||
|
:options="keysOptions"
|
||||||
|
:value="settings.structure.objectType"
|
||||||
|
@change="updateStructure('objectType', $event)"
|
||||||
|
/>
|
||||||
|
<Field>0 - node; 1 - edge</Field>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Node Id">
|
||||||
|
<Dropdown
|
||||||
|
:options="keysOptions"
|
||||||
|
:value="settings.structure.nodeId"
|
||||||
|
@change="updateStructure('nodeId', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Edge source">
|
||||||
|
<Dropdown
|
||||||
|
:options="keysOptions"
|
||||||
|
:value="settings.structure.edgeSource"
|
||||||
|
@change="updateStructure('edgeSource', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Edge target">
|
||||||
|
<Dropdown
|
||||||
|
:options="keysOptions"
|
||||||
|
:value="settings.structure.edgeTarget"
|
||||||
|
@change="updateStructure('edgeTarget', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Fold>
|
||||||
|
</Panel>
|
||||||
|
<Panel group="Style" name="Nodes">
|
||||||
|
<Fold name="Nodes">
|
||||||
|
<Field label="Label">
|
||||||
|
<Dropdown
|
||||||
|
:options="keysOptions"
|
||||||
|
:value="settings.style.nodes.label.source"
|
||||||
|
@change="updateNodes('label.source', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Size">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="nodeSizeTypeOptions"
|
||||||
|
:activeOption="settings.style.nodes.size.type"
|
||||||
|
@option-change="updateNodes('size.type', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<NumericInput
|
||||||
|
v-if="settings.style.nodes.size.type === 'constant'"
|
||||||
|
:value="settings.style.nodes.size.value"
|
||||||
|
:min="1"
|
||||||
|
@update="updateNodes('size.value', $event)"
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
v-if="settings.style.nodes.size.type === 'variable'"
|
||||||
|
:options="keysOptions"
|
||||||
|
:value="settings.style.nodes.size.source"
|
||||||
|
@change="updateNodes('size.source', $event)"
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
v-if="settings.style.nodes.size.type === 'calculated'"
|
||||||
|
:options="nodeCalculatedSizeMethodOptions"
|
||||||
|
:value="settings.style.nodes.size.method"
|
||||||
|
@change="updateNodes('size.method', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<template v-if="settings.style.nodes.size.type !== 'constant'">
|
||||||
|
<Field label="Size scale">
|
||||||
|
<NumericInput
|
||||||
|
:value="settings.style.nodes.size.scale"
|
||||||
|
@update="updateNodes('size.scale', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Size mode">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="nodeSizeModeOptions"
|
||||||
|
:activeOption="settings.style.nodes.size.mode"
|
||||||
|
@option-change="updateNodes('size.mode', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Minimum size">
|
||||||
|
<NumericInput
|
||||||
|
:value="settings.style.nodes.size.min"
|
||||||
|
@update="updateNodes('size.min', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Field label="Color">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="nodeColorTypeOptions"
|
||||||
|
:activeOption="settings.style.nodes.color.type"
|
||||||
|
@option-change="updateNodes('color.type', $event)"
|
||||||
|
/>
|
||||||
|
<Field v-if="settings.style.nodes.color.type === 'constant'">
|
||||||
|
<ColorPicker
|
||||||
|
:selectedColor="settings.style.nodes.color.value"
|
||||||
|
@color-change="updateNodes('color.value', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<template v-else>
|
||||||
|
<Field>
|
||||||
|
<Dropdown
|
||||||
|
v-if="settings.style.nodes.color.type === 'variable'"
|
||||||
|
:options="keysOptions"
|
||||||
|
:value="settings.style.nodes.color.source"
|
||||||
|
@change="updateNodes('color.source', $event)"
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
v-if="settings.style.nodes.color.type === 'calculated'"
|
||||||
|
:options="nodeCalculatedColorMethodOptions"
|
||||||
|
:value="settings.style.nodes.color.method"
|
||||||
|
@change="updateNodes('color.method', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="colorSourceUsageOptions"
|
||||||
|
:activeOption="settings.style.nodes.color.sourceUsage"
|
||||||
|
@option-change="updateNodes('color.sourceUsage', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
v-if="settings.style.nodes.color.sourceUsage === 'map_to'"
|
||||||
|
>
|
||||||
|
<ColorscalePicker
|
||||||
|
:selected="settings.style.nodes.color.colorscale"
|
||||||
|
className="colorscale-picker"
|
||||||
|
@colorscale-change="updateNodes('color.colorscale', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
v-if="settings.style.nodes.color.type !== 'constant'"
|
||||||
|
label="Color as"
|
||||||
|
>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="сolorAsOptions"
|
||||||
|
:activeOption="settings.style.nodes.color.mode"
|
||||||
|
@option-change="updateNodes('color.mode', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
v-if="settings.style.nodes.color.type !== 'constant'"
|
||||||
|
label="Colorscale direction"
|
||||||
|
>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="сolorscaleDirections"
|
||||||
|
:activeOption="settings.style.nodes.color.colorscaleDirection"
|
||||||
|
@option-change="
|
||||||
|
updateNodes('color.colorscaleDirection', $event)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Fold>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Panel group="Style" name="Edges">
|
||||||
|
<Fold name="Edges">
|
||||||
|
<Field label="Direction">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="visibilityOptions"
|
||||||
|
:activeOption="settings.style.edges.showDirection"
|
||||||
|
@option-change="updateEdges('showDirection', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Label">
|
||||||
|
<Dropdown
|
||||||
|
:options="keysOptions"
|
||||||
|
:value="settings.style.edges.label.source"
|
||||||
|
@change="updateEdges('label.source', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Size">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="edgeSizeTypeOptions"
|
||||||
|
:activeOption="settings.style.edges.size.type"
|
||||||
|
@option-change="updateEdges('size.type', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<NumericInput
|
||||||
|
v-if="settings.style.edges.size.type === 'constant'"
|
||||||
|
:value="settings.style.edges.size.value"
|
||||||
|
:min="1"
|
||||||
|
@update="updateEdges('size.value', $event)"
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
v-if="settings.style.edges.size.type === 'variable'"
|
||||||
|
:options="keysOptions"
|
||||||
|
:value="settings.style.edges.size.source"
|
||||||
|
@change="updateEdges('size.source', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<template v-if="settings.style.edges.size.type !== 'constant'">
|
||||||
|
<Field label="Size scale">
|
||||||
|
<NumericInput
|
||||||
|
:value="settings.style.edges.size.scale"
|
||||||
|
@update="updateEdges('size.scale', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Minimum size">
|
||||||
|
<NumericInput
|
||||||
|
:value="settings.style.edges.size.min"
|
||||||
|
@update="updateEdges('size.min', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Field label="Color">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="edgeColorTypeOptions"
|
||||||
|
:activeOption="settings.style.edges.color.type"
|
||||||
|
@option-change="updateEdges('color.type', $event)"
|
||||||
|
/>
|
||||||
|
<Field v-if="settings.style.edges.color.type === 'constant'">
|
||||||
|
<ColorPicker
|
||||||
|
:selectedColor="settings.style.edges.color.value"
|
||||||
|
@color-change="updateEdges('color.value', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<template v-else>
|
||||||
|
<Field>
|
||||||
|
<Dropdown
|
||||||
|
v-if="settings.style.edges.color.type === 'variable'"
|
||||||
|
:options="keysOptions"
|
||||||
|
:value="settings.style.edges.color.source"
|
||||||
|
@change="updateEdges('color.source', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="colorSourceUsageOptions"
|
||||||
|
:activeOption="settings.style.edges.color.sourceUsage"
|
||||||
|
@option-change="updateEdges('color.sourceUsage', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
v-if="settings.style.edges.color.sourceUsage === 'map_to'"
|
||||||
|
>
|
||||||
|
<ColorscalePicker
|
||||||
|
:selected="settings.style.edges.color.colorscale"
|
||||||
|
className="colorscale-picker"
|
||||||
|
@colorscale-change="updateEdges('color.colorscale', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
v-if="settings.style.edges.color.type !== 'constant'"
|
||||||
|
label="Color as"
|
||||||
|
>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="сolorAsOptions"
|
||||||
|
:activeOption="settings.style.edges.color.mode"
|
||||||
|
@option-change="updateEdges('color.mode', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
v-if="settings.style.edges.color.type !== 'constant'"
|
||||||
|
label="Colorscale direction"
|
||||||
|
>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="сolorscaleDirections"
|
||||||
|
:activeOption="settings.style.edges.color.colorscaleDirection"
|
||||||
|
@option-change="
|
||||||
|
updateEdges('color.colorscaleDirection', $event)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Fold>
|
||||||
|
</Panel>
|
||||||
|
<Panel group="Style" name="Layout">
|
||||||
|
<Fold name="Layout">
|
||||||
|
<Field label="Algorithm">
|
||||||
|
<Dropdown
|
||||||
|
:options="layoutOptions"
|
||||||
|
:value="settings.layout.type"
|
||||||
|
@change="updateLayout($event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<component
|
||||||
|
:is="layoutSettingsComponentMap[settings.layout.type]"
|
||||||
|
v-if="settings.layout.type !== 'circular'"
|
||||||
|
v-model="settings.layout.options"
|
||||||
|
:keyOptions="keysOptions"
|
||||||
|
@update:model-value="updateLayout(settings.layout.type)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Field v-if="settings.layout.type === 'forceAtlas2'">
|
||||||
|
<Button variant="primary" @click="toggleFA2Layout">
|
||||||
|
<template #node:icon>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
padding: '0 3px'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<RunIcon v-if="!fa2Running" />
|
||||||
|
<StopIcon v-else /></div
|
||||||
|
></template>
|
||||||
|
{{ fa2Running ? 'Stop' : 'Start' }}
|
||||||
|
</Button>
|
||||||
|
</Field>
|
||||||
|
</Fold>
|
||||||
|
</Panel>
|
||||||
|
</PanelMenuWrapper>
|
||||||
|
</GraphEditorControls>
|
||||||
|
<div
|
||||||
|
ref="graph"
|
||||||
|
:style="{
|
||||||
|
height: '100%',
|
||||||
|
width: '100%'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { markRaw } from 'vue'
|
||||||
|
import { applyPureReactInVue } from 'veaury'
|
||||||
|
import GraphEditorControls from '@/lib/GraphEditorControls.jsx'
|
||||||
|
import { PanelMenuWrapper, Panel, Fold, Section } from 'react-chart-editor'
|
||||||
|
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
|
||||||
|
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
|
||||||
|
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
||||||
|
import Button from 'react-chart-editor/lib/components/widgets/Button'
|
||||||
|
import ColorscalePicker from 'react-chart-editor/lib/components/widgets/ColorscalePicker'
|
||||||
|
import ColorPicker from 'react-chart-editor/lib/components/widgets/ColorPicker'
|
||||||
|
import Field from 'react-chart-editor/lib/components/fields/Field'
|
||||||
|
import RandomLayoutSettings from '@/components/Graph/RandomLayoutSettings.vue'
|
||||||
|
import ForceAtlasLayoutSettings from '@/components/Graph/ForceAtlasLayoutSettings.vue'
|
||||||
|
import CirclePackLayoutSettings from '@/components/Graph/CirclePackLayoutSettings.vue'
|
||||||
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
|
import FA2Layout from 'graphology-layout-forceatlas2/worker'
|
||||||
|
import forceAtlas2 from 'graphology-layout-forceatlas2'
|
||||||
|
import RunIcon from '@/components/svg/run.vue'
|
||||||
|
import StopIcon from '@/components/svg/stop.vue'
|
||||||
|
import {
|
||||||
|
buildNodes,
|
||||||
|
buildEdges,
|
||||||
|
updateNodes,
|
||||||
|
updateEdges
|
||||||
|
} from '@/lib/graphHelper'
|
||||||
|
import Graph from 'graphology'
|
||||||
|
import { circular, random, circlepack } from 'graphology-layout'
|
||||||
|
import Sigma from 'sigma'
|
||||||
|
import seedrandom from 'seedrandom'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
GraphEditorControls: applyPureReactInVue(GraphEditorControls),
|
||||||
|
PanelMenuWrapper: applyPureReactInVue(PanelMenuWrapper),
|
||||||
|
Panel: applyPureReactInVue(Panel),
|
||||||
|
PanelSection: applyPureReactInVue(Section),
|
||||||
|
Dropdown: applyPureReactInVue(Dropdown),
|
||||||
|
NumericInput: applyPureReactInVue(NumericInput),
|
||||||
|
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
||||||
|
Field: applyPureReactInVue(Field),
|
||||||
|
Fold: applyPureReactInVue(Fold),
|
||||||
|
ColorscalePicker: applyPureReactInVue(ColorscalePicker),
|
||||||
|
ColorPicker: applyPureReactInVue(ColorPicker),
|
||||||
|
Button: applyPureReactInVue(Button),
|
||||||
|
RunIcon,
|
||||||
|
StopIcon,
|
||||||
|
RandomLayoutSettings,
|
||||||
|
CirclePackLayoutSettings
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
dataSources: Object
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
graph: new Graph(),
|
||||||
|
renderer: null,
|
||||||
|
fa2Layout: null,
|
||||||
|
fa2Running: false,
|
||||||
|
nodeSizeTypeOptions: markRaw([
|
||||||
|
{ label: 'Constant', value: 'constant' },
|
||||||
|
{ label: 'Variable', value: 'variable' },
|
||||||
|
{ label: 'Calculated', value: 'calculated' }
|
||||||
|
]),
|
||||||
|
edgeSizeTypeOptions: markRaw([
|
||||||
|
{ label: 'Constant', value: 'constant' },
|
||||||
|
{ label: 'Variable', value: 'variable' }
|
||||||
|
]),
|
||||||
|
nodeCalculatedSizeMethodOptions: markRaw([
|
||||||
|
{ label: 'Degree', value: 'degree' },
|
||||||
|
{ label: 'In degree', value: 'inDegree' },
|
||||||
|
{ label: 'Out degree', value: 'outDegree' }
|
||||||
|
]),
|
||||||
|
nodeSizeModeOptions: markRaw([
|
||||||
|
{ label: 'Area', value: 'area' },
|
||||||
|
{ label: 'Diameter', value: 'diameter' }
|
||||||
|
]),
|
||||||
|
nodeColorTypeOptions: markRaw([
|
||||||
|
{ label: 'Constant', value: 'constant' },
|
||||||
|
{ label: 'Variable', value: 'variable' },
|
||||||
|
{ label: 'Calculated', value: 'calculated' }
|
||||||
|
]),
|
||||||
|
edgeColorTypeOptions: markRaw([
|
||||||
|
{ label: 'Constant', value: 'constant' },
|
||||||
|
{ label: 'Variable', value: 'variable' }
|
||||||
|
]),
|
||||||
|
nodeCalculatedColorMethodOptions: markRaw([
|
||||||
|
{ label: 'Degree', value: 'degree' },
|
||||||
|
{ label: 'In degree', value: 'inDegree' },
|
||||||
|
{ label: 'Out degree', value: 'outDegree' }
|
||||||
|
]),
|
||||||
|
сolorAsOptions: markRaw([
|
||||||
|
{ label: 'Continious', value: 'continious' },
|
||||||
|
{ label: 'Categorical', value: 'categorical' }
|
||||||
|
]),
|
||||||
|
сolorscaleDirections: markRaw([
|
||||||
|
{ label: 'Normal', value: 'normal' },
|
||||||
|
{ label: 'Recersed', value: 'reversed' }
|
||||||
|
]),
|
||||||
|
colorSourceUsageOptions: markRaw([
|
||||||
|
{ label: 'Direct', value: 'direct' },
|
||||||
|
{ label: 'Map to', value: 'map_to' }
|
||||||
|
]),
|
||||||
|
visibilityOptions: markRaw([
|
||||||
|
{ label: 'Show', value: true },
|
||||||
|
{ label: 'Hide', value: false }
|
||||||
|
]),
|
||||||
|
layoutOptions: markRaw([
|
||||||
|
{ label: 'Circular', value: 'circular' },
|
||||||
|
{ label: 'Random', value: 'random' },
|
||||||
|
{ label: 'Circle pack', value: 'circlepack' },
|
||||||
|
{ label: 'ForceAtlas2', value: 'forceAtlas2' }
|
||||||
|
]),
|
||||||
|
layoutSettingsComponentMap: markRaw({
|
||||||
|
random: RandomLayoutSettings,
|
||||||
|
circlepack: CirclePackLayoutSettings,
|
||||||
|
forceAtlas2: ForceAtlasLayoutSettings
|
||||||
|
}),
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
structure: {
|
||||||
|
nodeId: null,
|
||||||
|
objectType: null,
|
||||||
|
edgeSource: null,
|
||||||
|
edgeTarget: null
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
nodes: {
|
||||||
|
size: {
|
||||||
|
type: 'constant',
|
||||||
|
value: 16,
|
||||||
|
source: null,
|
||||||
|
scale: 1,
|
||||||
|
mode: 'diameter',
|
||||||
|
method: 'degree',
|
||||||
|
min: 0
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: 'constant',
|
||||||
|
value: '#1F77B4',
|
||||||
|
source: null,
|
||||||
|
sourceUsage: 'map_to',
|
||||||
|
colorscale: null,
|
||||||
|
colorscaleDirection: 'normal',
|
||||||
|
method: 'degree',
|
||||||
|
mode: 'continious'
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
source: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
edges: {
|
||||||
|
showDirection: true,
|
||||||
|
size: {
|
||||||
|
type: 'constant',
|
||||||
|
value: 2,
|
||||||
|
source: null,
|
||||||
|
scale: 1,
|
||||||
|
min: 0
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: 'constant',
|
||||||
|
value: '#a2b1c6',
|
||||||
|
source: null,
|
||||||
|
sourceUsage: 'map_to',
|
||||||
|
colorscale: null,
|
||||||
|
colorscaleDirection: 'normal',
|
||||||
|
mode: 'continious'
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
source: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: 'circular',
|
||||||
|
options: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layoutOptionsArchive: {
|
||||||
|
random: null,
|
||||||
|
circlepack: null,
|
||||||
|
forceAtlas2: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
records() {
|
||||||
|
if (!this.dataSources) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return this.dataSources[Object.keys(this.dataSources)[0] || 'doc'].map(
|
||||||
|
json => JSON.parse(json)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keysOptions() {
|
||||||
|
if (!this.dataSources) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const keySet = this.records.reduce((result, currentRecord) => {
|
||||||
|
Object.keys(currentRecord).forEach(key => result.add(key))
|
||||||
|
return result
|
||||||
|
}, new Set())
|
||||||
|
|
||||||
|
return Array.from(keySet)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
dataSources() {
|
||||||
|
if (this.dataSources) {
|
||||||
|
this.buildGraph()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'settings.structure': {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
this.buildGraph()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
buildGraph() {
|
||||||
|
if (this.renderer) {
|
||||||
|
this.renderer.kill()
|
||||||
|
}
|
||||||
|
this.graph.clear()
|
||||||
|
|
||||||
|
buildNodes(this.graph, this.dataSources, this.settings)
|
||||||
|
buildEdges(this.graph, this.dataSources, this.settings)
|
||||||
|
|
||||||
|
// Apply visual settings
|
||||||
|
updateNodes(this.graph, this.settings.style.nodes)
|
||||||
|
updateEdges(this.graph, this.settings.style.edges)
|
||||||
|
|
||||||
|
circular.assign(this.graph)
|
||||||
|
this.renderer = new Sigma(this.graph, this.$refs.graph, {
|
||||||
|
renderEdgeLabels: true,
|
||||||
|
allowInvalidContainer: true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateStructure(attributeName, value) {
|
||||||
|
this.settings.structure[attributeName] = value
|
||||||
|
},
|
||||||
|
updateNodes(attributeName, value) {
|
||||||
|
const attributePath = attributeName.split('.')
|
||||||
|
attributePath.reduce((result, current, index) => {
|
||||||
|
if (index === attributePath.length - 1) {
|
||||||
|
return (result[current] = value)
|
||||||
|
} else {
|
||||||
|
return result[current]
|
||||||
|
}
|
||||||
|
}, this.settings.style.nodes)
|
||||||
|
|
||||||
|
updateNodes(this.graph, {
|
||||||
|
[attributePath[0]]: this.settings.style.nodes[attributePath[0]]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateEdges(attributeName, value) {
|
||||||
|
const attributePath = attributeName.split('.')
|
||||||
|
attributePath.reduce((result, current, index) => {
|
||||||
|
if (index === attributePath.length - 1) {
|
||||||
|
return (result[current] = value)
|
||||||
|
} else {
|
||||||
|
return result[current]
|
||||||
|
}
|
||||||
|
}, this.settings.style.edges)
|
||||||
|
|
||||||
|
updateEdges(this.graph, {
|
||||||
|
[attributePath[0]]: this.settings.style.edges[attributePath[0]]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateLayout(layoutType) {
|
||||||
|
if (layoutType !== this.settings.layout.type) {
|
||||||
|
const prevLayout = this.settings.layout.type
|
||||||
|
this.layoutOptionsArchive[prevLayout] = this.settings.layout.options
|
||||||
|
this.settings.layout.options = this.layoutOptionsArchive[layoutType]
|
||||||
|
|
||||||
|
if (layoutType === 'forceAtlas2' && !this.settings.layout.options) {
|
||||||
|
const sensibleSettings = forceAtlas2.inferSettings(this.graph)
|
||||||
|
this.settings.layout.options = {
|
||||||
|
adjustSizes: false,
|
||||||
|
barnesHutOptimize: false,
|
||||||
|
barnesHutTheta: 0.5,
|
||||||
|
edgeWeightInfluence: 1,
|
||||||
|
gravity: 1,
|
||||||
|
linLogMode: false,
|
||||||
|
outboundAttractionDistribution: false,
|
||||||
|
scalingRatio: 1,
|
||||||
|
slowDown: 1,
|
||||||
|
strongGravityMode: false,
|
||||||
|
...sensibleSettings
|
||||||
|
}
|
||||||
|
} else if (layoutType === 'random' && !this.settings.layout.options) {
|
||||||
|
this.settings.layout.options = {
|
||||||
|
seedValue: 1
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
layoutType === 'circlepack' &&
|
||||||
|
!this.settings.layout.options
|
||||||
|
) {
|
||||||
|
this.settings.layout.options = {
|
||||||
|
seedValue: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.settings.layout.type = layoutType
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutType === 'circular') {
|
||||||
|
circular.assign(this.graph)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutType === 'random') {
|
||||||
|
random.assign(this.graph, {
|
||||||
|
rng: seedrandom(this.settings.layout.options.seedValue || 1)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutType === 'circlepack') {
|
||||||
|
this.graph.forEachNode(nodeId => {
|
||||||
|
this.graph.updateNode(nodeId, attributes => {
|
||||||
|
const newAttributes = { ...attributes }
|
||||||
|
// Delete old hierarchy attributes
|
||||||
|
Object.keys(newAttributes)
|
||||||
|
.filter(key => key.startsWith('hierarchyAttribute'))
|
||||||
|
.forEach(
|
||||||
|
hierarchyAttributeKey =>
|
||||||
|
delete newAttributes[hierarchyAttributeKey]
|
||||||
|
)
|
||||||
|
// Set new hierarchy attributes
|
||||||
|
this.settings.layout.options.hierarchyAttributes?.forEach(
|
||||||
|
(hierarchyAttribute, index) => {
|
||||||
|
newAttributes['hierarchyAttribute' + index] =
|
||||||
|
attributes.data[hierarchyAttribute]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return newAttributes
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
circlepack.assign(this.graph, {
|
||||||
|
hierarchyAttributes:
|
||||||
|
this.settings.layout.options.hierarchyAttributes?.map(
|
||||||
|
(_, index) => 'hierarchyAttribute' + index
|
||||||
|
) || [],
|
||||||
|
rng: seedrandom(this.settings.layout.options.seedValue || 1)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutType === 'forceAtlas2') {
|
||||||
|
if (this.fa2Layout) {
|
||||||
|
this.fa2Layout.kill()
|
||||||
|
}
|
||||||
|
this.fa2Layout = markRaw(
|
||||||
|
new FA2Layout(this.graph, {
|
||||||
|
getEdgeWeight: (_, attr) =>
|
||||||
|
attr.data[this.settings.layout.options.weightSource || 'weight'],
|
||||||
|
settings: this.settings.layout.options
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleFA2Layout() {
|
||||||
|
if (this.fa2Layout.isRunning()) {
|
||||||
|
this.fa2Running = false
|
||||||
|
this.fa2Layout.stop()
|
||||||
|
} else {
|
||||||
|
this.fa2Running = true
|
||||||
|
this.fa2Layout.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.plotly_editor > div {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.customPickerContainer) {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="graphContainer" class="chart-container">
|
||||||
|
<div v-show="!dataSources" class="warning chart-warning">
|
||||||
|
There is no data to build a graph. Run your SQL query and make sure the
|
||||||
|
result is not empty.
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="graph"
|
||||||
|
:style="{
|
||||||
|
height: !dataSources ? 'calc(100% - 40px)' : '100%',
|
||||||
|
'background-color': 'white'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<GraphEditor :dataSources="dataSources" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
|
import fIo from '@/lib/utils/fileIo'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
|
import GraphEditor from './GraphEditor.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Graph',
|
||||||
|
components: { GraphEditor },
|
||||||
|
props: {
|
||||||
|
dataSources: Object,
|
||||||
|
initOptions: Object,
|
||||||
|
importToPngEnabled: Boolean,
|
||||||
|
importToSvgEnabled: Boolean
|
||||||
|
},
|
||||||
|
emits: ['update:importToSvgEnabled', 'update', 'loadingImageCompleted'],
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.$emit('update:importToSvgEnabled', true)
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
methods: {
|
||||||
|
getOptionsForSave() {},
|
||||||
|
async saveAsPng() {
|
||||||
|
const url = await this.prepareCopy()
|
||||||
|
this.$emit('loadingImageCompleted')
|
||||||
|
fIo.downloadFromUrl(url, 'chart')
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveAsSvg() {
|
||||||
|
const url = await this.prepareCopy('svg')
|
||||||
|
fIo.downloadFromUrl(url, 'chart')
|
||||||
|
},
|
||||||
|
|
||||||
|
saveAsHtml() {},
|
||||||
|
async prepareCopy(type = 'png') {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chart-container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-warning {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
min-height: 242px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.editor_controls .sidebar__item:before) {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
<template>
|
||||||
|
<div class="pivot-ui">
|
||||||
|
<div :class="{ collapsed }">
|
||||||
|
<div class="row">
|
||||||
|
<label>Columns</label>
|
||||||
|
<multiselect
|
||||||
|
v-model="cols"
|
||||||
|
class="sqliteviz-select cols"
|
||||||
|
:options="colsToSelect"
|
||||||
|
:disabled="colsToSelect.length === 0"
|
||||||
|
:multiple="true"
|
||||||
|
:hideSelected="true"
|
||||||
|
:closeOnSelect="true"
|
||||||
|
:showLabels="false"
|
||||||
|
:max="colsToSelect.length"
|
||||||
|
openDirection="bottom"
|
||||||
|
placeholder=""
|
||||||
|
>
|
||||||
|
<template #maxElements>
|
||||||
|
<span class="no-results">No Results</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #placeholder>Choose columns</template>
|
||||||
|
|
||||||
|
<template #noResult>
|
||||||
|
<span class="no-results">No Results</span>
|
||||||
|
</template>
|
||||||
|
</multiselect>
|
||||||
|
<pivot-sort-btn v-model="colOrder" class="sort-btn" direction="col" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>Rows</label>
|
||||||
|
<multiselect
|
||||||
|
v-model="rows"
|
||||||
|
class="sqliteviz-select rows"
|
||||||
|
:options="rowsToSelect"
|
||||||
|
:disabled="rowsToSelect.length === 0"
|
||||||
|
:multiple="true"
|
||||||
|
:hideSelected="true"
|
||||||
|
:closeOnSelect="true"
|
||||||
|
:showLabels="false"
|
||||||
|
:max="rowsToSelect.length"
|
||||||
|
:optionHeight="29"
|
||||||
|
openDirection="bottom"
|
||||||
|
placeholder=""
|
||||||
|
>
|
||||||
|
<template #maxElements>
|
||||||
|
<span class="no-results">No Results</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #placeholder>Choose rows</template>
|
||||||
|
|
||||||
|
<template #noResult>
|
||||||
|
<span class="no-results">No Results</span>
|
||||||
|
</template>
|
||||||
|
</multiselect>
|
||||||
|
<pivot-sort-btn v-model="rowOrder" class="sort-btn" direction="row" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row aggregator">
|
||||||
|
<label>Aggregator</label>
|
||||||
|
<multiselect
|
||||||
|
v-model="aggregator"
|
||||||
|
class="sqliteviz-select short aggregator"
|
||||||
|
:options="aggregators"
|
||||||
|
label="name"
|
||||||
|
trackBy="name"
|
||||||
|
:closeOnSelect="true"
|
||||||
|
:showLabels="false"
|
||||||
|
:hideSelected="true"
|
||||||
|
:optionHeight="29"
|
||||||
|
openDirection="bottom"
|
||||||
|
placeholder="Choose a function"
|
||||||
|
>
|
||||||
|
<template #noResult>
|
||||||
|
<span class="no-results">No Results</span>
|
||||||
|
</template>
|
||||||
|
</multiselect>
|
||||||
|
|
||||||
|
<multiselect
|
||||||
|
v-show="valCount > 0"
|
||||||
|
v-model="val1"
|
||||||
|
class="sqliteviz-select aggr-arg"
|
||||||
|
:options="keyNames"
|
||||||
|
:disabled="keyNames.length === 0"
|
||||||
|
:closeOnSelect="true"
|
||||||
|
:showLabels="false"
|
||||||
|
:hideSelected="true"
|
||||||
|
:optionHeight="29"
|
||||||
|
openDirection="bottom"
|
||||||
|
placeholder="Choose an argument"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<multiselect
|
||||||
|
v-show="valCount > 1"
|
||||||
|
v-model="val2"
|
||||||
|
class="sqliteviz-select aggr-arg"
|
||||||
|
:options="keyNames"
|
||||||
|
:disabled="keyNames.length === 0"
|
||||||
|
:closeOnSelect="true"
|
||||||
|
:showLabels="false"
|
||||||
|
:hideSelected="true"
|
||||||
|
:optionHeight="29"
|
||||||
|
openDirection="bottom"
|
||||||
|
placeholder="Choose a second argument"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>View</label>
|
||||||
|
<multiselect
|
||||||
|
v-model="renderer"
|
||||||
|
class="sqliteviz-select short renderer"
|
||||||
|
:options="renderers"
|
||||||
|
label="name"
|
||||||
|
trackBy="name"
|
||||||
|
:closeOnSelect="true"
|
||||||
|
:allowEmpty="false"
|
||||||
|
:showLabels="false"
|
||||||
|
:hideSelected="true"
|
||||||
|
:optionHeight="29"
|
||||||
|
openDirection="bottom"
|
||||||
|
placeholder="Choose a view"
|
||||||
|
>
|
||||||
|
<template #noResult>
|
||||||
|
<span class="no-results">No Results</span>
|
||||||
|
</template>
|
||||||
|
</multiselect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="switcher" @click="collapsed = !collapsed">
|
||||||
|
{{ collapsed ? 'Show pivot settings' : 'Hide pivot settings' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import $ from 'jquery'
|
||||||
|
import Multiselect from 'vue-multiselect'
|
||||||
|
import PivotSortBtn from './PivotSortBtn'
|
||||||
|
import {
|
||||||
|
renderers,
|
||||||
|
aggregators,
|
||||||
|
zeroValAggregators,
|
||||||
|
twoValAggregators
|
||||||
|
} from '../pivotHelper'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PivotUi',
|
||||||
|
components: {
|
||||||
|
Multiselect,
|
||||||
|
PivotSortBtn
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
keyNames: Array,
|
||||||
|
modelValue: Object
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue', 'update'],
|
||||||
|
data() {
|
||||||
|
const aggregatorName =
|
||||||
|
(this.modelValue && this.modelValue.aggregatorName) || 'Count'
|
||||||
|
const rendererName =
|
||||||
|
(this.modelValue && this.modelValue.rendererName) || 'Table'
|
||||||
|
return {
|
||||||
|
collapsed: false,
|
||||||
|
renderer: {
|
||||||
|
name: rendererName,
|
||||||
|
fun: $.pivotUtilities.renderers[rendererName]
|
||||||
|
},
|
||||||
|
aggregator: {
|
||||||
|
name: aggregatorName,
|
||||||
|
fun: $.pivotUtilities.aggregators[aggregatorName]
|
||||||
|
},
|
||||||
|
rows: (this.modelValue && this.modelValue.rows) || [],
|
||||||
|
cols: (this.modelValue && this.modelValue.cols) || [],
|
||||||
|
val1:
|
||||||
|
(this.modelValue && this.modelValue.vals && this.modelValue.vals[0]) ||
|
||||||
|
'',
|
||||||
|
val2:
|
||||||
|
(this.modelValue && this.modelValue.vals && this.modelValue.vals[1]) ||
|
||||||
|
'',
|
||||||
|
colOrder: (this.modelValue && this.modelValue.colOrder) || 'key_a_to_z',
|
||||||
|
rowOrder: (this.modelValue && this.modelValue.rowOrder) || 'key_a_to_z'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
valCount() {
|
||||||
|
if (zeroValAggregators.includes(this.aggregator.name)) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (twoValAggregators.includes(this.aggregator.name)) {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1
|
||||||
|
},
|
||||||
|
renderers() {
|
||||||
|
return renderers
|
||||||
|
},
|
||||||
|
aggregators() {
|
||||||
|
return aggregators
|
||||||
|
},
|
||||||
|
rowsToSelect() {
|
||||||
|
return this.keyNames.filter(key => !this.cols.includes(key))
|
||||||
|
},
|
||||||
|
colsToSelect() {
|
||||||
|
return this.keyNames.filter(key => !this.rows.includes(key))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
renderer() {
|
||||||
|
this.returnValue()
|
||||||
|
},
|
||||||
|
aggregator() {
|
||||||
|
this.returnValue()
|
||||||
|
},
|
||||||
|
rows() {
|
||||||
|
this.returnValue()
|
||||||
|
},
|
||||||
|
cols() {
|
||||||
|
this.returnValue()
|
||||||
|
},
|
||||||
|
val1() {
|
||||||
|
this.returnValue()
|
||||||
|
},
|
||||||
|
val2() {
|
||||||
|
this.returnValue()
|
||||||
|
},
|
||||||
|
colOrder() {
|
||||||
|
this.returnValue()
|
||||||
|
},
|
||||||
|
rowOrder() {
|
||||||
|
this.returnValue()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
returnValue() {
|
||||||
|
const vals = []
|
||||||
|
for (let i = 1; i <= this.valCount; i++) {
|
||||||
|
vals.push(this[`val${i}`])
|
||||||
|
}
|
||||||
|
this.$emit('update')
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
rows: this.rows,
|
||||||
|
cols: this.cols,
|
||||||
|
colOrder: this.colOrder,
|
||||||
|
rowOrder: this.rowOrder,
|
||||||
|
aggregator: this.aggregator.fun(vals),
|
||||||
|
aggregatorName: this.aggregator.name,
|
||||||
|
renderer: this.renderer.fun,
|
||||||
|
rendererName: this.renderer.name,
|
||||||
|
vals
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.pivot-ui {
|
||||||
|
padding: 12px 24px;
|
||||||
|
color: var(--color-text-base);
|
||||||
|
font-size: 12px;
|
||||||
|
border-bottom: 1px solid var(--color-border-light);
|
||||||
|
background-color: var(--color-bg-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pivot-ui .row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pivot-ui .row label {
|
||||||
|
width: 76px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pivot-ui .row .sqliteviz-select.short {
|
||||||
|
width: 220px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pivot-ui .row .aggr-arg {
|
||||||
|
margin-left: 12px;
|
||||||
|
max-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pivot-ui .row .sort-btn {
|
||||||
|
margin-left: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.collapsed {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switcher {
|
||||||
|
display: block;
|
||||||
|
width: min-content;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switcher:hover {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
result is not empty.
|
result is not empty.
|
||||||
</div>
|
</div>
|
||||||
<pivot-ui
|
<pivot-ui
|
||||||
v-show="showViewSettings"
|
|
||||||
v-model="pivotOptions"
|
v-model="pivotOptions"
|
||||||
:keyNames="columns"
|
:keyNames="columns"
|
||||||
@update="$emit('update')"
|
@update="$emit('update')"
|
||||||
@@ -31,9 +30,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/index.vue'
|
import PivotUi from './PivotUi'
|
||||||
import pivotHelper from './pivotHelper'
|
import pivotHelper from './pivotHelper'
|
||||||
import Chart from '@/components/Chart'
|
import Chart from '@/views/MainView/Workspace/Tabs/Tab/DataView/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'
|
||||||
@@ -47,16 +46,14 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
dataSources: Object,
|
dataSources: Object,
|
||||||
initOptions: Object,
|
initOptions: Object,
|
||||||
exportToPngEnabled: Boolean,
|
importToPngEnabled: Boolean,
|
||||||
exportToSvgEnabled: Boolean,
|
importToSvgEnabled: Boolean
|
||||||
showViewSettings: Boolean
|
|
||||||
},
|
},
|
||||||
emits: [
|
emits: [
|
||||||
'loadingImageCompleted',
|
'loadingImageCompleted',
|
||||||
'update',
|
'update',
|
||||||
'update:exportToSvgEnabled',
|
'update:importToSvgEnabled',
|
||||||
'update:exportToPngEnabled',
|
'update:importToPngEnabled'
|
||||||
'update:exportToHtmlEnabled'
|
|
||||||
],
|
],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -113,11 +110,11 @@ export default {
|
|||||||
immediate: true,
|
immediate: true,
|
||||||
handler() {
|
handler() {
|
||||||
this.$emit(
|
this.$emit(
|
||||||
'update:exportToPngEnabled',
|
'update:importToPngEnabled',
|
||||||
this.pivotOptions.rendererName !== 'TSV Export'
|
this.pivotOptions.rendererName !== 'TSV Export'
|
||||||
)
|
)
|
||||||
this.$emit(
|
this.$emit(
|
||||||
'update:exportToSvgEnabled',
|
'update:importToSvgEnabled',
|
||||||
this.viewStandartChart || this.viewCustomChart
|
this.viewStandartChart || this.viewCustomChart
|
||||||
)
|
)
|
||||||
events.send('viz_pivot.render', null, {
|
events.send('viz_pivot.render', null, {
|
||||||
@@ -127,14 +124,8 @@ export default {
|
|||||||
},
|
},
|
||||||
pivotOptions() {
|
pivotOptions() {
|
||||||
this.show()
|
this.show()
|
||||||
},
|
|
||||||
showViewSettings() {
|
|
||||||
this.handleResize()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
|
||||||
this.$emit('update:exportToHtmlEnabled', true)
|
|
||||||
},
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.show()
|
this.show()
|
||||||
// We need to detect resizing because plotly doesn't resize when resize its container
|
// We need to detect resizing because plotly doesn't resize when resize its container
|
||||||
@@ -227,12 +218,12 @@ export default {
|
|||||||
|
|
||||||
async prepareCopy() {
|
async prepareCopy() {
|
||||||
if (this.viewCustomChart) {
|
if (this.viewCustomChart) {
|
||||||
return this.$refs.customChart.prepareCopy()
|
return await this.$refs.customChart.prepareCopy()
|
||||||
}
|
}
|
||||||
if (this.viewStandartChart) {
|
if (this.viewStandartChart) {
|
||||||
return chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
|
return await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
|
||||||
}
|
}
|
||||||
return pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
|
return await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveAsSvg() {
|
async saveAsSvg() {
|
||||||
@@ -326,8 +317,4 @@ export default {
|
|||||||
.pivot-output:empty {
|
.pivot-output:empty {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.js-plotly-plot) {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -4,21 +4,16 @@
|
|||||||
<component
|
<component
|
||||||
:is="mode"
|
:is="mode"
|
||||||
ref="viewComponent"
|
ref="viewComponent"
|
||||||
v-model:exportToPngEnabled="exportToPngEnabled"
|
v-model:importToPngEnabled="importToPngEnabled"
|
||||||
v-model:exportToSvgEnabled="exportToSvgEnabled"
|
v-model:importToSvgEnabled="importToSvgEnabled"
|
||||||
v-model:exportToHtmlEnabled="exportToHtmlEnabled"
|
:initOptions="mode === initMode ? initOptions : undefined"
|
||||||
v-model:exportToClipboardEnabled="exportToClipboardEnabled"
|
|
||||||
:initOptions="initOptionsByMode[mode]"
|
|
||||||
:data-sources="dataSource"
|
:data-sources="dataSource"
|
||||||
: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"
|
||||||
@@ -36,7 +31,6 @@
|
|||||||
<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"
|
||||||
@@ -48,31 +42,7 @@
|
|||||||
<div class="side-tool-bar-divider" />
|
<div class="side-tool-bar-divider" />
|
||||||
|
|
||||||
<icon-button
|
<icon-button
|
||||||
ref="settingsBtn"
|
:disabled="!importToPngEnabled || loadingImage"
|
||||||
:active="showViewSettings"
|
|
||||||
tooltip="Toggle visualisation settings visibility"
|
|
||||||
tooltipPosition="top-left"
|
|
||||||
@click="showViewSettings = !showViewSettings"
|
|
||||||
>
|
|
||||||
<settings-icon />
|
|
||||||
</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" />
|
|
||||||
|
|
||||||
<icon-button
|
|
||||||
ref="pngExportBtn"
|
|
||||||
:disabled="!exportToPngEnabled || loadingImage"
|
|
||||||
:loading="loadingImage"
|
:loading="loadingImage"
|
||||||
tooltip="Save as PNG image"
|
tooltip="Save as PNG image"
|
||||||
tooltipPosition="top-left"
|
tooltipPosition="top-left"
|
||||||
@@ -82,7 +52,7 @@
|
|||||||
</icon-button>
|
</icon-button>
|
||||||
<icon-button
|
<icon-button
|
||||||
ref="svgExportBtn"
|
ref="svgExportBtn"
|
||||||
:disabled="!exportToSvgEnabled"
|
:disabled="!importToSvgEnabled"
|
||||||
tooltip="Save as SVG"
|
tooltip="Save as SVG"
|
||||||
tooltipPosition="top-left"
|
tooltipPosition="top-left"
|
||||||
@click="saveAsSvg"
|
@click="saveAsSvg"
|
||||||
@@ -92,7 +62,6 @@
|
|||||||
|
|
||||||
<icon-button
|
<icon-button
|
||||||
ref="htmlExportBtn"
|
ref="htmlExportBtn"
|
||||||
:disabled="!exportToHtmlEnabled"
|
|
||||||
tooltip="Save as HTML"
|
tooltip="Save as HTML"
|
||||||
tooltipPosition="top-left"
|
tooltipPosition="top-left"
|
||||||
@click="saveAsHtml"
|
@click="saveAsHtml"
|
||||||
@@ -101,7 +70,6 @@
|
|||||||
</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"
|
||||||
@@ -112,10 +80,10 @@
|
|||||||
</side-tool-bar>
|
</side-tool-bar>
|
||||||
|
|
||||||
<loading-dialog
|
<loading-dialog
|
||||||
v-model="showLoadingDialog"
|
|
||||||
loadingMsg="Rendering the visualisation..."
|
loadingMsg="Rendering the visualisation..."
|
||||||
successMsg="Image is ready"
|
successMsg="Image is ready"
|
||||||
actionBtnName="Copy"
|
actionBtnName="Copy"
|
||||||
|
name="prepareCopy"
|
||||||
title="Copy to clipboard"
|
title="Copy to clipboard"
|
||||||
:loading="preparingCopy"
|
:loading="preparingCopy"
|
||||||
@action="copyToClipboard"
|
@action="copyToClipboard"
|
||||||
@@ -125,22 +93,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Chart from '@/components/Chart.vue'
|
import Chart from './Chart'
|
||||||
import Pivot from '@/components/Pivot'
|
import Pivot from './Pivot'
|
||||||
import Graph from '@/components/Graph/index.vue'
|
import Graph from './Graph'
|
||||||
import SideToolBar from '@/components/SideToolBar'
|
import SideToolBar from '../SideToolBar'
|
||||||
import IconButton from '@/components/Common/IconButton'
|
import IconButton from '@/components/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'
|
||||||
import SettingsIcon from '@/components/svg/settings.vue'
|
|
||||||
import HtmlIcon from '@/components/svg/html'
|
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/Common/LoadingDialog.vue'
|
import loadingDialog from '@/components/LoadingDialog'
|
||||||
import time from '@/lib/utils/time'
|
import time from '@/lib/utils/time'
|
||||||
import events from '@/lib/utils/events'
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
@@ -155,9 +121,7 @@ export default {
|
|||||||
ChartIcon,
|
ChartIcon,
|
||||||
PivotIcon,
|
PivotIcon,
|
||||||
GraphIcon,
|
GraphIcon,
|
||||||
SettingsIcon,
|
|
||||||
ExportToSvgIcon,
|
ExportToSvgIcon,
|
||||||
ViewCellValueIcon,
|
|
||||||
PngIcon,
|
PngIcon,
|
||||||
HtmlIcon,
|
HtmlIcon,
|
||||||
ClipboardIcon,
|
ClipboardIcon,
|
||||||
@@ -172,22 +136,12 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
mode: this.initMode || 'chart',
|
mode: this.initMode || 'chart',
|
||||||
exportToPngEnabled: true,
|
importToPngEnabled: true,
|
||||||
exportToSvgEnabled: true,
|
importToSvgEnabled: true,
|
||||||
exportToHtmlEnabled: true,
|
|
||||||
exportToClipboardEnabled: true,
|
|
||||||
loadingImage: false,
|
loadingImage: false,
|
||||||
copyingImage: false,
|
copyingImage: false,
|
||||||
preparingCopy: false,
|
preparingCopy: false,
|
||||||
dataToCopy: null,
|
dataToCopy: null
|
||||||
initOptionsByMode: {
|
|
||||||
chart: this.initMode === 'chart' ? this.initOptions : null,
|
|
||||||
pivot: this.initMode === 'pivot' ? this.initOptions : null,
|
|
||||||
graph: this.initMode === 'graph' ? this.initOptions : null
|
|
||||||
},
|
|
||||||
showLoadingDialog: false,
|
|
||||||
showViewSettings: true,
|
|
||||||
viewValuePanelVisible: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -196,11 +150,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
mode(newMode, oldMode) {
|
mode() {
|
||||||
this.$emit('update')
|
this.$emit('update')
|
||||||
this.exportToPngEnabled = true
|
this.importToPngEnabled = true
|
||||||
this.exportToClipboardEnabled = true
|
|
||||||
this.initOptionsByMode[oldMode] = this.getOptionsForSave()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -230,13 +182,14 @@ export default {
|
|||||||
async prepareCopy() {
|
async prepareCopy() {
|
||||||
if ('ClipboardItem' in window) {
|
if ('ClipboardItem' in window) {
|
||||||
this.preparingCopy = true
|
this.preparingCopy = true
|
||||||
this.showLoadingDialog = true
|
this.$modal.show('prepareCopy')
|
||||||
const t0 = performance.now()
|
const t0 = performance.now()
|
||||||
|
|
||||||
await time.sleep(0)
|
await time.sleep(0)
|
||||||
this.dataToCopy = await this.$refs.viewComponent.prepareCopy()
|
this.dataToCopy = await this.$refs.viewComponent.prepareCopy()
|
||||||
const t1 = performance.now()
|
const t1 = performance.now()
|
||||||
if (t1 - t0 < 950) {
|
if (t1 - t0 < 950) {
|
||||||
|
this.$modal.hide('prepareCopy')
|
||||||
this.copyToClipboard()
|
this.copyToClipboard()
|
||||||
} else {
|
} else {
|
||||||
this.preparingCopy = false
|
this.preparingCopy = false
|
||||||
@@ -249,13 +202,14 @@ export default {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
copyToClipboard() {
|
async copyToClipboard() {
|
||||||
cIo.copyImage(this.dataToCopy)
|
cIo.copyImage(this.dataToCopy)
|
||||||
this.showLoadingDialog = false
|
this.$modal.hide('prepareCopy')
|
||||||
this.exportSignal('clipboard')
|
this.exportSignal('clipboard')
|
||||||
},
|
},
|
||||||
cancelCopy() {
|
cancelCopy() {
|
||||||
this.dataToCopy = null
|
this.dataToCopy = null
|
||||||
|
this.$modal.hide('prepareCopy')
|
||||||
},
|
},
|
||||||
|
|
||||||
saveAsSvg() {
|
saveAsSvg() {
|
||||||
@@ -276,9 +230,7 @@ export default {
|
|||||||
events.send(
|
events.send(
|
||||||
this.mode === 'chart' || this.plotlyInPivot
|
this.mode === 'chart' || this.plotlyInPivot
|
||||||
? 'viz_plotly.export'
|
? 'viz_plotly.export'
|
||||||
: this.mode === 'graph'
|
: 'viz_pivot.export',
|
||||||
? 'viz_graph.export'
|
|
||||||
: 'viz_pivot.export',
|
|
||||||
null,
|
null,
|
||||||
eventLabels
|
eventLabels
|
||||||
)
|
)
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import IconButton from '@/components/Common/IconButton'
|
import IconButton from '@/components/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'
|
||||||
|
|
||||||
@@ -1,56 +1,48 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="value-viewer">
|
<div class="value-viewer">
|
||||||
<template v-if="!empty">
|
<div class="value-viewer-toolbar">
|
||||||
<div class="value-viewer-toolbar">
|
<button
|
||||||
<button
|
v-for="format in formats"
|
||||||
v-for="format in formats"
|
:key="format.value"
|
||||||
:key="format.value"
|
type="button"
|
||||||
type="button"
|
:aria-selected="currentFormat === format.value"
|
||||||
:aria-selected="currentFormat === format.value"
|
:class="format.value"
|
||||||
:class="format.value"
|
@click="currentFormat = format.value"
|
||||||
@click="currentFormat = format.value"
|
>
|
||||||
>
|
{{ format.text }}
|
||||||
{{ format.text }}
|
</button>
|
||||||
</button>
|
|
||||||
|
|
||||||
<button type="button" class="copy" @click="copyToClipboard">
|
<button type="button" class="copy" @click="copyToClipboard">Copy</button>
|
||||||
Copy
|
<button
|
||||||
</button>
|
type="button"
|
||||||
<button
|
class="line-wrap"
|
||||||
type="button"
|
:aria-selected="lineWrapping === true"
|
||||||
class="line-wrap"
|
@click="lineWrapping = !lineWrapping"
|
||||||
:aria-selected="lineWrapping === true"
|
>
|
||||||
@click="lineWrapping = !lineWrapping"
|
Line wrap
|
||||||
>
|
</button>
|
||||||
Line wrap
|
</div>
|
||||||
</button>
|
<div class="value-body">
|
||||||
</div>
|
<codemirror
|
||||||
<div class="value-body">
|
v-if="currentFormat === 'json' && formattedJson"
|
||||||
<codemirror
|
:value="formattedJson"
|
||||||
v-if="currentFormat === 'json' && formattedJson"
|
:options="cmOptions"
|
||||||
:value="formattedJson"
|
class="json-value original-style"
|
||||||
:options="cmOptions"
|
/>
|
||||||
class="json-value original-style"
|
<pre
|
||||||
/>
|
v-if="currentFormat === 'text'"
|
||||||
<pre
|
:class="[
|
||||||
v-if="currentFormat === 'text'"
|
'text-value',
|
||||||
:class="[
|
{ 'meta-value': isNull || isBlob },
|
||||||
'text-value',
|
{ 'line-wrap': lineWrapping }
|
||||||
{ 'meta-value': isNull || isBlob },
|
]"
|
||||||
{ 'line-wrap': lineWrapping }
|
>{{ cellText }}</pre
|
||||||
]"
|
>
|
||||||
>{{ cellText }}</pre
|
<logs
|
||||||
>
|
v-if="messages && messages.length > 0"
|
||||||
<logs
|
:messages="messages"
|
||||||
v-if="messages && messages.length > 0"
|
class="messages"
|
||||||
:messages="messages"
|
/>
|
||||||
class="messages"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div v-show="empty" class="empty-message">
|
|
||||||
{{ emptyMessage }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -65,22 +57,15 @@ 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/Common/Logs'
|
import Logs from '@/components/Logs'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ValueViewer',
|
|
||||||
components: {
|
components: {
|
||||||
Codemirror,
|
Codemirror,
|
||||||
Logs
|
Logs
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
value: [String, Number, Uint8Array],
|
cellValue: [String, Number, Uint8Array]
|
||||||
empty: Boolean,
|
|
||||||
emptyMessage: String,
|
|
||||||
defaultFormat: {
|
|
||||||
type: String,
|
|
||||||
default: 'text'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -88,7 +73,7 @@ export default {
|
|||||||
{ text: 'Text', value: 'text' },
|
{ text: 'Text', value: 'text' },
|
||||||
{ text: 'JSON', value: 'json' }
|
{ text: 'JSON', value: 'json' }
|
||||||
],
|
],
|
||||||
currentFormat: this.defaultFormat,
|
currentFormat: 'text',
|
||||||
lineWrapping: false,
|
lineWrapping: false,
|
||||||
formattedJson: '',
|
formattedJson: '',
|
||||||
messages: []
|
messages: []
|
||||||
@@ -109,13 +94,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
isBlob() {
|
isBlob() {
|
||||||
return this.value && ArrayBuffer.isView(this.value)
|
return this.cellValue && ArrayBuffer.isView(this.cellValue)
|
||||||
},
|
},
|
||||||
isNull() {
|
isNull() {
|
||||||
return this.value === null
|
return this.cellValue === null
|
||||||
},
|
},
|
||||||
cellText() {
|
cellText() {
|
||||||
const value = this.value
|
const value = this.cellValue
|
||||||
if (this.isNull) {
|
if (this.isNull) {
|
||||||
return 'NULL'
|
return 'NULL'
|
||||||
}
|
}
|
||||||
@@ -126,23 +111,17 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentFormat: {
|
currentFormat() {
|
||||||
immediate: true,
|
this.messages = []
|
||||||
handler() {
|
this.formattedJson = ''
|
||||||
this.messages = []
|
if (this.currentFormat === 'json') {
|
||||||
this.formattedJson = ''
|
this.formatJson(this.cellValue)
|
||||||
if (this.currentFormat === 'json') {
|
|
||||||
this.formatJson(this.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
value: {
|
cellValue() {
|
||||||
immediate: true,
|
this.messages = []
|
||||||
handler() {
|
if (this.currentFormat === 'json') {
|
||||||
this.messages = []
|
this.formatJson(this.cellValue)
|
||||||
if (this.currentFormat === 'json') {
|
|
||||||
this.formatJson(this.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -162,7 +141,7 @@ export default {
|
|||||||
},
|
},
|
||||||
copyToClipboard() {
|
copyToClipboard() {
|
||||||
cIo.copyText(
|
cIo.copyText(
|
||||||
this.currentFormat === 'json' ? this.formattedJson : this.value,
|
this.currentFormat === 'json' ? this.formattedJson : this.cellValue,
|
||||||
'The value is copied to clipboard.'
|
'The value is copied to clipboard.'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -174,10 +153,8 @@ 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;
|
||||||
@@ -242,14 +219,4 @@ 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>
|
||||||
@@ -1,66 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="runResultPanel" class="run-result-panel">
|
<div ref="runResultPanel" class="run-result-panel">
|
||||||
<splitpanes
|
<component
|
||||||
|
:is="viewValuePanelVisible ? 'splitpanes' : 'div'"
|
||||||
:before="{ size: 50, max: 100 }"
|
:before="{ size: 50, max: 100 }"
|
||||||
:after="{ size: 50, max: 100, hidden: !viewValuePanelVisible }"
|
:after="{ size: 50, max: 100 }"
|
||||||
: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 class="result-set-container">
|
<div
|
||||||
<div
|
:id="'run-result-left-pane-' + tab.id"
|
||||||
v-show="result === null && !isGettingResults && !error"
|
class="result-set-container"
|
||||||
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>
|
|
||||||
</template>
|
|
||||||
<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>
|
</template>
|
||||||
</splitpanes>
|
<div
|
||||||
|
: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>
|
||||||
|
</template>
|
||||||
|
</component>
|
||||||
|
|
||||||
<side-tool-bar panel="table" @switch-to="$emit('switchTo', $event)">
|
<side-tool-bar panel="table" @switch-to="$emit('switchTo', $event)">
|
||||||
<icon-button
|
<icon-button
|
||||||
@@ -106,36 +80,78 @@
|
|||||||
</side-tool-bar>
|
</side-tool-bar>
|
||||||
|
|
||||||
<loading-dialog
|
<loading-dialog
|
||||||
v-model="showLoadingDialog"
|
|
||||||
loadingMsg="Building CSV..."
|
loadingMsg="Building CSV..."
|
||||||
successMsg="CSV is ready"
|
successMsg="CSV is ready"
|
||||||
actionBtnName="Copy"
|
actionBtnName="Copy"
|
||||||
|
name="prepareCSVCopy"
|
||||||
title="Copy to clipboard"
|
title="Copy to clipboard"
|
||||||
:loading="preparingCopy"
|
:loading="preparingCopy"
|
||||||
@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/Common/Logs'
|
import Logs from '@/components/Logs'
|
||||||
import SqlTable from '@/components/SqlTable'
|
import SqlTable from '@/components/SqlTable/index.vue'
|
||||||
import LoadingIndicator from '@/components/Common/LoadingIndicator'
|
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||||
import SideToolBar from '@/components/SideToolBar'
|
import SideToolBar from '../SideToolBar'
|
||||||
import Splitpanes from '@/components/Common/Splitpanes'
|
import Splitpanes from '@/components/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/Common/IconButton'
|
import IconButton from '@/components/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/Common/LoadingDialog'
|
import loadingDialog from '@/components/LoadingDialog'
|
||||||
import events from '@/lib/utils/events'
|
import events from '@/lib/utils/events'
|
||||||
import ValueViewer from '@/components/ValueViewer'
|
import ValueViewer from './ValueViewer'
|
||||||
import Record from './Record/index.vue'
|
import Record from './Record/index.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -156,6 +172,7 @@ export default {
|
|||||||
Splitpanes
|
Splitpanes
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
tab: Object,
|
||||||
result: Object,
|
result: Object,
|
||||||
isGettingResults: Boolean,
|
isGettingResults: Boolean,
|
||||||
error: Object,
|
error: Object,
|
||||||
@@ -173,8 +190,21 @@ export default {
|
|||||||
viewRecord: false,
|
viewRecord: false,
|
||||||
defaultPage: 1,
|
defaultPage: 1,
|
||||||
defaultSelectedCell: null,
|
defaultSelectedCell: null,
|
||||||
enableTeleport: this.$store.state.isWorkspaceVisible,
|
enableTeleport: this.$store.state.isWorkspaceVisible
|
||||||
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: {
|
||||||
@@ -234,13 +264,14 @@ export default {
|
|||||||
|
|
||||||
if ('ClipboardItem' in window) {
|
if ('ClipboardItem' in window) {
|
||||||
this.preparingCopy = true
|
this.preparingCopy = true
|
||||||
this.showLoadingDialog = true
|
this.$modal.show('prepareCSVCopy')
|
||||||
const t0 = performance.now()
|
const t0 = performance.now()
|
||||||
|
|
||||||
await time.sleep(0)
|
await time.sleep(0)
|
||||||
this.dataToCopy = csv.serialize(this.result)
|
this.dataToCopy = csv.serialize(this.result)
|
||||||
const t1 = performance.now()
|
const t1 = performance.now()
|
||||||
if (t1 - t0 < 950) {
|
if (t1 - t0 < 950) {
|
||||||
|
this.$modal.hide('prepareCSVCopy')
|
||||||
this.copyToClipboard()
|
this.copyToClipboard()
|
||||||
} else {
|
} else {
|
||||||
this.preparingCopy = false
|
this.preparingCopy = false
|
||||||
@@ -256,11 +287,12 @@ export default {
|
|||||||
|
|
||||||
copyToClipboard() {
|
copyToClipboard() {
|
||||||
cIo.copyText(this.dataToCopy, 'CSV copied to clipboard successfully')
|
cIo.copyText(this.dataToCopy, 'CSV copied to clipboard successfully')
|
||||||
this.showLoadingDialog = false
|
this.$modal.hide('prepareCSVCopy')
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelCopy() {
|
cancelCopy() {
|
||||||
this.dataToCopy = null
|
this.dataToCopy = null
|
||||||
|
this.$modal.hide('prepareCSVCopy')
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleViewValuePanel() {
|
toggleViewValuePanel() {
|
||||||
@@ -301,12 +333,19 @@ 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;
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import IconButton from '@/components/Common/IconButton'
|
import IconButton from '@/components/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'
|
||||||
@@ -63,9 +63,7 @@ export default {
|
|||||||
border-left: 1px solid var(--color-border-light);
|
border-left: 1px solid var(--color-border-light);
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.side-tool-bar-divider {
|
.side-tool-bar-divider {
|
||||||
width: 26px;
|
width: 26px;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
@@ -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/Common/IconButton'
|
import IconButton from '@/components/IconButton'
|
||||||
import RunIcon from '@/components/svg/run'
|
import RunIcon from '@/components/svg/run'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -37,6 +37,7 @@
|
|||||||
: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"
|
||||||
@@ -63,11 +64,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Splitpanes from '@/components/Common/Splitpanes'
|
import Splitpanes from '@/components/Splitpanes'
|
||||||
import SqlEditor from '@/components/SqlEditor'
|
import SqlEditor from './SqlEditor'
|
||||||
import DataView from '@/components/DataView'
|
import DataView from './DataView'
|
||||||
import RunResult from '@/components/RunResult'
|
import RunResult from './RunResult'
|
||||||
import { nextTick, computed } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
|
|
||||||
import events from '@/lib/utils/events'
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
@@ -79,11 +80,6 @@ export default {
|
|||||||
RunResult,
|
RunResult,
|
||||||
Splitpanes
|
Splitpanes
|
||||||
},
|
},
|
||||||
provide() {
|
|
||||||
return {
|
|
||||||
tabLayout: computed(() => this.tab.layout)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
props: {
|
props: {
|
||||||
tab: Object
|
tab: Object
|
||||||
},
|
},
|
||||||
@@ -17,9 +17,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Splitpanes from '@/components/Common/Splitpanes'
|
import Splitpanes from '@/components/Splitpanes'
|
||||||
import Schema from '@/components/Schema'
|
import Schema from './Schema'
|
||||||
import Tabs from '@/components/Tabs'
|
import Tabs from './Tabs'
|
||||||
import events from '@/lib/utils/events'
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -42,8 +42,8 @@ export default {
|
|||||||
) {
|
) {
|
||||||
const stmt = [
|
const stmt = [
|
||||||
'/*',
|
'/*',
|
||||||
' * Your database is empty. In order to start building data visualisations',
|
' * Your database is empty. In order to start building charts',
|
||||||
' * you should create tables and insert data into them.',
|
' * you should create a table and insert data into it.',
|
||||||
' */',
|
' */',
|
||||||
'CREATE TABLE house',
|
'CREATE TABLE house',
|
||||||
'(',
|
'(',
|
||||||
@@ -54,20 +54,7 @@ export default {
|
|||||||
"('Gryffindor', 100),",
|
"('Gryffindor', 100),",
|
||||||
"('Hufflepuff', 90),",
|
"('Hufflepuff', 90),",
|
||||||
"('Ravenclaw', 95),",
|
"('Ravenclaw', 95),",
|
||||||
"('Slytherin', 80);",
|
"('Slytherin', 80);"
|
||||||
'',
|
|
||||||
'CREATE TABLE student',
|
|
||||||
'(',
|
|
||||||
' id INTEGER,',
|
|
||||||
' name TEXT,',
|
|
||||||
' house TEXT',
|
|
||||||
');',
|
|
||||||
'INSERT INTO student VALUES',
|
|
||||||
"(1, 'Harry Potter', 'Gryffindor'),",
|
|
||||||
"(2, 'Ron Weasley', 'Gryffindor'),",
|
|
||||||
"(3, 'Draco Malfoy', 'Slytherin'),",
|
|
||||||
"(4, 'Luna Lovegood', 'Ravenclaw'),",
|
|
||||||
"(5, 'Cedric Diggory', 'Hufflepuff');"
|
|
||||||
].join('\n')
|
].join('\n')
|
||||||
|
|
||||||
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MainMenu from '@/components/MainMenu'
|
import MainMenu from './MainMenu'
|
||||||
import '@/assets/styles/scrollbars.css'
|
import '@/assets/styles/scrollbars.css'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -1,22 +1,13 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import { shallowMount, mount } from '@vue/test-utils'
|
import { shallowMount } from '@vue/test-utils'
|
||||||
import { createStore } from 'vuex'
|
import { createStore } from 'vuex'
|
||||||
import App from '@/App.vue'
|
import App from '@/App'
|
||||||
import storedInquiries from '@/lib/storedInquiries'
|
import storedInquiries from '@/lib/storedInquiries'
|
||||||
import actions from '@/store/actions'
|
|
||||||
import mutations from '@/store/mutations'
|
import mutations from '@/store/mutations'
|
||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
|
||||||
import { routes } from '@/router'
|
|
||||||
|
|
||||||
describe('App.vue', () => {
|
describe('App.vue', () => {
|
||||||
let clock
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
clock = sinon.useFakeTimers()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
sinon.restore()
|
sinon.restore()
|
||||||
})
|
})
|
||||||
@@ -68,167 +59,4 @@ describe('App.vue', () => {
|
|||||||
{ id: 3, name: 'bar' }
|
{ id: 3, name: 'bar' }
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Updates store when inquirires change in local storage', async () => {
|
|
||||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([
|
|
||||||
{ id: 1, name: 'foo' },
|
|
||||||
{ id: 2, name: 'baz' },
|
|
||||||
{ id: 3, name: 'bar' }
|
|
||||||
])
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
predefinedInquiries: [],
|
|
||||||
inquiries: []
|
|
||||||
}
|
|
||||||
const store = createStore({ state, mutations })
|
|
||||||
shallowMount(App, {
|
|
||||||
global: { stubs: ['router-view'], plugins: [store] }
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(state.inquiries).to.eql([
|
|
||||||
{ id: 1, name: 'foo' },
|
|
||||||
{ id: 2, name: 'baz' },
|
|
||||||
{ id: 3, name: 'bar' }
|
|
||||||
])
|
|
||||||
|
|
||||||
storedInquiries.getStoredInquiries.returns([
|
|
||||||
{ id: 1, name: 'foo' },
|
|
||||||
{ id: 3, name: 'bar' }
|
|
||||||
])
|
|
||||||
window.dispatchEvent(new StorageEvent('storage', { key: 'myInquiries' }))
|
|
||||||
expect(state.inquiries).to.eql([
|
|
||||||
{ id: 1, name: 'foo' },
|
|
||||||
{ id: 3, name: 'bar' }
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Closes with saving and does not change the next tab', async () => {
|
|
||||||
const inquiries = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: {},
|
|
||||||
createdAt: '2020-11-07T20:57:04.492Z'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'bar',
|
|
||||||
query: 'SELECT * FROM bar',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: {},
|
|
||||||
createdAt: '2020-11-07T20:57:04.492Z'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns(inquiries)
|
|
||||||
const tab1 = {
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'select * from foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: {},
|
|
||||||
layout: {
|
|
||||||
sqlEditor: 'above',
|
|
||||||
table: 'bottom',
|
|
||||||
dataView: 'hidden'
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
columns: ['name', 'points'],
|
|
||||||
values: {
|
|
||||||
name: ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin'],
|
|
||||||
points: [100, 90, 95, 80]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isSaved: false
|
|
||||||
}
|
|
||||||
const tab2 = {
|
|
||||||
id: 2,
|
|
||||||
name: 'bar',
|
|
||||||
query: 'SELECT * FROM bar',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: {},
|
|
||||||
layout: {
|
|
||||||
sqlEditor: 'above',
|
|
||||||
table: 'hidden',
|
|
||||||
dataView: 'bottom'
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
columns: ['id'],
|
|
||||||
values: {
|
|
||||||
id: [1, 2, 3]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isSaved: true
|
|
||||||
}
|
|
||||||
// mock store state
|
|
||||||
const state = {
|
|
||||||
tabs: [tab1, tab2],
|
|
||||||
currentTabId: 1,
|
|
||||||
currentTab: tab1,
|
|
||||||
db: {},
|
|
||||||
inquiries
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = createStore({ state, mutations, actions })
|
|
||||||
const router = createRouter({
|
|
||||||
history: createWebHistory(),
|
|
||||||
routes: routes
|
|
||||||
})
|
|
||||||
router.push('/workspace')
|
|
||||||
|
|
||||||
// After this line, router is ready
|
|
||||||
await router.isReady()
|
|
||||||
|
|
||||||
const wrapper = mount(App, {
|
|
||||||
attachTo: document.body,
|
|
||||||
global: {
|
|
||||||
stubs: {
|
|
||||||
'router-link': true,
|
|
||||||
teleport: true,
|
|
||||||
transition: false,
|
|
||||||
schema: true,
|
|
||||||
AppDiagnosticInfo: true,
|
|
||||||
DataView: {
|
|
||||||
template: '<div></div>',
|
|
||||||
methods: { getOptionsForSave: sinon.stub() }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: [store, router]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// click on the close icon of the first tab
|
|
||||||
const firstTabCloseIcon = wrapper.findAll('.tab')[0].find('.close-icon')
|
|
||||||
await firstTabCloseIcon.trigger('click')
|
|
||||||
|
|
||||||
// find 'Save and close' in the dialog
|
|
||||||
const closeBtn = wrapper
|
|
||||||
.findAll('.dialog-buttons-container button')
|
|
||||||
.find(button => button.text() === 'Save and close')
|
|
||||||
|
|
||||||
// click 'Save and close' in the dialog
|
|
||||||
await closeBtn.trigger('click')
|
|
||||||
|
|
||||||
await nextTick()
|
|
||||||
await nextTick()
|
|
||||||
|
|
||||||
// check that tab is closed
|
|
||||||
expect(wrapper.findAllComponents({ name: 'Tab' })).to.have.lengthOf(1)
|
|
||||||
// check that the open tab didn't change
|
|
||||||
const firstTab = wrapper.findComponent({ name: 'Tab' })
|
|
||||||
expect(firstTab.props('tab').name).to.equal('bar')
|
|
||||||
expect(firstTab.props('tab').result).to.eql({
|
|
||||||
columns: ['id'],
|
|
||||||
values: {
|
|
||||||
id: [1, 2, 3]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
expect(firstTab.props('tab')).to.eql(tab2)
|
|
||||||
|
|
||||||
// check that the dialog is closed
|
|
||||||
await clock.tick(100)
|
|
||||||
await nextTick()
|
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
|
||||||
wrapper.unmount()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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/Common/CheckBox'
|
import CheckBox from '@/components/CheckBox'
|
||||||
|
|
||||||
describe('CheckBox', () => {
|
describe('CheckBox', () => {
|
||||||
it('unchecked by default', () => {
|
it('unchecked by default', () => {
|
||||||
@@ -1,825 +0,0 @@
|
|||||||
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
@@ -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/Common/LoadingIndicator'
|
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||||
|
|
||||||
describe('LoadingIndicator.vue', () => {
|
describe('LoadingIndicator.vue', () => {
|
||||||
it('Calculates animation class', async () => {
|
it('Calculates animation class', async () => {
|
||||||
@@ -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/Common/Logs'
|
import Logs from '@/components/Logs'
|
||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
|
|
||||||
let place
|
let place
|
||||||
@@ -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/Common/Splitpanes'
|
import Splitpanes from '@/components/Splitpanes'
|
||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
|
|
||||||
describe('Splitpanes.vue', () => {
|
describe('Splitpanes.vue', () => {
|
||||||
@@ -49,41 +49,6 @@ 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, {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import splitter from '@/components/Common/Splitpanes/splitter'
|
import splitter from '@/components/Splitpanes/splitter'
|
||||||
|
|
||||||
describe('splitter.js', () => {
|
describe('splitter.js', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -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/Common/Pager'
|
import Pager from '@/components/SqlTable/Pager'
|
||||||
|
|
||||||
describe('Pager.vue', () => {
|
describe('Pager.vue', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -413,7 +413,7 @@ describe('SQLite extensions', function () {
|
|||||||
WHERE ip.id <= p.id
|
WHERE ip.id <= p.id
|
||||||
) AS path
|
) AS path
|
||||||
FROM tmp, json_each(filename_array) AS p
|
FROM tmp, json_each(filename_array) AS p
|
||||||
WHERE p.key > 0 -- because the filenames start with the separator
|
WHERE p.id > 1 -- because the filenames start with the separator
|
||||||
`)
|
`)
|
||||||
expect(actual.values).to.eql({
|
expect(actual.values).to.eql({
|
||||||
path: [
|
path: [
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -39,101 +39,4 @@ 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'
|
|
||||||
}
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ describe('storedInquiries.js', () => {
|
|||||||
expect(inquiries).to.eql([])
|
expect(inquiries).to.eql([])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('getStoredInquiries migrates and returns inquiries of v1', () => {
|
it('getStoredInquiries migrate and returns inquiries of v1', () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'myQueries',
|
'myQueries',
|
||||||
JSON.stringify([
|
JSON.stringify([
|
||||||
@@ -55,114 +55,6 @@ 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)
|
||||||
@@ -179,7 +71,7 @@ describe('storedInquiries.js', () => {
|
|||||||
query: 'SELECT * from foo',
|
query: 'SELECT * from foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
createdAt: new Date(2021, 0, 1).toJSON(),
|
createdAt: new Date(2021, 0, 1),
|
||||||
isPredefined: true
|
isPredefined: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,8 +83,7 @@ describe('storedInquiries.js', () => {
|
|||||||
expect(copy).to.have.property('query').which.equal(base.query)
|
expect(copy).to.have.property('query').which.equal(base.query)
|
||||||
expect(copy).to.have.property('viewType').which.equal(base.viewType)
|
expect(copy).to.have.property('viewType').which.equal(base.viewType)
|
||||||
expect(copy).to.have.property('viewOptions').which.eql(base.viewOptions)
|
expect(copy).to.have.property('viewOptions').which.eql(base.viewOptions)
|
||||||
expect(copy).to.have.property('createdAt')
|
expect(copy).to.have.property('createdAt').which.within(now, nowPlusMinute)
|
||||||
expect(new Date(copy.createdAt)).within(now, nowPlusMinute)
|
|
||||||
expect(copy).to.not.have.property('isPredefined')
|
expect(copy).to.not.have.property('isPredefined')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -244,7 +135,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(3)
|
expect(parsedJson.version).to.equal(2)
|
||||||
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({
|
||||||
@@ -257,7 +148,7 @@ describe('storedInquiries.js', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('deserialiseInquiries migrates inquiries of v1', () => {
|
it('deserialiseInquiries migrates inquiries', () => {
|
||||||
const str = `[
|
const str = `[
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
@@ -296,110 +187,7 @@ describe('storedInquiries.js', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('deserialiseInquiries migrates inquiries of v2', () => {
|
it('deserialiseInquiries return array for one inquiry of v1', () => {
|
||||||
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,
|
||||||
@@ -426,7 +214,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": 3,
|
"version": 2,
|
||||||
"inquiries": [
|
"inquiries": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
@@ -486,7 +274,7 @@ describe('storedInquiries.js', () => {
|
|||||||
|
|
||||||
it('importInquiries', async () => {
|
it('importInquiries', async () => {
|
||||||
const str = `{
|
const str = `{
|
||||||
"version": 3,
|
"version": 2,
|
||||||
"inquiries": [{
|
"inquiries": [{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "foo",
|
"name": "foo",
|
||||||
@@ -511,7 +299,7 @@ describe('storedInquiries.js', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('readPredefinedInquiries v1', async () => {
|
it('readPredefinedInquiries old', async () => {
|
||||||
const str = `[
|
const str = `[
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
@@ -536,55 +324,18 @@ describe('storedInquiries.js', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('readPredefinedInquiries v2', async () => {
|
it('readPredefinedInquiries', 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)))
|
||||||
@@ -598,90 +349,6 @@ 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'
|
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ describe('tab.js', () => {
|
|||||||
query: undefined,
|
query: undefined,
|
||||||
viewOptions: undefined,
|
viewOptions: undefined,
|
||||||
isPredefined: undefined,
|
isPredefined: undefined,
|
||||||
updatedAt: undefined,
|
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
result: null,
|
result: null,
|
||||||
isGettingResults: false,
|
isGettingResults: false,
|
||||||
@@ -43,8 +42,7 @@ describe('tab.js', () => {
|
|||||||
viewType: 'pivot',
|
viewType: 'pivot',
|
||||||
viewOptions: 'this is view options object',
|
viewOptions: 'this is view options object',
|
||||||
name: 'Foo inquiry',
|
name: 'Foo inquiry',
|
||||||
createdAt: '2022-12-05T18:30:30',
|
createdAt: '2022-12-05T18:30:30'
|
||||||
updatedAt: '2022-12-06T18:30:30'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTab = new Tab(state, inquiry)
|
const newTab = new Tab(state, inquiry)
|
||||||
@@ -55,7 +53,6 @@ describe('tab.js', () => {
|
|||||||
query: 'SELECT * from foo',
|
query: 'SELECT * from foo',
|
||||||
viewOptions: 'this is view options object',
|
viewOptions: 'this is view options object',
|
||||||
isPredefined: undefined,
|
isPredefined: undefined,
|
||||||
updatedAt: '2022-12-06T18:30:30',
|
|
||||||
viewType: 'pivot',
|
viewType: 'pivot',
|
||||||
result: null,
|
result: null,
|
||||||
isGettingResults: false,
|
isGettingResults: false,
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ describe('actions', () => {
|
|||||||
tempName: 'Untitled',
|
tempName: 'Untitled',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: undefined,
|
viewOptions: undefined,
|
||||||
isSaved: false,
|
isSaved: false
|
||||||
updatedAt: undefined
|
|
||||||
})
|
})
|
||||||
expect(state.untitledLastIndex).to.equal(1)
|
expect(state.untitledLastIndex).to.equal(1)
|
||||||
|
|
||||||
@@ -31,8 +30,7 @@ describe('actions', () => {
|
|||||||
tempName: 'Untitled 1',
|
tempName: 'Untitled 1',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: undefined,
|
viewOptions: undefined,
|
||||||
isSaved: false,
|
isSaved: false
|
||||||
updatedAt: undefined
|
|
||||||
})
|
})
|
||||||
expect(state.untitledLastIndex).to.equal(2)
|
expect(state.untitledLastIndex).to.equal(2)
|
||||||
})
|
})
|
||||||
@@ -42,16 +40,16 @@ describe('actions', () => {
|
|||||||
tabs: [],
|
tabs: [],
|
||||||
untitledLastIndex: 0
|
untitledLastIndex: 0
|
||||||
}
|
}
|
||||||
const inquiry = {
|
const tab = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'test',
|
name: 'test',
|
||||||
query: 'SELECT * from foo',
|
query: 'SELECT * from foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: 'an object with view options',
|
viewOptions: 'an object with view options',
|
||||||
updatedAt: '2025-05-16T20:15:00Z'
|
isSaved: true
|
||||||
}
|
}
|
||||||
await addTab({ state }, inquiry)
|
await addTab({ state }, tab)
|
||||||
expect(state.tabs[0]).to.include(inquiry)
|
expect(state.tabs[0]).to.include(tab)
|
||||||
expect(state.untitledLastIndex).to.equal(0)
|
expect(state.untitledLastIndex).to.equal(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -168,26 +166,21 @@ describe('actions', () => {
|
|||||||
newName: 'foo'
|
newName: 'foo'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
expect(value.id).not.to.equal(tab.id)
|
expect(value.id).to.equal(tab.id)
|
||||||
expect(value.name).to.equal('foo')
|
expect(value.name).to.equal('foo')
|
||||||
expect(value.query).to.equal(tab.query)
|
expect(value.query).to.equal(tab.query)
|
||||||
expect(value.viewOptions).to.eql(['chart'])
|
expect(value.viewOptions).to.eql(['chart'])
|
||||||
expect(value).to.have.property('createdAt')
|
expect(value).to.have.property('createdAt').which.within(now, nowPlusMinute)
|
||||||
expect(new Date(value.createdAt)).within(now, nowPlusMinute)
|
|
||||||
expect(new Date(value.updatedAt)).within(now, nowPlusMinute)
|
|
||||||
expect(state.inquiries).to.eql([value])
|
expect(state.inquiries).to.eql([value])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('saveInquiry updates existing inquiry in the storage', async () => {
|
it('save updates existing inquiry in the storage', async () => {
|
||||||
const now = new Date()
|
|
||||||
const nowPlusMinute = new Date(now.getTime() + 60 * 1000)
|
|
||||||
const tab = {
|
const tab = {
|
||||||
id: 1,
|
id: 1,
|
||||||
query: 'select * from foo',
|
query: 'select * from foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
name: 'foo',
|
name: null,
|
||||||
updatedAt: '2025-05-16T20:15:00Z',
|
|
||||||
dataView: {
|
dataView: {
|
||||||
getOptionsForSave() {
|
getOptionsForSave() {
|
||||||
return ['chart']
|
return ['chart']
|
||||||
@@ -196,34 +189,34 @@ describe('actions', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
inquiries: [
|
inquiries: [],
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
query: 'select * from foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
name: 'foo',
|
|
||||||
createdAt: '2025-05-15T16:30:00Z',
|
|
||||||
updatedAt: '2025-05-16T20:15:00Z'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
tabs: [tab]
|
tabs: [tab]
|
||||||
}
|
}
|
||||||
|
|
||||||
tab.query = 'select * from bar'
|
const first = await saveInquiry(
|
||||||
await saveInquiry({ state }, { inquiryTab: tab, newName: '' })
|
{ state },
|
||||||
|
{
|
||||||
|
inquiryTab: tab,
|
||||||
|
newName: 'foo'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
tab.name = 'foo'
|
||||||
|
tab.query = 'select * from foo'
|
||||||
|
await saveInquiry({ state }, { inquiryTab: tab })
|
||||||
const inquiries = state.inquiries
|
const inquiries = state.inquiries
|
||||||
const updatedTab = inquiries[0]
|
const second = inquiries[0]
|
||||||
expect(inquiries).has.lengthOf(1)
|
expect(inquiries).has.lengthOf(1)
|
||||||
expect(updatedTab.id).to.equal(updatedTab.id)
|
expect(second.id).to.equal(first.id)
|
||||||
expect(updatedTab.name).to.equal(updatedTab.name)
|
expect(second.name).to.equal(first.name)
|
||||||
expect(updatedTab.query).to.equal(tab.query)
|
expect(second.query).to.equal(tab.query)
|
||||||
expect(updatedTab.viewOptions).to.eql(['chart'])
|
expect(second.viewOptions).to.eql(['chart'])
|
||||||
expect(updatedTab.createdAt).to.equal('2025-05-15T16:30:00Z')
|
expect(new Date(second.createdAt).getTime()).to.equal(
|
||||||
expect(new Date(updatedTab.updatedAt)).to.be.within(now, nowPlusMinute)
|
first.createdAt.getTime()
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("saveInquiry adds a new inquiry with new id if it's based on predefined inquiry", async () => {
|
it("save adds a new inquiry with new id if it's based on predefined inquiry", async () => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const nowPlusMinute = new Date(now.getTime() + 60 * 1000)
|
const nowPlusMinute = new Date(now.getTime() + 60 * 1000)
|
||||||
const tab = {
|
const tab = {
|
||||||
@@ -259,95 +252,6 @@ describe('actions', () => {
|
|||||||
expect(inquiries[0].name).to.equal('foo')
|
expect(inquiries[0].name).to.equal('foo')
|
||||||
expect(inquiries[0].query).to.equal(tab.query)
|
expect(inquiries[0].query).to.equal(tab.query)
|
||||||
expect(inquiries[0].viewOptions).to.eql(['chart'])
|
expect(inquiries[0].viewOptions).to.eql(['chart'])
|
||||||
expect(new Date(inquiries[0].updatedAt)).to.be.within(now, nowPlusMinute)
|
|
||||||
expect(new Date(inquiries[0].createdAt)).to.be.within(now, nowPlusMinute)
|
expect(new Date(inquiries[0].createdAt)).to.be.within(now, nowPlusMinute)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('saveInquiry adds new inquiry if newName is provided', async () => {
|
|
||||||
const now = new Date()
|
|
||||||
const nowPlusMinute = new Date(now.getTime() + 60 * 1000)
|
|
||||||
|
|
||||||
const tab = {
|
|
||||||
id: 1,
|
|
||||||
query: 'select * from foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
name: 'foo',
|
|
||||||
updatedAt: '2025-05-16T20:15:00Z',
|
|
||||||
dataView: {
|
|
||||||
getOptionsForSave() {
|
|
||||||
return ['chart']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const inquiry = {
|
|
||||||
id: 1,
|
|
||||||
query: 'select * from foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
name: 'foo',
|
|
||||||
createdAt: '2025-05-15T16:30:00Z',
|
|
||||||
updatedAt: '2025-05-16T20:15:00Z'
|
|
||||||
}
|
|
||||||
const state = {
|
|
||||||
inquiries: [inquiry],
|
|
||||||
tabs: [tab]
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = await saveInquiry(
|
|
||||||
{ state },
|
|
||||||
{
|
|
||||||
inquiryTab: tab,
|
|
||||||
newName: 'foo_new'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
expect(value.id).not.to.equal(tab.id)
|
|
||||||
expect(value.name).to.equal('foo_new')
|
|
||||||
expect(value.query).to.equal(tab.query)
|
|
||||||
expect(value.viewOptions).to.eql(['chart'])
|
|
||||||
expect(value).to.have.property('createdAt')
|
|
||||||
expect(new Date(value.createdAt)).within(now, nowPlusMinute)
|
|
||||||
expect(new Date(value.updatedAt)).within(now, nowPlusMinute)
|
|
||||||
expect(state.inquiries).to.eql([inquiry, value])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('saveInquiry adds new inquiry if the inquiry is not in the storeage anymore', async () => {
|
|
||||||
const now = new Date()
|
|
||||||
const nowPlusMinute = new Date(now.getTime() + 60 * 1000)
|
|
||||||
|
|
||||||
const tab = {
|
|
||||||
id: 1,
|
|
||||||
query: 'select * from foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
name: 'foo',
|
|
||||||
updatedAt: '2025-05-16T20:15:00Z',
|
|
||||||
dataView: {
|
|
||||||
getOptionsForSave() {
|
|
||||||
return ['chart']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
inquiries: [],
|
|
||||||
tabs: [tab]
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = await saveInquiry(
|
|
||||||
{ state },
|
|
||||||
{
|
|
||||||
inquiryTab: tab,
|
|
||||||
newName: ''
|
|
||||||
}
|
|
||||||
)
|
|
||||||
expect(value.id).to.equal(tab.id)
|
|
||||||
expect(value.name).to.equal('foo')
|
|
||||||
expect(value.query).to.equal(tab.query)
|
|
||||||
expect(value.viewOptions).to.eql(['chart'])
|
|
||||||
expect(value).to.have.property('createdAt')
|
|
||||||
expect(new Date(value.createdAt)).within(now, nowPlusMinute)
|
|
||||||
expect(new Date(value.updatedAt)).within(now, nowPlusMinute)
|
|
||||||
expect(state.inquiries).to.eql([value])
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ describe('mutations', () => {
|
|||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: { here_are: 'chart settings' },
|
viewOptions: { here_are: 'chart settings' },
|
||||||
isSaved: false,
|
isSaved: false,
|
||||||
isPredefined: false,
|
isPredefined: false
|
||||||
updatedAt: '2025-05-15T15:30:00Z'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newValues = {
|
const newValues = {
|
||||||
@@ -44,7 +43,6 @@ describe('mutations', () => {
|
|||||||
query: 'SELECT * from bar',
|
query: 'SELECT * from bar',
|
||||||
viewType: 'pivot',
|
viewType: 'pivot',
|
||||||
viewOptions: { here_are: 'pivot settings' },
|
viewOptions: { here_are: 'pivot settings' },
|
||||||
updatedAt: '2025-05-15T16:30:00Z',
|
|
||||||
isSaved: true
|
isSaved: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +58,6 @@ describe('mutations', () => {
|
|||||||
query: 'SELECT * from bar',
|
query: 'SELECT * from bar',
|
||||||
viewType: 'pivot',
|
viewType: 'pivot',
|
||||||
viewOptions: { here_are: 'pivot settings' },
|
viewOptions: { here_are: 'pivot settings' },
|
||||||
updatedAt: '2025-05-15T16:30:00Z',
|
|
||||||
isSaved: true
|
isSaved: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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/Inquiries'
|
import Inquiries from '@/views/MainView/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'
|
||||||
@@ -2,12 +2,10 @@ 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 '@/components/MainMenu'
|
import MainMenu from '@/views/MainView/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'
|
||||||
import actions from '@/store/actions'
|
|
||||||
import mutations from '@/store/mutations'
|
|
||||||
|
|
||||||
let wrapper = null
|
let wrapper = null
|
||||||
|
|
||||||
@@ -28,7 +26,7 @@ describe('MainMenu.vue', () => {
|
|||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Create, Save and Save as are visible only on /workspace page', async () => {
|
it('Create and Save are visible only on /workspace page', async () => {
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: { query: '', execute: sinon.stub() },
|
currentTab: { query: '', execute: sinon.stub() },
|
||||||
tabs: [{}],
|
tabs: [{}],
|
||||||
@@ -47,8 +45,6 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
expect(wrapper.find('#save-btn').exists()).to.equal(true)
|
expect(wrapper.find('#save-btn').exists()).to.equal(true)
|
||||||
expect(wrapper.find('#save-btn').isVisible()).to.equal(true)
|
expect(wrapper.find('#save-btn').isVisible()).to.equal(true)
|
||||||
expect(wrapper.find('#save-as-btn').exists()).to.equal(true)
|
|
||||||
expect(wrapper.find('#save-as-btn').isVisible()).to.equal(true)
|
|
||||||
expect(wrapper.find('#create-btn').exists()).to.equal(true)
|
expect(wrapper.find('#create-btn').exists()).to.equal(true)
|
||||||
expect(wrapper.find('#create-btn').isVisible()).to.equal(true)
|
expect(wrapper.find('#create-btn').isVisible()).to.equal(true)
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
@@ -69,7 +65,7 @@ describe('MainMenu.vue', () => {
|
|||||||
expect(wrapper.find('#create-btn').isVisible()).to.equal(true)
|
expect(wrapper.find('#create-btn').isVisible()).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Save and Save as are not visible if there is no tabs', () => {
|
it('Save is not visible if there is no tabs', () => {
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: null,
|
currentTab: null,
|
||||||
tabs: [],
|
tabs: [],
|
||||||
@@ -87,8 +83,6 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
expect(wrapper.find('#save-btn').exists()).to.equal(true)
|
expect(wrapper.find('#save-btn').exists()).to.equal(true)
|
||||||
expect(wrapper.find('#save-btn').isVisible()).to.equal(false)
|
expect(wrapper.find('#save-btn').isVisible()).to.equal(false)
|
||||||
expect(wrapper.find('#save-as-btn').exists()).to.equal(true)
|
|
||||||
expect(wrapper.find('#save-as-btn').isVisible()).to.equal(false)
|
|
||||||
expect(wrapper.find('#create-btn').exists()).to.equal(true)
|
expect(wrapper.find('#create-btn').exists()).to.equal(true)
|
||||||
expect(wrapper.find('#create-btn').isVisible()).to.equal(true)
|
expect(wrapper.find('#create-btn').isVisible()).to.equal(true)
|
||||||
})
|
})
|
||||||
@@ -117,12 +111,10 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
const vm = wrapper.vm
|
const vm = wrapper.vm
|
||||||
expect(wrapper.find('#save-btn').element.disabled).to.equal(false)
|
expect(wrapper.find('#save-btn').element.disabled).to.equal(false)
|
||||||
expect(wrapper.find('#save-as-btn').element.disabled).to.equal(false)
|
|
||||||
|
|
||||||
store.state.tabs[0].isSaved = true
|
store.state.tabs[0].isSaved = true
|
||||||
await vm.$nextTick()
|
await vm.$nextTick()
|
||||||
expect(wrapper.find('#save-btn').element.disabled).to.equal(true)
|
expect(wrapper.find('#save-btn').element.disabled).to.equal(true)
|
||||||
expect(wrapper.find('#save-as-btn').element.disabled).to.equal(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Creates a tab', async () => {
|
it('Creates a tab', async () => {
|
||||||
@@ -340,7 +332,7 @@ describe('MainMenu.vue', () => {
|
|||||||
expect(wrapper.vm.createNewInquiry.callCount).to.equal(4)
|
expect(wrapper.vm.createNewInquiry.callCount).to.equal(4)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Ctrl S calls onSave if the tab is unsaved and route path is /workspace', async () => {
|
it('Ctrl S calls checkInquiryBeforeSave if the tab is unsaved and route path is /workspace', async () => {
|
||||||
const tab = {
|
const tab = {
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
@@ -361,115 +353,42 @@ describe('MainMenu.vue', () => {
|
|||||||
plugins: [store]
|
plugins: [store]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
sinon.stub(wrapper.vm, 'onSave')
|
sinon.stub(wrapper.vm, 'checkInquiryBeforeSave')
|
||||||
|
|
||||||
const ctrlS = new KeyboardEvent('keydown', { key: 's', ctrlKey: true })
|
const ctrlS = new KeyboardEvent('keydown', { key: 's', ctrlKey: true })
|
||||||
const metaS = new KeyboardEvent('keydown', { key: 's', metaKey: true })
|
const metaS = new KeyboardEvent('keydown', { key: 's', metaKey: true })
|
||||||
// tab is unsaved and route is /workspace
|
// tab is unsaved and route is /workspace
|
||||||
document.dispatchEvent(ctrlS)
|
document.dispatchEvent(ctrlS)
|
||||||
expect(wrapper.vm.onSave.calledOnce).to.equal(true)
|
expect(wrapper.vm.checkInquiryBeforeSave.calledOnce).to.equal(true)
|
||||||
document.dispatchEvent(metaS)
|
document.dispatchEvent(metaS)
|
||||||
expect(wrapper.vm.onSave.calledTwice).to.equal(true)
|
expect(wrapper.vm.checkInquiryBeforeSave.calledTwice).to.equal(true)
|
||||||
|
|
||||||
// tab is saved and route is /workspace
|
// tab is saved and route is /workspace
|
||||||
store.state.tabs[0].isSaved = true
|
store.state.tabs[0].isSaved = true
|
||||||
document.dispatchEvent(ctrlS)
|
document.dispatchEvent(ctrlS)
|
||||||
expect(wrapper.vm.onSave.calledTwice).to.equal(true)
|
expect(wrapper.vm.checkInquiryBeforeSave.calledTwice).to.equal(true)
|
||||||
document.dispatchEvent(metaS)
|
document.dispatchEvent(metaS)
|
||||||
expect(wrapper.vm.onSave.calledTwice).to.equal(true)
|
expect(wrapper.vm.checkInquiryBeforeSave.calledTwice).to.equal(true)
|
||||||
|
|
||||||
// tab is unsaved and route is not /workspace
|
// tab is unsaved and route is not /workspace
|
||||||
wrapper.vm.$route.path = '/inquiries'
|
wrapper.vm.$route.path = '/inquiries'
|
||||||
store.state.tabs[0].isSaved = false
|
store.state.tabs[0].isSaved = false
|
||||||
document.dispatchEvent(ctrlS)
|
document.dispatchEvent(ctrlS)
|
||||||
expect(wrapper.vm.onSave.calledTwice).to.equal(true)
|
expect(wrapper.vm.checkInquiryBeforeSave.calledTwice).to.equal(true)
|
||||||
document.dispatchEvent(metaS)
|
document.dispatchEvent(metaS)
|
||||||
expect(wrapper.vm.onSave.calledTwice).to.equal(true)
|
expect(wrapper.vm.checkInquiryBeforeSave.calledTwice).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Ctrl Shift S calls onSaveAs if route path is /workspace', async () => {
|
it('Saves the inquiry when no need the new name', async () => {
|
||||||
const tab = {
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
execute: sinon.stub(),
|
|
||||||
isSaved: false
|
|
||||||
}
|
|
||||||
const state = {
|
|
||||||
currentTab: tab,
|
|
||||||
tabs: [tab],
|
|
||||||
db: {}
|
|
||||||
}
|
|
||||||
const store = createStore({ state })
|
|
||||||
const $route = { path: '/workspace' }
|
|
||||||
|
|
||||||
wrapper = shallowMount(MainMenu, {
|
|
||||||
global: {
|
|
||||||
mocks: { $route },
|
|
||||||
stubs: ['router-link'],
|
|
||||||
plugins: [store]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
sinon.stub(wrapper.vm, 'onSaveAs')
|
|
||||||
|
|
||||||
const ctrlS = new KeyboardEvent('keydown', {
|
|
||||||
key: 'S',
|
|
||||||
ctrlKey: true,
|
|
||||||
shiftKey: true
|
|
||||||
})
|
|
||||||
const metaS = new KeyboardEvent('keydown', {
|
|
||||||
key: 'S',
|
|
||||||
metaKey: true,
|
|
||||||
shiftKey: true
|
|
||||||
})
|
|
||||||
// tab is unsaved and route is /workspace
|
|
||||||
document.dispatchEvent(ctrlS)
|
|
||||||
expect(wrapper.vm.onSaveAs.calledOnce).to.equal(true)
|
|
||||||
document.dispatchEvent(metaS)
|
|
||||||
expect(wrapper.vm.onSaveAs.calledTwice).to.equal(true)
|
|
||||||
|
|
||||||
// tab is saved and route is /workspace
|
|
||||||
store.state.tabs[0].isSaved = true
|
|
||||||
document.dispatchEvent(ctrlS)
|
|
||||||
expect(wrapper.vm.onSaveAs.calledThrice).to.equal(true)
|
|
||||||
document.dispatchEvent(metaS)
|
|
||||||
expect(wrapper.vm.onSaveAs.callCount).to.equal(4)
|
|
||||||
|
|
||||||
// tab is unsaved and route is not /workspace
|
|
||||||
wrapper.vm.$route.path = '/inquiries'
|
|
||||||
store.state.tabs[0].isSaved = false
|
|
||||||
document.dispatchEvent(ctrlS)
|
|
||||||
expect(wrapper.vm.onSaveAs.callCount).to.equal(4)
|
|
||||||
document.dispatchEvent(metaS)
|
|
||||||
expect(wrapper.vm.onSaveAs.callCount).to.equal(4)
|
|
||||||
|
|
||||||
// tab is saved and route is not /workspace
|
|
||||||
wrapper.vm.$route.path = '/inquiries'
|
|
||||||
store.state.tabs[0].isSaved = true
|
|
||||||
document.dispatchEvent(ctrlS)
|
|
||||||
expect(wrapper.vm.onSaveAs.callCount).to.equal(4)
|
|
||||||
document.dispatchEvent(metaS)
|
|
||||||
expect(wrapper.vm.onSaveAs.callCount).to.equal(4)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Saves the inquiry when no need the new name and no update conflict', async () => {
|
|
||||||
const tab = {
|
const tab = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
updatedAt: '2025-05-15T15:30:00Z',
|
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
isSaved: false
|
isSaved: false
|
||||||
}
|
}
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: tab,
|
currentTab: tab,
|
||||||
inquiries: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
updatedAt: '2025-05-15T15:30:00Z',
|
|
||||||
createdAt: '2025-05-14T15:30:00Z'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
tabs: [tab],
|
tabs: [tab],
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
@@ -482,8 +401,7 @@ describe('MainMenu.vue', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: []
|
||||||
updatedAt: '2025-05-16T15:30:00Z'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const store = createStore({ state, mutations, actions })
|
const store = createStore({ state, mutations, actions })
|
||||||
@@ -528,8 +446,7 @@ describe('MainMenu.vue', () => {
|
|||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
isSaved: true,
|
isSaved: true
|
||||||
updatedAt: '2025-05-16T15:30:00Z'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -539,396 +456,6 @@ describe('MainMenu.vue', () => {
|
|||||||
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Inquiry conflict: overwrite', async () => {
|
|
||||||
const tab = {
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
updatedAt: '2025-05-15T15:30:00Z',
|
|
||||||
execute: sinon.stub(),
|
|
||||||
isSaved: false
|
|
||||||
}
|
|
||||||
const state = {
|
|
||||||
currentTab: tab,
|
|
||||||
inquiries: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM bar',
|
|
||||||
updatedAt: '2025-05-15T16:30:00Z',
|
|
||||||
createdAt: '2025-05-14T15:30:00Z'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
tabs: [tab],
|
|
||||||
db: {}
|
|
||||||
}
|
|
||||||
const mutations = {
|
|
||||||
updateTab: sinon.stub()
|
|
||||||
}
|
|
||||||
const actions = {
|
|
||||||
saveInquiry: sinon.stub().returns({
|
|
||||||
name: 'foo',
|
|
||||||
id: 1,
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
updatedAt: '2025-05-16T17:30:00Z',
|
|
||||||
createdAt: '2025-05-14T15:30:00Z'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const store = createStore({ state, mutations, actions })
|
|
||||||
const $route = { path: '/workspace' }
|
|
||||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(false)
|
|
||||||
|
|
||||||
wrapper = mount(MainMenu, {
|
|
||||||
attachTo: document.body,
|
|
||||||
global: {
|
|
||||||
mocks: { $route },
|
|
||||||
stubs: {
|
|
||||||
'router-link': true,
|
|
||||||
'app-diagnostic-info': true,
|
|
||||||
teleport: true,
|
|
||||||
transition: false
|
|
||||||
},
|
|
||||||
plugins: [store]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.find('#save-btn').trigger('click')
|
|
||||||
|
|
||||||
// check that the conflict dialog is open
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
|
||||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
|
||||||
'Inquiry saving conflict'
|
|
||||||
)
|
|
||||||
|
|
||||||
// find Overwrite in the dialog and click
|
|
||||||
await wrapper
|
|
||||||
.findAll('.dialog-buttons-container button')
|
|
||||||
.find(button => button.text() === 'Overwrite')
|
|
||||||
.trigger('click')
|
|
||||||
|
|
||||||
// check that the dialog is closed
|
|
||||||
await clock.tick(100)
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
|
||||||
|
|
||||||
// check that the inquiry was saved via saveInquiry (newName='')
|
|
||||||
expect(actions.saveInquiry.calledOnce).to.equal(true)
|
|
||||||
expect(actions.saveInquiry.args[0][1]).to.eql({
|
|
||||||
inquiryTab: state.currentTab,
|
|
||||||
newName: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// check that the tab was updated
|
|
||||||
expect(
|
|
||||||
mutations.updateTab.calledOnceWith(
|
|
||||||
state,
|
|
||||||
sinon.match({
|
|
||||||
tab,
|
|
||||||
newValues: {
|
|
||||||
name: 'foo',
|
|
||||||
id: 1,
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
isSaved: true,
|
|
||||||
updatedAt: '2025-05-16T17:30:00Z'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
).to.equal(true)
|
|
||||||
|
|
||||||
// check that 'inquirySaved' event was triggered on eventBus
|
|
||||||
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Inquiry conflict after saving new inquiry: overwrite', async () => {
|
|
||||||
const tab = {
|
|
||||||
id: 1,
|
|
||||||
name: null,
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
updatedAt: undefined,
|
|
||||||
execute: sinon.stub(),
|
|
||||||
dataView: { getOptionsForSave: sinon.stub() },
|
|
||||||
isSaved: false
|
|
||||||
}
|
|
||||||
const state = {
|
|
||||||
currentTab: tab,
|
|
||||||
inquiries: [],
|
|
||||||
tabs: [tab],
|
|
||||||
db: {}
|
|
||||||
}
|
|
||||||
const store = createStore({ state, mutations, actions })
|
|
||||||
const $route = { path: '/workspace' }
|
|
||||||
|
|
||||||
wrapper = mount(MainMenu, {
|
|
||||||
attachTo: document.body,
|
|
||||||
global: {
|
|
||||||
mocks: { $route },
|
|
||||||
stubs: {
|
|
||||||
'router-link': true,
|
|
||||||
'app-diagnostic-info': true,
|
|
||||||
teleport: true,
|
|
||||||
transition: false
|
|
||||||
},
|
|
||||||
plugins: [store]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.find('#save-btn').trigger('click')
|
|
||||||
|
|
||||||
// check that Save dialog is open
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
|
||||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
|
||||||
'Save inquiry'
|
|
||||||
)
|
|
||||||
|
|
||||||
// enter the name
|
|
||||||
await wrapper.find('.dialog-body input').setValue('foo')
|
|
||||||
|
|
||||||
// find Save in the dialog and click
|
|
||||||
await wrapper
|
|
||||||
.findAll('.dialog-buttons-container button')
|
|
||||||
.find(button => button.text() === 'Save')
|
|
||||||
.trigger('click')
|
|
||||||
|
|
||||||
// check that the dialog is closed
|
|
||||||
await clock.tick(100)
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
|
||||||
|
|
||||||
// check that now there is one inquiry saved
|
|
||||||
expect(state.inquiries.length).to.equal(1)
|
|
||||||
expect(state.inquiries[0].name).to.equal('foo')
|
|
||||||
expect(state.tabs[0].name).to.equal('foo')
|
|
||||||
|
|
||||||
// change the inquiry in store (like it's updated in another tab)
|
|
||||||
store.state.inquiries[0].query = 'SELECT * FROM foo_updated_in_another_tab'
|
|
||||||
store.state.inquiries[0].updatedAt = '2025-05-15T00:00:10Z'
|
|
||||||
store.state.currentTab.query = 'SELECT * FROM foo_new'
|
|
||||||
store.state.currentTab.isSaved = false
|
|
||||||
await nextTick()
|
|
||||||
await wrapper.find('#save-btn').trigger('click')
|
|
||||||
|
|
||||||
// check that the conflict dialog is open
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
|
||||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
|
||||||
'Inquiry saving conflict'
|
|
||||||
)
|
|
||||||
|
|
||||||
// find Overwrite in the dialog and click
|
|
||||||
await wrapper
|
|
||||||
.findAll('.dialog-buttons-container button')
|
|
||||||
.find(button => button.text() === 'Overwrite')
|
|
||||||
.trigger('click')
|
|
||||||
|
|
||||||
// check that the dialog is closed
|
|
||||||
await clock.tick(100)
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
|
||||||
|
|
||||||
// check that it's still one inquiry saved
|
|
||||||
expect(state.inquiries.length).to.equal(1)
|
|
||||||
expect(state.inquiries[0].name).to.equal('foo')
|
|
||||||
expect(state.tabs[0].name).to.equal('foo')
|
|
||||||
expect(state.inquiries[0].query).to.equal('SELECT * FROM foo_new')
|
|
||||||
expect(state.tabs[0].query).to.equal('SELECT * FROM foo_new')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Inquiry conflict: save as new', async () => {
|
|
||||||
const tab = {
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
updatedAt: '2025-05-15T15:30:00Z',
|
|
||||||
execute: sinon.stub(),
|
|
||||||
isSaved: false
|
|
||||||
}
|
|
||||||
const state = {
|
|
||||||
currentTab: tab,
|
|
||||||
inquiries: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM bar',
|
|
||||||
updatedAt: '2025-05-15T16:30:00Z',
|
|
||||||
createdAt: '2025-05-14T15:30:00Z'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
tabs: [tab]
|
|
||||||
}
|
|
||||||
const mutations = {
|
|
||||||
updateTab: sinon.stub()
|
|
||||||
}
|
|
||||||
const actions = {
|
|
||||||
saveInquiry: sinon.stub().returns({
|
|
||||||
name: 'foo_new',
|
|
||||||
id: 2,
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
updatedAt: '2025-05-16T17:30:00Z',
|
|
||||||
createdAt: '2025-05-16T17:30:00Z'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const store = createStore({ state, mutations, actions })
|
|
||||||
const $route = { path: '/workspace' }
|
|
||||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(false)
|
|
||||||
|
|
||||||
wrapper = mount(MainMenu, {
|
|
||||||
attachTo: document.body,
|
|
||||||
global: {
|
|
||||||
mocks: { $route },
|
|
||||||
stubs: {
|
|
||||||
'router-link': true,
|
|
||||||
'app-diagnostic-info': true,
|
|
||||||
teleport: true,
|
|
||||||
transition: false
|
|
||||||
},
|
|
||||||
plugins: [store]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.find('#save-btn').trigger('click')
|
|
||||||
// check that the conflict dialog is open
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
|
||||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
|
||||||
'Inquiry saving conflict'
|
|
||||||
)
|
|
||||||
|
|
||||||
await clock.tick(100)
|
|
||||||
|
|
||||||
// find "Save as new" in the dialog and click
|
|
||||||
|
|
||||||
await wrapper
|
|
||||||
.findAll('.dialog-buttons-container button')
|
|
||||||
.find(button => button.text() === 'Save as new')
|
|
||||||
.trigger('click')
|
|
||||||
|
|
||||||
// Hiding any dialog is done with tiny animation. Give time to finish it:
|
|
||||||
await clock.tick(100)
|
|
||||||
// Note: don't call nextTick before clock.tick. That leads to extra trap in
|
|
||||||
// trapStack and the test fails with focus-trap error in afterEach hook
|
|
||||||
// when unmount the component
|
|
||||||
|
|
||||||
// check that only one dialog open
|
|
||||||
expect(wrapper.findAll('.dialog.vfm').length).to.equal(1)
|
|
||||||
// enter the new name
|
|
||||||
await wrapper.find('.dialog-body input').setValue('foo_new')
|
|
||||||
|
|
||||||
// find Save in the dialog and click
|
|
||||||
await wrapper
|
|
||||||
.findAll('.dialog-buttons-container button')
|
|
||||||
.find(button => button.text() === 'Save')
|
|
||||||
.trigger('click')
|
|
||||||
|
|
||||||
// check that the dialog is closed
|
|
||||||
await clock.tick(100)
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
|
||||||
|
|
||||||
// check that the inquiry was saved via saveInquiry (newName='foo_new')
|
|
||||||
expect(actions.saveInquiry.calledOnce).to.equal(true)
|
|
||||||
expect(actions.saveInquiry.args[0][1]).to.eql({
|
|
||||||
inquiryTab: state.currentTab,
|
|
||||||
newName: 'foo_new'
|
|
||||||
})
|
|
||||||
|
|
||||||
// check that the tab was updated
|
|
||||||
expect(
|
|
||||||
mutations.updateTab.calledOnceWith(
|
|
||||||
state,
|
|
||||||
sinon.match({
|
|
||||||
tab,
|
|
||||||
newValues: {
|
|
||||||
name: 'foo_new',
|
|
||||||
id: 2,
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
isSaved: true,
|
|
||||||
updatedAt: '2025-05-16T17:30:00Z'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
).to.equal(true)
|
|
||||||
|
|
||||||
// check that 'inquirySaved' event was triggered on eventBus
|
|
||||||
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Inquiry conflict: cancel', async () => {
|
|
||||||
const tab = {
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
updatedAt: '2025-05-15T15:30:00Z',
|
|
||||||
execute: sinon.stub(),
|
|
||||||
isSaved: false
|
|
||||||
}
|
|
||||||
const state = {
|
|
||||||
currentTab: tab,
|
|
||||||
inquiries: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM bar',
|
|
||||||
updatedAt: '2025-05-15T16:30:00Z',
|
|
||||||
createdAt: '2025-05-14T15:30:00Z'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
tabs: [tab],
|
|
||||||
db: {}
|
|
||||||
}
|
|
||||||
const mutations = {
|
|
||||||
updateTab: sinon.stub()
|
|
||||||
}
|
|
||||||
const actions = {
|
|
||||||
saveInquiry: sinon.stub()
|
|
||||||
}
|
|
||||||
const store = createStore({ state, mutations, actions })
|
|
||||||
const $route = { path: '/workspace' }
|
|
||||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(false)
|
|
||||||
|
|
||||||
wrapper = mount(MainMenu, {
|
|
||||||
attachTo: document.body,
|
|
||||||
global: {
|
|
||||||
mocks: { $route },
|
|
||||||
stubs: {
|
|
||||||
'router-link': true,
|
|
||||||
'app-diagnostic-info': true,
|
|
||||||
teleport: true,
|
|
||||||
transition: false
|
|
||||||
},
|
|
||||||
plugins: [store]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.find('#save-btn').trigger('click')
|
|
||||||
|
|
||||||
// check that the conflict dialog is open
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
|
||||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
|
||||||
'Inquiry saving conflict'
|
|
||||||
)
|
|
||||||
|
|
||||||
// find Cancel in the dialog and click
|
|
||||||
await wrapper
|
|
||||||
.findAll('.dialog-buttons-container button')
|
|
||||||
.find(button => button.text() === 'Cancel')
|
|
||||||
.trigger('click')
|
|
||||||
|
|
||||||
// check that the dialog is closed
|
|
||||||
await clock.tick(100)
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
|
||||||
|
|
||||||
// check that the inquiry was not saved via storedInquiries.save
|
|
||||||
expect(actions.saveInquiry.called).to.equal(false)
|
|
||||||
|
|
||||||
// check that the tab was not updated
|
|
||||||
expect(mutations.updateTab.called).to.equal(false)
|
|
||||||
|
|
||||||
// check that 'inquirySaved' event is not listened on eventBus
|
|
||||||
expect(eventBus.$off.calledOnceWith('inquirySaved')).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Shows en error when the new name is needed but not specifyied', async () => {
|
it('Shows en error when the new name is needed but not specifyied', async () => {
|
||||||
const tab = {
|
const tab = {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -936,8 +463,7 @@ describe('MainMenu.vue', () => {
|
|||||||
tempName: 'Untitled',
|
tempName: 'Untitled',
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
isSaved: false,
|
isSaved: false
|
||||||
updatedAt: '2025-05-15T15:30:00Z'
|
|
||||||
}
|
}
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: tab,
|
currentTab: tab,
|
||||||
@@ -953,8 +479,7 @@ describe('MainMenu.vue', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: []
|
||||||
updatedAt: '2025-05-16T15:30:00Z'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const store = createStore({ state, mutations, actions })
|
const store = createStore({ state, mutations, actions })
|
||||||
@@ -997,15 +522,14 @@ describe('MainMenu.vue', () => {
|
|||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Saves the new inquiry with a new name', async () => {
|
it('Saves the inquiry with a new name', async () => {
|
||||||
const tab = {
|
const tab = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: null,
|
name: null,
|
||||||
tempName: 'Untitled',
|
tempName: 'Untitled',
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
isSaved: false,
|
isSaved: false
|
||||||
updatedAt: undefined
|
|
||||||
}
|
}
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: tab,
|
currentTab: tab,
|
||||||
@@ -1018,11 +542,10 @@ describe('MainMenu.vue', () => {
|
|||||||
const actions = {
|
const actions = {
|
||||||
saveInquiry: sinon.stub().returns({
|
saveInquiry: sinon.stub().returns({
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
id: 2,
|
id: 1,
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: []
|
||||||
updatedAt: '2025-05-15T15:30:00Z'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const store = createStore({ state, mutations, actions })
|
const store = createStore({ state, mutations, actions })
|
||||||
@@ -1060,6 +583,8 @@ describe('MainMenu.vue', () => {
|
|||||||
.find(button => button.text() === 'Save')
|
.find(button => button.text() === 'Save')
|
||||||
.trigger('click')
|
.trigger('click')
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
// check that the dialog is closed
|
// check that the dialog is closed
|
||||||
await clock.tick(100)
|
await clock.tick(100)
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||||
@@ -1079,12 +604,11 @@ describe('MainMenu.vue', () => {
|
|||||||
tab,
|
tab,
|
||||||
newValues: {
|
newValues: {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
id: 2,
|
id: 1,
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
isSaved: true,
|
isSaved: true
|
||||||
updatedAt: '2025-05-15T15:30:00Z'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -1126,8 +650,7 @@ describe('MainMenu.vue', () => {
|
|||||||
id: 2,
|
id: 2,
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: []
|
||||||
updatedAt: '2025-05-15T15:30:00Z'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const store = createStore({ state, mutations, actions })
|
const store = createStore({ state, mutations, actions })
|
||||||
@@ -1168,6 +691,8 @@ describe('MainMenu.vue', () => {
|
|||||||
.find(button => button.text() === 'Save')
|
.find(button => button.text() === 'Save')
|
||||||
.trigger('click')
|
.trigger('click')
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
// check that the dialog is closed
|
// check that the dialog is closed
|
||||||
await clock.tick(100)
|
await clock.tick(100)
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||||
@@ -1191,8 +716,7 @@ describe('MainMenu.vue', () => {
|
|||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
isSaved: true,
|
isSaved: true
|
||||||
updatedAt: '2025-05-15T15:30:00Z'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -1200,6 +724,19 @@ describe('MainMenu.vue', () => {
|
|||||||
|
|
||||||
// check that 'inquirySaved' event was triggered on eventBus
|
// check that 'inquirySaved' event was triggered on eventBus
|
||||||
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
||||||
|
|
||||||
|
// We saved predefined inquiry, so the tab will be created again
|
||||||
|
// (because of new id) and it will be without sql result and has default view - table.
|
||||||
|
// That's why we need to restore data and view.
|
||||||
|
// Check that result and view are preserved in the currentTab:
|
||||||
|
expect(state.currentTab.viewType).to.equal('chart')
|
||||||
|
expect(state.currentTab.result).to.eql({
|
||||||
|
columns: ['id', 'name'],
|
||||||
|
values: [
|
||||||
|
[1, 'Harry Potter'],
|
||||||
|
[2, 'Drako Malfoy']
|
||||||
|
]
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Cancel saving', async () => {
|
it('Cancel saving', async () => {
|
||||||
@@ -1224,7 +761,7 @@ describe('MainMenu.vue', () => {
|
|||||||
name: 'bar',
|
name: 'bar',
|
||||||
id: 2,
|
id: 2,
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart'
|
chart: []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const store = createStore({ state, mutations, actions })
|
const store = createStore({ state, mutations, actions })
|
||||||
@@ -1272,110 +809,4 @@ describe('MainMenu.vue', () => {
|
|||||||
// check that 'inquirySaved' event is not listened on eventBus
|
// check that 'inquirySaved' event is not listened on eventBus
|
||||||
expect(eventBus.$off.calledOnceWith('inquirySaved')).to.equal(true)
|
expect(eventBus.$off.calledOnceWith('inquirySaved')).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Save the inquiry as new', async () => {
|
|
||||||
const tab = {
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
updatedAt: '2025-05-15T15:30:00Z',
|
|
||||||
execute: sinon.stub(),
|
|
||||||
isSaved: true
|
|
||||||
}
|
|
||||||
const state = {
|
|
||||||
currentTab: tab,
|
|
||||||
inquiries: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM bar',
|
|
||||||
updatedAt: '2025-05-15T16:30:00Z',
|
|
||||||
createdAt: '2025-05-14T15:30:00Z'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
tabs: [tab],
|
|
||||||
db: {}
|
|
||||||
}
|
|
||||||
const mutations = {
|
|
||||||
updateTab: sinon.stub()
|
|
||||||
}
|
|
||||||
const actions = {
|
|
||||||
saveInquiry: sinon.stub().returns({
|
|
||||||
name: 'foo_new',
|
|
||||||
id: 2,
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
updatedAt: '2025-05-16T17:30:00Z',
|
|
||||||
createdAt: '2025-05-16T17:30:00Z'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const store = createStore({ state, mutations, actions })
|
|
||||||
const $route = { path: '/workspace' }
|
|
||||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(false)
|
|
||||||
|
|
||||||
wrapper = mount(MainMenu, {
|
|
||||||
attachTo: document.body,
|
|
||||||
global: {
|
|
||||||
mocks: { $route },
|
|
||||||
stubs: {
|
|
||||||
'router-link': true,
|
|
||||||
'app-diagnostic-info': true,
|
|
||||||
teleport: true,
|
|
||||||
transition: false
|
|
||||||
},
|
|
||||||
plugins: [store]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.find('#save-as-btn').trigger('click')
|
|
||||||
|
|
||||||
// check that Save dialog is open
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
|
||||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
|
||||||
'Save inquiry'
|
|
||||||
)
|
|
||||||
|
|
||||||
// enter the new name
|
|
||||||
await wrapper.find('.dialog-body input').setValue('foo_new')
|
|
||||||
|
|
||||||
// find Save in the dialog and click
|
|
||||||
await wrapper
|
|
||||||
.findAll('.dialog-buttons-container button')
|
|
||||||
.find(button => button.text() === 'Save')
|
|
||||||
.trigger('click')
|
|
||||||
|
|
||||||
// check that the dialog is closed
|
|
||||||
await clock.tick(100)
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
|
||||||
|
|
||||||
// check that the inquiry was saved via saveInquiry (newName='foo_new')
|
|
||||||
expect(actions.saveInquiry.calledOnce).to.equal(true)
|
|
||||||
expect(actions.saveInquiry.args[0][1]).to.eql({
|
|
||||||
inquiryTab: state.currentTab,
|
|
||||||
newName: 'foo_new'
|
|
||||||
})
|
|
||||||
|
|
||||||
// check that the tab was updated
|
|
||||||
expect(
|
|
||||||
mutations.updateTab.calledOnceWith(
|
|
||||||
state,
|
|
||||||
sinon.match({
|
|
||||||
tab,
|
|
||||||
newValues: {
|
|
||||||
name: 'foo_new',
|
|
||||||
id: 2,
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
isSaved: true,
|
|
||||||
updatedAt: '2025-05-16T17:30:00Z'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
).to.equal(true)
|
|
||||||
|
|
||||||
// check that 'inquirySaved' event was triggered on eventBus
|
|
||||||
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
@@ -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 '@/components/Schema'
|
import Schema from '@/views/MainView/Workspace/Schema'
|
||||||
import TableDescription from '@/components/Schema/TableDescription'
|
import TableDescription from '@/views/MainView/Workspace/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'
|
||||||
@@ -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 '@/components/Schema/TableDescription'
|
import TableDescription from '@/views/MainView/Workspace/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', () => {
|
||||||
@@ -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 '@/components/Chart.vue'
|
import Chart from '@/views/MainView/Workspace/Tabs/Tab/DataView/Chart/index.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,6 +15,7 @@ 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 }
|
||||||
@@ -29,6 +30,7 @@ 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 }
|
||||||
@@ -46,6 +48,7 @@ describe('Chart.vue', () => {
|
|||||||
points: [80]
|
points: [80]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mount the component
|
||||||
const wrapper = mount(Chart, {
|
const wrapper = mount(Chart, {
|
||||||
props: {
|
props: {
|
||||||
dataSources,
|
dataSources,
|
||||||
@@ -128,8 +131,6 @@ 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()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -186,8 +187,7 @@ 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,41 +207,4 @@ 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()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import DataView from '@/components/DataView.vue'
|
import DataView from '@/views/MainView/Workspace/Tabs/Tab/DataView'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
import cIo from '@/lib/utils/clipboardIo'
|
|
||||||
|
|
||||||
describe('DataView.vue', () => {
|
describe('DataView.vue', () => {
|
||||||
const $store = { state: { isWorkspaceVisible: true } }
|
const $store = { state: { isWorkspaceVisible: true } }
|
||||||
@@ -15,7 +14,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: {
|
||||||
mocks: { $store }
|
stubs: { chart: true }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -29,10 +28,7 @@ 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' }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -56,34 +52,19 @@ 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' }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Find chart and spy the method
|
// Find chart and spy the method
|
||||||
const chart = wrapper.findComponent({ name: 'Chart' }).vm
|
const chart = wrapper.findComponent({ name: 'Chart' }).vm
|
||||||
sinon.stub(chart, 'saveAsSvg')
|
sinon.spy(chart, 'saveAsSvg')
|
||||||
|
|
||||||
// Export to svg
|
// Export to svg
|
||||||
const svgBtn = wrapper.findComponent({ ref: 'svgExportBtn' })
|
const svgBtn = wrapper.findComponent({ ref: 'svgExportBtn' })
|
||||||
@@ -96,7 +77,7 @@ describe('DataView.vue', () => {
|
|||||||
|
|
||||||
// Find pivot and spy the method
|
// Find pivot and spy the method
|
||||||
const pivot = wrapper.findComponent({ name: 'pivot' }).vm
|
const pivot = wrapper.findComponent({ name: 'pivot' }).vm
|
||||||
sinon.stub(pivot, 'saveAsSvg')
|
sinon.spy(pivot, 'saveAsSvg')
|
||||||
|
|
||||||
// Switch to Custom Chart renderer
|
// Switch to Custom Chart renderer
|
||||||
pivot.pivotOptions.rendererName = 'Custom chart'
|
pivot.pivotOptions.rendererName = 'Custom chart'
|
||||||
@@ -105,22 +86,13 @@ 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' }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -141,76 +113,9 @@ 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 html
|
// Export to svg
|
||||||
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()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -241,7 +146,6 @@ describe('DataView.vue', () => {
|
|||||||
|
|
||||||
it('copy to clipboard more than 1 sec', async () => {
|
it('copy to clipboard more than 1 sec', async () => {
|
||||||
sinon.stub(window.navigator.clipboard, 'write').resolves()
|
sinon.stub(window.navigator.clipboard, 'write').resolves()
|
||||||
sinon.stub(cIo, 'copyImage')
|
|
||||||
const clock = sinon.useFakeTimers()
|
const clock = sinon.useFakeTimers()
|
||||||
const wrapper = mount(DataView, {
|
const wrapper = mount(DataView, {
|
||||||
attachTo: document.body,
|
attachTo: document.body,
|
||||||
@@ -261,7 +165,7 @@ describe('DataView.vue', () => {
|
|||||||
await copyBtn.trigger('click')
|
await copyBtn.trigger('click')
|
||||||
|
|
||||||
// The dialog is shown...
|
// The dialog is shown...
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(true)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
||||||
'Copy to clipboard'
|
'Copy to clipboard'
|
||||||
)
|
)
|
||||||
@@ -276,10 +180,11 @@ describe('DataView.vue', () => {
|
|||||||
// Wait untill prepareCopy is finished
|
// Wait untill prepareCopy is finished
|
||||||
await wrapper.vm.$refs.viewComponent.prepareCopy.returnValues[0]
|
await wrapper.vm.$refs.viewComponent.prepareCopy.returnValues[0]
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
// The dialog is shown...
|
// The dialog is shown...
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(true)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||||
|
|
||||||
// ... with Ready message...
|
// ... with Ready message...
|
||||||
expect(wrapper.find('.dialog-body').text()).to.equal('Image is ready')
|
expect(wrapper.find('.dialog-body').text()).to.equal('Image is ready')
|
||||||
@@ -291,13 +196,12 @@ describe('DataView.vue', () => {
|
|||||||
|
|
||||||
// The dialog is not shown...
|
// The dialog is not shown...
|
||||||
await clock.tick(100)
|
await clock.tick(100)
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('copy to clipboard less than 1 sec', async () => {
|
it('copy to clipboard less than 1 sec', async () => {
|
||||||
sinon.stub(window.navigator.clipboard, 'write').resolves()
|
sinon.stub(window.navigator.clipboard, 'write').resolves()
|
||||||
sinon.stub(cIo, 'copyImage')
|
|
||||||
const clock = sinon.useFakeTimers()
|
const clock = sinon.useFakeTimers()
|
||||||
const wrapper = mount(DataView, {
|
const wrapper = mount(DataView, {
|
||||||
attachTo: document.body,
|
attachTo: document.body,
|
||||||
@@ -322,9 +226,10 @@ describe('DataView.vue', () => {
|
|||||||
// Wait untill prepareCopy is finished
|
// Wait untill prepareCopy is finished
|
||||||
await wrapper.vm.$refs.viewComponent.prepareCopy.returnValues[0]
|
await wrapper.vm.$refs.viewComponent.prepareCopy.returnValues[0]
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
// The dialog is not shown...
|
// The dialog is not shown...
|
||||||
await clock.tick(100)
|
await clock.tick(100)
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||||
// copyToClipboard is called
|
// copyToClipboard is called
|
||||||
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(true)
|
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(true)
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
@@ -365,105 +270,9 @@ describe('DataView.vue', () => {
|
|||||||
|
|
||||||
// The dialog is not shown...
|
// The dialog is not shown...
|
||||||
await clock.tick(100)
|
await clock.tick(100)
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||||
// copyToClipboard is not called
|
// copyToClipboard is not called
|
||||||
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()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
@@ -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 '@/components/Pivot/index.vue'
|
import Pivot from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot'
|
||||||
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 '@/components/Pivot/pivotHelper'
|
import pivotHelper from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/pivotHelper'
|
||||||
|
|
||||||
describe('Pivot.vue', () => {
|
describe('Pivot.vue', () => {
|
||||||
let container
|
let container
|
||||||
@@ -586,48 +586,4 @@ 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()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
@@ -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 '@/components/Pivot/PivotUi/PivotSortBtn'
|
import PivotSortBtn from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/PivotUi/PivotSortBtn'
|
||||||
|
|
||||||
describe('PivotSortBtn.vue', () => {
|
describe('PivotSortBtn.vue', () => {
|
||||||
it('switches order', async () => {
|
it('switches order', async () => {
|
||||||
@@ -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 '@/components/Pivot/PivotUi'
|
import PivotUi from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/PivotUi'
|
||||||
|
|
||||||
describe('PivotUi.vue', () => {
|
describe('PivotUi.vue', () => {
|
||||||
it('returns value when settings changed', async () => {
|
it('returns value when settings changed', async () => {
|
||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
_getDataSources,
|
_getDataSources,
|
||||||
getPivotCanvas,
|
getPivotCanvas,
|
||||||
getPivotHtml
|
getPivotHtml
|
||||||
} from '@/components/Pivot/pivotHelper'
|
} from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/pivotHelper'
|
||||||
|
|
||||||
describe('pivotHelper.js', () => {
|
describe('pivotHelper.js', () => {
|
||||||
it('_getDataSources returns data sources', () => {
|
it('_getDataSources returns data sources', () => {
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user