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 . # docker build -t sqliteviz/test -f Dockerfile.test .
# #
FROM node:12.22-bullseye FROM node:18.20.8-bookworm
RUN set -ex; \ RUN set -ex; \
apt update; \ apt update; \
apt install -y chromium firefox-esr; \ apt install -y chromium firefox-esr; \
npm install -g npm@7 npm install -g npm@10
WORKDIR /tmp/build WORKDIR /tmp/build
@@ -19,6 +19,6 @@ RUN npm install
COPY . . COPY . .
RUN set -ex; \ 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 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.w3c_touch_events.enabled': 1,
'dom.events.asyncClipboard.clipboardItem': true 'dom.events.asyncClipboard.clipboardItem': true
} }
},
ChromiumHeadlessWebGL: {
base: 'ChromiumHeadless',
flags: [
'--headless=new',
'--use-angle=swiftshader',
'--use-gl=angle',
'--enable-webgl',
'--ignore-gpu-blocklist',
'--disable-gpu-sandbox',
'--no-sandbox'
]
} }
}, },
// start these browsers // start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['ChromiumHeadless', 'FirefoxHeadlessTouch'], browsers: ['ChromiumHeadlessWebGL', 'FirefoxHeadlessTouch'],
// Continuous Integration mode // Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits // if true, Karma captures browsers, runs the tests and exits

219
package-lock.json generated
View File

