1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2026-05-07 04:19:19 +08:00

Compare commits

..

51 Commits

Author SHA1 Message Date
lana-k
f3a75346fd fix typo 2026-04-15 22:14:59 +02:00
lana-k
366ffcff10 0.30.0 2026-04-08 22:29:52 +02:00
lana-k
778db0d9eb #136 tests 2026-04-08 22:16:22 +02:00
lana-k
c00d76904e fix lint 2026-04-06 20:37:27 +02:00
lana-k
4cc6ec86ea #136 save seedLayout settings 2026-04-06 20:31:51 +02:00
lana-k
c0d972c2ab Support circle pack as initial algorithm #136 2026-04-06 19:36:44 +02:00
lana-k
d9435a80c3 Add seed layout #136 2026-04-06 19:36:44 +02:00
lana-k
62fb92d824 premultiply colors in sigma 2026-04-06 19:36:44 +02:00
saaj
5019013eff Update Docker test file to NodeJS 18, fix Karma config filename (#135) 2026-04-05 17:13:06 +02:00
lana-k
c2c376219f fix lint 2026-02-28 17:59:39 +01:00
lana-k
0199415dde 0.29.0 2026-02-28 17:44:17 +01:00
lana-k
534b186d76 tests for graph editor 2026-02-28 16:53:43 +01:00
lana-k
4f6efb5bda test for DataView 2026-02-26 22:32:24 +01:00
lana-k
2c0b8f9124 enable webgl in chromium 2026-02-25 22:58:28 +01:00
lana-k
5265f5493e splitpanes test 2026-02-24 21:28:11 +01:00
lana-k
c0e59f6fb8 update run result test 2026-02-24 21:02:06 +01:00
lana-k
7471744633 add tests for Value Viewer 2026-02-24 20:53:03 +01:00
lana-k
e6e5efa8c6 tests for graphHelper 2026-02-22 22:30:18 +01:00
lana-k
57c36b3900 tests for migration 2026-02-07 22:08:39 +01:00
lana-k
1e8c1761e6 #133 highlight nodes and edges 2026-02-07 21:18:49 +01:00
lana-k
dd30e17ff5 #133 Add node data viewer 2026-01-25 22:15:48 +01:00
lana-k
4e5adc147f #132 node opacity 2026-01-22 22:25:36 +01:00
lana-k
7edc196a02 change repo structure 2026-01-15 21:53:12 +01:00
lana-k
85b5a200e2 fix tinycolor2 bundle 2025-12-27 21:11:01 +01:00
lana-k
a0ef93921f #131 fix label color 2025-12-26 20:56:08 +01:00
lana-k
859cd2ccfc #129 fix icon 2025-12-25 12:29:28 +01:00
lana-k
a59946c09d remove karma config 2025-12-24 22:14:48 +01:00
lana-k
7b06b3d9c8 uninstall mesa 2025-12-24 22:06:39 +01:00
lana-k
ced933f497 revert firefox base and env 2025-12-24 21:59:23 +01:00
lana-k
cda368f109 xvfb 2025-12-24 21:49:53 +01:00
lana-k
df67466c2f firefox base 2025-12-24 21:41:29 +01:00
lana-k
528549ae5a LIBGL_ALWAYS_SOFTWARE 2025-12-24 21:36:47 +01:00
lana-k
20f4dcc645 fix package names 2025-12-24 17:51:40 +01:00
lana-k
b8353ef0ce install mesa 2025-12-24 17:49:07 +01:00
lana-k
7975f419c9 anoter settings 2025-12-24 17:40:18 +01:00
lana-k
72aa0dd80b another settings 2025-12-24 17:30:05 +01:00
lana-k
e000ee71fc ensure webgl is enabled infirefox 2025-12-24 17:25:25 +01:00
lana-k
b6a12668d3 #43 fix lint errors 2025-12-24 16:17:49 +01:00
lana-k
713f5ac768 #43 graph 0.28.0 2025-12-23 21:41:17 +01:00
lana-k
5492609c3a update readme 2025-12-23 21:28:32 +01:00
lana-k
8bfd0f5944 tests 2025-12-23 21:15:44 +01:00
lana-k
a8006bcf52 tests 2025-12-17 21:26:57 +01:00
lana-k
1463f93bb0 tests for layouts 2025-12-13 17:57:48 +01:00
lana-k
5108495430 test for canvas 2025-12-13 13:00:13 +01:00
lana-k
d28968e539 tests 2025-12-07 19:56:16 +01:00
lana-k
68221cba6d tests 2025-11-15 14:29:41 +01:00
lana-k
65c1c18fcb tests 2025-11-08 22:23:38 +01:00
lana-k
d7db6a0f5d fix tests 2025-11-02 12:31:32 +01:00
lana-k
0a2af0bba3 events 2025-11-01 21:25:56 +01:00
lana-k
e4b35bac0a skip node if there is no node id 2025-11-01 19:48:22 +01:00
lana-k
3d1e822cdc link to docs, disable some settings, check result set 2025-11-01 15:49:34 +01:00
99 changed files with 8309 additions and 697 deletions

View File

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

View File

@@ -3,12 +3,12 @@
# docker build -t sqliteviz/test -f Dockerfile.test .
#
FROM node:12.22-bullseye
FROM node:18.20.8-bookworm
RUN set -ex; \
apt update; \
apt install -y chromium firefox-esr; \
npm install -g npm@7
npm install -g npm@10
WORKDIR /tmp/build
@@ -19,6 +19,6 @@ RUN npm install
COPY . .
RUN set -ex; \
sed -i 's/browsers: \[.*\],/browsers: ['"'FirefoxHeadlessTouch'"'],/' karma.conf.js
sed -i 's/browsers: \[.*\],/browsers: ['"'FirefoxHeadlessTouch'"'],/' karma.conf.cjs
RUN npm run lint -- --no-fix && npm run test

View File

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

View File

@@ -92,11 +92,23 @@ module.exports = function (config) {
'dom.w3c_touch_events.enabled': 1,
'dom.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
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['ChromiumHeadless', 'FirefoxHeadlessTouch'],
browsers: ['ChromiumHeadlessWebGL', 'FirefoxHeadlessTouch'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits

233
package-lock.json generated
View File

@@ -1,12 +1,13 @@
{
"name": "sqliteviz",
"version": "0.27.1",
"version": "0.29.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sqliteviz",
"version": "0.27.1",
"version": "0.29.0",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@sigma/export-image": "^3.0.0",
@@ -29,9 +30,10 @@
"react-chart-editor": "^0.46.1",
"react-dom": "^16.14.0",
"seedrandom": "^3.0.5",
"sigma": "^3.0.1",
"sigma": "3.0.2",
"sql.js": "file:./lib/sql-js",
"tiny-emitter": "^2.1.0",
"tinycolor2": "^1.4.2",
"veaury": "^2.5.1",
"vue": "^3.5.11",
"vue-final-modal": "^4.5.5",
@@ -63,6 +65,8 @@
"karma-spec-reporter": "^0.0.36",
"karma-vite": "^1.0.5",
"mocha": "^5.2.0",
"patch-package": "^8.0.1",
"postinstall-postinstall": "^2.1.0",
"prettier": "3.5.3",
"process": "^0.11.10",
"url-loader": "^4.1.1",
@@ -3092,6 +3096,12 @@
"license": "Apache-2.0",
"peer": true
},
"node_modules/@yarnpkg/lockfile": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz",
"integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==",
"dev": true
},
"node_modules/abbrev": {
"version": "2.0.0",
"license": "ISC",
@@ -4775,6 +4785,21 @@
"node": ">=6.0"
}
},
"node_modules/ci-info": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/sibiraj-s"
}
],
"engines": {
"node": ">=8"
}
},
"node_modules/cipher-base": {
"version": "1.0.6",
"dev": true,
@@ -7808,6 +7833,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/find-yarn-workspace-root": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz",
"integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==",
"dev": true,
"dependencies": {
"micromatch": "^4.0.2"
}
},
"node_modules/flat-cache": {
"version": "3.2.0",
"dev": true,
@@ -10357,11 +10391,36 @@
"dev": true,
"license": "MIT"
},
"node_modules/json-stable-stringify": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz",
"integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"isarray": "^2.0.5",
"jsonify": "^0.0.1",
"object-keys": "^1.1.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"dev": true,
"license": "MIT"
},
"node_modules/json-stable-stringify/node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
"dev": true
},
"node_modules/json-stringify-pretty-compact": {
"version": "4.0.0",
"license": "MIT"
@@ -10384,6 +10443,15 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsonify": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz",
"integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/jsonpointer": {
"version": "5.0.1",
"dev": true,
@@ -10682,6 +10750,15 @@
"node": ">=0.10.0"
}
},
"node_modules/klaw-sync": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz",
"integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.1.11"
}
},
"node_modules/lazy-cache": {
"version": "1.0.4",
"dev": true,
@@ -12317,6 +12394,22 @@
"wrappy": "1"
}
},
"node_modules/open": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
"integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
"dev": true,
"dependencies": {
"is-docker": "^2.0.0",
"is-wsl": "^2.1.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/optimist": {
"version": "0.6.1",
"dev": true,
@@ -12549,6 +12642,106 @@
"node": ">=0.10.0"
}
},
"node_modules/patch-package": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz",
"integrity": "sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==",
"dev": true,
"dependencies": {
"@yarnpkg/lockfile": "^1.1.0",
"chalk": "^4.1.2",
"ci-info": "^3.7.0",
"cross-spawn": "^7.0.3",
"find-yarn-workspace-root": "^2.0.0",
"fs-extra": "^10.0.0",
"json-stable-stringify": "^1.0.2",
"klaw-sync": "^6.0.0",
"minimist": "^1.2.6",
"open": "^7.4.2",
"semver": "^7.5.3",
"slash": "^2.0.0",
"tmp": "^0.2.4",
"yaml": "^2.2.2"
},
"bin": {
"patch-package": "index.js"
},
"engines": {
"node": ">=14",
"npm": ">5"
}
},
"node_modules/patch-package/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/patch-package/node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/patch-package/node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/patch-package/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/patch-package/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/patch-package/node_modules/yaml": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"dev": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/path-browserify": {
"version": "1.0.1",
"dev": true,
@@ -12993,6 +13186,13 @@
"version": "4.2.0",
"license": "MIT"
},
"node_modules/postinstall-postinstall": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz",
"integrity": "sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ==",
"dev": true,
"hasInstallScript": true
},
"node_modules/potpack": {
"version": "1.0.2",
"license": "ISC"
@@ -13341,6 +13541,11 @@
"react": "^0.14.0 || ^15.0.0"
}
},
"node_modules/react-chart-editor/node_modules/tinycolor2": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
},
"node_modules/react-color": {
"version": "2.19.3",
"license": "MIT",
@@ -14448,6 +14653,15 @@
"node": ">=4"
}
},
"node_modules/slash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/smob": {
"version": "1.5.0",
"dev": true,
@@ -15514,8 +15728,12 @@
"license": "MIT"
},
"node_modules/tinycolor2": {
"version": "1.6.0",
"license": "MIT"
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
"integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==",
"engines": {
"node": "*"
}
},
"node_modules/tinyglobby": {
"version": "0.2.12",
@@ -15561,9 +15779,10 @@
"license": "ISC"
},
"node_modules/tmp": {
"version": "0.2.3",
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.14"
}

