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

Compare commits

..

21 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
32 changed files with 3649 additions and 338 deletions

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

@@ -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

219
package-lock.json generated
View File

@@ -1,12 +1,13 @@
{
"name": "sqliteviz",
"version": "0.28.2",
"version": "0.29.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sqliteviz",
"version": "0.28.2",
"version": "0.29.0",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@sigma/export-image": "^3.0.0",
@@ -29,7 +30,7 @@
"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",
@@ -64,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",
@@ -3093,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",
@@ -4776,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,
@@ -7809,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,
@@ -10358,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"
@@ -10385,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,
@@ -10683,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,
@@ -12318,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,
@@ -12550,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,
@@ -12994,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"
@@ -14454,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,
@@ -15571,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.28.2",
"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,7 +34,7 @@
"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",
@@ -68,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

@@ -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

@@ -11,6 +11,7 @@
:initOptions="initOptionsByMode[mode]"
:data-sources="dataSource"
:showViewSettings="showViewSettings"
:showValueViewer="viewValuePanelVisible"
@loading-image-completed="loadingImage = false"
@update="$emit('update')"
/>
@@ -56,6 +57,17 @@
<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
@@ -126,6 +138,7 @@ 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/Common/LoadingDialog.vue'
import time from '@/lib/utils/time'
@@ -144,6 +157,7 @@ export default {
GraphIcon,
SettingsIcon,
ExportToSvgIcon,
ViewCellValueIcon,
PngIcon,
HtmlIcon,
ClipboardIcon,
@@ -172,7 +186,8 @@ export default {
graph: this.initMode === 'graph' ? this.initOptions : null
},
showLoadingDialog: false,
showViewSettings: true
showViewSettings: true,
viewValuePanelVisible: false
}
},
computed: {

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)"

View File

@@ -102,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

@@ -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

