1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2026-02-04 15:38:55 +08:00

Compare commits

..

78 Commits

Author SHA1 Message Date
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
lana-k
3ee825defe fix standart chart resize in pivot, improve performance 2025-04-03 22:36:50 +02:00
lana-k
77df3a8446 use only camel case for props 2025-03-30 21:01:06 +02:00
lana-k
559e04200c fix config name 2025-03-30 16:40:43 +02:00
lana-k
4568780526 fix release action 2025-03-30 16:33:23 +02:00
lana-k
fa9108bc08 add source maps 2025-03-30 16:23:51 +02:00
lana-k
df16383d49 linter 2025-03-30 15:57:47 +02:00
lana-k
6f7961e1b4 #63 update node and npm 2025-03-30 15:13:55 +02:00
lana-k
2741aa6f33 add titles, align row title to the left 2025-03-30 15:13:36 +02:00
lana-k
6ceac83db9 update version 2025-03-29 17:32:33 +01:00
lana-k
a46625ebe7 #122 add tests 2025-03-29 17:09:33 +01:00
lana-k
5ef0b32549 #63 test for lot resize 2025-03-29 16:09:33 +01:00
lana-k
f49fa0ea96 #63 test for chart updating 2025-03-29 13:32:20 +01:00
lana-k
108ae454c1 #122 add line wrapping 2025-03-26 22:18:55 +01:00
lana-k
43b6110c28 minimize column name cell in record view 2025-03-26 21:50:55 +01:00
lana-k
5a805fba80 fix plot update 2025-03-26 20:59:17 +01:00
lana-k
58cdab94c1 fix styles 2025-03-25 21:19:22 +01:00
lana-k
b3d81666be add more chunks 2025-03-25 21:18:23 +01:00
lana-k
fdf180d340 fix plot resize 2025-03-23 21:09:12 +01:00
lana-k
f2ff5aa2af slot comment 2025-03-20 22:17:16 +01:00
lana-k
0c1b91ab2f format 2025-03-20 22:04:15 +01:00
lana-k
5e2b34a856 fix codemirror styles 2025-03-17 21:43:07 +01:00
lana-k
24786c9069 add service worker 2025-03-16 23:04:03 +01:00
lana-k
c28d31b019 fix tests 2025-03-11 22:24:57 +01:00
lana-k
6009ebb447 fix resize 2025-03-09 22:26:53 +01:00
lana-k
b5504b91ce fix tests 2025-03-09 21:57:36 +01:00
lana-k
828cad6439 migrate to vite 2025-02-01 20:54:26 +01:00
lana-k
8fa3c2ae58 fix tests 2025-01-23 21:36:11 +01:00
lana-k
aa5c907095 Merge branch 'master' of github.com:lana-k/sqliteviz into migrate_to_vue3 2025-01-13 22:19:00 +01:00
lana-k
e14696b59e update tests 2025-01-05 17:13:55 +01:00
lana-k
eee67763b5 plotly in pivot 2024-12-10 21:41:07 +01:00
lana-k
637d8d26dd #63 migrate to Vue 3 2024-10-13 16:12:21 +02:00
lana-k
b30b2181e4 #63 update slot syntax 2024-10-05 15:43:22 +02:00
199 changed files with 24889 additions and 41677 deletions

29
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,29 @@
module.exports = {
root: true,
env: {
node: true,
es2022: true
},
extends: ['eslint:recommended', 'plugin:vue/vue3-recommended', 'prettier'],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-case-declarations': 'off',
'max-len': [2, 100, 4, { ignoreUrls: true }],
'vue/multi-word-component-names': 'off',
'vue/no-mutating-props': 'warn',
'vue/no-reserved-component-names': 'warn',
'vue/no-v-model-argument': 'off',
'vue/require-default-prop': 'off',
'vue/custom-event-name-casing': ['error', 'camelCase'],
'vue/attribute-hyphenation': ['error', 'never']
},
overrides: [
{
files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/**/*.spec.{j,t}s?(x)'],
env: {
mocha: true
}
}
]
}

View File

@@ -1,30 +0,0 @@
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/essential',
'@vue/standard'
],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-case-declarations': 'off',
'max-len': [2, 100, 4, { ignoreUrls: true }]
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/**/*.spec.{j,t}s?(x)'
],
env: {
mocha: true
}
}
]
}

14
.github/workflows/config.grenrc.cjs vendored Normal file
View File

@@ -0,0 +1,14 @@
module.exports = {
dataSource: 'milestones',
ignoreIssuesWith: ['wontfix', 'duplicate'],
milestoneMatch: 'v{{tag_name}}',
template: {
issue: '- {{name}} [{{text}}]({{url}})',
changelogTitle: '',
release: '{{body}}'
},
groupBy: {
Enhancements: ['enhancement', 'internal'],
'Bug fixes': ['bug']
}
}

View File

@@ -1,17 +0,0 @@
module.exports = {
dataSource: 'milestones',
ignoreIssuesWith: [
'wontfix',
'duplicate'
],
milestoneMatch: 'v{{tag_name}}',
template: {
issue: '- {{name}} [{{text}}]({{url}})',
changelogTitle: "",
release: "{{body}}",
},
groupBy: {
'Enhancements': ["enhancement", "internal"],
'Bug fixes': ["bug"]
}
}

View File

@@ -3,43 +3,43 @@ on:
workflow_dispatch:
push:
tags:
- '*'
- '*'
jobs:
deploy:
name: Create release
runs-on: ubuntu-latest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 16.x
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 18.x
- name: Update npm
run: npm install -g npm@8
- name: Update npm
run: npm install -g npm@10
- name: npm install and build
run: |
npm install
npm run build
- name: npm install and build
run: |
npm install
npm run build
- name: Create archives
run: |
cd dist
zip -9 -r ../dist.zip . -x "js/*.map" -x "/*.map"
zip -9 -r ../dist_map.zip .
- name: Create archives
run: |
cd dist
zip -9 -r ../dist.zip . -x "*.map"
zip -9 -r ../dist_map.zip .
- name: Create Release Notes
run: |
npm install github-release-notes@0.16.0 -g
gren changelog --generate --config="/.github/workflows/config.grenrc.js"
env:
GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Release Notes
run: |
npm install github-release-notes@0.16.0 -g
gren changelog --generate --config="/.github/workflows/config.grenrc.cjs"
env:
GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create release
uses: ncipollo/release-action@v1
with:
artifacts: "dist.zip,dist_map.zip"
token: ${{ secrets.GITHUB_TOKEN }}
bodyFile: "CHANGELOG.md"
- name: Create release
uses: ncipollo/release-action@v1
with:
artifacts: 'dist.zip,dist_map.zip'
token: ${{ secrets.GITHUB_TOKEN }}
bodyFile: 'CHANGELOG.md'

View File

@@ -3,35 +3,39 @@ on:
workflow_dispatch:
push:
branches:
- 'master'
- 'master'
pull_request:
branches:
- 'master'
- 'master'
jobs:
test:
name: Run tests
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 16.x
- name: Install browsers
run: |
export DEBIAN_FRONTEND=noninteractive
sudo apt-get update
sudo apt-get install -y chromium-browser firefox
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 18.x
- 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-esr \
xvfb
- name: Update npm
run: npm install -g npm@8
- name: Update npm
run: npm install -g npm@10
- name: Install the project
run: npm install
- name: Install the project
run: npm install
- name: Run lint
run: npm run lint -- --no-fix
- name: Run lint
run: npm run lint -- --no-fix
- name: Run karma tests
run: npm run test
- name: Run karma tests
run: xvfb-run -a npm test

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"trailingComma": "none",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"arrowParens": "avoid"
}

View File

@@ -3,7 +3,7 @@
# docker build -t sqliteviz/test -f Dockerfile.test .
#
FROM node:12.22-buster
FROM node:12.22-bullseye
RUN set -ex; \
apt update; \

View File

@@ -5,10 +5,11 @@
# sqliteviz
Sqliteviz is a single-page offline-first PWA for fully client-side visualisation
of SQLite databases, CSV, JSON or NDJSON files.
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
@@ -19,16 +20,22 @@ With sqliteviz you can:
https://user-images.githubusercontent.com/24638357/128249848-f8fab0f5-9add-46e0-a9c1-dd5085a8623e.mp4
## Quickstart
The latest release of sqliteviz is deployed on [sqliteviz.com/app][6].
## Wiki
For user documentation, check out sqliteviz [documentation][7].
## Motivation
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
@@ -42,3 +49,5 @@ It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4]
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
[11]: https://github.com/plotly/plotly.js
[12]: https://github.com/nicolaskruchten/pivottable
[13]: https://www.sigmajs.org/
[14]: https://graphology.github.io/

View File

@@ -1,5 +1,3 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
presets: ['@vue/cli-plugin-babel/preset']
}

View File

@@ -1,12 +1,12 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.png">
<link rel="manifest" href="<%= BASE_URL %>manifest.webmanifest">
<title><%= htmlWebpackPlugin.options.title %></title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="favicon.png" />
<link rel="manifest" href="manifest.webmanifest" />
<title>sqliteviz</title>
<style>
#sqliteviz-loading-wrapper {
position: fixed;
@@ -16,7 +16,7 @@
top: 0;
background-color: white;
}
#sqliteviz-loading-text {
display: block;
position: absolute;
@@ -27,7 +27,7 @@
font-family: sans-serif;
font-size: 20px;
}
#sqliteviz-loading-wrapper svg {
display: block;
position: absolute;
@@ -38,15 +38,18 @@
#sqliteviz-loading-wrapper circle {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
left: 0;
right: 0;
top: 0;
bottom: 0;
fill: none;
stroke-width: 5px;
stroke-linecap: round;
stroke: #119DFF;
stroke: #119dff;
}
#sqliteviz-loading-wrapper circle.bg {
stroke: #C8D4E3;
stroke: #c8d4e3;
}
#sqliteviz-loading-wrapper circle.front {
@@ -74,29 +77,22 @@
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
<strong>
We're sorry but this app doesn't work properly without JavaScript
enabled. Please enable it to continue.
</strong>
</noscript>
<div id="app">
<div id="sqliteviz-loading-wrapper">
<div id="sqliteviz-loading-text">LOADING</div>
<svg height="170" width="170" viewBox="0 0 170 170">
<circle
class="bg"
cx="85"
cy="85"
r="80"
/>
<circle
class="front"
cx="85"
cy="85"
r="80"
/>
<circle class="bg" cx="85" cy="85" r="80" />
<circle class="front" cx="85" cy="85" r="80" />
</svg>
</div>
</div>
<!-- extention slot start -->
<!-- extention slot end -->
<!-- built files will be auto injected -->
<script type="module" src="/src/main.js"></script>
</body>
</html>

10
jsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"include": ["src/**/*", "tests/**/*"],
"exclude": ["node_modules", "dist"],
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@\/*": ["./src/*"]
}
}
}

122
karma.conf.cjs Normal file
View File

@@ -0,0 +1,122 @@
module.exports = function (config) {
config.set({
vite: {
config: {
resolve: {
alias: {
vue: 'vue/dist/vue.esm-bundler.js'
}
},
server: {
preTransformRequests: false
}
},
coverage: {
enable: true,
include: 'src/*',
exclude: ['node_modules', 'src/components/svg/*'],
extension: ['.js', '.vue'],
requireEnv: false
}
},
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['mocha', 'sinon-chai', 'vite'],
// list of files / patterns to load in the browser
files: [
{
pattern: 'test.setup.js',
type: 'module',
watched: false,
served: false
},
{
pattern: 'tests/**/*.spec.js',
type: 'module',
watched: false,
served: false
},
{
pattern: 'src/assets/styles/*.css',
type: 'css',
watched: false,
served: false
}
],
plugins: [
'karma-vite',
'karma-mocha',
'karma-sinon-chai',
'karma-firefox-launcher',
'karma-chrome-launcher',
'karma-spec-reporter',
'karma-coverage'
],
// list of files / patterns to exclude
exclude: [],
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['spec', 'coverage'],
coverageReporter: {
dir: 'coverage',
reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }]
},
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN ||
// config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
customLaunchers: {
FirefoxHeadlessTouch: {
base: 'FirefoxHeadless',
prefs: {
'dom.w3c_touch_events.enabled': 1,
'dom.events.asyncClipboard.clipboardItem': true
}
}
},
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['ChromiumHeadless', 'FirefoxHeadlessTouch'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// Concurrency level
// how many browser should be started simultaneous
concurrency: 2,
client: {
captureConsole: true,
mocha: {
timeout: 7000
}
},
browserConsoleLogOptions: {
terminal: true,
level: ''
}
})
// Fix the timezone
process.env.TZ = 'Europe/Amsterdam'
}

View File

@@ -1,203 +0,0 @@
// Karma configuration
'use strict'
const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
function resolve (dir) {
return path.join(__dirname, dir)
}
module.exports = function (config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['mocha', 'sinon-chai'],
// list of files / patterns to load in the browser
files: [
'./karma.files.js',
{
pattern: 'node_modules/sql.js/dist/sql-wasm.wasm',
watched: false,
included: false,
served: true,
nocache: false
}
],
// list of files / patterns to exclude
exclude: [],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'./karma.files.js': ['webpack']
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['spec', 'coverage'],
coverageReporter: {
dir: 'coverage',
reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }]
},
// !!DONOT delete this reporter, or vue-cli-addon-ui-karma doesnot work
jsonResultReporter: {
outputFile: 'report/karma-result.json',
isSynchronous: true
},
junitReporter: {
outputDir: 'report', // results will be saved as $outputDir/$browserName.xml
// if included, results will be saved as $outputDir/$browserName/$outputFile
outputFile: undefined,
suite: '', // suite will become the package name attribute in xml testsuite element
useBrowserName: true, // add browser name to report and classes names
// function (browser, result) to customize the name attribute in xml testcase element
nameFormatter: undefined,
// function (browser, result) to customize the classname attribute in xml testcase element
classNameFormatter: undefined,
properties: {} // key value pairs add to the <properties> section of the report
},
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN ||
// config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
customLaunchers: {
FirefoxHeadlessTouch: {
base: 'FirefoxHeadless',
prefs: {
'dom.w3c_touch_events.enabled': 1,
'dom.events.asyncClipboard.clipboardItem': true
}
}
},
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['ChromiumHeadless', 'FirefoxHeadlessTouch'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// Concurrency level
// how many browser should be started simultaneous
concurrency: 2,
client: {
captureConsole: true,
mocha: {
timeout: 7000
}
},
browserConsoleLogOptions: {
terminal: true,
level: ''
},
webpack: {
mode: 'development',
entry: './src/main.js',
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
vue$: 'vue/dist/vue.esm.js',
'@': resolve('src')
}
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: [
{
loader: 'babel-loader'
}
]
},
{
test: /\.js$/,
include: /src/,
exclude: /(node_modules|bower_components|\.spec\.js$)/,
use: [
{
loader: 'istanbul-instrumenter-loader',
options: {
esModules: true
}
}
]
},
{
test: /worker\.js$/,
loader: 'worker-loader'
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader'
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
js: 'babel-loader'
},
postLoaders: {
js: 'istanbul-instrumenter-loader?esModules=true'
}
}
},
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader']
},
{
test: /\.scss$/,
use: ['vue-style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: resolve('fonts/[name].[hash:7].[ext]')
}
}
]
},
plugins: [new VueLoaderPlugin()],
node: {
fs: 'empty'
}
},
webpackMiddleware: {
watchOptions: {
ignored: /node_modules/
}
},
proxies: {
'/_karma_webpack_/sql-wasm.wasm': '/base/node_modules/sql.js/dist/sql-wasm.wasm',
'/base/sql-wasm.wasm': '/base/node_modules/sql.js/dist/sql-wasm.wasm'
}
})
// Fix the timezone
process.env.TZ = 'Europe/Amsterdam'
}

View File

@@ -1,21 +0,0 @@
import Vue from 'vue'
import { VuePlugin } from 'vuera'
import VModal from 'vue-js-modal'
Vue.use(VuePlugin)
Vue.use(VModal)
Vue.config.productionTip = false
// require all test files (files that ends with .spec.js)
const testsContext = require.context('./tests', true, /\.spec.js$/)
// Read more about why we need to call testContext:
// https://www.npmjs.com/package/require-context#context-api
testsContext.keys().forEach(testsContext)
// require all src files except main.js and router/index.js for coverage.
// you can also change this to match only the subset of files that
// you want coverage for.
// We don't include router/index.js to avoid installing VueRouter globally in tests
const srcContext = require.context('./src', true, /^\.\/(?!(main|(router(\/)?(index)?))(\.js)?$)/)
srcContext.keys().forEach(srcContext)

View File

