1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-06 10:08:52 +08:00

93 Commits

Author SHA1 Message Date
lana-k
4232f15c04 autostart, reset and fixes 2025-08-17 21:21:21 +02:00
lana-k
9d562d11b8 export and background 2025-06-09 21:08:51 +02:00
lana-k
54cdbbc8b9 wip 2025-06-06 21:24:04 +02:00
lana-k
1601514cca wip 2025-06-01 19:16:26 +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
3a05b27400 #121 0.25.1 2025-01-12 22:03:06 +01:00
lana-k
108d96a753 #121 fix lint 2025-01-12 22:00:48 +01:00
lana-k
f55a8caa92 #121 tests 2025-01-12 21:42:17 +01:00
lana-k
87f9f9eb01 use actions, add store tests 2025-01-05 22:30:12 +01:00
lana-k
d6408bdd85 #121 save inquiries in store 2025-01-05 21:06:06 +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
lana-k
378b9fb580 #113 upgrade plotly 2024-09-23 16:46:50 +02:00
lana-k
244ba9eb08 #116 add JSON/NDJSON 2024-09-17 11:35:53 +02:00
lana-k
53e5194295 #116 update tests 2024-09-16 23:49:02 +02:00
lana-k
04274ef19a #116 fix lint 2024-09-15 18:08:46 +02:00
lana-k
3893a66f4e Merge branch 'master' of github.com:lana-k/sqliteviz 2024-09-05 22:15:38 +02:00
lana-k
1b6b7c71e9 #116 JSON file import 2024-09-05 22:15:12 +02:00
saaj
3f6427ff0e Build sqlitelua for scalar, aggregate & table-valued UDFs in Lua (#118)
* Update base Docker images

* Use performance.now() instead of Date.now() for time promise tests

* Build sqlitelua: user scalar, aggregate & table-valued functions in Lua
2024-08-25 21:03:34 +02:00
lana-k
a2464d839f #115 fix version number 2024-01-07 13:55:38 +01:00
lana-k
316e603c3c #115 style fixes 2024-01-07 13:37:21 +01:00
lana-k
88466eca5e #115 fix lint 2024-01-07 12:31:53 +01:00
lana-k
5123e39a60 #115 version 2024-01-07 12:14:08 +01:00
lana-k
4c8401f32f #115 scroll record to beginning 2024-01-06 20:36:43 +01:00
lana-k
d949629ee4 #115 fix new lines - use pre 2024-01-06 18:55:45 +01:00
lana-k
7a18e415c8 #115 add styles for blob and null 2024-01-06 16:51:35 +01:00
lana-k
878689b3f7 fix svg button state 2024-01-06 12:03:06 +01:00
lana-k
42f040975d #115 tests 2024-01-06 11:23:23 +01:00
lana-k
78e9ca2120 #115 fix tests 2024-01-03 18:26:07 +01:00
lana-k
96af391f20 #115 clear message 2024-01-02 13:57:42 +01:00
lana-k
f58b62eb0c #115 add messages 2023-12-27 23:00:05 +01:00
lana-k
b17040d3ef #115 copy cell value 2023-12-27 22:22:49 +01:00
lana-k
bc6154b9ad #115 add icons 2023-12-27 21:30:43 +01:00
lana-k
3aea8c951b #115 update value when switch row 2023-12-26 20:45:11 +01:00
lana-k
1e982a1196 #115 unselect on paging 2023-10-31 22:27:47 +01:00
lana-k
6ecbde7fd3 #115 style fixes 2023-10-31 20:48:30 +01:00
lana-k
5ee881432a #115 select cell between modes; pass record number 2023-10-29 20:01:51 +01:00
lana-k
735e4ec7f6 #115 record and row navigator 2023-10-28 22:51:28 +02:00
lana-k
07d31dbfe9 #115 unselect 2023-10-28 19:48:36 +02:00
lana-k
ac1f7de62c #115 formats and call selections 2023-10-27 22:50:54 +02:00
lana-k
96877de532 #115 move focus 2023-10-27 18:47:45 +02:00
lana-k
b60fc28e47 #115 json view 2023-10-27 17:14:14 +02:00
lana-k
bec3d9c737 #115 add split in result set 2023-10-25 20:43:22 +02:00
lana-k
8aac7af481 update package.json 2023-07-03 23:33:52 +02:00
lana-k
6982204e68 Update currentTab when close tabs #112 2023-07-03 23:13:09 +02:00
lana-k
41e0ae7332 fix test for firefox #110 2023-06-29 23:14:08 +02:00
lana-k
ebb5af4f10 send event when sharing 2023-06-29 22:57:39 +02:00
lana-k
ae26358b25 add test #110 2023-06-29 22:28:41 +02:00
lana-k
d9ee702b8e update papaparse #111 2023-06-29 22:14:28 +02:00
lana-k
446045fa55 Catch parsing errors in compete #110 2023-06-29 22:13:56 +02:00
lana-k
1a9d1b308b check data format #109 2023-06-10 20:05:42 +02:00
lana-k
014ecf145e update version 2023-06-10 19:11:15 +02:00
lana-k
0044d82b6f Loading remote database and inquiries #109 2023-06-05 22:31:39 +02:00
lana-k
998e8d66f7 Tab refactor 2023-06-01 14:42:51 +02:00
lana-k
db3dbdf993 Merge branch 'master' of github.com:lana-k/sqliteviz 2023-05-17 21:41:17 +02:00
lana-k
4e13a16e33 No blocking while loading predifined #107 2023-05-17 21:37:41 +02:00
saaj
9c0103fd05 SQLite 3.41.0 and pearson correlation extension function (#106)
* Build SQLite 3.41.0

* Update pivot_vtab

* Add Pearson correlation coefficient function extension, build

* Add an easy way to running test locally without Nodejs

* Use RSS sum to pick top2 processes for the report

* Try previous Ubuntu LTS as a workaround for Firefox worker not starting
2023-03-04 22:51:25 +01:00
saaj
e4b117ffb9 Sqljs upgrade and benchmark improvements (#103)
* Update to sql.js 1.7.0

* Update to emsdk 3.0.1, replace/remove deprecated/irrelevant settings

- Renamed .bc extension to .o
- Remove deprecated INLINING_LIMIT setting
- Remove SINGLE_FILE

* Update SQLite to 3.39.3

* Collect and plot CPU and RSS charts from the benchmark containers

* Move procpath commands to a playbook, plot only top 2 RSS & CPU usage

* Optimise for size, put -flto for both compile and link
2023-03-04 17:00:46 +01:00
lana-k
6320f818cb fix undefined in tests and chart metrics 2022-07-30 16:42:30 +02:00
lana-k
3c456ef135 fix sql hint: read properties of undefined #99 2022-07-29 15:27:01 +02:00
lana-k
c713c713b7 fix paths #97 2022-07-20 23:15:14 +02:00
lana-k
babf0074c0 add artifact with source map #97 2022-07-20 22:49:26 +02:00
lana-k
e71e6700c1 improve events 2022-07-20 22:47:40 +02:00
lana-k
84e66b8167 Update README.md 2022-07-10 23:18:52 +02:00
205 changed files with 32962 additions and 32806 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,42 +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: 10.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@7
- 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 archive
run: |
cd dist
zip -9 -r dist.zip . -x "js/*.map" -x "/*.map"
- 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/dist.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,35 @@ on:
workflow_dispatch:
push:
branches:
- 'master'
- 'master'
pull_request:
branches:
- 'master'
- 'master'
jobs:
test:
name: Run tests
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 10.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 apt-get update
sudo apt-get install -y chromium-browser firefox
- name: Update npm
run: npm install -g npm@7
- 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: npm run test

7
.prettierrc Normal file
View File

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

24
Dockerfile.test Normal file
View File

@@ -0,0 +1,24 @@
# An easy way to run tests locally without Nodejs installed:
#
# docker build -t sqliteviz/test -f Dockerfile.test .
#
FROM node:12.22-buster
RUN set -ex; \
apt update; \
apt install -y chromium firefox-esr; \
npm install -g npm@7
WORKDIR /tmp/build
COPY package.json package-lock.json ./
COPY lib lib
RUN npm install
COPY . .
RUN set -ex; \
sed -i 's/browsers: \[.*\],/browsers: ['"'FirefoxHeadlessTouch'"'],/' karma.conf.js
RUN npm run lint -- --no-fix && npm run test

View File

@@ -4,11 +4,13 @@
# sqliteviz
Sqliteviz is a single-page offline-first PWA for fully client-side visualisation of SQLite databases or CSV files.
Sqliteviz is a single-page offline-first PWA for fully client-side visualisation
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
- import a CSV file into a SQLite database and visualize imported data
- 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
- import/export inquiries from/to a JSON file
@@ -18,15 +20,19 @@ 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 [Wiki][7].
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].
[1]: https://github.com/plotly/falcon
@@ -35,7 +41,7 @@ It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4]
[4]: https://github.com/sql-js/sql.js
[5]: https://github.com/vuejs/vue
[6]: https://sqliteviz.com/app/
[7]: https://github.com/lana-k/sqliteviz/wiki
[7]: https://sqliteviz.com/docs
[8]: https://github.com/surmon-china/vue-codemirror#readme
[9]: https://www.papaparse.com/
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries

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

@@ -1,4 +1,4 @@
FROM emscripten/emsdk:2.0.24
FROM emscripten/emsdk:3.0.1
WORKDIR /tmp/build

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
@@ -43,6 +43,10 @@ SQLite [miscellaneous extensions][3] included:
SQLite 3rd party extensions included:
1. [pivot_vtab][5] -- a pivot virtual table
2. `pearson` correlation coefficient function extension from [sqlean][21]
(which is part of [squib][20])
3. [sqlitelua][22] -- a virtual table `luafunctions` which allows to define custom scalar,
aggregate and table-valued functions in Lua
To ease the step to have working clone locally, the build is committed into
the repository.
@@ -80,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
@@ -99,3 +103,6 @@ described in [this message from SQLite Forum][12]:
[17]: https://sqlite.org/contrib/
[18]: https://sqlite.org/contrib//download/extension-functions.c?get=25
[19]: https://github.com/lana-k/sqliteviz/blob/master/tests/lib/database/sqliteExtensions.spec.js
[20]: https://github.com/mrwilson/squib/blob/master/pearson.c
[21]: https://github.com/nalgeon/sqlean/blob/incubator/src/pearson.c
[22]: https://github.com/kev82/sqlitelua

View File

@@ -1,10 +1,8 @@
FROM node:14-bullseye
FROM node:20.14-bookworm
RUN set -ex; \
echo 'deb http://deb.debian.org/debian unstable main' \
> /etc/apt/sources.list.d/unstable.list; \
apt-get update; \
apt-get install -y -t unstable firefox; \
apt-get install -y firefox-esr; \
apt-get install -y chromium
WORKDIR /tmp/build

View File

@@ -1,14 +1,25 @@
# SQLite WebAssembly build micro-benchmark
This directory contains a micro-benchmark for evaluating SQLite
WebAssembly builds performance on typical SQL queries, run from
`make.sh` script. It can also serve as a smoke test.
This directory contains a micro-benchmark for evaluating SQLite WebAssembly
builds performance on read and write SQL queries, run from `make.sh` script. If
the script has permission to `nice` processes and [Procpath][1] is installed,
e.g. it is run with `sudo -E env PATH=$PATH ./make.sh`, it'll `renice` all
processes running inside the benchmark containers. It can also serve as a smoke
test (e.g. for memory leaks).
The benchmark operates on a set of SQLite WebAssembly builds expected
in `lib/build-$NAME` directories each containing `sql-wasm.js` and
`sql-wasm.wasm`. Then it creates a Docker image for each, and runs
the benchmark in Firefox and Chromium using Karma in the container.
The benchmark operates on a set of SQLite WebAssembly builds expected in
`lib/build-$NAME` directories each containing `sql-wasm.js` and
`sql-wasm.wasm`. Then it creates a Docker image for each, and runs the
benchmark in Firefox and Chromium using Karma in the container.
After successful run, the benchmark result of each build is contained
in `build-$NAME-result.json`. The JSON result files can be analysed
using `result-analysis.ipynb` Jupyter notebook.
After successful run, the benchmark produces the following per each build:
- `build-$NAME-result.json`
- `build-$NAME.sqlite` (if Procpath is installed)
- `build-$NAME.svg` (if Procpath is installed)
These files can be analysed using `result-analysis.ipynb` Jupyter notebook.
The SVG is a chart with CPU and RSS usage of each test container (i.e. Chromium
run, then Firefox run per container).
[1]: https://pypi.org/project/Procpath/

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

@@ -1,7 +1,8 @@
#!/bin/bash -e
cleanup () {
rm -rf lib/dist $flag_file
rm -rf lib/dist "$renice_flag_file"
docker rm -f sqljs-benchmark-run 2> /dev/null || true
}
trap cleanup EXIT
@@ -11,34 +12,36 @@ if [ ! -f sample.csv ]; then
| gunzip -c > sample.csv
fi
PLAYBOOK=procpath/karma_docker.procpath
# for renice to work run like "sudo -E env PATH=$PATH ./make.sh"
test_ni=$(nice -n -1 nice)
if [ $test_ni == -1 ]; then
flag_file=$(mktemp)
test_ni=$(nice -n -5 nice)
if [ $test_ni == -5 ]; then
renice_flag_file=$(mktemp)
fi
(
while [ -f $flag_file ]; do
root_pid=$(
docker ps -f status=running -f name='^sqljs-benchmark-' -q \
| xargs -r -I{} -- docker inspect -f '{{.State.Pid}}' {}
)
if [ ! -z $root_pid ]; then
procpath query -d $'\n' "$..children[?(@.stat.pid == $root_pid)]..pid" \
| xargs -I{} -- renice -n -1 -p {} > /dev/null
fi
sleep 1
done &
)
{
while [ -f $renice_flag_file ]; do
procpath --logging-level ERROR play -f $PLAYBOOK renice:watch
done
} &
shopt -s nullglob
for d in lib/build-* ; do
rm -rf lib/dist
cp -r $d lib/dist
sample_name=$(basename $d)
name=$(basename $d)
docker build -t sqliteviz/sqljs-benchmark:$name .
docker rm sqljs-benchmark-$name 2> /dev/null || true
docker run -it --cpus 2 --name sqljs-benchmark-$name sqliteviz/sqljs-benchmark:$name
docker cp sqljs-benchmark-$name:/tmp/build/suite-result.json ${name}-result.json
docker rm sqljs-benchmark-$name
docker build -t sqliteviz/sqljs-benchmark .
docker rm sqljs-benchmark-run 2> /dev/null || true
docker run -d -it --cpus 2 --name sqljs-benchmark-run sqliteviz/sqljs-benchmark
{
rm -f ${sample_name}.sqlite
procpath play -f $PLAYBOOK -o database_file=${sample_name}.sqlite track:record
procpath play -f $PLAYBOOK -o database_file=${sample_name}.sqlite \
-o plot_file=${sample_name}.svg track:plot
} &
docker attach sqljs-benchmark-run
docker cp sqljs-benchmark-run:/tmp/build/suite-result.json ${sample_name}-result.json
docker rm sqljs-benchmark-run
done

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

@@ -0,0 +1,28 @@
# This command may run when "sqljs-benchmark-run" does not yet exist or run
[renice:watch]
interval: 2
repeat: 30
environment:
ROOT_PID=docker inspect -f "{{.State.Pid}}" sqljs-benchmark-run 2> /dev/null || true
query:
PIDS=$..children[?(@.stat.pid in [$ROOT_PID])]..pid
command:
echo $PIDS | tr , '\n' | xargs --no-run-if-empty -I{} -- renice -n -5 -p {}
# Expected input arguments: database_file
[track:record]
interval: 1
stop_without_result: 1
environment:
ROOT_PID=docker inspect -f "{{.State.Pid}}" sqljs-benchmark-run
query:
$..children[?(@.stat.pid == $ROOT_PID)]
pid_list: $ROOT_PID
# Expected input arguments: database_file, plot_file
[track:plot]
moving_average_window: 5
title: Chromium vs Firefox (№1 RSS, №2 CPU)
custom_query_file:
procpath/top2_rss.sql
procpath/top2_cpu.sql

View File

@@ -0,0 +1,29 @@
WITH diff_all AS (
SELECT
record_id,
ts,
stat_pid,
stat_utime + stat_stime - LAG(stat_utime + stat_stime) OVER (
PARTITION BY stat_pid
ORDER BY record_id
) tick_diff,
ts - LAG(ts) OVER (
PARTITION BY stat_pid
ORDER BY record_id
) ts_diff
FROM record
), diff AS (
SELECT * FROM diff_all WHERE tick_diff IS NOT NULL
), one_time_pid_condition AS (
SELECT stat_pid
FROM record
GROUP BY 1
ORDER BY SUM(stat_utime + stat_stime) DESC
LIMIT 2
)
SELECT
ts,
stat_pid pid,
100.0 * tick_diff / (SELECT value FROM meta WHERE key = 'clock_ticks') / ts_diff value
FROM diff
JOIN one_time_pid_condition USING(stat_pid)

View File

@@ -0,0 +1,13 @@
WITH one_time_pid_condition AS (
SELECT stat_pid
FROM record
GROUP BY 1
ORDER BY SUM(stat_rss) DESC
LIMIT 2
)
SELECT
ts,
stat_pid pid,
stat_rss / 1024.0 / 1024 * (SELECT value FROM meta WHERE key = 'page_size') value
FROM record
JOIN one_time_pid_condition USING(stat_pid)

File diff suppressed because one or more lines are too long

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

@@ -2,9 +2,11 @@ import logging
import subprocess
from pathlib import Path
# See the setting descriptions on these pages:
# - https://emscripten.org/docs/optimizing/Optimizing-Code.html
# - https://github.com/emscripten-core/emscripten/blob/main/src/settings.js
cflags = (
'-O2',
# SQLite configuration
'-DSQLITE_DEFAULT_CACHE_SIZE=-65536', # 64 MiB
'-DSQLITE_DEFAULT_MEMSTATUS=0',
'-DSQLITE_DEFAULT_SYNCHRONOUS=0',
@@ -13,26 +15,27 @@ cflags = (
'-DSQLITE_ENABLE_FTS3',
'-DSQLITE_ENABLE_FTS3_PARENTHESIS',
'-DSQLITE_ENABLE_FTS5',
'-DSQLITE_ENABLE_JSON1',
'-DSQLITE_ENABLE_NORMALIZE',
'-DSQLITE_EXTRA_INIT=extra_init',
'-DSQLITE_OMIT_DEPRECATED',
'-DSQLITE_OMIT_LOAD_EXTENSION',
'-DSQLITE_OMIT_SHARED_CACHE',
'-DSQLITE_THREADSAFE=0',
# Compile-time optimisation
'-Os', # reduces the code size about in half comparing to -O2
'-flto',
'-Isrc', '-Isrc/lua',
)
emflags = (
# Base
'--memory-init-file', '0',
'-s', 'RESERVED_FUNCTION_POINTERS=64',
'-s', 'ALLOW_TABLE_GROWTH=1',
'-s', 'SINGLE_FILE=0',
# WASM
'-s', 'WASM=1',
'-s', 'ALLOW_MEMORY_GROWTH=1',
# Optimisation
'-s', 'INLINING_LIMIT=50',
'-O3',
'-s', 'ENVIRONMENT=web,worker',
# Link-time optimisation
'-Os',
'-flto',
# sql.js
'-s', 'EXPORTED_FUNCTIONS=@src/sqljs/exported_functions.json',
@@ -50,22 +53,32 @@ def build(src: Path, dst: Path):
'emcc',
*cflags,
'-c', src / 'sqlite3.c',
'-o', out / 'sqlite3.bc',
'-o', out / 'sqlite3.o',
])
logging.info('Building LLVM bitcode for extension-functions.c')
subprocess.check_call([
'emcc',
*cflags,
'-c', src / 'extension-functions.c',
'-o', out / 'extension-functions.bc',
'-o', out / 'extension-functions.o',
])
logging.info('Building LLVM bitcode for SQLite Lua extension')
subprocess.check_call([
'emcc',
*cflags,
'-shared',
*(src / 'lua').glob('*.c'),
*(src / 'sqlitelua').glob('*.c'),
'-o', out / 'sqlitelua.o',
])
logging.info('Building WASM from bitcode')
subprocess.check_call([
'emcc',
*emflags,
out / 'sqlite3.bc',
out / 'extension-functions.bc',
out / 'sqlite3.o',
out / 'extension-functions.o',
out / 'sqlitelua.o',
'-o', out / 'sql-wasm.js',
])

View File

@@ -1,14 +1,16 @@
import logging
import re
import shutil
import subprocess
import sys
import tarfile
import zipfile
from io import BytesIO
from pathlib import Path
from urllib import request
amalgamation_url = 'https://sqlite.org/2022/sqlite-amalgamation-3390000.zip'
amalgamation_url = 'https://sqlite.org/2023/sqlite-amalgamation-3410000.zip'
# Extension-functions
# ===================
@@ -28,10 +30,17 @@ extension_urls = (
('https://sqlite.org/src/raw/09f967dc?at=decimal.c', 'sqlite3_decimal_init'),
# Third-party extension
# =====================
('https://github.com/jakethaw/pivot_vtab/raw/08ab0797/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
('https://github.com/jakethaw/pivot_vtab/raw/9323ef93/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
# ===========================================
('https://github.com/kev82/sqlitelua/raw/db479510/src/main.c', 'sqlite3_luafunctions_init'),
)
sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.5.0.zip'
lua_url = 'http://www.lua.org/ftp/lua-5.3.5.tar.gz'
sqlitelua_url = 'https://github.com/kev82/sqlitelua/archive/db479510.zip'
sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.7.0.zip'
def _generate_extra_init_c_function(init_function_names):
@@ -58,6 +67,38 @@ def _get_amalgamation(tgt: Path):
shutil.copyfileobj(fr, fw)
def _get_lua(tgt: Path):
# Library definitions from lua/Makefile
lib_str = '''
CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o \
lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o \
ltm.o lundump.o lvm.o lzio.o
LIB_O= lauxlib.o lbaselib.o lbitlib.o lcorolib.o ldblib.o liolib.o \
lmathlib.o loslib.o lstrlib.o ltablib.o lutf8lib.o loadlib.o linit.o
LUA_O= lua.o
'''
header_only_files = {'lprefix', 'luaconf', 'llimits', 'lualib'}
lib_names = set(re.findall(r'(\w+)\.o', lib_str)) | header_only_files
logging.info('Downloading and extracting Lua %s', lua_url)
archive = tarfile.open(fileobj=BytesIO(request.urlopen(lua_url).read()))
(tgt / 'lua').mkdir()
for tarinfo in archive:
tarpath = Path(tarinfo.name)
if tarpath.match('src/*') and tarpath.stem in lib_names:
with (tgt / 'lua' / tarpath.name).open('wb') as fw:
shutil.copyfileobj(archive.extractfile(tarinfo), fw)
logging.info('Downloading and extracting SQLite Lua extension %s', sqlitelua_url)
archive = zipfile.ZipFile(BytesIO(request.urlopen(sqlitelua_url).read()))
archive_root_dir = zipfile.Path(archive, archive.namelist()[0])
(tgt / 'sqlitelua').mkdir()
for zpath in (archive_root_dir / 'src').iterdir():
if zpath.name != 'main.c':
with zpath.open() as fr, (tgt / 'sqlitelua' / zpath.name).open('wb') as fw:
shutil.copyfileobj(fr, fw)
def _get_contrib_functions(tgt: Path):
request.urlretrieve(contrib_functions_url, tgt / 'extension-functions.c')
@@ -69,6 +110,7 @@ def _get_extensions(tgt: Path):
for url, init_fn in extension_urls:
logging.info('Downloading and appending to amalgamation %s', url)
with request.urlopen(url) as resp:
f.write(b'\n')
shutil.copyfileobj(resp, f)
init_functions.append(init_fn)
@@ -89,6 +131,7 @@ def _get_sqljs(tgt: Path):
def configure(tgt: Path):
_get_amalgamation(tgt)
_get_contrib_functions(tgt)
_get_lua(tgt)
_get_extensions(tgt)
_get_sqljs(tgt)

File diff suppressed because one or more lines are too long

Binary file not shown.

45752
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,62 +1,85 @@
{
"name": "sqliteviz",
"version": "0.20.0",
"version": "0.26.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": {
"codemirror": "^5.57.0",
"@sigma/export-image": "^3.0.0",
"buffer": "^6.0.3",
"codemirror": "^5.65.18",
"codemirror-editor-vue3": "^2.8.0",
"core-js": "^3.6.5",
"dataurl-to-blob": "^0.0.1",
"graphology": "^0.26.0",
"graphology-layout": "^0.6.1",
"graphology-layout-forceatlas2": "^0.10.1",
"html2canvas": "^1.1.4",
"jquery": "^3.6.0",
"nanoid": "^3.1.12",
"papaparse": "^5.3.1",
"papaparse": "^5.4.1",
"pivottable": "^2.23.0",
"plotly.js": "^1.58.4",
"plotly.js": "^2.35.2",
"promise-worker": "^2.0.1",
"react": "^16.13.1",
"react-chart-editor": "^0.45.0",
"react-dom": "^16.13.1",
"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",
"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"
"eslint-plugin-vue": "^9.28.0",
"flush-promises": "^1.0.2",
"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

@@ -1,6 +1,6 @@
{
"background_color": "white",
"description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases or CSV.",
"description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases, CSV, JSON or NDJSON.",
"display": "fullscreen",
"icons": [
{
@@ -27,4 +27,4 @@
"name": "sqliteviz",
"short_name": "sqliteviz",
"start_url": "index.html"
}
}

View File

@@ -1,58 +1,85 @@
<template>
<div id="app">
<router-view/>
<router-view />
<modals-container />
</div>
</template>
<script>
import storedInquiries from '@/lib/storedInquiries'
import { ModalsContainer } from 'vue-final-modal'
export default {
components: { ModalsContainer },
computed: {
inquiries() {
return this.$store.state.inquiries
}
},
watch: {
inquiries: {
deep: true,
handler() {
storedInquiries.updateStorage(this.inquiries)
}
}
},
created() {
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
}
}
</script>
<style>
@font-face {
font-family: "Open Sans";
src: url("~@/assets/fonts/OpenSans-Regular.woff2");
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-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-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-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-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-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,4 @@
color: var(--color-text-base);
font-size: 13px;
padding: 0 24px;
}
}

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);
@@ -107,3 +108,9 @@ table.sqliteviz-table {
font-size: 11px;
color: var(--color-text-base);
}
.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

@@ -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,387 +0,0 @@
<template>
<modal
:name="dialogName"
classes="dialog"
height="auto"
width="80%"
scrollable
:clickToClose="false"
>
<div class="dialog-header">
CSV import
<close-icon @click="cancelCsvImport" :disabled="disableDialog"/>
</div>
<div class="dialog-body">
<text-field
label="Table name"
v-model="tableName"
width="484px"
:disabled="disableDialog"
:error-msg="tableNameError"
id="csv-table-name"
/>
<div class="chars">
<delimiter-selector
v-model="delimiter"
width="210px"
class="char-input"
@input="previewCsv"
:disabled="disableDialog"
/>
<text-field
label="Quote char"
hint="The character used to quote fields."
v-model="quoteChar"
width="93px"
:disabled="disableDialog"
class="char-input"
id="quote-char"
/>
<text-field
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"
width="93px"
:disabled="disableDialog"
class="char-input"
id="escape-char"
/>
</div>
<check-box
@click="header = $event"
:init="true"
label="Use first row as column headers"
:disabled="disableDialog"
/>
<sql-table
v-if="previewData
&& (previewData.rowCount > 0 || Object.keys(previewData).length > 0)
"
:data-set="previewData"
class="preview-table"
:preview="true"
/>
<div v-else class="no-data">No data</div>
<logs
class="import-csv-errors"
:messages="importCsvMessages"
/>
</div>
<div class="dialog-buttons-container">
<button
class="secondary"
:disabled="disableDialog"
@click="cancelCsvImport"
id="csv-cancel"
>
Cancel
</button>
<button
v-show="!importCsvCompleted"
class="primary"
:disabled="disableDialog"
@click="loadFromCsv(file)"
id="csv-import"
>
Import
</button>
<button
v-show="importCsvCompleted"
class="primary"
:disabled="disableDialog"
@click="finish"
id="csv-finish"
>
Finish
</button>
</div>
</modal>
</template>
<script>
import csv from '@/lib/csv'
import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField'
import DelimiterSelector from './DelimiterSelector'
import CheckBox from '@/components/CheckBox'
import SqlTable from '@/components/SqlTable'
import Logs from '@/components/Logs'
import time from '@/lib/utils/time'
import fIo from '@/lib/utils/fileIo'
import { send } from '@/lib/utils/events'
export default {
name: 'CsvImport',
components: {
CloseIcon,
TextField,
DelimiterSelector,
CheckBox,
SqlTable,
Logs
},
props: ['file', 'db', 'dialogName'],
data () {
return {
disableDialog: false,
tableName: '',
delimiter: '',
quoteChar: '"',
escapeChar: '"',
header: true,
importCsvCompleted: false,
importCsvMessages: [],
previewData: null,
addedTable: null,
tableNameError: ''
}
},
watch: {
quoteChar () {
this.previewCsv()
},
escapeChar () {
this.previewCsv()
},
header () {
this.previewCsv()
},
tableName: time.debounce(function () {
this.tableNameError = ''
if (!this.tableName) {
return
}
this.db.validateTableName(this.tableName)
.catch(err => {
this.tableNameError = err.message + '. Try another table name.'
})
}, 400)
},
methods: {
cancelCsvImport () {
if (!this.disableDialog) {
if (this.addedTable) {
this.db.execute(`DROP TABLE "${this.addedTable}"`)
this.db.refreshSchema()
}
this.$modal.hide(this.dialogName)
this.$emit('cancel')
}
},
reset () {
this.header = true
this.quoteChar = '"'
this.escapeChar = '"'
this.delimiter = ''
this.tableName = ''
this.disableDialog = false
this.importCsvCompleted = false
this.importCsvMessages = []
this.previewData = null
this.addedTable = null
this.tableNameError = ''
},
open () {
this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file))
this.$modal.show(this.dialogName)
},
async previewCsv () {
this.importCsvCompleted = false
const config = {
preview: 3,
quoteChar: this.quoteChar || '"',
escapeChar: this.escapeChar,
header: this.header,
delimiter: this.delimiter
}
try {
const start = new Date()
const parseResult = await csv.parse(this.file, config)
const end = new Date()
this.previewData = parseResult.data
this.delimiter = parseResult.delimiter
// In parseResult.messages we can get parse errors
this.importCsvMessages = parseResult.messages || []
if (!parseResult.hasErrors) {
this.importCsvMessages.push({
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
type: 'success'
})
}
} catch (err) {
this.importCsvMessages = [{
message: err,
type: 'error'
}]
}
},
async loadFromCsv (file) {
if (!this.tableName) {
this.tableNameError = "Table name can't be empty"
return
}
this.disableDialog = true
const config = {
quoteChar: this.quoteChar || '"',
escapeChar: this.escapeChar,
header: this.header,
delimiter: this.delimiter
}
const parseCsvMsg = {
message: 'Parsing CSV...',
type: 'info'
}
this.importCsvMessages.push(parseCsvMsg)
const parseCsvLoadingIndicator = setTimeout(() => { parseCsvMsg.type = 'loading' }, 1000)
const importMsg = {
message: 'Importing CSV into a SQLite database...',
type: 'info'
}
let importLoadingIndicator = null
const updateProgress = progress => {
this.$set(importMsg, 'progress', progress)
}
const progressCounterId = this.db.createProgressCounter(updateProgress)
try {
let start = new Date()
const parseResult = await csv.parse(this.file, config)
let end = new Date()
if (!parseResult.hasErrors) {
const rowCount = parseResult.rowCount
let period = time.getPeriod(start, end)
parseCsvMsg.type = 'success'
if (parseResult.messages.length > 0) {
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
parseCsvMsg.message = `${rowCount} rows are parsed in ${period}.`
} else {
// Inform about csv parsing success
parseCsvMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
}
// Loading indicator for csv parsing is not needed anymore
clearTimeout(parseCsvLoadingIndicator)
// Add info about import start
this.importCsvMessages.push(importMsg)
// Show import progress after 1 second
importLoadingIndicator = setTimeout(() => {
importMsg.type = 'loading'
}, 1000)
// Add table
start = new Date()
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 CSV into a SQLite database is completed in ${period}.`
importMsg.type = 'success'
// Loading indicator for import is not needed anymore
clearTimeout(importLoadingIndicator)
this.importCsvCompleted = true
} else {
parseCsvMsg.message = 'Parsing ended with errors.'
parseCsvMsg.type = 'info'
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
}
} catch (err) {
if (parseCsvMsg.type === 'loading') {
parseCsvMsg.type = 'info'
}
if (importMsg.type === 'loading') {
importMsg.type = 'info'
}
this.importCsvMessages.push({
message: err,
type: 'error'
})
}
clearTimeout(parseCsvLoadingIndicator)
clearTimeout(importLoadingIndicator)
this.db.deleteProgressCounter(progressCounterId)
this.disableDialog = false
},
async finish () {
this.$modal.hide(this.dialogName)
const stmt = [
'/*',
` * 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')
const tabId = await this.$store.dispatch('addTab', { query: stmt })
this.$store.commit('setCurrentTabId', tabId)
this.importCsvCompleted = false
this.$emit('finish')
send('inquiry.create', undefined, { auto: true })
}
}
}
</script>
<style scoped>
.dialog-body {
padding-bottom: 0;
}
.chars {
display: flex;
align-items: flex-end;
margin: 24px 0 20px;
}
.char-input {
margin-right: 44px;
}
.preview-table {
margin-top: 18px;
}
.import-csv-errors {
height: 136px;
margin-top: 8px;
}
.no-data {
margin-top: 32px;
background-color: white;
border-radius: 5px;
position: relative;
border: 1px solid var(--color-border-light);
box-sizing: border-box;
height: 147px;
font-size: 13px;
color: var(--color-text-base);
display: flex;
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,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

@@ -0,0 +1,518 @@
<template>
<modal
:modalId="dialogName"
class="dialog"
contentClass="import-modal"
scrollable
:clickToClose="false"
>
<div class="dialog-header">
{{ typeName }} import
<close-icon :disabled="disableDialog" @click="cancelImport" />
</div>
<div class="dialog-body">
<text-field
id="csv-json-table-name"
v-model="tableName"
label="Table name"
width="484px"
:disabled="disableDialog"
:errorMsg="tableNameError"
/>
<div v-if="!isJson && !isNdJson" class="chars">
<delimiter-selector
v-model="delimiter"
width="210px"
class="char-input"
:disabled="disableDialog"
@input="preview"
/>
<text-field
id="quote-char"
v-model="quoteChar"
label="Quote char"
hint="The character used to quote fields."
width="93px"
:disabled="disableDialog"
class="char-input"
@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").
'
maxHintWidth="242px"
width="93px"
:disabled="disableDialog"
class="char-input"
@input="preview"
/>
</div>
<check-box
v-if="!isJson && !isNdJson"
:init="header"
label="Use first row as column headers"
:disabled="disableDialog"
@click="changeHeaderDisplaying"
/>
<sql-table
v-if="previewData && previewData.rowCount > 0"
:data-set="previewData"
:preview="true"
class="preview-table"
/>
<div v-else class="no-data">No data</div>
<logs class="import-errors" :messages="importMessages" />
</div>
<div class="dialog-buttons-container">
<button
id="import-cancel"
class="secondary"
:disabled="disableDialog"
@click="cancelImport"
>
Cancel
</button>
<button
v-show="!importCompleted"
id="import-start"
class="primary"
:disabled="disableDialog || disableImport"
@click="loadToDb(file)"
>
Import
</button>
<button
v-show="importCompleted"
id="import-finish"
class="primary"
:disabled="disableDialog"
@click="finish"
>
Finish
</button>
</div>
</modal>
</template>
<script>
import csv from '@/lib/csv'
import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField'
import DelimiterSelector from './DelimiterSelector'
import CheckBox from '@/components/CheckBox'
import SqlTable from '@/components/SqlTable'
import Logs from '@/components/Logs'
import time from '@/lib/utils/time'
import fIo from '@/lib/utils/fileIo'
import events from '@/lib/utils/events'
export default {
name: 'CsvJsonImport',
components: {
CloseIcon,
TextField,
DelimiterSelector,
CheckBox,
SqlTable,
Logs
},
props: {
file: File,
db: Object,
dialogName: String
},
emits: ['cancel', 'finish'],
data() {
return {
disableDialog: false,
disableImport: false,
tableName: '',
delimiter: '',
quoteChar: '"',
escapeChar: '"',
header: true,
importCompleted: false,
importMessages: [],
previewData: null,
addedTable: null,
tableNameError: ''
}
},
computed: {
isJson() {
return fIo.isJSON(this.file)
},
isNdJson() {
return fIo.isNDJSON(this.file)
},
typeName() {
return this.isJson || this.isNdJson ? 'JSON' : 'CSV'
}
},
watch: {
isJson() {
if (this.isJson) {
this.delimiter = '\u001E'
this.header = false
}
},
isNdJson() {
if (this.isNdJson) {
this.delimiter = '\u001E'
this.header = false
}
},
tableName: time.debounce(function () {
this.tableNameError = ''
if (!this.tableName) {
return
}
this.db.validateTableName(this.tableName).catch(err => {
this.tableNameError = err.message + '. Try another table name.'
})
}, 400)
},
methods: {
changeHeaderDisplaying(e) {
this.header = e
this.preview()
},
cancelImport() {
if (!this.disableDialog) {
if (this.addedTable) {
this.db.execute(`DROP TABLE "${this.addedTable}"`)
this.db.refreshSchema()
}
this.$modal.hide(this.dialogName)
this.$emit('cancel')
}
},
reset() {
this.header = !this.isJson && !this.isNdJson
this.quoteChar = '"'
this.escapeChar = '"'
this.delimiter = !this.isJson && !this.isNdJson ? '' : '\u001E'
this.tableName = ''
this.disableDialog = false
this.disableImport = false
this.importCompleted = false
this.importMessages = []
this.previewData = null
this.addedTable = null
this.tableNameError = ''
},
open() {
this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file))
this.$modal.show(this.dialogName)
},
async preview() {
this.disableImport = false
if (!this.file) {
return
}
this.importCompleted = false
const config = {
preview: 3,
quoteChar: this.quoteChar || '"',
escapeChar: this.escapeChar,
header: this.header,
delimiter: this.delimiter,
columns: !this.isJson && !this.isNdJson ? null : ['doc']
}
try {
const start = new Date()
const parseResult = this.isJson
? await this.getJsonParseResult(this.file)
: await csv.parse(this.file, config)
const end = new Date()
this.previewData = parseResult.data
this.previewData.rowCount = parseResult.rowCount
this.delimiter = parseResult.delimiter
// In parseResult.messages we can get parse errors
this.importMessages = parseResult.messages || []
if (this.previewData.rowCount === 0) {
this.disableImport = true
this.importMessages.push({
type: 'info',
message: 'No rows to import.'
})
}
if (!parseResult.hasErrors) {
this.importMessages.push({
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
type: 'success'
})
}
} catch (err) {
console.error(err)
this.importMessages = [
{
message: err,
type: 'error'
}
]
}
},
async getJsonParseResult(file) {
const jsonContent = await fIo.getFileContent(file)
const isEmpty = !jsonContent.trim()
return {
data: {
columns: ['doc'],
values: { doc: !isEmpty ? [jsonContent] : [] }
},
hasErrors: false,
messages: [],
rowCount: +!isEmpty
}
},
async loadToDb(file) {
if (!this.tableName) {
this.tableNameError = "Table name can't be empty"
return
}
this.disableDialog = true
const config = {
quoteChar: this.quoteChar || '"',
escapeChar: this.escapeChar,
header: this.header,
delimiter: this.delimiter,
columns: !this.isJson && !this.isNdJson ? null : ['doc']
}
let parsingMsg = {}
this.importMessages.push({
message: `Parsing ${this.typeName}...`,
type: 'info'
})
// Get *reactive* link to parsing message for later updates
parsingMsg = this.importMessages[this.importMessages.length - 1]
const parsingLoadingIndicator = setTimeout(() => {
parsingMsg.type = 'loading'
}, 1000)
let importMsg = {}
let importLoadingIndicator = null
const updateProgress = progress => {
importMsg.progress = progress
}
const progressCounterId = this.db.createProgressCounter(updateProgress)
try {
let start = new Date()
const parseResult = this.isJson
? await this.getJsonParseResult(file)
: await csv.parse(this.file, config)
let end = new Date()
if (!parseResult.hasErrors) {
const rowCount = parseResult.rowCount
let period = time.getPeriod(start, end)
parsingMsg.type = 'success'
if (parseResult.messages.length > 0) {
this.importMessages = this.importMessages.concat(
parseResult.messages
)
parsingMsg.message = `${rowCount} rows are parsed in ${period}.`
} else {
// Inform about parsing success
parsingMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
}
// Loading indicator for parsing is not needed anymore
clearTimeout(parsingLoadingIndicator)
// Add info about import start
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(() => {
importMsg.type = 'loading'
}, 1000)
// Add table
start = new Date()
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.type = 'success'
// Loading indicator for import is not needed anymore
clearTimeout(importLoadingIndicator)
this.importCompleted = true
} else {
parsingMsg.message = 'Parsing ended with errors.'
parsingMsg.type = 'info'
this.importMessages = this.importMessages.concat(parseResult.messages)
}
} catch (err) {
console.error(err)
if (parsingMsg.type === 'loading') {
parsingMsg.type = 'info'
}
if (importMsg.type === 'loading') {
importMsg.type = 'info'
}
this.importMessages.push({
message: err,
type: 'error'
})
}
clearTimeout(parsingLoadingIndicator)
clearTimeout(importLoadingIndicator)
this.db.deleteProgressCounter(progressCounterId)
this.disableDialog = false
},
async finish() {
this.$modal.hide(this.dialogName)
const stmt = this.getQueryExample()
const tabId = await this.$store.dispatch('addTab', { query: stmt })
this.$store.commit('setCurrentTabId', tabId)
this.importCompleted = false
this.$emit('finish')
events.send('inquiry.create', null, { auto: true })
},
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')
},
getNdJsonQueryExample() {
try {
const firstRowJson = JSON.parse(this.previewData.values.doc[0])
const firstKey = Object.keys(firstRowJson)[0]
return [
'/*',
` * 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.',
' */',
`SELECT doc->>'${firstKey}'`,
`FROM "${this.addedTable}"`
].join('\n')
} catch (err) {
console.error(err)
return [
'/*',
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
' */',
'SELECT *',
`FROM "${this.addedTable}"`
].join('\n')
}
},
getJsonQueryExample() {
try {
const firstRowJson = JSON.parse(this.previewData.values.doc[0])
const firstKey = Object.keys(firstRowJson)[0]
return [
'/*',
` * 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.',
' */',
'SELECT *',
`FROM "${this.addedTable}"`,
`JOIN json_each(doc, '$.${firstKey}')`
].join('\n')
} catch (err) {
console.error(err)
return [
'/*',
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
' */',
'SELECT *',
`FROM "${this.addedTable}"`
].join('\n')
}
}
}
}
</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;
}
.chars {
display: flex;
align-items: flex-end;
margin: 0 0 20px;
}
.char-input {
margin-right: 44px;
}
.preview-table {
margin-top: 18px;
}
.import-errors {
height: 136px;
margin-top: 8px;
}
.no-data {
margin-top: 32px;
background-color: white;
border-radius: 5px;
position: relative;
border: 1px solid var(--color-border-light);
box-sizing: border-box;
height: 147px;
font-size: 13px;
color: var(--color-text-base);
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@@ -1,53 +1,54 @@
<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 or CSV 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>
<!--Parse csv dialog -->
<csv-import
ref="addCsv"
<!--Parse csv or json dialog -->
<csv-json-import
ref="addCsvJson"
:file="file"
:db="newDb"
dialog-name="importFromCsv"
@cancel="cancelCsvImport"
dialogName="importFromCsvJson"
@cancel="cancelImport"
@finish="finish"
/>
</div>
@@ -57,17 +58,21 @@
import fIo from '@/lib/utils/fileIo'
import ChangeDbIcon from '@/components/svg/changeDb'
import database from '@/lib/database'
import CsvImport from '@/components/CsvImport'
import { send } from '@/lib/utils/events'
import CsvJsonImport from '@/components/CsvJsonImport'
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)
}
},
@@ -77,11 +82,8 @@ export default {
default: 'unset'
}
},
components: {
ChangeDbIcon,
CsvImport
},
data () {
emits: [],
data() {
return {
state: '',
animationPromise: Promise.resolve(),
@@ -89,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'
@@ -102,51 +104,56 @@ export default {
}
},
methods: {
cancelCsvImport () {
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()
if (fIo.isDatabase(file)) {
this.loadDb(file)
} else {
send('database.import', file.size, {
from: 'csv',
const isJson = fIo.isJSON(file) || fIo.isNDJSON(file)
events.send('database.import', file.size, {
from: isJson ? 'json' : 'csv',
new_db: true
})
this.file = file
await this.$nextTick()
const csvImport = this.$refs.addCsv
csvImport.reset()
return Promise.all([csvImport.previewCsv(), this.animationPromise])
.then(csvImport.open)
const csvJsonImportModal = this.$refs.addCsvJson
csvJsonImportModal.reset()
return Promise.all([
csvJsonImportModal.preview(),
this.animationPromise
]).then(csvJsonImportModal.open)
}
},
browse () {
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
browse() {
fIo
.getFileFromUser('.db,.sqlite,.sqlite3,.csv,.json,.ndjson')
.then(this.checkFile)
},
drop (event) {
drop(event) {
this.checkFile(event.dataTransfer.files[0])
}
}
@@ -240,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,117 @@
<template>
<Field label="Adjust sizes">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.adjustSizes"
@option-change="update('adjustSizes', $event)"
/>
</Field>
<Field label="Barnes-Hut optimize">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.barnesHutOptimize"
@option-change="update('barnesHutOptimize', $event)"
/>
</Field>
<Field v-show="modelValue.barnesHutOptimize" label="Barnes-Hut Theta">
<NumericInput
:value="modelValue.barnesHutTheta"
@update="update('barnesHutTheta', $event)"
/>
</Field>
<Field label="Gravity">
<NumericInput
:value="modelValue.gravity"
@update="update('gravity', $event)"
/>
</Field>
<Field label="Strong gravity mode">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.strongGravityMode"
@option-change="update('strongGravityMode', $event)"
/>
</Field>
<Field label="Noack's LinLog model">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.linLogMode"
@option-change="update('linLogMode', $event)"
/>
</Field>
<Field label="Out bound attraction distribution">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.outboundAttractionDistribution"
@option-change="update('outboundAttractionDistribution', $event)"
/>
</Field>
<Field label="Slow down">
<NumericInput
:value="modelValue.slowDown"
:min="0"
@update="update('slowDown', $event)"
/>
</Field>
<Field label="Edge weight">
<Dropdown
:options="keyOptions"
:value="modelValue.weightSource"
@change="update('weightSource', $event)"
/>
</Field>
<Field v-show="modelValue.weightSource" label="Edge 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,77 @@
<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 Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import Multiselect from 'vue-multiselect'
import 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
Field: applyPureReactInVue(Field),
NumericInput: applyPureReactInVue(NumericInput),
Dropdown: applyPureReactInVue(Dropdown),
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,137 @@
<template>
<Field label="Color">
<RadioBlocks
:options="edgeColorTypeOptions"
:activeOption="modelValue.type"
@option-change="updateColorType"
/>
<Field v-if="modelValue.type === 'constant'">
<ColorPicker
:selectedColor="modelValue.value"
@color-change="updateSettings('value', $event)"
/>
</Field>
<template v-else>
<Field>
<Dropdown
v-if="modelValue.type === 'variable'"
:options="keyOptions"
:value="modelValue.source"
@change="updateSettings('source', $event)"
/>
</Field>
<Field>
<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'" label="Color as">
<RadioBlocks
:options="сolorAsOptions"
:activeOption="modelValue.mode"
@option-change="updateSettings('mode', $event)"
/>
</Field>
<Field v-if="modelValue.type !== 'constant'" label="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">
<RadioBlocks
:options="edgeSizeTypeOptions"
:activeOption="modelValue.type"
@option-change="updateSizeType"
/>
<Field>
<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">
<NumericInput
:value="modelValue.scale"
@update="updateSettings('scale', $event)"
/>
</Field>
<Field label="Minimum size">
<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,43 @@
<template>
<Field label="Initial iterations">
<NumericInput
:value="modelValue.initialIterationsAmount"
:min="1"
@update="update('initialIterationsAmount', $event)"
/>
</Field>
<Field label="Scaling ratio">
<NumericInput
:value="modelValue.scalingRatio"
@update="update('scalingRatio', $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,155 @@
<template>
<Field label="Color">
<RadioBlocks
:options="nodeColorTypeOptions"
:activeOption="modelValue.type"
@option-change="updateColorType"
/>
<Field v-if="modelValue.type === 'constant'">
<ColorPicker
:selectedColor="modelValue.value"
@color-change="updateSettings('value', $event)"
/>
</Field>
<template v-else>
<Field>
<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"
@change="updateSettings('method', $event)"
/>
</Field>
<Field>
<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'" label="Color as">
<RadioBlocks
:options="сolorAsOptions"
:activeOption="modelValue.mode"
@option-change="updateSettings('mode', $event)"
/>
</Field>
<Field v-if="modelValue.type !== 'constant'" label="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',
sourceUsage: 'map_to',
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,118 @@
<template>
<Field label="Size">
<RadioBlocks
:options="nodeSizeTypeOptions"
:activeOption="modelValue.type"
@option-change="updateSizeType"
/>
<Field>
<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)"
/>
<Dropdown
v-if="modelValue.type === 'calculated'"
:options="nodeCalculatedSizeMethodOptions"
:value="modelValue.method"
@change="updateSettings('method', $event)"
/>
</Field>
</Field>
<template v-if="modelValue.type !== 'constant'">
<Field label="Size scale">
<NumericInput
:value="modelValue.scale"
@update="updateSettings('scale', $event)"
/>
</Field>
<Field label="Size mode">
<RadioBlocks
:options="nodeSizeModeOptions"
:activeOption="modelValue.mode"
@option-change="updateSettings('mode', $event)"
/>
</Field>
<Field label="Minimum size">
<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

@@ -1,6 +1,7 @@
<template>
<div
:class="['icon-btn', { active }, { disabled }]"
<button
:class="['icon-btn', { active }]"
:disabled="disabled"
@click="onClick"
@mouseenter="showTooltip($event, tooltipPosition)"
@mouseleave="hideTooltip"
@@ -9,10 +10,15 @@
<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>
</div>
</button>
</template>
<script>
@@ -21,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')
}
@@ -38,35 +51,36 @@ export default {
box-sizing: border-box;
width: 26px;
height: 26px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
position: relative;
background-color: transparent;
border: none;
}
.icon-btn:hover {
border: 1px solid var(--color-border);
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);
}
.disabled.icon-btn .icon >>> path,
.disabled.icon-btn .icon >>> circle {
.icon-btn:disabled .icon :deep(path),
.icon-btn:disabled .icon :deep(circle) {
fill: var(--color-border);
}
.disabled.icon-btn {
.icon-btn:disabled {
cursor: default;
pointer-events: none;
}
.disabled.icon-btn:hover .icon >>> path {
.disabled.icon-btn:hover .icon :deep(path) {
fill: var(--color-border);
}

View File

@@ -1,27 +1,32 @@
<template>
<modal
:name="name"
classes="dialog"
height="auto"
:modalId="name"
class="dialog"
:clickToClose="false"
:contentTransition="{ name: 'loading-dialog' }"
:overlayTransition="{ name: 'loading-dialog' }"
>
<div class="dialog-header">
{{ title }}
<close-icon @click="$emit('cancel')" :disabled="loading"/>
<close-icon :disabled="loading" @click="$emit('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')"
>
@@ -29,6 +34,7 @@
</button>
<button
class="primary"
type="button"
:disabled="loading"
@click="$emit('action')"
>
@@ -43,7 +49,8 @@ import LoadingIndicator from '@/components/LoadingIndicator'
import CloseIcon from '@/components/svg/close'
export default {
name: 'loadingDialog',
name: 'LoadingDialog',
components: { LoadingIndicator, CloseIcon },
props: {
loadingMsg: String,
successMsg: String,
@@ -52,22 +59,47 @@ export default {
title: String,
loading: Boolean
},
emits: ['cancel', 'action'],
watch: {
loading () {
loading() {
if (this.loading) {
this.$modal.show(this.name)
}
}
},
components: { LoadingIndicator, CloseIcon },
methods: {
cancel () {
cancel() {
this.$emit('cancel')
}
}
}
</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>
@@ -75,17 +78,30 @@ export default {
props: {
horizontal: { type: Boolean, default: false },
before: { type: Object },
after: { type: Object }
after: { type: Object },
default: {
type: Object,
default: () => {
return {
before: 50,
after: 50
}
}
}
},
data () {
emits: [],
data() {
return {
container: null,
paneBefore: this.before,
paneAfter: this.after,
beforeMinimising: {
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,
@@ -95,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 = ''
@@ -122,7 +142,7 @@ export default {
transform: translation + rotation
}
},
directionAfterIconStyle () {
directionAfterIconStyle() {
const expanded = this.paneAfter.size !== 0
const translation = 'translate(-50%, -50%)'
let rotation = ''
@@ -138,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
@@ -189,7 +220,7 @@ export default {
this.unbindEvents()
},
moveSplitter (event) {
moveSplitter(event) {
const splitterInfo = {
container: this.container,
paneBeforeMax: this.paneBefore.max,
@@ -201,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>
@@ -227,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%;
@@ -269,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,
@@ -327,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,92 +1,118 @@
<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">
<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">
<div class="cell-data" :style="cellStyle">
{{ dataSet.values[col][rowIndex - 1 + currentPageData.start] }}
</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" />
<pager
v-show="pageCount > 1"
v-model="currentPage"
:pageCount="pageCount"
/>
</div>
</div>
</template>
<script>
import Pager from './Pager'
import Pager from './Pager.vue'
export default {
name: 'SqlTable',
components: { Pager },
props: {
dataSet: Object,
time: String,
time: [String, Number],
pageSize: {
type: Number,
default: 20
},
preview: Boolean
page: {
type: Number,
default: 1
},
preview: Boolean,
selectedCellCoordinates: Object
},
data () {
emits: ['updateSelectedCell'],
data() {
return {
header: null,
tableWidth: null,
currentPage: 1,
resizeObserver: null
currentPage: this.page,
resizeObserver: null,
selectedCellElement: null
}
},
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) {
@@ -99,8 +125,54 @@ export default {
}
}
},
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}"]`
)
if (cell) {
this.selectCell(cell)
}
}
},
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.table)
},
methods: {
calculateHeadersWidth () {
isBlob(value) {
return value && ArrayBuffer.isView(value)
},
isNull(value) {
return value === null
},
getCellValue(col, rowIndex) {
return this.dataSet.values[col][rowIndex - 1 + this.currentPageData.start]
},
getCellText(col, rowIndex) {
const value = this.getCellValue(col, rowIndex)
if (this.isNull(value)) {
return 'NULL'
}
if (this.isBlob(value)) {
return 'BLOB'
}
return value
},
calculateHeadersWidth() {
this.tableWidth = this.$refs['table-container'].offsetWidth
this.$nextTick(() => {
this.header = this.$refs.th.map(th => {
@@ -108,26 +180,102 @@ export default {
})
})
},
onScrollTable () {
this.$refs['header-container'].scrollLeft = this.$refs['table-container'].scrollLeft
}
},
mounted () {
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
this.resizeObserver.observe(this.$refs.table)
this.calculateHeadersWidth()
},
beforeDestroy () {
this.resizeObserver.unobserve(this.$refs.table)
},
watch: {
currentPageData: 'calculateHeadersWidth',
dataSet () {
this.currentPage = 1
onScrollTable() {
this.$refs['header-container'].scrollLeft =
this.$refs['table-container'].scrollLeft
},
onTableKeydown(e) {
const keyCodeMap = {
37: 'left',
39: 'right',
38: 'up',
40: 'down'
}
if (
!this.selectedCellElement ||
!Object.keys(keyCodeMap).includes(e.keyCode.toString())
) {
return
}
e.preventDefault()
this.moveFocusInTable(this.selectedCellElement, keyCodeMap[e.keyCode])
},
onCellClick(e) {
this.selectCell(e.target.closest('td'), false)
},
selectCell(cell, scrollTo = true) {
if (!cell) {
if (this.selectedCellElement) {
this.selectedCellElement.ariaSelected = 'false'
}
this.selectedCellElement = cell
} else if (!cell.ariaSelected || cell.ariaSelected === 'false') {
if (this.selectedCellElement) {
this.selectedCellElement.ariaSelected = 'false'
}
cell.ariaSelected = 'true'
this.selectedCellElement = cell
} else {
cell.ariaSelected = 'false'
this.selectedCellElement = null
}
if (this.selectedCellElement && scrollTo) {
this.selectedCellElement.scrollIntoView()
}
this.$emit('updateSelectedCell', this.selectedCellElement)
},
moveFocusInTable(initialCell, direction) {
const currentRowIndex = +initialCell.dataset.row
const currentColIndex = +initialCell.dataset.col
let newRowIndex, newColIndex
if (direction === 'right') {
if (currentColIndex === this.columns.length - 1) {
newRowIndex = currentRowIndex + 1
newColIndex = 0
} else {
newRowIndex = currentRowIndex
newColIndex = currentColIndex + 1
}
} else if (direction === 'left') {
if (currentColIndex === 0) {
newRowIndex = currentRowIndex - 1
newColIndex = this.columns.length - 1
} else {
newRowIndex = currentRowIndex
newColIndex = currentColIndex - 1
}
} else if (direction === 'up') {
newRowIndex = currentRowIndex - 1
newColIndex = currentColIndex
} else if (direction === 'down') {
newRowIndex = currentRowIndex + 1
newColIndex = currentColIndex
}
const newCell = this.$refs.table.querySelector(
`td[data-col="${newColIndex}"][data-row="${newRowIndex}"]`
)
if (newCell) {
this.selectCell(newCell)
}
}
}
}
</script>
<style scoped>
table.sqliteviz-table:focus {
outline: none;
}
.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);
}
</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,12 +29,12 @@
</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">
Add new table from CSV
<span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
Add new table from CSV, JSON or NDJSON
</span>
</span>
</template>
@@ -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

@@ -0,0 +1,19 @@
<template>
<svg
width="28"
height="27"
viewBox="0 0 28 27"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17.9475 8.33625L12.7838 13.5L17.9475 18.6638L16.35 20.25L9.60001
13.5L16.35 6.75L17.9475 8.33625Z"
fill="#506784"
/>
</svg>
</template>
<script>
export default {}
</script>

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 or CSV
</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

@@ -0,0 +1,25 @@
<template>
<svg
width="27"
height="27"
viewBox="0 0 27 27"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17.3474 8.33625L12.1837 13.5L17.3474 18.6638L15.7499 20.25L8.99991
13.5L15.7499 6.75L17.3474 8.33625Z"
fill="#506784"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.19995 19.8L7.19995 7.20001H9.19995V19.8H7.19995Z"
fill="#506784"
/>
</svg>
</template>
<script>
export default {}
</script>

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

@@ -0,0 +1,47 @@
<template>
<svg width="19" height="19" viewBox="0 0 19 19" fill="none">
<g clip-path="url(#clip0_2130_5292)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M1.85303 11.3794L1.85303 7.80371L5.86304 7.80371L5.86304
11.3794L1.85303 11.3794ZM7.36304 11.3794L7.36304 7.80371L11.3428
7.80371L11.3428 11.3794L7.36304 11.3794ZM12.8428 11.3794L16.853
11.3794L16.853 7.80371L12.8428 7.80371L12.8428 11.3794ZM15.353
6.30371L16.853 6.30371C17.6815 6.30371 18.353 6.97528 18.353
7.80371L18.353 11.3794C18.353 12.2078 17.6815 12.8794 16.853
12.8794L15.353 12.8794L15.353 14.3111C15.353 15.0153 14.7603 15.5916
14.0358 15.5916L4.67027 15.5916C3.94579 15.5916 3.35303 15.0153 3.35303
14.3111L3.35303 12.8794L1.85303 12.8794C1.0246 12.8794 0.353027 12.2078
0.353027 11.3794L0.353027 7.80371C0.353027 6.97528 1.0246 6.30371
1.85303 6.30371L3.35303 6.30371L3.35303 4.87201C3.35303 4.16349 3.94139
3.59155 4.67027 3.59155L14.0358 3.59155C14.7604 3.59155 15.353 4.16117
15.353 4.87201L15.353 6.30371ZM14.0315 6.30371L14.0315 4.87086L11.887
4.87086L11.887 6.30371L12.8428 6.30371L14.0315 6.30371ZM10.387
6.30371L10.387 4.87086L8.26685 4.87086L8.26685 6.30371L10.387
6.30371ZM6.76685 6.30371L6.76685 4.87086L4.67027 4.87086L4.67027
6.30371L6.76685 6.30371ZM4.67027 12.8794L4.67027 14.3121L6.76685
14.3121L6.76685 12.8794L4.67027 12.8794ZM8.26685 12.8794L8.26685
14.3121L10.387 14.3121L10.387 12.8794L8.26685 12.8794ZM11.887
12.8794L11.887 14.3121L14.0315 14.3121L14.0315 12.8794L11.887 12.8794Z"
fill="#A2B1C6"
/>
</g>
<defs>
<clipPath id="clip0_2130_5292">
<rect
width="18"
height="18"
fill="white"
transform="translate(0.353027 18.5916) rotate(-90)"
/>
</clipPath>
</defs>
</svg>
</template>
<script>
export default {
name: 'RowIcon'
}
</script>

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

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

@@ -0,0 +1,44 @@
<template>
<svg width="19" height="19" viewBox="0 0 19 19" fill="none">
<g clip-path="url(#clip0_2131_6054)">
<path
d="M3.53784 11.5846L3.53784 3.14734L11.9751 3.14734V7.676C12.4655 7.51991
12.9771 7.47439 13.4751 7.53264V3.14734C13.4751 2.31891 12.8035 1.64734
11.9751 1.64734L3.53784 1.64734C2.70941 1.64734 2.03784 2.31891 2.03784
3.14734L2.03784 11.5846C2.03784 12.413 2.70942 13.0846 3.53784
13.0846H10.0831C9.771 12.6184 9.58279 12.1055 9.51083
11.5846H3.53784Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14.7887 9.9291C15.4307 10.8837 15.1773 12.1779 14.2228
12.8199C13.2682 13.4618 11.974 13.2084 11.332 12.2539C10.69 11.2993
10.9434 10.0051 11.898 9.3631C12.8525 8.72113 14.1468 8.97454 14.7887
9.9291ZM14.4606 14.3901L16.6181 17.5982C16.8492 17.9419 17.3153 18.0331
17.659 17.802C18.0027 17.5708 18.0939 17.1048 17.8628 16.7611L15.6884
13.5279C16.7949 12.3365 16.9801 10.4996 16.0334 9.092C14.9292 7.45002
12.7029 7.01412 11.0609 8.1184C9.41891 9.22268 8.98302 11.449 10.0873
13.0909C11.062 14.5403 12.9109 15.05 14.4606 14.3901Z"
fill="#A2B1C6"
/>
</g>
<defs>
<clipPath id="clip0_2131_6054">
<rect
width="18"
height="18"
fill="white"
transform="translate(0.5 18.5916) rotate(-90)"
/>
</clipPath>
</defs>
</svg>
</template>
<script>
export default {
name: 'ViewCellValueIcon'
}
</script>

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,9 +7,9 @@ const hintsByCode = {
}
export default {
getResult (source) {
getResult(source, columns) {
const result = {
columns: []
columns: columns || []
}
const values = {}
if (source.meta.fields) {
@@ -24,8 +24,18 @@ export default {
return value
})
})
} else if (columns) {
columns.forEach((col, i) => {
values[col] = source.data.map(row => {
let value = row[i]
if (value instanceof Date) {
value = value.toISOString()
}
return value
})
})
} else {
for (let i = 0; i <= source.data[0].length - 1; i++) {
for (let i = 0; source.data[0] && i <= source.data[0].length - 1; i++) {
const colName = `col${i + 1}`
result.columns.push(colName)
values[colName] = source.data.map(row => {
@@ -42,7 +52,7 @@ export default {
return result
},
prepareForExport (resultSet) {
prepareForExport(resultSet) {
const columns = resultSet.columns
const rowCount = resultSet.values[columns[0]].length
const result = {
@@ -51,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
@@ -73,21 +85,26 @@ export default {
comments: false,
step: undefined,
complete: results => {
const res = {
data: this.getResult(results),
delimiter: results.meta.delimiter,
hasErrors: false,
rowCount: results.data.length
let res
try {
res = {
data: this.getResult(results, config.columns),
delimiter: results.meta.delimiter,
hasErrors: false,
rowCount: results.data.length
}
res.messages = results.errors.map(msg => {
msg.type = msg.code === 'UndetectableDelimiter' ? 'info' : 'error'
if (msg.type === 'error') res.hasErrors = true
msg.hint = hintsByCode[msg.code]
return msg
})
} catch (error) {
reject(error)
}
res.messages = results.errors.map(msg => {
msg.type = msg.code === 'UndetectableDelimiter' ? 'info' : 'error'
if (msg.type === 'error') res.hasErrors = true
msg.hint = hintsByCode[msg.code]
return msg
})
resolve(res)
},
error: (error, file) => {
error: error => {
reject(error)
},
download: false,
@@ -107,7 +124,7 @@ export default {
})
},
serialize (resultSet) {
serialize(resultSet) {
return Papa.unparse(this.prepareForExport(resultSet), { delimiter: '\t' })
}
}

View File

@@ -1,46 +1,50 @@
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 {}
}
const dataSorces = {}
const dataSources = {}
sqlResult.columns.forEach((column, index) => {
dataSorces[column] = sqlResult.values.map(row => row[index])
dataSources[column] = sqlResult.values.map(row => row[index])
})
return dataSorces
return dataSources
}
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)
})

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