@@ -1,12 +1,13 @@
{ {
"name": "sqliteviz", "name": "sqliteviz",
"version": "0.28.2", "version": "0.29.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sqliteviz", "name": "sqliteviz",
"version": "0.28.2", "version": "0.29.0",
"hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@sigma/export-image": "^3.0.0", "@sigma/export-image": "^3.0.0",
@@ -29,7 +30,7 @@
"react-chart-editor": "^0.46.1", "react-chart-editor": "^0.46.1",
"react-dom": "^16.14.0", "react-dom": "^16.14.0",
"seedrandom": "^3.0.5", "seedrandom": "^3.0.5",
"sigma": "^3.0.1", "sigma": "3.0.2",
"sql.js": "file:./lib/sql-js", "sql.js": "file:./lib/sql-js",
"tiny-emitter": "^2.1.0", "tiny-emitter": "^2.1.0",
"tinycolor2": "^1.4.2", "tinycolor2": "^1.4.2",
@@ -64,6 +65,8 @@
"karma-spec-reporter": "^0.0.36", "karma-spec-reporter": "^0.0.36",
"karma-vite": "^1.0.5", "karma-vite": "^1.0.5",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"patch-package": "^8.0.1",
"postinstall-postinstall": "^2.1.0",
"prettier": "3.5.3", "prettier": "3.5.3",
"process": "^0.11.10", "process": "^0.11.10",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
@@ -3093,6 +3096,12 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true "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": { "node_modules/abbrev": {
"version": "2.0.0", "version": "2.0.0",
"license": "ISC", "license": "ISC",
@@ -4776,6 +4785,21 @@
"node": ">=6.0" "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": { "node_modules/cipher-base": {
"version": "1.0.6", "version": "1.0.6",
"dev": true, "dev": true,
@@ -7809,6 +7833,15 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/flat-cache": {
"version": "3.2.0", "version": "3.2.0",
"dev": true, "dev": true,
@@ -10358,11 +10391,36 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1", "version": "1.0.1",
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/json-stringify-pretty-compact": {
"version": "4.0.0", "version": "4.0.0",
"license": "MIT" "license": "MIT"
@@ -10385,6 +10443,15 @@
"graceful-fs": "^4.1.6" "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": { "node_modules/jsonpointer": {
"version": "5.0.1", "version": "5.0.1",
"dev": true, "dev": true,
@@ -10683,6 +10750,15 @@
"node": ">=0.10.0" "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": { "node_modules/lazy-cache": {
"version": "1.0.4", "version": "1.0.4",
"dev": true, "dev": true,
@@ -12318,6 +12394,22 @@
"wrappy": "1" "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": { "node_modules/optimist": {
"version": "0.6.1", "version": "0.6.1",
"dev": true, "dev": true,
@@ -12550,6 +12642,106 @@
"node": ">=0.10.0" "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": { "node_modules/path-browserify": {
"version": "1.0.1", "version": "1.0.1",
"dev": true, "dev": true,
@@ -12994,6 +13186,13 @@
"version": "4.2.0", "version": "4.2.0",
"license": "MIT" "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": { "node_modules/potpack": {
"version": "1.0.2", "version": "1.0.2",
"license": "ISC" "license": "ISC"
@@ -14454,6 +14653,15 @@
"node": ">=4" "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": { "node_modules/smob": {
"version": "1.5.0", "version": "1.5.0",
"dev": true, "dev": true,
@@ -15571,9 +15779,10 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/tmp": { "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, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=14.14" "node": ">=14.14"
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "sqliteviz", "name": "sqliteviz",
"version": "0.28.2", "version": "0.30.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": true, "private": true,
"type": "module", "type": "module",
@@ -10,7 +10,8 @@
"serve": "vite preview", "serve": "vite preview",
"test": "karma start karma.conf.cjs", "test": "karma start karma.conf.cjs",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src", "lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
"format": "prettier . --write" "format": "prettier . --write",
"postinstall": "patch-package"
}, },
"dependencies": { "dependencies": {
"@sigma/export-image": "^3.0.0", "@sigma/export-image": "^3.0.0",
@@ -33,7 +34,7 @@
"react-chart-editor": "^0.46.1", "react-chart-editor": "^0.46.1",
"react-dom": "^16.14.0", "react-dom": "^16.14.0",
"seedrandom": "^3.0.5", "seedrandom": "^3.0.5",
"sigma": "^3.0.1", "sigma": "3.0.2",
"sql.js": "file:./lib/sql-js", "sql.js": "file:./lib/sql-js",
"tiny-emitter": "^2.1.0", "tiny-emitter": "^2.1.0",
"tinycolor2": "^1.4.2", "tinycolor2": "^1.4.2",
@@ -68,6 +69,8 @@
"karma-spec-reporter": "^0.0.36", "karma-spec-reporter": "^0.0.36",
"karma-vite": "^1.0.5", "karma-vite": "^1.0.5",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"patch-package": "^8.0.1",
"postinstall-postinstall": "^2.1.0",
"prettier": "3.5.3", "prettier": "3.5.3",
"process": "^0.11.10", "process": "^0.11.10",
"url-loader": "^4.1.1", "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" :style="movableSplitterStyle"
/> />
<div <div
v-show="!before.hidden"
ref="left" ref="left"
class="splitpanes-pane" class="splitpanes-pane"
:size="paneBefore.size"
max-size="30"
:style="styles.before" :style="styles.before"
> >
<slot name="left-pane" /> <slot name="left-pane" />
</div> </div>
<!-- Splitter start--> <!-- Splitter start-->
<div <div
v-show="!before.hidden && !after.hidden"
class="splitpanes-splitter" class="splitpanes-splitter"
@mousedown="bindEvents" @mousedown="bindEvents"
@touchstart="bindEvents" @touchstart="bindEvents"
@@ -64,7 +64,12 @@
</div> </div>
</div> </div>
<!-- splitter end --> <!-- splitter end -->
<div ref="right" class="splitpanes-pane" :style="styles.after"> <div
v-show="!after.hidden"
ref="right"
class="splitpanes-pane"
:style="styles.after"
>
<slot name="right-pane" /> <slot name="right-pane" />
</div> </div>
</div> </div>
@@ -114,10 +119,12 @@ export default {
styles() { styles() {
return { return {
before: { before: {
[this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%` [this.horizontal ? 'height' : 'width']:
`${this.after.hidden ? 100 : this.paneBefore.size}%`
}, },
after: { after: {
[this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%` [this.horizontal ? 'height' : 'width']:
`${this.before.hidden ? 100 : this.paneAfter.size}%`
} }
} }
}, },

View File

@@ -11,6 +11,7 @@
:initOptions="initOptionsByMode[mode]" :initOptions="initOptionsByMode[mode]"
:data-sources="dataSource" :data-sources="dataSource"
:showViewSettings="showViewSettings" :showViewSettings="showViewSettings"
:showValueViewer="viewValuePanelVisible"
@loading-image-completed="loadingImage = false" @loading-image-completed="loadingImage = false"
@update="$emit('update')" @update="$emit('update')"
/> />
@@ -56,6 +57,17 @@
<settings-icon /> <settings-icon />
</icon-button> </icon-button>
<icon-button
v-if="mode === 'graph'"
ref="viewNodeOrEdgeBtn"
tooltip="View node or edge details"
tooltipPosition="top-left"
:active="viewValuePanelVisible"
@click="viewValuePanelVisible = !viewValuePanelVisible"
>
<view-cell-value-icon />
</icon-button>
<div class="side-tool-bar-divider" /> <div class="side-tool-bar-divider" />
<icon-button <icon-button
@@ -126,6 +138,7 @@ import HtmlIcon from '@/components/svg/html'
import ExportToSvgIcon from '@/components/svg/exportToSvg' import ExportToSvgIcon from '@/components/svg/exportToSvg'
import PngIcon from '@/components/svg/png' import PngIcon from '@/components/svg/png'
import ClipboardIcon from '@/components/svg/clipboard' import ClipboardIcon from '@/components/svg/clipboard'
import ViewCellValueIcon from '@/components/svg/viewCellValue'
import cIo from '@/lib/utils/clipboardIo' import cIo from '@/lib/utils/clipboardIo'
import loadingDialog from '@/components/Common/LoadingDialog.vue' import loadingDialog from '@/components/Common/LoadingDialog.vue'
import time from '@/lib/utils/time' import time from '@/lib/utils/time'
@@ -144,6 +157,7 @@ export default {
GraphIcon, GraphIcon,
SettingsIcon, SettingsIcon,
ExportToSvgIcon, ExportToSvgIcon,
ViewCellValueIcon,
PngIcon, PngIcon,
HtmlIcon, HtmlIcon,
ClipboardIcon, ClipboardIcon,
@@ -172,7 +186,8 @@ export default {
graph: this.initMode === 'graph' ? this.initOptions : null graph: this.initMode === 'graph' ? this.initOptions : null
}, },
showLoadingDialog: false, showLoadingDialog: false,
showViewSettings: true showViewSettings: true,
viewValuePanelVisible: false
} }
}, },
computed: { computed: {

View File

@@ -28,7 +28,7 @@
</multiselect> </multiselect>
</Field> </Field>
<Field label="Seed value"> <Field label="Seed value" fieldContainerClassName="test_seed_value">
<NumericInput <NumericInput
:value="modelValue.seedValue" :value="modelValue.seedValue"
@update="update('seedValue', $event)" @update="update('seedValue', $event)"

View File

@@ -102,7 +102,7 @@ export default {
]), ]),
сolorscaleDirections: markRaw([ сolorscaleDirections: markRaw([
{ label: 'Normal', value: 'normal' }, { label: 'Normal', value: 'normal' },
{ label: 'Recersed', value: 'reversed' } { label: 'Reversed', value: 'reversed' }
]), ]),
colorSourceUsageOptions: markRaw([ colorSourceUsageOptions: markRaw([
{ label: 'Direct', value: 'direct' }, { 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" @color-change="settings.style.backgroundColor = $event"
/> />
</Field> </Field>
<Field label="Highlight mode">
<Dropdown
:options="highlightModeOptions"
:value="settings.style.highlightMode"
className="test_highlight_mode_select"
@change="updateHighlightNodeMode"
/>
</Field>
</Fold> </Fold>
</Panel> </Panel>
<Panel group="Style" name="Nodes"> <Panel group="Style" name="Nodes">
@@ -162,6 +171,17 @@
/> />
</Fold> </Fold>
<template v-if="settings.layout.type === 'forceAtlas2'"> <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"> <Fold name="Advanced layout settings">
<AdvancedForceAtlasLayoutSettings <AdvancedForceAtlasLayoutSettings
v-model="settings.layout.options" v-model="settings.layout.options"
@@ -173,6 +193,7 @@
<Button <Button
variant="secondary" variant="secondary"
class="test_fa2_reset" class="test_fa2_reset"
title="Set the settings to default or previously saved ones."
@click="resetFA2LayoutSettings" @click="resetFA2LayoutSettings"
> >
Reset Reset
@@ -183,16 +204,25 @@
@click="toggleFA2Layout" @click="toggleFA2Layout"
> >
<template #node:icon> <template #node:icon>
<div <div>
:style="{
padding: '0 3px'
}"
>
<RunIcon v-if="!fa2Running" /> <RunIcon v-if="!fa2Running" />
<StopIcon v-else /> <PauseIcon v-else />
</div> </div>
</template> </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> </Button>
</div> </div>
</template> </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 Field from 'react-chart-editor/lib/components/fields/Field'
import RandomLayoutSettings from '@/components/Graph/RandomLayoutSettings.vue' import RandomLayoutSettings from '@/components/Graph/RandomLayoutSettings.vue'
import ForceAtlasLayoutSettings from '@/components/Graph/ForceAtlasLayoutSettings.vue' import ForceAtlasLayoutSettings from '@/components/Graph/ForceAtlasLayoutSettings.vue'
import ForceAtlasSeedLayoutSettings from '@/components/Graph/ForceAtlasSeedLayoutSettings.vue'
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
import AdvancedForceAtlasLayoutSettings from '@/components/Graph/AdvancedForceAtlasLayoutSettings.vue' import AdvancedForceAtlasLayoutSettings from '@/components/Graph/AdvancedForceAtlasLayoutSettings.vue'
import CirclePackLayoutSettings from '@/components/Graph/CirclePackLayoutSettings.vue' import CirclePackLayoutSettings from '@/components/Graph/CirclePackLayoutSettings.vue'
import FA2Layout from 'graphology-layout-forceatlas2/worker' import FA2Layout from 'graphology-layout-forceatlas2/worker'
import * as forceAtlas2 from 'graphology-layout-forceatlas2' import * as forceAtlas2 from 'graphology-layout-forceatlas2'
import RunIcon from '@/components/svg/run.vue' import RunIcon from '@/components/svg/run.vue'
import StopIcon from '@/components/svg/stop.vue' import RestartIcon from '@/components/svg/restart.vue'
import PauseIcon from '@/components/svg/pause.vue'
import { downloadAsPNG, drawOnCanvas } from '@sigma/export-image' import { downloadAsPNG, drawOnCanvas } from '@sigma/export-image'
import { import {
buildNodes, buildNodes,
buildEdges, buildEdges,
updateNodes, updateNodes,
updateEdges updateEdges,
reduceNodes,
reduceEdges,
clearNodeCoordinates
} from '@/lib/graphHelper' } from '@/lib/graphHelper'
import Graph from 'graphology' import Graph from 'graphology'
import { circular, random, circlepack } from 'graphology-layout' import { circular, random, circlepack } from 'graphology-layout'
@@ -262,14 +297,16 @@ export default {
Button: applyPureReactInVue(Button), Button: applyPureReactInVue(Button),
ColorPicker: applyPureReactInVue(ColorPicker), ColorPicker: applyPureReactInVue(ColorPicker),
RunIcon, RunIcon,
StopIcon, RestartIcon,
PauseIcon,
RandomLayoutSettings, RandomLayoutSettings,
CirclePackLayoutSettings, CirclePackLayoutSettings,
NodeColorSettings, NodeColorSettings,
NodeSizeSettings, NodeSizeSettings,
EdgeSizeSettings, EdgeSizeSettings,
EdgeColorSettings, EdgeColorSettings,
AdvancedForceAtlasLayoutSettings AdvancedForceAtlasLayoutSettings,
ForceAtlasSeedLayoutSettings
}, },
inject: ['tabLayout'], inject: ['tabLayout'],
props: { props: {
@@ -277,7 +314,7 @@ export default {
initOptions: Object, initOptions: Object,
showViewSettings: Boolean showViewSettings: Boolean
}, },
emits: ['update'], emits: ['update', 'selectItem', 'clearSelection'],
data() { data() {
return { return {
graph: new Graph({ multi: true, allowSelfLoops: true }), graph: new Graph({ multi: true, allowSelfLoops: true }),
@@ -300,7 +337,16 @@ export default {
circlepack: CirclePackLayoutSettings, circlepack: CirclePackLayoutSettings,
forceAtlas2: ForceAtlasLayoutSettings 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 settings: this.initOptions
? JSON.parse(JSON.stringify(this.initOptions)) ? JSON.parse(JSON.stringify(this.initOptions))
: { : {
@@ -312,6 +358,7 @@ export default {
}, },
style: { style: {
backgroundColor: 'white', backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: { nodes: {
size: { size: {
type: 'constant', type: 'constant',
@@ -352,7 +399,15 @@ export default {
random: null, random: null,
circlepack: null, circlepack: null,
forceAtlas2: null forceAtlas2: null
} },
highlightModeOptions: markRaw([
{ label: 'Node alone', value: 'node_alone' },
{ label: 'Node and neighbors', value: 'node_and_neighbors' },
{
label: 'Include edges between neighbors',
value: 'include_neighbor_edges'
}
])
} }
}, },
computed: { computed: {
@@ -379,6 +434,46 @@ export default {
}, new Set()) }, new Set())
return Array.from(keySet) return Array.from(keySet)
},
neighborsOfSelectedNode() {
if (this.settings.style.highlightMode === 'node_alone') {
return undefined
}
return this.selectedNodeId
? new Set(this.graph.neighbors(this.selectedNodeId))
: undefined
},
neighborsOfHoveredNode() {
if (this.settings.style.highlightMode === 'node_alone') {
return undefined
}
return this.hoveredNodeId
? new Set(this.graph.neighbors(this.hoveredNodeId))
: undefined
},
hoveredEdgeExtremities() {
return this.hoveredEdgeId
? this.graph.extremities(this.hoveredEdgeId)
: []
},
selectedEdgeExtremities() {
return this.selectedEdgeId
? this.graph.extremities(this.selectedEdgeId)
: []
},
interactionState() {
return {
selectedNodeId: this.selectedNodeId,
hoveredNodeId: this.hoveredNodeId,
selectedEdgeId: this.selectedEdgeId,
hoveredEdgeId: this.hoveredEdgeId,
neighborsOfSelectedNode: this.neighborsOfSelectedNode,
neighborsOfHoveredNode: this.neighborsOfHoveredNode,
hoveredEdgeExtremities: this.hoveredEdgeExtremities,
selectedEdgeExtremities: this.selectedEdgeExtremities
}
} }
}, },
watch: { watch: {
@@ -423,6 +518,7 @@ export default {
}, },
methods: { methods: {
buildGraph() { buildGraph() {
this.clearSelection()
if (this.renderer) { if (this.renderer) {
this.renderer.kill() this.renderer.kill()
} }
@@ -440,12 +536,85 @@ export default {
renderEdgeLabels: true, renderEdgeLabels: true,
allowInvalidContainer: true, allowInvalidContainer: true,
labelColor: { attribute: 'labelColor', color: '#444444' }, labelColor: { attribute: 'labelColor', color: '#444444' },
edgeLabelColor: { attribute: 'labelColor', color: '#a2b1c6' } edgeLabelColor: { attribute: 'labelColor', color: '#a2b1c6' },
enableEdgeEvents: true,
zIndex: true,
nodeReducer: (node, data) =>
reduceNodes(node, data, this.interactionState, this.settings),
edgeReducer: (edge, data) =>
reduceEdges(
edge,
data,
this.interactionState,
this.settings,
this.graph
)
}) })
this.renderer.on('clickNode', ({ node }) => {
this.selectedNodeId = node
this.selectedEdgeId = undefined
this.$emit('selectItem', this.graph.getNodeAttributes(node).data)
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('clickEdge', ({ edge }) => {
this.selectedEdgeId = edge
this.selectedNodeId = undefined
this.$emit('selectItem', this.graph.getEdgeAttributes(edge).data)
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('clickStage', () => {
this.clearSelection()
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('enterNode', ({ node }) => {
this.hoveredNodeId = node
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('enterEdge', ({ edge }) => {
this.hoveredEdgeId = edge
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('leaveNode', () => {
this.hoveredNodeId = undefined
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('leaveEdge', () => {
this.hoveredEdgeId = undefined
this.renderer.refresh({
skipIndexation: true
})
})
if (this.settings.layout.type === 'forceAtlas2') { if (this.settings.layout.type === 'forceAtlas2') {
this.autoRunFA2Layout() this.autoRunFA2Layout()
} }
}, },
clearSelection() {
this.selectedNodeId = undefined
this.selectedEdgeId = undefined
this.$emit('clearSelection')
},
updateHighlightNodeMode(mode) {
this.settings.style.highlightMode = mode
if (this.renderer) {
this.renderer.refresh({
skipIndexation: true
})
}
},
updateStructure(attributeName, value) { updateStructure(attributeName, value) {
this.settings.structure[attributeName] = value this.settings.structure[attributeName] = value
}, },
@@ -505,75 +674,76 @@ export default {
this.fa2Layout.kill() this.fa2Layout.kill()
} }
if (layoutType === 'circular') { this.layoutMethodMap[layoutType]()
circular.assign(this.graph)
return
}
if (layoutType === 'random') { if (layoutType === 'forceAtlas2' && layoutType !== prevLayout) {
random.assign(this.graph, { this.autoRunFA2Layout()
rng: seedrandom(this.settings.layout.options.seedValue)
})
return
} }
},
if (layoutType === 'circlepack') { applyCircularLayout() {
this.graph.forEachNode(nodeId => { circular.assign(this.graph)
this.graph.updateNode(nodeId, attributes => { },
const newAttributes = { ...attributes } applyRandomLayout() {
// Delete old hierarchy attributes random.assign(this.graph, {
Object.keys(newAttributes) rng: seedrandom(this.settings.layout.options.seedValue)
.filter(key => key.startsWith('hierarchyAttribute')) })
.forEach( },
hierarchyAttributeKey => applyCirclePackLayout() {
delete newAttributes[hierarchyAttributeKey] this.graph.forEachNode(nodeId => {
) this.graph.updateNode(nodeId, attributes => {
// Set new hierarchy attributes const newAttributes = { ...attributes }
this.settings.layout.options.hierarchyAttributes?.forEach( // Delete old hierarchy attributes
(hierarchyAttribute, index) => { Object.keys(newAttributes)
newAttributes['hierarchyAttribute' + index] = .filter(key => key.startsWith('hierarchyAttribute'))
attributes.data[hierarchyAttribute] .forEach(
} hierarchyAttributeKey =>
delete newAttributes[hierarchyAttributeKey]
) )
// Set new hierarchy attributes
return newAttributes this.settings.layout.options.hierarchyAttributes?.forEach(
}) (hierarchyAttribute, index) => {
}) newAttributes['hierarchyAttribute' + index] =
attributes.data[hierarchyAttribute]
circlepack.assign(this.graph, { }
hierarchyAttributes:
this.settings.layout.options.hierarchyAttributes?.map(
(_, index) => 'hierarchyAttribute' + index
) || [],
rng: seedrandom(this.settings.layout.options.seedValue)
})
return
}
if (layoutType === 'forceAtlas2') {
if (
!this.graph.someNode(
(nodeKey, attributes) =>
typeof attributes.x === 'number' &&
typeof attributes.y === 'number'
) )
) {
circular.assign(this.graph)
}
this.fa2Layout = markRaw( return newAttributes
new FA2Layout(this.graph, { })
getEdgeWeight: (_, attr) => })
this.settings.layout.options.weightSource
? attr.data[this.settings.layout.options.weightSource] circlepack.assign(this.graph, {
: 1, hierarchyAttributes:
settings: this.settings.layout.options 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() { toggleFA2Layout() {
if (this.fa2Layout.isRunning()) { if (this.fa2Layout.isRunning()) {
@@ -594,6 +764,15 @@ export default {
this.checkIteration = null this.checkIteration = null
} }
}, },
restartFA2Layout() {
if (this.fa2Layout.isRunning()) {
this.stopFA2Layout()
}
this.fa2Layout.kill()
clearNodeCoordinates(this.graph)
this.applyFA2Layout()
this.autoRunFA2Layout()
},
autoRunFA2Layout() { autoRunFA2Layout() {
let iteration = 1 let iteration = 1
this.checkIteration = () => { this.checkIteration = () => {
@@ -611,6 +790,7 @@ export default {
setRecommendedFA2Settings() { setRecommendedFA2Settings() {
const sensibleSettings = forceAtlas2.default.inferSettings(this.graph) const sensibleSettings = forceAtlas2.default.inferSettings(this.graph)
this.settings.layout.options = { this.settings.layout.options = {
initialAlgorithm: 'circular',
initialIterationsAmount: 50, initialIterationsAmount: 50,
adjustSizes: false, adjustSizes: false,
barnesHutOptimize: false, barnesHutOptimize: false,
@@ -672,4 +852,8 @@ export default {
flex-grow: 1; flex-grow: 1;
flex-basis: 0; flex-basis: 0;
} }
.force-atlas-buttons :deep(.button__icon > div) {
padding: 0 3px;
}
</style> </style>

View File

@@ -139,7 +139,7 @@ export default {
]), ]),
сolorscaleDirections: markRaw([ сolorscaleDirections: markRaw([
{ label: 'Normal', value: 'normal' }, { label: 'Normal', value: 'normal' },
{ label: 'Recersed', value: 'reversed' } { label: 'Reversed', value: 'reversed' }
]), ]),
colorSourceUsageOptions: markRaw([ colorSourceUsageOptions: markRaw([
{ label: 'Direct', value: 'direct' }, { label: 'Direct', value: 'direct' },

View File

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

View File

@@ -13,21 +13,38 @@
documentation</a documentation</a
>. >.
</div> </div>
<div <splitpanes
:before="{ size: 70, max: 100 }"
:after="{ size: 30, max: 50, hidden: !showValueViewer }"
:default="{ before: 70, after: 30 }"
class="graph" class="graph"
:style="{ :style="{
height: height:
!dataSources || !dataSourceIsValid ? 'calc(100% - 40px)' : '100%' !dataSources || !dataSourceIsValid ? 'calc(100% - 40px)' : '100%'
}" }"
> >
<GraphEditor <template #left-pane>
ref="graphEditor" <div ref="graphEditorContainer" :style="{ height: '100%' }">
:dataSources="dataSources" <GraphEditor
:initOptions="initOptions" ref="graphEditor"
:showViewSettings="showViewSettings" :dataSources="dataSources"
@update="$emit('update')" :initOptions="initOptions"
/> :showViewSettings="showViewSettings"
</div> @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> </div>
</template> </template>
@@ -35,17 +52,20 @@
import 'react-chart-editor/lib/react-chart-editor.css' import 'react-chart-editor/lib/react-chart-editor.css'
import GraphEditor from '@/components/Graph/GraphEditor.vue' import GraphEditor from '@/components/Graph/GraphEditor.vue'
import { dataSourceIsValid } from '@/lib/graphHelper' import { dataSourceIsValid } from '@/lib/graphHelper'
import ValueViewer from '@/components/ValueViewer.vue'
import Splitpanes from '@/components/Common/Splitpanes'
export default { export default {
name: 'Graph', name: 'Graph',
components: { GraphEditor }, components: { GraphEditor, ValueViewer, Splitpanes },
props: { props: {
dataSources: Object, dataSources: Object,
initOptions: Object, initOptions: Object,
exportToPngEnabled: Boolean, exportToPngEnabled: Boolean,
exportToSvgEnabled: Boolean, exportToSvgEnabled: Boolean,
exportToHtmlEnabled: Boolean, exportToHtmlEnabled: Boolean,
showViewSettings: Boolean showViewSettings: Boolean,
showValueViewer: Boolean
}, },
emits: [ emits: [
'update:exportToSvgEnabled', 'update:exportToSvgEnabled',
@@ -57,7 +77,8 @@ export default {
], ],
data() { data() {
return { return {
resizeObserver: null resizeObserver: null,
selectedItem: null
} }
}, },
computed: { computed: {
@@ -83,10 +104,10 @@ export default {
}, },
mounted() { mounted() {
this.resizeObserver = new ResizeObserver(this.handleResize) this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.graphContainer) this.resizeObserver.observe(this.$refs.graphEditorContainer)
}, },
beforeUnmount() { beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.graphContainer) this.resizeObserver.unobserve(this.$refs.graphEditorContainer)
}, },
methods: { methods: {
getOptionsForSave() { getOptionsForSave() {
@@ -100,7 +121,7 @@ export default {
return this.$refs.graphEditor.prepareCopy() return this.$refs.graphEditor.prepareCopy()
}, },
async handleResize() { async handleResize() {
const renderer = this.$refs.graphEditor.renderer const renderer = this.$refs.graphEditor?.renderer
if (renderer) { if (renderer) {
renderer.refresh() renderer.refresh()
renderer.getCamera().animatedReset({ duration: 600 }) renderer.getCamera().animatedReset({ duration: 600 })

View File

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

View File

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

View File

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

View File

@@ -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) { export function buildNodes(graph, dataSources, options) {
const docColumn = Object.keys(dataSources)[0] const docColumn = Object.keys(dataSources)[0]
const { objectType, nodeId } = options.structure 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) { function getUpdateLabelMethod(labelSettings) {
const { source, color } = labelSettings const { source, color } = labelSettings
return attributes => { return attributes => {

View File

@@ -12,6 +12,18 @@ export default {
inquiries.forEach(inquiry => { inquiries.forEach(inquiry => {
if (inquiry.viewType === 'graph') { if (inquiry.viewType === 'graph') {
inquiry.viewOptions.style.nodes.color.opacity = 100 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 migrate = migration._migrate
const myInquiriesKey = 'myInquiries' const myInquiriesKey = 'myInquiries'
const latestVersion = 4
export default { export default {
version: 3, version: latestVersion,
myInquiriesKey, myInquiriesKey,
getStoredInquiries() { getStoredInquiries() {
let myInquiries = JSON.parse(localStorage.getItem(myInquiriesKey)) let myInquiries = JSON.parse(localStorage.getItem(myInquiriesKey))
@@ -21,8 +22,8 @@ export default {
return [] return []
} }
if (myInquiries.version === 2) { if (myInquiries.version < latestVersion) {
myInquiries = migrate(2, myInquiries.inquiries) myInquiries = migrate(myInquiries.version, myInquiries.inquiries)
this.updateStorage(myInquiries) this.updateStorage(myInquiries)
return myInquiries return myInquiries
} }
@@ -69,6 +70,8 @@ export default {
// Turn data into array if they are not // Turn data into array if they are not
inquiryList = !Array.isArray(inquiries) ? [inquiries] : inquiries inquiryList = !Array.isArray(inquiries) ? [inquiries] : inquiries
inquiryList = migrate(1, inquiryList) inquiryList = migrate(1, inquiryList)
} else if (inquiries.version < latestVersion) {
inquiryList = migrate(inquiries.version, inquiries.inquiries)
} else { } else {
inquiryList = inquiries.inquiries || [] inquiryList = inquiries.inquiries || []
} }
@@ -108,6 +111,8 @@ export default {
if (!data.version) { if (!data.version) {
return data.length > 0 ? migrate(1, data) : [] return data.length > 0 ? migrate(1, data) : []
} else if (data.version < latestVersion) {
return migrate(data.version, data.inquiries)
} else { } else {
return data.inquiries return data.inquiries
} }

View File

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

View File

@@ -418,4 +418,52 @@ describe('DataView.vue', () => {
wrapper.findComponent({ name: 'graph' }).props('initOptions') wrapper.findComponent({ name: 'graph' }).props('initOptions')
).to.eql({ test_options: 'latest_graph_options' }) ).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 sinon from 'sinon'
import { mount, flushPromises } from '@vue/test-utils' import { mount, flushPromises } from '@vue/test-utils'
import Graph from '@/components/Graph/index.vue' import Graph from '@/components/Graph/index.vue'
import { nextTick } from 'vue'
function getPixels(canvas) { function getPixels(canvas) {
const context = canvas.getContext('webgl2') const context = canvas.getContext('webgl2')
@@ -163,6 +164,326 @@ describe('Graph.vue', () => {
wrapper.unmount() 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 () => { it('nodes and edges are rendered', async () => {
const wrapper = mount(Graph, { const wrapper = mount(Graph, {
attachTo: document.body, attachTo: document.body,

View File

@@ -1309,7 +1309,7 @@ describe('GraphEditor', () => {
expect(startSpy.calledOnce).to.equal(true) expect(startSpy.calledOnce).to.equal(true)
await waitCondition(() => stopSpy.callCount === 1) await waitCondition(() => stopSpy.callCount === 1)
expect(wrapper.text()).to.contain('Start') expect(wrapper.text()).to.contain('Continue')
const coordinates = graph const coordinates = graph
.export() .export()
@@ -1349,6 +1349,7 @@ describe('GraphEditor', () => {
layout: { layout: {
type: 'forceAtlas2', type: 'forceAtlas2',
options: { options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 55, initialIterationsAmount: 55,
gravity: 1.5, gravity: 1.5,
scalingRatio: 1.2, scalingRatio: 1.2,
@@ -1383,7 +1384,7 @@ describe('GraphEditor', () => {
expect(startSpy.calledOnce).to.equal(true) expect(startSpy.calledOnce).to.equal(true)
await waitCondition(() => stopSpy.callCount === 1) await waitCondition(() => stopSpy.callCount === 1)
expect(wrapper.text()).to.contain('Start') expect(wrapper.text()).to.contain('Continue')
const initialCoordinates = graph const initialCoordinates = graph
.export() .export()
@@ -1397,7 +1398,7 @@ describe('GraphEditor', () => {
new Event('blur', { bubbles: true }) new Event('blur', { bubbles: true })
) )
// Call nextTick after setting number input, // 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() await nextTick()
expect(wrapper.vm.settings.layout.options.gravity).to.equal(12) expect(wrapper.vm.settings.layout.options.gravity).to.equal(12)
@@ -1405,6 +1406,30 @@ describe('GraphEditor', () => {
// Algorithm wasn't called // Algorithm wasn't called
expect(startSpy.calledOnce).to.equal(true) 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 // Change scaling ratio
const scalingInput = wrapper.find('.test_fa2_scaling input') const scalingInput = wrapper.find('.test_fa2_scaling input')
await scalingInput.setValue(2) await scalingInput.setValue(2)
@@ -1493,13 +1518,13 @@ describe('GraphEditor', () => {
false false
) )
// Click Start // Click Continue
const toggleButton = wrapper.find('button.test_fa2_toggle') const toggleButton = wrapper.find('button.test_fa2_toggle')
await toggleButton.trigger('click') await toggleButton.trigger('click')
expect(toggleButton.text()).to.contain('Stop') expect(toggleButton.text()).to.contain('Pause')
expect(startSpy.callCount).to.equal(2) expect(startSpy.callCount).to.equal(2)
// Wait a bit and click Stop // Wait a bit and click Pause
await time.sleep(500) await time.sleep(500)
await toggleButton.trigger('click') await toggleButton.trigger('click')
expect(stopSpy.callCount).to.equal(2) expect(stopSpy.callCount).to.equal(2)
@@ -1513,9 +1538,479 @@ describe('GraphEditor', () => {
// Click Reset // Click Reset
await wrapper.find('button.test_fa2_reset').trigger('click') 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(startSpy.callCount).to.equal(2)
expect(wrapper.vm.settings.layout.options).to.eql({ 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, initialIterationsAmount: 55,
gravity: 1.5, gravity: 1.5,
scalingRatio: 1.2, scalingRatio: 1.2,
@@ -1603,6 +2098,26 @@ describe('GraphEditor', () => {
) )
await nextTick() 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 // Change scaling ratio
const scalingInput = wrapper.find('.test_fa2_scaling input') const scalingInput = wrapper.find('.test_fa2_scaling input')
await scalingInput.setValue(2) await scalingInput.setValue(2)
@@ -1671,6 +2186,8 @@ describe('GraphEditor', () => {
await nextTick() await nextTick()
expect(wrapper.vm.settings.layout.options).to.eql({ expect(wrapper.vm.settings.layout.options).to.eql({
initialAlgorithm: 'random',
seedValue: 123,
initialIterationsAmount: 120, initialIterationsAmount: 120,
gravity: 12, gravity: 12,
scalingRatio: 2, scalingRatio: 2,
@@ -1756,6 +2273,7 @@ describe('GraphEditor', () => {
layout: { layout: {
type: 'forceAtlas2', type: 'forceAtlas2',
options: { options: {
initialAlgorithm: 'circular',
initialIterationsAmount: 50, initialIterationsAmount: 50,
gravity: 1.5, gravity: 1.5,
scalingRatio: 1.2, scalingRatio: 1.2,
@@ -1788,7 +2306,7 @@ describe('GraphEditor', () => {
expect(startSpy.calledOnce).to.equal(true) expect(startSpy.calledOnce).to.equal(true)
await waitCondition(() => stopSpy.callCount === 1) await waitCondition(() => stopSpy.callCount === 1)
// Click Start // Click Continue
const toggleButton = wrapper.find('button.test_fa2_toggle') const toggleButton = wrapper.find('button.test_fa2_toggle')
await toggleButton.trigger('click') await toggleButton.trigger('click')
expect(startSpy.callCount).to.equal(2) expect(startSpy.callCount).to.equal(2)
@@ -1884,7 +2402,7 @@ describe('GraphEditor', () => {
expect(startSpy.calledOnce).to.equal(true) expect(startSpy.calledOnce).to.equal(true)
await waitCondition(() => stopSpy.callCount === 1) await waitCondition(() => stopSpy.callCount === 1)
expect(wrapper.text()).to.contain('Start') expect(wrapper.text()).to.contain('Continue')
const coordinates = graph const coordinates = graph
.export() .export()

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -39,4 +39,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' } 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', updatedAt: '2026-01-19T21:49:40.708Z',
createdAt: '2026-01-19T21:46:13.899Z' createdAt: '2026-01-19T21:46:13.899Z'
}, },
@@ -125,6 +140,7 @@ describe('storedInquiries.js', () => {
}, },
style: { style: {
backgroundColor: 'white', backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: { nodes: {
size: { type: 'constant', value: 10 }, size: { type: 'constant', value: 10 },
color: { color: {
@@ -144,9 +160,25 @@ describe('storedInquiries.js', () => {
label: { source: null, color: '#a2b1c6' } 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', updatedAt: '2026-01-19T21:49:40.708Z',
createdAt: '2026-01-19T21:46:13.899Z' createdAt: '2026-01-19T21:46:13.899Z'
}, },
@@ -243,7 +275,7 @@ describe('storedInquiries.js', () => {
const str = storedInquiries.serialiseInquiries(inquiryList) const str = storedInquiries.serialiseInquiries(inquiryList)
const parsedJson = JSON.parse(str) const parsedJson = JSON.parse(str)
expect(parsedJson.version).to.equal(3) expect(parsedJson.version).to.equal(4)
expect(parsedJson.inquiries).to.have.lengthOf(2) expect(parsedJson.inquiries).to.have.lengthOf(2)
expect(parsedJson.inquiries[1]).to.eql(inquiryList[1]) expect(parsedJson.inquiries[1]).to.eql(inquiryList[1])
expect(parsedJson.inquiries[0]).to.eql({ expect(parsedJson.inquiries[0]).to.eql({
@@ -256,7 +288,7 @@ describe('storedInquiries.js', () => {
}) })
}) })
it('deserialiseInquiries migrates inquiries', () => { it('deserialiseInquiries migrates inquiries of v1', () => {
const str = `[ const str = `[
{ {
"id": 1, "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 = ` const str = `
{ {
"id": 1, "id": 1,
@@ -382,7 +548,7 @@ describe('storedInquiries.js', () => {
it('importInquiries', async () => { it('importInquiries', async () => {
const str = `{ const str = `{
"version": 3, "version": 4,
"inquiries": [{ "inquiries": [{
"id": 1, "id": 1,
"name": "foo", "name": "foo",
@@ -407,7 +573,7 @@ describe('storedInquiries.js', () => {
]) ])
}) })
it('readPredefinedInquiries old', async () => { it('readPredefinedInquiries v1', async () => {
const str = `[ const str = `[
{ {
"id": 1, "id": 1,
@@ -432,18 +598,70 @@ describe('storedInquiries.js', () => {
]) ])
}) })
it('readPredefinedInquiries', async () => { it('readPredefinedInquiries v2', async () => {
const str = `{ const str = `{
"version": 3, "version": 2,
"inquiries": [ "inquiries": [
{ {
"id": 1, "id": 1,
"name": "foo", "name": "foo",
"query": "select * from foo", "query": "select * from foo",
"viewType": "chart", "viewType": "chart",
"viewOptions": [], "viewOptions": [],
"createdAt": "2020-11-03T14:17:49.524Z" "createdAt": "2020-11-03T14:17:49.524Z"
}] },
{
"id": "Xh1Hc9v7P3mRPZVM59QiC",
"query": "SELECT * from doc",
"viewType": "graph",
"viewOptions": {
"structure": {
"nodeId": "node_id",
"objectType": "object_type",
"edgeSource": "source",
"edgeTarget": "target"
},
"style": {
"backgroundColor": "white",
"nodes": {
"size": { "type": "constant", "value": 10 },
"color": {
"type": "calculated",
"method": "degree",
"colorscale": null,
"mode": "continious",
"colorscaleDirection": "reversed"
},
"label": { "source": "label", "color": "#444444" }
},
"edges": {
"showDirection": true,
"size": { "type": "constant", "value": 2 },
"color": { "type": "constant", "value": "#a2b1c6" },
"label": { "source": null, "color": "#a2b1c6" }
}
},
"layout": {
"type": "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))) sinon.stub(fu, 'readFile').returns(Promise.resolve(new Response(str)))
@@ -457,6 +675,106 @@ describe('storedInquiries.js', () => {
viewType: 'chart', viewType: 'chart',
viewOptions: [], viewOptions: [],
createdAt: '2020-11-03T14:17:49.524Z' createdAt: '2020-11-03T14:17:49.524Z'
},
{
id: 'Xh1Hc9v7P3mRPZVM59QiC',
query: 'SELECT * from doc',
viewType: 'graph',
viewOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: {
size: { type: 'constant', value: 10 },
color: {
type: 'calculated',
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'reversed',
opacity: 100
},
label: { source: 'label', color: '#444444' }
},
edges: {
showDirection: true,
size: { type: 'constant', value: 2 },
color: { type: 'constant', value: '#a2b1c6' },
label: { source: null, color: '#a2b1c6' }
}
},
layout: {
type: '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'
} }
]) ])
}) })