@@ -5,7 +5,7 @@ a custom version of [sql.js][1]. It allows sqliteviz to have more recent
version of SQLite build with a number of useful extensions.
`Makefile` from [sql.js][1] is rewritten as more comprehensible `configure.py`
and `build.py` Python scripts that run in `emscripten/emsdk` Docker container.
and `build.py` Python scripts that run in `emscripten/emsdk` Docker container.
## Extension
@@ -84,15 +84,15 @@ described in [this message from SQLite Forum][12]:
> amalgamation code and the extensions would thereafter be automatically
> initialized on each connection.
[1]: https://github.com/sql-js/sql.js
[2]: https://sqlite.org/amalgamation.html
[3]: https://sqlite.org/src/dir?ci=trunk&name=ext/misc
[4]: https://sqlite.org/fts5.html
[5]: https://github.com/jakethaw/pivot_vtab
[6]: https://sqlite.org/series.html
[7]: https://sqlite.org/src/file/ext/misc/series.c
[8]: https://sqlite.org/src/file/ext/misc/closure.c
[9]: https://sqlite.org/src/file/ext/misc/uuid.c
[1]: https://github.com/sql-js/sql.js
[2]: https://sqlite.org/amalgamation.html
[3]: https://sqlite.org/src/dir?ci=trunk&name=ext/misc
[4]: https://sqlite.org/fts5.html
[5]: https://github.com/jakethaw/pivot_vtab
[6]: https://sqlite.org/series.html
[7]: https://sqlite.org/src/file/ext/misc/series.c
[8]: https://sqlite.org/src/file/ext/misc/closure.c
[9]: https://sqlite.org/src/file/ext/misc/uuid.c
[10]: https://sqlite.org/src/file/ext/misc/regexp.c
[11]: https://charlesleifer.com/blog/querying-tree-structures-in-sqlite-using-python-and-the-transitive-closure-extension/
[12]: https://sqlite.org/forum/forumpost/6ad7d4f4bebe5e06?raw

View File

