Compare commits

...
99 Commits
Author SHA1 Message Date
lana-k 2e3057908f fix lint 2026-10-17 19:49:03 +02:00
lana-k 381f33b9e2 0.32.1 2026-10-17 19:35:51 +02:00
lana-k a68f799fdf #143 clear btn tests 2026-10-17 19:35:24 +02:00
lana-k 835a88cbbb #142 fix tooltips 2026-10-17 19:31:22 +02:00
lana-k a210e77f51 #141 fix search panel layout, #143 add clear button to text input 2026-10-16 22:34:18 +02:00
lana-k df6ba00f2b #142 improve tooltips 2026-10-16 22:33:05 +02:00
lana-k 8cace0586a 0.31.0 2026-10-13 22:08:13 +02:00
lana-k 0457a6f527 #140 fix lint 2026-10-13 21:58:05 +02:00
lana-k 30f28e9a7d #140 test graph rerendering 2026-10-12 22:54:58 +02:00
lana-k aff0c2c81f #140 tests for search panel component 2026-10-11 23:03:51 +02:00
lana-k 239e2667d4 test graphHelper 2026-10-11 20:59:21 +02:00
lana-k 761052313d #140 node search + convert svg to css 2026-10-07 22:24:08 +02:00
lana-k 529c3390b3 #139 fix deselection in multi-trace charts 2026-07-26 20:34:08 +02:00
lana-k acea6090e8 0.31.0 2026-07-26 17:55:10 +02:00
lana-k 130996b8c1 #139 tests 2026-07-26 17:54:06 +02:00
lana-k 6074ccc835 #139 tests 2026-07-26 14:59:08 +02:00
lana-k 62786e7dda #139 tests 2026-07-23 23:14:17 +02:00
lana-k 6fbddb69bb #139 tests 2026-07-21 22:55:03 +02:00
lana-k 9838421d65 #139 view details of selected points 2026-07-20 21:37:24 +02:00
lana-k 686a09a8ce fix import 2026-07-12 21:56:29 +02:00
lana-k 4e9d1a785c 0.30.1 2026-07-12 17:19:37 +02:00
lana-k 2b6de12dc5 fix: show custom chart settings in pivot #138 2026-07-12 17:19:07 +02:00
lana-k 39d1604959 fix: save chart settings wihtout data #137 2026-07-12 17:18:41 +02:00
lana-k 053b8fbecb wip 2026-07-08 20:39:43 +02:00
lana-k 1043a03c96 fix plotly patch file 2026-07-05 19:38:03 +02:00
lana-k b6ec71338b fix plotly warnings see issue 7809 2026-07-05 19:12:24 +02:00
lana-k 4bb6676648 selection view wip 2026-05-25 17:50:57 +02:00
lana-k f3a75346fd fix typo 2026-04-15 22:14:59 +02:00
lana-k 366ffcff10 0.30.0 2026-04-08 22:29:52 +02:00
lana-k 778db0d9eb #136 tests 2026-04-08 22:16:22 +02:00
lana-k c00d76904e fix lint 2026-04-06 20:37:27 +02:00
lana-k 4cc6ec86ea #136 save seedLayout settings 2026-04-06 20:31:51 +02:00
lana-k c0d972c2ab Support circle pack as initial algorithm #136 2026-04-06 19:36:44 +02:00
lana-k d9435a80c3 Add seed layout #136 2026-04-06 19:36:44 +02:00
lana-k 62fb92d824 premultiply colors in sigma 2026-04-06 19:36:44 +02:00
saaj 5019013eff Update Docker test file to NodeJS 18, fix Karma config filename (#135) 2026-04-05 17:13:06 +02:00
lana-k c2c376219f fix lint 2026-02-28 17:59:39 +01:00
lana-k 0199415dde 0.29.0 2026-02-28 17:44:17 +01:00
lana-k 534b186d76 tests for graph editor 2026-02-28 16:53:43 +01:00
lana-k 4f6efb5bda test for DataView 2026-02-26 22:32:24 +01:00
lana-k 2c0b8f9124 enable webgl in chromium 2026-02-25 22:58:28 +01:00
lana-k 5265f5493e splitpanes test 2026-02-24 21:28:11 +01:00
lana-k c0e59f6fb8 update run result test 2026-02-24 21:02:06 +01:00
lana-k 7471744633 add tests for Value Viewer 2026-02-24 20:53:03 +01:00
lana-k e6e5efa8c6 tests for graphHelper 2026-02-22 22:30:18 +01:00
lana-k 57c36b3900 tests for migration 2026-02-07 22:08:39 +01:00
lana-k 1e8c1761e6 #133 highlight nodes and edges 2026-02-07 21:18:49 +01:00
lana-k dd30e17ff5 #133 Add node data viewer 2026-01-25 22:15:48 +01:00
lana-k 4e5adc147f #132 node opacity 2026-01-22 22:25:36 +01:00
lana-k 7edc196a02 change repo structure 2026-01-15 21:53:12 +01:00
lana-k 85b5a200e2 fix tinycolor2 bundle 2025-12-27 21:11:01 +01:00
lana-k a0ef93921f #131 fix label color 2025-12-26 20:56:08 +01:00
lana-k 859cd2ccfc #129 fix icon 2025-12-25 12:29:28 +01:00
lana-k a59946c09d remove karma config 2025-12-24 22:14:48 +01:00
lana-k 7b06b3d9c8 uninstall mesa 2025-12-24 22:06:39 +01:00
lana-k ced933f497 revert firefox base and env 2025-12-24 21:59:23 +01:00
lana-k cda368f109 xvfb 2025-12-24 21:49:53 +01:00
lana-k df67466c2f firefox base 2025-12-24 21:41:29 +01:00
lana-k 528549ae5a LIBGL_ALWAYS_SOFTWARE 2025-12-24 21:36:47 +01:00
lana-k 20f4dcc645 fix package names 2025-12-24 17:51:40 +01:00
lana-k b8353ef0ce install mesa 2025-12-24 17:49:07 +01:00
lana-k 7975f419c9 anoter settings 2025-12-24 17:40:18 +01:00
lana-k 72aa0dd80b another settings 2025-12-24 17:30:05 +01:00
lana-k e000ee71fc ensure webgl is enabled infirefox 2025-12-24 17:25:25 +01:00
lana-k b6a12668d3 #43 fix lint errors 2025-12-24 16:17:49 +01:00
lana-k 713f5ac768 #43 graph 0.28.0 2025-12-23 21:41:17 +01:00
lana-k 5492609c3a update readme 2025-12-23 21:28:32 +01:00
lana-k 8bfd0f5944 tests 2025-12-23 21:15:44 +01:00
lana-k a8006bcf52 tests 2025-12-17 21:26:57 +01:00
lana-k 1463f93bb0 tests for layouts 2025-12-13 17:57:48 +01:00
lana-k 5108495430 test for canvas 2025-12-13 13:00:13 +01:00
lana-k d28968e539 tests 2025-12-07 19:56:16 +01:00
lana-k 68221cba6d tests 2025-11-15 14:29:41 +01:00
lana-k 65c1c18fcb tests 2025-11-08 22:23:38 +01:00
lana-k d7db6a0f5d fix tests 2025-11-02 12:31:32 +01:00
lana-k 0a2af0bba3 events 2025-11-01 21:25:56 +01:00
lana-k e4b35bac0a skip node if there is no node id 2025-11-01 19:48:22 +01:00
lana-k 3d1e822cdc link to docs, disable some settings, check result set 2025-11-01 15:49:34 +01:00
lana-k 3d6479be7a visualisation settings toggle 2025-10-28 22:51:13 +01:00
lana-k 218ab52ab3 autostart, reset and fixes 2025-10-28 13:39:38 +01:00
lana-k f178937440 export and background 2025-10-28 13:39:38 +01:00
lana-k 411bd694c0 wip 2025-10-28 13:38:17 +01:00
lana-k d2969de127 wip 2025-10-28 13:38:17 +01:00
lana-k b59c21c14e update tests 2025-10-19 18:25:03 +02:00
lana-k 4ed4b54a28 fix warnings 2025-10-17 21:01:40 +02:00
lana-k 2c2bb7d6d3 0.27.1 2025-10-16 22:29:56 +02:00
lana-k efbd985b36 #128 tests 2025-10-16 22:28:33 +02:00
lana-k 9cf7d0e5dc #128 fix save and close 2025-10-09 22:49:54 +02:00
lana-k 0a8c09b58d #127 fix for new inquiry 2025-10-08 21:04:17 +02:00
lana-k 931cf380bc #127 tests 2025-10-08 19:39:56 +02:00
lana-k f0f96ac663 tests 2025-10-05 20:59:34 +02:00
lana-k 45530cc9d6 add save as event 2025-10-05 14:27:50 +02:00
lana-k 6fbf75b601 fix tests 2025-10-03 22:13:33 +02:00
lana-k d3fbf08569 #31 fix deleting inquiry 2025-09-29 21:17:36 +02:00
lana-k be6a19a30f #127 fix copy to clipboard 2025-09-28 22:11:18 +02:00
lana-k 07d7a9d54b #31 handle concurrent saving 2025-09-27 21:59:32 +02:00
lana-k cdd925b8af #16 save as 2025-09-27 17:01:50 +02:00
lana-k 12fa0749b1 Update package.json 2025-07-30 23:27:35 +02:00
saaj 75bf849823 Build SQLite 3.50.3 (#124)
* Build SQLite 3.50.3

* Update pivot_vtab, base in Dockerfile.test, fix test after SQLite 3.47

* Update CI image for tests
2025-07-30 23:26:22 +02:00
147 changed files with 16133 additions and 7274 deletions
+7 -3
View File
@@ -11,7 +11,7 @@ on:
jobs:
test:
name: Run tests
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v2
- name: Use Node.js
@@ -21,8 +21,12 @@ jobs:
- name: Install browsers
run: |
export DEBIAN_FRONTEND=noninteractive
sudo add-apt-repository -y ppa:mozillateam/ppa
sudo apt-get update
sudo apt-get install -y chromium-browser firefox
sudo apt-get install -y \
chromium-browser \
firefox-esr \
xvfb
- name: Update npm
run: npm install -g npm@10
@@ -34,4 +38,4 @@ jobs:
run: npm run lint -- --no-fix
- name: Run karma tests
run: npm run test
run: xvfb-run -a npm test
+3 -3
View File
@@ -3,12 +3,12 @@
# docker build -t sqliteviz/test -f Dockerfile.test .
#
FROM node:12.22-buster
FROM node:18.20.8-bookworm
RUN set -ex; \
apt update; \
apt install -y chromium firefox-esr; \
npm install -g npm@7
npm install -g npm@10
WORKDIR /tmp/build
@@ -19,6 +19,6 @@ RUN npm install
COPY . .
RUN set -ex; \
sed -i 's/browsers: \[.*\],/browsers: ['"'FirefoxHeadlessTouch'"'],/' karma.conf.js
sed -i 's/browsers: \[.*\],/browsers: ['"'FirefoxHeadlessTouch'"'],/' karma.conf.cjs
RUN npm run lint -- --no-fix && npm run test
+6 -2
View File
@@ -9,7 +9,7 @@ of SQLite databases, CSV, JSON or NDJSON files.
With sqliteviz you can:
- run SQL queries against a SQLite database and create [Plotly][11] charts and pivot tables based on the result sets
- run SQL queries against a SQLite database and create [Plotly][11] charts, graphs and pivot tables based on the result sets
- import a CSV/JSON/NDJSON file into a SQLite database and visualize imported data
- export result set to CSV file
- manage inquiries and run them against different databases
@@ -33,7 +33,9 @@ It's a kind of middleground between [Plotly Falcon][1] and [Redash][2].
## Components
It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4] and [Vue-Codemirror][8] in [Vue.js][5]. CSV parsing is performed with [Papa Parse][9].
It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4]
and [Vue-Codemirror][8] in [Vue.js][5]. CSV parsing is performed with [Papa Parse][9].
Graphs are visualized with [Sigma.js][13] and [Graphology][14].
[1]: https://github.com/plotly/falcon
[2]: https://github.com/getredash/redash
@@ -47,3 +49,5 @@ It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4]
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
[11]: https://github.com/plotly/plotly.js
[12]: https://github.com/nicolaskruchten/pivottable
[13]: https://www.sigmajs.org/
[14]: https://graphology.github.io/
+1 -1
View File
@@ -4,7 +4,7 @@
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@*": ["./src/*"]
"@\/*": ["./src/*"]
}
}
}
+14 -2
View File
@@ -14,7 +14,7 @@ module.exports = function (config) {
coverage: {
enable: true,
include: 'src/*',
exclude: ['node_modules', 'src/components/svg/*'],
exclude: ['node_modules'],
extension: ['.js', '.vue'],
requireEnv: false
}
@@ -92,11 +92,23 @@ module.exports = function (config) {
'dom.w3c_touch_events.enabled': 1,
'dom.events.asyncClipboard.clipboardItem': true
}
},
ChromiumHeadlessWebGL: {
base: 'ChromiumHeadless',
flags: [
'--headless=new',
'--use-angle=swiftshader',
'--use-gl=angle',
'--enable-webgl',
'--ignore-gpu-blocklist',
'--disable-gpu-sandbox',
'--no-sandbox'
]
}
},
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['ChromiumHeadless', 'FirefoxHeadlessTouch'],
browsers: ['ChromiumHeadlessWebGL', 'FirefoxHeadlessTouch'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
+7 -7
View File
@@ -10,7 +10,7 @@ from pathlib import Path
from urllib import request
amalgamation_url = 'https://sqlite.org/2023/sqlite-amalgamation-3410000.zip'
amalgamation_url = 'https://sqlite.org/2025/sqlite-amalgamation-3500300.zip'
# Extension-functions
# ===================
@@ -22,15 +22,15 @@ contrib_functions_url = 'https://sqlite.org/contrib/download/extension-functions
extension_urls = (
# Miscellaneous extensions
# ========================
('https://sqlite.org/src/raw/8d79354f?at=series.c', 'sqlite3_series_init'),
('https://sqlite.org/src/raw/dbfd8543?at=closure.c', 'sqlite3_closure_init'),
('https://sqlite.org/src/raw/e212edb2?at=series.c', 'sqlite3_series_init'),
('https://sqlite.org/src/raw/5559daf1?at=closure.c', 'sqlite3_closure_init'),
('https://sqlite.org/src/raw/5bb2264c?at=uuid.c', 'sqlite3_uuid_init'),
('https://sqlite.org/src/raw/5853b0e5?at=regexp.c', 'sqlite3_regexp_init'),
('https://sqlite.org/src/raw/b9086e22?at=percentile.c', 'sqlite3_percentile_init'),
('https://sqlite.org/src/raw/09f967dc?at=decimal.c', 'sqlite3_decimal_init'),
('https://sqlite.org/src/raw/388e7f23?at=regexp.c', 'sqlite3_regexp_init'),
('https://sqlite.org/src/raw/72e05a21?at=percentile.c', 'sqlite3_percentile_init'),
('https://sqlite.org/src/raw/228d47e9?at=decimal.c', 'sqlite3_decimal_init'),
# Third-party extension
# =====================
('https://github.com/jakethaw/pivot_vtab/raw/9323ef93/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
('https://github.com/jakethaw/pivot_vtab/raw/e7705f34/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
('https://github.com/nalgeon/sqlean/raw/95e8d21a/src/pearson.c', 'sqlite3_pearson_init'),
# Third-party extension with own dependencies
# ===========================================
+1 -1
View File
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.
+2341 -4615
View File
File diff suppressed because it is too large Load Diff
+13 -3
View File
@@ -1,6 +1,6 @@
{
"name": "sqliteviz",
"version": "0.26.0",
"version": "0.32.1",
"license": "Apache-2.0",
"private": true,
"type": "module",
@@ -10,26 +10,34 @@
"serve": "vite preview",
"test": "karma start karma.conf.cjs",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
"format": "prettier . --write"
"format": "prettier . --write",
"postinstall": "patch-package"
},
"dependencies": {
"@sigma/export-image": "^3.0.0",
"buffer": "^6.0.3",
"codemirror": "^5.65.18",
"codemirror-editor-vue3": "^2.8.0",
"core-js": "^3.6.5",
"dataurl-to-blob": "^0.0.1",
"graphology": "^0.26.0",
"graphology-layout": "^0.6.1",
"graphology-layout-forceatlas2": "^0.10.1",
"html2canvas": "^1.1.4",
"jquery": "^3.6.0",
"nanoid": "^3.1.12",
"papaparse": "^5.4.1",
"pivottable": "^2.23.0",
"plotly.js": "^2.35.2",
"plotly.js": "^2.35.3",
"promise-worker": "^2.0.1",
"react": "^16.14.0",
"react-chart-editor": "^0.46.1",
"react-dom": "^16.14.0",
"seedrandom": "^3.0.5",
"sigma": "3.0.2",
"sql.js": "file:./lib/sql-js",
"tiny-emitter": "^2.1.0",
"tinycolor2": "^1.4.2",
"veaury": "^2.5.1",
"vue": "^3.5.11",
"vue-final-modal": "^4.5.5",
@@ -61,6 +69,8 @@
"karma-spec-reporter": "^0.0.36",
"karma-vite": "^1.0.5",
"mocha": "^5.2.0",
"patch-package": "^8.0.1",
"postinstall-postinstall": "^2.1.0",
"prettier": "3.5.3",
"process": "^0.11.10",
"url-loader": "^4.1.1",
+26
View File
@@ -0,0 +1,26 @@
diff --git a/node_modules/plotly.js/dist/plotly.js b/node_modules/plotly.js/dist/plotly.js
index 28f5612..5b9956d 100644
--- a/node_modules/plotly.js/dist/plotly.js
+++ b/node_modules/plotly.js/dist/plotly.js
@@ -34384,6 +34384,9 @@ var layoutUIControlPatterns = [{
pattern: /^(shapes|annotations)/,
attr: 'editrevision'
}, {
+ pattern: /^selections/,
+ attr: 'selectionrevision'
+},{
pattern: /^title\.text$/,
attr: 'editrevision'
}];
diff --git a/node_modules/plotly.js/src/plot_api/plot_api.js b/node_modules/plotly.js/src/plot_api/plot_api.js
index 75e98f5..5c38053 100644
--- a/node_modules/plotly.js/src/plot_api/plot_api.js
+++ b/node_modules/plotly.js/src/plot_api/plot_api.js
@@ -2394,6 +2394,7 @@ var layoutUIControlPatterns = [
{pattern: /^legend\.(x|y)$/, attr: 'editrevision'},
{pattern: /^(shapes|annotations)/, attr: 'editrevision'},
+ {pattern: /^selections/, attr: 'selectionrevision'},
{pattern: /^title\.text$/, attr: 'editrevision'}
];
+24
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];
}
+5 -45
View File
@@ -1,16 +1,13 @@
<template>
<div id="app">
<router-view />
<modals-container />
</div>
</template>
<script>
import storedInquiries from '@/lib/storedInquiries'
import { ModalsContainer } from 'vue-final-modal'
export default {
components: { ModalsContainer },
computed: {
inquiries() {
return this.$store.state.inquiries
@@ -26,53 +23,16 @@ export default {
},
created() {
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
addEventListener('storage', event => {
if (event.key === storedInquiries.myInquiriesKey) {
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
}
})
}
}
</script>
<style>
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Regular.woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-SemiBold.woff2');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Bold.woff2');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Italic.woff2');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-SemiBoldItalic.woff2');
font-weight: 600;
font-style: italic;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-BoldItalic.woff2');
font-weight: 700;
font-style: italic;
}
#app,
.dialog,
input,
File diff suppressed because one or more lines are too long
+7
View File
@@ -4,3 +4,10 @@
font-size: 13px;
padding: 0 24px;
}
.data-view-warning {
height: 40px;
line-height: 40px;
border-bottom: 1px solid var(--color-border);
box-sizing: border-box;
}
+1
View File
@@ -10,4 +10,5 @@
border-radius: var(--border-radius-medium);
white-space: nowrap;
z-index: 999;
pointer-events: none;
}
+45
View File
@@ -0,0 +1,45 @@
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Regular.woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-SemiBold.woff2');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Bold.woff2');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Italic.woff2');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-SemiBoldItalic.woff2');
font-weight: 600;
font-style: italic;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-BoldItalic.woff2');
font-weight: 700;
font-style: italic;
}
a {
color: var(--color-accent-shade);
}
@@ -8,7 +8,12 @@
<modal modalId="app-info" class="dialog" contentClass="app-info-modal">
<div class="dialog-header">
App info
<close-icon @click="$modal.hide('app-info')" />
<IconButton
icon="myicon myicon-close"
variant="secondary"
:iconSize="14"
@click="$modal.hide('app-info')"
/>
</div>
<div class="dialog-body">
<div v-for="(item, index) in info" :key="index" class="info-item">
@@ -26,12 +31,12 @@
</template>
<script>
import CloseIcon from '@/components/svg/close'
import { version } from '../../../package.json'
import IconButton from '@/components/Common/IconButton.vue'
import { version } from '../../package.json'
export default {
name: 'AppDiagnosticInfo',
components: { CloseIcon },
components: { IconButton },
data() {
return {
info: [
@@ -1,29 +1,45 @@
<template>
<div ref="chartContainer" class="chart-container">
<div v-show="!dataSources" class="warning chart-warning">
<div class="chart-container">
<div v-show="!dataSources" class="warning data-view-warning">
There is no data to build a chart. Run your SQL query and make sure the
result is not empty.
</div>
<div
<splitpanes
:before="{ size: 70, max: 100 }"
:after="{ size: 30, max: 50, hidden: !showValueViewer }"
:default="{ before: 70, after: 30 }"
class="chart"
:style="{ height: !dataSources ? 'calc(100% - 40px)' : '100%' }"
>
<PlotlyEditor
ref="plotlyEditor"
:data="state.data"
:layout="state.layout"
:frames="state.frames"
:config="config"
:dataSources="dataSources"
:dataSourceOptions="dataSourceOptions"
:plotly="plotly"
:useResizeHandler="useResizeHandler"
:debug="true"
:advancedTraceTypeSelector="true"
@update="update"
@render="onRender"
/>
</div>
<template #left-pane>
<div ref="chartContainer" :style="{ height: '100%' }">
<PlotlyEditor
ref="plotlyEditor"
:data="state.data"
:layout="state.layout"
:frames="state.frames"
:config="config"
:dataSources="dataSources"
:dataSourceOptions="dataSourceOptions"
:plotly="plotly"
:useResizeHandler="useResizeHandler"
:debug="true"
:advancedTraceTypeSelector="true"
:hideControls="!showViewSettings"
@update="update"
@render="onRender"
/>
</div>
</template>
<template v-if="showValueViewer" #right-pane>
<value-viewer
:empty="!selectedItems || selectedItems.length === 0"
emptyMessage="No points selected to view"
:value="JSON.stringify(selectedItems)"
defaultFormat="json"
/>
</template>
</splitpanes>
</div>
</template>
@@ -36,35 +52,50 @@ import chartHelper from '@/lib/chartHelper'
import * as dereference from 'react-chart-editor/lib/lib/dereference'
import fIo from '@/lib/utils/fileIo'
import events from '@/lib/utils/events'
import Splitpanes from '@/components/Common/Splitpanes'
import ValueViewer from '@/components/ValueViewer.vue'
import { nextTick } from 'vue'
export default {
name: 'Chart',
components: {
PlotlyEditor: applyPureReactInVue(ReactPlotlyEditorWithPlotRef)
PlotlyEditor: applyPureReactInVue(ReactPlotlyEditorWithPlotRef),
Splitpanes,
ValueViewer
},
props: {
dataSources: Object,
initOptions: Object,
importToPngEnabled: Boolean,
importToSvgEnabled: Boolean,
forPivot: Boolean
exportToPngEnabled: Boolean,
exportToSvgEnabled: Boolean,
forPivot: Boolean,
showViewSettings: Boolean,
showValueViewer: Boolean
},
emits: ['update:importToSvgEnabled', 'update', 'loadingImageCompleted'],
emits: [
'update:exportToSvgEnabled',
'update:exportToHtmlEnabled',
'update',
'loadingImageCompleted'
],
data() {
return {
plotly,
state: this.initOptions || {
data: [],
layout: { autosize: true },
frames: []
},
state: this.initOptions
? JSON.parse(JSON.stringify(this.initOptions))
: {
data: [],
layout: { autosize: true },
frames: []
},
config: {
editable: true,
displaylogo: false,
modeBarButtonsToRemove: ['toImage']
},
resizeObserver: null,
useResizeHandler: this.$store.state.isWorkspaceVisible
useResizeHandler: this.$store.state.isWorkspaceVisible,
selectedItems: null
}
},
computed: {
@@ -77,9 +108,13 @@ export default {
// we need to update state.data in order to update the graph
// https://github.com/plotly/react-chart-editor/issues/948
if (this.dataSources) {
chartHelper.clearSelection(this.state.data, this.state.layout)
dereference.default(this.state.data, this.dataSources)
this.updatePlotly()
}
},
showViewSettings() {
this.handleResize()
}
},
created() {
@@ -102,15 +137,27 @@ export default {
},
{ deep: true }
)
this.$emit('update:importToSvgEnabled', true)
this.$emit('update:exportToSvgEnabled', true)
this.$emit('update:exportToHtmlEnabled', true)
},
mounted() {
async mounted() {
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.chartContainer)
if (this.dataSources) {
dereference.default(this.state.data, this.dataSources)
this.updatePlotly()
}
this.handleResize()
await nextTick()
const plotlyDiv =
this.$refs.plotlyEditor?.$el?.querySelector('.js-plotly-plot')
plotlyDiv?.on('plotly_selected', selectionEvent => {
if (selectionEvent) {
this.selectedItems = this.getSelectedData()
}
})
plotlyDiv?.on('plotly_deselect', () => {
this.selectedItems = this.getSelectedData()
})
},
activated() {
this.useResizeHandler = true
@@ -123,12 +170,18 @@ export default {
},
methods: {
async handleResize() {
// Call updatePlotly twice because there is a small gap (for scrolling?)
// on right and bottom of the plot.
// After the second call it's good.
this.updatePlotly()
this.updatePlotly()
},
onRender() {
// TODO: check changes and enable Save button if needed
},
update(data, layout, frames) {
chartHelper.clearSelection(data, layout)
this.state = { data, layout, frames }
this.$emit('update')
},
@@ -161,10 +214,13 @@ export default {
'text/html'
)
},
async prepareCopy(type = 'png') {
return await chartHelper.getImageDataUrl(
this.$refs.plotlyEditor.$el,
type
prepareCopy(type = 'png') {
return chartHelper.getImageDataUrl(this.$refs.plotlyEditor.$el, type)
},
getSelectedData() {
return chartHelper.getRowsByIndexFromDataSources(
this.dataSources,
chartHelper.getSelectedPointsIndexes(this.state.data)
)
}
}
@@ -176,13 +232,6 @@ export default {
height: 100%;
}
.chart-warning {
height: 40px;
line-height: 40px;
border-bottom: 1px solid var(--color-border);
box-sizing: border-box;
}
.chart {
min-height: 242px;
}
@@ -1,12 +1,22 @@
<template>
<button
:class="['icon-btn', { active }]"
:class="['icon-btn', { active }, 'icon-btn-' + variant]"
:disabled="disabled"
@click="onClick"
:style="{
color: iconColor,
fontSize: iconSize + 'px',
width: width + 'px',
height: width + 'px'
}"
@click.stop="onClick"
@mouseenter="showTooltip($event, tooltipPosition)"
@mouseleave="hideTooltip"
>
<div class="icon"><slot /></div>
<div class="icon">
<slot>
<i :class="icon" />
</slot>
</div>
<div v-show="loading" class="icon-in-progress">
<loading-indicator />
</div>
@@ -14,7 +24,7 @@
v-if="tooltip"
ref="tooltip"
class="icon-tooltip"
:style="tooltipStyle"
:style="{ ...tooltipStyle, maxWidth: maxTooltipWidth }"
>
{{ tooltip }}
</span>
@@ -23,7 +33,7 @@
<script>
import tooltipMixin from '@/tooltipMixin'
import LoadingIndicator from '@/components/LoadingIndicator'
import LoadingIndicator from '@/components/Common/LoadingIndicator'
export default {
name: 'SideBarButton',
@@ -34,9 +44,26 @@ export default {
disabled: Boolean,
tooltip: String,
tooltipPosition: String,
loading: Boolean
maxTooltipWidth: String,
loading: Boolean,
icon: String,
iconColor: { type: String, default: 'var(--color-text-light-2)' },
variant: {
type: String,
default: 'accent',
validator: value => ['accent', 'secondary'].includes(value)
},
iconSize: {
type: Number,
default: 18
}
},
emits: ['click'],
computed: {
width() {
return this.iconSize + 8
}
},
methods: {
onClick() {
this.hideTooltip()
@@ -49,8 +76,6 @@ export default {
<style scoped>
.icon-btn {
box-sizing: border-box;
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
@@ -58,21 +83,27 @@ export default {
background-color: transparent;
border: none;
}
.icon-btn:hover {
.icon-btn .icon i {
font-size: inherit;
}
.icon-btn.icon-btn-accent:hover {
border: 1px solid var(--color-border);
border-radius: var(--border-radius-medium-2);
}
.icon-btn:hover .icon :deep(path),
.icon-btn.active .icon :deep(path),
.icon-btn:hover .icon :deep(circle),
.icon-btn.active .icon :deep(circle) {
fill: var(--color-accent);
.icon-btn.icon-btn-accent:hover,
.icon-btn.icon-btn-accent.active {
color: var(--color-accent) !important;
}
.icon-btn:disabled .icon :deep(path),
.icon-btn:disabled .icon :deep(circle) {
fill: var(--color-border);
.icon-btn.icon-btn-secondary:hover,
.icon-btn.icon-btn-secondary.active {
color: var(--color-text-base) !important;
}
.icon-btn:disabled {
color: var(--color-border) !important;
}
.icon-btn:disabled {
@@ -80,10 +111,6 @@ export default {
pointer-events: none;
}
.disabled.icon-btn:hover .icon :deep(path) {
fill: var(--color-border);
}
.icon-in-progress {
position: absolute;
width: 26px;
@@ -1,14 +1,21 @@
<template>
<modal
:modalId="name"
v-model="show"
class="dialog"
:clickToClose="false"
:contentTransition="{ name: 'loading-dialog' }"
:overlayTransition="{ name: 'loading-dialog' }"
@update:model-value="$emit('update:modelValue', $event)"
>
<div class="dialog-header">
{{ title }}
<close-icon :disabled="loading" @click="$emit('cancel')" />
<IconButton
icon="myicon myicon-close"
variant="secondary"
:iconSize="14"
:disabled="loading"
@click="cancel"
/>
</div>
<div class="dialog-body">
<div v-if="loading" class="loading-dialog-body">
@@ -28,7 +35,7 @@
class="secondary"
type="button"
:disabled="loading"
@click="$emit('cancel')"
@click="cancel"
>
Cancel
</button>
@@ -45,31 +52,40 @@
</template>
<script>
import LoadingIndicator from '@/components/LoadingIndicator'
import CloseIcon from '@/components/svg/close'
import LoadingIndicator from '@/components/Common/LoadingIndicator'
import IconButton from '@/components/Common/IconButton.vue'
export default {
name: 'LoadingDialog',
components: { LoadingIndicator, CloseIcon },
components: { LoadingIndicator, IconButton },
props: {
modelValue: Boolean,
loadingMsg: String,
successMsg: String,
actionBtnName: String,
name: String,
title: String,
loading: Boolean
},
emits: ['cancel', 'action'],
emits: ['cancel', 'action', 'update:modelValue'],
data() {
return {
show: this.modelValue
}
},
watch: {
modelValue() {
this.show = this.modelValue
},
loading() {
if (this.loading) {
this.$modal.show(this.name)
this.$emit('update:modelValue', true)
}
}
},
methods: {
cancel() {
this.$emit('cancel')
this.$emit('update:modelValue', false)
}
}
}
@@ -18,7 +18,7 @@
</template>
<script>
import LoadingIndicator from '@/components/LoadingIndicator'
import LoadingIndicator from '@/components/Common/LoadingIndicator'
export default {
name: 'Logs',
@@ -13,16 +13,16 @@
:style="movableSplitterStyle"
/>
<div
v-show="!before.hidden"
ref="left"
class="splitpanes-pane"
:size="paneBefore.size"
max-size="30"
:style="styles.before"
>
<slot name="left-pane" />
</div>
<!-- Splitter start-->
<div
v-show="!before.hidden && !after.hidden"
class="splitpanes-splitter"
@mousedown="bindEvents"
@touchstart="bindEvents"
@@ -64,7 +64,12 @@
</div>
</div>
<!-- splitter end -->
<div ref="right" class="splitpanes-pane" :style="styles.after">
<div
v-show="!after.hidden"
ref="right"
class="splitpanes-pane"
:style="styles.after"
>
<slot name="right-pane" />
</div>
</div>
@@ -114,10 +119,12 @@ export default {
styles() {
return {
before: {
[this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%`
[this.horizontal ? 'height' : 'width']:
`${this.after.hidden ? 100 : this.paneBefore.size}%`
},
after: {
[this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%`
[this.horizontal ? 'height' : 'width']:
`${this.before.hidden ? 100 : this.paneAfter.size}%`
}
}
},
@@ -5,31 +5,47 @@
:class="['text-field-label', { error: errorMsg }, { disabled: disabled }]"
>
{{ label }}
<hint-icon
<IconButton
v-if="hint"
icon="myicon myicon-hint"
variant="secondary"
class="hint"
:hint="hint"
:maxWidth="maxHintWidth || '149px'"
:tooltip="hint"
:maxTooltipWidth="maxHintWidth"
:iconSize="20"
/>
</div>
<input
type="text"
:placeholder="placeholder"
:class="{ error: errorMsg }"
:style="{ width: width }"
:value="modelValue"
:disabled="disabled"
@input="$emit('update:modelValue', $event.target.value)"
/>
<div :class="['text-field-container', { disabled }]">
<input
type="text"
:placeholder="placeholder"
:class="{ error: errorMsg }"
:style="{ width: width }"
:value="modelValue"
:disabled="disabled"
@input="$emit('update:modelValue', $event.target.value)"
/>
<button
v-if="showClearButton"
type="button"
:disabled="disabled"
class="clear-btn"
@click="$emit('update:modelValue', '')"
>
<i class="myicon myicon-clear" />
</button>
</div>
<div v-show="errorMsg" class="text-field-error">{{ errorMsg }}</div>
</div>
</template>
<script>
import HintIcon from '@/components/svg/hint'
import IconButton from '@/components/Common/IconButton.vue'
export default {
name: 'TextField',
components: { HintIcon },
components: { IconButton },
props: {
placeholder: String,
label: String,
@@ -37,23 +53,41 @@ export default {
modelValue: String,
width: String,
hint: String,
maxHintWidth: String,
disabled: Boolean
maxHintWidth: { type: String, default: '149px' },
disabled: Boolean,
showClearButton: Boolean
},
emits: ['update:modelValue']
}
</script>
<style scoped>
input {
.text-field-container {
background: var(--color-white);
border: 1px solid var(--color-border);
color: var(--color-text-base);
border-radius: var(--border-radius-medium-2);
height: 36px;
padding: 0 8px;
font-size: 13px;
box-sizing: border-box;
display: flex;
align-items: center;
}
.text-field-container.disabled {
background: var(--color-bg-light);
}
.text-field-container.error {
border-color: var(--color-text-error);
}
input {
background: inherit;
border: none;
color: var(--color-text-base);
height: 100%;
padding: 0;
display: block;
}
@@ -66,15 +100,10 @@ input:focus {
}
input:disabled {
background: var(--color-bg-light);
color: var(--color-text-light-2);
cursor: default;
}
input.error {
border-color: var(--color-text-error);
}
.text-field-label {
font-size: 12px;
color: var(--color-text-base);
@@ -86,8 +115,8 @@ input.error {
.text-field-label .hint {
position: absolute;
top: -2px;
right: -22px;
top: -6px;
right: -26px;
}
.text-field-label.error {
@@ -108,4 +137,31 @@ input.error {
.text-field-error:first-letter {
text-transform: uppercase;
}
:deep(.icon-tooltip) {
display: block;
white-space: normal;
height: auto;
line-height: normal;
padding: 6px;
box-sizing: border-box;
}
.clear-btn {
background: transparent;
border: none;
padding: 0;
height: 20px;
width: 20px;
color: var(--color-border);
font-size: 20px;
}
.clear-btn i {
display: block;
}
.clear-btn:not(:disabled):hover {
color: var(--color-danger);
}
</style>
@@ -19,11 +19,23 @@
<div class="name">{{ getSymbolName(modelValue) }}</div>
</div>
<div class="controls" @click.stop>
<clear-icon :disabled="disabled" @click="clear" />
<drop-down-chevron
<button
type="button"
:disabled="disabled"
class="clear-btn"
@click="clear"
>
<i class="myicon myicon-clear" />
</button>
<button
type="button"
:disabled="disabled"
class="open-list-btn"
@click="!disabled && (showOptions = !showOptions)"
/>
>
<i class="myicon myicon-dropdown-chevron" />
</button>
</div>
</div>
<div v-show="showOptions" class="options" :style="{ width: width }">
@@ -42,12 +54,9 @@
<script>
import ascii from './ascii'
import DropDownChevron from '@/components/svg/dropDownChevron'
import ClearIcon from '@/components/svg/clear'
export default {
name: 'DelimiterSelector',
components: { DropDownChevron, ClearIcon },
props: {
modelValue: String,
width: String,
@@ -130,6 +139,7 @@ export default {
.controls {
display: flex;
align-items: center;
gap: 4px;
}
.options {
@@ -207,4 +217,22 @@ input:disabled {
.disabled .delimiter-selector-container {
background: var(--color-bg-light);
}
.controls button {
background: transparent;
border: none;
padding: 0;
height: 20px;
width: 20px;
color: var(--color-border);
font-size: 20px;
}
.clear-btn:not(:disabled):hover {
color: var(--color-danger);
}
.open-list-btn:not(:disabled):hover {
color: var(--color-accent);
}
</style>
+13 -6
View File
@@ -8,7 +8,14 @@
>
<div class="dialog-header">
{{ typeName }} import
<close-icon :disabled="disableDialog" @click="cancelImport" />
<IconButton
icon="myicon myicon-close"
variant="secondary"
:iconSize="14"
:disabled="disableDialog"
data-test="import-close-btn"
@click="cancelImport"
/>
</div>
<div class="dialog-body">
<text-field
@@ -101,12 +108,12 @@
<script>
import csv from '@/lib/csv'
import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField'
import IconButton from '@/components/Common/IconButton.vue'
import TextField from '@/components/Common/TextField'
import DelimiterSelector from './DelimiterSelector'
import CheckBox from '@/components/CheckBox'
import CheckBox from '@/components/Common/CheckBox'
import SqlTable from '@/components/SqlTable'
import Logs from '@/components/Logs'
import Logs from '@/components/Common/Logs'
import time from '@/lib/utils/time'
import fIo from '@/lib/utils/fileIo'
import events from '@/lib/utils/events'
@@ -114,8 +121,8 @@ import events from '@/lib/utils/events'
export default {
name: 'CsvJsonImport',
components: {
CloseIcon,
TextField,
IconButton,
DelimiterSelector,
CheckBox,
SqlTable,
@@ -4,78 +4,111 @@
<component
:is="mode"
ref="viewComponent"
v-model:importToPngEnabled="importToPngEnabled"
v-model:importToSvgEnabled="importToSvgEnabled"
:initOptions="mode === initMode ? initOptions : undefined"
v-model:exportToPngEnabled="exportToPngEnabled"
v-model:exportToSvgEnabled="exportToSvgEnabled"
v-model:exportToHtmlEnabled="exportToHtmlEnabled"
v-model:exportToClipboardEnabled="exportToClipboardEnabled"
:initOptions="initOptionsByMode[mode]"
:data-sources="dataSource"
:showViewSettings="showViewSettings"
:showValueViewer="viewValuePanelVisible"
@loading-image-completed="loadingImage = false"
@update="$emit('update')"
/>
</div>
<side-tool-bar panel="dataView" @switch-to="$emit('switchTo', $event)">
<icon-button
ref="chartBtn"
icon="myicon myicon-chart"
:active="mode === 'chart'"
tooltip="Switch to chart"
tooltipPosition="top-left"
@click="mode = 'chart'"
>
<chart-icon />
</icon-button>
/>
<icon-button
ref="pivotBtn"
icon="myicon myicon-pivot"
:active="mode === 'pivot'"
tooltip="Switch to pivot"
tooltipPosition="top-left"
@click="mode = 'pivot'"
>
<pivot-icon />
</icon-button>
/>
<icon-button
ref="graphBtn"
icon="myicon myicon-graph"
:active="mode === 'graph'"
tooltip="Switch to graph"
tooltipPosition="top-left"
@click="mode = 'graph'"
/>
<div class="side-tool-bar-divider" />
<icon-button
:disabled="!importToPngEnabled || loadingImage"
ref="settingsBtn"
icon="myicon myicon-settings"
:active="showViewSettings"
tooltip="Toggle visualisation settings visibility"
tooltipPosition="top-left"
@click="showViewSettings = !showViewSettings"
/>
<icon-button
v-if="mode !== 'pivot'"
ref="viewDetailsBtn"
icon="myicon myicon-view-cell-value"
tooltip="View details"
tooltipPosition="top-left"
:active="viewValuePanelVisible"
@click="viewValuePanelVisible = !viewValuePanelVisible"
/>
<div class="side-tool-bar-divider" />
<icon-button
ref="pngExportBtn"
icon="myicon myicon-png"
:disabled="!exportToPngEnabled || loadingImage"
:loading="loadingImage"
tooltip="Save as PNG image"
tooltipPosition="top-left"
@click="saveAsPng"
>
<png-icon />
</icon-button>
/>
<icon-button
ref="svgExportBtn"
:disabled="!importToSvgEnabled"
icon="myicon myicon-export-to-svg"
:disabled="!exportToSvgEnabled"
tooltip="Save as SVG"
tooltipPosition="top-left"
@click="saveAsSvg"
>
<export-to-svg-icon />
</icon-button>
/>
<icon-button
ref="htmlExportBtn"
icon="myicon myicon-html"
:disabled="!exportToHtmlEnabled"
tooltip="Save as HTML"
tooltipPosition="top-left"
@click="saveAsHtml"
>
<HtmlIcon />
</icon-button>
/>
<icon-button
ref="copyToClipboardBtn"
icon="myicon myicon-clipboard"
:disabled="!exportToClipboardEnabled"
:loading="copyingImage"
tooltip="Copy visualisation to clipboard"
tooltipPosition="top-left"
@click="prepareCopy"
>
<clipboard-icon />
</icon-button>
/>
</side-tool-bar>
<loading-dialog
v-model="showLoadingDialog"
loadingMsg="Rendering the visualisation..."
successMsg="Image is ready"
actionBtnName="Copy"
name="prepareCopy"
title="Copy to clipboard"
:loading="preparingCopy"
@action="copyToClipboard"
@@ -85,18 +118,13 @@
</template>
<script>
import Chart from './Chart'
import Pivot from './Pivot'
import SideToolBar from '../SideToolBar'
import IconButton from '@/components/IconButton'
import ChartIcon from '@/components/svg/chart'
import PivotIcon from '@/components/svg/pivot'
import HtmlIcon from '@/components/svg/html'
import ExportToSvgIcon from '@/components/svg/exportToSvg'
import PngIcon from '@/components/svg/png'
import ClipboardIcon from '@/components/svg/clipboard'
import Chart from '@/components/Chart.vue'
import Pivot from '@/components/Pivot'
import Graph from '@/components/Graph/index.vue'
import SideToolBar from '@/components/SideToolBar'
import IconButton from '@/components/Common/IconButton'
import cIo from '@/lib/utils/clipboardIo'
import loadingDialog from '@/components/LoadingDialog'
import loadingDialog from '@/components/Common/LoadingDialog.vue'
import time from '@/lib/utils/time'
import events from '@/lib/utils/events'
@@ -105,14 +133,9 @@ export default {
components: {
Chart,
Pivot,
Graph,
SideToolBar,
IconButton,
ChartIcon,
PivotIcon,
ExportToSvgIcon,
PngIcon,
HtmlIcon,
ClipboardIcon,
loadingDialog
},
props: {
@@ -124,12 +147,22 @@ export default {
data() {
return {
mode: this.initMode || 'chart',
importToPngEnabled: true,
importToSvgEnabled: true,
exportToPngEnabled: true,
exportToSvgEnabled: true,
exportToHtmlEnabled: true,
exportToClipboardEnabled: true,
loadingImage: false,
copyingImage: false,
preparingCopy: false,
dataToCopy: null
dataToCopy: null,
initOptionsByMode: {
chart: this.initMode === 'chart' ? this.initOptions : null,
pivot: this.initMode === 'pivot' ? this.initOptions : null,
graph: this.initMode === 'graph' ? this.initOptions : null
},
showLoadingDialog: false,
showViewSettings: true,
viewValuePanelVisible: false
}
},
computed: {
@@ -138,9 +171,11 @@ export default {
}
},
watch: {
mode() {
mode(newMode, oldMode) {
this.$emit('update')
this.importToPngEnabled = true
this.exportToPngEnabled = true
this.exportToClipboardEnabled = true
this.initOptionsByMode[oldMode] = this.getOptionsForSave()
}
},
methods: {
@@ -170,14 +205,13 @@ export default {
async prepareCopy() {
if ('ClipboardItem' in window) {
this.preparingCopy = true
this.$modal.show('prepareCopy')
this.showLoadingDialog = true
const t0 = performance.now()
await time.sleep(0)
this.dataToCopy = await this.$refs.viewComponent.prepareCopy()
const t1 = performance.now()
if (t1 - t0 < 950) {
this.$modal.hide('prepareCopy')
this.copyToClipboard()
} else {
this.preparingCopy = false
@@ -190,14 +224,13 @@ export default {
)
}
},
async copyToClipboard() {
copyToClipboard() {
cIo.copyImage(this.dataToCopy)
this.$modal.hide('prepareCopy')
this.showLoadingDialog = false
this.exportSignal('clipboard')
},
cancelCopy() {
this.dataToCopy = null
this.$modal.hide('prepareCopy')
},
saveAsSvg() {
@@ -218,7 +251,9 @@ export default {
events.send(
this.mode === 'chart' || this.plotlyInPivot
? 'viz_plotly.export'
: 'viz_pivot.export',
: this.mode === 'graph'
? 'viz_graph.export'
: 'viz_pivot.export',
null,
eventLabels
)
+15 -3
View File
@@ -1,6 +1,11 @@
<template>
<div class="db-uploader-container" :style="{ width }">
<change-db-icon v-if="type === 'small'" @click="browse" />
<IconButton
v-if="type === 'small'"
icon="myicon myicon-change-db"
tooltip="Load another database, CSV, JSON or NDJSON"
@click="browse"
/>
<div v-if="type === 'illustrated'" class="drop-area-container">
<div
class="drop-area"
@@ -56,15 +61,15 @@
<script>
import fIo from '@/lib/utils/fileIo'
import ChangeDbIcon from '@/components/svg/changeDb'
import database from '@/lib/database'
import CsvJsonImport from '@/components/CsvJsonImport'
import events from '@/lib/utils/events'
import IconButton from '@/components/Common/IconButton'
export default {
name: 'DbUploader',
components: {
ChangeDbIcon,
IconButton,
CsvJsonImport
},
props: {
@@ -125,6 +130,13 @@ export default {
},
async checkFile(file) {
if (!file) {
alert(
'You should drop a file. ' +
'It can be an SQLite database, CSV file, JSON or NDJSON file.'
)
return
}
this.state = 'dropping'
this.newDb = database.getNewDatabase()
@@ -0,0 +1,141 @@
<template>
<Field label="Scaling ratio" fieldContainerClassName="test_fa2_scaling">
<NumericInput
:value="modelValue.scalingRatio"
@update="update('scalingRatio', $event)"
/>
</Field>
<Field
label="Prevent overlapping"
fieldContainerClassName="test_fa2_adjustSizes"
>
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.adjustSizes"
@option-change="update('adjustSizes', $event)"
/>
</Field>
<Field
label="Barnes-Hut optimize"
fieldContainerClassName="test_fa2_barnes_hut"
>
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.barnesHutOptimize"
@option-change="update('barnesHutOptimize', $event)"
/>
</Field>
<Field
v-show="modelValue.barnesHutOptimize"
label="Barnes-Hut Theta"
fieldContainerClassName="test_fa2_barnes_theta"
>
<NumericInput
:value="modelValue.barnesHutTheta"
@update="update('barnesHutTheta', $event)"
/>
</Field>
<Field
label="Strong gravity mode"
fieldContainerClassName="test_fa2_strong_gravity"
>
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.strongGravityMode"
@option-change="update('strongGravityMode', $event)"
/>
</Field>
<Field
label="Noack's LinLog model"
fieldContainerClassName="test_fa2_lin_log"
>
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.linLogMode"
@option-change="update('linLogMode', $event)"
/>
</Field>
<Field
label="Outbound attraction distribution"
fieldContainerClassName="test_fa2_outbound_attraction"
>
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.outboundAttractionDistribution"
@option-change="update('outboundAttractionDistribution', $event)"
/>
</Field>
<Field label="Slow down" fieldContainerClassName="test_fa2_slow_down">
<NumericInput
:value="modelValue.slowDown"
:min="0"
@update="update('slowDown', $event)"
/>
</Field>
<Field label="Edge weight">
<Dropdown
:options="keyOptions"
:value="modelValue.weightSource"
className="test_fa2_weight_source"
@change="update('weightSource', $event)"
/>
</Field>
<Field
v-show="modelValue.weightSource"
label="Edge weight influence"
fieldContainerClassName="test_fa2_weight_influence"
>
<NumericInput
:value="modelValue.edgeWeightInfluence"
@update="update('edgeWeightInfluence', $event)"
/>
</Field>
</template>
<script>
import { markRaw } from 'vue'
import { applyPureReactInVue } from 'veaury'
import Field from 'react-chart-editor/lib/components/fields/Field'
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
Field: applyPureReactInVue(Field),
RadioBlocks: applyPureReactInVue(RadioBlocks),
Dropdown: applyPureReactInVue(Dropdown),
NumericInput: applyPureReactInVue(NumericInput)
},
props: {
modelValue: Object,
keyOptions: Array
},
emits: ['update:modelValue'],
data() {
return {
booleanOptions: markRaw([
{ label: 'Yes', value: true },
{ label: 'No', value: false }
])
}
},
methods: {
update(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>
@@ -0,0 +1,75 @@
<template>
<Field
label="Hierarchy attributes"
fieldContainerClassName="multiselect-field"
>
<multiselect
:modelValue="modelValue.hierarchyAttributes"
class="sqliteviz-select"
:options="keyOptions"
:multiple="true"
:hideSelected="true"
:closeOnSelect="true"
:showLabels="false"
:max="keyOptions.length"
placeholder=""
openDirection="bottom"
@update:model-value="update('hierarchyAttributes', $event)"
>
<template #maxElements>
<span class="no-results">No Results</span>
</template>
<template #placeholder>Select an Option</template>
<template #noResult>
<span class="no-results">No Results</span>
</template>
</multiselect>
</Field>
<Field label="Seed value" fieldContainerClassName="test_seed_value">
<NumericInput
:value="modelValue.seedValue"
@update="update('seedValue', $event)"
/>
</Field>
</template>
<script>
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 Multiselect from 'vue-multiselect'
import 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
Field: applyPureReactInVue(Field),
NumericInput: applyPureReactInVue(NumericInput),
Multiselect
},
props: {
modelValue: Object,
keyOptions: Array
},
emits: ['update:modelValue'],
methods: {
update(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>
<style scoped>
:deep(.sqliteviz-select.multiselect--active .multiselect__input) {
width: 100% !important;
}
:deep(.multiselect-field .field__widget > *) {
flex-grow: 1 !important;
}
</style>
+148
View File
@@ -0,0 +1,148 @@
<template>
<Field label="Color" fieldContainerClassName="test_edge_color">
<RadioBlocks
:options="edgeColorTypeOptions"
:activeOption="modelValue.type"
@option-change="updateColorType"
/>
<Field
v-if="modelValue.type === 'constant'"
fieldContainerClassName="test_edge_color_value"
>
<ColorPicker
:selectedColor="modelValue.value"
@color-change="updateSettings('value', $event)"
/>
</Field>
<template v-else>
<Field fieldContainerClassName="test_edge_color_value">
<Dropdown
v-if="modelValue.type === 'variable'"
:options="keyOptions"
:value="modelValue.source"
@change="updateSettings('source', $event)"
/>
</Field>
<Field fieldContainerClassName="test_edge_color_mapping_mode">
<RadioBlocks
:options="colorSourceUsageOptions"
:activeOption="modelValue.sourceUsage"
@option-change="updateSettings('sourceUsage', $event)"
/>
</Field>
<Field v-if="modelValue.sourceUsage === 'map_to'">
<ColorscalePicker
:selected="modelValue.colorscale"
className="colorscale-picker"
@colorscale-change="updateSettings('colorscale', $event)"
/>
</Field>
</template>
</Field>
<Field
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
label="Color as"
fieldContainerClassName="test_edge_color_as"
>
<RadioBlocks
:options="сolorAsOptions"
:activeOption="modelValue.mode"
@option-change="updateSettings('mode', $event)"
/>
</Field>
<Field
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
label="Colorscale direction"
fieldContainerClassName="test_edge_color_colorscale_direction"
>
<RadioBlocks
:options="сolorscaleDirections"
:activeOption="modelValue.colorscaleDirection"
@option-change="updateSettings('colorscaleDirection', $event)"
/>
</Field>
</template>
<script>
import { markRaw } from 'vue'
import { applyPureReactInVue } from 'veaury'
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
import ColorscalePicker from 'react-chart-editor/lib/components/widgets/ColorscalePicker'
import ColorPicker from 'react-chart-editor/lib/components/widgets/ColorPicker'
import Field from 'react-chart-editor/lib/components/fields/Field'
import 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
Dropdown: applyPureReactInVue(Dropdown),
RadioBlocks: applyPureReactInVue(RadioBlocks),
Field: applyPureReactInVue(Field),
ColorscalePicker: applyPureReactInVue(ColorscalePicker),
ColorPicker: applyPureReactInVue(ColorPicker)
},
props: {
modelValue: Object,
keyOptions: Array
},
emits: ['update:modelValue'],
data() {
return {
edgeColorTypeOptions: markRaw([
{ label: 'Constant', value: 'constant' },
{ label: 'Variable', value: 'variable' }
]),
сolorAsOptions: markRaw([
{ label: 'Continious', value: 'continious' },
{ label: 'Categorical', value: 'categorical' }
]),
сolorscaleDirections: markRaw([
{ label: 'Normal', value: 'normal' },
{ label: 'Reversed', value: 'reversed' }
]),
colorSourceUsageOptions: markRaw([
{ label: 'Direct', value: 'direct' },
{ label: 'Map to', value: 'map_to' }
]),
defaultColorSettings: {
constant: { value: '#1F77B4' },
variable: {
source: null,
sourceUsage: 'map_to',
colorscale: null,
mode: 'categorical',
colorscaleDirection: 'normal'
}
}
}
},
methods: {
updateColorType(newColorType) {
const currentColorType = this.modelValue.type
this.defaultColorSettings[currentColorType] = this.modelValue
this.$emit('update:modelValue', {
type: newColorType,
...this.defaultColorSettings[newColorType]
})
},
updateSettings(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>
<style scoped>
:deep(.customPickerContainer) {
float: right;
}
</style>
+93
View File
@@ -0,0 +1,93 @@
<template>
<Field label="Size" fieldContainerClassName="test_edge_size">
<RadioBlocks
:options="edgeSizeTypeOptions"
:activeOption="modelValue.type"
@option-change="updateSizeType"
/>
<Field fieldContainerClassName="test_edge_size_value">
<NumericInput
v-if="modelValue.type === 'constant'"
:value="modelValue.value"
:min="1"
@update="updateSettings('value', $event)"
/>
<Dropdown
v-if="modelValue.type === 'variable'"
:options="keyOptions"
:value="modelValue.source"
@change="updateSettings('source', $event)"
/>
</Field>
</Field>
<template v-if="modelValue.type !== 'constant'">
<Field label="Size scale" fieldContainerClassName="test_edge_size_scale">
<NumericInput
:value="modelValue.scale"
@update="updateSettings('scale', $event)"
/>
</Field>
<Field label="Minimum size" fieldContainerClassName="test_edge_size_min">
<NumericInput
:value="modelValue.min"
@update="updateSettings('min', $event)"
/>
</Field>
</template>
</template>
<script>
import { markRaw } from 'vue'
import { applyPureReactInVue } from 'veaury'
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
import Field from 'react-chart-editor/lib/components/fields/Field'
import 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
Dropdown: applyPureReactInVue(Dropdown),
NumericInput: applyPureReactInVue(NumericInput),
RadioBlocks: applyPureReactInVue(RadioBlocks),
Field: applyPureReactInVue(Field)
},
props: {
modelValue: Object,
keyOptions: Array
},
emits: ['update:modelValue'],
data() {
return {
edgeSizeTypeOptions: markRaw([
{ label: 'Constant', value: 'constant' },
{ label: 'Variable', value: 'variable' }
]),
defaultSizeSettings: {
constant: { value: 4 },
variable: { source: null, scale: 1, min: 1 }
}
}
},
methods: {
updateSizeType(newSizeType) {
const currentSizeType = this.modelValue.type
this.defaultSizeSettings[currentSizeType] = this.modelValue
this.$emit('update:modelValue', {
type: newSizeType,
...this.defaultSizeSettings[newSizeType]
})
},
updateSettings(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>
@@ -0,0 +1,46 @@
<template>
<Field
label="Initial iterations"
fieldContainerClassName="test_fa2_iteration_amount"
>
<NumericInput
:value="modelValue.initialIterationsAmount"
:min="1"
@update="update('initialIterationsAmount', $event)"
/>
</Field>
<Field label="Gravity" fieldContainerClassName="test_fa2_gravity">
<NumericInput
:value="modelValue.gravity"
@update="update('gravity', $event)"
/>
</Field>
</template>
<script>
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 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
Field: applyPureReactInVue(Field),
NumericInput: applyPureReactInVue(NumericInput)
},
props: {
modelValue: Object,
keyOptions: Array
},
emits: ['update:modelValue'],
methods: {
update(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>
@@ -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>
+909
View File
@@ -0,0 +1,909 @@
<template>
<div :class="['plotly_editor', { with_controls: showViewSettings }]">
<GraphEditorControls v-show="showViewSettings">
<PanelMenuWrapper>
<Panel group="Structure" name="Graph">
<Fold name="Graph">
<Field>
Map your result set records to node and edge properties required
to build a graph. Learn more about result set requirements in the
<a href="https://sqliteviz.com/docs/graph/" target="_blank">
documentation</a
>.
</Field>
<Field ref="objectTypeField" label="Object type">
<Dropdown
:options="keysOptions"
:value="settings.structure.objectType"
className="test_object_type_select"
@change="updateStructure('objectType', $event)"
/>
<Field>
A field indicating if the record is node (value&nbsp;0) or edge
(value&nbsp;1).
</Field>
</Field>
<Field label="Node Id">
<Dropdown
:options="keysOptions"
:value="settings.structure.nodeId"
className="test_node_id_select"
@change="updateStructure('nodeId', $event)"
/>
<Field> A field keeping unique node identifier. </Field>
</Field>
<Field label="Edge source">
<Dropdown
:options="keysOptions"
:value="settings.structure.edgeSource"
className="test_edge_source_select"
@change="updateStructure('edgeSource', $event)"
/>
<Field>
A field keeping a node identifier where the edge starts.
</Field>
</Field>
<Field label="Edge target">
<Dropdown
:options="keysOptions"
:value="settings.structure.edgeTarget"
className="test_edge_target_select"
@change="updateStructure('edgeTarget', $event)"
/>
<Field>
A field keeping a node identifier where the edge ends.
</Field>
</Field>
</Fold>
</Panel>
<Panel group="Style" name="General">
<Fold name="General">
<Field label="Background color">
<ColorPicker
:selectedColor="settings.style.backgroundColor"
@color-change="settings.style.backgroundColor = $event"
/>
</Field>
<Field label="Highlight mode">
<Dropdown
:options="highlightModeOptions"
:value="settings.style.highlightMode"
className="test_highlight_mode_select"
@change="updateHighlightNodeMode"
/>
</Field>
</Fold>
</Panel>
<Panel group="Style" name="Nodes">
<Fold name="Nodes">
<Field label="Label">
<Dropdown
:options="keysOptions"
:value="settings.style.nodes.label.source"
className="test_label_select"
@change="updateNodes('label.source', $event)"
/>
</Field>
<Field label="Label color">
<ColorPicker
:selectedColor="settings.style.nodes.label.color"
@color-change="updateNodes('label.color', $event)"
/>
</Field>
<NodeSizeSettings
v-model="settings.style.nodes.size"
:keyOptions="keysOptions"
@update:model-value="updateNodes('size', $event)"
/>
<NodeColorSettings
v-model="settings.style.nodes.color"
:keyOptions="keysOptions"
@update:model-value="updateNodes('color', $event)"
/>
</Fold>
</Panel>
<Panel group="Style" name="Edges">
<Fold name="Edges">
<Field
label="Direction"
fieldContainerClassName="test_edge_direction"
>
<RadioBlocks
:options="visibilityOptions"
:activeOption="settings.style.edges.showDirection"
@option-change="updateEdges('showDirection', $event)"
/>
</Field>
<Field label="Label">
<Dropdown
:options="keysOptions"
:value="settings.style.edges.label.source"
className="test_edge_label_select"
@change="updateEdges('label.source', $event)"
/>
</Field>
<Field label="Label color">
<ColorPicker
:selectedColor="settings.style.edges.label.color"
@color-change="updateEdges('label.color', $event)"
/>
</Field>
<EdgeSizeSettings
v-model="settings.style.edges.size"
:keyOptions="keysOptions"
@update:model-value="updateEdges('size', $event)"
/>
<EdgeColorSettings
v-model="settings.style.edges.color"
:keyOptions="keysOptions"
@update:model-value="updateEdges('color', $event)"
/>
</Fold>
</Panel>
<Panel group="Style" name="Layout">
<Fold name="Layout">
<Field label="Algorithm">
<Dropdown
:options="layoutOptions"
:value="settings.layout.type"
:clearable="false"
className="test_layout_algorithm_select"
@change="updateLayout($event)"
/>
</Field>
<component
:is="layoutSettingsComponentMap[settings.layout.type]"
v-if="settings.layout.type !== 'circular'"
v-model="settings.layout.options"
:keyOptions="keysOptions"
@update:model-value="updateLayout(settings.layout.type)"
/>
</Fold>
<template v-if="settings.layout.type === 'forceAtlas2'">
<Fold name="Seed layout">
<Field>
If you already built a graph using another layout, the initial
algorithm doesn't apply unless you restart it.
</Field>
<ForceAtlasSeedLayoutSettings
v-model="settings.layout.options"
:keyOptions="keysOptions"
@update:model-value="updateLayout(settings.layout.type)"
/>
</Fold>
<Fold name="Advanced layout settings">
<AdvancedForceAtlasLayoutSettings
v-model="settings.layout.options"
:keyOptions="keysOptions"
@update:model-value="updateLayout(settings.layout.type)"
/>
</Fold>
<div class="force-atlas-buttons">
<Button
variant="secondary"
class="test_fa2_reset"
title="Set the settings to default or previously saved ones."
@click="resetFA2LayoutSettings"
>
Reset
</Button>
<Button
variant="primary"
class="test_fa2_toggle"
@click="toggleFA2Layout"
>
<template #node:icon>
<div class="button-icon-container">
<i v-if="!fa2Running" class="myicon myicon-run" />
<i v-else class="myicon myicon-pause" />
</div>
</template>
{{ 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 class="button-icon-container">
<i class="myicon myicon-restart" />
</div>
</template>
Restart
</Button>
</div>
</template>
</Panel>
</PanelMenuWrapper>
</GraphEditorControls>
<div
ref="graph"
class="test_graph_output"
:style="{
height: '100%',
width: '100%',
backgroundColor: settings.style.backgroundColor
}"
/>
<GraphSearchPanel
v-model:nodesMatchingSearchQuery="nodesMatchingSearchQuery"
v-model:focusedFoundNodeId="focusedFoundNodeId"
:graph="graph"
class="search-panel"
/>
</div>
</template>
<script>
import { markRaw } from 'vue'
import GraphSearchPanel from '@/components/Graph/GraphSearchPanel.vue'
import { applyPureReactInVue } from 'veaury'
import GraphEditorControls from '@/lib/GraphEditorControls.jsx'
import { PanelMenuWrapper, Panel, Fold, Section } from 'react-chart-editor'
import 'react-chart-editor/lib/react-chart-editor.css'
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
import ColorPicker from 'react-chart-editor/lib/components/widgets/ColorPicker'
import Button from 'react-chart-editor/lib/components/widgets/Button'
import Field from 'react-chart-editor/lib/components/fields/Field'
import RandomLayoutSettings from '@/components/Graph/RandomLayoutSettings.vue'
import ForceAtlasLayoutSettings from '@/components/Graph/ForceAtlasLayoutSettings.vue'
import ForceAtlasSeedLayoutSettings from '@/components/Graph/ForceAtlasSeedLayoutSettings.vue'
// eslint-disable-next-line max-len
import AdvancedForceAtlasLayoutSettings from '@/components/Graph/AdvancedForceAtlasLayoutSettings.vue'
import CirclePackLayoutSettings from '@/components/Graph/CirclePackLayoutSettings.vue'
import FA2Layout from 'graphology-layout-forceatlas2/worker'
import * as forceAtlas2 from 'graphology-layout-forceatlas2'
import { downloadAsPNG, drawOnCanvas } from '@sigma/export-image'
import {
buildNodes,
buildEdges,
updateNodes,
updateEdges,
reduceNodes,
reduceEdges,
clearNodeCoordinates
} from '@/lib/graphHelper'
import Graph from 'graphology'
import { circular, random, circlepack } from 'graphology-layout'
import Sigma from 'sigma'
import seedrandom from 'seedrandom'
import NodeColorSettings from '@/components/Graph/NodeColorSettings.vue'
import NodeSizeSettings from '@/components/Graph/NodeSizeSettings.vue'
import EdgeSizeSettings from '@/components/Graph/EdgeSizeSettings.vue'
import EdgeColorSettings from '@/components/Graph/EdgeColorSettings.vue'
import events from '@/lib/utils/events'
export default {
components: {
GraphEditorControls: applyPureReactInVue(GraphEditorControls),
PanelMenuWrapper: applyPureReactInVue(PanelMenuWrapper),
Panel: applyPureReactInVue(Panel),
PanelSection: applyPureReactInVue(Section),
Dropdown: applyPureReactInVue(Dropdown),
RadioBlocks: applyPureReactInVue(RadioBlocks),
Field: applyPureReactInVue(Field),
Fold: applyPureReactInVue(Fold),
Button: applyPureReactInVue(Button),
ColorPicker: applyPureReactInVue(ColorPicker),
RandomLayoutSettings,
CirclePackLayoutSettings,
NodeColorSettings,
NodeSizeSettings,
EdgeSizeSettings,
EdgeColorSettings,
AdvancedForceAtlasLayoutSettings,
ForceAtlasSeedLayoutSettings,
GraphSearchPanel
},
inject: ['tabLayout'],
props: {
dataSources: Object,
initOptions: Object,
showViewSettings: Boolean
},
emits: ['update', 'selectItem', 'clearSelection'],
data() {
return {
nodesMatchingSearchQuery: null,
focusedFoundNodeId: null,
graph: new Graph({ multi: true, allowSelfLoops: true }),
renderer: null,
fa2Layout: null,
fa2Running: false,
checkIteration: null,
visibilityOptions: markRaw([
{ label: 'Show', value: true },
{ label: 'Hide', value: false }
]),
layoutOptions: markRaw([
{ label: 'Circular', value: 'circular' },
{ label: 'Random', value: 'random' },
{ label: 'Circle pack', value: 'circlepack' },
{ label: 'ForceAtlas2', value: 'forceAtlas2' }
]),
layoutSettingsComponentMap: markRaw({
random: RandomLayoutSettings,
circlepack: CirclePackLayoutSettings,
forceAtlas2: ForceAtlasLayoutSettings
}),
layoutMethodMap: markRaw({
circular: this.applyCircularLayout,
random: this.applyRandomLayout,
circlepack: this.applyCirclePackLayout,
forceAtlas2: this.applyFA2Layout
}),
selectedNodeId: undefined,
hoveredNodeId: undefined,
selectedEdgeId: undefined,
hoveredEdgeId: undefined,
settings: this.initOptions
? JSON.parse(JSON.stringify(this.initOptions))
: {
structure: {
nodeId: null,
objectType: null,
edgeSource: null,
edgeTarget: null
},
style: {
backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: {
size: {
type: 'constant',
value: 10
},
color: {
type: 'constant',
value: '#1F77B4',
opacity: 100
},
label: {
source: null,
color: '#444444'
}
},
edges: {
showDirection: true,
size: {
type: 'constant',
value: 2
},
color: {
type: 'constant',
value: '#a2b1c6'
},
label: {
source: null,
color: '#a2b1c6'
}
}
},
layout: {
type: 'circular',
options: null
}
},
layoutOptionsArchive: {
random: null,
circlepack: null,
forceAtlas2: null
},
highlightModeOptions: markRaw([
{ label: 'Node alone', value: 'node_alone' },
{ label: 'Node and neighbors', value: 'node_and_neighbors' },
{
label: 'Include edges between neighbors',
value: 'include_neighbor_edges'
}
])
}
},
computed: {
records() {
if (!this.dataSources) {
return []
}
const firstColumnName = Object.keys(this.dataSources)[0]
try {
return (
this.dataSources[firstColumnName].map(json => JSON.parse(json)) || []
)
} catch {
return []
}
},
keysOptions() {
if (!this.dataSources) {
return []
}
const keySet = this.records.reduce((result, currentRecord) => {
Object.keys(currentRecord).forEach(key => result.add(key))
return result
}, new Set())
return Array.from(keySet)
},
neighborsOfSelectedNode() {
if (this.settings.style.highlightMode === 'node_alone') {
return undefined
}
return this.selectedNodeId
? new Set(this.graph.neighbors(this.selectedNodeId))
: undefined
},
neighborsOfHoveredNode() {
if (this.settings.style.highlightMode === 'node_alone') {
return undefined
}
return this.hoveredNodeId
? new Set(this.graph.neighbors(this.hoveredNodeId))
: undefined
},
hoveredEdgeExtremities() {
return this.hoveredEdgeId
? this.graph.extremities(this.hoveredEdgeId)
: []
},
selectedEdgeExtremities() {
return this.selectedEdgeId
? this.graph.extremities(this.selectedEdgeId)
: []
},
interactionState() {
return {
selectedNodeId: this.selectedNodeId,
hoveredNodeId: this.hoveredNodeId,
selectedEdgeId: this.selectedEdgeId,
hoveredEdgeId: this.hoveredEdgeId,
neighborsOfSelectedNode: this.neighborsOfSelectedNode,
neighborsOfHoveredNode: this.neighborsOfHoveredNode,
hoveredEdgeExtremities: this.hoveredEdgeExtremities,
selectedEdgeExtremities: this.selectedEdgeExtremities,
nodesMatchingSearchQuery: this.nodesMatchingSearchQuery,
focusedFoundNodeId: this.focusedFoundNodeId
}
}
},
watch: {
dataSources() {
if (this.dataSources) {
this.buildGraph()
}
},
settings: {
deep: true,
handler() {
this.$emit('update')
}
},
'settings.structure': {
deep: true,
handler() {
this.buildGraph()
}
},
'settings.layout.type': {
immediate: true,
handler() {
events.send('viz_graph.render', null, {
layout: this.settings.layout.type
})
}
},
tabLayout: {
deep: true,
handler() {
if (this.tabLayout.dataView !== 'hidden' && this.renderer) {
this.renderer.scheduleRender()
}
}
},
nodesMatchingSearchQuery() {
if (this.renderer) {
this.renderer.refresh({
skipIndexation: true
})
}
},
focusedFoundNodeId() {
if (this.renderer) {
this.renderer.refresh({
skipIndexation: true
})
}
if (this.focusedFoundNodeId) {
this.moveCameraToNode(this.focusedFoundNodeId)
}
}
},
mounted() {
if (this.dataSources) {
this.buildGraph()
}
},
methods: {
buildGraph() {
this.clearSelection()
this.focusedFoundNodeId = null
this.nodesMatchingSearchQuery = null
if (this.renderer) {
this.renderer.kill()
}
this.graph.clear()
buildNodes(this.graph, this.dataSources, this.settings)
buildEdges(this.graph, this.dataSources, this.settings)
// Apply visual settings
updateNodes(this.graph, this.settings.style.nodes)
updateEdges(this.graph, this.settings.style.edges)
this.updateLayout(this.settings.layout.type)
this.renderer = new Sigma(this.graph, this.$refs.graph, {
renderEdgeLabels: true,
allowInvalidContainer: true,
labelColor: { attribute: 'labelColor', color: '#444444' },
edgeLabelColor: { attribute: 'labelColor', color: '#a2b1c6' },
enableEdgeEvents: true,
zIndex: true,
nodeReducer: (node, data) =>
reduceNodes(node, data, this.interactionState, this.settings),
edgeReducer: (edge, data) =>
reduceEdges(
edge,
data,
this.interactionState,
this.settings,
this.graph
)
})
this.renderer.on('clickNode', ({ node }) => {
this.selectedNodeId = node
this.selectedEdgeId = undefined
this.$emit('selectItem', this.graph.getNodeAttributes(node).data)
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('clickEdge', ({ edge }) => {
this.selectedEdgeId = edge
this.selectedNodeId = undefined
this.$emit('selectItem', this.graph.getEdgeAttributes(edge).data)
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('clickStage', () => {
this.clearSelection()
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('enterNode', ({ node }) => {
this.hoveredNodeId = node
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('enterEdge', ({ edge }) => {
this.hoveredEdgeId = edge
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('leaveNode', () => {
this.hoveredNodeId = undefined
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('leaveEdge', () => {
this.hoveredEdgeId = undefined
this.renderer.refresh({
skipIndexation: true
})
})
if (this.settings.layout.type === 'forceAtlas2') {
this.autoRunFA2Layout()
}
},
clearSelection() {
this.selectedNodeId = undefined
this.selectedEdgeId = undefined
this.$emit('clearSelection')
},
updateHighlightNodeMode(mode) {
this.settings.style.highlightMode = mode
if (this.renderer) {
this.renderer.refresh({
skipIndexation: true
})
}
},
updateStructure(attributeName, value) {
this.settings.structure[attributeName] = value
},
updateNodes(attributeName, value) {
const attributePath = attributeName.split('.')
attributePath.reduce((result, current, index) => {
if (index === attributePath.length - 1) {
return (result[current] = value)
} else {
return result[current]
}
}, this.settings.style.nodes)
updateNodes(this.graph, {
[attributePath[0]]: this.settings.style.nodes[attributePath[0]]
})
},
updateEdges(attributeName, value) {
const attributePath = attributeName.split('.')
attributePath.reduce((result, current, index) => {
if (index === attributePath.length - 1) {
return (result[current] = value)
} else {
return result[current]
}
}, this.settings.style.edges)
updateEdges(this.graph, {
[attributePath[0]]: this.settings.style.edges[attributePath[0]]
})
},
updateLayout(layoutType) {
const prevLayout = this.settings.layout.type
// Change layout type? - restore layout settings or set default settings
if (layoutType !== prevLayout) {
this.layoutOptionsArchive[prevLayout] = this.settings.layout.options
this.settings.layout.options = this.layoutOptionsArchive[layoutType]
if (!this.settings.layout.options) {
if (layoutType === 'forceAtlas2') {
this.setRecommendedFA2Settings()
} else if (['random', 'circlepack'].includes(layoutType)) {
this.settings.layout.options = {
seedValue: 1
}
}
}
this.settings.layout.type = layoutType
}
// In any case kill FA2 if it exists
if (this.fa2Layout) {
if (this.fa2Layout.isRunning()) {
this.stopFA2Layout()
}
this.fa2Layout.kill()
}
this.layoutMethodMap[layoutType]()
if (layoutType === 'forceAtlas2' && layoutType !== prevLayout) {
this.autoRunFA2Layout()
}
},
applyCircularLayout() {
circular.assign(this.graph)
},
applyRandomLayout() {
random.assign(this.graph, {
rng: seedrandom(this.settings.layout.options.seedValue)
})
},
applyCirclePackLayout() {
this.graph.forEachNode(nodeId => {
this.graph.updateNode(nodeId, attributes => {
const newAttributes = { ...attributes }
// Delete old hierarchy attributes
Object.keys(newAttributes)
.filter(key => key.startsWith('hierarchyAttribute'))
.forEach(
hierarchyAttributeKey =>
delete newAttributes[hierarchyAttributeKey]
)
// Set new hierarchy attributes
this.settings.layout.options.hierarchyAttributes?.forEach(
(hierarchyAttribute, index) => {
newAttributes['hierarchyAttribute' + index] =
attributes.data[hierarchyAttribute]
}
)
return newAttributes
})
})
circlepack.assign(this.graph, {
hierarchyAttributes:
this.settings.layout.options.hierarchyAttributes?.map(
(_, index) => 'hierarchyAttribute' + index
) || [],
rng: seedrandom(this.settings.layout.options.seedValue)
})
},
applyFA2Layout() {
if (
!this.graph.someNode(
(nodeKey, attributes) =>
typeof attributes.x === 'number' && typeof attributes.y === 'number'
)
) {
this.layoutMethodMap[this.settings.layout.options.initialAlgorithm]()
}
const fa2settings = { ...this.settings.layout.options }
// delete all custom settings because they can break the algorithm running
delete fa2settings.initialAlgorithm
delete fa2settings.seedValue
delete fa2settings.initialIterationsAmount
delete fa2settings.hierarchyAttributes
this.fa2Layout = markRaw(
new FA2Layout(this.graph, {
getEdgeWeight: (_, attr) =>
this.settings.layout.options.weightSource
? attr.data[this.settings.layout.options.weightSource]
: 1,
settings: fa2settings
})
)
},
toggleFA2Layout() {
if (this.fa2Layout.isRunning()) {
this.stopFA2Layout()
} else {
this.fa2Running = true
this.fa2Layout.start()
}
},
stopFA2Layout() {
this.fa2Running = false
this.fa2Layout.stop()
if (this.checkIteration) {
this.fa2Layout.worker.removeEventListener(
'message',
this.checkIteration
)
this.checkIteration = null
}
},
restartFA2Layout() {
if (this.fa2Layout.isRunning()) {
this.stopFA2Layout()
}
this.fa2Layout.kill()
clearNodeCoordinates(this.graph)
this.applyFA2Layout()
this.autoRunFA2Layout()
},
autoRunFA2Layout() {
let iteration = 1
this.checkIteration = () => {
if (
iteration === this.settings.layout.options.initialIterationsAmount
) {
this.stopFA2Layout()
}
iteration++
}
this.fa2Layout.worker.addEventListener('message', this.checkIteration)
this.fa2Running = true
this.fa2Layout.start()
},
setRecommendedFA2Settings() {
const sensibleSettings = forceAtlas2.default.inferSettings(this.graph)
this.settings.layout.options = {
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,
...sensibleSettings
}
if (
[Infinity, -Infinity].includes(this.settings.layout.options.slowDown)
) {
this.settings.layout.options.slowDown = 1
}
},
resetFA2LayoutSettings() {
if (this.initOptions?.layout.type === 'forceAtlas2') {
this.settings.layout = JSON.parse(
JSON.stringify(this.initOptions.layout)
)
} else {
this.setRecommendedFA2Settings()
}
this.updateLayout(this.settings.layout.type)
},
saveAsPng() {
return downloadAsPNG(this.renderer, {
backgroundColor: this.settings.style.backgroundColor
})
},
prepareCopy() {
return drawOnCanvas(this.renderer, {
backgroundColor: this.settings.style.backgroundColor
})
},
moveCameraToNode(nodeId) {
const nodePosition = this.renderer.getNodeDisplayData(nodeId)
this.renderer.getCamera().animate(nodePosition, {
duration: 500
})
}
}
}
</script>
<style scoped>
.plotly_editor {
position: relative;
}
.plotly_editor.with_controls > div {
display: flex !important;
}
:deep(.customPickerContainer) {
float: right;
}
.force-atlas-buttons {
display: flex;
width: 100%;
gap: 16px;
}
.force-atlas-buttons :deep(button) {
flex-grow: 1;
flex-basis: 0;
}
.force-atlas-buttons :deep(.button__icon > div) {
padding: 0 3px;
}
.search-panel {
top: 0;
right: 0;
}
.button-icon-container {
font-size: 16px;
filter: drop-shadow(0 1px 2px rgba(42, 63, 95, 0.7));
vertical-align: bottom;
}
.button-icon-container i {
vertical-align: top;
}
</style>
+244
View File
@@ -0,0 +1,244 @@
<template>
<div class="graph-search-panel">
<IconButton
icon="myicon myicon-open-search"
:title="panelOpen ? 'Close search panel' : 'Open search panel'"
variant="secondary"
:class="{ 'close-search-panel': panelOpen }"
data-test="toggle-search-panel-visibility-btn"
@click="panelOpen = !panelOpen"
/>
<div v-show="panelOpen" class="graph-search-panel--search-controls">
<TextField
v-model.trim="searchQuery"
width="250px"
placeholder="Search nodes..."
:errorMsg="regExpIsValid ? '' : 'Invalid regular expression'"
showClearButton
class="query-input"
@keydown.enter.prevent="search"
@update:model-value="onSearchInput"
/>
<IconButton
icon="myicon myicon-match-case"
title="Match case"
:active="matchCase"
data-test="match-case-btn"
@click="matchCase = !matchCase"
/>
<IconButton
icon="myicon myicon-use-reg-exp"
title="Use regular expression"
:active="useRegExp"
data-test="use-reg-exp-btn"
@click="useRegExp = !useRegExp"
/>
<div class="counter">
<span
v-if="
nodesMatchingSearchQuery?.length > 0 && currentFoundNodeIndex === -1
"
>
{{ shortResultCountString }}
</span>
<span
v-if="
nodesMatchingSearchQuery?.length > 0 && currentFoundNodeIndex !== -1
"
>{{ currentFoundNodeIndex + 1 }} of
{{ nodesMatchingSearchQuery.length }}
</span>
<span
v-show="
nodesMatchingSearchQuery && nodesMatchingSearchQuery.length === 0
"
data-test="no-results-text"
>
No results
</span>
</div>
<IconButton
icon="myicon myicon-arrow"
title="Previous match"
:disabled="arrowsDisabled"
data-test="previous-match-btn"
@click="toNextFoundNode(-1)"
/>
<IconButton
icon="myicon myicon-arrow"
title="Next match"
:disabled="arrowsDisabled"
class="next-match-btn"
data-test="next-match-btn"
@click="toNextFoundNode(1)"
/>
</div>
</div>
</template>
<script>
import TextField from '@/components/Common/TextField.vue'
import IconButton from '@/components/Common/IconButton.vue'
import time from '@/lib/utils/time'
function getRegExp(query, matchCase) {
try {
const regExp = new RegExp(query, !matchCase ? 'i' : '')
return regExp
} catch (e) {
console.error('Invalid regular expression:', e)
return null
}
}
export default {
components: {
TextField,
IconButton
},
props: ['graph', 'nodesMatchingSearchQuery', 'focusedFoundNodeId'],
data() {
return {
searchQuery: '',
currentFoundNodeIndex: -1,
useRegExp: false,
matchCase: false,
panelOpen: false
}
},
computed: {
regExp() {
return this.useRegExp
? getRegExp(this.searchQuery, this.matchCase)
: new RegExp(
this.searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
!this.matchCase ? 'i' : ''
)
},
regExpIsValid() {
return !this.searchQuery || this.regExp
},
arrowsDisabled() {
return (
!this.nodesMatchingSearchQuery ||
this.nodesMatchingSearchQuery.length === 0
)
},
shortResultCountString() {
const count = this.nodesMatchingSearchQuery?.length ?? 0
const formattedCount =
count > 1000
? new Intl.NumberFormat('en', {
notation: 'compact',
compactDisplay: 'short',
maximumSignificantDigits: 3
}).format(count)
: count
return `${formattedCount} ${count === 1 ? 'node' : 'nodes'} found`
}
},
watch: {
focusedFoundNodeId() {
if (!this.focusedFoundNodeId) {
this.currentFoundNodeIndex = -1
}
},
useRegExp() {
this.search()
},
matchCase() {
this.search()
}
},
methods: {
onSearchInput: time.debounce(function () {
this.search()
}, 500),
search() {
this.currentFoundNodeIndex = -1
if (this.regExpIsValid) {
const foundNodes = this.searchQuery ? this.getFoundNodes() : null
this.$emit('update:nodesMatchingSearchQuery', foundNodes)
} else {
this.$emit('update:nodesMatchingSearchQuery', null)
}
this.$emit('update:focusedFoundNodeId', null)
},
getFoundNodes() {
if (!this.graph) {
return null
}
return this.graph.filterNodes((nodeId, nodeData) => {
return this.regExp.test(nodeData.label)
})
},
toNextFoundNode(direction = 1) {
this.currentFoundNodeIndex =
(this.currentFoundNodeIndex +
direction +
this.nodesMatchingSearchQuery.length) %
this.nodesMatchingSearchQuery.length
const nodeId = this.nodesMatchingSearchQuery[this.currentFoundNodeIndex]
this.$emit('update:focusedFoundNodeId', nodeId)
}
}
}
</script>
<style scoped>
.graph-search-panel {
position: absolute;
background-color: var(--color-bg-light);
padding: 4px;
border-width: 0px 0px 1px 1px;
border-style: solid;
border-color: var(--color-border-light);
border-radius: 0 0 0 4px;
display: flex;
}
.graph-search-panel--search-controls {
display: flex;
align-items: center;
gap: 4px;
}
.next-match-btn :deep(i) {
transform: rotate(180deg);
}
.close-search-panel :deep(i) {
transform: scaleX(-1);
}
.query-input :deep(.text-field-container) {
height: 26px;
}
.counter {
min-width: 60px;
font-size: 12px;
color: var(--color-text-base);
}
.query-input :deep(input.error) {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.query-input :deep(.text-field-error) {
width: 250px;
border-width: 0px 1px 1px;
border-style: solid;
border-color: var(--color-text-error);
box-sizing: border-box;
background-color: var(--color-bg-danger);
margin-top: 0;
border-radius: 0 0 4px 4px;
color: var(--color-danger);
}
</style>
+192
View File
@@ -0,0 +1,192 @@
<template>
<Field label="Color" fieldContainerClassName="test_node_color">
<RadioBlocks
:options="nodeColorTypeOptions"
:activeOption="modelValue.type"
@option-change="updateColorType"
/>
<Field
v-if="modelValue.type === 'constant'"
fieldContainerClassName="test_node_color_value"
>
<ColorPicker
:selectedColor="modelValue.value"
@color-change="updateSettings('value', $event)"
/>
</Field>
<template v-else>
<Field fieldContainerClassName="test_node_color_value">
<Dropdown
v-if="modelValue.type === 'variable'"
:options="keyOptions"
:value="modelValue.source"
@change="updateSettings('source', $event)"
/>
<Dropdown
v-if="modelValue.type === 'calculated'"
:options="nodeCalculatedColorMethodOptions"
:value="modelValue.method"
:clearable="false"
@change="updateSettings('method', $event)"
/>
</Field>
<Field
v-if="modelValue.type === 'variable'"
fieldContainerClassName="test_node_color_mapping_mode"
>
<RadioBlocks
:options="colorSourceUsageOptions"
:activeOption="modelValue.sourceUsage"
@option-change="updateSettings('sourceUsage', $event)"
/>
</Field>
<Field
v-if="
modelValue.sourceUsage === 'map_to' ||
modelValue.type === 'calculated'
"
>
<ColorscalePicker
:selected="modelValue.colorscale"
className="colorscale-picker"
@colorscale-change="updateSettings('colorscale', $event)"
/>
</Field>
</template>
</Field>
<Field label="Opacity" fieldContainerClassName="test_node_opacity">
<NumericInput
:value="modelValue.opacity"
:showSlider="true"
:integerOnly="true"
:max="100"
:min="0"
units="%"
@update="updateSettings('opacity', $event)"
/>
</Field>
<Field
v-if="modelValue.type === 'map_to' || modelValue.type === 'calculated'"
label="Color as"
fieldContainerClassName="test_node_color_as"
>
<RadioBlocks
:options="сolorAsOptions"
:activeOption="modelValue.mode"
@option-change="updateSettings('mode', $event)"
/>
</Field>
<Field
v-if="
modelValue.sourceUsage === 'map_to' || modelValue.type === 'calculated'
"
label="Colorscale direction"
fieldContainerClassName="test_node_color_colorscale_direction"
>
<RadioBlocks
:options="сolorscaleDirections"
:activeOption="modelValue.colorscaleDirection"
@option-change="updateSettings('colorscaleDirection', $event)"
/>
</Field>
</template>
<script>
import { markRaw } from 'vue'
import { applyPureReactInVue } from 'veaury'
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
import ColorscalePicker from 'react-chart-editor/lib/components/widgets/ColorscalePicker'
import ColorPicker from 'react-chart-editor/lib/components/widgets/ColorPicker'
import Field from 'react-chart-editor/lib/components/fields/Field'
import 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
NumericInput: applyPureReactInVue(NumericInput),
Dropdown: applyPureReactInVue(Dropdown),
RadioBlocks: applyPureReactInVue(RadioBlocks),
Field: applyPureReactInVue(Field),
ColorscalePicker: applyPureReactInVue(ColorscalePicker),
ColorPicker: applyPureReactInVue(ColorPicker)
},
props: {
modelValue: Object,
keyOptions: Array
},
emits: ['update:modelValue'],
data() {
return {
nodeColorTypeOptions: markRaw([
{ label: 'Constant', value: 'constant' },
{ label: 'Variable', value: 'variable' },
{ label: 'Calculated', value: 'calculated' }
]),
nodeCalculatedColorMethodOptions: markRaw([
{ label: 'Degree', value: 'degree' },
{ label: 'In degree', value: 'inDegree' },
{ label: 'Out degree', value: 'outDegree' }
]),
сolorAsOptions: markRaw([
{ label: 'Continious', value: 'continious' },
{ label: 'Categorical', value: 'categorical' }
]),
сolorscaleDirections: markRaw([
{ label: 'Normal', value: 'normal' },
{ label: 'Reversed', value: 'reversed' }
]),
colorSourceUsageOptions: markRaw([
{ label: 'Direct', value: 'direct' },
{ label: 'Map to', value: 'map_to' }
]),
defaultColorSettings: {
constant: { value: '#1F77B4', opacity: 100 },
variable: {
source: null,
sourceUsage: 'map_to',
colorscale: null,
mode: 'categorical',
colorscaleDirection: 'normal',
opacity: 100
},
calculated: {
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'normal',
opacity: 100
}
}
}
},
methods: {
updateColorType(newColorType) {
const currentColorType = this.modelValue.type
this.defaultColorSettings[currentColorType] = this.modelValue
this.$emit('update:modelValue', {
type: newColorType,
...this.defaultColorSettings[newColorType]
})
},
updateSettings(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>
<style scoped>
:deep(.customPickerContainer) {
float: right;
}
</style>
+120
View File
@@ -0,0 +1,120 @@
<template>
<Field label="Size" fieldContainerClassName="test_node_size">
<RadioBlocks
:options="nodeSizeTypeOptions"
:activeOption="modelValue.type"
@option-change="updateSizeType"
/>
<Field fieldContainerClassName="test_node_size_value">
<NumericInput
v-if="modelValue.type === 'constant'"
:value="modelValue.value"
:min="1"
class="test_node_size_value"
@update="updateSettings('value', $event)"
/>
<Dropdown
v-if="modelValue.type === 'variable'"
:options="keyOptions"
:value="modelValue.source"
@change="updateSettings('source', $event)"
/>
<Dropdown
v-if="modelValue.type === 'calculated'"
:options="nodeCalculatedSizeMethodOptions"
:value="modelValue.method"
:clearable="false"
@change="updateSettings('method', $event)"
/>
</Field>
</Field>
<template v-if="modelValue.type !== 'constant'">
<Field label="Size scale" fieldContainerClassName="test_node_size_scale">
<NumericInput
:value="modelValue.scale"
@update="updateSettings('scale', $event)"
/>
</Field>
<Field label="Size mode" fieldContainerClassName="test_node_size_mode">
<RadioBlocks
:options="nodeSizeModeOptions"
:activeOption="modelValue.mode"
@option-change="updateSettings('mode', $event)"
/>
</Field>
<Field label="Minimum size" fieldContainerClassName="test_node_size_min">
<NumericInput
:value="modelValue.min"
@update="updateSettings('min', $event)"
/>
</Field>
</template>
</template>
<script>
import { markRaw } from 'vue'
import { applyPureReactInVue } from 'veaury'
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
import Field from 'react-chart-editor/lib/components/fields/Field'
import 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
Dropdown: applyPureReactInVue(Dropdown),
NumericInput: applyPureReactInVue(NumericInput),
RadioBlocks: applyPureReactInVue(RadioBlocks),
Field: applyPureReactInVue(Field)
},
props: {
modelValue: Object,
keyOptions: Array
},
emits: ['update:modelValue'],
data() {
return {
nodeSizeTypeOptions: markRaw([
{ label: 'Constant', value: 'constant' },
{ label: 'Variable', value: 'variable' },
{ label: 'Calculated', value: 'calculated' }
]),
nodeCalculatedSizeMethodOptions: markRaw([
{ label: 'Degree', value: 'degree' },
{ label: 'In degree', value: 'inDegree' },
{ label: 'Out degree', value: 'outDegree' }
]),
nodeSizeModeOptions: markRaw([
{ label: 'Area', value: 'area' },
{ label: 'Diameter', value: 'diameter' }
]),
defaultSizeSettings: {
constant: { value: 4 },
variable: { source: null, scale: 1, mode: 'diameter', min: 1 },
calculated: { method: 'degree', scale: 1, mode: 'diameter', min: 1 }
}
}
},
methods: {
updateSizeType(newSizeType) {
const currentSizeType = this.modelValue.type
this.defaultSizeSettings[currentSizeType] = this.modelValue
this.$emit('update:modelValue', {
type: newSizeType,
...this.defaultSizeSettings[newSizeType]
})
},
updateSettings(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>
@@ -0,0 +1,34 @@
<template>
<Field label="Seed value" fieldContainerClassName="test_seed_value">
<NumericInput
:value="modelValue.seedValue"
@update="update('seedValue', $event)"
/>
</Field>
</template>
<script>
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 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
Field: applyPureReactInVue(Field),
NumericInput: applyPureReactInVue(NumericInput)
},
props: {
modelValue: Object
},
emits: ['update:modelValue'],
methods: {
update(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>
+146
View File
@@ -0,0 +1,146 @@
<template>
<div ref="graphContainer" class="graph-container">
<div v-show="!dataSources" class="warning data-view-warning no-data">
There is no data to build a graph. Run your SQL query and make sure the
result is not empty.
</div>
<div
v-show="!dataSourceIsValid"
class="warning data-view-warning invalid-data"
>
Result set is invalid for graph visualisation. Learn more in
<a href="https://sqliteviz.com/docs/graph/" target="_blank">
documentation</a
>.
</div>
<splitpanes
:before="{ size: 70, max: 100 }"
:after="{ size: 30, max: 50, hidden: !showValueViewer }"
:default="{ before: 70, after: 30 }"
class="graph"
:style="{
height:
!dataSources || !dataSourceIsValid ? 'calc(100% - 40px)' : '100%'
}"
>
<template #left-pane>
<div ref="graphEditorContainer" :style="{ height: '100%' }">
<GraphEditor
ref="graphEditor"
:dataSources="dataSources"
:initOptions="initOptions"
:showViewSettings="showViewSettings"
@update="$emit('update')"
@select-item="selectedItem = $event"
@clear-selection="selectedItem = null"
/>
</div>
</template>
<template v-if="showValueViewer" #right-pane>
<value-viewer
:empty="!selectedItem"
emptyMessage="No node or edge selected to view"
:value="JSON.stringify(selectedItem)"
defaultFormat="json"
/>
</template>
</splitpanes>
</div>
</template>
<script>
import 'react-chart-editor/lib/react-chart-editor.css'
import GraphEditor from '@/components/Graph/GraphEditor.vue'
import { dataSourceIsValid } from '@/lib/graphHelper'
import ValueViewer from '@/components/ValueViewer.vue'
import Splitpanes from '@/components/Common/Splitpanes'
export default {
name: 'Graph',
components: { GraphEditor, ValueViewer, Splitpanes },
props: {
dataSources: Object,
initOptions: Object,
exportToPngEnabled: Boolean,
exportToSvgEnabled: Boolean,
exportToHtmlEnabled: Boolean,
showViewSettings: Boolean,
showValueViewer: Boolean
},
emits: [
'update:exportToSvgEnabled',
'update:exportToHtmlEnabled',
'update:exportToPngEnabled',
'update:exportToClipboardEnabled',
'update',
'loadingImageCompleted'
],
data() {
return {
resizeObserver: null,
selectedItem: null
}
},
computed: {
dataSourceIsValid() {
return !this.dataSources || dataSourceIsValid(this.dataSources)
}
},
watch: {
async showViewSettings() {
await this.$nextTick()
this.handleResize()
},
dataSources() {
this.$emit('update:exportToPngEnabled', !!this.dataSources)
this.$emit('update:exportToClipboardEnabled', !!this.dataSources)
}
},
created() {
this.$emit('update:exportToSvgEnabled', false)
this.$emit('update:exportToHtmlEnabled', false)
this.$emit('update:exportToPngEnabled', !!this.dataSources)
this.$emit('update:exportToClipboardEnabled', !!this.dataSources)
},
mounted() {
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.graphEditorContainer)
},
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.graphEditorContainer)
},
methods: {
getOptionsForSave() {
return this.$refs.graphEditor.settings
},
async saveAsPng() {
await this.$refs.graphEditor.saveAsPng()
this.$emit('loadingImageCompleted')
},
prepareCopy() {
return this.$refs.graphEditor.prepareCopy()
},
async handleResize() {
const renderer = this.$refs.graphEditor?.renderer
if (renderer) {
renderer.refresh()
renderer.getCamera().animatedReset({ duration: 600 })
}
}
}
}
</script>
<style scoped>
.graph-container {
height: 100%;
}
.graph {
min-height: 242px;
}
:deep(.editor_controls .sidebar__item:before) {
width: 0;
}
</style>
@@ -10,14 +10,22 @@
</div>
<div id="nav-buttons">
<button
v-show="currentInquiry && $route.path === '/workspace'"
v-show="currentInquiryTab && $route.path === '/workspace'"
id="save-btn"
class="primary"
:disabled="isSaved"
@click="checkInquiryBeforeSave"
@click="onSave(false)"
>
Save
</button>
<button
v-show="currentInquiryTab && $route.path === '/workspace'"
id="save-as-btn"
class="primary"
@click="onSaveAs"
>
Save as
</button>
<button id="create-btn" class="primary" @click="createNewInquiry">
Create
</button>
@@ -28,7 +36,12 @@
<modal modalId="save" class="dialog" contentStyle="width: 560px;">
<div class="dialog-header">
Save inquiry
<close-icon @click="cancelSave" />
<IconButton
icon="myicon myicon-close"
variant="secondary"
:iconSize="14"
@click="cancelSave"
/>
</div>
<div class="dialog-body">
<div v-show="isPredefined" id="save-note">
@@ -45,15 +58,47 @@
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="cancelSave">Cancel</button>
<button class="primary" @click="saveInquiry">Save</button>
<button class="primary" @click="validateSaveFormAndSaveInquiry">
Save
</button>
</div>
</modal>
<!-- Inquiery saving conflict dialog -->
<modal
modalId="inquiry-conflict"
class="dialog"
contentStyle="width: 560px;"
>
<div class="dialog-header">
Inquiry saving conflict
<IconButton
icon="myicon myicon-close"
variant="secondary"
:iconSize="14"
@click="cancelSave"
/>
</div>
<div class="dialog-body">
<div id="save-note">
<img src="~@/assets/images/info.svg" />
This inquiry has been modified in the mean time. This can happen if an
inquiry is saved in another window or browser tab. Do you want to
overwrite that changes or save the current state as a new inquiry?
</div>
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="cancelSave">Cancel</button>
<button class="primary" @click="onSave(true)">Overwrite</button>
<button class="primary" @click="onSaveAs">Save as new</button>
</div>
</modal>
</nav>
</template>
<script>
import TextField from '@/components/TextField'
import CloseIcon from '@/components/svg/close'
import TextField from '@/components/Common/TextField'
import IconButton from '@/components/Common/IconButton.vue'
import storedInquiries from '@/lib/storedInquiries'
import AppDiagnosticInfo from './AppDiagnosticInfo'
import events from '@/lib/utils/events'
@@ -63,7 +108,7 @@ export default {
name: 'MainMenu',
components: {
TextField,
CloseIcon,
IconButton,
AppDiagnosticInfo
},
data() {
@@ -73,25 +118,28 @@ export default {
}
},
computed: {
currentInquiry() {
inquiries() {
return this.$store.state.inquiries
},
currentInquiryTab() {
return this.$store.state.currentTab
},
isSaved() {
return this.currentInquiry && this.currentInquiry.isSaved
return this.currentInquiryTab && this.currentInquiryTab.isSaved
},
isPredefined() {
return this.currentInquiry && this.currentInquiry.isPredefined
return this.currentInquiryTab && this.currentInquiryTab.isPredefined
},
runDisabled() {
return (
this.currentInquiry &&
(!this.$store.state.db || !this.currentInquiry.query)
this.currentInquiryTab &&
(!this.$store.state.db || !this.currentInquiryTab.query)
)
}
},
created() {
eventBus.$on('createNewInquiry', this.createNewInquiry)
eventBus.$on('saveInquiry', this.checkInquiryBeforeSave)
eventBus.$on('saveInquiry', this.onSave)
document.addEventListener('keydown', this._keyListener)
},
beforeUnmount() {
@@ -109,83 +157,110 @@ export default {
events.send('inquiry.create', null, { auto: false })
},
cancelSave() {
this.$modal.hide('save')
eventBus.$off('inquirySaved')
},
checkInquiryBeforeSave() {
this.errorMsg = null
this.name = ''
if (storedInquiries.isTabNeedName(this.currentInquiry)) {
this.$modal.show('save')
} else {
this.saveInquiry()
}
this.$modal.hide('save')
this.$modal.hide('inquiry-conflict')
eventBus.$off('inquirySaved')
},
async saveInquiry() {
const isNeedName = storedInquiries.isTabNeedName(this.currentInquiry)
if (isNeedName && !this.name) {
onSave(skipConcurrentEditingCheck = false) {
if (storedInquiries.isTabNeedName(this.currentInquiryTab)) {
this.openSaveModal()
return
}
if (!skipConcurrentEditingCheck) {
const inquiryInStore = this.inquiries.find(
inquiry => inquiry.id === this.currentInquiryTab.id
)
if (
inquiryInStore &&
inquiryInStore.updatedAt !== this.currentInquiryTab.updatedAt
) {
this.$modal.show('inquiry-conflict')
return
}
}
this.saveInquiry()
},
onSaveAs() {
this.openSaveModal()
},
openSaveModal() {
this.$modal.hide('inquiry-conflict')
this.errorMsg = null
this.name = ''
this.$modal.show('save')
},
validateSaveFormAndSaveInquiry() {
if (!this.name) {
this.errorMsg = "Inquiry name can't be empty"
return
}
const dataSet = this.currentInquiry.result
const tabView = this.currentInquiry.view
this.saveInquiry()
},
async saveInquiry() {
const eventName =
this.currentInquiryTab.name && this.name
? 'inquiry.saveAs'
: 'inquiry.save'
// Save inquiry
const value = await this.$store.dispatch('saveInquiry', {
inquiryTab: this.currentInquiry,
inquiryTab: this.currentInquiryTab,
newName: this.name
})
// Update tab in store
this.$store.commit('updateTab', {
tab: this.currentInquiry,
tab: this.currentInquiryTab,
newValues: {
name: value.name,
id: value.id,
query: value.query,
viewType: value.viewType,
viewOptions: value.viewOptions,
isSaved: true
isSaved: true,
updatedAt: value.updatedAt
}
})
// Restore data:
// e.g. if we save predefined inquiry the tab will be created again
// (because of new id) and
// it will be without sql result and has default view - table.
// That's why we need to restore data and view
this.$nextTick(() => {
this.currentInquiry.result = dataSet
this.currentInquiry.view = tabView
})
// Hide dialog
// Hide dialogs
this.$modal.hide('save')
this.$modal.hide('inquiry-conflict')
this.errorMsg = null
this.name = ''
// Signal about saving
eventBus.$emit('inquirySaved')
events.send('inquiry.save')
events.send(eventName)
},
_keyListener(e) {
if (this.$route.path === '/workspace') {
if (this.$route.path === '/workspace' && this.currentInquiryTab) {
// Run query Ctrl+R or Ctrl+Enter
if ((e.key === 'r' || e.key === 'Enter') && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
if (!this.runDisabled) {
this.currentInquiry.execute()
this.currentInquiryTab.execute()
}
return
}
// Save inquiry Ctrl+S
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
if (e.key === 's' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
e.preventDefault()
if (!this.isSaved) {
this.checkInquiryBeforeSave()
this.onSave()
}
return
}
// Save inquiry as Ctrl+Shift+S
if (e.key === 'S' && (e.ctrlKey || e.metaKey) && e.shiftKey) {
e.preventDefault()
this.onSaveAs()
return
}
}
// New (blank) inquiry Ctrl+B
if (e.key === 'b' && (e.ctrlKey || e.metaKey)) {
@@ -1,22 +1,19 @@
<template>
<div :class="['pivot-sort-btn', direction]" @click="changeSorting">
{{ modelValue.includes('key') ? 'key' : 'value' }}
<sort-icon
class="sort-icon"
:horizontal="direction === 'col'"
:asc="modelValue.includes('a_to_z')"
<i
:class="[
'myicon myicon-sort sort-icon',
{ vertical: direction === 'row' },
{ asc: modelValue.includes('a_to_z') }
]"
/>
</div>
</template>
<script>
import SortIcon from '@/components/svg/sort'
export default {
name: 'PivotSortBtn',
components: {
SortIcon
},
props: {
direction: String,
modelValue: String
@@ -56,9 +53,6 @@ export default {
color: var(--color-text-active);
border-color: var(--color-border-dark);
}
.pivot-sort-btn:hover :deep(.sort-icon path) {
fill: var(--color-text-active);
}
.pivot-sort-btn.col {
flex-direction: column;
@@ -69,7 +63,19 @@ export default {
flex-direction: row;
}
.pivot-sort-btn.row .sort-icon {
margin-left: 2px;
.sort-icon {
font-size: 12px;
}
.sort-icon.asc {
transform: rotate(180deg);
}
.sort-icon.vertical {
transform: rotate(90deg);
margin-right: -3px;
margin-left: -1px;
}
.sort-icon.vertical.asc {
transform: rotate(-90deg);
}
</style>
+300
View File
@@ -0,0 +1,300 @@
<template>
<div class="pivot-ui">
<div class="row">
<label>Columns</label>
<multiselect
v-model="cols"
class="sqliteviz-select cols"
:options="colsToSelect"
:disabled="colsToSelect.length === 0"
:multiple="true"
:hideSelected="true"
:closeOnSelect="true"
:showLabels="false"
:max="colsToSelect.length"
openDirection="bottom"
placeholder=""
>
<template #maxElements>
<span class="no-results">No Results</span>
</template>
<template #placeholder>Choose columns</template>
<template #noResult>
<span class="no-results">No Results</span>
</template>
</multiselect>
<pivot-sort-btn v-model="colOrder" class="sort-btn" direction="col" />
</div>
<div class="row">
<label>Rows</label>
<multiselect
v-model="rows"
class="sqliteviz-select rows"
:options="rowsToSelect"
:disabled="rowsToSelect.length === 0"
:multiple="true"
:hideSelected="true"
:closeOnSelect="true"
:showLabels="false"
:max="rowsToSelect.length"
:optionHeight="29"
openDirection="bottom"
placeholder=""
>
<template #maxElements>
<span class="no-results">No Results</span>
</template>
<template #placeholder>Choose rows</template>
<template #noResult>
<span class="no-results">No Results</span>
</template>
</multiselect>
<pivot-sort-btn v-model="rowOrder" class="sort-btn" direction="row" />
</div>
<div class="row aggregator">
<label>Aggregator</label>
<multiselect
v-model="aggregator"
class="sqliteviz-select short aggregator"
:options="aggregators"
label="name"
trackBy="name"
:closeOnSelect="true"
:showLabels="false"
:hideSelected="true"
:optionHeight="29"
openDirection="bottom"
placeholder="Choose a function"
>
<template #noResult>
<span class="no-results">No Results</span>
</template>
</multiselect>
<multiselect
v-show="valCount > 0"
v-model="val1"
class="sqliteviz-select aggr-arg"
:options="keyNames"
:disabled="keyNames.length === 0"
:closeOnSelect="true"
:showLabels="false"
:hideSelected="true"
:optionHeight="29"
openDirection="bottom"
placeholder="Choose an argument"
/>
<multiselect
v-show="valCount > 1"
v-model="val2"
class="sqliteviz-select aggr-arg"
:options="keyNames"
:disabled="keyNames.length === 0"
:closeOnSelect="true"
:showLabels="false"
:hideSelected="true"
:optionHeight="29"
openDirection="bottom"
placeholder="Choose a second argument"
/>
</div>
<div class="row">
<label>View</label>
<multiselect
v-model="renderer"
class="sqliteviz-select short renderer"
:options="renderers"
label="name"
trackBy="name"
:closeOnSelect="true"
:allowEmpty="false"
:showLabels="false"
:hideSelected="true"
:optionHeight="29"
openDirection="bottom"
placeholder="Choose a view"
>
<template #noResult>
<span class="no-results">No Results</span>
</template>
</multiselect>
</div>
</div>
</template>
<script>
import $ from 'jquery'
import Multiselect from 'vue-multiselect'
import PivotSortBtn from './PivotSortBtn'
import {
renderers,
aggregators,
zeroValAggregators,
twoValAggregators
} from '../pivotHelper'
export default {
name: 'PivotUi',
components: {
Multiselect,
PivotSortBtn
},
props: {
keyNames: Array,
modelValue: Object
},
emits: ['update:modelValue', 'update'],
data() {
const aggregatorName =
(this.modelValue && this.modelValue.aggregatorName) || 'Count'
const rendererName =
(this.modelValue && this.modelValue.rendererName) || 'Table'
return {
renderer: {
name: rendererName,
fun: $.pivotUtilities.renderers[rendererName]
},
aggregator: {
name: aggregatorName,
fun: $.pivotUtilities.aggregators[aggregatorName]
},
rows: (this.modelValue && this.modelValue.rows) || [],
cols: (this.modelValue && this.modelValue.cols) || [],
val1:
(this.modelValue && this.modelValue.vals && this.modelValue.vals[0]) ||
'',
val2:
(this.modelValue && this.modelValue.vals && this.modelValue.vals[1]) ||
'',
colOrder: (this.modelValue && this.modelValue.colOrder) || 'key_a_to_z',
rowOrder: (this.modelValue && this.modelValue.rowOrder) || 'key_a_to_z'
}
},
computed: {
valCount() {
if (zeroValAggregators.includes(this.aggregator.name)) {
return 0
}
if (twoValAggregators.includes(this.aggregator.name)) {
return 2
}
return 1
},
renderers() {
return renderers
},
aggregators() {
return aggregators
},
rowsToSelect() {
return this.keyNames.filter(key => !this.cols.includes(key))
},
colsToSelect() {
return this.keyNames.filter(key => !this.rows.includes(key))
}
},
watch: {
renderer() {
this.returnValue()
},
aggregator() {
this.returnValue()
},
rows() {
this.returnValue()
},
cols() {
this.returnValue()
},
val1() {
this.returnValue()
},
val2() {
this.returnValue()
},
colOrder() {
this.returnValue()
},
rowOrder() {
this.returnValue()
}
},
methods: {
returnValue() {
const vals = []
for (let i = 1; i <= this.valCount; i++) {
vals.push(this[`val${i}`])
}
this.$emit('update')
this.$emit('update:modelValue', {
rows: this.rows,
cols: this.cols,
colOrder: this.colOrder,
rowOrder: this.rowOrder,
aggregator: this.aggregator.fun(vals),
aggregatorName: this.aggregator.name,
renderer: this.renderer.fun,
rendererName: this.renderer.name,
vals
})
}
}
}
</script>
<style scoped>
.pivot-ui {
padding: 12px 24px;
color: var(--color-text-base);
font-size: 12px;
border-bottom: 1px solid var(--color-border-light);
background-color: var(--color-bg-light);
}
.pivot-ui .row {
display: flex;
align-items: center;
margin: 12px 0;
}
.pivot-ui .row label {
width: 76px;
flex-shrink: 0;
}
.pivot-ui .row .sqliteviz-select.short {
width: 220px;
flex-shrink: 0;
}
.pivot-ui .row .aggr-arg {
margin-left: 12px;
max-width: 220px;
}
.pivot-ui .row .sort-btn {
margin-left: 12px;
flex-shrink: 0;
}
.switcher {
display: block;
width: min-content;
white-space: nowrap;
margin: auto;
cursor: pointer;
}
.switcher:hover {
color: var(--color-accent);
}
</style>
@@ -5,6 +5,7 @@
result is not empty.
</div>
<pivot-ui
v-show="showViewSettings"
v-model="pivotOptions"
:keyNames="columns"
@update="$emit('update')"
@@ -18,6 +19,7 @@
<chart
ref="customChart"
v-bind="customChartComponentProps"
:showViewSettings="showViewSettings"
@update="$emit('update')"
@loading-image-completed="$emit('loadingImageCompleted')"
/>
@@ -30,9 +32,9 @@ import fIo from '@/lib/utils/fileIo'
import $ from 'jquery'
import 'pivottable'
import 'pivottable/dist/pivot.css'
import PivotUi from './PivotUi'
import PivotUi from './PivotUi/index.vue'
import pivotHelper from './pivotHelper'
import Chart from '@/views/MainView/Workspace/Tabs/Tab/DataView/Chart'
import Chart from '@/components/Chart'
import chartHelper from '@/lib/chartHelper'
import events from '@/lib/utils/events'
import plotly from 'plotly.js'
@@ -46,14 +48,16 @@ export default {
props: {
dataSources: Object,
initOptions: Object,
importToPngEnabled: Boolean,
importToSvgEnabled: Boolean
exportToPngEnabled: Boolean,
exportToSvgEnabled: Boolean,
showViewSettings: Boolean
},
emits: [
'loadingImageCompleted',
'update',
'update:importToSvgEnabled',
'update:importToPngEnabled'
'update:exportToSvgEnabled',
'update:exportToPngEnabled',
'update:exportToHtmlEnabled'
],
data() {
return {
@@ -110,11 +114,11 @@ export default {
immediate: true,
handler() {
this.$emit(
'update:importToPngEnabled',
'update:exportToPngEnabled',
this.pivotOptions.rendererName !== 'TSV Export'
)
this.$emit(
'update:importToSvgEnabled',
'update:exportToSvgEnabled',
this.viewStandartChart || this.viewCustomChart
)
events.send('viz_pivot.render', null, {
@@ -124,8 +128,14 @@ export default {
},
pivotOptions() {
this.show()
},
showViewSettings() {
this.handleResize()
}
},
created() {
this.$emit('update:exportToHtmlEnabled', true)
},
mounted() {
this.show()
// We need to detect resizing because plotly doesn't resize when resize its container
@@ -218,12 +228,12 @@ export default {
async prepareCopy() {
if (this.viewCustomChart) {
return await this.$refs.customChart.prepareCopy()
return this.$refs.customChart.prepareCopy()
}
if (this.viewStandartChart) {
return await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
return chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
}
return await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
return pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
},
async saveAsSvg() {
@@ -317,4 +327,8 @@ export default {
.pivot-output:empty {
flex-grow: 0;
}
:deep(.js-plotly-plot) {
height: 100%;
}
</style>
@@ -1,54 +1,50 @@
<template>
<div class="record-navigator">
<icon-button
icon="myicon myicon-edge-arrow"
iconColor="var(--color-text-base)"
:disabled="modelValue === 0"
tooltip="First row"
tooltipPosition="top-left"
class="first"
@click="$emit('update:modelValue', 0)"
>
<edge-arrow-icon :disabled="false" />
</icon-button>
/>
<icon-button
icon="myicon myicon-arrow"
iconColor="var(--color-text-base)"
:disabled="modelValue === 0"
tooltip="Previous row"
tooltipPosition="top-left"
class="prev"
@click="$emit('update:modelValue', modelValue - 1)"
>
<arrow-icon :disabled="false" />
</icon-button>
/>
<icon-button
icon="myicon myicon-arrow"
iconColor="var(--color-text-base)"
:disabled="modelValue === total - 1"
tooltip="Next row"
tooltipPosition="top-left"
class="next"
@click="$emit('update:modelValue', modelValue + 1)"
>
<arrow-icon :disabled="false" />
</icon-button>
/>
<icon-button
icon="myicon myicon-edge-arrow"
iconColor="var(--color-text-base)"
:disabled="modelValue === total - 1"
tooltip="Last row"
tooltipPosition="top-left"
class="last"
@click="$emit('update:modelValue', total - 1)"
>
<edge-arrow-icon :disabled="false" />
</icon-button>
/>
</div>
</template>
<script>
import IconButton from '@/components/IconButton'
import ArrowIcon from '@/components/svg/arrow'
import EdgeArrowIcon from '@/components/svg/edgeArrow'
import IconButton from '@/components/Common/IconButton'
export default {
components: {
IconButton,
ArrowIcon,
EdgeArrowIcon
IconButton
},
props: {
modelValue: Number,
@@ -63,8 +59,8 @@ export default {
display: flex;
}
.record-navigator .next,
.record-navigator .last {
.record-navigator .next :deep(i),
.record-navigator .last :deep(i) {
transform: rotate(180deg);
}
</style>
@@ -1,157 +1,133 @@
<template>
<div ref="runResultPanel" class="run-result-panel">
<component
:is="viewValuePanelVisible ? 'splitpanes' : 'div'"
<splitpanes
:before="{ size: 50, max: 100 }"
:after="{ size: 50, max: 100 }"
:after="{ size: 50, max: 100, hidden: !viewValuePanelVisible }"
:default="{ before: 50, after: 50 }"
class="run-result-panel-content"
>
<template #left-pane>
<div
:id="'run-result-left-pane-' + tab.id"
class="result-set-container"
/>
</template>
<div
:id="'run-result-result-set-' + tab.id"
class="result-set-container"
/>
<template v-if="viewValuePanelVisible" #right-pane>
<div class="value-viewer-container">
<value-viewer
v-show="selectedCell"
:cellValue="
selectedCell
? result.values[result.columns[selectedCell.dataset.col]][
selectedCell.dataset.row
]
: ''
"
/>
<div v-show="!selectedCell" class="table-preview">
No cell selected to view
<div class="result-set-container">
<div
v-show="result === null && !isGettingResults && !error"
class="table-preview result-before"
>
Run your query and get results here
</div>
<div v-if="isGettingResults" class="table-preview result-in-progress">
<loading-indicator :size="30" />
Fetching results...
</div>
<div
v-show="result === undefined && !isGettingResults && !error"
class="table-preview result-empty"
>
No rows retrieved according to your query
</div>
<logs v-if="error" :messages="[error]" />
<sql-table
v-if="result && !viewRecord"
:data-set="result"
:time="time"
:pageSize="pageSize"
:page="defaultPage"
:selectedCellCoordinates="defaultSelectedCell"
class="straight"
@update-selected-cell="onUpdateSelectedCell"
/>
<record
v-if="result && viewRecord"
ref="recordView"
:data-set="result"
:time="time"
:selectedColumnIndex="selectedCell ? +selectedCell.dataset.col : 0"
:rowIndex="selectedCell ? +selectedCell.dataset.row : 0"
@update-selected-cell="onUpdateSelectedCell"
/>
</div>
</template>
</component>
<template v-if="viewValuePanelVisible" #right-pane>
<value-viewer
:empty="!selectedCell"
emptyMessage="No cell selected to view"
:value="
selectedCell
? result.values[result.columns[selectedCell.dataset.col]][
selectedCell.dataset.row
]
: ''
"
/>
</template>
</splitpanes>
<side-tool-bar panel="table" @switch-to="$emit('switchTo', $event)">
<icon-button
icon="myicon myicon-export-to-csv"
:disabled="!result"
tooltip="Export result set to CSV file"
tooltipPosition="top-left"
@click="exportToCsv"
>
<export-to-csv-icon />
</icon-button>
/>
<icon-button
ref="copyToClipboardBtn"
icon="myicon myicon-clipboard"
:disabled="!result"
tooltip="Copy result set to clipboard"
tooltipPosition="top-left"
@click="prepareCopy"
>
<clipboard-icon />
</icon-button>
/>
<icon-button
ref="rowBtn"
icon="myicon myicon-row"
:disabled="!result"
tooltip="View record"
tooltipPosition="top-left"
:active="viewRecord"
@click="toggleViewRecord"
>
<row-icon />
</icon-button>
/>
<icon-button
ref="viewCellValueBtn"
icon="myicon myicon-view-cell-value"
:disabled="!result"
tooltip="View value"
tooltipPosition="top-left"
:active="viewValuePanelVisible"
@click="toggleViewValuePanel"
>
<view-cell-value-icon />
</icon-button>
/>
</side-tool-bar>
<loading-dialog
v-model="showLoadingDialog"
loadingMsg="Building CSV..."
successMsg="CSV is ready"
actionBtnName="Copy"
name="prepareCSVCopy"
title="Copy to clipboard"
:loading="preparingCopy"
@action="copyToClipboard"
@cancel="cancelCopy"
/>
<teleport defer :to="resultSetTeleportTarget" :disabled="!enableTeleport">
<div>
<div
v-show="result === null && !isGettingResults && !error"
class="table-preview result-before"
>
Run your query and get results here
</div>
<div v-if="isGettingResults" class="table-preview result-in-progress">
<loading-indicator :size="30" />
Fetching results...
</div>
<div
v-show="result === undefined && !isGettingResults && !error"
class="table-preview result-empty"
>
No rows retrieved according to your query
</div>
<logs v-if="error" :messages="[error]" />
<sql-table
v-if="result && !viewRecord"
:data-set="result"
:time="time"
:pageSize="pageSize"
:page="defaultPage"
:selectedCellCoordinates="defaultSelectedCell"
class="straight"
@update-selected-cell="onUpdateSelectedCell"
/>
<record
v-if="result && viewRecord"
ref="recordView"
:data-set="result"
:time="time"
:selectedColumnIndex="selectedCell ? +selectedCell.dataset.col : 0"
:rowIndex="selectedCell ? +selectedCell.dataset.row : 0"
@update-selected-cell="onUpdateSelectedCell"
/>
</div>
</teleport>
</div>
</template>
<script>
import Logs from '@/components/Logs'
import SqlTable from '@/components/SqlTable/index.vue'
import LoadingIndicator from '@/components/LoadingIndicator'
import SideToolBar from '../SideToolBar'
import Splitpanes from '@/components/Splitpanes'
import ExportToCsvIcon from '@/components/svg/exportToCsv'
import ClipboardIcon from '@/components/svg/clipboard'
import ViewCellValueIcon from '@/components/svg/viewCellValue'
import RowIcon from '@/components/svg/row'
import IconButton from '@/components/IconButton'
import Logs from '@/components/Common/Logs'
import SqlTable from '@/components/SqlTable'
import LoadingIndicator from '@/components/Common/LoadingIndicator'
import SideToolBar from '@/components/SideToolBar'
import Splitpanes from '@/components/Common/Splitpanes'
import IconButton from '@/components/Common/IconButton'
import csv from '@/lib/csv'
import fIo from '@/lib/utils/fileIo'
import cIo from '@/lib/utils/clipboardIo'
import time from '@/lib/utils/time'
import loadingDialog from '@/components/LoadingDialog'
import loadingDialog from '@/components/Common/LoadingDialog'
import events from '@/lib/utils/events'
import ValueViewer from './ValueViewer'
import ValueViewer from '@/components/ValueViewer'
import Record from './Record/index.vue'
export default {
@@ -161,18 +137,13 @@ export default {
LoadingIndicator,
Logs,
SideToolBar,
ExportToCsvIcon,
IconButton,
ClipboardIcon,
ViewCellValueIcon,
RowIcon,
loadingDialog,
ValueViewer,
Record,
Splitpanes
},
props: {
tab: Object,
result: Object,
isGettingResults: Boolean,
error: Object,
@@ -190,21 +161,8 @@ export default {
viewRecord: false,
defaultPage: 1,
defaultSelectedCell: null,
enableTeleport: this.$store.state.isWorkspaceVisible
}
},
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
enableTeleport: this.$store.state.isWorkspaceVisible,
showLoadingDialog: false
}
},
watch: {
@@ -264,14 +222,13 @@ export default {
if ('ClipboardItem' in window) {
this.preparingCopy = true
this.$modal.show('prepareCSVCopy')
this.showLoadingDialog = true
const t0 = performance.now()
await time.sleep(0)
this.dataToCopy = csv.serialize(this.result)
const t1 = performance.now()
if (t1 - t0 < 950) {
this.$modal.hide('prepareCSVCopy')
this.copyToClipboard()
} else {
this.preparingCopy = false
@@ -287,12 +244,11 @@ export default {
copyToClipboard() {
cIo.copyText(this.dataToCopy, 'CSV copied to clipboard successfully')
this.$modal.hide('prepareCSVCopy')
this.showLoadingDialog = false
},
cancelCopy() {
this.dataToCopy = null
this.$modal.hide('prepareCSVCopy')
},
toggleViewValuePanel() {
@@ -333,19 +289,12 @@ export default {
width: 0;
}
.result-set-container,
.result-set-container > div {
.result-set-container {
position: relative;
height: 100%;
width: 100%;
box-sizing: border-box;
}
.value-viewer-container {
height: 100%;
width: 100%;
background-color: var(--color-white);
position: relative;
}
.table-preview {
position: absolute;
@@ -1,7 +1,7 @@
<template>
<div>
<div class="table-name" @click="colVisible = !colVisible">
<tree-chevron :expanded="colVisible" />
<i :class="['myicon myicon-tree-chevron', { expanded: colVisible }]" />
{{ name }}
</div>
<div v-show="colVisible" class="columns">
@@ -14,11 +14,8 @@
</template>
<script>
import TreeChevron from '@/components/svg/treeChevron'
export default {
name: 'TableDescription',
components: { TreeChevron },
props: {
name: String,
columns: Array
@@ -5,12 +5,25 @@
</div>
<div id="db">
<div class="db-name" @click="schemaVisible = !schemaVisible">
<tree-chevron v-show="schema.length > 0" :expanded="schemaVisible" />
<i
v-show="schema.length > 0"
:class="['myicon myicon-tree-chevron', { expanded: schemaVisible }]"
/>
{{ dbName }}
</div>
<db-uploader id="db-edit" type="small" />
<export-icon tooltip="Export database" @click="exportToFile" />
<add-table-icon @click="addCsvJson" />
<IconButton
icon="myicon myicon-export"
tooltip="Export database"
data-test="export-db-btn"
@click="exportToFile"
/>
<IconButton
icon="myicon myicon-add-table"
tooltip=" Add new table from CSV, JSON or NDJSON"
data-test="add-csv-json-btn"
@click="addCsvJson"
/>
</div>
<div v-show="schemaVisible" class="schema">
<table-description
@@ -35,22 +48,18 @@
import fIo from '@/lib/utils/fileIo'
import events from '@/lib/utils/events'
import TableDescription from './TableDescription'
import TextField from '@/components/TextField'
import TreeChevron from '@/components/svg/treeChevron'
import TextField from '@/components/Common/TextField'
import DbUploader from '@/components/DbUploader'
import ExportIcon from '@/components/svg/export'
import AddTableIcon from '@/components/svg/addTable'
import CsvJsonImport from '@/components/CsvJsonImport'
import IconButton from '@/components/Common/IconButton'
export default {
name: 'Schema',
components: {
TableDescription,
TextField,
TreeChevron,
DbUploader,
ExportIcon,
AddTableIcon,
IconButton,
CsvJsonImport
},
data() {
@@ -142,8 +151,20 @@ export default {
flex-shrink: 0;
}
.db-name:hover .chevron-icon path,
:deep(.table-name:hover .chevron-icon path) {
fill: var(--color-gray-dark);
.db-name:hover .myicon-tree-chevron,
:deep(.table-name:hover .myicon-tree-chevron) {
color: var(--color-gray-dark);
}
:deep(.myicon-tree-chevron) {
color: var(--color-border-dark);
font-size: 9px;
transition: transform 0.15s ease-in-out;
}
:deep(.myicon-tree-chevron.expanded) {
color: var(--color-border-dark);
transform: rotate(90deg);
color: var(--color-gray-dark);
}
</style>
@@ -2,33 +2,30 @@
<div class="side-tool-bar">
<icon-button
ref="sqlEditorBtn"
icon="myicon myicon-sql-editor"
:active="panel === 'sqlEditor'"
tooltip="Switch panel to SQL editor"
tooltipPosition="top-left"
@click="$emit('switchTo', 'sqlEditor')"
>
<sql-editor-icon />
</icon-button>
/>
<icon-button
ref="tableBtn"
icon="myicon myicon-table"
:active="panel === 'table'"
tooltip="Switch panel to result set"
tooltipPosition="top-left"
@click="$emit('switchTo', 'table')"
>
<table-icon />
</icon-button>
/>
<icon-button
ref="dataViewBtn"
icon="myicon myicon-data-view"
:active="panel === 'dataView'"
tooltip="Switch panel to data view"
tooltipPosition="top-left"
@click="$emit('switchTo', 'dataView')"
>
<data-view-icon />
</icon-button>
/>
<div v-if="$slots.default" class="side-tool-bar-divider" />
@@ -37,18 +34,12 @@
</template>
<script>
import IconButton from '@/components/IconButton'
import TableIcon from '@/components/svg/table'
import SqlEditorIcon from '@/components/svg/sqlEditor'
import DataViewIcon from '@/components/svg/dataView'
import IconButton from '@/components/Common/IconButton'
export default {
name: 'SideToolBar',
components: {
IconButton,
SqlEditorIcon,
DataViewIcon,
TableIcon
IconButton
},
props: {
panel: String
@@ -63,7 +54,9 @@ export default {
border-left: 1px solid var(--color-border-light);
padding: 6px;
}
</style>
<style>
.side-tool-bar-divider {
width: 26px;
height: 1px;
@@ -12,14 +12,13 @@
<side-tool-bar panel="sqlEditor" @switch-to="$emit('switchTo', $event)">
<icon-button
ref="runBtn"
icon="myicon myicon-run"
:disabled="runDisabled"
:loading="isGettingResults"
tooltip="Run SQL query"
tooltipPosition="top-left"
@click="$emit('run')"
>
<run-icon :disabled="runDisabled" />
</icon-button>
/>
</side-tool-bar>
</div>
</template>
@@ -34,16 +33,14 @@ import 'codemirror/theme/neo.css'
import 'codemirror/addon/hint/show-hint.css'
import 'codemirror/addon/display/autorefresh.js'
import SideToolBar from '../SideToolBar'
import IconButton from '@/components/IconButton'
import RunIcon from '@/components/svg/run'
import IconButton from '@/components/Common/IconButton.vue'
export default {
name: 'SqlEditor',
components: {
Codemirror,
SideToolBar,
IconButton,
RunIcon
IconButton
},
props: { modelValue: String, isGettingResults: Boolean },
emits: ['update:modelValue', 'run', 'switchTo'],
@@ -69,7 +69,7 @@
</template>
<script>
import Pager from './Pager.vue'
import Pager from '@/components/Common/Pager.vue'
export default {
name: 'SqlTable',
@@ -37,7 +37,6 @@
:disabled="!enableTeleport"
>
<run-result
:tab="tab"
:result="tab.result"
:isGettingResults="tab.isGettingResults"
:error="tab.error"
@@ -64,11 +63,11 @@
</template>
<script>
import Splitpanes from '@/components/Splitpanes'
import SqlEditor from './SqlEditor'
import DataView from './DataView'
import RunResult from './RunResult'
import { nextTick } from 'vue'
import Splitpanes from '@/components/Common/Splitpanes'
import SqlEditor from '@/components/SqlEditor'
import DataView from '@/components/DataView'
import RunResult from '@/components/RunResult'
import { nextTick, computed } from 'vue'
import events from '@/lib/utils/events'
@@ -80,6 +79,11 @@ export default {
RunResult,
Splitpanes
},
provide() {
return {
tabLayout: computed(() => this.tab.layout)
}
},
props: {
tab: Object
},
@@ -12,13 +12,14 @@
<span v-if="tab.name">{{ tab.name }}</span>
<span v-else class="tab-untitled">{{ tab.tempName }}</span>
</div>
<div>
<close-icon
class="close-icon"
:size="10"
@click="beforeCloseTab(tab)"
/>
</div>
<IconButton
icon="myicon myicon-close"
variant="secondary"
:iconSize="10"
class="close-icon"
@click="beforeCloseTab(tab)"
/>
</div>
</div>
<tab v-for="tab in tabs" :key="tab.id" :tab="tab" />
@@ -37,7 +38,13 @@
? closingTab.name || `[${closingTab.tempName}]`
: ''
}}
<close-icon @click="$modal.hide('close-warn')" />
<IconButton
icon="myicon myicon-close"
variant="secondary"
:iconSize="14"
@click="$modal.hide('close-warn')"
/>
</div>
<div class="dialog-body">
You have unsaved changes. Save changes in
@@ -65,13 +72,13 @@
<script>
import Tab from './Tab'
import CloseIcon from '@/components/svg/close'
import IconButton from '@/components/Common/IconButton.vue'
import eventBus from '@/lib/eventBus'
export default {
components: {
Tab,
CloseIcon
IconButton
},
emits: [],
data() {
@@ -155,6 +162,7 @@ export default {
position: relative;
max-width: 200px;
display: flex;
align-items: center;
flex-shrink: 1;
min-width: 0;
}
@@ -1,48 +1,56 @@
<template>
<div class="value-viewer">
<div class="value-viewer-toolbar">
<button
v-for="format in formats"
:key="format.value"
type="button"
:aria-selected="currentFormat === format.value"
:class="format.value"
@click="currentFormat = format.value"
>
{{ format.text }}
</button>
<template v-if="!empty">
<div class="value-viewer-toolbar">
<button
v-for="format in formats"
:key="format.value"
type="button"
:aria-selected="currentFormat === format.value"
:class="format.value"
@click="currentFormat = format.value"
>
{{ format.text }}
</button>
<button type="button" class="copy" @click="copyToClipboard">Copy</button>
<button
type="button"
class="line-wrap"
:aria-selected="lineWrapping === true"
@click="lineWrapping = !lineWrapping"
>
Line wrap
</button>
</div>
<div class="value-body">
<codemirror
v-if="currentFormat === 'json' && formattedJson"
:value="formattedJson"
:options="cmOptions"
class="json-value original-style"
/>
<pre
v-if="currentFormat === 'text'"
:class="[
'text-value',
{ 'meta-value': isNull || isBlob },
{ 'line-wrap': lineWrapping }
]"
>{{ cellText }}</pre
>
<logs
v-if="messages && messages.length > 0"
:messages="messages"
class="messages"
/>
<button type="button" class="copy" @click="copyToClipboard">
Copy
</button>
<button
type="button"
class="line-wrap"
:aria-selected="lineWrapping === true"
@click="lineWrapping = !lineWrapping"
>
Line wrap
</button>
</div>
<div class="value-body">
<codemirror
v-if="currentFormat === 'json' && formattedJson"
:value="formattedJson"
:options="cmOptions"
class="json-value original-style"
/>
<pre
v-if="currentFormat === 'text'"
:class="[
'text-value',
{ 'meta-value': isNull || isBlob },
{ 'line-wrap': lineWrapping }
]"
>{{ cellText }}</pre
>
<logs
v-if="messages && messages.length > 0"
:messages="messages"
class="messages"
/>
</div>
</template>
<div v-show="empty" class="empty-message">
{{ emptyMessage }}
</div>
</div>
</template>
@@ -57,15 +65,22 @@ import 'codemirror/addon/fold/foldgutter.css'
import 'codemirror/addon/fold/brace-fold.js'
import 'codemirror/theme/neo.css'
import cIo from '@/lib/utils/clipboardIo'
import Logs from '@/components/Logs'
import Logs from '@/components/Common/Logs'
export default {
name: 'ValueViewer',
components: {
Codemirror,
Logs
},
props: {
cellValue: [String, Number, Uint8Array]
value: [String, Number, Uint8Array],
empty: Boolean,
emptyMessage: String,
defaultFormat: {
type: String,
default: 'text'
}
},
data() {
return {
@@ -73,7 +88,7 @@ export default {
{ text: 'Text', value: 'text' },
{ text: 'JSON', value: 'json' }
],
currentFormat: 'text',
currentFormat: this.defaultFormat,
lineWrapping: false,
formattedJson: '',
messages: []
@@ -94,13 +109,13 @@ export default {
}
},
isBlob() {
return this.cellValue && ArrayBuffer.isView(this.cellValue)
return this.value && ArrayBuffer.isView(this.value)
},
isNull() {
return this.cellValue === null
return this.value === null
},
cellText() {
const value = this.cellValue
const value = this.value
if (this.isNull) {
return 'NULL'
}
@@ -111,17 +126,23 @@ export default {
}
},
watch: {
currentFormat() {
this.messages = []
this.formattedJson = ''
if (this.currentFormat === 'json') {
this.formatJson(this.cellValue)
currentFormat: {
immediate: true,
handler() {
this.messages = []
this.formattedJson = ''
if (this.currentFormat === 'json') {
this.formatJson(this.value)
}
}
},
cellValue() {
this.messages = []
if (this.currentFormat === 'json') {
this.formatJson(this.cellValue)
value: {
immediate: true,
handler() {
this.messages = []
if (this.currentFormat === 'json') {
this.formatJson(this.value)
}
}
}
},
@@ -141,7 +162,7 @@ export default {
},
copyToClipboard() {
cIo.copyText(
this.currentFormat === 'json' ? this.formattedJson : this.cellValue,
this.currentFormat === 'json' ? this.formattedJson : this.value,
'The value is copied to clipboard.'
)
}
@@ -153,8 +174,10 @@ export default {
.value-viewer {
background-color: var(--color-white);
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
position: relative;
}
.value-viewer-toolbar {
display: flex;
@@ -219,4 +242,14 @@ export default {
width: 1px;
background: var(--color-text-base);
}
.empty-message {
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--color-text-base);
font-size: 13px;
text-align: center;
}
</style>
-69
View File
@@ -1,69 +0,0 @@
<template>
<span>
<svg
class="icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
@click.stop="onClick"
@mouseenter="showTooltip"
@mouseleave="hideTooltip"
>
<g clip-path="url(#clip0)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
:d="
`M13.6573 1.5H2.59985C1.77485 1.5 1.09985 2.175 1.09985 3V13.6649C1.09985 14.4899
1.77485 15.1649 2.59985
15.1649H9.84V13.6649H8.87866V9.08244H13.6573V9.83777H15.1573V3C15.1573
2.17 14.4873 1.5 13.6573 1.5ZM13.6573
7.58244V3H8.87866V7.58244H13.6573ZM7.37866 3H2.59985V7.58244H7.37866V3ZM2.59985
9.08244V13.6649H7.37866V9.08244H2.59985ZM13.1702
10.8434H15.6702V13.1717H18.0001V15.6717H15.6702V18H13.1702V15.6717H10.8401V13.` +
'1717H13.1702V10.8434Z'
"
fill="#A2B1C6"
/>
</g>
<defs>
<clipPath id="clip0">
<rect width="18" height="18" fill="white" />
</clipPath>
</defs>
</svg>
<span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
Add new table from CSV, JSON or NDJSON
</span>
</span>
</template>
<script>
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'AddTableIcon',
mixins: [tooltipMixin],
props: { tooltip: String },
emits: ['click'],
methods: {
onClick() {
this.hideTooltip()
this.$emit('click')
}
}
}
</script>
<style scoped>
.icon {
display: block;
margin: 0;
cursor: pointer;
}
.icon:hover path {
fill: var(--color-accent);
}
</style>
-19
View File
@@ -1,19 +0,0 @@
<template>
<svg
width="28"
height="27"
viewBox="0 0 28 27"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17.9475 8.33625L12.7838 13.5L17.9475 18.6638L16.35 20.25L9.60001
13.5L16.35 6.75L17.9475 8.33625Z"
fill="#506784"
/>
</svg>
</template>
<script>
export default {}
</script>
-54
View File
@@ -1,54 +0,0 @@
<template>
<div>
<svg
class="db-edit-icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
@click.stop="onClick"
@mouseenter="showTooltip"
@mouseleave="hideTooltip"
>
<path
d="M3 10.5V12.75C3 14.25 5.2875 15.54 8.25 15.75V13.5825L8.3475 13.5C5.34 13.32 3 12.045 3
10.5ZM9 9.75C5.685 9.75 3 8.4075 3 6.75V9C3 10.6575 5.685 12 9 12C9.2925 12 9.5775 12
9.87 12L12.75 9.09C11.55 9.54 10.2825 9.75 9 9.75ZM9 2.25C5.685 2.25 3 3.5925 3 5.25C3
6.9075 5.685 8.25 9 8.25C12.315 8.25 15 6.9075 15 5.25C15 3.5925 12.315 2.25 9 2.25ZM15.75
8.3475C15.6375 8.3475 15.5325 8.3925 15.4575 8.475L14.7075 9.225L16.245 10.725L16.995
9.975C17.1525 9.825 17.16 9.57 16.995 9.3975L16.065 8.475C15.99 8.3925 15.885 8.3475 15.78
8.3475H15.75ZM14.28 9.66L9.75 14.205V15.75H11.295L15.84 11.1975L14.28 9.66Z"
fill="#A2B1C6"
/>
</svg>
<span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
Load another database, CSV, JSON or NDJSON
</span>
</div>
</template>
<script>
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'ChangeDbIcon',
mixins: [tooltipMixin],
emits: ['click'],
methods: {
onClick() {
this.hideTooltip()
this.$emit('click')
}
}
}
</script>
<style scoped>
.db-edit-icon {
display: block;
cursor: pointer;
}
.db-edit-icon:hover path {
fill: var(--color-accent);
}
</style>
-47
View File
@@ -1,47 +0,0 @@
<template>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.41943 16V10H10.4194V16H8.41943Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.41943 16L2.41943 10H4.41943V16H2.41943Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.4194 16V7H13.4194V16H11.4194Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14.4194 16V8H16.4194V16H14.4194Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.41943 12V16H5.41943V12H7.41943Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12.4332 1.80676L16.6265 6.00001L15.9194 6.70712L12.4055 3.19326L5.93169 9.1691L1.71436
5.55424L2.36515 4.79499L5.90707 7.83092L12.4332 1.80676Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'ChartIcon'
}
</script>
-46
View File
@@ -1,46 +0,0 @@
<template>
<svg
:class="['clear-icon', { disabled: disabled }]"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.3481 14.849C13.8791 15.318 13.1191 15.318 12.6511 14.849L10.0001 11.819L7.34907
14.848C6.88007 15.317 6.12007 15.317 5.65207 14.848C5.18307 14.379 5.18307 13.619
5.65207 13.151L8.41007 10.001L5.65107 6.84898C5.18207 6.37998 5.18207 5.62098 5.65107
5.15198C6.12007 4.68298 6.87907 4.68298 7.34807 5.15198L10.0001 8.18298L12.6511
5.15198C13.1201 4.68298 13.8791 4.68298 14.3481 5.15198C14.8171 5.62098 14.8171 6.38098
14.3481 6.84898L11.5901 10.001L14.3481 13.151C14.8171 13.62 14.8171 14.38 14.3481
14.849V14.849Z"
fill="#C8D4E3"
/>
</svg>
</template>
<script>
export default {
name: 'ClearIcon',
props: { disabled: Boolean }
}
</script>
<style scoped>
.clear-icon {
cursor: pointer;
}
.clear-icon:hover path {
fill: var(--color-danger);
}
.disabled.clear-icon {
cursor: default;
}
.disabled.clear-icon:hover path {
fill: #c8d4e3;
}
</style>
-27
View File
@@ -1,27 +0,0 @@
<template>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
d="M14.1917 1.3851H12.4806V0.703125C12.4806 0.314758 12.1658 0 11.7775 0H6.246C5.85764 0
5.54288 0.314758 5.54288 0.703125V1.3851H3.83203C2.86276 1.3851 2.07422 2.17365 2.07422
3.14291V16.2422C2.07422 17.2115 2.86276 18 3.83203 18H14.1917C15.161 18 15.9495 17.2115
15.9495 16.2422V3.14291C15.9495 2.17365 15.1609 1.3851 14.1917 1.3851V1.3851ZM11.0744
1.40625V2.77034H6.94899C6.94899 2.30301 6.94899 1.91698 6.94899 1.40625H11.0744ZM14.5433
16.2422C14.5433 16.4361 14.3855 16.5938 14.1917 16.5938H3.83203C3.63812 16.5938 3.48047
16.4361 3.48047 16.2422V3.14291C3.48047 2.94914 3.63812 2.79135 3.83203
2.79135H5.54274V3.47346C5.54274 3.86183 5.85764 4.17659 6.24586 4.17659H11.7775C12.1657
4.17659 12.4806 3.86183 12.4806 3.47346V2.79135H14.1917C14.3855 2.79135 14.5433 2.94914
14.5433 3.14291V16.2422ZM12.2745 8.20967C12.5491 8.48433 12.5491 8.92955 12.2745
9.20407L8.5795 12.899C8.30498 13.1737 7.85976 13.1737 7.5851 12.899L5.74887
11.0628C5.47435 10.7883 5.47435 10.3431 5.74887 10.0684C6.02353 9.7939 6.46861 9.7939
6.74327 10.0684L8.08237 11.4075L11.2801 8.20981C11.5547 7.93515 11.9998 7.93515 12.2745
8.20967V8.20967Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'ClipboardIcon'
}
</script>
-48
View File
@@ -1,48 +0,0 @@
<template>
<svg
:class="['icon', { disabled: disabled }]"
:width="size"
:height="size"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@click.stop="$emit('click')"
>
<path
d="M14 1.41L12.59 0L7 5.59L1.41 0L0 1.41L5.59 7L0 12.59L1.41 14L7 8.41L12.59 14L14
12.59L8.41 7L14 1.41Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'CloseIcon',
props: {
size: {
type: Number,
required: false,
default: 14
},
disabled: {
type: Boolean,
required: false,
default: false
}
},
emits: ['click']
}
</script>
<style scoped>
.icon {
cursor: pointer;
}
.disabled {
pointer-events: none;
}
.icon:hover path {
fill: var(--color-text-base);
}
</style>
-32
View File
@@ -1,32 +0,0 @@
<template>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M13.5552 6.91522C13.7584 6.91357 13.9535 6.99442 14.096 7.13926L17.2582 10.3541C17.5486
10.6494 17.5447 11.1242 17.2494 11.4147C16.9541 11.7051 16.4793 11.7012 16.1888
11.4059L13.57 8.74357L9.29577 13.2318C9.01977 13.5216 8.56484 13.5436 8.2621
13.2819L5.35435 10.7677L2.03285 13.7321C1.72382 14.0079 1.24971 13.981 0.973901
13.6719C0.69809 13.3629 0.725022 12.8888 1.03406 12.613L4.8471 9.20986C5.12827 8.95892
5.55198 8.95559 5.83705 9.20208L8.70249 11.6797L13.0182 7.14796C13.1583 7.00084 13.3521
6.91686 13.5552 6.91522Z"
fill="#A2B1C6"
/>
<circle cx="5.50049" cy="6.00339" r="1.5" fill="#A2B1C6" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M16.036 1.21788L1.96546 1.213C1.14046 1.213 0.465462 1.888 0.465462 2.713L0.464111
15.2821C0.464111 16.1071 1.13911 16.7821 1.96411 16.7821L16.0347 16.787C16.8674 16.787
17.5347 16.1121 17.5347 15.287L17.536 2.71788C17.536 1.88787 16.866 1.21788 16.036
1.21788ZM16.0374 2.71788L1.96424 2.713L1.96289 15.2773L16.036 15.2821L16.0374 2.71788Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'DataViewIcon'
}
</script>
-40
View File
@@ -1,40 +0,0 @@
<template>
<svg
:class="['chevron-icon', { disabled: disabled }]"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.175 7.15002L10 10.975L13.825 7.15002L15 8.33336L10 13.3334L5 8.33336L6.175 7.15002Z"
fill="#C8D4E3"
/>
</svg>
</template>
<script>
export default {
name: 'DropDownChevron',
props: { disabled: Boolean }
}
</script>
<style scoped>
.chevron-icon {
cursor: pointer;
}
.chevron-icon:hover path {
fill: var(--color-accent);
}
.disabled.chevron-icon {
cursor: default;
}
.disabled.chevron-icon:hover path {
fill: #c8d4e3;
}
</style>
-25
View File
@@ -1,25 +0,0 @@
<template>
<svg
width="27"
height="27"
viewBox="0 0 27 27"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17.3474 8.33625L12.1837 13.5L17.3474 18.6638L15.7499 20.25L8.99991
13.5L15.7499 6.75L17.3474 8.33625Z"
fill="#506784"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.19995 19.8L7.19995 7.20001H9.19995V19.8H7.19995Z"
fill="#506784"
/>
</svg>
</template>
<script>
export default {}
</script>
-56
View File
@@ -1,56 +0,0 @@
<template>
<span>
<svg
class="icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
@click.stop="onClick"
@mouseenter="showTooltip($event, tooltipPosition)"
@mouseleave="hideTooltip"
>
<path
d="M10.5 1.5H4.5C3.675 1.5 3 2.175 3 3V15C3 15.825 3.675 16.5 4.5 16.5H13.5C14.325 16.5
15 15.825 15 15V6L10.5 1.5ZM13.5 15H4.5V3H9.75V6.75H13.5V15ZM12 8.25V13.575L10.425
12L8.325 14.1L6.225 12L8.325 9.9L6.675 8.25H12Z"
fill="#A2B1C6"
/>
</svg>
<span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
{{ tooltip }}
</span>
</span>
</template>
<script>
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'ExportIcon',
mixins: [tooltipMixin],
props: {
tooltip: String,
tooltipPosition: String
},
emits: ['click'],
methods: {
onClick() {
this.hideTooltip()
this.$emit('click')
}
}
}
</script>
<style scoped>
.icon {
display: block;
margin: 0 12px;
cursor: pointer;
}
.icon:hover path {
fill: var(--color-accent);
}
</style>
-56
View File
@@ -1,56 +0,0 @@
<template>
<svg width="19" height="18" viewBox="0 0 19 18" fill="none">
<path
d="M6.07959 13.5756C6.05908 14.0209 5.93896 14.415 5.71924 14.7578C5.49951 15.0976 5.19043
15.3613 4.79199 15.5488C4.39648 15.7363 3.94385 15.83 3.43408 15.83C2.59326 15.83
1.93115 15.5561 1.44775 15.0082C0.964355 14.4604 0.722656 13.687 0.722656
12.6879V12.3715C0.722656 11.7446 0.831055 11.1967 1.04785 10.728C1.26758 10.2563 1.58252
9.89301 1.99268 9.63812C2.40283 9.38031 2.87744 9.2514 3.4165 9.2514C4.19287 9.2514
4.81689 9.45648 5.28857 9.86664C5.76025 10.2739 6.02832 10.8364 6.09277
11.5541H4.55469C4.54297 11.1645 4.44482 10.8847 4.26025 10.7148C4.07568 10.5449 3.79443
10.4599 3.4165 10.4599C3.03271 10.4599 2.75146 10.6035 2.57275 10.8906C2.39404 11.1777
2.30029 11.6362 2.2915 12.2661V12.7187C2.2915 13.4013 2.37646 13.8891 2.54639
14.1821C2.71924 14.475 3.01514 14.6215 3.43408 14.6215C3.78857 14.6215 4.05957 14.538
4.24707 14.371C4.43457 14.204 4.53418 13.9389 4.5459 13.5756H6.07959ZM10.1094
14.0414C10.1094 13.8159 10.0288 13.6401 9.86768 13.5141C9.70947 13.3881 9.42969 13.2578
9.02832 13.123C8.62695 12.9882 8.29883 12.8578 8.04395 12.7319C7.21484 12.3246 6.80029
11.7651 6.80029 11.0532C6.80029 10.6987 6.90283 10.3867 7.10791 10.1171C7.31592 9.84467
7.60889 9.63373 7.98682 9.48431C8.36475 9.33197 8.78955 9.2558 9.26123 9.2558C9.72119
9.2558 10.1328 9.33783 10.4961 9.50189C10.8623 9.66595 11.1465 9.90033 11.3486
10.205C11.5508 10.5068 11.6519 10.8525 11.6519 11.2421H10.1138C10.1138 10.9814 10.0332
10.7792 9.87207 10.6357C9.71387 10.4921 9.49854 10.4203 9.22607 10.4203C8.95068 10.4203
8.73242 10.4819 8.57129 10.6049C8.41309 10.725 8.33398 10.8788 8.33398 11.0663C8.33398
11.2304 8.42188 11.3798 8.59766 11.5146C8.77344 11.6464 9.08252 11.7841 9.5249
11.9277C9.96729 12.0683 10.3306 12.2206 10.6147 12.3847C11.3062 12.7831 11.6519 13.3325
11.6519 14.0327C11.6519 14.5922 11.4409 15.0317 11.019 15.351C10.5972 15.6703 10.0186
15.83 9.2832 15.83C8.76465 15.83 8.29443 15.7377 7.87256 15.5532C7.45361 15.3657 7.13721
15.1108 6.92334 14.7885C6.7124 14.4633 6.60693 14.0898 6.60693 13.6679H8.15381C8.15381
14.0107 8.2417 14.2641 8.41748 14.4282C8.59619 14.5893 8.88477 14.6699 9.2832
14.6699C9.53809 14.6699 9.73877 14.6157 9.88525 14.5073C10.0347 14.3959 10.1094 14.2407
10.1094 14.0414ZM14.9258 14.0019L16.2002 9.34369H17.9229L15.7695 15.7421H14.082L11.9463
9.34369H13.6558L14.9258 14.0019Z"
fill="#A2B1C6"
/>
<path
d="M3.03345 0.991333H4.89869V2.49133H3.03345V7.93074H1.53345V2.49133C1.53345 1.66633
2.20845 0.991333 3.03345 0.991333Z"
fill="#A2B1C6"
/>
<path
d="M15.917 0.991333H14.0517V2.49133H15.917V7.93074H17.417V2.49133C17.417 1.66633 16.742
0.991333 15.917 0.991333Z"
fill="#A2B1C6"
/>
<path
d="M9.53043 1.28245e-05L13.2199 3.83975L10.9929 3.7953L10.9337 6.76455L7.96442
6.70529L8.02368 3.73603L5.6907 3.68947L9.53043 1.28245e-05Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'ExportToCsvIcon'
}
</script>
-55
View File
@@ -1,55 +0,0 @@
<template>
<svg width="19" height="18" viewBox="0 0 19 18" fill="none">
<path
d="M4.28369 13.9966C4.28369 13.7711 4.20312 13.5953 4.04199 13.4693C3.88379 13.3433 3.604
13.213 3.20264 13.0782C2.80127 12.9434 2.47314 12.813 2.21826 12.6871C1.38916 12.2798
0.974609 11.7203 0.974609 11.0084C0.974609 10.6539 1.07715 10.3419 1.28223
10.0723C1.49023 9.79987 1.7832 9.58893 2.16113 9.43951C2.53906 9.28717 2.96387 9.211
3.43555 9.211C3.89551 9.211 4.30713 9.29303 4.67041 9.45709C5.03662 9.62115 5.3208
9.85553 5.52295 10.1602C5.7251 10.462 5.82617 10.8077 5.82617 11.1973H4.28809C4.28809
10.9366 4.20752 10.7344 4.04639 10.5909C3.88818 10.4473 3.67285 10.3755 3.40039
10.3755C3.125 10.3755 2.90674 10.4371 2.74561 10.5601C2.5874 10.6802 2.5083 10.834
2.5083 11.0215C2.5083 11.1856 2.59619 11.335 2.77197 11.4698C2.94775 11.6016 3.25684
11.7393 3.69922 11.8829C4.1416 12.0235 4.50488 12.1758 4.78906 12.3399C5.48047 12.7383
5.82617 13.2877 5.82617 13.9879C5.82617 14.5474 5.61523 14.9869 5.19336 15.3062C4.77148
15.6255 4.19287 15.7852 3.45752 15.7852C2.93896 15.7852 2.46875 15.6929 2.04688
15.5084C1.62793 15.3209 1.31152 15.066 1.09766 14.7437C0.886719 14.4185 0.78125 14.045
0.78125 13.6231H2.32812C2.32812 13.9659 2.41602 14.2193 2.5918 14.3834C2.77051 14.5445
3.05908 14.6251 3.45752 14.6251C3.7124 14.6251 3.91309 14.5709 4.05957 14.4625C4.20898
14.3511 4.28369 14.1959 4.28369 13.9966ZM9.1001 13.9571L10.3745 9.29889H12.0972L9.94385
15.6973H8.25635L6.12061 9.29889H7.83008L9.1001 13.9571ZM17.7441 14.9063C17.5068 15.17
17.1597 15.3824 16.7026 15.5435C16.2456 15.7047 15.7446 15.7852 15.1997 15.7852C14.3618
15.7852 13.6924 15.5289 13.1914 15.0162C12.6904 14.5035 12.4224 13.7901 12.3872
12.876L12.3828 12.3223C12.3828 11.6924 12.4941 11.1431 12.7168 10.6744C12.9395 10.2027
13.2573 9.84088 13.6704 9.58893C14.0864 9.33405 14.5669 9.2066 15.1118 9.2066C15.9087
9.2066 16.5269 9.38971 16.9663 9.75592C17.4087 10.1192 17.6665 10.6627 17.7397
11.3863H16.2544C16.2017 11.0289 16.0874 10.774 15.9116 10.6216C15.7358 10.4693 15.4868
10.3931 15.1646 10.3931C14.7778 10.3931 14.479 10.5572 14.2681 10.8853C14.0571 11.2134
13.9502 11.6822 13.9473 12.2916V12.6783C13.9473 13.317 14.0557 13.7974 14.2725
14.1197C14.4922 14.439 14.8364 14.5987 15.3052 14.5987C15.7065 14.5987 16.0054 14.5093
16.2017 14.3306V13.3375H15.1294V12.2784H17.7441V14.9063Z"
fill="#A2B1C6"
/>
<path
d="M2.86768 0.991333H4.73292V2.49133H2.86768V7.93074H1.36768V2.49133C1.36768 1.66633
2.04268 0.991333 2.86768 0.991333Z"
fill="#A2B1C6"
/>
<path
d="M15.7512 0.991333H13.886V2.49133H15.7512V7.93074H17.2512V2.49133C17.2512 1.66633 16.5762
0.991333 15.7512 0.991333Z"
fill="#A2B1C6"
/>
<path
d="M9.36466 -1.76931e-05L13.0541 3.83972L10.8272 3.79527L10.7679 6.76452L7.79865
6.70526L7.85791 3.736L5.52492 3.68944L9.36466 -1.76931e-05Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'ExportToSvgIcon'
}
</script>
-82
View File
@@ -1,82 +0,0 @@
<template>
<div>
<svg
class="hint-icon"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
@click.stop="onClick"
@mouseenter="showTooltip"
@mouseleave="hideTooltip"
>
<path
d="M8.75 14.1666H10.4167V12.5H8.75V14.1666ZM9.58333 16.25C5.90833 16.25 2.91667 13.2583
2.91667 9.58333C2.91667 5.90833 5.90833 2.91667 9.58333 2.91667C13.2583 2.91667 16.25
5.90833 16.25 9.58333C16.25 13.2583 13.2583 16.25 9.58333 16.25ZM9.58333 1.25C8.48898
1.25 7.40535 1.46555 6.3943 1.88434C5.38326 2.30313 4.4646 2.91696 3.69078
3.69078C2.12797 5.25358 1.25 7.3732 1.25 9.58333C1.25 11.7935 2.12797 13.9131 3.69078
15.4759C4.4646 16.2497 5.38326 16.8635 6.3943 17.2823C7.40535 17.7011 8.48898 17.9167
9.58333 17.9167C11.7935 17.9167 13.9131 17.0387 15.4759 15.4759C17.0387 13.9131 17.9167
11.7935 17.9167 9.58333C17.9167 8.48898 17.7011 7.40535 17.2823 6.3943C16.8635 5.38326
16.2497 4.4646 15.4759 3.69078C14.7021 2.91696 13.7834 2.30313 12.7724 1.88434C11.7613
1.46555 10.6777 1.25 9.58333 1.25Z"
fill="#A2B1C6"
/>
<path
d="M9.91601 4.51787C8.98167 4.42606 8.05144 4.69097 7.36309 5.24472C6.68735 5.78828
6.2998 6.56661 6.2998 7.38012H7.92488C7.92488 6.97463 8.11059 6.60187 8.44779
6.33061C8.79784 6.049 9.25647 5.92005 9.73896 5.96755C10.4832 6.04076 11.0828 6.57277
11.1647 7.23265C11.2306 7.764 10.9661 8.28194 10.4744 8.58426C9.38676 9.25303 8.73742
10.343 8.73742 11.5H10.3625C10.3625 10.8243 10.7477 10.184 11.3929 9.78733C12.3808
9.17985 12.9122 8.13913 12.7798 7.07124C12.6144 5.73863 11.41 4.66476 9.91601 4.51787Z"
fill="#A2B1C6"
/>
</svg>
<span
ref="tooltip"
class="icon-tooltip"
:style="{ ...tooltipStyle, maxWidth: maxWidth }"
>
{{ hint }}
</span>
</div>
</template>
<script>
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'HintIcon',
mixins: [tooltipMixin],
props: {
hint: String,
maxWidth: String
},
emits: ['click'],
methods: {
onClick() {
this.hideTooltip()
this.$emit('click')
}
}
}
</script>
<style scoped>
.hint-icon {
display: block;
cursor: pointer;
}
.hint-icon:hover path {
fill: var(--color-text-base);
}
.icon-tooltip {
display: block;
white-space: normal;
height: auto;
line-height: normal;
padding: 6px;
box-sizing: border-box;
}
</style>
-49
View File
@@ -1,49 +0,0 @@
<template>
<svg
width="19"
height="18"
viewBox="0 0 19 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.1626 10.0745L7.56641 10.8831V12.2322L3.68164 10.6501V9.4812L7.56641
7.89917V9.2439L5.1626 10.0745ZM8.99023 13.3H7.93994L10.124 6.35229H11.1787L8.99023
13.3ZM14.1099 10.0613L11.7192 9.24829V7.90356L15.582 9.4856V10.6545L11.7192
12.2366V10.8918L14.1099 10.0613Z"
fill="#A2B1C6"
/>
<path
d="M2.17041 0.0637207H16.2185V1.56372H2.17041V9.30354H0.67041V1.56372C0.67041 0.73872
1.34541 0.0637207 2.17041 0.0637207Z"
fill="#A2B1C6"
/>
<path
d="M17.1704 0.0637207H15.3052V1.56372H17.1704V9.84163H18.6704V1.56372C18.6704 0.73872
17.9954 0.0637207 17.1704 0.0637207Z"
fill="#A2B1C6"
/>
<path
d="M2.17041 17.1098H15.8754V15.6098H2.17041V8.78486H0.67041V15.6098C0.67041 16.4348
1.34541 17.1098 2.17041 17.1098Z"
fill="#A2B1C6"
/>
<path
d="M17.1704 17.1098H15.3052V15.6098H17.1704V8.55939H18.6704V15.6098C18.6704 16.4348
17.9954 17.1098 17.1704 17.1098Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M18.1197 4.13787H1.76172V3.03787H18.1197V4.13787Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'HtmlIcon'
}
</script>
-22
View File
@@ -1,22 +0,0 @@
<template>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M15.7661 5.13901L18.3407 9.43008H16.5161V12.8467C16.5161 13.7957 16.2783 14.6451 15.6714
15.2521C15.0645 15.859 14.215 16.0967 13.2661 16.0967H9.84942V17.9214L5.55835
15.3467L9.84942 12.7721V14.5967H13.2661C13.9838 14.5967 14.3844 14.4178 14.6108
14.1914C14.8372 13.965 15.0161 13.5645 15.0161 12.8467V9.43008H13.1914L15.7661 5.13901Z"
fill="#A2B1C6"
/>
<path d="M6.41943 0H18.4194V4H6.41943V0Z" fill="#A2B1C6" />
<path d="M0.419434 6H4.41943V18H0.419434V6Z" fill="#A2B1C6" />
<path d="M0.419434 0H4.41943V4H0.419434V0Z" fill="#A2B1C6" />
</svg>
</template>
<script>
export default {
name: 'PivotIcon'
}
</script>
-39
View File
@@ -1,39 +0,0 @@
<template>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
d="M9 5.51953C6.57686 5.51953 4.60547 7.49092 4.60547 9.91406C4.60547 12.3372 6.57686
14.3086 9 14.3086C11.4231 14.3086 13.3945 12.3372 13.3945 9.91406C13.3945 7.49092 11.4231
5.51953 9 5.51953ZM9 12.9023C7.35226 12.9023 6.01172 11.5618 6.01172 9.91406C6.01172
8.26632 7.35226 6.92578 9 6.92578C10.6477 6.92578 11.9883 8.26632 11.9883
9.91406C11.9883 11.5618 10.6477 12.9023 9 12.9023Z"
fill="#A2B1C6"
/>
<path
d="M15.8906 3.41016H13.304C13.2221 3.41016 13.1483 3.36547 13.1104 3.29319L12.3948
1.78945C12.3928 1.78534 12.3908 1.78126 12.3887 1.77718C12.1117 1.22312 11.5548 0.878906
10.9353 0.878906H7.11478C6.49529 0.878906 5.93835 1.22312 5.66135 1.77722C5.65928 1.7813
5.65731 1.78538 5.65534 1.78949L4.9397 3.2933C4.90173 3.36547 4.82797 3.41016 4.74609
3.41016H2.10938C0.946266 3.41016 0 4.35642 0 5.51953V15.0117C0 16.1748 0.946266 17.1211
2.10938 17.1211H15.8906C17.0537 17.1211 18 16.1748 18 15.0117V5.51953C18 4.35642 17.0537
3.41016 15.8906 3.41016ZM16.5938 15.0117C16.5938 15.3994 16.2783 15.7148 15.8906
15.7148H2.10938C1.72167 15.7148 1.40625 15.3994 1.40625 15.0117V5.51953C1.40625 5.13183
1.72167 4.81641 2.10938 4.81641H4.74609C5.36555 4.81641 5.92249 4.47223 6.19952
3.91816C6.2016 3.91409 6.20357 3.90997 6.20557 3.90586L6.92121 2.40205C6.95914 2.32984
7.0329 2.28516 7.11478 2.28516H10.9353C11.0172 2.28516 11.091 2.32984 11.1289
2.40202L11.8445 3.90582C11.8465 3.90994 11.8485 3.91405 11.8506 3.91813C12.1276 4.47219
12.6846 4.81637 13.304 4.81637H15.8906C16.2783 4.81637 16.5938 5.13179 16.5938
5.5195V15.0117Z"
fill="#A2B1C6"
/>
<path
d="M15.1875 6.22266H13.7812V7.62891H15.1875V6.22266Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'PngIcon'
}
</script>
-47
View File
@@ -1,47 +0,0 @@
<template>
<svg width="19" height="19" viewBox="0 0 19 19" fill="none">
<g clip-path="url(#clip0_2130_5292)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M1.85303 11.3794L1.85303 7.80371L5.86304 7.80371L5.86304
11.3794L1.85303 11.3794ZM7.36304 11.3794L7.36304 7.80371L11.3428
7.80371L11.3428 11.3794L7.36304 11.3794ZM12.8428 11.3794L16.853
11.3794L16.853 7.80371L12.8428 7.80371L12.8428 11.3794ZM15.353
6.30371L16.853 6.30371C17.6815 6.30371 18.353 6.97528 18.353
7.80371L18.353 11.3794C18.353 12.2078 17.6815 12.8794 16.853
12.8794L15.353 12.8794L15.353 14.3111C15.353 15.0153 14.7603 15.5916
14.0358 15.5916L4.67027 15.5916C3.94579 15.5916 3.35303 15.0153 3.35303
14.3111L3.35303 12.8794L1.85303 12.8794C1.0246 12.8794 0.353027 12.2078
0.353027 11.3794L0.353027 7.80371C0.353027 6.97528 1.0246 6.30371
1.85303 6.30371L3.35303 6.30371L3.35303 4.87201C3.35303 4.16349 3.94139
3.59155 4.67027 3.59155L14.0358 3.59155C14.7604 3.59155 15.353 4.16117
15.353 4.87201L15.353 6.30371ZM14.0315 6.30371L14.0315 4.87086L11.887
4.87086L11.887 6.30371L12.8428 6.30371L14.0315 6.30371ZM10.387
6.30371L10.387 4.87086L8.26685 4.87086L8.26685 6.30371L10.387
6.30371ZM6.76685 6.30371L6.76685 4.87086L4.67027 4.87086L4.67027
6.30371L6.76685 6.30371ZM4.67027 12.8794L4.67027 14.3121L6.76685
14.3121L6.76685 12.8794L4.67027 12.8794ZM8.26685 12.8794L8.26685
14.3121L10.387 14.3121L10.387 12.8794L8.26685 12.8794ZM11.887
12.8794L11.887 14.3121L14.0315 14.3121L14.0315 12.8794L11.887 12.8794Z"
fill="#A2B1C6"
/>
</g>
<defs>
<clipPath id="clip0_2130_5292">
<rect
width="18"
height="18"
fill="white"
transform="translate(0.353027 18.5916) rotate(-90)"
/>
</clipPath>
</defs>
</svg>
</template>
<script>
export default {
name: 'RowIcon'
}
</script>
-14
View File
@@ -1,14 +0,0 @@
<template>
<svg width="12" height="13" viewBox="0 0 12 13" fill="none">
<path
d="M11.1624 6.94358L0.770043 12.9436L0.770043 0.943573L11.1624 6.94358Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'RunIcon'
}
</script>
-56
View File
@@ -1,56 +0,0 @@
<template>
<svg
:class="['sort-icon', { horizontal }, { asc }]"
width="6"
height="12"
viewBox="0 0 6 12"
fill="none"
>
<path
d="M2.99932 -3.63032e-05C2.75092 -3.63032e-05 2.54932 0.201563 2.54932 0.449964L2.54932
11.55C2.54932 11.7984 2.75092 12 2.99932 12C3.24772 12 3.44932 11.7984 3.44932
11.55L3.44932 0.449964C3.44932 0.201563 3.24772 -3.63032e-05 2.99932 -3.63032e-05Z"
fill="#506784"
/>
<path
d="M2.99915 1.80492e-05C2.8839 1.80492e-05 2.76865 0.0438534 2.68109 0.132073L0.581055
2.232C0.405273 2.40789 0.405273 2.69287 0.581055 2.86865C0.756946 3.04443 1.04193 3.04443
1.21771 2.86865L2.99969 1.08667L4.78168 2.86865C4.95746 3.04443 5.24255 3.04443 5.41833
2.86865C5.59412 2.69287 5.59412 2.40789 5.41833 2.232L3.3183 0.132073C3.22953 0.0438534
3.11428 1.80492e-05 2.99915 1.80492e-05V1.80492e-05Z"
fill="#506784"
/>
</svg>
</template>
<script>
export default {
name: 'SortIcon',
props: {
horizontal: {
type: Boolean,
required: false,
default: false
},
asc: {
type: Boolean,
required: false,
default: true
}
}
}
</script>
<style scoped>
svg.asc {
transform: rotate(180deg);
}
svg.horizontal {
transform: rotate(-90deg);
}
svg.horizontal.asc {
transform: rotate(90deg);
}
</style>
-60
View File
@@ -1,60 +0,0 @@
<template>
<svg width="18" height="19" viewBox="0 0 18 19" fill="none">
<g clip-path="url(#clip0)">
<path
d="M4.5 1.51343H10.5L15 6.01343V8.45284H13.5V6.76343H9.75V3.01343H4.5V8.45284H3V3.01343C3
2.18843 3.675 1.51343 4.5 1.51343Z"
fill="#A2B1C6"
/>
<path
d="M4.28369 14.8127C4.28369 14.5872 4.20312 14.4114 4.04199 14.2854C3.88379 14.1594
3.604 14.0291 3.20264 13.8943C2.80127 13.7595 2.47314 13.6292 2.21826 13.5032C1.38916
13.0959 0.974609 12.5364 0.974609 11.8245C0.974609 11.47 1.07715 11.158 1.28223
10.8884C1.49023 10.616 1.7832 10.405 2.16113 10.2556C2.53906 10.1033 2.96387 10.0271
3.43555 10.0271C3.89551 10.0271 4.30713 10.1091 4.67041 10.2732C5.03662 10.4373 5.3208
10.6716 5.52295 10.9763C5.7251 11.2781 5.82617 11.6238 5.82617 12.0134H4.28809C4.28809
11.7527 4.20752 11.5505 4.04639 11.407C3.88818 11.2634 3.67285 11.1917 3.40039
11.1917C3.125 11.1917 2.90674 11.2532 2.74561 11.3762C2.5874 11.4963 2.5083
11.6501 2.5083 11.8376C2.5083 12.0017 2.59619 12.1511 2.77197 12.2859C2.94775 12.4177
3.25684 12.5554 3.69922 12.699C4.1416 12.8396 4.50488 12.9919 4.78906 13.156C5.48047
13.5544 5.82617 14.1038 5.82617 14.804C5.82617 15.3635 5.61523 15.803 5.19336
16.1223C4.77148 16.4417 4.19287 16.6013 3.45752 16.6013C2.93896 16.6013 2.46875 16.509
2.04688 16.3245C1.62793 16.137 1.31152 15.8821 1.09766 15.5598C0.886719 15.2346
0.78125 14.8611 0.78125 14.4392H2.32812C2.32812 14.782 2.41602 15.0354 2.5918
15.1995C2.77051 15.3606 3.05908 15.4412 3.45752 15.4412C3.7124 15.4412 3.91309
15.387 4.05957 15.2786C4.20898 15.1672 4.28369 15.012 4.28369 14.8127ZM12.0444
13.446C12.0444 14.0378 11.9463 14.5549 11.75 14.9973C11.5537 15.4368 11.2827 15.7898
10.937 16.0564L11.9697 16.8738L11.0161 17.6824L9.64062 16.575C9.51172 16.5925 9.38281
16.6013 9.25391 16.6013C8.70898 16.6013 8.22559 16.4753 7.80371 16.2234C7.38184 15.9714
7.05225 15.6111 6.81494 15.1423C6.58057 14.6736 6.45898 14.1345 6.4502
13.5251V13.1868C6.4502 12.5569 6.56445 12.0032 6.79297 11.5256C7.02441 11.0481 7.35254
10.679 7.77734 10.4182C8.20508 10.1575 8.69434 10.0271 9.24512 10.0271C9.78711 10.0271
10.2705 10.156 10.6953 10.4138C11.1201 10.6716 11.4497 11.0393 11.6841 11.5168C11.9214
11.9915 12.0415 12.5364 12.0444 13.1516V13.446ZM10.4756 13.178C10.4756 12.5422 10.3687
12.0603 10.1548 11.7322C9.94385 11.4011 9.64062 11.2356 9.24512 11.2356C8.83789 11.2356
8.53174 11.3982 8.32666 11.7234C8.12158 12.0486 8.01758 12.5247 8.01465
13.1516V13.446C8.01465 14.0759 8.11865 14.5593 8.32666 14.8962C8.53467 15.2302 8.84375
15.3972 9.25391 15.3972C9.64648 15.3972 9.94678 15.2317 10.1548 14.9006C10.3657
14.5696 10.4727 14.0935 10.4756 13.4724V13.178ZM14.3735
15.3269H17.0586V16.5134H12.8311V10.115H14.3735V15.3269Z"
fill="#A2B1C6"
/>
</g>
<defs>
<clipPath id="clip0">
<rect
width="18"
height="18"
fill="white"
transform="translate(0 0.0134277)"
/>
</clipPath>
</defs>
</svg>
</template>
<script>
export default {
name: 'SqlEditorIcon'
}
</script>
-42
View File
@@ -1,42 +0,0 @@
<template>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14.5 2.16512L3.5 2.16999C2.675 2.16999 2 2.84499 2 3.66999V14.3349C2 15.1599 2.675
15.8349 3.5 15.8349L14.5 15.83C15.3327 15.83 16 15.1551 16 14.33V3.66512C16 2.83511 15.33
2.16512 14.5 2.16512ZM14.5014 3.66512L3.49878 3.66999V14.33L14.5014 14.3251V3.66512Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.27319 14.7069L6.27319 3.32135L7.77319 3.32135L7.77319 14.7069L6.27319 14.7069Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.2639 14.6932L10.2639 3.30772L11.7639 3.30772L11.7639 14.6932L10.2639 14.6932Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M15.5108 7.48325H2.4895V5.98325H15.5108V7.48325Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M15.5108 11.6056H2.4895V10.1056H15.5108V11.6056Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'TableIcon'
}
</script>
-36
View File
@@ -1,36 +0,0 @@
<template>
<svg
:style="{ transform: expanded ? 'rotate(90deg)' : 'rotate(0)' }"
class="chevron-icon"
width="9"
height="9"
viewBox="0 0 8 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0.721924 9.93097L4.85292 5.79997L0.721924 1.66897L1.99992 0.399973L7.39992
5.79997L1.99992 11.2L0.721924 9.93097Z"
:fill="expanded ? '#506784' : 'rgba(80, 103, 132, 0.5)'"
/>
</svg>
</template>
<script>
export default {
name: 'TreeChevron',
props: {
expanded: {
type: Boolean,
required: true
}
}
}
</script>
<style scoped>
.chevron-icon {
-webkit-transition: transform 0.15s ease-in-out;
transition: transform 0.15s ease-in-out;
}
</style>
-44
View File
@@ -1,44 +0,0 @@
<template>
<svg width="19" height="19" viewBox="0 0 19 19" fill="none">
<g clip-path="url(#clip0_2131_6054)">
<path
d="M3.53784 11.5846L3.53784 3.14734L11.9751 3.14734V7.676C12.4655 7.51991
12.9771 7.47439 13.4751 7.53264V3.14734C13.4751 2.31891 12.8035 1.64734
11.9751 1.64734L3.53784 1.64734C2.70941 1.64734 2.03784 2.31891 2.03784
3.14734L2.03784 11.5846C2.03784 12.413 2.70942 13.0846 3.53784
13.0846H10.0831C9.771 12.6184 9.58279 12.1055 9.51083
11.5846H3.53784Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14.7887 9.9291C15.4307 10.8837 15.1773 12.1779 14.2228
12.8199C13.2682 13.4618 11.974 13.2084 11.332 12.2539C10.69 11.2993
10.9434 10.0051 11.898 9.3631C12.8525 8.72113 14.1468 8.97454 14.7887
9.9291ZM14.4606 14.3901L16.6181 17.5982C16.8492 17.9419 17.3153 18.0331
17.659 17.802C18.0027 17.5708 18.0939 17.1048 17.8628 16.7611L15.6884
13.5279C16.7949 12.3365 16.9801 10.4996 16.0334 9.092C14.9292 7.45002
12.7029 7.01412 11.0609 8.1184C9.41891 9.22268 8.98302 11.449 10.0873
13.0909C11.062 14.5403 12.9109 15.05 14.4606 14.3901Z"
fill="#A2B1C6"
/>
</g>
<defs>
<clipPath id="clip0_2131_6054">
<rect
width="18"
height="18"
fill="white"
transform="translate(0.5 18.5916) rotate(-90)"
/>
</clipPath>
</defs>
</svg>
</template>
<script>
export default {
name: 'ViewCellValueIcon'
}
</script>
+52
View File
@@ -0,0 +1,52 @@
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { localizeString } from 'react-chart-editor/lib'
class EditorControls extends Component {
constructor(props, context) {
super(props, context)
this.localize = key =>
localizeString(this.props.dictionaries || {}, this.props.locale, key)
}
getChildContext() {
return {
dictionaries: this.props.dictionaries || {},
localize: this.localize,
locale: this.props.locale
}
}
render() {
return (
<div
className={
'editor_controls plotly-editor--theme-provider' +
`${this.props.className ? ` ${this.props.className}` : ''}`
}
>
{this.props.children}
</div>
)
}
}
EditorControls.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
dictionaries: PropTypes.object,
locale: PropTypes.string
}
EditorControls.defaultProps = {
locale: 'en'
}
EditorControls.childContextTypes = {
dictionaries: PropTypes.object,
locale: PropTypes.string,
localize: PropTypes.func
}
export default EditorControls
+38 -1
View File
@@ -13,6 +13,37 @@ export function getOptionsFromDataSources(dataSources) {
}))
}
export function getSelectedPointsIndexes(stateData) {
if (!stateData) {
return []
}
return Array.from(
new Set(stateData.flatMap(data => data.selectedpoints || []))
)
}
export function getRowsByIndexFromDataSources(dataSources, rowIndexes) {
if (!dataSources) {
return []
}
const dataSourceColumns = Object.keys(dataSources)
return rowIndexes.map(rowIndex =>
dataSourceColumns.reduce((result, columnName) => {
result[columnName] = dataSources[columnName][rowIndex]
return result
}, {})
)
}
export function clearSelection(data, layout) {
if (layout?.selections?.length > 0) {
layout.selections = []
data.forEach(dataItem => delete dataItem.selectedpoints)
}
}
export function getOptionsForSave(state, dataSources) {
// we don't need to save the data, only settings
// so we modify state.data using dereference
@@ -22,6 +53,9 @@ export function getOptionsForSave(state, dataSources) {
emptySources[key] = []
}
dereference.default(stateCopy.data, emptySources)
// Also, we don't need to save selections
clearSelection(stateCopy.data, stateCopy.layout)
return stateCopy
}
@@ -72,5 +106,8 @@ export default {
getOptionsForSave,
getImageDataUrl,
getHtml,
getChartData
getChartData,
getRowsByIndexFromDataSources,
clearSelection,
getSelectedPointsIndexes
}
+500
View File
@@ -0,0 +1,500 @@
import { COLOR_PICKER_CONSTANTS } from 'react-colorscales'
import tinycolor from 'tinycolor2'
const TYPE_NODE = 0
const TYPE_EDGE = 1
const DEFAULT_SCALE = COLOR_PICKER_CONSTANTS.DEFAULT_SCALE
export function dataSourceIsValid(dataSources) {
const docColumn = Object.keys(dataSources)[0]
if (!docColumn) {
return false
}
try {
const records = dataSources[docColumn].slice(0, 10)
records.forEach(record => {
const parsedRec = JSON.parse(record)
if (Object.keys(parsedRec).length < 2) {
throw new Error('The records must have at least 2 keys')
}
})
const firstRecord = JSON.parse(records[0])
if (
!Object.keys(firstRecord).some(key => {
return records
.map(record => JSON.parse(record)[key])
.every(value => value === 0 || value === 1)
})
) {
throw new Error(
'There must be a common key used as object type: 0 - node, 1 - edge'
)
}
return true
} catch (err) {
return false
}
}
export function clearNodeCoordinates(graph) {
graph.forEachNode((nodeId, attributes) => {
attributes.x = undefined
attributes.y = undefined
})
}
export function buildNodes(graph, dataSources, options) {
const docColumn = Object.keys(dataSources)[0]
const { objectType, nodeId } = options.structure
if (objectType && nodeId) {
const nodes = dataSources[docColumn]
.map(json => JSON.parse(json))
.filter(item => item[objectType] === TYPE_NODE)
nodes.forEach(node => {
if (node[nodeId]) {
graph.addNode(node[nodeId], {
data: node
})
}
})
}
}
export function buildEdges(graph, dataSources, options) {
const docColumn = Object.keys(dataSources)[0]
const { objectType, edgeSource, edgeTarget } = options.structure
if (objectType && edgeSource && edgeTarget) {
const edges = dataSources[docColumn]
.map(json => JSON.parse(json))
.filter(item => item[objectType] === TYPE_EDGE)
edges.forEach(edge => {
const source = edge[edgeSource]
const target = edge[edgeTarget]
if (graph.hasNode(source) && graph.hasNode(target)) {
graph.addEdge(source, target, {
data: edge
})
}
})
}
}
export function updateNodes(graph, attributeUpdates) {
const changeMethods = []
if (attributeUpdates.label) {
changeMethods.push(getUpdateLabelMethod(attributeUpdates.label))
}
if (attributeUpdates.size) {
changeMethods.push(getUpdateSizeMethod(graph, attributeUpdates.size))
}
if (attributeUpdates.color) {
changeMethods.push(getUpdateNodeColorMethod(graph, attributeUpdates.color))
}
graph.forEachNode(nodeId => {
graph.updateNode(nodeId, attributes => {
const newAttributes = { ...attributes }
changeMethods.forEach(method => method(newAttributes, nodeId))
return newAttributes
})
})
}
export function updateEdges(graph, attributeUpdates) {
const changeMethods = []
if (attributeUpdates.label) {
changeMethods.push(getUpdateLabelMethod(attributeUpdates.label))
}
if (attributeUpdates.size) {
changeMethods.push(getUpdateSizeMethod(graph, attributeUpdates.size))
}
if (attributeUpdates.color) {
changeMethods.push(getUpdateEdgeColorMethod(graph, attributeUpdates.color))
}
if ('showDirection' in attributeUpdates) {
changeMethods.push(
attributes =>
(attributes.type = attributeUpdates.showDirection ? 'arrow' : 'line')
)
}
graph.forEachEdge((edgeId, attributes, source, target) => {
graph.updateEdgeWithKey(edgeId, source, target, attr => {
const newAttributes = { ...attr }
changeMethods.forEach(method => method(newAttributes, edgeId))
return newAttributes
})
})
}
export function reduceNodes(nodeId, nodeData, interactionState, settings) {
const {
selectedNodeId,
hoveredNodeId,
selectedEdgeId,
hoveredEdgeId,
neighborsOfSelectedNode,
neighborsOfHoveredNode,
selectedEdgeExtremities,
hoveredEdgeExtremities,
nodesMatchingSearchQuery,
focusedFoundNodeId
} = interactionState
const res = { ...nodeData }
if (
selectedNodeId ||
hoveredNodeId ||
hoveredEdgeId ||
selectedEdgeId ||
nodesMatchingSearchQuery ||
focusedFoundNodeId
) {
res.zIndex = 2
const matchQuery =
nodesMatchingSearchQuery && nodesMatchingSearchQuery.includes(nodeId)
res.highlighted = [
selectedNodeId,
hoveredNodeId,
focusedFoundNodeId
].includes(nodeId)
if (res.highlighted) {
res.labelColor = 'black'
}
const isInHoveredFamily =
neighborsOfHoveredNode?.has(nodeId) ||
hoveredEdgeExtremities.includes(nodeId)
const isInSelectedFamily =
neighborsOfSelectedNode?.has(nodeId) ||
selectedEdgeExtremities.includes(nodeId)
if (res.highlighted || isInSelectedFamily || isInHoveredFamily) {
res.forceLabel = true
} else if (!matchQuery) {
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,
nodesMatchingSearchQuery
} = interactionState
const res = { ...edgeData }
if (
hoveredEdgeId ||
selectedEdgeId ||
selectedNodeId ||
hoveredNodeId ||
nodesMatchingSearchQuery
) {
const extremities = graph.extremities(edgeId)
res.zIndex = 2
const isHighlighted = hoveredEdgeId === edgeId || selectedEdgeId === edgeId
let isVisible =
isHighlighted ||
(nodesMatchingSearchQuery &&
extremities.every(nodeId => nodesMatchingSearchQuery.includes(nodeId)))
if (settings.style.highlightMode === 'node_and_neighbors') {
isVisible =
isVisible ||
(selectedNodeId && extremities.includes(selectedNodeId)) ||
(hoveredNodeId && extremities.includes(hoveredNodeId))
} else if (settings.style.highlightMode === 'include_neighbor_edges') {
isVisible =
isVisible ||
(selectedNodeId &&
extremities.every(
n => n === selectedNodeId || neighborsOfSelectedNode.has(n)
)) ||
(hoveredNodeId &&
extremities.every(
n => n === hoveredNodeId || neighborsOfHoveredNode.has(n)
))
}
if (isHighlighted) {
res.size = res.size * 2
res.forceLabel = true
} else if (!isVisible) {
res.color = getDiminishedColor(
edgeData.color,
settings.style.backgroundColor
)
res.zIndex = 1
res.label = ''
}
}
return res
}
export function getDiminishedColor(color, bgColor) {
const colorObj = tinycolor(color)
const colorOpacity = colorObj.getAlpha()
colorObj.setAlpha(0.25 * colorOpacity)
const fg = colorObj.toRgb()
const bg = tinycolor(bgColor).toRgb()
const r = Math.round(fg.r * fg.a + bg.r * (1 - fg.a))
const g = Math.round(fg.g * fg.a + bg.g * (1 - fg.a))
const b = Math.round(fg.b * fg.a + bg.b * (1 - fg.a))
return tinycolor({ r, g, b, a: 1 }).toHexString()
}
function getUpdateLabelMethod(labelSettings) {
const { source, color } = labelSettings
return attributes => {
const label = attributes.data[source] ?? ''
attributes.label = label.toString()
attributes.labelColor = color
}
}
function getUpdateSizeMethod(graph, sizeSettings) {
const { type, value, source, scale, mode, min, method } = sizeSettings
if (type === 'constant') {
return attributes => (attributes.size = value)
} else if (type === 'variable') {
return attributes => {
attributes.size = getVariabledSize(
mode,
attributes.data[source],
scale,
min
)
}
} else {
return (attributes, nodeId) => {
attributes.size = getVariabledSize(
mode,
graph[method](nodeId),
scale,
min
)
}
}
}
function getDirectVariableColorUpdateMethod(source, opacity = 100) {
return attributes => {
const color = tinycolor(attributes.data[source])
const colorOpacity = color.getAlpha()
attributes.color = color
.setAlpha((opacity / 100) * colorOpacity)
.toHex8String()
}
}
function getUpdateNodeColorMethod(graph, colorSettings) {
const {
type,
value,
source,
sourceUsage,
colorscale,
colorscaleDirection,
mode,
method,
opacity
} = colorSettings
if (type === 'constant') {
const color = tinycolor(value)
const colorOpacity = color.getAlpha()
return attributes =>
(attributes.color = color
.setAlpha((opacity / 100) * colorOpacity)
.toHex8String())
} else if (type === 'variable') {
return sourceUsage === 'map_to'
? getColorMethod(
graph,
mode,
(nodeId, attributes) => attributes.data[source],
colorscale,
colorscaleDirection,
getNodeValueScale,
opacity
)
: getDirectVariableColorUpdateMethod(source, opacity)
} else {
return getColorMethod(
graph,
mode,
nodeId => graph[method](nodeId),
colorscale,
colorscaleDirection,
getNodeValueScale,
opacity
)
}
}
function getUpdateEdgeColorMethod(graph, colorSettings) {
const {
type,
value,
source,
sourceUsage,
colorscale,
colorscaleDirection,
mode
} = colorSettings
if (type === 'constant') {
return attributes => (attributes.color = value)
} else {
return sourceUsage === 'map_to'
? getColorMethod(
graph,
mode,
(edgeId, attributes) => attributes.data[source],
colorscale,
colorscaleDirection,
getEdgeValueScale
)
: getDirectVariableColorUpdateMethod(source)
}
}
function getVariabledSize(mode, value, scale, min) {
if (mode === 'diameter') {
return Math.max((value / 2) * scale, min / 2)
} else if (mode === 'area') {
return Math.max(Math.sqrt((value / 2) * scale), min / 2)
} else {
return Math.max(value * scale, min)
}
}
function getColorMethod(
graph,
mode,
sourceGetter,
selectedColorscale,
colorscaleDirection,
valueScaleGetter,
opacity = 100
) {
const opacityFactor = opacity / 100
const valueScale = valueScaleGetter(graph, sourceGetter)
let colorscale = selectedColorscale || DEFAULT_SCALE
if (colorscaleDirection === 'reversed') {
colorscale = [...colorscale].reverse()
}
if (mode === 'categorical') {
const colorMap = Object.fromEntries(
valueScale.map((value, index) => [
value,
colorscale[index % colorscale.length]
])
)
return (attributes, nodeId) => {
const category = sourceGetter(nodeId, attributes)
attributes.color = tinycolor(colorMap[category])
.setAlpha(opacityFactor)
.toHex8String()
}
} else {
const min = valueScale[0]
const max = valueScale[valueScale.length - 1]
const normalizedColorscale = colorscale.map((color, index) => [
index / (colorscale.length - 1),
tinycolor(color).toRgb()
])
return (attributes, nodeId) => {
const value = sourceGetter(nodeId, attributes)
const normalizedValue = (value - min) / (max - min)
if (isNaN(normalizedValue)) {
attributes.color = tinycolor('#000000')
.setAlpha(opacityFactor)
.toHex8String()
return
}
const exactMatch = normalizedColorscale.find(
([value]) => value === normalizedValue
)
if (exactMatch) {
attributes.color = tinycolor(exactMatch[1])
.setAlpha(opacityFactor)
.toHex8String()
return
}
const rightColorIndex = normalizedColorscale.findIndex(
([value]) => value > normalizedValue
)
const leftColorIndex = rightColorIndex - 1
const right = normalizedColorscale[rightColorIndex]
const left = normalizedColorscale[leftColorIndex]
const interpolationFactor =
(normalizedValue - left[0]) / (right[0] - left[0])
const r0 = left[1].r
const g0 = left[1].g
const b0 = left[1].b
const r1 = right[1].r
const g1 = right[1].g
const b1 = right[1].b
attributes.color = tinycolor({
r: r0 + interpolationFactor * (r1 - r0),
g: g0 + interpolationFactor * (g1 - g0),
b: b0 + interpolationFactor * (b1 - b0)
})
.setAlpha(opacityFactor)
.toHex8String()
}
}
}
function getNodeValueScale(graph, sourceGetter) {
const scaleSet = graph.reduceNodes((res, nodeId, attributes) => {
res.add(sourceGetter(nodeId, attributes))
return res
}, new Set())
return Array.from(scaleSet).sort((a, b) => a - b)
}
function getEdgeValueScale(graph, sourceGetter) {
const scaleSet = graph.reduceEdges((res, edgeId, attributes) => {
res.add(sourceGetter(edgeId, attributes))
return res
}, new Set())
return Array.from(scaleSet).sort((a, b) => a - b)
}
+37 -6
View File
@@ -1,12 +1,43 @@
import { clearSelection } from '@/lib/chartHelper'
export default {
_migrate(installedVersion, inquiries) {
if (installedVersion === 1) {
inquiries.forEach(inquire => {
inquire.viewType = 'chart'
inquire.viewOptions = inquire.chart
delete inquire.chart
if (installedVersion < 2) {
inquiries.forEach(inquiry => {
inquiry.viewType = 'chart'
inquiry.viewOptions = inquiry.chart
delete inquiry.chart
})
return inquiries
}
if (installedVersion < 3) {
inquiries.forEach(inquiry => {
if (inquiry.viewType === 'graph') {
inquiry.viewOptions.style.nodes.color.opacity = 100
inquiry.viewOptions.style.highlightMode = 'node_and_neighbors'
}
})
}
if (installedVersion < 4) {
inquiries.forEach(inquiry => {
if (
inquiry.viewType === 'graph' &&
inquiry.viewOptions.layout.type === 'forceAtlas2'
) {
inquiry.viewOptions.layout.options.initialAlgorithm = 'circular'
}
})
}
if (installedVersion < 5) {
inquiries.forEach(inquiry => {
if (inquiry.viewType === 'chart') {
clearSelection(inquiry.viewOptions.data, inquiry.viewOptions.layout)
}
})
}
return inquiries
}
}
+22 -8
View File
@@ -4,11 +4,14 @@ import events from '@/lib/utils/events'
import migration from './_migrations'
const migrate = migration._migrate
const myInquiriesKey = 'myInquiries'
const latestVersion = 5
export default {
version: 2,
version: latestVersion,
myInquiriesKey,
getStoredInquiries() {
let myInquiries = JSON.parse(localStorage.getItem('myInquiries'))
let myInquiries = JSON.parse(localStorage.getItem(myInquiriesKey))
if (!myInquiries) {
const oldInquiries = localStorage.getItem('myQueries')
if (oldInquiries) {
@@ -19,14 +22,21 @@ export default {
return []
}
return (myInquiries && myInquiries.inquiries) || []
if (myInquiries.version < latestVersion) {
myInquiries = migrate(myInquiries.version, myInquiries.inquiries)
this.updateStorage(myInquiries)
return myInquiries
}
return myInquiries.inquiries || []
},
duplicateInquiry(baseInquiry) {
const newInquiry = JSON.parse(JSON.stringify(baseInquiry))
newInquiry.name = newInquiry.name + ' Copy'
newInquiry.id = nanoid()
newInquiry.createdAt = new Date()
newInquiry.createdAt = new Date().toJSON()
newInquiry.updatedAt = new Date().toJSON()
delete newInquiry.isPredefined
return newInquiry
@@ -38,7 +48,7 @@ export default {
updateStorage(inquiries) {
localStorage.setItem(
'myInquiries',
myInquiriesKey,
JSON.stringify({ version: this.version, inquiries })
)
},
@@ -60,6 +70,8 @@ export default {
// Turn data into array if they are not
inquiryList = !Array.isArray(inquiries) ? [inquiries] : inquiries
inquiryList = migrate(1, inquiryList)
} else if (inquiries.version < latestVersion) {
inquiryList = migrate(inquiries.version, inquiries.inquiries)
} else {
inquiryList = inquiries.inquiries || []
}
@@ -79,11 +91,11 @@ export default {
importInquiries() {
return fu.importFile().then(str => {
const inquires = this.deserialiseInquiries(str)
const inquiries = this.deserialiseInquiries(str)
events.send('inquiry.import', inquires.length)
events.send('inquiry.import', inquiries.length)
return inquires
return inquiries
})
},
export(inquiryList, fileName) {
@@ -99,6 +111,8 @@ export default {
if (!data.version) {
return data.length > 0 ? migrate(1, data) : []
} else if (data.version < latestVersion) {
return migrate(data.version, data.inquiries)
} else {
return data.inquiries
}
+1
View File
@@ -28,6 +28,7 @@ export default class Tab {
this.isSaved = !!inquiry.id
this.state = state
this.updatedAt = inquiry.updatedAt
}
async execute() {
+2
View File
@@ -5,6 +5,7 @@ import store from '@/store'
import { createVfm, VueFinalModal, useVfm } from 'vue-final-modal'
import '@/assets/styles/variables.css'
import '@/assets/styles/typography.css'
import '@/assets/styles/buttons.css'
import '@/assets/styles/tables.css'
import '@/assets/styles/dialogs.css'
@@ -12,6 +13,7 @@ import '@/assets/styles/tooltips.css'
import '@/assets/styles/messages.css'
import 'vue-multiselect/dist/vue-multiselect.css'
import '@/assets/styles/multiselect.css'
import '@/assets/icons.css'
import 'vue-final-modal/style.css'
if (!['localhost', '127.0.0.1'].includes(location.hostname)) {
+3 -3
View File
@@ -1,13 +1,13 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import Workspace from '@/views/MainView/Workspace'
import Inquiries from '@/views/MainView/Inquiries'
import Workspace from '@/views/Workspace'
import Inquiries from '@/views/Inquiries'
import Welcome from '@/views/Welcome'
import MainView from '@/views/MainView'
import LoadView from '@/views/LoadView'
import store from '@/store'
import database from '@/lib/database'
const routes = [
export const routes = [
{
path: '/',
name: 'Welcome',
+12 -7
View File
@@ -17,28 +17,33 @@ export default {
},
async saveInquiry({ state }, { inquiryTab, newName }) {
const value = {
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
id: inquiryTab.isPredefined || newName ? nanoid() : inquiryTab.id,
query: inquiryTab.query,
viewType: inquiryTab.dataView.mode,
viewOptions: inquiryTab.dataView.getOptionsForSave(),
name: newName || inquiryTab.name
name: newName || inquiryTab.name,
updatedAt: new Date().toJSON()
}
// Get inquiries from local storage
const myInquiries = state.inquiries
let inquiryIndex
// Set createdAt
if (newName) {
value.createdAt = new Date()
value.createdAt = new Date().toJSON()
} else {
var inquiryIndex = myInquiries.findIndex(
inquiryIndex = myInquiries.findIndex(
oldInquiry => oldInquiry.id === inquiryTab.id
)
value.createdAt = myInquiries[inquiryIndex].createdAt
value.createdAt =
inquiryIndex !== -1
? myInquiries[inquiryIndex].createdAt
: new Date().toJSON()
}
// Insert in inquiries list
if (newName) {
if (newName || inquiryIndex === -1) {
myInquiries.push(value)
} else {
myInquiries.splice(inquiryIndex, 1, value)
+5 -1
View File
@@ -7,7 +7,8 @@ export default {
},
updateTab(state, { tab, newValues }) {
const { name, id, query, viewType, viewOptions, isSaved } = newValues
const { name, id, query, viewType, viewOptions, isSaved, updatedAt } =
newValues
const oldId = tab.id
if (id && state.currentTabId === oldId) {
@@ -36,6 +37,9 @@ export default {
// Saved inquiry is not predefined
delete tab.isPredefined
}
if (updatedAt) {
tab.updatedAt = updatedAt
}
},
deleteTab(state, tab) {
+21 -17
View File
@@ -3,7 +3,8 @@ export default {
return {
tooltipStyle: {
visibility: 'hidden'
}
},
showTooltipTimeout: null
}
},
computed: {
@@ -13,27 +14,30 @@ export default {
},
methods: {
showTooltip(e, tooltipPosition) {
const position = tooltipPosition
? tooltipPosition.split('-')
: ['top', 'right']
const offset = 12
this.showTooltipTimeout = setTimeout(() => {
const position = tooltipPosition
? tooltipPosition.split('-')
: ['top', 'right']
const offset = 12
if (position[0] === 'top') {
this.tooltipStyle.top = e.clientY - offset + 'px'
} else {
this.tooltipStyle.top = e.clientY + offset + 'px'
}
if (position[0] === 'top') {
this.tooltipStyle.top = e.clientY - offset + 'px'
} else {
this.tooltipStyle.top = e.clientY + offset + 'px'
}
if (position[1] === 'right') {
this.tooltipStyle.left = e.clientX + offset + 'px'
} else {
this.tooltipStyle.left =
e.clientX - offset - this.tooltipElement.offsetWidth + 'px'
}
if (position[1] === 'right') {
this.tooltipStyle.left = e.clientX + offset + 'px'
} else {
this.tooltipStyle.left =
e.clientX - offset - this.tooltipElement.offsetWidth + 'px'
}
this.tooltipStyle.visibility = 'visible'
this.tooltipStyle.visibility = 'visible'
}, 500)
},
hideTooltip() {
this.showTooltipTimeout && clearTimeout(this.showTooltipTimeout)
this.tooltipStyle.visibility = 'hidden'
}
}
@@ -115,18 +115,35 @@
{{ createdAtFormatted(inquiry.createdAt) }}
</div>
<div class="icons-container">
<rename-icon
<IconButton
v-if="!inquiry.isPredefined"
icon="myicon myicon-rename"
tooltip="Rename inquiry"
data-test="rename-btn"
@click="showRenameDialog(inquiry.id)"
/>
<copy-icon @click="duplicateInquiry(index)" />
<export-icon
<IconButton
icon="myicon myicon-copy"
tooltip="Duplicate inquiry"
data-test="copy-btn"
@click="duplicateInquiry(index)"
/>
<IconButton
icon="myicon myicon-export"
tooltip="Export inquiry to file"
tooltipPosition="top-left"
data-test="export-btn"
@click="exportToFile([inquiry], `${inquiry.name}.json`)"
/>
<delete-icon
<IconButton
v-if="!inquiry.isPredefined"
icon="myicon myicon-delete"
tooltip="Delete inquiry"
tooltipPosition="top-left"
data-test="delete-btn"
@click="showDeleteDialog(new Set().add(inquiry.id))"
/>
</div>
@@ -143,7 +160,12 @@
<modal modalId="rename" class="dialog" contentStyle="width: 560px;">
<div class="dialog-header">
Rename inquiry
<close-icon @click="$modal.hide('rename')" />
<IconButton
icon="myicon myicon-close"
variant="secondary"
:iconSize="14"
@click="$modal.hide('rename')"
/>
</div>
<div class="dialog-body">
<text-field
@@ -163,7 +185,12 @@
<modal modalId="delete" class="dialog" contentStyle="width: 480px;">
<div class="dialog-header">
Delete {{ deleteGroup ? 'inquiries' : 'inquiry' }}
<close-icon @click="$modal.hide('delete')" />
<IconButton
icon="myicon myicon-close"
variant="secondary"
:iconSize="14"
@click="$modal.hide('delete')"
/>
</div>
<div class="dialog-body">
{{ deleteDialogMsg }}
@@ -184,14 +211,10 @@
</template>
<script>
import RenameIcon from './svg/rename'
import CopyIcon from './svg/copy'
import ExportIcon from '@/components/svg/export'
import DeleteIcon from './svg/delete'
import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField'
import CheckBox from '@/components/CheckBox'
import LoadingIndicator from '@/components/LoadingIndicator'
import IconButton from '@/components/Common/IconButton.vue'
import TextField from '@/components/Common/TextField'
import CheckBox from '@/components/Common/CheckBox'
import LoadingIndicator from '@/components/Common/LoadingIndicator'
import tooltipMixin from '@/tooltipMixin'
import storedInquiries from '@/lib/storedInquiries'
import eventBus from '@/lib/eventBus'
@@ -199,11 +222,7 @@ import eventBus from '@/lib/eventBus'
export default {
name: 'Inquiries',
components: {
RenameIcon,
CopyIcon,
ExportIcon,
DeleteIcon,
CloseIcon,
IconButton,
TextField,
CheckBox,
LoadingIndicator
@@ -609,6 +628,7 @@ table.sqliteviz-table {
}
.sqliteviz-table tbody tr:hover .icons-container {
display: flex;
gap: 8px;
}
.dialog input {
width: 100%;
+1 -1
View File
@@ -15,7 +15,7 @@
<script>
import fu from '@/lib/utils/fileIo'
import database from '@/lib/database'
import Logs from '@/components/Logs'
import Logs from '@/components/Common/Logs'
import events from '@/lib/utils/events'
export default {
@@ -10,7 +10,7 @@
</template>
<script>
import MainMenu from './MainMenu'
import MainMenu from '@/components/MainMenu'
import '@/assets/styles/scrollbars.css'
export default {
-53
View File
@@ -1,53 +0,0 @@
<template>
<span>
<svg
class="icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
@click.stop="onClick"
@mouseenter="showTooltip"
@mouseleave="hideTooltip"
>
<path
d="M14.25 15.75H6V5.25H14.25V15.75ZM14.25 3.75H6C5.60218 3.75 5.22064 3.90804 4.93934
4.18934C4.65804 4.47064 4.5 4.85218 4.5 5.25V15.75C4.5 16.1478 4.65804 16.5294 4.93934
16.8107C5.22064 17.092 5.60218 17.25 6 17.25H14.25C14.6478 17.25 15.0294 17.092 15.3107
16.8107C15.592 16.5294 15.75 16.1478 15.75 15.75V5.25C15.75 4.85218 15.592 4.47064
15.3107 4.18934C15.0294 3.90804 14.6478 3.75 14.25 3.75ZM12 0.75H3C2.60218 0.75 2.22064
0.908035 1.93934 1.18934C1.65804 1.47064 1.5 1.85218 1.5 2.25V12.75H3V2.25H12V0.75Z"
fill="#A2B1C6"
/>
</svg>
<span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
Duplicate inquiry
</span>
</span>
</template>
<script>
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'CopyIcon',
mixins: [tooltipMixin],
emits: ['click'],
methods: {
onClick() {
this.hideTooltip()
this.$emit('click')
}
}
}
</script>
<style scoped>
.icon {
display: block;
margin: 0 12px;
}
.icon:hover path {
fill: var(--color-accent);
}
</style>

Some files were not shown because too many files have changed in this diff Show More