@@ -67,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">
@@ -162,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"
@@ -173,6 +193,7 @@
<Button
variant="secondary"
class="test_fa2_reset"
title="Set the settings to default or previously saved ones."
@click="resetFA2LayoutSettings"
>
Reset
@@ -183,16 +204,25 @@
@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>
@@ -225,19 +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 * 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'
@@ -262,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: {
@@ -277,7 +314,7 @@ export default {
initOptions: Object,
showViewSettings: Boolean
},
emits: ['update'],
emits: ['update', 'selectItem', 'clearSelection'],
data() {
return {
graph: new Graph({ multi: true, allowSelfLoops: true }),
@@ -300,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))
: {
@@ -312,6 +358,7 @@ export default {
},
style: {
backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: {
size: {
type: 'constant',
@@ -352,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: {
@@ -379,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: {
@@ -423,6 +518,7 @@ export default {
},
methods: {
buildGraph() {
this.clearSelection()
if (this.renderer) {
this.renderer.kill()
}
@@ -440,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
},
@@ -505,19 +674,21 @@ export default {
this.fa2Layout.kill()
}
if (layoutType === 'circular') {
circular.assign(this.graph)
return
}
this.layoutMethodMap[layoutType]()
if (layoutType === 'random') {
if (layoutType === 'forceAtlas2' && layoutType !== prevLayout) {
this.autoRunFA2Layout()
}
},
applyCircularLayout() {
circular.assign(this.graph)
},
applyRandomLayout() {
random.assign(this.graph, {
rng: seedrandom(this.settings.layout.options.seedValue)
})
return
}
if (layoutType === 'circlepack') {
},
applyCirclePackLayout() {
this.graph.forEachNode(nodeId => {
this.graph.updateNode(nodeId, attributes => {
const newAttributes = { ...attributes }
@@ -547,33 +718,32 @@ export default {
) || [],
rng: seedrandom(this.settings.layout.options.seedValue)
})
return
}
if (layoutType === 'forceAtlas2') {
},
applyFA2Layout() {
if (
!this.graph.someNode(
(nodeKey, attributes) =>
typeof attributes.x === 'number' &&
typeof attributes.y === 'number'
typeof attributes.x === 'number' && typeof attributes.y === 'number'
)
) {
circular.assign(this.graph)
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: this.settings.layout.options
settings: fa2settings
})
)
if (layoutType !== prevLayout) {
this.autoRunFA2Layout()
}
}
},
toggleFA2Layout() {
if (this.fa2Layout.isRunning()) {
@@ -594,6 +764,15 @@ export default {
this.checkIteration = null
}
},
restartFA2Layout() {
if (this.fa2Layout.isRunning()) {
this.stopFA2Layout()
}
this.fa2Layout.kill()
clearNodeCoordinates(this.graph)
this.applyFA2Layout()
this.autoRunFA2Layout()
},
autoRunFA2Layout() {
let iteration = 1
this.checkIteration = () => {
@@ -611,6 +790,7 @@ export default {
setRecommendedFA2Settings() {
const sensibleSettings = forceAtlas2.default.inferSettings(this.graph)
this.settings.layout.options = {
initialAlgorithm: 'circular',
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
@@ -672,4 +852,8 @@ export default {
flex-grow: 1;
flex-basis: 0;
}
.force-atlas-buttons :deep(.button__icon > div) {
padding: 0 3px;
}
</style>

View File

@@ -139,7 +139,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,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

@@ -13,21 +13,38 @@
documentation</a
>.
</div>
<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>
@@ -35,17 +52,20 @@
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 },
components: { GraphEditor, ValueViewer, Splitpanes },
props: {
dataSources: Object,
initOptions: Object,
exportToPngEnabled: Boolean,
exportToSvgEnabled: Boolean,
exportToHtmlEnabled: Boolean,
showViewSettings: Boolean
showViewSettings: Boolean,
showValueViewer: Boolean
},
emits: [
'update:exportToSvgEnabled',
@@ -57,7 +77,8 @@ export default {
],
data() {
return {
resizeObserver: null
resizeObserver: null,
selectedItem: null
}
},
computed: {
@@ -83,10 +104,10 @@ export default {
},
mounted() {
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.graphContainer)
this.resizeObserver.observe(this.$refs.graphEditorContainer)
},
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.graphContainer)
this.resizeObserver.unobserve(this.$refs.graphEditorContainer)
},
methods: {
getOptionsForSave() {
@@ -100,7 +121,7 @@ export default {
return this.$refs.graphEditor.prepareCopy()
},
async handleResize() {
const renderer = this.$refs.graphEditor.renderer
const renderer = this.$refs.graphEditor?.renderer
if (renderer) {
renderer.refresh()
renderer.getCamera().animatedReset({ duration: 600 })

View File

@@ -1,27 +1,57 @@
<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 class="result-set-container">
<div
:id="'run-result-left-pane-' + tab.id"
class="result-set-container"
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>
<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="
:empty="!selectedCell"
emptyMessage="No cell selected to view"
:value="
selectedCell
? result.values[result.columns[selectedCell.dataset.col]][
selectedCell.dataset.row
@@ -29,12 +59,8 @@
: ''
"
/>
<div v-show="!selectedCell" class="table-preview">
No cell selected to view
</div>
</div>
</template>
</component>
</splitpanes>
<side-tool-bar panel="table" @switch-to="$emit('switchTo', $event)">
<icon-button
@@ -89,48 +115,6 @@
@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>
@@ -151,7 +135,7 @@ import cIo from '@/lib/utils/clipboardIo'
import time from '@/lib/utils/time'
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

@@ -37,7 +37,6 @@
:disabled="!enableTeleport"
>
<run-result
:tab="tab"
:result="tab.result"
:isGettingResults="tab.isGettingResults"
:error="tab.error"

View File

@@ -1,5 +1,6 @@
<template>
<div class="value-viewer">
<template v-if="!empty">
<div class="value-viewer-toolbar">
<button
v-for="format in formats"
@@ -12,7 +13,9 @@
{{ format.text }}
</button>
<button type="button" class="copy" @click="copyToClipboard">Copy</button>
<button type="button" class="copy" @click="copyToClipboard">
Copy
</button>
<button
type="button"
class="line-wrap"
@@ -44,6 +47,11 @@
class="messages"
/>
</div>
</template>
<div v-show="empty" class="empty-message">
{{ emptyMessage }}
</div>
</div>
</template>
@@ -60,12 +68,19 @@ import cIo from '@/lib/utils/clipboardIo'
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() {
currentFormat: {
immediate: true,
handler() {
this.messages = []
this.formattedJson = ''
if (this.currentFormat === 'json') {
this.formatJson(this.cellValue)
this.formatJson(this.value)
}
}
},
cellValue() {
value: {
immediate: true,
handler() {
this.messages = []
if (this.currentFormat === 'json') {
this.formatJson(this.cellValue)
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

@@ -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>

View File

@@ -1,21 +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

@@ -36,6 +36,13 @@ export function dataSourceIsValid(dataSources) {
}
}
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]
const { objectType, nodeId } = options.structure
@@ -127,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 => {

View File

@@ -12,6 +12,18 @@ export default {
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'
}
})
}

View File

@@ -5,9 +5,10 @@ import migration from './_migrations'
const migrate = migration._migrate
const myInquiriesKey = 'myInquiries'
const latestVersion = 4
export default {
version: 3,
version: latestVersion,
myInquiriesKey,
getStoredInquiries() {
let myInquiries = JSON.parse(localStorage.getItem(myInquiriesKey))
@@ -21,8 +22,8 @@ export default {
return []
}
if (myInquiries.version === 2) {
myInquiries = migrate(2, myInquiries.inquiries)
if (myInquiries.version < latestVersion) {
myInquiries = migrate(myInquiries.version, myInquiries.inquiries)
this.updateStorage(myInquiries)
return myInquiries
}
@@ -69,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 || []
}
@@ -108,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

@@ -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

@@ -418,4 +418,52 @@ describe('DataView.vue', () => {
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

@@ -2,6 +2,7 @@ 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')
@@ -163,6 +164,326 @@ describe('Graph.vue', () => {
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,

View File

@@ -1309,7 +1309,7 @@ describe('GraphEditor', () => {
expect(startSpy.calledOnce).to.equal(true)
await waitCondition(() => stopSpy.callCount === 1)
expect(wrapper.text()).to.contain('Start')
expect(wrapper.text()).to.contain('Continue')
const coordinates = graph
.export()
@@ -1349,6 +1349,7 @@ describe('GraphEditor', () => {
layout: {
type: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
@@ -1383,7 +1384,7 @@ describe('GraphEditor', () => {
expect(startSpy.calledOnce).to.equal(true)
await waitCondition(() => stopSpy.callCount === 1)
expect(wrapper.text()).to.contain('Start')
expect(wrapper.text()).to.contain('Continue')
const initialCoordinates = graph
.export()
@@ -1397,7 +1398,7 @@ describe('GraphEditor', () => {
new Event('blur', { bubbles: true })
)
// Call nextTick after setting number input,
// otherwise the value will be changed beck to initial for some reason
// otherwise the value will be changed back to initial for some reason
await nextTick()
expect(wrapper.vm.settings.layout.options.gravity).to.equal(12)
@@ -1405,6 +1406,30 @@ describe('GraphEditor', () => {
// Algorithm wasn't called
expect(startSpy.calledOnce).to.equal(true)
// Change initial algorithm
await wrapper
.find(
'.test_fa2_initial_layout_algorithm_select .dropdown-container .Select__indicator'
)
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[1].trigger('click')
await nextTick()
expect(wrapper.vm.settings.layout.options.initialAlgorithm).to.equal(
'random'
)
// Change seed value
const seedValueInput = wrapper.find('.test_seed_value input')
await seedValueInput.setValue(123)
seedValueInput.wrapperElement.dispatchEvent(
new Event('blur', { bubbles: true })
)
await nextTick()
expect(wrapper.vm.settings.layout.options.seedValue).to.equal(123)
// Change scaling ratio
const scalingInput = wrapper.find('.test_fa2_scaling input')
await scalingInput.setValue(2)
@@ -1493,13 +1518,13 @@ describe('GraphEditor', () => {
false
)
// Click Start
// Click Continue
const toggleButton = wrapper.find('button.test_fa2_toggle')
await toggleButton.trigger('click')
expect(toggleButton.text()).to.contain('Stop')
expect(toggleButton.text()).to.contain('Pause')
expect(startSpy.callCount).to.equal(2)
// Wait a bit and click Stop
// Wait a bit and click Pause
await time.sleep(500)
await toggleButton.trigger('click')
expect(stopSpy.callCount).to.equal(2)
@@ -1513,9 +1538,479 @@ describe('GraphEditor', () => {
// Click Reset
await wrapper.find('button.test_fa2_reset').trigger('click')
expect(toggleButton.text()).to.contain('Start')
expect(toggleButton.text()).to.contain('Continue')
expect(startSpy.callCount).to.equal(2)
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'circular',
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
})
wrapper.unmount()
})
it('FA2: restarts and applies selected initial layout', async () => {
const stopSpy = sinon.spy(FA2Layout.prototype, 'stop')
const startSpy = sinon.spy(FA2Layout.prototype, 'start')
const wrapper = mount(GraphEditor, {
attachTo: document.body,
props: {
dataSources: {
doc: [
'{"type": 0, "node_id": 1, "size": 20}',
'{"type": 0, "node_id": 2, "size": 2}',
'{"type": 0, "node_id": 3, "size": 2}',
'{"type": 0, "node_id": 4, "size": 2}',
'{"type": 1, "source": 1, "target": 3, "wgt": 20}',
'{"type": 1, "source": 1, "target": 2, "wgt": 15}',
'{"type": 1, "source": 1, "target": 4, "wgt": 5}'
]
},
initOptions: {
...defaultInitOptions,
structure: {
nodeId: 'node_id',
objectType: 'type',
edgeSource: 'source',
edgeTarget: 'target'
},
layout: {
type: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
}
}
},
showViewSettings: true
},
global: {
provide: {
tabLayout: { dataView: 'above' }
}
}
})
const graph = wrapper.vm.graph
const styleMenuItem = wrapper.findAll('.sidebar__group__title')[1]
await styleMenuItem.trigger('click')
const layoutMenuItem = wrapper.findAll('.sidebar__item')[4]
await layoutMenuItem.trigger('click')
expect(startSpy.calledOnce).to.equal(true)
await waitCondition(() => stopSpy.callCount === 1)
const initialCoordinates = graph
.export()
.nodes.map(node => `x:${node.attributes.x},y:${node.attributes.y}`)
.join()
// Change initial algorithm to Random
await wrapper
.find(
'.test_fa2_initial_layout_algorithm_select .dropdown-container .Select__indicator'
)
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[1].trigger('click')
await nextTick()
// Click Restart
const restartButton = wrapper.find('button.test_fa2_restart')
await restartButton.trigger('click')
const toggleButton = wrapper.find('button.test_fa2_toggle')
expect(toggleButton.text()).to.contain('Pause')
expect(startSpy.callCount).to.equal(2)
// Wait until restarting finished
await waitCondition(() => stopSpy.callCount === 2)
const randomCoordinates1 = graph
.export()
.nodes.map(node => `x:${node.attributes.x},y:${node.attributes.y}`)
.join()
// Change seed value to 123
let seedValueInput = wrapper.find('.test_seed_value input')
await seedValueInput.setValue(123)
seedValueInput.wrapperElement.dispatchEvent(
new Event('blur', { bubbles: true })
)
await nextTick()
// Click Restart
await restartButton.trigger('click')
expect(toggleButton.text()).to.contain('Pause')
expect(startSpy.callCount).to.equal(3)
// Wait until restarting finished
await waitCondition(() => stopSpy.callCount === 3)
const randomCoordinates2 = graph
.export()
.nodes.map(node => `x:${node.attributes.x},y:${node.attributes.y}`)
.join()
// Change seed value back to 1
await seedValueInput.setValue(1)
seedValueInput.wrapperElement.dispatchEvent(
new Event('blur', { bubbles: true })
)
await nextTick()
// Click Restart
await restartButton.trigger('click')
// Wait until restarting finished
await waitCondition(() => stopSpy.callCount === 4)
const randomCoordinates1After = graph
.export()
.nodes.map(node => `x:${node.attributes.x},y:${node.attributes.y}`)
.join()
// Change initial algorithm to CirclePack
await wrapper
.find(
'.test_fa2_initial_layout_algorithm_select .dropdown-container .Select__indicator'
)
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[2].trigger('click')
await nextTick()
// Change seed value to 456
seedValueInput = wrapper.find('.test_seed_value input')
await seedValueInput.setValue(456)
seedValueInput.wrapperElement.dispatchEvent(
new Event('blur', { bubbles: true })
)
await nextTick()
// Set another hierarchy
const hierarchyInput = wrapper.find(
'.multiselect.sqliteviz-select .multiselect__select'
)
await hierarchyInput.trigger('mousedown')
await wrapper
.find('ul.multiselect__content')
.findAll('li')[2]
.find('span')
.trigger('click')
await nextTick()
// Click Restart
await restartButton.trigger('click')
// Wait until restarting finished
await waitCondition(() => stopSpy.callCount === 5)
const circlePackCoordinates = graph
.export()
.nodes.map(node => `x:${node.attributes.x},y:${node.attributes.y}`)
.join()
expect(initialCoordinates).not.to.equal(randomCoordinates1)
expect(randomCoordinates1).not.to.equal(randomCoordinates2)
expect(randomCoordinates1).to.equal(randomCoordinates1After)
expect(circlePackCoordinates).not.to.equal(randomCoordinates1After)
expect(circlePackCoordinates).not.to.equal(initialCoordinates)
wrapper.unmount()
})
it('FA2: restore previously set parameters for initial layout', async () => {
const wrapper = mount(GraphEditor, {
attachTo: document.body,
props: {
dataSources: {
doc: [
'{"type": 0, "node_id": 1}',
'{"type": 0, "node_id": 2}',
'{"type": 1, "source": 1, "target": 2, "wgt": 15}'
]
},
initOptions: {
...defaultInitOptions,
structure: {
nodeId: 'node_id',
objectType: 'type',
edgeSource: 'source',
edgeTarget: 'target'
},
layout: {
type: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
}
}
},
showViewSettings: true
},
global: {
provide: {
tabLayout: { dataView: 'above' }
}
}
})
const styleMenuItem = wrapper.findAll('.sidebar__group__title')[1]
await styleMenuItem.trigger('click')
const layoutMenuItem = wrapper.findAll('.sidebar__item')[4]
await layoutMenuItem.trigger('click')
// Change initial algorithm to Random
await wrapper
.find(
'.test_fa2_initial_layout_algorithm_select .dropdown-container .Select__indicator'
)
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[1].trigger('click')
await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'random',
seedValue: 1,
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
})
// Change seed value to 123
let seedValueInput = wrapper.find('.test_seed_value input')
await seedValueInput.setValue(123)
seedValueInput.wrapperElement.dispatchEvent(
new Event('blur', { bubbles: true })
)
await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'random',
seedValue: 123,
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
})
// Change initial algorithm to CirclePack
await wrapper
.find(
'.test_fa2_initial_layout_algorithm_select .dropdown-container .Select__indicator'
)
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[2].trigger('click')
await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'circlepack',
seedValue: 1,
hierarchyAttributes: [],
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
})
// Change seed value to 456
seedValueInput = wrapper.find('.test_seed_value input')
await seedValueInput.setValue(456)
seedValueInput.wrapperElement.dispatchEvent(
new Event('blur', { bubbles: true })
)
await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'circlepack',
seedValue: 456,
hierarchyAttributes: [],
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
})
// Set another hierarchy
const hierarchyInput = wrapper.find(
'.multiselect.sqliteviz-select .multiselect__select'
)
await hierarchyInput.trigger('mousedown')
await wrapper
.find('ul.multiselect__content')
.findAll('li')[1]
.find('span')
.trigger('click')
await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'circlepack',
seedValue: 456,
hierarchyAttributes: ['node_id'],
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
})
// Change initial algorithm to Random
await wrapper
.find(
'.test_fa2_initial_layout_algorithm_select .dropdown-container .Select__indicator'
)
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[1].trigger('click')
await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'random',
seedValue: 123,
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
})
// Change initial algorithm to Circular
await wrapper
.find(
'.test_fa2_initial_layout_algorithm_select .dropdown-container .Select__indicator'
)
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[0].trigger('click')
await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'circular',
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
adjustSizes: true,
barnesHutOptimize: true,
barnesHutTheta: 0.5,
strongGravityMode: false,
linLogMode: true,
outboundAttractionDistribution: false,
slowDown: 1,
weightSource: 'wgt',
edgeWeightInfluence: 0.5
})
// Change initial algorithm to CirclePack
await wrapper
.find(
'.test_fa2_initial_layout_algorithm_select .dropdown-container .Select__indicator'
)
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[2].trigger('click')
await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'circlepack',
seedValue: 456,
hierarchyAttributes: ['node_id'],
initialIterationsAmount: 55,
gravity: 1.5,
scalingRatio: 1.2,
@@ -1603,6 +2098,26 @@ describe('GraphEditor', () => {
)
await nextTick()
// Change initial algorithm
await wrapper
.find(
'.test_fa2_initial_layout_algorithm_select .dropdown-container .Select__indicator'
)
.wrapperElement.dispatchEvent(
new MouseEvent('mousedown', { bubbles: true })
)
await wrapper.findAll('.Select__menu .Select__option')[1].trigger('click')
await nextTick()
// Change seed value
const seedValueInput = wrapper.find('.test_seed_value input')
await seedValueInput.setValue(123)
seedValueInput.wrapperElement.dispatchEvent(
new Event('blur', { bubbles: true })
)
await nextTick()
// Change scaling ratio
const scalingInput = wrapper.find('.test_fa2_scaling input')
await scalingInput.setValue(2)
@@ -1671,6 +2186,8 @@ describe('GraphEditor', () => {
await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'random',
seedValue: 123,
initialIterationsAmount: 120,
gravity: 12,
scalingRatio: 2,
@@ -1756,6 +2273,7 @@ describe('GraphEditor', () => {
layout: {
type: 'forceAtlas2',
options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 50,
gravity: 1.5,
scalingRatio: 1.2,
@@ -1788,7 +2306,7 @@ describe('GraphEditor', () => {
expect(startSpy.calledOnce).to.equal(true)
await waitCondition(() => stopSpy.callCount === 1)
// Click Start
// Click Continue
const toggleButton = wrapper.find('button.test_fa2_toggle')
await toggleButton.trigger('click')
expect(startSpy.callCount).to.equal(2)
@@ -1884,7 +2402,7 @@ describe('GraphEditor', () => {
expect(startSpy.calledOnce).to.equal(true)
await waitCondition(() => stopSpy.callCount === 1)
expect(wrapper.text()).to.contain('Start')
expect(wrapper.text()).to.contain('Continue')
const coordinates = graph
.export()

View File

@@ -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

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { mount } from '@vue/test-utils'
import ValueViewer from '@/components/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

@@ -92,9 +92,24 @@ describe('storedInquiries.js', () => {
label: { source: null, color: '#a2b1c6' }
}
},
layout: { type: 'circular', options: null }
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',
name: 'student graph FA2',
updatedAt: '2026-01-19T21:49:40.708Z',
createdAt: '2026-01-19T21:46:13.899Z'
},
@@ -125,6 +140,7 @@ describe('storedInquiries.js', () => {
},
style: {
backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: {
size: { type: 'constant', value: 10 },
color: {
@@ -144,9 +160,25 @@ describe('storedInquiries.js', () => {
label: { source: null, color: '#a2b1c6' }
}
},
layout: { type: 'circular', options: null }
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',
name: 'student graph FA2',
updatedAt: '2026-01-19T21:49:40.708Z',
createdAt: '2026-01-19T21:46:13.899Z'
},
@@ -243,7 +275,7 @@ describe('storedInquiries.js', () => {
const str = storedInquiries.serialiseInquiries(inquiryList)
const parsedJson = JSON.parse(str)
expect(parsedJson.version).to.equal(3)
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({
@@ -256,7 +288,7 @@ describe('storedInquiries.js', () => {
})
})
it('deserialiseInquiries migrates inquiries', () => {
it('deserialiseInquiries migrates inquiries of v1', () => {
const str = `[
{
"id": 1,
@@ -295,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,
@@ -382,7 +548,7 @@ describe('storedInquiries.js', () => {
it('importInquiries', async () => {
const str = `{
"version": 3,
"version": 4,
"inquiries": [{
"id": 1,
"name": "foo",
@@ -407,7 +573,7 @@ describe('storedInquiries.js', () => {
])
})
it('readPredefinedInquiries old', async () => {
it('readPredefinedInquiries v1', async () => {
const str = `[
{
"id": 1,
@@ -432,9 +598,9 @@ describe('storedInquiries.js', () => {
])
})
it('readPredefinedInquiries', async () => {
it('readPredefinedInquiries v2', async () => {
const str = `{
"version": 3,
"version": 2,
"inquiries": [
{
"id": 1,
@@ -443,7 +609,59 @@ 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",
"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)))
@@ -457,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'
}
])
})