View File

@@ -1,6 +1,6 @@
{
"name": "sqliteviz",
"version": "0.27.1",
"version": "0.30.0",
"license": "Apache-2.0",
"private": true,
"type": "module",
@@ -10,7 +10,8 @@
"serve": "vite preview",
"test": "karma start karma.conf.cjs",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
"format": "prettier . --write"
"format": "prettier . --write",
"postinstall": "patch-package"
},
"dependencies": {
"@sigma/export-image": "^3.0.0",
@@ -33,9 +34,10 @@
"react-chart-editor": "^0.46.1",
"react-dom": "^16.14.0",
"seedrandom": "^3.0.5",
"sigma": "^3.0.1",
"sigma": "3.0.2",
"sql.js": "file:./lib/sql-js",
"tiny-emitter": "^2.1.0",
"tinycolor2": "^1.4.2",
"veaury": "^2.5.1",
"vue": "^3.5.11",
"vue-final-modal": "^4.5.5",
@@ -67,6 +69,8 @@
"karma-spec-reporter": "^0.0.36",
"karma-vite": "^1.0.5",
"mocha": "^5.2.0",
"patch-package": "^8.0.1",
"postinstall-postinstall": "^2.1.0",
"prettier": "3.5.3",
"process": "^0.11.10",
"url-loader": "^4.1.1",

24
patches/sigma+3.0.2.patch Normal file
View File

@@ -0,0 +1,24 @@
diff --git a/node_modules/sigma/dist/colors-beb06eb2.esm.js b/node_modules/sigma/dist/colors-beb06eb2.esm.js
index b7130d1..bf101b6 100644
--- a/node_modules/sigma/dist/colors-beb06eb2.esm.js
+++ b/node_modules/sigma/dist/colors-beb06eb2.esm.js
@@ -51,7 +51,6 @@ function _nonIterableRest() {
function _slicedToArray(r, e) {
return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
}
-
var HTML_COLORS = {
black: "#000000",
silver: "#C0C0C0",
@@ -267,7 +266,10 @@ for (var htmlColor in HTML_COLORS) {
FLOAT_COLOR_CACHE[HTML_COLORS[htmlColor]] = FLOAT_COLOR_CACHE[htmlColor];
}
function rgbaToFloat(r, g, b, a, masking) {
- INT32[0] = a << 24 | b << 16 | g << 8 | r;
+ const r_= Math.floor(r * a / 255)
+ const g_= Math.floor(g * a / 255)
+ const b_= Math.floor(b * a / 255)
+ INT32[0] = a << 24 | b_ << 16 | g_ << 8 | r_;
if (masking) INT32[0] = INT32[0] & 0xfeffffff;
return FLOAT32[0];
}

View File

@@ -33,48 +33,6 @@ export default {
</script>
<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,
.dialog,
input,

View File

@@ -4,3 +4,10 @@
font-size: 13px;
padding: 0 24px;
}
.data-view-warning {
height: 40px;
line-height: 40px;
border-bottom: 1px solid var(--color-border);
box-sizing: border-box;
}

View File

@@ -0,0 +1,45 @@
@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);
}

View File

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

View File

@@ -1,6 +1,6 @@
<template>
<div ref="chartContainer" class="chart-container">
<div v-show="!dataSources" class="warning chart-warning">
<div v-show="!dataSources" class="warning data-view-warning">
There is no data to build a chart. Run your SQL query and make sure the
result is not empty.
</div>
@@ -120,8 +120,8 @@ export default {
this.resizeObserver.observe(this.$refs.chartContainer)
if (this.dataSources) {
dereference.default(this.state.data, this.dataSources)
this.updatePlotly()
}
this.handleResize()
},
activated() {
this.useResizeHandler = true
@@ -134,6 +134,10 @@ export default {
},
methods: {
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()
},
onRender() {
@@ -184,13 +188,6 @@ export default {
height: 100%;
}
.chart-warning {
height: 40px;
line-height: 40px;
border-bottom: 1px solid var(--color-border);
box-sizing: border-box;
}
.chart {
min-height: 242px;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,15 @@
<template>
<Field label="Adjust sizes">
<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"
@@ -7,7 +17,10 @@
/>
</Field>
<Field label="Barnes-Hut optimize">
<Field
label="Barnes-Hut optimize"
fieldContainerClassName="test_fa2_barnes_hut"
>
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.barnesHutOptimize"
@@ -15,21 +28,21 @@
/>
</Field>
<Field v-show="modelValue.barnesHutOptimize" label="Barnes-Hut Theta">
<Field
v-show="modelValue.barnesHutOptimize"
label="Barnes-Hut Theta"
fieldContainerClassName="test_fa2_barnes_theta"
>
<NumericInput
:value="modelValue.barnesHutTheta"
@update="update('barnesHutTheta', $event)"
/>
</Field>
<Field label="Gravity">
<NumericInput
:value="modelValue.gravity"
@update="update('gravity', $event)"
/>
</Field>
<Field label="Strong gravity mode">
<Field
label="Strong gravity mode"
fieldContainerClassName="test_fa2_strong_gravity"
>
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.strongGravityMode"
@@ -37,7 +50,10 @@
/>
</Field>
<Field label="Noack's LinLog model">
<Field
label="Noack's LinLog model"
fieldContainerClassName="test_fa2_lin_log"
>
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.linLogMode"
@@ -45,7 +61,10 @@
/>
</Field>
<Field label="Out bound attraction distribution">
<Field
label="Outbound attraction distribution"
fieldContainerClassName="test_fa2_outbound_attraction"
>
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.outboundAttractionDistribution"
@@ -53,7 +72,7 @@
/>
</Field>
<Field label="Slow down">
<Field label="Slow down" fieldContainerClassName="test_fa2_slow_down">
<NumericInput
:value="modelValue.slowDown"
:min="0"
@@ -65,10 +84,15 @@
<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">
<Field
v-show="modelValue.weightSource"
label="Edge weight influence"
fieldContainerClassName="test_fa2_weight_influence"
>
<NumericInput
:value="modelValue.edgeWeightInfluence"
@update="update('edgeWeightInfluence', $event)"

View File

@@ -28,7 +28,7 @@
</multiselect>
</Field>
<Field label="Seed value">
<Field label="Seed value" fieldContainerClassName="test_seed_value">
<NumericInput
:value="modelValue.seedValue"
@update="update('seedValue', $event)"
@@ -40,7 +40,6 @@
import { applyPureReactInVue } from 'veaury'
import Field from 'react-chart-editor/lib/components/fields/Field'
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 'react-chart-editor/lib/react-chart-editor.css'
@@ -48,7 +47,6 @@ export default {
components: {
Field: applyPureReactInVue(Field),
NumericInput: applyPureReactInVue(NumericInput),
Dropdown: applyPureReactInVue(Dropdown),
Multiselect
},
props: {

View File

@@ -1,18 +1,21 @@
<template>
<Field label="Color">
<Field label="Color" fieldContainerClassName="test_edge_color">
<RadioBlocks
:options="edgeColorTypeOptions"
:activeOption="modelValue.type"
@option-change="updateColorType"
/>
<Field v-if="modelValue.type === 'constant'">
<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>
<Field fieldContainerClassName="test_edge_color_value">
<Dropdown
v-if="modelValue.type === 'variable'"
:options="keyOptions"
@@ -21,7 +24,7 @@
/>
</Field>
<Field>
<Field fieldContainerClassName="test_edge_color_mapping_mode">
<RadioBlocks
:options="colorSourceUsageOptions"
:activeOption="modelValue.sourceUsage"
@@ -39,7 +42,11 @@
</template>
</Field>
<Field v-if="modelValue.type !== 'constant'" label="Color as">
<Field
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
label="Color as"
fieldContainerClassName="test_edge_color_as"
>
<RadioBlocks
:options="сolorAsOptions"
:activeOption="modelValue.mode"
@@ -47,7 +54,11 @@
/>
</Field>
<Field v-if="modelValue.type !== 'constant'" label="Colorscale direction">
<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"
@@ -91,7 +102,7 @@ export default {
]),
сolorscaleDirections: markRaw([
{ label: 'Normal', value: 'normal' },
{ label: 'Recersed', value: 'reversed' }
{ label: 'Reversed', value: 'reversed' }
]),
colorSourceUsageOptions: markRaw([
{ label: 'Direct', value: 'direct' },

View File

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

View File

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

View File

@@ -0,0 +1,86 @@
<template>
<Field label="Initial algorithm">
<Dropdown
:options="layoutOptions"
:value="modelValue.initialAlgorithm"
:clearable="false"
className="test_fa2_initial_layout_algorithm_select"
@change="updateInitialAlgorithm"
/>
</Field>
<component
:is="layoutSettingsComponentMap[modelValue.initialAlgorithm]"
v-if="modelValue.initialAlgorithm !== 'circular'"
:modelValue="modelValue"
:keyOptions="keyOptions"
@update:model-value="$emit('update:modelValue', $event)"
/>
</template>
<script>
import { markRaw } from 'vue'
import { applyPureReactInVue } from 'veaury'
import Field from 'react-chart-editor/lib/components/fields/Field'
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 CirclePackLayoutSettings from '@/components/Graph/CirclePackLayoutSettings.vue'
import RandomLayoutSettings from '@/components/Graph/RandomLayoutSettings.vue'
export default {
components: {
Field: applyPureReactInVue(Field),
Dropdown: applyPureReactInVue(Dropdown),
NumericInput: applyPureReactInVue(NumericInput),
RandomLayoutSettings,
CirclePackLayoutSettings
},
props: {
modelValue: Object,
keyOptions: Array
},
emits: ['update:modelValue'],
data() {
return {
layoutOptions: markRaw([
{ label: 'Circular', value: 'circular' },
{ label: 'Random', value: 'random' },
{ label: 'Circle pack', value: 'circlepack' }
]),
layoutSettingsComponentMap: markRaw({
random: RandomLayoutSettings,
circlepack: CirclePackLayoutSettings
}),
defaultSeedLayoutSettings: {
circular: { initialAlgorithm: 'circular' },
random: { initialAlgorithm: 'random', seedValue: 1 },
circlepack: {
initialAlgorithm: 'circlepack',
hierarchyAttributes: [],
seedValue: 1
}
}
}
},
methods: {
updateInitialAlgorithm(newAlgorithm) {
const newSettings = {
...this.modelValue
}
const prevAlgorithmSettings =
this.defaultSeedLayoutSettings[this.modelValue.initialAlgorithm]
Object.keys(prevAlgorithmSettings).forEach(key => {
delete newSettings[key]
prevAlgorithmSettings[key] = this.modelValue[key]
})
this.$emit('update:modelValue', {
...newSettings,
...this.defaultSeedLayoutSettings[newAlgorithm]
})
}
}
}
</script>

View File

@@ -7,12 +7,15 @@
<Field>
Map your result set records to node and edge properties required
to build a graph. Learn more about result set requirements in the
documentation.
<a href="https://sqliteviz.com/docs/graph/" target="_blank">
documentation</a
>.
</Field>
<Field label="Object type">
<Field ref="objectTypeField" label="Object type">
<Dropdown
:options="keysOptions"
:value="settings.structure.objectType"
className="test_object_type_select"
@change="updateStructure('objectType', $event)"
/>
<Field>
@@ -25,6 +28,7 @@
<Dropdown
:options="keysOptions"
:value="settings.structure.nodeId"
className="test_node_id_select"
@change="updateStructure('nodeId', $event)"
/>
<Field> A field keeping unique node identifier. </Field>
@@ -34,6 +38,7 @@
<Dropdown
:options="keysOptions"
:value="settings.structure.edgeSource"
className="test_edge_source_select"
@change="updateStructure('edgeSource', $event)"
/>
<Field>
@@ -45,6 +50,7 @@
<Dropdown
:options="keysOptions"
:value="settings.structure.edgeTarget"
className="test_edge_target_select"
@change="updateStructure('edgeTarget', $event)"
/>
<Field>
@@ -61,6 +67,15 @@
@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">
@@ -69,6 +84,7 @@
<Dropdown
:options="keysOptions"
:value="settings.style.nodes.label.source"
className="test_label_select"
@change="updateNodes('label.source', $event)"
/>
</Field>
@@ -95,7 +111,10 @@
<Panel group="Style" name="Edges">
<Fold name="Edges">
<Field label="Direction">
<Field
label="Direction"
fieldContainerClassName="test_edge_direction"
>
<RadioBlocks
:options="visibilityOptions"
:activeOption="settings.style.edges.showDirection"
@@ -107,6 +126,7 @@
<Dropdown
:options="keysOptions"
:value="settings.style.edges.label.source"
className="test_edge_label_select"
@change="updateEdges('label.source', $event)"
/>
</Field>
@@ -137,6 +157,8 @@
<Dropdown
:options="layoutOptions"
:value="settings.layout.type"
:clearable="false"
className="test_layout_algorithm_select"
@change="updateLayout($event)"
/>
</Field>
@@ -149,6 +171,17 @@
/>
</Fold>
<template v-if="settings.layout.type === 'forceAtlas2'">
<Fold name="Seed layout">
<Field>
If you already built a graph using another layout, the initial
algorithm doesn't apply unless you restart it.
</Field>
<ForceAtlasSeedLayoutSettings
v-model="settings.layout.options"
:keyOptions="keysOptions"
@update:model-value="updateLayout(settings.layout.type)"
/>
</Fold>
<Fold name="Advanced layout settings">
<AdvancedForceAtlasLayoutSettings
v-model="settings.layout.options"
@@ -157,21 +190,39 @@
/>
</Fold>
<div class="force-atlas-buttons">
<Button variant="secondary" @click="resetFA2LayoutSettings">
<Button
variant="secondary"
class="test_fa2_reset"
title="Set the settings to default or previously saved ones."
@click="resetFA2LayoutSettings"
>
Reset
</Button>
<Button variant="primary" @click="toggleFA2Layout">
<Button
variant="primary"
class="test_fa2_toggle"
@click="toggleFA2Layout"
>
<template #node:icon>
<div
:style="{
padding: '0 3px'
}"
>
<div>
<RunIcon v-if="!fa2Running" />
<StopIcon v-else />
<PauseIcon v-else />
</div>
</template>
{{ fa2Running ? 'Stop' : 'Start' }}
{{ fa2Running ? 'Pause' : 'Continue' }}
</Button>
<Button
variant="primary"
class="test_fa2_restart"
title="Clear node coordinates and run the layout algorithm anew."
@click="restartFA2Layout"
>
<template #node:icon>
<div>
<RestartIcon />
</div>
</template>
Restart
</Button>
</div>
</template>
@@ -181,6 +232,7 @@
<div
ref="graph"
class="test_graph_output"
:style="{
height: '100%',
width: '100%',
@@ -203,18 +255,24 @@ 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'
import ForceAtlasSeedLayoutSettings from '@/components/Graph/ForceAtlasSeedLayoutSettings.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 forceAtlas2 from 'graphology-layout-forceatlas2'
import * as forceAtlas2 from 'graphology-layout-forceatlas2'
import RunIcon from '@/components/svg/run.vue'
import StopIcon from '@/components/svg/stop.vue'
import RestartIcon from '@/components/svg/restart.vue'
import PauseIcon from '@/components/svg/pause.vue'
import { downloadAsPNG, drawOnCanvas } from '@sigma/export-image'
import {
buildNodes,
buildEdges,
updateNodes,
updateEdges
updateEdges,
reduceNodes,
reduceEdges,
clearNodeCoordinates
} from '@/lib/graphHelper'
import Graph from 'graphology'
import { circular, random, circlepack } from 'graphology-layout'
@@ -224,6 +282,7 @@ 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: {
@@ -238,14 +297,16 @@ export default {
Button: applyPureReactInVue(Button),
ColorPicker: applyPureReactInVue(ColorPicker),
RunIcon,
StopIcon,
RestartIcon,
PauseIcon,
RandomLayoutSettings,
CirclePackLayoutSettings,
NodeColorSettings,
NodeSizeSettings,
EdgeSizeSettings,
EdgeColorSettings,
AdvancedForceAtlasLayoutSettings
AdvancedForceAtlasLayoutSettings,
ForceAtlasSeedLayoutSettings
},
inject: ['tabLayout'],
props: {
@@ -253,7 +314,7 @@ export default {
initOptions: Object,
showViewSettings: Boolean
},
emits: ['update'],
emits: ['update', 'selectItem', 'clearSelection'],
data() {
return {
graph: new Graph({ multi: true, allowSelfLoops: true }),
@@ -276,7 +337,16 @@ export default {
circlepack: CirclePackLayoutSettings,
forceAtlas2: ForceAtlasLayoutSettings
}),
layoutMethodMap: markRaw({
circular: this.applyCircularLayout,
random: this.applyRandomLayout,
circlepack: this.applyCirclePackLayout,
forceAtlas2: this.applyFA2Layout
}),
selectedNodeId: undefined,
hoveredNodeId: undefined,
selectedEdgeId: undefined,
hoveredEdgeId: undefined,
settings: this.initOptions
? JSON.parse(JSON.stringify(this.initOptions))
: {
@@ -288,14 +358,16 @@ export default {
},
style: {
backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: {
size: {
type: 'constant',
value: 4
value: 10
},
color: {
type: 'constant',
value: '#1F77B4'
value: '#1F77B4',
opacity: 100
},
label: {
source: null,
@@ -327,7 +399,15 @@ export default {
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: {
@@ -335,11 +415,10 @@ export default {
if (!this.dataSources) {
return []
}
const firstColumnName = Object.keys(this.dataSources)[0]
try {
return (
this.dataSources[Object.keys(this.dataSources)[0] || 'doc'].map(
json => JSON.parse(json)
) || []
this.dataSources[firstColumnName].map(json => JSON.parse(json)) || []
)
} catch {
return []
@@ -355,6 +434,46 @@ export default {
}, 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: {
@@ -375,6 +494,14 @@ export default {
this.buildGraph()
}
},
'settings.layout.type': {
immediate: true,
handler() {
events.send('viz_graph.render', null, {
layout: this.settings.layout.type
})
}
},
tabLayout: {
deep: true,
handler() {
@@ -391,6 +518,7 @@ export default {
},
methods: {
buildGraph() {
this.clearSelection()
if (this.renderer) {
this.renderer.kill()
}
@@ -408,12 +536,85 @@ export default {
renderEdgeLabels: true,
allowInvalidContainer: true,
labelColor: { attribute: 'labelColor', color: '#444444' },
edgeLabelColor: { attribute: 'labelColor', color: '#a2b1c6' }
edgeLabelColor: { attribute: 'labelColor', color: '#a2b1c6' },
enableEdgeEvents: true,
zIndex: true,
nodeReducer: (node, data) =>
reduceNodes(node, data, this.interactionState, this.settings),
edgeReducer: (edge, data) =>
reduceEdges(
edge,
data,
this.interactionState,
this.settings,
this.graph
)
})
this.renderer.on('clickNode', ({ node }) => {
this.selectedNodeId = node
this.selectedEdgeId = undefined
this.$emit('selectItem', this.graph.getNodeAttributes(node).data)
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('clickEdge', ({ edge }) => {
this.selectedEdgeId = edge
this.selectedNodeId = undefined
this.$emit('selectItem', this.graph.getEdgeAttributes(edge).data)
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('clickStage', () => {
this.clearSelection()
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('enterNode', ({ node }) => {
this.hoveredNodeId = node
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('enterEdge', ({ edge }) => {
this.hoveredEdgeId = edge
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('leaveNode', () => {
this.hoveredNodeId = undefined
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('leaveEdge', () => {
this.hoveredEdgeId = undefined
this.renderer.refresh({
skipIndexation: true
})
})
if (this.settings.layout.type === 'forceAtlas2') {
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
},
@@ -473,75 +674,76 @@ export default {
this.fa2Layout.kill()
}
if (layoutType === 'circular') {
circular.assign(this.graph)
return
}
this.layoutMethodMap[layoutType]()
if (layoutType === 'random') {
random.assign(this.graph, {
rng: seedrandom(this.settings.layout.options.seedValue || 1)
})
return
if (layoutType === 'forceAtlas2' && layoutType !== prevLayout) {
this.autoRunFA2Layout()
}
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]
}
},
applyCircularLayout() {
circular.assign(this.graph)
},
applyRandomLayout() {
random.assign(this.graph, {
rng: seedrandom(this.settings.layout.options.seedValue)
})
},
applyCirclePackLayout() {
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]
)
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.graph.someNode(
(nodeKey, attributes) =>
typeof attributes.x === 'number' &&
typeof attributes.y === 'number'
// Set new hierarchy attributes
this.settings.layout.options.hierarchyAttributes?.forEach(
(hierarchyAttribute, index) => {
newAttributes['hierarchyAttribute' + index] =
attributes.data[hierarchyAttribute]
}
)
) {
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
})
return newAttributes
})
})
circlepack.assign(this.graph, {
hierarchyAttributes:
this.settings.layout.options.hierarchyAttributes?.map(
(_, index) => 'hierarchyAttribute' + index
) || [],
rng: seedrandom(this.settings.layout.options.seedValue)
})
},
applyFA2Layout() {
if (
!this.graph.someNode(
(nodeKey, attributes) =>
typeof attributes.x === 'number' && typeof attributes.y === 'number'
)
if (layoutType !== prevLayout) {
this.autoRunFA2Layout()
}
) {
this.layoutMethodMap[this.settings.layout.options.initialAlgorithm]()
}
const fa2settings = { ...this.settings.layout.options }
// delete all custom settings because they can break the algorithm running
delete fa2settings.initialAlgorithm
delete fa2settings.seedValue
delete fa2settings.initialIterationsAmount
delete fa2settings.hierarchyAttributes
this.fa2Layout = markRaw(
new FA2Layout(this.graph, {
getEdgeWeight: (_, attr) =>
this.settings.layout.options.weightSource
? attr.data[this.settings.layout.options.weightSource]
: 1,
settings: fa2settings
})
)
},
toggleFA2Layout() {
if (this.fa2Layout.isRunning()) {
@@ -562,11 +764,16 @@ export default {
this.checkIteration = null
}
},
autoRunFA2Layout() {
restartFA2Layout() {
if (this.fa2Layout.isRunning()) {
this.stopFA2Layout()
}
this.fa2Layout.kill()
clearNodeCoordinates(this.graph)
this.applyFA2Layout()
this.autoRunFA2Layout()
},
autoRunFA2Layout() {
let iteration = 1
this.checkIteration = () => {
if (
@@ -581,8 +788,9 @@ export default {
this.fa2Layout.start()
},
setRecommendedFA2Settings() {
const sensibleSettings = forceAtlas2.inferSettings(this.graph)
const sensibleSettings = forceAtlas2.default.inferSettings(this.graph)
this.settings.layout.options = {
initialAlgorithm: 'circular',
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
@@ -644,4 +852,8 @@ export default {
flex-grow: 1;
flex-basis: 0;
}
.force-atlas-buttons :deep(.button__icon > div) {
padding: 0 3px;
}
</style>

View File

@@ -1,18 +1,21 @@
<template>
<Field label="Color">
<Field label="Color" fieldContainerClassName="test_node_color">
<RadioBlocks
:options="nodeColorTypeOptions"
:activeOption="modelValue.type"
@option-change="updateColorType"
/>
<Field v-if="modelValue.type === 'constant'">
<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>
<Field fieldContainerClassName="test_node_color_value">
<Dropdown
v-if="modelValue.type === 'variable'"
:options="keyOptions"
@@ -23,11 +26,15 @@
v-if="modelValue.type === 'calculated'"
:options="nodeCalculatedColorMethodOptions"
:value="modelValue.method"
:clearable="false"
@change="updateSettings('method', $event)"
/>
</Field>
<Field>
<Field
v-if="modelValue.type === 'variable'"
fieldContainerClassName="test_node_color_mapping_mode"
>
<RadioBlocks
:options="colorSourceUsageOptions"
:activeOption="modelValue.sourceUsage"
@@ -35,7 +42,12 @@
/>
</Field>
<Field v-if="modelValue.sourceUsage === 'map_to'">
<Field
v-if="
modelValue.sourceUsage === 'map_to' ||
modelValue.type === 'calculated'
"
>
<ColorscalePicker
:selected="modelValue.colorscale"
className="colorscale-picker"
@@ -45,7 +57,23 @@
</template>
</Field>
<Field v-if="modelValue.type !== 'constant'" label="Color as">
<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"
@@ -53,7 +81,13 @@
/>
</Field>
<Field v-if="modelValue.type !== 'constant'" label="Colorscale direction">
<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"
@@ -65,6 +99,7 @@
<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'
@@ -74,6 +109,7 @@ import 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
NumericInput: applyPureReactInVue(NumericInput),
Dropdown: applyPureReactInVue(Dropdown),
RadioBlocks: applyPureReactInVue(RadioBlocks),
Field: applyPureReactInVue(Field),
@@ -103,27 +139,28 @@ export default {
]),
сolorscaleDirections: markRaw([
{ label: 'Normal', value: 'normal' },
{ label: 'Recersed', value: 'reversed' }
{ label: 'Reversed', value: 'reversed' }
]),
colorSourceUsageOptions: markRaw([
{ label: 'Direct', value: 'direct' },
{ label: 'Map to', value: 'map_to' }
]),
defaultColorSettings: {
constant: { value: '#1F77B4' },
constant: { value: '#1F77B4', opacity: 100 },
variable: {
source: null,
sourceUsage: 'map_to',
colorscale: null,
mode: 'categorical',
colorscaleDirection: 'normal'
colorscaleDirection: 'normal',
opacity: 100
},
calculated: {
method: 'degree',
sourceUsage: 'map_to',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'normal'
colorscaleDirection: 'normal',
opacity: 100
}
}
}

View File

@@ -1,16 +1,17 @@
<template>
<Field label="Size">
<Field label="Size" fieldContainerClassName="test_node_size">
<RadioBlocks
:options="nodeSizeTypeOptions"
:activeOption="modelValue.type"
@option-change="updateSizeType"
/>
<Field>
<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
@@ -23,20 +24,21 @@
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">
<Field label="Size scale" fieldContainerClassName="test_node_size_scale">
<NumericInput
:value="modelValue.scale"
@update="updateSettings('scale', $event)"
/>
</Field>
<Field label="Size mode">
<Field label="Size mode" fieldContainerClassName="test_node_size_mode">
<RadioBlocks
:options="nodeSizeModeOptions"
:activeOption="modelValue.mode"
@@ -44,7 +46,7 @@
/>
</Field>
<Field label="Minimum size">
<Field label="Minimum size" fieldContainerClassName="test_node_size_min">
<NumericInput
:value="modelValue.min"
@update="updateSettings('min', $event)"

View File

@@ -1,5 +1,5 @@
<template>
<Field label="Seed value">
<Field label="Seed value" fieldContainerClassName="test_seed_value">
<NumericInput
:value="modelValue.seedValue"
@update="update('seedValue', $event)"

View File

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

View File

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

View File

@@ -31,9 +31,9 @@ import fIo from '@/lib/utils/fileIo'
import $ from 'jquery'
import 'pivottable'
import 'pivottable/dist/pivot.css'
import PivotUi from './PivotUi'
import PivotUi from './PivotUi/index.vue'
import pivotHelper from './pivotHelper'
import Chart from '@/views/MainView/Workspace/Tabs/Tab/DataView/Chart'
import Chart from '@/components/Chart'
import chartHelper from '@/lib/chartHelper'
import events from '@/lib/utils/events'
import plotly from 'plotly.js'
@@ -227,12 +227,12 @@ export default {
async prepareCopy() {
if (this.viewCustomChart) {
return await this.$refs.customChart.prepareCopy()
return this.$refs.customChart.prepareCopy()
}
if (this.viewStandartChart) {
return await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
return chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
}
return await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
return pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
},
async saveAsSvg() {
@@ -326,4 +326,8 @@ export default {
.pivot-output:empty {
flex-grow: 0;
}
:deep(.js-plotly-plot) {
height: 100%;
}
</style>

View File

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

View File

@@ -1,40 +1,66 @@
<template>
<div ref="runResultPanel" class="run-result-panel">
<component
:is="viewValuePanelVisible ? 'splitpanes' : 'div'"
<splitpanes
:before="{ size: 50, max: 100 }"
:after="{ size: 50, max: 100 }"
:after="{ size: 50, max: 100, hidden: !viewValuePanelVisible }"
:default="{ before: 50, after: 50 }"
class="run-result-panel-content"
>
<template #left-pane>
<div
:id="'run-result-left-pane-' + tab.id"
class="result-set-container"
/>
</template>
<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 class="result-set-container">
<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>
</template>
</component>
<template v-if="viewValuePanelVisible" #right-pane>
<value-viewer
:empty="!selectedCell"
emptyMessage="No cell selected to view"
:value="
selectedCell
? result.values[result.columns[selectedCell.dataset.col]][
selectedCell.dataset.row
]
: ''
"
/>
</template>
</splitpanes>
<side-tool-bar panel="table" @switch-to="$emit('switchTo', $event)">
<icon-button
@@ -89,69 +115,27 @@
@action="copyToClipboard"
@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>
</template>
<script>
import Logs from '@/components/Logs'
import SqlTable from '@/components/SqlTable/index.vue'
import LoadingIndicator from '@/components/LoadingIndicator'
import SideToolBar from '../SideToolBar'
import Splitpanes from '@/components/Splitpanes'
import Logs from '@/components/Common/Logs'
import SqlTable from '@/components/SqlTable'
import LoadingIndicator from '@/components/Common/LoadingIndicator'
import SideToolBar from '@/components/SideToolBar'
import Splitpanes from '@/components/Common/Splitpanes'
import ExportToCsvIcon from '@/components/svg/exportToCsv'
import ClipboardIcon from '@/components/svg/clipboard'
import ViewCellValueIcon from '@/components/svg/viewCellValue'
import RowIcon from '@/components/svg/row'
import IconButton from '@/components/IconButton'
import IconButton from '@/components/Common/IconButton'
import csv from '@/lib/csv'
import fIo from '@/lib/utils/fileIo'
import cIo from '@/lib/utils/clipboardIo'
import time from '@/lib/utils/time'
import loadingDialog from '@/components/LoadingDialog'
import loadingDialog from '@/components/Common/LoadingDialog'
import events from '@/lib/utils/events'
import ValueViewer from './ValueViewer'
import ValueViewer from '@/components/ValueViewer'
import Record from './Record/index.vue'
export default {
@@ -172,7 +156,6 @@ export default {
Splitpanes
},
props: {
tab: Object,
result: Object,
isGettingResults: Boolean,
error: Object,
@@ -194,20 +177,6 @@ export default {
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: {
result() {
this.defaultSelectedCell = null
@@ -332,19 +301,12 @@ export default {
width: 0;
}
.result-set-container,
.result-set-container > div {
.result-set-container {
position: relative;
height: 100%;
width: 100%;
box-sizing: border-box;
}
.value-viewer-container {
height: 100%;
width: 100%;
background-color: var(--color-white);
position: relative;
}
.table-preview {
position: absolute;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 4C3 3.44772 3.14924 3 3.33333 3H6.66667C6.85076 3 7 3.44772 7
4V14C7 14.5523 6.85076 15 6.66667 15H3.33333C3.14924 15 3 14.5523 3 14V4Z"
fill="#A2B1C6"
/>
<path
d="M11 4C11 3.44772 11.1492 3 11.3333 3H14.6667C14.8508 3 15 3.44772 15
4V14C15 14.5523 14.8508 15 14.6667 15H11.3333C11.1492 15 11 14.5523 11
14V4Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'PauseIcon'
}
</script>

View File

@@ -0,0 +1,36 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.34708 3.32922C8.63088 2.90468 10.017 2.90308 11.3022
3.32434C11.9084 3.52308 12.4768 3.8114 12.9906 4.1759V3.34387C12.9906
2.79159 13.4384 2.34387 13.9906 2.34387C14.5429 2.34387 14.9906 2.79159
14.9906 3.34387V6.67102C14.9906 7.22331 14.5429 7.67102 13.9906
7.67102H10.6635C10.1112 7.67102 9.66349 7.22331 9.66349 6.67102C9.66351
6.11876 10.1112 5.67102 10.6635 5.67102H11.6313C11.3351 5.4851 11.0154
5.33498 10.6791 5.22473C9.80069 4.93681 8.85289 4.93738 7.97501
5.22766C7.09726 5.51795 6.33574 6.08228 5.80216 6.83704C5.26867
7.59191 4.99088 8.49846 5.01017 9.42297C5.02954 10.3475 5.34482
11.2417 5.90958 11.9738C6.47435 12.7058 7.25871 13.237 8.14787
13.4904C9.03716 13.7437 9.9843 13.7055 10.85 13.381C11.7157 13.0565
12.4548 12.463 12.9584 11.6876C13.2592 11.2244 13.879 11.0929 14.3422
11.3937C14.805 11.6945 14.9366 12.3135 14.6361 12.7765C13.8996 13.9106
12.8185 14.7794 11.5522 15.254C10.2859 15.7287 8.90058 15.7846 7.60001
15.4142C6.2994 15.0437 5.15167 14.2661 4.3256 13.1954C3.49956 12.1247
3.03849 10.8169 3.01017 9.46497C2.98191 8.1131 3.38782 6.7872 4.16837
5.68274C4.94892 4.57842 6.06331 3.75379 7.34708 3.32922Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'RestartIcon'
}
</script>

File diff suppressed because one or more lines are too long

View File

@@ -1,20 +0,0 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 4C3 3.44772 3.44772 3 4 3H14C14.5523 3 15 3.44772 15 4V14C15 14.5523 14.5523 15 14 15H4C3.44772 15 3 14.5523 3 14V4Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'StopIcon'
}
</script>

View File

@@ -5,8 +5,46 @@ const TYPE_NODE = 0
const TYPE_EDGE = 1
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 clearNodeCoordinates(graph) {
graph.forEachNode((nodeId, attributes) => {
attributes.x = undefined
attributes.y = undefined
})
}
export function buildNodes(graph, dataSources, options) {
const docColumn = Object.keys(dataSources)[0] || 'doc'
const docColumn = Object.keys(dataSources)[0]
const { objectType, nodeId } = options.structure
if (objectType && nodeId) {
@@ -14,16 +52,17 @@ export function buildNodes(graph, dataSources, options) {
.map(json => JSON.parse(json))
.filter(item => item[objectType] === TYPE_NODE)
nodes.forEach(node => {
graph.addNode(node[nodeId], {
data: node,
labelColor: options.style.nodes.label.color
})
if (node[nodeId]) {
graph.addNode(node[nodeId], {
data: node
})
}
})
}
}
export function buildEdges(graph, dataSources, options) {
const docColumn = Object.keys(dataSources)[0] || 'doc'
const docColumn = Object.keys(dataSources)[0]
const { objectType, edgeSource, edgeTarget } = options.structure
if (objectType && edgeSource && edgeTarget) {
@@ -36,8 +75,7 @@ export function buildEdges(graph, dataSources, options) {
const target = edge[edgeTarget]
if (graph.hasNode(source) && graph.hasNode(target)) {
graph.addEdge(source, target, {
data: edge,
labelColor: options.style.edges.label.color
data: edge
})
}
})
@@ -96,6 +134,122 @@ export function updateEdges(graph, attributeUpdates) {
})
}
export function reduceNodes(nodeId, nodeData, interactionState, settings) {
const {
selectedNodeId,
hoveredNodeId,
selectedEdgeId,
hoveredEdgeId,
neighborsOfSelectedNode,
neighborsOfHoveredNode,
selectedEdgeExtremities,
hoveredEdgeExtremities
} = interactionState
const res = { ...nodeData }
if (selectedNodeId || hoveredNodeId || hoveredEdgeId || selectedEdgeId) {
res.zIndex = 2
res.highlighted = nodeId === selectedNodeId || nodeId === hoveredNodeId
if (res.highlighted) {
res.labelColor = 'black'
}
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) {
const { source, color } = labelSettings
return attributes => {
@@ -110,16 +264,34 @@ function getUpdateSizeMethod(graph, sizeSettings) {
if (type === 'constant') {
return attributes => (attributes.size = value)
} else if (type === 'variable') {
return getVariabledSizeMethod(mode, source, scale, min)
return attributes => {
attributes.size = getVariabledSize(
mode,
attributes.data[source],
scale,
min
)
}
} else {
return (attributes, nodeId) =>
(attributes.size = Math.max(graph[method](nodeId) * scale, min))
return (attributes, nodeId) => {
attributes.size = getVariabledSize(
mode,
graph[method](nodeId),
scale,
min
)
}
}
}
function getDirectVariableColorUpdateMethod(source) {
return attributes =>
(attributes.color = tinycolor(attributes.data[source]).toHexString())
function getDirectVariableColorUpdateMethod(source, opacity = 100) {
return attributes => {
const color = tinycolor(attributes.data[source])
const colorOpacity = color.getAlpha()
attributes.color = color
.setAlpha((opacity / 100) * colorOpacity)
.toHex8String()
}
}
function getUpdateNodeColorMethod(graph, colorSettings) {
@@ -131,10 +303,16 @@ function getUpdateNodeColorMethod(graph, colorSettings) {
colorscale,
colorscaleDirection,
mode,
method
method,
opacity
} = colorSettings
if (type === 'constant') {
return attributes => (attributes.color = value)
const color = tinycolor(value)
const colorOpacity = color.getAlpha()
return attributes =>
(attributes.color = color
.setAlpha((opacity / 100) * colorOpacity)
.toHex8String())
} else if (type === 'variable') {
return sourceUsage === 'map_to'
? getColorMethod(
@@ -143,9 +321,10 @@ function getUpdateNodeColorMethod(graph, colorSettings) {
(nodeId, attributes) => attributes.data[source],
colorscale,
colorscaleDirection,
getNodeValueScale
getNodeValueScale,
opacity
)
: getDirectVariableColorUpdateMethod(source)
: getDirectVariableColorUpdateMethod(source, opacity)
} else {
return getColorMethod(
graph,
@@ -153,7 +332,8 @@ function getUpdateNodeColorMethod(graph, colorSettings) {
nodeId => graph[method](nodeId),
colorscale,
colorscaleDirection,
getNodeValueScale
getNodeValueScale,
opacity
)
}
}
@@ -184,22 +364,13 @@ function getUpdateEdgeColorMethod(graph, colorSettings) {
}
}
function getVariabledSizeMethod(mode, source, scale, min) {
function getVariabledSize(mode, value, scale, min) {
if (mode === 'diameter') {
return attributes =>
(attributes.size = Math.max(
(attributes.data[source] / 2) * scale,
min / 2
))
return Math.max((value / 2) * scale, min / 2)
} else if (mode === 'area') {
return attributes =>
(attributes.size = Math.max(
Math.sqrt((attributes.data[source] / 2) * scale),
min / 2
))
return Math.max(Math.sqrt((value / 2) * scale), min / 2)
} else {
return attributes =>
(attributes.size = Math.max(attributes.data[source] * scale, min))
return Math.max(value * scale, min)
}
}
@@ -209,8 +380,10 @@ function getColorMethod(
sourceGetter,
selectedColorscale,
colorscaleDirection,
valueScaleGetter
valueScaleGetter,
opacity = 100
) {
const opacityFactor = opacity / 100
const valueScale = valueScaleGetter(graph, sourceGetter)
let colorscale = selectedColorscale || DEFAULT_SCALE
if (colorscaleDirection === 'reversed') {
@@ -224,10 +397,11 @@ function getColorMethod(
colorscale[index % colorscale.length]
])
)
return (attributes, nodeId) => {
const category = sourceGetter(nodeId, attributes)
attributes.color = colorMap[category]
attributes.color = tinycolor(colorMap[category])
.setAlpha(opacityFactor)
.toHex8String()
}
} else {
const min = valueScale[0]
@@ -240,20 +414,25 @@ function getColorMethod(
const value = sourceGetter(nodeId, attributes)
const normalizedValue = (value - min) / (max - min)
if (isNaN(normalizedValue)) {
attributes.color = tinycolor('#000000')
.setAlpha(opacityFactor)
.toHex8String()
return
}
const exactMatch = normalizedColorscale.find(
([value]) => value === normalizedValue
)
if (exactMatch) {
attributes.color = tinycolor(exactMatch[1]).toHexString()
attributes.color = tinycolor(exactMatch[1])
.setAlpha(opacityFactor)
.toHex8String()
return
}
const rightColorIndex = normalizedColorscale.findIndex(
([value]) => value >= normalizedValue
([value]) => value > normalizedValue
)
const leftColorIndex = (rightColorIndex || 1) - 1
const leftColorIndex = rightColorIndex - 1
const right = normalizedColorscale[rightColorIndex]
const left = normalizedColorscale[leftColorIndex]
const interpolationFactor =
@@ -270,7 +449,9 @@ function getColorMethod(
r: r0 + interpolationFactor * (r1 - r0),
g: g0 + interpolationFactor * (g1 - g0),
b: b0 + interpolationFactor * (b1 - b0)
}).toHexString()
})
.setAlpha(opacityFactor)
.toHex8String()
}
}
}
@@ -290,18 +471,3 @@ function getEdgeValueScale(graph, sourceGetter) {
}, new Set())
return Array.from(scaleSet).sort((a, b) => a - b)
}
export function getOptionsFromDataSources(dataSources) {
if (!dataSources) {
return []
}
return Object.keys(dataSources).map(name => ({
value: name,
label: name
}))
}
export default {
getOptionsFromDataSources
}

View File

@@ -1,12 +1,33 @@
export default {
_migrate(installedVersion, inquiries) {
if (installedVersion === 1) {
inquiries.forEach(inquire => {
inquire.viewType = 'chart'
inquire.viewOptions = inquire.chart
delete inquire.chart
if (installedVersion < 2) {
inquiries.forEach(inquiry => {
inquiry.viewType = 'chart'
inquiry.viewOptions = inquiry.chart
delete inquiry.chart
})
return inquiries
}
if (installedVersion < 3) {
inquiries.forEach(inquiry => {
if (inquiry.viewType === 'graph') {
inquiry.viewOptions.style.nodes.color.opacity = 100
inquiry.viewOptions.style.highlightMode = 'node_and_neighbors'
}
})
}
if (installedVersion < 4) {
inquiries.forEach(inquiry => {
if (
inquiry.viewType === 'graph' &&
inquiry.viewOptions.layout.type === 'forceAtlas2'
) {
inquiry.viewOptions.layout.options.initialAlgorithm = 'circular'
}
})
}
return inquiries
}
}

View File

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

View File

@@ -5,6 +5,7 @@ import store from '@/store'
import { createVfm, VueFinalModal, useVfm } from 'vue-final-modal'
import '@/assets/styles/variables.css'
import '@/assets/styles/typography.css'
import '@/assets/styles/buttons.css'
import '@/assets/styles/tables.css'
import '@/assets/styles/dialogs.css'

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,110 +0,0 @@
<template>
<div ref="graphContainer" class="graph-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%'
}"
>
<GraphEditor
ref="graphEditor"
:dataSources="dataSources"
:initOptions="initOptions"
:showViewSettings="showViewSettings"
@update="$emit('update')"
/>
</div>
</div>
</template>
<script>
import 'react-chart-editor/lib/react-chart-editor.css'
import events from '@/lib/utils/events'
import GraphEditor from './GraphEditor.vue'
export default {
name: 'Graph',
components: { GraphEditor },
props: {
dataSources: Object,
initOptions: Object,
exportToPngEnabled: Boolean,
exportToSvgEnabled: Boolean,
exportToHtmlEnabled: Boolean,
showViewSettings: Boolean
},
emits: [
'update:exportToSvgEnabled',
'update:exportToHtmlEnabled',
'update',
'loadingImageCompleted'
],
data() {
return {
resizeObserver: null
}
},
created() {
this.$emit('update:exportToSvgEnabled', false)
this.$emit('update:exportToHtmlEnabled', false)
},
mounted() {
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.graphContainer)
},
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.graphContainer)
},
watch: {
async showViewSettings() {
await this.$nextTick()
this.handleResize()
}
},
methods: {
getOptionsForSave() {
return this.$refs.graphEditor.settings
},
async saveAsPng() {
await this.$refs.graphEditor.saveAsPng()
this.$emit('loadingImageCompleted')
},
async prepareCopy() {
return await this.$refs.graphEditor.prepareCopy()
},
async handleResize() {
const renderer = this.$refs.graphEditor.renderer
if (renderer) {
renderer.refresh()
renderer.getCamera().animatedReset({ duration: 600 })
}
}
}
}
</script>
<style scoped>
.graph-container {
height: 100%;
}
.chart-warning {
height: 40px;
line-height: 40px;
border-bottom: 1px solid var(--color-border);
box-sizing: border-box;
}
.graph {
min-height: 242px;
}
:deep(.editor_controls .sidebar__item:before) {
width: 0;
}
</style>

View File

@@ -17,9 +17,9 @@
</template>
<script>
import Splitpanes from '@/components/Splitpanes'
import Schema from './Schema'
import Tabs from './Tabs'
import Splitpanes from '@/components/Common/Splitpanes'
import Schema from '@/components/Schema'
import Tabs from '@/components/Tabs'
import events from '@/lib/utils/events'
export default {
@@ -42,8 +42,8 @@ export default {
) {
const stmt = [
'/*',
' * Your database is empty. In order to start building charts',
' * you should create a table and insert data into it.',
' * Your database is empty. In order to start building data visualisations',
' * you should create tables and insert data into them.',
' */',
'CREATE TABLE house',
'(',
@@ -54,7 +54,20 @@ export default {
"('Gryffindor', 100),",
"('Hufflepuff', 90),",
"('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')
const tabId = await this.$store.dispatch('addTab', { query: stmt })

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import Splitpanes from '@/components/Splitpanes'
import Splitpanes from '@/components/Common/Splitpanes'
import { nextTick } from 'vue'
describe('Splitpanes.vue', () => {
@@ -49,6 +49,41 @@ describe('Splitpanes.vue', () => {
).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 () => {
// mount the component
const wrapper = shallowMount(Splitpanes, {

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -39,4 +39,262 @@ 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: 'forceAtlas2',
options: {
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false
}
}
},
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: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false
}
}
},
createdAt: '2021-05-07T11:05:50.877Z'
}
])
})
it('migrates from version 3 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',
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: 'forceAtlas2',
options: {
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false
}
}
},
createdAt: '2021-05-07T11:05:50.877Z'
}
]
expect(migrations._migrate(3, 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: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false
}
}
},
createdAt: '2021-05-07T11:05:50.877Z'
}
])
})
})

View File

@@ -18,7 +18,7 @@ describe('storedInquiries.js', () => {
expect(inquiries).to.eql([])
})
it('getStoredInquiries migrate and returns inquiries of v1', () => {
it('getStoredInquiries migrates and returns inquiries of v1', () => {
localStorage.setItem(
'myQueries',
JSON.stringify([
@@ -55,6 +55,145 @@ 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: 'forceAtlas2',
options: {
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false
}
}
},
name: 'student graph FA2',
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: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false
}
}
},
name: 'student graph FA2',
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', () => {
const data = [{ id: 1 }, { id: 2 }]
storedInquiries.updateStorage(data)
@@ -136,7 +275,7 @@ describe('storedInquiries.js', () => {
const str = storedInquiries.serialiseInquiries(inquiryList)
const parsedJson = JSON.parse(str)
expect(parsedJson.version).to.equal(2)
expect(parsedJson.version).to.equal(4)
expect(parsedJson.inquiries).to.have.lengthOf(2)
expect(parsedJson.inquiries[1]).to.eql(inquiryList[1])
expect(parsedJson.inquiries[0]).to.eql({
@@ -149,7 +288,7 @@ describe('storedInquiries.js', () => {
})
})
it('deserialiseInquiries migrates inquiries', () => {
it('deserialiseInquiries migrates inquiries of v1', () => {
const str = `[
{
"id": 1,
@@ -188,7 +327,141 @@ describe('storedInquiries.js', () => {
])
})
it('deserialiseInquiries return array for one inquiry of v1', () => {
it('deserialiseInquiries migrates inquiries of v2', () => {
const str = `{
"version": 2,
"inquiries": [
{
"id": 1,
"name": "foo",
"query": "select * from foo",
"viewType": "chart",
"viewOptions": [],
"createdAt": "2020-11-03T14:17:49.524Z"
},
{
"id": "Xh1Hc9v7P3mRPZVM59QiC",
"query": "SELECT * from doc",
"viewType": "graph",
"viewOptions": {
"structure": {
"nodeId": "node_id",
"objectType": "object_type",
"edgeSource": "source",
"edgeTarget": "target"
},
"style": {
"backgroundColor": "white",
"nodes": {
"size": { "type": "constant", "value": 10 },
"color": {
"type": "calculated",
"method": "degree",
"colorscale": null,
"mode": "continious",
"colorscaleDirection": "reversed"
},
"label": { "source": "label", "color": "#444444" }
},
"edges": {
"showDirection": true,
"size": { "type": "constant", "value": 2 },
"color": { "type": "constant", "value": "#a2b1c6" },
"label": { "source": null, "color": "#a2b1c6" }
}
},
"layout": {
"type": "forceAtlas2",
"options": {
"initialIterationsAmount": 50,
"adjustSizes": false,
"barnesHutOptimize": false,
"barnesHutTheta": 0.5,
"edgeWeightInfluence": 0,
"gravity": 1,
"linLogMode": false,
"outboundAttractionDistribution": false,
"scalingRatio": 1,
"slowDown": 1,
"strongGravityMode": false
}
}
},
"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: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false
}
}
},
name: 'student graph',
createdAt: '2026-01-19T21:46:13.899Z'
}
])
})
it('deserialiseInquiries returns array for one inquiry of v1', () => {
const str = `
{
"id": 1,
@@ -215,7 +488,7 @@ describe('storedInquiries.js', () => {
it('deserialiseInquiries generates new id to avoid duplication', () => {
storedInquiries.updateStorage([{ id: 1 }])
const str = `{
"version": 2,
"version": 3,
"inquiries": [
{
"id": 1,
@@ -275,7 +548,7 @@ describe('storedInquiries.js', () => {
it('importInquiries', async () => {
const str = `{
"version": 2,
"version": 4,
"inquiries": [{
"id": 1,
"name": "foo",
@@ -300,7 +573,7 @@ describe('storedInquiries.js', () => {
])
})
it('readPredefinedInquiries old', async () => {
it('readPredefinedInquiries v1', async () => {
const str = `[
{
"id": 1,
@@ -325,18 +598,70 @@ describe('storedInquiries.js', () => {
])
})
it('readPredefinedInquiries', async () => {
it('readPredefinedInquiries v2', async () => {
const str = `{
"version": 2,
"inquiries": [
{
"id": 1,
"name": "foo",
"query": "select * from foo",
"viewType": "chart",
"viewOptions": [],
"createdAt": "2020-11-03T14:17:49.524Z"
}]
{
"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": "forceAtlas2",
"options": {
"initialIterationsAmount": 50,
"adjustSizes": false,
"barnesHutOptimize": false,
"barnesHutTheta": 0.5,
"edgeWeightInfluence": 0,
"gravity": 1,
"linLogMode": false,
"outboundAttractionDistribution": false,
"scalingRatio": 1,
"slowDown": 1,
"strongGravityMode": false
}
}
},
"name": "student graph",
"createdAt": "2026-01-19T21:46:13.899Z"
}
]
}
`
sinon.stub(fu, 'readFile').returns(Promise.resolve(new Response(str)))
@@ -350,6 +675,106 @@ describe('storedInquiries.js', () => {
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: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false
}
}
},
name: 'student graph',
createdAt: '2026-01-19T21:46:13.899Z'
}
])
})
it('readPredefinedInquiries', async () => {
const str = `{
"version": 4,
"inquiries": [
{
"id": 1,
"name": "foo",
"query": "select * from foo",
"viewType": "chart",
"viewOptions": [],
"createdAt": "2020-11-03T14:17:49.524Z"
},
{
"id": 2,
"name": "boo",
"query": "select * from boo",
"viewType": "graph",
"viewOptions": {},
"createdAt": "2020-11-03T14:17:49.524Z"
}
]
}
`
sinon.stub(fu, 'readFile').returns(Promise.resolve(new Response(str)))
const inquiries = await storedInquiries.readPredefinedInquiries()
expect(fu.readFile.calledOnceWith('./inquiries.json')).to.equal(true)
expect(inquiries).to.eql([
{
id: 1,
name: 'foo',
query: 'select * from foo',
viewType: 'chart',
viewOptions: [],
createdAt: '2020-11-03T14:17:49.524Z'
},
{
id: 2,
name: 'boo',
query: 'select * from boo',
viewType: 'graph',
viewOptions: {},
createdAt: '2020-11-03T14:17:49.524Z'
}
])
})

20
tests/testUtils.js Normal file
View File

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

View File

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

View File

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