@@ -1,12 +1,15 @@
module.exports = function (config) {
const timeout = 15 * 60 * 1000
config.set({
frameworks: ['mocha'],
files: [
'suite.js',
{ pattern: 'node_modules/sql.js/dist/sql-wasm.wasm', served: true, included: false },
{
pattern: 'node_modules/sql.js/dist/sql-wasm.wasm',
served: true,
included: false
},
{ pattern: 'sample.csv', served: true, included: false }
],
@@ -15,7 +18,10 @@ module.exports = function (config) {
singleRun: true,
customLaunchers: {
ChromiumHeadlessNoSandbox: { base: 'ChromiumHeadless', flags: ['--no-sandbox'] }
ChromiumHeadlessNoSandbox: {
base: 'ChromiumHeadless',
flags: ['--no-sandbox']
}
},
browsers: ['ChromiumHeadlessNoSandbox', 'FirefoxHeadless'],
concurrency: 1,
@@ -33,11 +39,11 @@ module.exports = function (config) {
logLevel: config.LOG_INFO,
browserConsoleLogOptions: { terminal: true, level: config.LOG_INFO },
preprocessors: { 'suite.js': [ 'webpack' ] },
preprocessors: { 'suite.js': ['webpack'] },
webpack: {
mode: 'development',
module: {
noParse: [ __dirname + '/node_modules/benchmark/benchmark.js' ]
noParse: [__dirname + '/node_modules/benchmark/benchmark.js']
},
node: { fs: 'empty' }
},
@@ -47,6 +53,5 @@ module.exports = function (config) {
},
jsonToFileReporter: { outputPath: '.', fileName: 'suite-result.json' }
})
}

View File

@@ -2,7 +2,7 @@
"name": "sqlite-webassembly-microbenchmark",
"private": true,
"dependencies": {
"@babel/core" : "^7.14.8",
"@babel/core": "^7.14.8",
"babel-loader": "^8.2.2",
"benchmark": "^2.1.4",
"lodash": "^4.17.4",
@@ -11,7 +11,7 @@
"karma": "^6.3.4",
"karma-chrome-launcher": "^3.1.0",
"karma-firefox-launcher": "^2.1.1",
"karma-json-to-file-reporter" : "^1.0.1",
"karma-json-to-file-reporter": "^1.0.1",
"karma-mocha": "^2.0.1",
"karma-webpack": "^4.0.2",
"webpack": "^4.46.0",

View File

@@ -4,7 +4,6 @@ import lodash from 'lodash'
import Papa from 'papaparse'
import useragent from 'ua-parser-js'
describe('SQLite build benchmark', function () {
let parsedCsv
let sqlModule
@@ -18,7 +17,7 @@ describe('SQLite build benchmark', function () {
importToTable(selectDb, parsedCsv)
})
function benchmarkImport () {
function benchmarkImport() {
const db = new sqlModule.Database()
try {
importToTable(db, parsedCsv)
@@ -27,7 +26,7 @@ describe('SQLite build benchmark', function () {
}
}
function benchmarkSelect () {
function benchmarkSelect() {
const result = selectDb.exec(`
SELECT county, AVG(avg_depth) avg_depth_c
FROM (
@@ -50,11 +49,9 @@ describe('SQLite build benchmark', function () {
suite.add('select', { initCount: 3, minSamples: 50, fn: benchmarkSelect })
await run(suite)
})
})
function importToTable (db, parsedCsv, chunkSize = 1024) {
function importToTable(db, parsedCsv, chunkSize = 1024) {
const columnListString = parsedCsv.meta.fields.join(', ')
db.exec(`CREATE TABLE csv_import(${columnListString})`)
@@ -67,7 +64,6 @@ function importToTable (db, parsedCsv, chunkSize = 1024) {
})
}
class PromiseWrapper {
constructor() {
this.promise = new Promise((resolve, reject) => {
@@ -89,11 +85,11 @@ function parseCsv(url) {
})
}
function chunkArray (arr, size) {
function chunkArray(arr, size) {
return arr.reduce(function (result, value, index) {
const chunkIndex = Math.floor(index / size)
if(!(chunkIndex in result)) {
if (!(chunkIndex in result)) {
result[chunkIndex] = []
}
result[chunkIndex].push(value)
@@ -102,8 +98,7 @@ function chunkArray (arr, size) {
}, [])
}
function createSuite () {
function createSuite() {
// Combined workaround from:
// - https://github.com/bestiejs/benchmark.js/issues/106
// - https://github.com/bestiejs/benchmark.js/issues/237
@@ -117,24 +112,26 @@ function createSuite () {
return new bm.Suite()
}
function run (suite) {
function run(suite) {
const suiteResult = new PromiseWrapper()
suite
.on('cycle', function (event) {
console.info(String(event.target))
})
.on('complete', function () {
console.log(JSON.stringify({
browser: useragent(navigator.userAgent).browser,
result: this.filter('successful')
}))
console.log(
JSON.stringify({
browser: useragent(navigator.userAgent).browser,
result: this.filter('successful')
})
)
suiteResult.resolve()
})
.on('error', function (event) {
console.error('Benchmark failed', String(event.target))
suiteResult.reject()
})
.run({async: true})
.run({ async: true })
return suiteResult.promise
}

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
# ===========================================

File diff suppressed because one or more lines are too long

Binary file not shown.

45324
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,27 @@
{
"name": "sqliteviz",
"version": "0.25.1",
"version": "0.28.0",
"license": "Apache-2.0",
"private": true,
"type": "module",
"scripts": {
"serve": "vue-cli-service serve",
"build": "NODE_OPTIONS=--max_old_space_size=4096 vue-cli-service build",
"test": "vue-cli-service karma",
"lint": "vue-cli-service lint"
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"test": "karma start karma.conf.cjs",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
"format": "prettier . --write"
},
"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",
@@ -23,41 +32,54 @@
"react": "^16.14.0",
"react-chart-editor": "^0.46.1",
"react-dom": "^16.14.0",
"seedrandom": "^3.0.5",
"sigma": "^3.0.1",
"sql.js": "file:./lib/sql-js",
"vue": "^2.6.11",
"vue-codemirror": "^4.0.6",
"vue-js-modal": "^2.0.0-rc.6",
"vue-multiselect": "^2.1.6",
"vue-router": "^3.2.0",
"vue2-teleport": "^1.0.1",
"vuejs-paginate": "^2.1.0",
"vuera": "^0.2.7",
"vuex": "^3.4.0"
"tiny-emitter": "^2.1.0",
"veaury": "^2.5.1",
"vue": "^3.5.11",
"vue-final-modal": "^4.5.5",
"vue-multiselect": "^3.0.0-beta.3",
"vue-router": "^4.4.5",
"vuejs-paginate-next": "^1.0.2",
"vuex": "^4.1.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.4.0",
"@vue/cli-plugin-eslint": "^4.4.0",
"@vue/cli-plugin-router": "^4.4.0",
"@vue/cli-plugin-vuex": "^4.4.0",
"@vue/cli-service": "^4.4.0",
"@vue/eslint-config-standard": "^5.1.2",
"@vue/test-utils": "^1.1.2",
"babel-eslint": "^10.1.0",
"@babel/core": "^7.25.7",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/eslint-config-standard": "^8.0.1",
"@vue/test-utils": "^2.4.6",
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"eslint": "^6.7.2",
"chai-as-promised": "^8.0.1",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^6.2.2",
"eslint-plugin-vue": "^9.28.0",
"flush-promises": "^1.0.2",
"karma": "^3.1.4",
"karma-firefox-launcher": "^2.1.0",
"karma-webpack": "^4.0.2",
"vue-cli-plugin-ui-karma": "^0.2.5",
"vue-template-compiler": "^2.6.11",
"workbox-webpack-plugin": "^6.1.5",
"worker-loader": "^3.0.8"
"karma": "^6.4.4",
"karma-coverage": "^2.2.1",
"karma-coverage-istanbul-reporter": "^3.0.3",
"karma-firefox-launcher": "^2.1.3",
"karma-mocha": "^1.3.0",
"karma-spec-reporter": "^0.0.36",
"karma-vite": "^1.0.5",
"mocha": "^5.2.0",
"prettier": "3.5.3",
"process": "^0.11.10",
"url-loader": "^4.1.1",
"vite": "^5.4.14",
"vite-plugin-istanbul": "^5.0.0",
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-pwa": "^0.21.1",
"vite-plugin-static-copy": "^2.2.0",
"vue-cli-plugin-ui-karma": "^0.2.5"
},
"overrides": {
"karma-vite": {
"vite-plugin-istanbul": "$vite-plugin-istanbul"
}
}
}

View File

@@ -1 +1 @@
[]
[]

View File

@@ -27,4 +27,4 @@
"name": "sqliteviz",
"short_name": "sqliteviz",
"start_url": "index.html"
}
}

View File

@@ -1,6 +1,6 @@
<template>
<div id="app">
<router-view/>
<router-view />
</div>
</template>
@@ -8,74 +8,38 @@
import storedInquiries from '@/lib/storedInquiries'
export default {
created () {
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
},
computed: {
inquiries () {
inquiries() {
return this.$store.state.inquiries
}
},
watch: {
inquiries: {
deep: true,
handler () {
handler() {
storedInquiries.updateStorage(this.inquiries)
}
}
},
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,
label,
button,
.plotly_editor * {
font-family: "Open Sans", Helvetica, Arial, sans-serif;
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@@ -59,5 +59,3 @@ button.secondary:disabled {
text-shadow: none;
cursor: default;
}

View File

@@ -1,6 +1,13 @@
.dialog {
display: flex;
justify-content: center;
align-items: center;
}
.dialog .vfm__content {
border-radius: var(--border-radius-big);
box-shadow: 0px 2px 9px rgba(80, 103, 132, 0.8);
background-color: white;
overflow: hidden;
}
.dialog-header {
@@ -16,7 +23,7 @@
}
.dialog-body {
min-height: 60px;
min-height: 56px;
background-color: var(--color-bg-light);
padding: 24px;
border-top: 1px solid var(--color-border-light);
@@ -35,6 +42,6 @@
margin-left: 16px;
}
.vm--overlay {
.vfm__overlay.vfm--overlay {
background-color: rgba(162, 177, 198, 0.5);
}

View File

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

View File

@@ -62,14 +62,14 @@
margin: 2px;
}
.sqliteviz-select .multiselect__tag-icon:after {
content: url('~@/assets/images/delete-tag.svg');
content: url('@/assets/images/delete-tag.svg');
height: 14px;
width: 14px;
}
.sqliteviz-select .multiselect__tag-icon:focus:after,
.sqliteviz-select .multiselect__tag-icon:hover:after {
content: url('~@/assets/images/delete-tag-hover.svg');
content: url('@/assets/images/delete-tag-hover.svg');
}
.sqliteviz-select .multiselect__tag-icon:focus,
@@ -102,7 +102,7 @@
}
.sqliteviz-select .multiselect__select:before {
content: url('~@/assets/images/arrow.svg');
content: url('@/assets/images/arrow.svg');
border: none;
top: 0;
}
@@ -116,14 +116,14 @@
}
.sqliteviz-select .multiselect__select:hover:before {
content: url('~@/assets/images/arrow-hover.svg');
content: url('@/assets/images/arrow-hover.svg');
}
.sqliteviz-select.multiselect--active .multiselect__tags {
border-radius: var(--border-radius-medium-2);
}
.sqliteviz-select .multiselect__option .no-results {
.sqliteviz-select .multiselect__option .no-results {
color: var(--color-text-light-2);
}
@@ -133,4 +133,4 @@
.sqliteviz-select.multiselect--disabled .multiselect__select {
background: unset;
}
}

View File

@@ -3,13 +3,13 @@
width: 5px;
height: 5px;
}
/* Track */
::-webkit-scrollbar-track {
background: transparent;
background: transparent;
border-radius: 5px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: var(--color-accent);

View File

@@ -37,7 +37,7 @@
height: calc(100% - 27px);
}
@supports (-moz-appearance:none) {
@supports (-moz-appearance: none) {
.header-container {
top: 0;
padding-left: 6px;
@@ -59,7 +59,8 @@ table.sqliteviz-table {
margin-top: -35px;
border-collapse: collapse;
}
.sqliteviz-table thead th, .fixed-header {
.sqliteviz-table thead th,
.fixed-header {
font-size: 14px;
font-weight: 600;
box-sizing: border-box;
@@ -71,7 +72,7 @@ table.sqliteviz-table {
}
.sqliteviz-table tbody td {
font-size: 13px;
background-color:white;
background-color: white;
color: var(--color-text-base);
box-sizing: border-box;
border-bottom: 1px solid var(--color-border-light);
@@ -108,8 +109,8 @@ table.sqliteviz-table {
color: var(--color-text-base);
}
.sqliteviz-table tbody td[data-isNull="true"],
.sqliteviz-table tbody td[data-isBlob="true"] {
.sqliteviz-table tbody td[data-isNull='true'],
.sqliteviz-table tbody td[data-isBlob='true'] {
color: var(--color-text-light-2);
font-style: italic;
}

View File

@@ -4,10 +4,10 @@
text-align: center;
font-size: 12px;
padding: 0 6px;
line-height: 19px;;
line-height: 19px;
position: fixed;
height: 19px;
border-radius: var(--border-radius-medium);
white-space: nowrap;
z-index: 999;
}
}

View File

@@ -0,0 +1,45 @@
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Regular.woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-SemiBold.woff2');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Bold.woff2');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Italic.woff2');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-SemiBoldItalic.woff2');
font-weight: 600;
font-style: italic;
}
@font-face {
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-BoldItalic.woff2');
font-weight: 700;
font-style: italic;
}
a {
color: var(--color-accent-shade);
}

View File

@@ -1,21 +1,19 @@
:root {
--color-white: #ffffff;
--color-gray-light: #F3F6FA;
--color-gray-light-2: #DFE8F3;
--color-gray-light-3: #C8D4E3;
--color-gray-light-4:#EBF0F8;
--color-gray-light-5:#f8f8f9;
--color-gray-medium: #A2B1C6;
--color-gray-light: #f3f6fa;
--color-gray-light-2: #dfe8f3;
--color-gray-light-3: #c8d4e3;
--color-gray-light-4: #ebf0f8;
--color-gray-light-5: #f8f8f9;
--color-gray-medium: #a2b1c6;
--color-gray-dark: #506784;
--color-blue-medium: #119DFF;
--color-blue-dark: #0D76BF;
--color-blue-dark-2: #2A3F5F;
--color-red: #EF553B;
--color-red-2: #DE350B;
--color-red-light: #FFBDAD;
--color-yellow: #FBEFCB;
--color-blue-medium: #119dff;
--color-blue-dark: #0d76bf;
--color-blue-dark-2: #2a3f5f;
--color-red: #ef553b;
--color-red-2: #de350b;
--color-red-light: #ffbdad;
--color-yellow: #fbefcb;
--color-bg-light: var(--color-gray-light);
--color-bg-light-2: var(--color-gray-light-2);
@@ -48,6 +46,3 @@
.plotly-editor--theme-provider {
--sidebar-width: 112px;
}

View File

@@ -1,18 +1,27 @@
<template>
<div
:class="['checkbox-container', { 'checked': checked }, {'disabled': disabled}]"
:class="[
'checkbox-container',
{ checked: checked },
{ disabled: disabled }
]"
@click.stop="onClick"
>
<div v-show="!checked" class="unchecked" />
<img
v-show="checked && !disabled"
:src="theme === 'light'
? require('@/assets/images/checkbox_checked_light.svg')
: require('@/assets/images/checkbox_checked.svg')"
v-show="checked && !disabled && theme === 'light'"
class="checked-light"
src="~@/assets/images/checkbox_checked_light.svg"
/>
<img
v-show="checked && !disabled && theme !== 'light'"
class="checked"
src="~@/assets/images/checkbox_checked.svg"
/>
<img
v-show="checked && disabled"
:src="require('@/assets/images/checkbox_checked_disabled.svg')"
class="checked-disabled"
src="~@/assets/images/checkbox_checked_disabled.svg"
/>
<span v-if="label" class="label">{{ label }}</span>
</div>
@@ -26,7 +35,7 @@ export default {
type: String,
required: false,
default: 'accent',
validator: (value) => {
validator: value => {
return ['accent', 'light'].includes(value)
}
},
@@ -46,13 +55,14 @@ export default {
default: false
}
},
data () {
emits: ['click'],
data() {
return {
checked: this.init
}
},
methods: {
onClick () {
onClick() {
if (!this.disabled) {
this.checked = !this.checked
this.$emit('click', this.checked)
@@ -80,7 +90,7 @@ export default {
}
img {
display: block;
display: block;
}
.label {
margin-left: 6px;
@@ -100,6 +110,6 @@ img {
.disabled .unchecked,
.disabled .unchecked:hover {
background-color: var(--color-bg-light-2);
background-color: var(--color-bg-light-2);
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div :class="{ 'disabled': disabled }">
<div :class="{ disabled: disabled }">
<div class="text-field-label">Delimiter</div>
<div
class="delimiter-selector-container"
@@ -8,21 +8,21 @@
>
<div class="value">
<input
:class="{ 'filled': filled }"
ref="delimiterInput"
v-model="inputValue"
:class="{ filled: filled }"
type="text"
maxlength="1"
v-model="inputValue"
@click.stop
:disabled="disabled"
@click.stop
/>
<div class="name">{{ getSymbolName(value) }}</div>
<div class="name">{{ getSymbolName(modelValue) }}</div>
</div>
<div class="controls" @click.stop>
<clear-icon @click.native="clear" :disabled="disabled"/>
<clear-icon :disabled="disabled" @click="clear" />
<drop-down-chevron
:disabled="disabled"
@click.native="!disabled && (showOptions = !showOptions)"
@click="!disabled && (showOptions = !showOptions)"
/>
</div>
</div>
@@ -30,10 +30,11 @@
<div
v-for="(option, index) in options"
:key="index"
@click="chooseOption(option)"
class="option"
@click="chooseOption(option)"
>
<pre>{{option}}</pre><div>{{ getSymbolName(option) }}</div>
<pre>{{ option }}</pre>
<div>{{ getSymbolName(option) }}</div>
</div>
</div>
</div>
@@ -46,9 +47,14 @@ import ClearIcon from '@/components/svg/clear'
export default {
name: 'DelimiterSelector',
props: ['value', 'width', 'disabled'],
components: { DropDownChevron, ClearIcon },
data () {
props: {
modelValue: String,
width: String,
disabled: Boolean
},
emits: ['update:modelValue'],
data() {
return {
showOptions: false,
options: [',', '\t', ' ', '|', ';', '\u001F', '\u001E'],
@@ -57,36 +63,36 @@ export default {
}
},
watch: {
inputValue () {
inputValue() {
if (this.inputValue) {
this.filled = true
if (this.inputValue !== this.value) {
this.$emit('input', this.inputValue)
if (this.inputValue !== this.modelValue) {
this.$emit('update:modelValue', this.inputValue)
}
} else {
this.filled = false
}
}
},
created () {
this.inputValue = this.value
created() {
this.inputValue = this.modelValue
},
methods: {
getSymbolName (str) {
getSymbolName(str) {
if (!str) {
return ''
}
return ascii[str.charCodeAt(0).toString()].name
},
chooseOption (option) {
chooseOption(option) {
this.inputValue = option
this.showOptions = false
},
onContainerClick (event) {
onContainerClick() {
this.$refs.delimiterInput.focus()
},
clear () {
clear() {
if (!this.disabled) {
this.inputValue = ''
this.$refs.delimiterInput.focus()

View File

@@ -1,24 +1,23 @@
<template>
<modal
:name="dialogName"
classes="dialog"
height="auto"
width="80%"
:modalId="dialogName"
class="dialog"
contentClass="import-modal"
scrollable
:clickToClose="false"
>
<div class="dialog-header">
{{ typeName }} import
<close-icon @click="cancelImport" :disabled="disableDialog"/>
<close-icon :disabled="disableDialog" @click="cancelImport" />
</div>
<div class="dialog-body">
<text-field
label="Table name"
id="csv-json-table-name"
v-model="tableName"
label="Table name"
width="484px"
:disabled="disableDialog"
:error-msg="tableNameError"
id="csv-json-table-name"
:errorMsg="tableNameError"
/>
<div v-if="!isJson && !isNdJson" class="chars">
<delimiter-selector
@@ -29,27 +28,27 @@
@input="preview"
/>
<text-field
id="quote-char"
v-model="quoteChar"
label="Quote char"
hint="The character used to quote fields."
v-model="quoteChar"
width="93px"
:disabled="disableDialog"
class="char-input"
id="quote-char"
@input="preview"
/>
<text-field
id="escape-char"
v-model="escapeChar"
label="Escape char"
hint='
The character used to escape the quote character within a field
(e.g. "column with ""quotes"" in text").
'
max-hint-width="242px"
v-model="escapeChar"
maxHintWidth="242px"
width="93px"
:disabled="disableDialog"
class="char-input"
id="escape-char"
@input="preview"
/>
</div>
@@ -67,35 +66,32 @@
class="preview-table"
/>
<div v-else class="no-data">No data</div>
<logs
class="import-errors"
:messages="importMessages"
/>
<logs class="import-errors" :messages="importMessages" />
</div>
<div class="dialog-buttons-container">
<button
id="import-cancel"
class="secondary"
:disabled="disableDialog"
@click="cancelImport"
id="import-cancel"
>
Cancel
</button>
<button
v-show="!importCompleted"
id="import-start"
class="primary"
:disabled="disableDialog || disableImport"
@click="loadToDb(file)"
id="import-start"
>
Import
</button>
<button
v-show="importCompleted"
id="import-finish"
class="primary"
:disabled="disableDialog"
@click="finish"
id="import-finish"
>
Finish
</button>
@@ -130,7 +126,8 @@ export default {
db: Object,
dialogName: String
},
data () {
emits: ['cancel', 'finish'],
data() {
return {
disableDialog: false,
disableImport: false,
@@ -147,24 +144,24 @@ export default {
}
},
computed: {
isJson () {
isJson() {
return fIo.isJSON(this.file)
},
isNdJson () {
isNdJson() {
return fIo.isNDJSON(this.file)
},
typeName () {
typeName() {
return this.isJson || this.isNdJson ? 'JSON' : 'CSV'
}
},
watch: {
isJson () {
isJson() {
if (this.isJson) {
this.delimiter = '\u001E'
this.header = false
}
},
isNdJson () {
isNdJson() {
if (this.isNdJson) {
this.delimiter = '\u001E'
this.header = false
@@ -175,18 +172,17 @@ export default {
if (!this.tableName) {
return
}
this.db.validateTableName(this.tableName)
.catch(err => {
this.tableNameError = err.message + '. Try another table name.'
})
this.db.validateTableName(this.tableName).catch(err => {
this.tableNameError = err.message + '. Try another table name.'
})
}, 400)
},
methods: {
changeHeaderDisplaying (e) {
changeHeaderDisplaying(e) {
this.header = e
this.preview()
},
cancelImport () {
cancelImport() {
if (!this.disableDialog) {
if (this.addedTable) {
this.db.execute(`DROP TABLE "${this.addedTable}"`)
@@ -196,7 +192,7 @@ export default {
this.$emit('cancel')
}
},
reset () {
reset() {
this.header = !this.isJson && !this.isNdJson
this.quoteChar = '"'
this.escapeChar = '"'
@@ -210,11 +206,11 @@ export default {
this.addedTable = null
this.tableNameError = ''
},
open () {
open() {
this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file))
this.$modal.show(this.dialogName)
},
async preview () {
async preview() {
this.disableImport = false
if (!this.file) {
return
@@ -257,13 +253,15 @@ export default {
}
} catch (err) {
console.error(err)
this.importMessages = [{
message: err,
type: 'error'
}]
this.importMessages = [
{
message: err,
type: 'error'
}
]
}
},
async getJsonParseResult (file) {
async getJsonParseResult(file) {
const jsonContent = await fIo.getFileContent(file)
const isEmpty = !jsonContent.trim()
return {
@@ -273,10 +271,10 @@ export default {
},
hasErrors: false,
messages: [],
rowCount: +(!isEmpty)
rowCount: +!isEmpty
}
},
async loadToDb (file) {
async loadToDb(file) {
if (!this.tableName) {
this.tableNameError = "Table name can't be empty"
return
@@ -290,21 +288,22 @@ export default {
delimiter: this.delimiter,
columns: !this.isJson && !this.isNdJson ? null : ['doc']
}
const parsingMsg = {
let parsingMsg = {}
this.importMessages.push({
message: `Parsing ${this.typeName}...`,
type: 'info'
}
this.importMessages.push(parsingMsg)
const parsingLoadingIndicator = setTimeout(() => { parsingMsg.type = 'loading' }, 1000)
})
// Get *reactive* link to parsing message for later updates
parsingMsg = this.importMessages[this.importMessages.length - 1]
const parsingLoadingIndicator = setTimeout(() => {
parsingMsg.type = 'loading'
}, 1000)
const importMsg = {
message: `Importing ${this.typeName} into a SQLite database...`,
type: 'info'
}
let importMsg = {}
let importLoadingIndicator = null
const updateProgress = progress => {
this.$set(importMsg, 'progress', progress)
importMsg.progress = progress
}
const progressCounterId = this.db.createProgressCounter(updateProgress)
@@ -322,7 +321,9 @@ export default {
parsingMsg.type = 'success'
if (parseResult.messages.length > 0) {
this.importMessages = this.importMessages.concat(parseResult.messages)
this.importMessages = this.importMessages.concat(
parseResult.messages
)
parsingMsg.message = `${rowCount} rows are parsed in ${period}.`
} else {
// Inform about parsing success
@@ -333,7 +334,11 @@ export default {
clearTimeout(parsingLoadingIndicator)
// Add info about import start
this.importMessages.push(importMsg)
this.importMessages.push({
message: `Importing ${this.typeName} into a SQLite database...`,
type: 'info'
})
importMsg = this.importMessages[this.importMessages.length - 1]
// Show import progress after 1 second
importLoadingIndicator = setTimeout(() => {
@@ -342,14 +347,19 @@ export default {
// Add table
start = new Date()
await this.db.addTableFromCsv(this.tableName, parseResult.data, progressCounterId)
await this.db.addTableFromCsv(
this.tableName,
parseResult.data,
progressCounterId
)
end = new Date()
this.addedTable = this.tableName
// Inform about import success
period = time.getPeriod(start, end)
importMsg.message = `Importing ${this.typeName} ` +
`into a SQLite database is completed in ${period}.`
importMsg.message =
`Importing ${this.typeName} ` +
`into a SQLite database is completed in ${period}.`
importMsg.type = 'success'
// Loading indicator for import is not needed anymore
@@ -382,7 +392,7 @@ export default {
this.db.deleteProgressCounter(progressCounterId)
this.disableDialog = false
},
async finish () {
async finish() {
this.$modal.hide(this.dialogName)
const stmt = this.getQueryExample()
const tabId = await this.$store.dispatch('addTab', { query: stmt })
@@ -391,18 +401,20 @@ export default {
this.$emit('finish')
events.send('inquiry.create', null, { auto: true })
},
getQueryExample () {
return this.isNdJson ? this.getNdJsonQueryExample()
: this.isJson ? this.getJsonQueryExample()
getQueryExample() {
return this.isNdJson
? this.getNdJsonQueryExample()
: this.isJson
? this.getJsonQueryExample()
: [
'/*',
` * Your CSV file has been imported into ${this.addedTable} table.`,
' * You can run this SQL query to make all CSV records available for charting.',
' */',
`SELECT * FROM "${this.addedTable}"`
].join('\n')
'/*',
` * Your CSV file has been imported into ${this.addedTable} table.`,
' * You can run this SQL query to make all CSV records available for charting.',
' */',
`SELECT * FROM "${this.addedTable}"`
].join('\n')
},
getNdJsonQueryExample () {
getNdJsonQueryExample() {
try {
const firstRowJson = JSON.parse(this.previewData.values.doc[0])
const firstKey = Object.keys(firstRowJson)[0]
@@ -410,7 +422,7 @@ export default {
'/*',
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
` * Run this SQL query to get values of property ${firstKey} ` +
'and make them available for charting.',
'and make them available for charting.',
' */',
`SELECT doc->>'${firstKey}'`,
`FROM "${this.addedTable}"`
@@ -426,7 +438,7 @@ export default {
].join('\n')
}
},
getJsonQueryExample () {
getJsonQueryExample() {
try {
const firstRowJson = JSON.parse(this.previewData.values.doc[0])
const firstKey = Object.keys(firstRowJson)[0]
@@ -434,7 +446,7 @@ export default {
'/*',
` * Your JSON file has been imported into ${this.addedTable} table.`,
` * Run this SQL query to get values of property ${firstKey} ` +
'and make them available for charting.',
'and make them available for charting.',
' */',
'SELECT *',
`FROM "${this.addedTable}"`,
@@ -455,13 +467,22 @@ export default {
}
</script>
<style>
.import-modal {
width: 80%;
max-width: 1152px;
margin: auto;
left: 0 !important;
}
</style>
<style scoped>
.dialog-body {
padding-bottom: 0;
}
#csv-json-table-name {
margin-bottom: 24px;
margin-bottom: 24px;
}
.chars {
@@ -494,11 +515,4 @@ margin-bottom: 24px;
justify-content: center;
align-items: center;
}
/* https://github.com/euvl/vue-js-modal/issues/623 */
>>> .vm--modal {
max-width: 1152px;
margin: auto;
left: 0 !important;
}
</style>

View File

@@ -1,43 +1,43 @@
<template>
<div class="db-uploader-container" :style="{ width }">
<change-db-icon v-if="type === 'small'" @click="browse"/>
<change-db-icon v-if="type === 'small'" @click="browse" />
<div v-if="type === 'illustrated'" class="drop-area-container">
<div
class="drop-area"
@dragover.prevent="state = 'dragover'"
@dragleave.prevent="state=''"
@dragleave.prevent="state = ''"
@drop.prevent="drop"
@click="browse"
>
<div class="text">
Drop the database, CSV, JSON or NDJSON file here
or click to choose a file from your computer.
Drop the database, CSV, JSON or NDJSON file here or click to choose a
file from your computer.
</div>
</div>
</div>
<div v-if="type === 'illustrated'" id="img-container">
<img id="drop-file-top-img" :src="require('@/assets/images/top.svg')" />
<img id="drop-file-top-img" src="~@/assets/images/top.svg" />
<img
id="left-arm-img"
:class="{'swing': state === 'dragover'}"
:src="require('@/assets/images/leftArm.svg')"
:class="{ swing: state === 'dragover' }"
src="~@/assets/images/leftArm.svg"
/>
<img
id="file-img"
ref="fileImg"
:class="{
'swing': state === 'dragover',
'fly': state === 'dropping',
'hidden': state === 'dropped'
swing: state === 'dragover',
fly: state === 'dropping',
hidden: state === 'dropped'
}"
:src="require('@/assets/images/file.png')"
src="~@/assets/images/file.png"
/>
<img id="drop-file-bottom-img" :src="require('@/assets/images/bottom.svg')" />
<img id="body-img" :src="require('@/assets/images/body.svg')" />
<img id="drop-file-bottom-img" src="~@/assets/images/bottom.svg" />
<img id="body-img" src="~@/assets/images/body.svg" />
<img
id="right-arm-img"
:class="{'swing': state === 'dragover'}"
:src="require('@/assets/images/rightArm.svg')"
:class="{ swing: state === 'dragover' }"
src="~@/assets/images/rightArm.svg"
/>
</div>
<div id="error" class="error"></div>
@@ -47,7 +47,7 @@
ref="addCsvJson"
:file="file"
:db="newDb"
dialog-name="importFromCsvJson"
dialogName="importFromCsvJson"
@cancel="cancelImport"
@finish="finish"
/>
@@ -63,12 +63,16 @@ import events from '@/lib/utils/events'
export default {
name: 'DbUploader',
components: {
ChangeDbIcon,
CsvJsonImport
},
props: {
type: {
type: String,
required: false,
default: 'small',
validator: (value) => {
validator: value => {
return ['illustrated', 'small'].includes(value)
}
},
@@ -78,11 +82,8 @@ export default {
default: 'unset'
}
},
components: {
ChangeDbIcon,
CsvJsonImport
},
data () {
emits: [],
data() {
return {
state: '',
animationPromise: Promise.resolve(),
@@ -90,9 +91,9 @@ export default {
newDb: null
}
},
mounted () {
mounted() {
if (this.type === 'illustrated') {
this.animationPromise = new Promise((resolve) => {
this.animationPromise = new Promise(resolve => {
this.$refs.fileImg.addEventListener('animationend', event => {
if (event.animationName.startsWith('fly')) {
this.state = 'dropped'
@@ -103,26 +104,27 @@ export default {
}
},
methods: {
cancelImport () {
cancelImport() {
if (this.newDb) {
this.newDb.shutDown()
this.newDb = null
}
},
async finish () {
async finish() {
this.$store.commit('setDb', this.newDb)
if (this.$route.path !== '/workspace') {
this.$router.push('/workspace')
}
},
loadDb (file) {
return Promise.all([this.newDb.loadDb(file), this.animationPromise])
.then(this.finish)
loadDb(file) {
return Promise.all([this.newDb.loadDb(file), this.animationPromise]).then(
this.finish
)
},
async checkFile (file) {
async checkFile(file) {
this.state = 'dropping'
this.newDb = database.getNewDatabase()
@@ -139,16 +141,19 @@ export default {
await this.$nextTick()
const csvJsonImportModal = this.$refs.addCsvJson
csvJsonImportModal.reset()
return Promise.all([csvJsonImportModal.preview(), this.animationPromise])
.then(csvJsonImportModal.open)
return Promise.all([
csvJsonImportModal.preview(),
this.animationPromise
]).then(csvJsonImportModal.open)
}
},
browse () {
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv,.json,.ndjson')
browse() {
fIo
.getFileFromUser('.db,.sqlite,.sqlite3,.csv,.json,.ndjson')
.then(this.checkFile)
},
drop (event) {
drop(event) {
this.checkFile(event.dataTransfer.files[0])
}
}
@@ -242,11 +247,15 @@ export default {
transform-origin: 0 56px;
}
#file-img.swing {
transform-origin: -74px 139px;
transform-origin: -74px 139px;
}
@keyframes swing {
0% { transform: rotate(0deg); }
100% { transform: rotate(-7deg); }
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(-7deg);
}
}
#file-img.fly {

View File

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

View File

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

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: 'Recersed', value: 'reversed' }
]),
colorSourceUsageOptions: markRaw([
{ label: 'Direct', value: 'direct' },
{ label: 'Map to', value: 'map_to' }
]),
defaultColorSettings: {
constant: { value: '#1F77B4' },
variable: {
source: null,
sourceUsage: 'map_to',
colorscale: null,
mode: 'categorical',
colorscaleDirection: 'normal'
}
}
}
},
methods: {
updateColorType(newColorType) {
const currentColorType = this.modelValue.type
this.defaultColorSettings[currentColorType] = this.modelValue
this.$emit('update:modelValue', {
type: newColorType,
...this.defaultColorSettings[newColorType]
})
},
updateSettings(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>
<style scoped>
:deep(.customPickerContainer) {
float: right;
}
</style>

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>

View File

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

View File

@@ -0,0 +1,672 @@
<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>
</Fold>
</Panel>
<Panel group="Style" name="Nodes">
<Fold name="Nodes">
<Field label="Label">
<Dropdown
:options="keysOptions"
:value="settings.style.nodes.label.source"
className="test_label_select"
@change="updateNodes('label.source', $event)"
/>
</Field>
<Field label="Label color">
<ColorPicker
:selectedColor="settings.style.nodes.label.color"
@color-change="updateNodes('label.color', $event)"
/>
</Field>
<NodeSizeSettings
v-model="settings.style.nodes.size"
:keyOptions="keysOptions"
@update:model-value="updateNodes('size', $event)"
/>
<NodeColorSettings
v-model="settings.style.nodes.color"
:keyOptions="keysOptions"
@update:model-value="updateNodes('color', $event)"
/>
</Fold>
</Panel>
<Panel group="Style" name="Edges">
<Fold name="Edges">
<Field
label="Direction"
fieldContainerClassName="test_edge_direction"
>
<RadioBlocks
:options="visibilityOptions"
:activeOption="settings.style.edges.showDirection"
@option-change="updateEdges('showDirection', $event)"
/>
</Field>
<Field label="Label">
<Dropdown
:options="keysOptions"
:value="settings.style.edges.label.source"
className="test_edge_label_select"
@change="updateEdges('label.source', $event)"
/>
</Field>
<Field label="Label color">
<ColorPicker
:selectedColor="settings.style.edges.label.color"
@color-change="updateEdges('label.color', $event)"
/>
</Field>
<EdgeSizeSettings
v-model="settings.style.edges.size"
:keyOptions="keysOptions"
@update:model-value="updateEdges('size', $event)"
/>
<EdgeColorSettings
v-model="settings.style.edges.color"
:keyOptions="keysOptions"
@update:model-value="updateEdges('color', $event)"
/>
</Fold>
</Panel>
<Panel group="Style" name="Layout">
<Fold name="Layout">
<Field label="Algorithm">
<Dropdown
:options="layoutOptions"
:value="settings.layout.type"
:clearable="false"
className="test_layout_algorithm_select"
@change="updateLayout($event)"
/>
</Field>
<component
:is="layoutSettingsComponentMap[settings.layout.type]"
v-if="settings.layout.type !== 'circular'"
v-model="settings.layout.options"
:keyOptions="keysOptions"
@update:model-value="updateLayout(settings.layout.type)"
/>
</Fold>
<template v-if="settings.layout.type === 'forceAtlas2'">
<Fold name="Advanced layout settings">
<AdvancedForceAtlasLayoutSettings
v-model="settings.layout.options"
:keyOptions="keysOptions"
@update:model-value="updateLayout(settings.layout.type)"
/>
</Fold>
<div class="force-atlas-buttons">
<Button
variant="secondary"
class="test_fa2_reset"
@click="resetFA2LayoutSettings"
>
Reset
</Button>
<Button
variant="primary"
class="test_fa2_toggle"
@click="toggleFA2Layout"
>
<template #node:icon>
<div
:style="{
padding: '0 3px'
}"
>
<RunIcon v-if="!fa2Running" />
<StopIcon v-else />
</div>
</template>
{{ fa2Running ? 'Stop' : 'Start' }}
</Button>
</div>
</template>
</Panel>
</PanelMenuWrapper>
</GraphEditorControls>
<div
ref="graph"
class="test_graph_output"
:style="{
height: '100%',
width: '100%',
backgroundColor: settings.style.backgroundColor
}"
/>
</div>
</template>
<script>
import { markRaw } from 'vue'
import { applyPureReactInVue } from 'veaury'
import GraphEditorControls from '@/lib/GraphEditorControls.jsx'
import { PanelMenuWrapper, Panel, Fold, Section } from 'react-chart-editor'
import 'react-chart-editor/lib/react-chart-editor.css'
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
import ColorPicker from 'react-chart-editor/lib/components/widgets/ColorPicker'
import Button from 'react-chart-editor/lib/components/widgets/Button'
import Field from 'react-chart-editor/lib/components/fields/Field'
import RandomLayoutSettings from '@/components/Graph/RandomLayoutSettings.vue'
import ForceAtlasLayoutSettings from '@/components/Graph/ForceAtlasLayoutSettings.vue'
// eslint-disable-next-line max-len
import AdvancedForceAtlasLayoutSettings from '@/components/Graph/AdvancedForceAtlasLayoutSettings.vue'
import CirclePackLayoutSettings from '@/components/Graph/CirclePackLayoutSettings.vue'
import FA2Layout from 'graphology-layout-forceatlas2/worker'
import * as forceAtlas2 from 'graphology-layout-forceatlas2'
import RunIcon from '@/components/svg/run.vue'
import StopIcon from '@/components/svg/stop.vue'
import { downloadAsPNG, drawOnCanvas } from '@sigma/export-image'
import {
buildNodes,
buildEdges,
updateNodes,
updateEdges
} from '@/lib/graphHelper'
import Graph from 'graphology'
import { circular, random, circlepack } from 'graphology-layout'
import Sigma from 'sigma'
import seedrandom from 'seedrandom'
import NodeColorSettings from '@/components/Graph/NodeColorSettings.vue'
import NodeSizeSettings from '@/components/Graph/NodeSizeSettings.vue'
import EdgeSizeSettings from '@/components/Graph/EdgeSizeSettings.vue'
import EdgeColorSettings from '@/components/Graph/EdgeColorSettings.vue'
import events from '@/lib/utils/events'
export default {
components: {
GraphEditorControls: applyPureReactInVue(GraphEditorControls),
PanelMenuWrapper: applyPureReactInVue(PanelMenuWrapper),
Panel: applyPureReactInVue(Panel),
PanelSection: applyPureReactInVue(Section),
Dropdown: applyPureReactInVue(Dropdown),
RadioBlocks: applyPureReactInVue(RadioBlocks),
Field: applyPureReactInVue(Field),
Fold: applyPureReactInVue(Fold),
Button: applyPureReactInVue(Button),
ColorPicker: applyPureReactInVue(ColorPicker),
RunIcon,
StopIcon,
RandomLayoutSettings,
CirclePackLayoutSettings,
NodeColorSettings,
NodeSizeSettings,
EdgeSizeSettings,
EdgeColorSettings,
AdvancedForceAtlasLayoutSettings
},
inject: ['tabLayout'],
props: {
dataSources: Object,
initOptions: Object,
showViewSettings: Boolean
},
emits: ['update'],
data() {
return {
graph: new Graph({ multi: true, allowSelfLoops: true }),
renderer: null,
fa2Layout: null,
fa2Running: false,
checkIteration: null,
visibilityOptions: markRaw([
{ label: 'Show', value: true },
{ label: 'Hide', value: false }
]),
layoutOptions: markRaw([
{ label: 'Circular', value: 'circular' },
{ label: 'Random', value: 'random' },
{ label: 'Circle pack', value: 'circlepack' },
{ label: 'ForceAtlas2', value: 'forceAtlas2' }
]),
layoutSettingsComponentMap: markRaw({
random: RandomLayoutSettings,
circlepack: CirclePackLayoutSettings,
forceAtlas2: ForceAtlasLayoutSettings
}),
settings: this.initOptions
? JSON.parse(JSON.stringify(this.initOptions))
: {
structure: {
nodeId: null,
objectType: null,
edgeSource: null,
edgeTarget: null
},
style: {
backgroundColor: 'white',
nodes: {
size: {
type: 'constant',
value: 10
},
color: {
type: 'constant',
value: '#1F77B4'
},
label: {
source: null,
color: '#444444'
}
},
edges: {
showDirection: true,
size: {
type: 'constant',
value: 2
},
color: {
type: 'constant',
value: '#a2b1c6'
},
label: {
source: null,
color: '#a2b1c6'
}
}
},
layout: {
type: 'circular',
options: null
}
},
layoutOptionsArchive: {
random: null,
circlepack: null,
forceAtlas2: null
}
}
},
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)
}
},
watch: {
dataSources() {
if (this.dataSources) {
this.buildGraph()
}
},
settings: {
deep: true,
handler() {
this.$emit('update')
}
},
'settings.structure': {
deep: true,
handler() {
this.buildGraph()
}
},
'settings.layout.type': {
immediate: true,
handler() {
events.send('viz_graph.render', null, {
layout: this.settings.layout.type
})
}
},
tabLayout: {
deep: true,
handler() {
if (this.tabLayout.dataView !== 'hidden' && this.renderer) {
this.renderer.scheduleRender()
}
}
}
},
mounted() {
if (this.dataSources) {
this.buildGraph()
}
},
methods: {
buildGraph() {
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
})
if (this.settings.layout.type === 'forceAtlas2') {
this.autoRunFA2Layout()
}
},
updateStructure(attributeName, value) {
this.settings.structure[attributeName] = value
},
updateNodes(attributeName, value) {
const attributePath = attributeName.split('.')
attributePath.reduce((result, current, index) => {
if (index === attributePath.length - 1) {
return (result[current] = value)
} else {
return result[current]
}
}, this.settings.style.nodes)
updateNodes(this.graph, {
[attributePath[0]]: this.settings.style.nodes[attributePath[0]]
})
},
updateEdges(attributeName, value) {
const attributePath = attributeName.split('.')
attributePath.reduce((result, current, index) => {
if (index === attributePath.length - 1) {
return (result[current] = value)
} else {
return result[current]
}
}, this.settings.style.edges)
updateEdges(this.graph, {
[attributePath[0]]: this.settings.style.edges[attributePath[0]]
})
},
updateLayout(layoutType) {
const prevLayout = this.settings.layout.type
// Change layout type? - restore layout settings or set default settings
if (layoutType !== prevLayout) {
this.layoutOptionsArchive[prevLayout] = this.settings.layout.options
this.settings.layout.options = this.layoutOptionsArchive[layoutType]
if (!this.settings.layout.options) {
if (layoutType === 'forceAtlas2') {
this.setRecommendedFA2Settings()
} else if (['random', 'circlepack'].includes(layoutType)) {
this.settings.layout.options = {
seedValue: 1
}
}
}
this.settings.layout.type = layoutType
}
// In any case kill FA2 if it exists
if (this.fa2Layout) {
if (this.fa2Layout.isRunning()) {
this.stopFA2Layout()
}
this.fa2Layout.kill()
}
if (layoutType === 'circular') {
circular.assign(this.graph)
return
}
if (layoutType === 'random') {
random.assign(this.graph, {
rng: seedrandom(this.settings.layout.options.seedValue)
})
return
}
if (layoutType === 'circlepack') {
this.graph.forEachNode(nodeId => {
this.graph.updateNode(nodeId, attributes => {
const newAttributes = { ...attributes }
// Delete old hierarchy attributes
Object.keys(newAttributes)
.filter(key => key.startsWith('hierarchyAttribute'))
.forEach(
hierarchyAttributeKey =>
delete newAttributes[hierarchyAttributeKey]
)
// Set new hierarchy attributes
this.settings.layout.options.hierarchyAttributes?.forEach(
(hierarchyAttribute, index) => {
newAttributes['hierarchyAttribute' + index] =
attributes.data[hierarchyAttribute]
}
)
return newAttributes
})
})
circlepack.assign(this.graph, {
hierarchyAttributes:
this.settings.layout.options.hierarchyAttributes?.map(
(_, index) => 'hierarchyAttribute' + index
) || [],
rng: seedrandom(this.settings.layout.options.seedValue)
})
return
}
if (layoutType === 'forceAtlas2') {
if (
!this.graph.someNode(
(nodeKey, attributes) =>
typeof attributes.x === 'number' &&
typeof attributes.y === 'number'
)
) {
circular.assign(this.graph)
}
this.fa2Layout = markRaw(
new FA2Layout(this.graph, {
getEdgeWeight: (_, attr) =>
this.settings.layout.options.weightSource
? attr.data[this.settings.layout.options.weightSource]
: 1,
settings: this.settings.layout.options
})
)
if (layoutType !== prevLayout) {
this.autoRunFA2Layout()
}
}
},
toggleFA2Layout() {
if (this.fa2Layout.isRunning()) {
this.stopFA2Layout()
} else {
this.fa2Running = true
this.fa2Layout.start()
}
},
stopFA2Layout() {
this.fa2Running = false
this.fa2Layout.stop()
if (this.checkIteration) {
this.fa2Layout.worker.removeEventListener(
'message',
this.checkIteration
)
this.checkIteration = null
}
},
autoRunFA2Layout() {
let iteration = 1
this.checkIteration = () => {
if (
iteration === this.settings.layout.options.initialIterationsAmount
) {
this.stopFA2Layout()
}
iteration++
}
this.fa2Layout.worker.addEventListener('message', this.checkIteration)
this.fa2Running = true
this.fa2Layout.start()
},
setRecommendedFA2Settings() {
const sensibleSettings = forceAtlas2.default.inferSettings(this.graph)
this.settings.layout.options = {
initialIterationsAmount: 50,
adjustSizes: false,
barnesHutOptimize: false,
barnesHutTheta: 0.5,
edgeWeightInfluence: 0,
gravity: 1,
linLogMode: false,
outboundAttractionDistribution: false,
scalingRatio: 1,
slowDown: 1,
strongGravityMode: false,
...sensibleSettings
}
if (
[Infinity, -Infinity].includes(this.settings.layout.options.slowDown)
) {
this.settings.layout.options.slowDown = 1
}
},
resetFA2LayoutSettings() {
if (this.initOptions?.layout.type === 'forceAtlas2') {
this.settings.layout = JSON.parse(
JSON.stringify(this.initOptions.layout)
)
} else {
this.setRecommendedFA2Settings()
}
this.updateLayout(this.settings.layout.type)
},
saveAsPng() {
return downloadAsPNG(this.renderer, {
backgroundColor: this.settings.style.backgroundColor
})
},
prepareCopy() {
return drawOnCanvas(this.renderer, {
backgroundColor: this.settings.style.backgroundColor
})
}
}
}
</script>
<style scoped>
.plotly_editor.with_controls > div {
display: flex !important;
}
:deep(.customPickerContainer) {
float: right;
}
.force-atlas-buttons {
display: flex;
width: 100%;
gap: 16px;
}
.force-atlas-buttons :deep(button) {
flex-grow: 1;
flex-basis: 0;
}
</style>

View File

@@ -0,0 +1,178 @@
<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
v-if="
modelValue.sourceUsage === '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 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 {
nodeColorTypeOptions: markRaw([
{ label: 'Constant', value: 'constant' },
{ label: 'Variable', value: 'variable' },
{ label: 'Calculated', value: 'calculated' }
]),
nodeCalculatedColorMethodOptions: markRaw([
{ label: 'Degree', value: 'degree' },
{ label: 'In degree', value: 'inDegree' },
{ label: 'Out degree', value: 'outDegree' }
]),
сolorAsOptions: markRaw([
{ label: 'Continious', value: 'continious' },
{ label: 'Categorical', value: 'categorical' }
]),
сolorscaleDirections: markRaw([
{ label: 'Normal', value: 'normal' },
{ label: 'Recersed', value: 'reversed' }
]),
colorSourceUsageOptions: markRaw([
{ label: 'Direct', value: 'direct' },
{ label: 'Map to', value: 'map_to' }
]),
defaultColorSettings: {
constant: { value: '#1F77B4' },
variable: {
source: null,
sourceUsage: 'map_to',
colorscale: null,
mode: 'categorical',
colorscaleDirection: 'normal'
},
calculated: {
method: 'degree',
colorscale: null,
mode: 'continious',
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>

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>

View File

@@ -0,0 +1,34 @@
<template>
<Field label="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>

View File

@@ -10,7 +10,12 @@
<div v-show="loading" class="icon-in-progress">
<loading-indicator />
</div>
<span v-if="tooltip" class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
<span
v-if="tooltip"
ref="tooltip"
class="icon-tooltip"
:style="tooltipStyle"
>
{{ tooltip }}
</span>
</button>
@@ -22,11 +27,18 @@ import LoadingIndicator from '@/components/LoadingIndicator'
export default {
name: 'SideBarButton',
props: ['active', 'disabled', 'tooltip', 'tooltipPosition', 'loading'],
components: { LoadingIndicator },
mixins: [tooltipMixin],
props: {
active: Boolean,
disabled: Boolean,
tooltip: String,
tooltipPosition: String,
loading: Boolean
},
emits: ['click'],
methods: {
onClick () {
onClick() {
this.hideTooltip()
this.$emit('click')
}
@@ -51,15 +63,15 @@ export default {
border-radius: var(--border-radius-medium-2);
}
.icon-btn:hover .icon >>> path,
.icon-btn.active .icon >>> path,
.icon-btn:hover .icon >>> circle,
.icon-btn.active .icon >>> circle {
.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:disabled .icon >>> path,
.icon-btn:disabled .icon >>> circle {
.icon-btn:disabled .icon :deep(path),
.icon-btn:disabled .icon :deep(circle) {
fill: var(--color-border);
}
@@ -68,7 +80,7 @@ export default {
pointer-events: none;
}
.disabled.icon-btn:hover .icon >>> path {
.disabled.icon-btn:hover .icon :deep(path) {
fill: var(--color-border);
}

View File

@@ -1,34 +1,41 @@
<template>
<modal
:name="name"
classes="dialog"
height="auto"
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 @click="$emit('cancel')" :disabled="loading"/>
<close-icon :disabled="loading" @click="cancel" />
</div>
<div class="dialog-body">
<div v-if="loading" class="loading-dialog-body">
<loading-indicator :size="30" class="state-icon"/>
<loading-indicator :size="30" class="state-icon" />
{{ loadingMsg }}
</div>
<div v-else class="loading-dialog-body">
<img :src="require('@/assets/images/success.svg')" class="success-icon state-icon" />
<img
src="~@/assets/images/success.svg"
class="success-icon state-icon"
/>
{{ successMsg }}
</div>
</div>
<div class="dialog-buttons-container">
<button
class="secondary"
type="button"
:disabled="loading"
@click="$emit('cancel')"
@click="cancel"
>
Cancel
</button>
<button
class="primary"
type="button"
:disabled="loading"
@click="$emit('action')"
>
@@ -43,31 +50,66 @@ import LoadingIndicator from '@/components/LoadingIndicator'
import CloseIcon from '@/components/svg/close'
export default {
name: 'loadingDialog',
name: 'LoadingDialog',
components: { LoadingIndicator, CloseIcon },
props: {
modelValue: Boolean,
loadingMsg: String,
successMsg: String,
actionBtnName: String,
name: String,
title: String,
loading: Boolean
},
emits: ['cancel', 'action', 'update:modelValue'],
data() {
return {
show: this.modelValue
}
},
watch: {
loading () {
modelValue() {
this.show = this.modelValue
},
loading() {
if (this.loading) {
this.$modal.show(this.name)
this.$emit('update:modelValue', true)
}
}
},
components: { LoadingIndicator, CloseIcon },
methods: {
cancel () {
cancel() {
this.$emit('cancel')
this.$emit('update:modelValue', false)
}
}
}
</script>
<style>
.loading-dialog-enter-active {
animation: show-modal 1s linear 0s 1;
}
.loading-dialog-leave-active {
opacity: 0;
}
@keyframes show-modal {
0% {
opacity: 0;
}
99% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.loading-modal {
width: 400px;
}
</style>
<style scoped>
.loading-dialog-body {
display: flex;

View File

@@ -1,5 +1,10 @@
<template>
<svg :class="animationClass" :height="size" :width="size" :viewBox="`0 0 ${size} ${size}`">
<svg
:class="animationClass"
:height="size"
:width="size"
:viewBox="`0 0 ${size} ${size}`"
>
<circle
class="loader-svg bg"
:style="{ strokeWidth }"
@@ -9,7 +14,11 @@
/>
<circle
class="loader-svg front"
:style="{ strokeDasharray: circleProgress, strokeDashoffset: offset, strokeWidth }"
:style="{
strokeDasharray: circleProgress,
strokeDashoffset: offset,
strokeWidth
}"
:cx="size / 2"
:cy="size / 2"
:r="radius"
@@ -31,23 +40,26 @@ export default {
default: 20
}
},
emits: [],
computed: {
circleProgress () {
circleProgress() {
const circle = this.radius * 3.14 * 2
const dash = this.progress ? (circle * this.progress) / 100 : circle * 1 / 3
const dash = this.progress
? (circle * this.progress) / 100
: (circle * 1) / 3
const space = circle - dash
return `${dash}px, ${space}px`
},
animationClass () {
animationClass() {
return this.progress === undefined ? 'loading' : 'progress'
},
radius () {
radius() {
return this.size / 2 - this.strokeWidth
},
offset () {
return this.radius * 3.14 / 2
offset() {
return (this.radius * 3.14) / 2
},
strokeWidth () {
strokeWidth() {
return this.size / 10
}
}
@@ -57,7 +69,10 @@ export default {
<style scoped>
.loader-svg {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
left: 0;
right: 0;
top: 0;
bottom: 0;
fill: none;
stroke-linecap: round;
stroke: var(--color-accent);
@@ -69,7 +84,7 @@ export default {
.loading .loader-svg.front {
will-change: transform;
animation: fill-animation-loading 1s cubic-bezier(1,1,1,1) 0s infinite;
animation: fill-animation-loading 1s cubic-bezier(1, 1, 1, 1) 0s infinite;
transform-origin: center;
}
@@ -96,10 +111,10 @@ export default {
}
.progress .loader-svg.bg {
animation: bg-animation 1.5s cubic-bezier(1,1,1,1) 0s infinite;
animation: bg-animation 1.5s cubic-bezier(1, 1, 1, 1) 0s infinite;
}
@keyframes bg-animation{
@keyframes bg-animation {
0% {
r: 8;
}
@@ -108,8 +123,7 @@ export default {
r: 9;
}
100% {
r: 8;
r: 8;
}
}
</style>

View File

@@ -1,10 +1,17 @@
<template>
<div class="logs-container" ref="logsContainer">
<div ref="logsContainer" class="logs-container">
<div v-for="(msg, index) in messages" :key="index" class="msg">
<img v-if="msg.type === 'error'" :src="require('@/assets/images/error.svg')">
<img v-if="msg.type === 'info'" :src="require('@/assets/images/info.svg')" width="20px">
<img v-if="msg.type === 'success'" :src="require('@/assets/images/success.svg')">
<loading-indicator v-if="msg.type === 'loading'" :progress="msg.progress" />
<img v-if="msg.type === 'error'" src="~@/assets/images/error.svg" />
<img
v-if="msg.type === 'info'"
src="~@/assets/images/info.svg"
width="20px"
/>
<img v-if="msg.type === 'success'" src="~@/assets/images/success.svg" />
<loading-indicator
v-if="msg.type === 'loading'"
:progress="msg.progress"
/>
<span class="msg-text">{{ serializeMessage(msg) }}</span>
</div>
</div>
@@ -14,17 +21,18 @@
import LoadingIndicator from '@/components/LoadingIndicator'
export default {
name: 'logs',
props: ['messages'],
name: 'Logs',
components: { LoadingIndicator },
props: { messages: Array },
emits: [],
watch: {
'messages.length': 'scrollToBottom'
},
mounted () {
mounted() {
this.scrollToBottom()
},
methods: {
async scrollToBottom () {
async scrollToBottom() {
const container = this.$refs.logsContainer
if (container) {
await this.$nextTick()
@@ -32,7 +40,7 @@ export default {
}
},
serializeMessage (msg) {
serializeMessage(msg) {
let result = ''
if (msg.row !== null && msg.row !== undefined) {
if (msg.type === 'error') {
@@ -43,7 +51,7 @@ export default {
}
result += msg.message
if (!(/(\.|!|\?)$/.test(result))) {
if (!/(\.|!|\?)$/.test(result)) {
result += '.'
}

View File

@@ -7,10 +7,14 @@
{ 'splitpanes-dragging': dragging }
]"
>
<div class="movable-splitter" ref="movableSplitter" :style="movableSplitterStyle" />
<div
class="splitpanes-pane"
ref="movableSplitter"
class="movable-splitter"
:style="movableSplitterStyle"
/>
<div
ref="left"
class="splitpanes-pane"
:size="paneBefore.size"
max-size="30"
:style="styles.before"
@@ -27,8 +31,11 @@
:class="[
'toggle-btns',
{
'both': after.max === 100 && before.max === 100 &&
paneAfter.size > 0 && paneBefore.size > 0
both:
after.max === 100 &&
before.max === 100 &&
paneAfter.size > 0 &&
paneBefore.size > 0
}
]"
>
@@ -39,9 +46,9 @@
>
<img
class="direction-icon"
:src="require('@/assets/images/chevron.svg')"
src="~@/assets/images/chevron.svg"
:style="directionBeforeIconStyle"
>
/>
</div>
<div
v-if="before.max === 100 && paneBefore.size > 0"
@@ -50,18 +57,14 @@
>
<img
class="direction-icon"
:src="require('@/assets/images/chevron.svg')"
src="~@/assets/images/chevron.svg"
:style="directionAfterIconStyle"
>
/>
</div>
</div>
</div>
<!-- splitter end -->
<div
class="splitpanes-pane"
ref="right"
:style="styles.after"
>
<div ref="right" class="splitpanes-pane" :style="styles.after">
<slot name="right-pane" />
</div>
</div>
@@ -86,15 +89,19 @@ export default {
}
}
},
data () {
emits: [],
data() {
return {
container: null,
paneBefore: this.before,
paneAfter: this.after,
beforeMinimising: !this.after.size || !this.before.size ? this.default : {
before: this.before.size,
after: this.after.size
},
beforeMinimising:
!this.after.size || !this.before.size
? this.default
: {
before: this.before.size,
after: this.after.size
},
dragging: false,
movableSplitter: {
top: 0,
@@ -104,19 +111,23 @@ export default {
}
},
computed: {
styles () {
styles() {
return {
before: { [this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%` },
after: { [this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%` }
before: {
[this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%`
},
after: {
[this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%`
}
}
},
movableSplitterStyle () {
movableSplitterStyle() {
const style = { ...this.movableSplitter }
style.top += '%'
style.left += '%'
return style
},
directionBeforeIconStyle () {
directionBeforeIconStyle() {
const expanded = this.paneBefore.size !== 0
const translation = 'translate(-50%, -50%) '
let rotation = ''
@@ -131,7 +142,7 @@ export default {
transform: translation + rotation
}
},
directionAfterIconStyle () {
directionAfterIconStyle() {
const expanded = this.paneAfter.size !== 0
const translation = 'translate(-50%, -50%)'
let rotation = ''
@@ -147,37 +158,48 @@ export default {
}
}
},
mounted() {
this.container = this.$refs.container
},
methods: {
bindEvents () {
bindEvents() {
// Passive: false to prevent scrolling while touch dragging.
document.addEventListener('mousemove', this.onMouseMove, { passive: false })
document.addEventListener('mousemove', this.onMouseMove, {
passive: false
})
document.addEventListener('mouseup', this.onMouseUp)
if ('ontouchstart' in window) {
document.addEventListener('touchmove', this.onMouseMove, { passive: false })
document.addEventListener('touchmove', this.onMouseMove, {
passive: false
})
document.addEventListener('touchend', this.onMouseUp)
}
},
unbindEvents () {
document.removeEventListener('mousemove', this.onMouseMove, { passive: false })
unbindEvents() {
document.removeEventListener('mousemove', this.onMouseMove, {
passive: false
})
document.removeEventListener('mouseup', this.onMouseUp)
if ('ontouchstart' in window) {
document.removeEventListener('touchmove', this.onMouseMove, { passive: false })
document.removeEventListener('touchmove', this.onMouseMove, {
passive: false
})
document.removeEventListener('touchend', this.onMouseUp)
}
},
onMouseMove (event) {
onMouseMove(event) {
event.preventDefault()
this.dragging = true
this.movableSplitter.visibility = 'visible'
this.moveSplitter(event)
},
onMouseUp () {
onMouseUp() {
if (this.dragging) {
const dragPercentage = this.horizontal
? this.movableSplitter.top
@@ -198,7 +220,7 @@ export default {
this.unbindEvents()
},
moveSplitter (event) {
moveSplitter(event) {
const splitterInfo = {
container: this.container,
paneBeforeMax: this.paneBefore.max,
@@ -210,21 +232,19 @@ export default {
this.movableSplitter[dir] = offset
},
togglePane (pane) {
togglePane(pane) {
if (pane.size > 0) {
this.beforeMinimising.before = this.paneBefore.size
this.beforeMinimising.after = this.paneAfter.size
pane.size = 0
const otherPane = pane === this.paneBefore ? this.paneAfter : this.paneBefore
const otherPane =
pane === this.paneBefore ? this.paneAfter : this.paneBefore
otherPane.size = 100 - pane.size
} else {
this.paneBefore.size = this.beforeMinimising.before
this.paneAfter.size = this.beforeMinimising.after
}
}
},
mounted () {
this.container = this.$refs.container
}
}
</script>
@@ -236,9 +256,15 @@ export default {
position: relative;
}
.splitpanes-vertical {flex-direction: row;}
.splitpanes-horizontal {flex-direction: column;}
.splitpanes-dragging * {user-select: none;}
.splitpanes-vertical {
flex-direction: row;
}
.splitpanes-horizontal {
flex-direction: column;
}
.splitpanes-dragging * {
user-select: none;
}
.splitpanes-pane {
width: 100%;
@@ -278,14 +304,14 @@ export default {
.movable-splitter {
position: absolute;
background-color:rgba(162, 177, 198, 0.5);
background-color: rgba(162, 177, 198, 0.5);
}
.splitpanes-vertical > .splitpanes-splitter,
.splitpanes-vertical > .movable-splitter {
width: 8px;
z-index: 5;
height: 100%
height: 100%;
}
.splitpanes-horizontal > .splitpanes-splitter,
@@ -336,20 +362,32 @@ export default {
left: 50%;
}
.splitpanes-horizontal > .splitpanes-splitter .toggle-btns.both .toggle-btn:first-child {
.splitpanes-horizontal
> .splitpanes-splitter
.toggle-btns.both
.toggle-btn:first-child {
border-radius: var(--border-radius-small) 0 0 var(--border-radius-small);
}
.splitpanes-horizontal > .splitpanes-splitter .toggle-btns.both .toggle-btn:last-child {
.splitpanes-horizontal
> .splitpanes-splitter
.toggle-btns.both
.toggle-btn:last-child {
border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0;
margin-left: -1px;
}
.splitpanes-vertical > .splitpanes-splitter .toggle-btns.both .toggle-btn:first-child {
.splitpanes-vertical
> .splitpanes-splitter
.toggle-btns.both
.toggle-btn:first-child {
border-radius: var(--border-radius-small) var(--border-radius-small) 0 0;
}
.splitpanes-vertical > .splitpanes-splitter .toggle-btns.both .toggle-btn:last-child {
.splitpanes-vertical
> .splitpanes-splitter
.toggle-btns.both
.toggle-btn:last-child {
border-radius: 0 0 var(--border-radius-small) var(--border-radius-small);
margin-top: -1px;
}

View File

@@ -1,10 +1,9 @@
export default {
// Get the cursor position relative to the splitpane container.
getCurrentMouseDrag (event, container) {
getCurrentMouseDrag(event, container) {
const rect = container.getBoundingClientRect()
const { clientX, clientY } = ('ontouchstart' in window && event.touches)
? event.touches[0]
: event
const { clientX, clientY } =
'ontouchstart' in window && event.touches ? event.touches[0] : event
return {
x: clientX - rect.left,
y: clientY - rect.top
@@ -12,23 +11,35 @@ export default {
},
// Returns the drag percentage of the splitter relative to the 2 panes it's inbetween.
getCurrentDragPercentage (event, container, isHorisontal) {
getCurrentDragPercentage(event, container, isHorisontal) {
let drag = this.getCurrentMouseDrag(event, container)
drag = drag[isHorisontal ? 'y' : 'x']
const containerSize = container[isHorisontal ? 'clientHeight' : 'clientWidth']
return drag * 100 / containerSize
const containerSize =
container[isHorisontal ? 'clientHeight' : 'clientWidth']
return (drag * 100) / containerSize
},
// Returns the new position in percents.
calculateOffset (event, { container, isHorisontal, paneBeforeMax, paneAfterMax }) {
const dragPercentage = this.getCurrentDragPercentage(event, container, isHorisontal)
calculateOffset(
event,
{ container, isHorisontal, paneBeforeMax, paneAfterMax }
) {
const dragPercentage = this.getCurrentDragPercentage(
event,
container,
isHorisontal
)
const paneBeforeMaxReached = paneBeforeMax < 100 && (dragPercentage >= paneBeforeMax)
const paneAfterMaxReached = paneAfterMax < 100 && (dragPercentage <= 100 - paneAfterMax)
const paneBeforeMaxReached =
paneBeforeMax < 100 && dragPercentage >= paneBeforeMax
const paneAfterMaxReached =
paneAfterMax < 100 && dragPercentage <= 100 - paneAfterMax
// Prevent dragging beyond pane max.
if (paneBeforeMaxReached || paneAfterMaxReached) {
return paneBeforeMaxReached ? paneBeforeMax : Math.max(100 - paneAfterMax, 0)
return paneBeforeMaxReached
? paneBeforeMax
: Math.max(100 - paneAfterMax, 0)
} else {
return Math.min(Math.max(dragPercentage, 0), paneBeforeMax)
}

View File

@@ -1,32 +1,36 @@
<template>
<paginate
:page-count="pageCount"
:page-range="5"
:margin-pages="1"
:prev-text="chevron"
:next-text="chevron"
:no-li-surround="true"
container-class="paginator-continer"
page-link-class="paginator-page-link"
active-class="paginator-active-page"
break-view-link-class="paginator-break"
next-link-class="paginator-next"
prev-link-class="paginator-prev"
disabled-class="paginator-disabled"
v-model="page"
:pageCount="pageCount"
:pageRange="5"
:marginPages="1"
:prevText="chevron"
:nextText="chevron"
:noLiSurround="true"
containerClass="paginator-continer"
pageLinkClass="paginator-page-link"
activeClass="paginator-active-page"
breakViewLinkClass="paginator-break"
nextLinkClass="paginator-next"
prevLinkClass="paginator-prev"
disabledClass="paginator-disabled"
/>
</template>
<script>
import Paginate from 'vuejs-paginate'
import Paginate from 'vuejs-paginate-next'
export default {
name: 'Pager',
components: { Paginate },
props: ['pageCount', 'value'],
data () {
props: {
pageCount: Number,
modelValue: Number
},
emits: ['update:modelValue'],
data() {
return {
page: this.value,
page: this.modelValue,
chevron: `
<svg width="9" height="9" viewBox="0 0 8 12" fill="none">
<path
@@ -38,11 +42,11 @@ export default {
}
},
watch: {
page () {
this.$emit('input', this.page)
page() {
this.$emit('update:modelValue', this.page)
},
value () {
this.page = this.value
modelValue() {
this.page = this.modelValue
}
}
}
@@ -54,48 +58,52 @@ export default {
align-items: center;
line-height: 10px;
}
>>> .paginator-page-link {
:deep(a) {
cursor: pointer;
}
:deep(.paginator-page-link) {
padding: 2px 3px;
margin: 0 5px;
display: block;
color: var(--color-text-base);
font-size: 11px;
}
>>> .paginator-page-link:hover {
:deep(.paginator-page-link:hover) {
color: var(--color-text-active);
}
>>> .paginator-page-link:active,
>>> .paginator-page-link:visited,
>>> .paginator-page-link:focus,
>>> .paginator-next:active,
>>> .paginator-next:visited,
>>> .paginator-next:focus,
>>> .paginator-prev:active,
>>> .paginator-prev:visited,
>>> .paginator-prev:focus {
:deep(.paginator-page-link:active),
:deep(.paginator-page-link:visited),
:deep(.paginator-page-link:focus),
:deep(.paginator-next:active),
:deep(.paginator-next:visited),
:deep(.paginator-next:focus),
:deep(.paginator-prev:active),
:deep(.paginator-prev:visited),
:deep(.paginator-prev:focus) {
outline: none;
}
>>> .paginator-active-page,
>>> .paginator-active-page:hover {
:deep(.paginator-active-page),
:deep(.paginator-active-page:hover) {
color: var(--color-accent);
}
>>> .paginator-break:hover,
>>> .paginator-disabled:hover {
:deep(.paginator-break:hover),
:deep(.paginator-disabled:hover) {
cursor: default;
}
>>> .paginator-prev svg {
:deep(.paginator-prev svg) {
transform: rotate(180deg);
}
>>> .paginator-next:hover path,
>>> .paginator-prev:hover path {
fill: var(--color-text-active);
:deep(.paginator-next:hover path),
:deep(.paginator-prev:hover path) {
fill: var(--color-text-active);
}
>>> .paginator-disabled path,
>>> .paginator-disabled:hover path {
:deep(.paginator-disabled path),
:deep(.paginator-disabled:hover path) {
fill: var(--color-text-light-2);
}
</style>

View File

@@ -1,74 +1,75 @@
<template>
<div>
<div class="rounded-bg">
<div class="header-container" ref="header-container">
<div ref="header-container" class="header-container">
<div>
<div
v-for="(th, index) in header"
:key="index"
class="fixed-header"
:style="{ width: `${th.width}px` }"
:key="index"
:title="th.name"
>
{{ th.name }}
</div>
</div>
</div>
</div>
<div
class="table-container"
ref="table-container"
class="table-container"
@scroll="onScrollTable"
>
<table
ref="table"
class="sqliteviz-table"
tabindex="0"
@keydown="onTableKeydown"
>
<thead>
<tr>
<th v-for="(th, index) in columns" :key="index" ref="th">
<div class="cell-data" :style="cellStyle">{{ th }}</div>
</th>
</tr>
</thead>
<tbody>
<tr v-for="rowIndex in currentPageData.count" :key="rowIndex">
<td
v-for="(col, colIndex) in columns"
:data-col="colIndex"
:data-row="pageSize * (currentPage - 1) + rowIndex - 1"
:data-isNull="isNull(getCellValue(col, rowIndex))"
:data-isBlob="isBlob(getCellValue(col, rowIndex))"
:key="colIndex"
:aria-selected="false"
@click="onCellClick"
>
<div class="cell-data" :style="cellStyle">
{{ getCellText(col, rowIndex) }}
</div>
</td>
</tr>
</tbody>
</table>
<table
ref="table"
class="sqliteviz-table"
tabindex="0"
@keydown="onTableKeydown"
>
<thead>
<tr>
<th v-for="(th, index) in columns" :key="index" ref="th">
<div class="cell-data" :style="cellStyle">{{ th }}</div>
</th>
</tr>
</thead>
<tbody>
<tr v-for="rowIndex in currentPageData.count" :key="rowIndex">
<td
v-for="(col, colIndex) in columns"
:key="colIndex"
:data-col="colIndex"
:data-row="pageSize * (currentPage - 1) + rowIndex - 1"
:data-isNull="isNull(getCellValue(col, rowIndex))"
:data-isBlob="isBlob(getCellValue(col, rowIndex))"
:aria-selected="false"
@click="onCellClick"
>
<div class="cell-data" :style="cellStyle">
{{ getCellText(col, rowIndex) }}
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="table-footer">
<div class="table-footer-count">
{{ rowCount }} {{rowCount === 1 ? 'row' : 'rows'}} retrieved
{{ rowCount }} {{ rowCount === 1 ? 'row' : 'rows' }} retrieved
<span v-if="preview">for preview</span>
<span v-if="time">in {{ time }}</span>
</div>
<pager
v-show="pageCount > 1"
:page-count="pageCount"
v-model="currentPage"
:pageCount="pageCount"
/>
</div>
</div>
</template>
<script>
import Pager from './Pager'
import Pager from './Pager.vue'
export default {
name: 'SqlTable',
@@ -87,7 +88,8 @@ export default {
preview: Boolean,
selectedCellCoordinates: Object
},
data () {
emits: ['updateSelectedCell'],
data() {
return {
header: null,
tableWidth: null,
@@ -97,20 +99,20 @@ export default {
}
},
computed: {
columns () {
columns() {
return this.dataSet.columns
},
rowCount () {
rowCount() {
return this.dataSet.values[this.columns[0]].length
},
cellStyle () {
cellStyle() {
const eq = this.tableWidth / this.columns.length
return { maxWidth: `${Math.max(eq, 100)}px` }
},
pageCount () {
pageCount() {
return Math.ceil(this.rowCount / this.pageSize)
},
currentPageData () {
currentPageData() {
const start = (this.currentPage - 1) * this.pageSize
let end = start + this.pageSize
if (end > this.rowCount - 1) {
@@ -123,31 +125,44 @@ export default {
}
}
},
mounted () {
watch: {
currentPageData() {
this.calculateHeadersWidth()
this.selectCell(null)
},
dataSet() {
this.currentPage = 1
}
},
mounted() {
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
this.resizeObserver.observe(this.$refs.table)
this.calculateHeadersWidth()
if (this.selectedCellCoordinates) {
const { row, col } = this.selectedCellCoordinates
const cell = this.$refs.table
.querySelector(`td[data-col="${col}"][data-row="${row}"]`)
const cell = this.$refs.table.querySelector(
`td[data-col="${col}"][data-row="${row}"]`
)
if (cell) {
this.selectCell(cell)
}
}
},
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.table)
},
methods: {
isBlob (value) {
isBlob(value) {
return value && ArrayBuffer.isView(value)
},
isNull (value) {
isNull(value) {
return value === null
},
getCellValue (col, rowIndex) {
getCellValue(col, rowIndex) {
return this.dataSet.values[col][rowIndex - 1 + this.currentPageData.start]
},
getCellText (col, rowIndex) {
getCellText(col, rowIndex) {
const value = this.getCellValue(col, rowIndex)
if (this.isNull(value)) {
return 'NULL'
@@ -157,7 +172,7 @@ export default {
}
return value
},
calculateHeadersWidth () {
calculateHeadersWidth() {
this.tableWidth = this.$refs['table-container'].offsetWidth
this.$nextTick(() => {
this.header = this.$refs.th.map(th => {
@@ -165,10 +180,11 @@ export default {
})
})
},
onScrollTable () {
this.$refs['header-container'].scrollLeft = this.$refs['table-container'].scrollLeft
onScrollTable() {
this.$refs['header-container'].scrollLeft =
this.$refs['table-container'].scrollLeft
},
onTableKeydown (e) {
onTableKeydown(e) {
const keyCodeMap = {
37: 'left',
39: 'right',
@@ -186,10 +202,10 @@ export default {
this.moveFocusInTable(this.selectedCellElement, keyCodeMap[e.keyCode])
},
onCellClick (e) {
onCellClick(e) {
this.selectCell(e.target.closest('td'), false)
},
selectCell (cell, scrollTo = true) {
selectCell(cell, scrollTo = true) {
if (!cell) {
if (this.selectedCellElement) {
this.selectedCellElement.ariaSelected = 'false'
@@ -212,7 +228,7 @@ export default {
this.$emit('updateSelectedCell', this.selectedCellElement)
},
moveFocusInTable (initialCell, direction) {
moveFocusInTable(initialCell, direction) {
const currentRowIndex = +initialCell.dataset.row
const currentColIndex = +initialCell.dataset.col
let newRowIndex, newColIndex
@@ -241,24 +257,13 @@ export default {
newColIndex = currentColIndex
}
const newCell = this.$refs.table
.querySelector(`td[data-col="${newColIndex}"][data-row="${newRowIndex}"]`)
const newCell = this.$refs.table.querySelector(
`td[data-col="${newColIndex}"][data-row="${newRowIndex}"]`
)
if (newCell) {
this.selectCell(newCell)
}
}
},
beforeDestroy () {
this.resizeObserver.unobserve(this.$refs.table)
},
watch: {
currentPageData () {
this.calculateHeadersWidth()
this.selectCell(null)
},
dataSet () {
this.currentPage = 1
}
}
}
</script>
@@ -270,7 +275,7 @@ table.sqliteviz-table:focus {
.sqliteviz-table tbody td:hover {
background-color: var(--color-bg-light-3);
}
.sqliteviz-table tbody td[aria-selected="true"] {
box-shadow:inset 0 0 0 1px var(--color-accent);
.sqliteviz-table tbody td[aria-selected='true'] {
box-shadow: inset 0 0 0 1px var(--color-accent);
}
</style>

View File

@@ -1,17 +1,25 @@
<template>
<div>
<div v-if="label" :class="['text-field-label', { error: errorMsg }, {'disabled': disabled}]">
<div
v-if="label"
:class="['text-field-label', { error: errorMsg }, { disabled: disabled }]"
>
{{ label }}
<hint-icon class="hint" v-if="hint" :hint="hint" :max-width="maxHintWidth || '149px'"/>
<hint-icon
v-if="hint"
class="hint"
:hint="hint"
:maxWidth="maxHintWidth || '149px'"
/>
</div>
<input
type="text"
:placeholder="placeholder"
:class="{ error: errorMsg }"
:style="{ width: width }"
:value="value"
:value="modelValue"
:disabled="disabled"
@input="$emit('input', $event.target.value)"
@input="$emit('update:modelValue', $event.target.value)"
/>
<div v-show="errorMsg" class="text-field-error">{{ errorMsg }}</div>
</div>
@@ -20,9 +28,19 @@
<script>
import HintIcon from '@/components/svg/hint'
export default {
name: 'textField',
props: ['placeholder', 'label', 'errorMsg', 'value', 'width', 'hint', 'maxHintWidth', 'disabled'],
components: { HintIcon }
name: 'TextField',
components: { HintIcon },
props: {
placeholder: String,
label: String,
errorMsg: String,
modelValue: String,
width: String,
hint: String,
maxHintWidth: String,
disabled: Boolean
},
emits: ['update:modelValue']
}
</script>
@@ -66,7 +84,7 @@ input.error {
position: relative;
}
.text-field-label .hint{
.text-field-label .hint {
position: absolute;
top: -2px;
right: -22px;

View File

@@ -29,11 +29,11 @@
</g>
<defs>
<clipPath id="clip0">
<rect width="18" height="18" fill="white"/>
<rect width="18" height="18" fill="white" />
</clipPath>
</defs>
</svg>
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
<span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
Add new table from CSV, JSON or NDJSON
</span>
</span>
@@ -45,9 +45,10 @@ import tooltipMixin from '@/tooltipMixin'
export default {
name: 'AddTableIcon',
mixins: [tooltipMixin],
props: ['tooltip'],
props: { tooltip: String },
emits: ['click'],
methods: {
onClick () {
onClick() {
this.hideTooltip()
this.$emit('click')
}

View File

@@ -11,10 +11,9 @@
13.5L16.35 6.75L17.9475 8.33625Z"
fill="#506784"
/>
</svg>
</svg>
</template>
<script>
export default {
}
export default {}
</script>

View File

@@ -1,40 +1,41 @@
<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
<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 class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
Load another database, CSV, JSON or NDJSON
</span>
</div>
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',
name: 'ChangeDbIcon',
mixins: [tooltipMixin],
emits: ['click'],
methods: {
onClick () {
onClick() {
this.hideTooltip()
this.$emit('click')
}

View File

@@ -1,10 +1,5 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
@@ -46,7 +41,6 @@
</template>
<script>
export default {
name: 'ChartIcon'
}

View File

@@ -1,6 +1,6 @@
<template>
<svg
:class="['clear-icon', {'disabled': disabled}]"
:class="['clear-icon', { disabled: disabled }]"
width="20"
height="20"
viewBox="0 0 20 20"
@@ -21,10 +21,9 @@
</template>
<script>
export default {
name: 'ClearIcon',
props: ['disabled']
props: { disabled: Boolean }
}
</script>
@@ -42,6 +41,6 @@ export default {
}
.disabled.clear-icon:hover path {
fill: #C8D4E3;
fill: #c8d4e3;
}
</style>

View File

@@ -1,10 +1,5 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<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
@@ -26,7 +21,6 @@
</template>
<script>
export default {
name: 'ClipboardIcon'
}

View File

@@ -1,12 +1,12 @@
<template>
<svg
@click.stop="$emit('click')"
:class="['icon', {'disabled': disabled }]"
: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
@@ -30,7 +30,8 @@ export default {
required: false,
default: false
}
}
},
emits: ['click']
}
</script>

View File

@@ -1,10 +1,5 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
@@ -17,7 +12,7 @@
6.91686 13.5552 6.91522Z"
fill="#A2B1C6"
/>
<circle cx="5.50049" cy="6.00339" r="1.5" fill="#A2B1C6"/>
<circle cx="5.50049" cy="6.00339" r="1.5" fill="#A2B1C6" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
@@ -27,11 +22,10 @@
1.21788ZM16.0374 2.71788L1.96424 2.713L1.96289 15.2773L16.036 15.2821L16.0374 2.71788Z"
fill="#A2B1C6"
/>
</svg>
</svg>
</template>
<script>
export default {
name: 'DataViewIcon'
}

View File

@@ -1,6 +1,6 @@
<template>
<svg
:class="['chevron-icon', {'disabled': disabled}]"
:class="['chevron-icon', { disabled: disabled }]"
width="20"
height="20"
viewBox="0 0 20 20"
@@ -15,10 +15,9 @@
</template>
<script>
export default {
name: 'DropDownChevron',
props: ['disabled']
props: { disabled: Boolean }
}
</script>
@@ -36,6 +35,6 @@ export default {
}
.disabled.chevron-icon:hover path {
fill: #C8D4E3;
fill: #c8d4e3;
}
</style>

View File

@@ -21,6 +21,5 @@
</template>
<script>
export default {
}
export default {}
</script>

View File

@@ -17,7 +17,7 @@
fill="#A2B1C6"
/>
</svg>
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
<span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
{{ tooltip }}
</span>
</span>
@@ -29,9 +29,13 @@ import tooltipMixin from '@/tooltipMixin'
export default {
name: 'ExportIcon',
mixins: [tooltipMixin],
props: ['tooltip', 'tooltipPosition'],
props: {
tooltip: String,
tooltipPosition: String
},
emits: ['click'],
methods: {
onClick () {
onClick() {
this.hideTooltip()
this.$emit('click')
}

View File

@@ -1,10 +1,5 @@
<template>
<svg
width="19"
height="18"
viewBox="0 0 19 18"
fill="none"
>
<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
@@ -34,7 +29,7 @@
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"
fill="#A2B1C6"
/>
<path
d="M3.03345 0.991333H4.89869V2.49133H3.03345V7.93074H1.53345V2.49133C1.53345 1.66633
@@ -55,7 +50,6 @@
</template>
<script>
export default {
name: 'ExportToCsvIcon'
}

View File

@@ -1,10 +1,5 @@
<template>
<svg
width="19"
height="18"
viewBox="0 0 19 18"
fill="none"
>
<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
@@ -54,7 +49,6 @@
</template>
<script>
export default {
name: 'ExportToSvgIcon'
}

View File

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

View File

@@ -33,7 +33,11 @@
fill="#A2B1C6"
/>
</svg>
<span class="icon-tooltip" :style="{...tooltipStyle, maxWidth: maxWidth }" ref="tooltip">
<span
ref="tooltip"
class="icon-tooltip"
:style="{ ...tooltipStyle, maxWidth: maxWidth }"
>
{{ hint }}
</span>
</div>
@@ -44,10 +48,14 @@ import tooltipMixin from '@/tooltipMixin'
export default {
name: 'HintIcon',
props: ['hint', 'maxWidth'],
mixins: [tooltipMixin],
props: {
hint: String,
maxWidth: String
},
emits: ['click'],
methods: {
onClick () {
onClick() {
this.hideTooltip()
this.$emit('click')
}

View File

@@ -11,8 +11,8 @@
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"
/>
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"

View File

@@ -1,10 +1,5 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
@@ -14,14 +9,13 @@
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"/>
<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'
}

View File

@@ -1,10 +1,5 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<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
@@ -30,7 +25,10 @@
5.5195V15.0117Z"
fill="#A2B1C6"
/>
<path d="M15.1875 6.22266H13.7812V7.62891H15.1875V6.22266Z" fill="#A2B1C6"/>
<path
d="M15.1875 6.22266H13.7812V7.62891H15.1875V6.22266Z"
fill="#A2B1C6"
/>
</svg>
</template>

View File

@@ -1,15 +1,10 @@
<template>
<svg
width="19"
height="19"
viewBox="0 0 19 19"
fill="none"
>
<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
<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
@@ -29,24 +24,23 @@
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"
/>
fill="#A2B1C6"
/>
</g>
<defs>
<clipPath id="clip0_2130_5292">
<rect
width="18"
height="18"
fill="white"
transform="translate(0.353027 18.5916) rotate(-90)"
/>
width="18"
height="18"
fill="white"
transform="translate(0.353027 18.5916) rotate(-90)"
/>
</clipPath>
</defs>
</svg>
</svg>
</template>
<script>
export default {
name: 'RowIcon'
}

View File

@@ -1,16 +1,13 @@
<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 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'
}

View File

@@ -0,0 +1,130 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_2236_2707)">
<path
d="M8.11441 0.0274963C7.73011 0.0274918 7.35916 0.168422 7.0718
0.423594C6.78445 0.678766 6.60065 1.03046 6.55522 1.41207L7.89167
1.57122C7.89815 1.51669 7.9244 1.46644 7.96546 1.42997C8.00651 1.39351
8.05951 1.37337 8.11441 1.37337V0.0274963ZM9.88559
0.0274963H8.11441V1.37337H9.88559V0.0274963ZM11.4448
1.41207C11.3994 1.03046 11.2156 0.678766 10.9282 0.423594C10.6408
0.168422 10.2699 0.0274918 9.88559 0.0274963V1.37337C9.94049 1.37337
9.99349 1.39351 10.0345 1.42997C10.0756 1.46644 10.1018 1.51669
10.1083 1.57122L11.4448 1.41207ZM11.5815 2.56055L11.4448
1.41207L10.1083 1.57122L10.2449 2.7197L11.5815 2.56055ZM14.349
3.0888L13.282 3.5464L13.8123 4.78326L14.8793 4.32566L14.349
3.0888ZM16.3276 3.74682C16.1355 3.41401 15.8279 3.16322 15.4633
3.04195C15.0986 2.92067 14.7021 2.93735 14.349 3.0888L14.8793
4.32566C14.9298 4.304 14.9865 4.30172 15.0386 4.31904C15.0907
4.33636 15.1346 4.3722 15.1621 4.41976L16.3276 3.74682ZM17.2132
5.28067L16.3276 3.74682L15.1621 4.41976L16.0477 5.95361L17.2132
5.28067ZM16.7937 7.32326C17.1015 7.09311 17.3142 6.75809 17.3915
6.38165C17.4688 6.00521 17.4054 5.61348 17.2132 5.28067L16.0477
5.95361C16.0751 6.00117 16.0842 6.05714 16.0731 6.11093C16.0621
6.16471 16.0317 6.21257 15.9877 6.24544L16.7937 7.32326ZM15.7861
8.07673L16.7937 7.32326L15.9877 6.24544L14.9801 6.99891L15.7861
8.07673ZM16.7937 10.6767L15.7861 9.92327L14.9802 11.0011L15.9877
11.7546L16.7937 10.6767ZM17.2132 12.7193C17.4054 12.3865 17.4688
11.9948 17.3915 11.6183C17.3142 11.2419 17.1015 10.9069 16.7937
10.6767L15.9877 11.7546C16.0317 11.7874 16.0621 11.8353 16.0731
11.8891C16.0842 11.9429 16.0751 11.9988 16.0477 12.0464L17.2132
12.7193ZM16.3276 14.2532L17.2132 12.7193L16.0477 12.0464L15.1621
13.5802L16.3276 14.2532ZM14.349 14.9112C14.7021 15.0626 15.0986
15.0793 15.4633 14.958C15.8279 14.8368 16.1355 14.586 16.3276
14.2532L15.1621 13.5802C15.1346 13.6278 15.0907 13.6636 15.0386
13.6809C14.9865 13.6982 14.9298 13.6959 14.8793 13.6742L14.349
14.9112ZM13.282 14.4536L14.349 14.9112L14.8793 13.6742L13.8125
13.2167L13.282 14.4536ZM11.4448 16.5879L11.5814 15.4394L10.245
15.2803L10.1083 16.4288L11.4448 16.5879ZM9.88559 17.9725C10.2699
17.9725 10.6408 17.8316 10.9282 17.5764C11.2156 17.3212 11.3994
16.9695 11.4448 16.5879L10.1083 16.4288C10.1018 16.4833 10.0756
16.5336 10.0345 16.57C9.99349 16.6065 9.94049 16.6266 9.88559
16.6266V17.9725ZM8.11441
17.9725H9.88559V16.6266H8.11441V17.9725ZM6.55522
16.5879C6.60065 16.9695 6.78445 17.3212 7.0718 17.5764C7.35916
17.8316 7.73011 17.9725 8.11441 17.9725V16.6266C8.05951 16.6266
8.00651 16.6065 7.96546 16.57C7.9244 16.5336 7.89815 16.4833
7.89167 16.4288L6.55522 16.5879ZM6.44172 15.6342L6.55522 1
6.5879L7.89167 16.4288L7.77817 15.475L6.44172 15.6342ZM4.00097
13.2967L3.12066 13.6742L3.65104 14.9112L4.53136 14.5337L4.00097
13.2967ZM3.12066 13.6742C3.07021 13.6959 3.01346 13.6982 2.96138
13.6809C2.90929 13.6636 2.86536 13.6278 2.83791 13.5802L1.67238
14.2532C1.86453 14.586 2.17205 14.8368 2.53672 14.958C2.90138
15.0793 3.29785 15.0626 3.65104 14.9112L3.12066 13.6742ZM2.83791
13.5802L1.95233 12.0464L0.786798 12.7193L1.67238 14.2532L2.83791
13.5802ZM1.95233 12.0464C1.92487 11.9988 1.91582 11.9429 1.92688
11.8891C1.93794 11.8353 1.96834 11.7874 2.01233 11.7546L1.20626
10.6767C0.898499 10.9069 0.685823 11.2419 0.608517 11.6183C0.531211
11.9948 0.594643 12.3865 0.786798 12.7193L1.95233 12.0464ZM2.01233
11.7546L2.70007 11.2403L1.89389 10.1625L1.20626 10.6767L2.01233
11.7546ZM1.20626 7.32326L1.894 7.83761L2.70007 6.75979L2.01233
6.24544L1.20626 7.32326ZM0.786798 5.28067C0.594643 5.61348
0.531211 6.00521 0.608517 6.38165C0.685823 6.75809 0.898499
7.09311 1.20626 7.32326L2.01233 6.24544C1.96834 6.21257
1.93794 6.16471 1.92688 6.11093C1.91582 6.05714 1.92487
6.00117 1.95233 5.95361L0.786798 5.28067ZM1.67238
3.74682L0.786798 5.28067L1.95233 5.95361L2.83791
4.41976L1.67238 3.74682ZM3.65104 3.0888C3.29785
2.93735 2.90138 2.92067 2.53672 3.04195C2.17205 3.16322 1.86453
3.41401 1.67238 3.74682L2.83791 4.41976C2.86536 4.37223 2.90929
4.33641 2.96138 4.31909C3.01346 4.30176 3.07021 4.30414 3.12066
4.32577L3.65104 3.0888ZM4.53136 3.46632L3.65104 3.0888L3.12066
4.32577L4.00097 4.70329L4.53136 3.46632ZM6.55522 1.41207L6.44172
2.36584L7.77817 2.52499L7.89167 1.57122L6.55522 1.41207ZM6.3585
4.5022C7.04018 4.11795 7.6696 3.4366 7.77817 2.52499L6.44172
2.36584C6.39887 2.72575 6.13116 3.08544 5.69756 3.32994L6.3585
4.5022ZM4.00097 4.70329C4.81792 5.05367 5.68948 4.87938 6.3585
4.5022L5.69756 3.32994C5.28213 3.56412 4.85852 3.60663 4.53136
3.46632L4.00097 4.70329ZM3.72866 9C3.72866 8.20369 3.44288 7.3153
2.70007 6.75979L1.894 7.83761C2.19884 8.06562 2.38278 8.48834 2.38278
9H3.72866ZM6.3585 13.4978C5.68948 13.1206 4.81792 12.9463 4.00097
13.2967L4.53136 14.5337C4.85852 14.3934 5.28213 14.436 5.69756
14.6703L6.3585 13.4978ZM2.70007 11.2403C3.44299 10.6848 3.72866
9.79631 3.72866 9H2.38278C2.38278 9.51177 2.19873 9.9346 1.89389
10.1625L2.70007 11.2403ZM11.5243 13.4358C10.9034 13.8057 10.3448
14.4432 10.245 15.2803L11.5814 15.4394C11.6183 15.1293 11.8415
14.8134 12.2132 14.5919L11.5243 13.4358ZM13.8125 13.2167C13.0169
12.8756 12.1673 13.0527 11.5243 13.4358L12.2132 14.5919C12.5925
14.3659 12.9839 14.3258 13.282 14.4536L13.8125 13.2167ZM7.77817
15.475C7.6696 14.5635 7.04018 13.8822 6.3585 13.4978L5.69756
14.6703C6.13116 14.9147 6.39887 15.2742 6.44172 15.6342L7.77817
15.475ZM14.047 9C14.047 9.71656 14.316 10.5045 14.9802
11.0011L15.7861 9.92327C15.547 9.74449 15.3929 9.41139 15.3929
9H14.047ZM14.9801 6.99891C14.3159 7.49553 14.047 8.28332 14.047
9H15.3929C15.3929 8.58861 15.547 8.2555 15.7861 8.07673L14.9801
6.99891ZM11.5243 4.56422C12.1673 4.94734 13.0168 5.12444 13.8123
4.78326L13.282 3.5464C12.9839 3.67426 12.5925 3.63399 12.2132
3.408L11.5243 4.56422ZM10.2449 2.7197C10.3446 3.55683 10.9034
4.19433 11.5243 4.56422L12.2132 3.408C11.8415 3.1866 11.6184
2.87077 11.5815 2.56055L10.2449 2.7197Z"
fill="#A2B1C6"
/>
<path
d="M11.0935 9C11.0935 7.84373 10.1562 6.90642 8.99988 6.90642C7.84361
6.90642 6.90629 7.84373 6.90629 9C6.90629 10.1563 7.84361 11.0936
8.99988 11.0936C10.1562 11.0936 11.0935 10.1563 11.0935 9ZM12.2898
9C12.2898 10.817 10.8169 12.2899 8.99988 12.2899C7.18289 12.2899
5.70996 10.817 5.70996 9C5.70996 7.18301 7.18289 5.71008 8.99988
5.71008C10.8169 5.71008 12.2898 7.18301 12.2898 9Z"
fill="#A2B1C6"
/>
</g>
<defs>
<clipPath id="clip0_2236_2707">
<rect width="18" height="18" fill="white" />
</clipPath>
</defs>
</svg>
</template>
<script>
export default {
name: 'SettingsIcon'
}
</script>

View File

@@ -24,7 +24,6 @@
</template>
<script>
export default {
name: 'SortIcon',
props: {

View File

@@ -1,10 +1,5 @@
<template>
<svg
width="18"
height="19"
viewBox="0 0 18 19"
fill="none"
>
<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
@@ -47,14 +42,18 @@
</g>
<defs>
<clipPath id="clip0">
<rect width="18" height="18" fill="white" transform="translate(0 0.0134277)"/>
<rect
width="18"
height="18"
fill="white"
transform="translate(0 0.0134277)"
/>
</clipPath>
</defs>
</svg>
</template>
<script>
export default {
name: 'SqlEditorIcon'
}

View File

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

View File

@@ -1,10 +1,5 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
@@ -41,7 +36,6 @@
</template>
<script>
export default {
name: 'TableIcon'
}

View File

@@ -17,9 +17,8 @@
</template>
<script>
export default {
name: 'treeChevron',
name: 'TreeChevron',
props: {
expanded: {
type: Boolean,
@@ -31,7 +30,7 @@ export default {
<style scoped>
.chevron-icon {
-webkit-transition: transform .15s ease-in-out;
transition: transform .15s ease-in-out;
-webkit-transition: transform 0.15s ease-in-out;
transition: transform 0.15s ease-in-out;
}
</style>

View File

@@ -1,24 +1,19 @@
<template>
<svg
width="19"
height="19"
viewBox="0 0 19 19"
fill="none"
>
<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
<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
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
@@ -26,24 +21,23 @@
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>
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'
}

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

View File

@@ -0,0 +1,65 @@
import ReactPlotlyEditor from 'react-chart-editor'
import React, { createRef } from 'react'
import EditorControls from 'react-chart-editor/lib/EditorControls'
/**
* This extended ReactPlotlyEditor has a reference to PlotComponent.
* The reference makes it possible to call updatePlotly method of PlotComponent.
* updatePlotly method allows smoothly resize the plot
* when resize chart editor container.
*/
export default class ReactPlotlyEditorWithPlotRef extends ReactPlotlyEditor {
constructor(props) {
super(props)
this.plotComponentRef = createRef()
}
render() {
return (
<div className="plotly_editor">
{!this.props.hideControls && (
<EditorControls
graphDiv={this.state.graphDiv}
dataSources={this.props.dataSources}
dataSourceOptions={this.props.dataSourceOptions}
plotly={this.props.plotly}
onUpdate={this.props.onUpdate}
advancedTraceTypeSelector={this.props.advancedTraceTypeSelector}
locale={this.props.locale}
traceTypesConfig={this.props.traceTypesConfig}
dictionaries={this.props.dictionaries}
showFieldTooltips={this.props.showFieldTooltips}
srcConverters={this.props.srcConverters}
makeDefaultTrace={this.props.makeDefaultTrace}
glByDefault={this.props.glByDefault}
mapBoxAccess={Boolean(
this.props.config && this.props.config.mapboxAccessToken
)}
fontOptions={this.props.fontOptions}
chartHelp={this.props.chartHelp}
customConfig={this.props.customConfig}
>
{this.props.children}
</EditorControls>
)}
<div
className="plotly_editor_plot"
style={{ width: '100%', height: '100%' }}
>
<this.PlotComponent
ref={this.plotComponentRef}
data={this.props.data}
layout={this.props.layout}
frames={this.props.frames}
config={this.props.config}
useResizeHandler={this.props.useResizeHandler}
debug={this.props.debug}
onInitialized={this.handleRender}
onUpdate={this.handleRender}
style={{ width: '100%', height: '100%' }}
divId={this.props.divId}
/>
</div>
</div>
)
}
}

View File

@@ -1,8 +1,8 @@
import dereference from 'react-chart-editor/lib/lib/dereference'
import * as dereference from 'react-chart-editor/lib/lib/dereference'
import plotly from 'plotly.js'
import { nanoid } from 'nanoid'
export function getOptionsFromDataSources (dataSources) {
export function getOptionsFromDataSources(dataSources) {
if (!dataSources) {
return []
}
@@ -13,7 +13,7 @@ export function getOptionsFromDataSources (dataSources) {
}))
}
export function getOptionsForSave (state, dataSources) {
export function getOptionsForSave(state, dataSources) {
// we don't need to save the data, only settings
// so we modify state.data using dereference
const stateCopy = JSON.parse(JSON.stringify(state))
@@ -21,11 +21,11 @@ export function getOptionsForSave (state, dataSources) {
for (const key in dataSources) {
emptySources[key] = []
}
dereference(stateCopy.data, emptySources)
dereference.default(stateCopy.data, emptySources)
return stateCopy
}
export async function getImageDataUrl (element, type) {
export async function getImageDataUrl(element, type) {
const chartElement = element.querySelector('.js-plotly-plot')
return await plotly.toImage(chartElement, {
format: type,
@@ -34,7 +34,7 @@ export async function getImageDataUrl (element, type) {
})
}
export function getChartData (element) {
export function getChartData(element) {
const chartElement = element.querySelector('.js-plotly-plot')
return {
data: chartElement.data,
@@ -42,7 +42,7 @@ export function getChartData (element) {
}
}
export function getHtml (options) {
export function getHtml(options) {
const chartId = nanoid()
return `
<script src="https://cdn.plot.ly/plotly-latest.js" charset="UTF-8"></script>

View File

@@ -7,7 +7,7 @@ const hintsByCode = {
}
export default {
getResult (source, columns) {
getResult(source, columns) {
const result = {
columns: columns || []
}
@@ -52,7 +52,7 @@ export default {
return result
},
prepareForExport (resultSet) {
prepareForExport(resultSet) {
const columns = resultSet.columns
const rowCount = resultSet.values[columns[0]].length
const result = {
@@ -61,13 +61,15 @@ export default {
}
for (let rowNumber = 0; rowNumber < rowCount; rowNumber++) {
result.data.push(columns.map(column => resultSet.values[column][rowNumber]))
result.data.push(
columns.map(column => resultSet.values[column][rowNumber])
)
}
return result
},
parse (file, config = {}) {
parse(file, config = {}) {
return new Promise((resolve, reject) => {
const defaultConfig = {
delimiter: '', // auto-detect
@@ -122,7 +124,7 @@ export default {
})
},
serialize (resultSet) {
serialize(resultSet) {
return Papa.unparse(this.prepareForExport(resultSet), { delimiter: '\t' })
}
}

View File

@@ -1,10 +1,15 @@
import initSqlJs from 'sql.js/dist/sql-wasm.js'
import initSqlJs from 'sql.js'
import dbUtils from './_statements'
import wasmUrl from 'sql.js/dist/sql-wasm.wasm?url'
let SQL = null
const sqlModuleReady = initSqlJs().then(sqlModule => { SQL = sqlModule })
const sqlModuleReady = initSqlJs({
locateFile: () => wasmUrl
}).then(sqlModule => {
SQL = sqlModule
})
function _getDataSourcesFromSqlResult (sqlResult) {
function _getDataSourcesFromSqlResult(sqlResult) {
if (!sqlResult) {
return {}
}
@@ -16,31 +21,30 @@ function _getDataSourcesFromSqlResult (sqlResult) {
}
export default class Sql {
constructor () {
constructor() {
this.db = null
}
static build () {
return sqlModuleReady
.then(() => {
return new Sql()
})
static build() {
return sqlModuleReady.then(() => {
return new Sql()
})
}
createDb (buffer) {
createDb(buffer) {
if (this.db != null) this.db.close()
this.db = new SQL.Database(buffer)
return this.db
}
open (buffer) {
open(buffer) {
this.createDb(buffer && new Uint8Array(buffer))
return {
ready: true
}
}
exec (sql, params) {
exec(sql, params) {
if (this.db === null) {
this.createDb()
}
@@ -56,7 +60,7 @@ export default class Sql {
})
}
import (tabName, data, progressCounterId, progressCallback, chunkSize = 1500) {
import(tabName, data, progressCounterId, progressCallback, chunkSize = 1500) {
if (this.db === null) {
this.createDb()
}
@@ -77,7 +81,10 @@ export default class Sql {
}
this.db.exec('COMMIT')
count++
progressCallback({ progress: 100 * (count / chunksAmount), id: progressCounterId })
progressCallback({
progress: 100 * (count / chunksAmount),
id: progressCounterId
})
}
return {
@@ -85,11 +92,11 @@ export default class Sql {
}
}
export () {
export() {
return this.db.export()
}
close () {
close() {
if (this.db) {
this.db.close()
}

View File

@@ -1,8 +1,10 @@
export default {
* generateChunks (data, size) {
*generateChunks(data, size) {
const matrix = Object.keys(data).map(col => data[col])
const [row] = matrix
const transposedMatrix = row.map((value, column) => matrix.map(row => row[column]))
const transposedMatrix = row.map((value, column) =>
matrix.map(row => row[column])
)
const count = Math.ceil(transposedMatrix.length / size)
@@ -13,13 +15,13 @@ export default {
}
},
getInsertStmt (tabName, columns) {
getInsertStmt(tabName, columns) {
const colList = `"${columns.join('", "')}"`
const params = columns.map(() => '?').join(', ')
return `INSERT INTO "${tabName}" (${colList}) VALUES (${params});`
},
getCreateStatement (tabName, data) {
getCreateStatement(tabName, data) {
let result = `CREATE table "${tabName}"(`
for (const col in data) {
// Get the first row of values to determine types
@@ -38,7 +40,8 @@ export default {
type = 'TEXT'
break
}
default: type = 'TEXT'
default:
type = 'TEXT'
}
result += `"${col}" ${type}, `
}

View File

@@ -3,7 +3,7 @@ import Sql from './_sql'
const sqlReady = Sql.build()
function processMsg (sql) {
function processMsg(sql) {
const data = this
switch (data && data.action) {
case 'open':
@@ -28,14 +28,12 @@ function processMsg (sql) {
}
}
function onError (error) {
function onError(error) {
return {
error: error.message
}
}
registerPromiseWorker(data => {
return sqlReady
.then(processMsg.bind(data))
.catch(onError)
return sqlReady.then(processMsg.bind(data)).catch(onError)
})

View File

@@ -1,7 +1,4 @@
import fu from '@/lib/utils/fileIo'
// We can import workers like so because of worker-loader:
// https://webpack.js.org/loaders/worker-loader/
import Worker from './_worker.js'
// Use promise-worker in order to turn worker into the promise based one:
// https://github.com/nolanlawson/promise-worker
@@ -9,8 +6,10 @@ import PromiseWorker from 'promise-worker'
import events from '@/lib/utils/events'
function getNewDatabase () {
const worker = new Worker()
function getNewDatabase() {
const worker = new Worker(new URL('./_worker.js', import.meta.url), {
type: 'module'
})
return new Database(worker)
}
@@ -20,7 +19,7 @@ export default {
let progressCounterIds = 0
class Database {
constructor (worker) {
constructor(worker) {
this.dbName = null
this.schema = null
this.worker = worker
@@ -31,29 +30,33 @@ class Database {
const progress = e.data.progress
if (progress !== undefined) {
const id = e.data.id
this.importProgresses[id].dispatchEvent(new CustomEvent('progress', {
detail: progress
}))
this.importProgresses[id].dispatchEvent(
new CustomEvent('progress', {
detail: progress
})
)
}
})
}
shutDown () {
shutDown() {
this.worker.terminate()
}
createProgressCounter (callback) {
createProgressCounter(callback) {
const id = progressCounterIds++
this.importProgresses[id] = new EventTarget()
this.importProgresses[id].addEventListener('progress', e => { callback(e.detail) })
this.importProgresses[id].addEventListener('progress', e => {
callback(e.detail)
})
return id
}
deleteProgressCounter (id) {
deleteProgressCounter(id) {
delete this.importProgresses[id]
}
async addTableFromCsv (tabName, data, progressCounterId) {
async addTableFromCsv(tabName, data, progressCounterId) {
const result = await this.pw.postMessage({
action: 'import',
data,
@@ -68,9 +71,12 @@ class Database {
this.refreshSchema()
}
async loadDb (file) {
async loadDb(file) {
const fileContent = file ? await fu.readAsArrayBuffer(file) : null
const res = await this.pw.postMessage({ action: 'open', buffer: fileContent })
const res = await this.pw.postMessage({
action: 'open',
buffer: fileContent
})
if (res.error) {
throw new Error(res.error)
@@ -85,7 +91,7 @@ class Database {
})
}
async refreshSchema () {
async refreshSchema() {
const getSchemaSql = `
WITH columns as (
SELECT
@@ -103,7 +109,7 @@ class Database {
this.schema = JSON.parse(result.values.objects[0])
}
async execute (commands) {
async execute(commands) {
await this.pw.postMessage({ action: 'reopen' })
const results = await this.pw.postMessage({ action: 'exec', sql: commands })
@@ -114,7 +120,7 @@ class Database {
return results[results.length - 1]
}
async export (fileName) {
async export(fileName) {
const data = await this.pw.postMessage({ action: 'export' })
if (data.error) {
@@ -124,13 +130,15 @@ class Database {
events.send('database.export', data.byteLength, { to: 'sqlite' })
}
async validateTableName (name) {
async validateTableName(name) {
if (name.startsWith('sqlite_')) {
throw new Error("Table name can't start with sqlite_")
}
if (/[^\w]/.test(name)) {
throw new Error('Table name can contain only letters, digits and underscores')
throw new Error(
'Table name can contain only letters, digits and underscores'
)
}
if (/^(\d)/.test(name)) {
@@ -140,7 +148,7 @@ class Database {
await this.execute(`BEGIN; CREATE TABLE "${name}"(id); ROLLBACK;`)
}
sanitizeTableName (tabName) {
sanitizeTableName(tabName) {
return tabName
.replace(/[^\w]/g, '_') // replace everything that is not letter, digit or _ with _
.replace(/^(\d)/, '_$1') // add _ at beginning if starts with digit

8
src/lib/eventBus.js Normal file
View File

@@ -0,0 +1,8 @@
import emitter from 'tiny-emitter/instance'
export default {
$on: (...args) => emitter.on(...args),
$once: (...args) => emitter.once(...args),
$off: (...args) => emitter.off(...args),
$emit: (...args) => emitter.emit(...args)
}

327
src/lib/graphHelper.js Normal file
View File

@@ -0,0 +1,327 @@
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 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
})
})
}
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) {
return attributes =>
(attributes.color = tinycolor(attributes.data[source]).toHexString())
}
function getUpdateNodeColorMethod(graph, colorSettings) {
const {
type,
value,
source,
sourceUsage,
colorscale,
colorscaleDirection,
mode,
method
} = colorSettings
if (type === 'constant') {
return attributes => (attributes.color = value)
} else if (type === 'variable') {
return sourceUsage === 'map_to'
? getColorMethod(
graph,
mode,
(nodeId, attributes) => attributes.data[source],
colorscale,
colorscaleDirection,
getNodeValueScale
)
: getDirectVariableColorUpdateMethod(source)
} else {
return getColorMethod(
graph,
mode,
nodeId => graph[method](nodeId),
colorscale,
colorscaleDirection,
getNodeValueScale
)
}
}
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
) {
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 = colorMap[category]
}
} 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 = '#000000'
return
}
const exactMatch = normalizedColorscale.find(
([value]) => value === normalizedValue
)
if (exactMatch) {
attributes.color = tinycolor(exactMatch[1]).toHexString()
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)
}).toHexString()
}
}
}
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)
}

View File

@@ -1,5 +1,5 @@
export default {
_migrate (installedVersion, inquiries) {
_migrate(installedVersion, inquiries) {
if (installedVersion === 1) {
inquiries.forEach(inquire => {
inquire.viewType = 'chart'

View File

@@ -4,11 +4,13 @@ import events from '@/lib/utils/events'
import migration from './_migrations'
const migrate = migration._migrate
const myInquiriesKey = 'myInquiries'
export default {
version: 2,
getStoredInquiries () {
let myInquiries = JSON.parse(localStorage.getItem('myInquiries'))
myInquiriesKey,
getStoredInquiries() {
let myInquiries = JSON.parse(localStorage.getItem(myInquiriesKey))
if (!myInquiries) {
const oldInquiries = localStorage.getItem('myQueries')
if (oldInquiries) {
@@ -22,31 +24,39 @@ export default {
return (myInquiries && myInquiries.inquiries) || []
},
duplicateInquiry (baseInquiry) {
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
},
isTabNeedName (inquiryTab) {
isTabNeedName(inquiryTab) {
return inquiryTab.isPredefined || !inquiryTab.name
},
updateStorage (inquiries) {
localStorage.setItem('myInquiries', JSON.stringify({ version: this.version, inquiries }))
updateStorage(inquiries) {
localStorage.setItem(
myInquiriesKey,
JSON.stringify({ version: this.version, inquiries })
)
},
serialiseInquiries (inquiryList) {
serialiseInquiries(inquiryList) {
const preparedData = JSON.parse(JSON.stringify(inquiryList))
preparedData.forEach(inquiry => delete inquiry.isPredefined)
return JSON.stringify({ version: this.version, inquiries: preparedData }, null, 4)
return JSON.stringify(
{ version: this.version, inquiries: preparedData },
null,
4
)
},
deserialiseInquiries (str) {
deserialiseInquiries(str) {
const inquiries = JSON.parse(str)
let inquiryList = []
if (!inquiries.version) {
@@ -59,7 +69,9 @@ export default {
// Generate new ids if they are the same as existing inquiries
inquiryList.forEach(inquiry => {
const allInquiriesIds = this.getStoredInquiries().map(inquiry => inquiry.id)
const allInquiriesIds = this.getStoredInquiries().map(
inquiry => inquiry.id
)
if (allInquiriesIds.includes(inquiry.id)) {
inquiry.id = nanoid()
}
@@ -68,24 +80,23 @@ export default {
return inquiryList
},
importInquiries () {
return fu.importFile()
.then(str => {
const inquires = this.deserialiseInquiries(str)
importInquiries() {
return fu.importFile().then(str => {
const inquires = this.deserialiseInquiries(str)
events.send('inquiry.import', inquires.length)
events.send('inquiry.import', inquires.length)
return inquires
})
return inquires
})
},
export (inquiryList, fileName) {
export(inquiryList, fileName) {
const jsonStr = this.serialiseInquiries(inquiryList)
fu.exportToFile(jsonStr, fileName)
events.send('inquiry.export', inquiryList.length)
},
async readPredefinedInquiries () {
async readPredefinedInquiries() {
const res = await fu.readFile('./inquiries.json')
const data = await res.json()

View File

@@ -3,12 +3,14 @@ import time from '@/lib/utils/time'
import events from '@/lib/utils/events'
export default class Tab {
constructor (state, inquiry = {}) {
constructor(state, inquiry = {}) {
this.id = inquiry.id || nanoid()
this.name = inquiry.id ? inquiry.name : null
this.tempName = inquiry.name || (state.untitledLastIndex
? `Untitled ${state.untitledLastIndex}`
: 'Untitled')
this.tempName =
inquiry.name ||
(state.untitledLastIndex
? `Untitled ${state.untitledLastIndex}`
: 'Untitled')
this.query = inquiry.query
this.viewOptions = inquiry.viewOptions || undefined
this.isPredefined = inquiry.isPredefined
@@ -26,9 +28,10 @@ export default class Tab {
this.isSaved = !!inquiry.id
this.state = state
this.updatedAt = inquiry.updatedAt
}
async execute () {
async execute() {
this.isGettingResults = true
this.result = null
this.error = null
@@ -39,7 +42,8 @@ export default class Tab {
this.time = time.getPeriod(start, new Date())
if (this.result && this.result.values) {
events.send('resultset.create',
events.send(
'resultset.create',
this.result.values[this.result.columns[0]].length
)
}

View File

@@ -2,14 +2,14 @@ import Lib from 'plotly.js/src/lib'
import dataUrlToBlob from 'dataurl-to-blob'
export default {
async copyText (str, notifyMessage) {
async copyText(str, notifyMessage) {
await navigator.clipboard.writeText(str)
if (notifyMessage) {
Lib.notifier(notifyMessage, 'long')
}
},
async copyImage (source) {
async copyImage(source) {
if (source instanceof HTMLCanvasElement) {
return this._copyCanvas(source)
} else {
@@ -17,24 +17,29 @@ export default {
}
},
async _copyBlob (blob) {
async _copyBlob(blob) {
await navigator.clipboard.write([
new ClipboardItem({ // eslint-disable-line no-undef
new ClipboardItem({
// eslint-disable-line no-undef
[blob.type]: blob
})
])
},
async _copyFromDataUrl (url) {
async _copyFromDataUrl(url) {
const blob = dataUrlToBlob(url)
await this._copyBlob(blob)
Lib.notifier('Image copied to clipboard successfully', 'long')
},
async _copyCanvas (canvas) {
canvas.toBlob(async (blob) => {
await this._copyBlob(blob)
Lib.notifier('Image copied to clipboard successfully', 'long')
}, 'image/png', 1)
async _copyCanvas(canvas) {
canvas.toBlob(
async blob => {
await this._copyBlob(blob)
Lib.notifier('Image copied to clipboard successfully', 'long')
},
'image/png',
1
)
}
}

View File

@@ -1,5 +1,5 @@
export default {
send (name, value, labels) {
send(name, value, labels) {
const event = new CustomEvent('sqliteviz-app-event', {
detail: {
name,

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