mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-06 18:18:53 +08:00
Compare commits
115 Commits
0.17.0
...
1601514cca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1601514cca | ||
|
|
3ee825defe | ||
|
|
77df3a8446 | ||
|
|
559e04200c | ||
|
|
4568780526 | ||
|
|
fa9108bc08 | ||
|
|
df16383d49 | ||
|
|
6f7961e1b4 | ||
|
|
2741aa6f33 | ||
|
|
6ceac83db9 | ||
|
|
a46625ebe7 | ||
|
|
5ef0b32549 | ||
|
|
f49fa0ea96 | ||
|
|
108ae454c1 | ||
|
|
43b6110c28 | ||
|
|
5a805fba80 | ||
|
|
58cdab94c1 | ||
|
|
b3d81666be | ||
|
|
fdf180d340 | ||
|
|
f2ff5aa2af | ||
|
|
0c1b91ab2f | ||
|
|
5e2b34a856 | ||
|
|
24786c9069 | ||
|
|
c28d31b019 | ||
|
|
6009ebb447 | ||
|
|
b5504b91ce | ||
|
|
828cad6439 | ||
|
|
8fa3c2ae58 | ||
|
|
aa5c907095 | ||
|
|
3a05b27400 | ||
|
|
108d96a753 | ||
|
|
f55a8caa92 | ||
|
|
87f9f9eb01 | ||
|
|
d6408bdd85 | ||
|
|
e14696b59e | ||
|
|
eee67763b5 | ||
|
|
637d8d26dd | ||
|
|
b30b2181e4 | ||
|
|
378b9fb580 | ||
|
|
244ba9eb08 | ||
|
|
53e5194295 | ||
|
|
04274ef19a | ||
|
|
3893a66f4e | ||
|
|
1b6b7c71e9 | ||
|
|
3f6427ff0e | ||
|
|
a2464d839f | ||
|
|
316e603c3c | ||
|
|
88466eca5e | ||
|
|
5123e39a60 | ||
|
|
4c8401f32f | ||
|
|
d949629ee4 | ||
|
|
7a18e415c8 | ||
|
|
878689b3f7 | ||
|
|
42f040975d | ||
|
|
78e9ca2120 | ||
|
|
96af391f20 | ||
|
|
f58b62eb0c | ||
|
|
b17040d3ef | ||
|
|
bc6154b9ad | ||
|
|
3aea8c951b | ||
|
|
1e982a1196 | ||
|
|
6ecbde7fd3 | ||
|
|
5ee881432a | ||
|
|
735e4ec7f6 | ||
|
|
07d31dbfe9 | ||
|
|
ac1f7de62c | ||
|
|
96877de532 | ||
|
|
b60fc28e47 | ||
|
|
bec3d9c737 | ||
|
|
8aac7af481 | ||
|
|
6982204e68 | ||
|
|
41e0ae7332 | ||
|
|
ebb5af4f10 | ||
|
|
ae26358b25 | ||
|
|
d9ee702b8e | ||
|
|
446045fa55 | ||
|
|
1a9d1b308b | ||
|
|
014ecf145e | ||
|
|
0044d82b6f | ||
|
|
998e8d66f7 | ||
|
|
db3dbdf993 | ||
|
|
4e13a16e33 | ||
|
|
9c0103fd05 | ||
|
|
e4b117ffb9 | ||
|
|
6320f818cb | ||
|
|
3c456ef135 | ||
|
|
c713c713b7 | ||
|
|
babf0074c0 | ||
|
|
e71e6700c1 | ||
|
|
84e66b8167 | ||
|
|
9e84cf269e | ||
|
|
e897b4913b | ||
|
|
0646f58ca0 | ||
|
|
c674bf11e3 | ||
|
|
2d8a91675e | ||
|
|
45b1021559 | ||
|
|
7216e996d1 | ||
|
|
6eae9a0f2d | ||
|
|
7486b32bd1 | ||
|
|
2c564767f8 | ||
|
|
289a727cbe | ||
|
|
5f2b8ba5a9 | ||
|
|
f0a4212e2b | ||
|
|
c8deff32c1 | ||
|
|
d56604a7d6 | ||
|
|
48e311bff8 | ||
|
|
518b22b489 | ||
|
|
a20dd7f849 | ||
|
|
310a939109 | ||
|
|
bb9ba08902 | ||
|
|
c7c727ff78 | ||
|
|
8669a6a9e5 | ||
|
|
c1cc5bb95e | ||
|
|
9c55e76a41 | ||
|
|
70a9edf57e |
29
.eslintrc.cjs
Normal file
29
.eslintrc.cjs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
30
.eslintrc.js
30
.eslintrc.js
@@ -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
14
.github/workflows/config.grenrc.cjs
vendored
Normal 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']
|
||||||
|
}
|
||||||
|
}
|
||||||
17
.github/workflows/config.grenrc.js
vendored
17
.github/workflows/config.grenrc.js
vendored
@@ -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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
29
.github/workflows/main.yml
vendored
29
.github/workflows/main.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Deploy to GitHub Pages and create release
|
name: Create release
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
@@ -7,48 +7,39 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
name: Deploy to GitHub Pages and create release
|
name: Create release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 10.x
|
node-version: 18.x
|
||||||
|
|
||||||
- name: Update npm
|
- name: Update npm
|
||||||
run: npm install -g npm@7
|
run: npm install -g npm@10
|
||||||
|
|
||||||
- name: npm install and build
|
- name: npm install and build
|
||||||
run: |
|
run: |
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
- name: Create archive
|
- name: Create archives
|
||||||
run: |
|
run: |
|
||||||
cd dist
|
cd dist
|
||||||
zip -9 -r dist.zip . -x "js/*.map" -x "/*.map"
|
zip -9 -r ../dist.zip . -x "*.map"
|
||||||
|
zip -9 -r ../dist_map.zip .
|
||||||
|
|
||||||
- name: Create Release Notes
|
- name: Create Release Notes
|
||||||
run: |
|
run: |
|
||||||
npm install github-release-notes@0.16.0 -g
|
npm install github-release-notes@0.16.0 -g
|
||||||
gren changelog --generate --config="/.github/workflows/config.grenrc.js"
|
gren changelog --generate --config="/.github/workflows/config.grenrc.cjs"
|
||||||
env:
|
env:
|
||||||
GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: ncipollo/release-action@v1
|
uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
artifacts: "dist/dist.zip"
|
artifacts: 'dist.zip,dist_map.zip'
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
bodyFile: "CHANGELOG.md"
|
bodyFile: 'CHANGELOG.md'
|
||||||
|
|
||||||
- name: Deploy 🚀
|
|
||||||
uses: JamesIves/github-pages-deploy-action@4.1.1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
branch: build # The branch the action should deploy to.
|
|
||||||
folder: dist/ # The folder the action should deploy.
|
|
||||||
clean: true # Automatically remove deleted files from the deploy branch
|
|
||||||
clean-exclude: .nojekyll
|
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -11,13 +11,13 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: Run tests
|
name: Run tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 10.x
|
node-version: 18.x
|
||||||
- name: Install browsers
|
- name: Install browsers
|
||||||
run: |
|
run: |
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
sudo apt-get install -y chromium-browser firefox
|
sudo apt-get install -y chromium-browser firefox
|
||||||
|
|
||||||
- name: Update npm
|
- name: Update npm
|
||||||
run: npm install -g npm@7
|
run: npm install -g npm@10
|
||||||
|
|
||||||
- name: Install the project
|
- name: Install the project
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|||||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "none",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
||||||
24
Dockerfile.test
Normal file
24
Dockerfile.test
Normal 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
|
||||||
18
README.md
18
README.md
@@ -4,11 +4,13 @@
|
|||||||
|
|
||||||
# sqliteviz
|
# 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:
|
With sqliteviz you can:
|
||||||
|
|
||||||
- run SQL queries against a SQLite database and create [Plotly][11] charts and pivot tables based on the result sets
|
- run SQL queries against a SQLite database and create [Plotly][11] charts 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
|
- export result set to CSV file
|
||||||
- manage inquiries and run them against different databases
|
- manage inquiries and run them against different databases
|
||||||
- import/export inquiries from/to a JSON file
|
- 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
|
https://user-images.githubusercontent.com/24638357/128249848-f8fab0f5-9add-46e0-a9c1-dd5085a8623e.mp4
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
The latest release of sqliteviz is deployed on GitHub Pages at [lana-k.github.io/sqliteviz][6].
|
|
||||||
|
The latest release of sqliteviz is deployed on [sqliteviz.com/app][6].
|
||||||
|
|
||||||
## Wiki
|
## Wiki
|
||||||
For user documentation, check out sqliteviz [Wiki][7].
|
|
||||||
|
For user documentation, check out sqliteviz [documentation][7].
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
It's a kind of middleground between [Plotly Falcon][1] and [Redash][2].
|
It's a kind of middleground between [Plotly Falcon][1] and [Redash][2].
|
||||||
|
|
||||||
## Components
|
## Components
|
||||||
|
|
||||||
It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4] and [Vue-Codemirror][8] in [Vue.js][5]. CSV parsing is performed with [Papa Parse][9].
|
It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4] and [Vue-Codemirror][8] in [Vue.js][5]. CSV parsing is performed with [Papa Parse][9].
|
||||||
|
|
||||||
[1]: https://github.com/plotly/falcon
|
[1]: https://github.com/plotly/falcon
|
||||||
@@ -34,8 +40,8 @@ It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4]
|
|||||||
[3]: https://github.com/plotly/react-chart-editor
|
[3]: https://github.com/plotly/react-chart-editor
|
||||||
[4]: https://github.com/sql-js/sql.js
|
[4]: https://github.com/sql-js/sql.js
|
||||||
[5]: https://github.com/vuejs/vue
|
[5]: https://github.com/vuejs/vue
|
||||||
[6]: https://lana-k.github.io/sqliteviz/
|
[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
|
[8]: https://github.com/surmon-china/vue-codemirror#readme
|
||||||
[9]: https://www.papaparse.com/
|
[9]: https://www.papaparse.com/
|
||||||
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
|
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: ['@vue/cli-plugin-babel/preset']
|
||||||
'@vue/cli-plugin-babel/preset'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.png">
|
<link rel="icon" href="favicon.png" />
|
||||||
<link rel="manifest" href="<%= BASE_URL %>manifest.webmanifest">
|
<link rel="manifest" href="manifest.webmanifest" />
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<title>sqliteviz</title>
|
||||||
<style>
|
<style>
|
||||||
#sqliteviz-loading-wrapper {
|
#sqliteviz-loading-wrapper {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -38,15 +38,18 @@
|
|||||||
|
|
||||||
#sqliteviz-loading-wrapper circle {
|
#sqliteviz-loading-wrapper circle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0; right: 0; top: 0; bottom: 0;
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke-width: 5px;
|
stroke-width: 5px;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
stroke: #119DFF;
|
stroke: #119dff;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sqliteviz-loading-wrapper circle.bg {
|
#sqliteviz-loading-wrapper circle.bg {
|
||||||
stroke: #C8D4E3;
|
stroke: #c8d4e3;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sqliteviz-loading-wrapper circle.front {
|
#sqliteviz-loading-wrapper circle.front {
|
||||||
@@ -69,30 +72,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<!-- head extention slot start -->
|
||||||
|
<!-- head extention slot end -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<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>
|
</noscript>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div id="sqliteviz-loading-wrapper">
|
<div id="sqliteviz-loading-wrapper">
|
||||||
<div id="sqliteviz-loading-text">LOADING</div>
|
<div id="sqliteviz-loading-text">LOADING</div>
|
||||||
<svg height="170" width="170" viewBox="0 0 170 170">
|
<svg height="170" width="170" viewBox="0 0 170 170">
|
||||||
<circle
|
<circle class="bg" cx="85" cy="85" r="80" />
|
||||||
class="bg"
|
<circle class="front" cx="85" cy="85" r="80" />
|
||||||
cx="85"
|
|
||||||
cy="85"
|
|
||||||
r="80"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
class="front"
|
|
||||||
cx="85"
|
|
||||||
cy="85"
|
|
||||||
r="80"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- extention slot start -->
|
||||||
|
<!-- extention slot end -->
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
10
jsconfig.json
Normal file
10
jsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"include": ["src/**/*", "tests/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
122
karma.conf.cjs
Normal file
122
karma.conf.cjs
Normal 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'
|
||||||
|
}
|
||||||
203
karma.conf.js
203
karma.conf.js
@@ -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'
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM emscripten/emsdk:2.0.24
|
FROM emscripten/emsdk:3.0.1
|
||||||
|
|
||||||
WORKDIR /tmp/build
|
WORKDIR /tmp/build
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ SQLite [miscellaneous extensions][3] included:
|
|||||||
SQLite 3rd party extensions included:
|
SQLite 3rd party extensions included:
|
||||||
|
|
||||||
1. [pivot_vtab][5] -- a pivot virtual table
|
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
|
To ease the step to have working clone locally, the build is committed into
|
||||||
the repository.
|
the repository.
|
||||||
@@ -99,3 +103,6 @@ described in [this message from SQLite Forum][12]:
|
|||||||
[17]: https://sqlite.org/contrib/
|
[17]: https://sqlite.org/contrib/
|
||||||
[18]: https://sqlite.org/contrib//download/extension-functions.c?get=25
|
[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
|
[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
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
FROM node:12-buster
|
FROM node:20.14-bookworm
|
||||||
|
|
||||||
RUN set -ex; \
|
RUN set -ex; \
|
||||||
echo 'deb http://deb.debian.org/debian unstable main' \
|
|
||||||
> /etc/apt/sources.list.d/unstable.list; \
|
|
||||||
apt-get update; \
|
apt-get update; \
|
||||||
apt-get install -y -t unstable firefox; \
|
apt-get install -y firefox-esr; \
|
||||||
apt-get install -y chromium
|
apt-get install -y chromium
|
||||||
|
|
||||||
WORKDIR /tmp/build
|
WORKDIR /tmp/build
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
# SQLite WebAssembly build micro-benchmark
|
# SQLite WebAssembly build micro-benchmark
|
||||||
|
|
||||||
This directory contains a micro-benchmark for evaluating SQLite
|
This directory contains a micro-benchmark for evaluating SQLite WebAssembly
|
||||||
WebAssembly builds performance on typical SQL queries, run from
|
builds performance on read and write SQL queries, run from `make.sh` script. If
|
||||||
`make.sh` script. It can also serve as a smoke test.
|
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
|
The benchmark operates on a set of SQLite WebAssembly builds expected in
|
||||||
in `lib/build-$NAME` directories each containing `sql-wasm.js` and
|
`lib/build-$NAME` directories each containing `sql-wasm.js` and
|
||||||
`sql-wasm.wasm`. Then it creates a Docker image for each, and runs
|
`sql-wasm.wasm`. Then it creates a Docker image for each, and runs the
|
||||||
the benchmark in Firefox and Chromium using Karma in the container.
|
benchmark in Firefox and Chromium using Karma in the container.
|
||||||
|
|
||||||
After successful run, the benchmark result of each build is contained
|
After successful run, the benchmark produces the following per each build:
|
||||||
in `build-$NAME-result.json`. The JSON result files can be analysed
|
|
||||||
using `result-analysis.ipynb` Jupyter notebook.
|
- `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/
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
module.exports = function (config) {
|
module.exports = function (config) {
|
||||||
const timeout = 15 * 60 * 1000
|
const timeout = 15 * 60 * 1000
|
||||||
config.set({
|
config.set({
|
||||||
|
|
||||||
frameworks: ['mocha'],
|
frameworks: ['mocha'],
|
||||||
|
|
||||||
files: [
|
files: [
|
||||||
'suite.js',
|
'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 }
|
{ pattern: 'sample.csv', served: true, included: false }
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -15,7 +18,10 @@ module.exports = function (config) {
|
|||||||
singleRun: true,
|
singleRun: true,
|
||||||
|
|
||||||
customLaunchers: {
|
customLaunchers: {
|
||||||
ChromiumHeadlessNoSandbox: { base: 'ChromiumHeadless', flags: ['--no-sandbox'] }
|
ChromiumHeadlessNoSandbox: {
|
||||||
|
base: 'ChromiumHeadless',
|
||||||
|
flags: ['--no-sandbox']
|
||||||
|
}
|
||||||
},
|
},
|
||||||
browsers: ['ChromiumHeadlessNoSandbox', 'FirefoxHeadless'],
|
browsers: ['ChromiumHeadlessNoSandbox', 'FirefoxHeadless'],
|
||||||
concurrency: 1,
|
concurrency: 1,
|
||||||
@@ -47,6 +53,5 @@ module.exports = function (config) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
jsonToFileReporter: { outputPath: '.', fileName: 'suite-result.json' }
|
jsonToFileReporter: { outputPath: '.', fileName: 'suite-result.json' }
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,47 @@
|
|||||||
#!/bin/bash -e
|
#!/bin/bash -e
|
||||||
|
|
||||||
|
cleanup () {
|
||||||
|
rm -rf lib/dist "$renice_flag_file"
|
||||||
|
docker rm -f sqljs-benchmark-run 2> /dev/null || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
if [ ! -f sample.csv ]; then
|
if [ ! -f sample.csv ]; then
|
||||||
wget --header="accept-encoding: gzip" -q -O- \
|
wget --header="accept-encoding: gzip" -q -O- \
|
||||||
https://github.com/plotly/datasets/raw/547090bd/wellspublic.csv \
|
https://github.com/plotly/datasets/raw/547090bd/wellspublic.csv \
|
||||||
| gunzip -c > sample.csv
|
| gunzip -c > sample.csv
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for d in lib/build-* ; do
|
PLAYBOOK=procpath/karma_docker.procpath
|
||||||
rm -r lib/dist || true
|
|
||||||
cp -r $d lib/dist
|
|
||||||
|
|
||||||
name=$(basename $d)
|
# for renice to work run like "sudo -E env PATH=$PATH ./make.sh"
|
||||||
docker build -t sqliteviz/sqljs-benchmark:$name .
|
test_ni=$(nice -n -5 nice)
|
||||||
docker rm sqljs-benchmark-$name 2> /dev/null || true
|
if [ $test_ni == -5 ]; then
|
||||||
docker run -it --name sqljs-benchmark-$name sqliteviz/sqljs-benchmark:$name
|
renice_flag_file=$(mktemp)
|
||||||
docker cp sqljs-benchmark-$name:/tmp/build/suite-result.json ${name}-result.json
|
fi
|
||||||
docker rm sqljs-benchmark-$name
|
{
|
||||||
|
while [ -f $renice_flag_file ]; do
|
||||||
|
procpath --logging-level ERROR play -f $PLAYBOOK renice:watch
|
||||||
done
|
done
|
||||||
|
} &
|
||||||
|
|
||||||
rm -r lib/dist
|
shopt -s nullglob
|
||||||
|
for d in lib/build-* ; do
|
||||||
|
rm -rf lib/dist
|
||||||
|
cp -r $d lib/dist
|
||||||
|
sample_name=$(basename $d)
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
28
lib/sql-js/benchmark/procpath/karma_docker.procpath
Normal file
28
lib/sql-js/benchmark/procpath/karma_docker.procpath
Normal 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
|
||||||
29
lib/sql-js/benchmark/procpath/top2_cpu.sql
Normal file
29
lib/sql-js/benchmark/procpath/top2_cpu.sql
Normal 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)
|
||||||
13
lib/sql-js/benchmark/procpath/top2_rss.sql
Normal file
13
lib/sql-js/benchmark/procpath/top2_rss.sql
Normal 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
@@ -4,7 +4,6 @@ import lodash from 'lodash'
|
|||||||
import Papa from 'papaparse'
|
import Papa from 'papaparse'
|
||||||
import useragent from 'ua-parser-js'
|
import useragent from 'ua-parser-js'
|
||||||
|
|
||||||
|
|
||||||
describe('SQLite build benchmark', function () {
|
describe('SQLite build benchmark', function () {
|
||||||
let parsedCsv
|
let parsedCsv
|
||||||
let sqlModule
|
let sqlModule
|
||||||
@@ -50,10 +49,8 @@ describe('SQLite build benchmark', function () {
|
|||||||
suite.add('select', { initCount: 3, minSamples: 50, fn: benchmarkSelect })
|
suite.add('select', { initCount: 3, minSamples: 50, fn: benchmarkSelect })
|
||||||
await run(suite)
|
await run(suite)
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
function importToTable(db, parsedCsv, chunkSize = 1024) {
|
function importToTable(db, parsedCsv, chunkSize = 1024) {
|
||||||
const columnListString = parsedCsv.meta.fields.join(', ')
|
const columnListString = parsedCsv.meta.fields.join(', ')
|
||||||
db.exec(`CREATE TABLE csv_import(${columnListString})`)
|
db.exec(`CREATE TABLE csv_import(${columnListString})`)
|
||||||
@@ -67,7 +64,6 @@ function importToTable (db, parsedCsv, chunkSize = 1024) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PromiseWrapper {
|
class PromiseWrapper {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.promise = new Promise((resolve, reject) => {
|
this.promise = new Promise((resolve, reject) => {
|
||||||
@@ -102,7 +98,6 @@ function chunkArray (arr, size) {
|
|||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function createSuite() {
|
function createSuite() {
|
||||||
// Combined workaround from:
|
// Combined workaround from:
|
||||||
// - https://github.com/bestiejs/benchmark.js/issues/106
|
// - https://github.com/bestiejs/benchmark.js/issues/106
|
||||||
@@ -124,10 +119,12 @@ function run (suite) {
|
|||||||
console.info(String(event.target))
|
console.info(String(event.target))
|
||||||
})
|
})
|
||||||
.on('complete', function () {
|
.on('complete', function () {
|
||||||
console.log(JSON.stringify({
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
browser: useragent(navigator.userAgent).browser,
|
browser: useragent(navigator.userAgent).browser,
|
||||||
result: this.filter('successful')
|
result: this.filter('successful')
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
suiteResult.resolve()
|
suiteResult.resolve()
|
||||||
})
|
})
|
||||||
.on('error', function (event) {
|
.on('error', function (event) {
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import logging
|
|||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
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 = (
|
cflags = (
|
||||||
'-O2',
|
# SQLite configuration
|
||||||
'-DSQLITE_DEFAULT_CACHE_SIZE=-65536', # 64 MiB
|
'-DSQLITE_DEFAULT_CACHE_SIZE=-65536', # 64 MiB
|
||||||
'-DSQLITE_DEFAULT_MEMSTATUS=0',
|
'-DSQLITE_DEFAULT_MEMSTATUS=0',
|
||||||
'-DSQLITE_DEFAULT_SYNCHRONOUS=0',
|
'-DSQLITE_DEFAULT_SYNCHRONOUS=0',
|
||||||
@@ -13,26 +15,27 @@ cflags = (
|
|||||||
'-DSQLITE_ENABLE_FTS3',
|
'-DSQLITE_ENABLE_FTS3',
|
||||||
'-DSQLITE_ENABLE_FTS3_PARENTHESIS',
|
'-DSQLITE_ENABLE_FTS3_PARENTHESIS',
|
||||||
'-DSQLITE_ENABLE_FTS5',
|
'-DSQLITE_ENABLE_FTS5',
|
||||||
'-DSQLITE_ENABLE_JSON1',
|
|
||||||
'-DSQLITE_ENABLE_NORMALIZE',
|
'-DSQLITE_ENABLE_NORMALIZE',
|
||||||
'-DSQLITE_EXTRA_INIT=extra_init',
|
'-DSQLITE_EXTRA_INIT=extra_init',
|
||||||
'-DSQLITE_OMIT_DEPRECATED',
|
'-DSQLITE_OMIT_DEPRECATED',
|
||||||
'-DSQLITE_OMIT_LOAD_EXTENSION',
|
'-DSQLITE_OMIT_LOAD_EXTENSION',
|
||||||
'-DSQLITE_OMIT_SHARED_CACHE',
|
'-DSQLITE_OMIT_SHARED_CACHE',
|
||||||
'-DSQLITE_THREADSAFE=0',
|
'-DSQLITE_THREADSAFE=0',
|
||||||
|
# Compile-time optimisation
|
||||||
|
'-Os', # reduces the code size about in half comparing to -O2
|
||||||
|
'-flto',
|
||||||
|
'-Isrc', '-Isrc/lua',
|
||||||
)
|
)
|
||||||
emflags = (
|
emflags = (
|
||||||
# Base
|
# Base
|
||||||
'--memory-init-file', '0',
|
'--memory-init-file', '0',
|
||||||
'-s', 'RESERVED_FUNCTION_POINTERS=64',
|
|
||||||
'-s', 'ALLOW_TABLE_GROWTH=1',
|
'-s', 'ALLOW_TABLE_GROWTH=1',
|
||||||
'-s', 'SINGLE_FILE=0',
|
|
||||||
# WASM
|
# WASM
|
||||||
'-s', 'WASM=1',
|
'-s', 'WASM=1',
|
||||||
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
||||||
# Optimisation
|
'-s', 'ENVIRONMENT=web,worker',
|
||||||
'-s', 'INLINING_LIMIT=50',
|
# Link-time optimisation
|
||||||
'-O3',
|
'-Os',
|
||||||
'-flto',
|
'-flto',
|
||||||
# sql.js
|
# sql.js
|
||||||
'-s', 'EXPORTED_FUNCTIONS=@src/sqljs/exported_functions.json',
|
'-s', 'EXPORTED_FUNCTIONS=@src/sqljs/exported_functions.json',
|
||||||
@@ -50,22 +53,32 @@ def build(src: Path, dst: Path):
|
|||||||
'emcc',
|
'emcc',
|
||||||
*cflags,
|
*cflags,
|
||||||
'-c', src / 'sqlite3.c',
|
'-c', src / 'sqlite3.c',
|
||||||
'-o', out / 'sqlite3.bc',
|
'-o', out / 'sqlite3.o',
|
||||||
])
|
])
|
||||||
logging.info('Building LLVM bitcode for extension-functions.c')
|
logging.info('Building LLVM bitcode for extension-functions.c')
|
||||||
subprocess.check_call([
|
subprocess.check_call([
|
||||||
'emcc',
|
'emcc',
|
||||||
*cflags,
|
*cflags,
|
||||||
'-c', src / 'extension-functions.c',
|
'-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')
|
logging.info('Building WASM from bitcode')
|
||||||
subprocess.check_call([
|
subprocess.check_call([
|
||||||
'emcc',
|
'emcc',
|
||||||
*emflags,
|
*emflags,
|
||||||
out / 'sqlite3.bc',
|
out / 'sqlite3.o',
|
||||||
out / 'extension-functions.bc',
|
out / 'extension-functions.o',
|
||||||
|
out / 'sqlitelua.o',
|
||||||
'-o', out / 'sql-wasm.js',
|
'-o', out / 'sql-wasm.js',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tarfile
|
||||||
import zipfile
|
import zipfile
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib import request
|
from urllib import request
|
||||||
|
|
||||||
|
|
||||||
amalgamation_url = 'https://sqlite.org/2021/sqlite-amalgamation-3360000.zip'
|
amalgamation_url = 'https://sqlite.org/2023/sqlite-amalgamation-3410000.zip'
|
||||||
|
|
||||||
# Extension-functions
|
# Extension-functions
|
||||||
# ===================
|
# ===================
|
||||||
@@ -20,7 +22,7 @@ contrib_functions_url = 'https://sqlite.org/contrib/download/extension-functions
|
|||||||
extension_urls = (
|
extension_urls = (
|
||||||
# Miscellaneous extensions
|
# Miscellaneous extensions
|
||||||
# ========================
|
# ========================
|
||||||
('https://sqlite.org/src/raw/c6bd5d24?at=series.c', 'sqlite3_series_init'),
|
('https://sqlite.org/src/raw/8d79354f?at=series.c', 'sqlite3_series_init'),
|
||||||
('https://sqlite.org/src/raw/dbfd8543?at=closure.c', 'sqlite3_closure_init'),
|
('https://sqlite.org/src/raw/dbfd8543?at=closure.c', 'sqlite3_closure_init'),
|
||||||
('https://sqlite.org/src/raw/5bb2264c?at=uuid.c', 'sqlite3_uuid_init'),
|
('https://sqlite.org/src/raw/5bb2264c?at=uuid.c', 'sqlite3_uuid_init'),
|
||||||
('https://sqlite.org/src/raw/5853b0e5?at=regexp.c', 'sqlite3_regexp_init'),
|
('https://sqlite.org/src/raw/5853b0e5?at=regexp.c', 'sqlite3_regexp_init'),
|
||||||
@@ -28,10 +30,17 @@ extension_urls = (
|
|||||||
('https://sqlite.org/src/raw/09f967dc?at=decimal.c', 'sqlite3_decimal_init'),
|
('https://sqlite.org/src/raw/09f967dc?at=decimal.c', 'sqlite3_decimal_init'),
|
||||||
# Third-party extension
|
# 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):
|
def _generate_extra_init_c_function(init_function_names):
|
||||||
@@ -58,6 +67,38 @@ def _get_amalgamation(tgt: Path):
|
|||||||
shutil.copyfileobj(fr, fw)
|
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):
|
def _get_contrib_functions(tgt: Path):
|
||||||
request.urlretrieve(contrib_functions_url, tgt / 'extension-functions.c')
|
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:
|
for url, init_fn in extension_urls:
|
||||||
logging.info('Downloading and appending to amalgamation %s', url)
|
logging.info('Downloading and appending to amalgamation %s', url)
|
||||||
with request.urlopen(url) as resp:
|
with request.urlopen(url) as resp:
|
||||||
|
f.write(b'\n')
|
||||||
shutil.copyfileobj(resp, f)
|
shutil.copyfileobj(resp, f)
|
||||||
init_functions.append(init_fn)
|
init_functions.append(init_fn)
|
||||||
|
|
||||||
@@ -89,6 +131,7 @@ def _get_sqljs(tgt: Path):
|
|||||||
def configure(tgt: Path):
|
def configure(tgt: Path):
|
||||||
_get_amalgamation(tgt)
|
_get_amalgamation(tgt)
|
||||||
_get_contrib_functions(tgt)
|
_get_contrib_functions(tgt)
|
||||||
|
_get_lua(tgt)
|
||||||
_get_extensions(tgt)
|
_get_extensions(tgt)
|
||||||
_get_sqljs(tgt)
|
_get_sqljs(tgt)
|
||||||
|
|
||||||
|
|||||||
2
lib/sql-js/dist/sql-wasm.js
vendored
2
lib/sql-js/dist/sql-wasm.js
vendored
File diff suppressed because one or more lines are too long
BIN
lib/sql-js/dist/sql-wasm.wasm
vendored
BIN
lib/sql-js/dist/sql-wasm.wasm
vendored
Binary file not shown.
50429
package-lock.json
generated
50429
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
98
package.json
98
package.json
@@ -1,62 +1,84 @@
|
|||||||
{
|
{
|
||||||
"name": "sqliteviz",
|
"name": "sqliteviz",
|
||||||
"version": "0.17.0",
|
"version": "0.26.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"dev": "vite",
|
||||||
"build": "NODE_OPTIONS=--max_old_space_size=4096 vue-cli-service build",
|
"build": "vite build",
|
||||||
"test": "vue-cli-service karma",
|
"serve": "vite preview",
|
||||||
"lint": "vue-cli-service lint"
|
"test": "karma start karma.conf.cjs",
|
||||||
|
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
|
||||||
|
"format": "prettier . --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"codemirror": "^5.57.0",
|
"buffer": "^6.0.3",
|
||||||
|
"codemirror": "^5.65.18",
|
||||||
|
"codemirror-editor-vue3": "^2.8.0",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"dataurl-to-blob": "^0.0.1",
|
"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",
|
"html2canvas": "^1.1.4",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"nanoid": "^3.1.12",
|
"nanoid": "^3.1.12",
|
||||||
"papaparse": "^5.3.1",
|
"papaparse": "^5.4.1",
|
||||||
"pivottable": "^2.23.0",
|
"pivottable": "^2.23.0",
|
||||||
"plotly.js": "^1.58.4",
|
"plotly.js": "^2.35.2",
|
||||||
"promise-worker": "^2.0.1",
|
"promise-worker": "^2.0.1",
|
||||||
"react": "^16.13.1",
|
"react": "^16.14.0",
|
||||||
"react-chart-editor": "^0.45.0",
|
"react-chart-editor": "^0.46.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.14.0",
|
||||||
|
"seedrandom": "^3.0.5",
|
||||||
|
"sigma": "^3.0.1",
|
||||||
"sql.js": "file:./lib/sql-js",
|
"sql.js": "file:./lib/sql-js",
|
||||||
"vue": "^2.6.11",
|
"tiny-emitter": "^2.1.0",
|
||||||
"vue-codemirror": "^4.0.6",
|
"veaury": "^2.5.1",
|
||||||
"vue-js-modal": "^2.0.0-rc.6",
|
"vue": "^3.5.11",
|
||||||
"vue-multiselect": "^2.1.6",
|
"vue-final-modal": "^4.5.5",
|
||||||
"vue-router": "^3.2.0",
|
"vue-multiselect": "^3.0.0-beta.3",
|
||||||
"vue2-teleport": "^1.0.1",
|
"vue-router": "^4.4.5",
|
||||||
"vuejs-paginate": "^2.1.0",
|
"vuejs-paginate-next": "^1.0.2",
|
||||||
"vuera": "^0.2.7",
|
"vuex": "^4.1.0"
|
||||||
"vuex": "^3.4.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^4.4.0",
|
"@babel/core": "^7.25.7",
|
||||||
"@vue/cli-plugin-eslint": "^4.4.0",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"@vue/cli-plugin-router": "^4.4.0",
|
"@vue/eslint-config-standard": "^8.0.1",
|
||||||
"@vue/cli-plugin-vuex": "^4.4.0",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"@vue/cli-service": "^4.4.0",
|
|
||||||
"@vue/eslint-config-standard": "^5.1.2",
|
|
||||||
"@vue/test-utils": "^1.1.2",
|
|
||||||
"babel-eslint": "^10.1.0",
|
|
||||||
"chai": "^4.1.2",
|
"chai": "^4.1.2",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^8.0.1",
|
||||||
"eslint": "^6.7.2",
|
"eslint": "^8.57.1",
|
||||||
|
"eslint-config-prettier": "^10.1.1",
|
||||||
"eslint-plugin-import": "^2.20.2",
|
"eslint-plugin-import": "^2.20.2",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
"eslint-plugin-standard": "^4.0.0",
|
"eslint-plugin-standard": "^4.0.0",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"eslint-plugin-vue": "^9.28.0",
|
||||||
"karma": "^3.1.4",
|
"flush-promises": "^1.0.2",
|
||||||
"karma-firefox-launcher": "^2.1.0",
|
"karma": "^6.4.4",
|
||||||
"karma-webpack": "^4.0.2",
|
"karma-coverage": "^2.2.1",
|
||||||
"vue-cli-plugin-ui-karma": "^0.2.5",
|
"karma-coverage-istanbul-reporter": "^3.0.3",
|
||||||
"vue-template-compiler": "^2.6.11",
|
"karma-firefox-launcher": "^2.1.3",
|
||||||
"workbox-webpack-plugin": "^6.1.5",
|
"karma-mocha": "^1.3.0",
|
||||||
"worker-loader": "^3.0.8"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"background_color": "white",
|
"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",
|
"display": "fullscreen",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
|
|||||||
53
src/App.vue
53
src/App.vue
@@ -1,58 +1,85 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<router-view />
|
<router-view />
|
||||||
|
<modals-container />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<style>
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Open Sans";
|
font-family: 'Open Sans';
|
||||||
src: url("~@/assets/fonts/OpenSans-Regular.woff2");
|
src: url('@/assets/fonts/OpenSans-Regular.woff2');
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Open Sans";
|
font-family: 'Open Sans';
|
||||||
src: url("~@/assets/fonts/OpenSans-SemiBold.woff2");
|
src: url('@/assets/fonts/OpenSans-SemiBold.woff2');
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Open Sans";
|
font-family: 'Open Sans';
|
||||||
src: url("~@/assets/fonts/OpenSans-Bold.woff2");
|
src: url('@/assets/fonts/OpenSans-Bold.woff2');
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Open Sans";
|
font-family: 'Open Sans';
|
||||||
src: url("~@/assets/fonts/OpenSans-Italic.woff2");
|
src: url('@/assets/fonts/OpenSans-Italic.woff2');
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Open Sans";
|
font-family: 'Open Sans';
|
||||||
src: url("~@/assets/fonts/OpenSans-SemiBoldItalic.woff2");
|
src: url('@/assets/fonts/OpenSans-SemiBoldItalic.woff2');
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Open Sans";
|
font-family: 'Open Sans';
|
||||||
src: url("~@/assets/fonts/OpenSans-BoldItalic.woff2");
|
src: url('@/assets/fonts/OpenSans-BoldItalic.woff2');
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app,
|
#app,
|
||||||
|
.dialog,
|
||||||
input,
|
input,
|
||||||
label,
|
label,
|
||||||
button,
|
button,
|
||||||
.plotly_editor * {
|
.plotly_editor * {
|
||||||
font-family: "Open Sans", Helvetica, Arial, sans-serif;
|
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/assets/images/logo_simple.svg
Normal file
3
src/assets/images/logo_simple.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M26.8311 34.6554C25.4675 33.8178 24.177 32.8655 22.9735 31.8086V14.3616H30.5728V36.753C29.3146 36.0982 28.0673 35.399 26.8311 34.6554ZM41.4669 25.8486H33.8675V38.1514C36.3477 39.3055 38.884 40.3334 41.4669 41.2313V25.8486ZM22.9735 35.3046L22.4768 34.9051C21.7152 34.2725 21.0033 33.6232 20.3245 32.9739L2.2947 30.8763L5.60596 37.3024L28.7848 39.2002C26.7511 38.0537 24.8082 36.7513 22.9735 35.3046ZM41.0695 44.6441C38.4829 43.7946 35.9458 42.7997 33.4702 41.6641L32.543 41.198L17.2616 40.1825L19.8444 45.593L46.5 46.209C44.6788 45.7761 42.8411 45.2434 41.0695 44.6441ZM9.34768 14.3616C12.2649 19.4905 15.735 24.2807 19.6954 28.6455V11.2651L2.99007 2.99115L1.5 22.3859L18.702 31.2592C14.1919 26.5283 10.9703 20.7087 9.34768 14.3616Z" fill="#119DFF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 862 B |
@@ -59,5 +59,3 @@ button.secondary:disabled {
|
|||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
.dialog {
|
.dialog {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.dialog .vfm__content {
|
||||||
border-radius: var(--border-radius-big);
|
border-radius: var(--border-radius-big);
|
||||||
box-shadow: 0px 2px 9px rgba(80, 103, 132, 0.8);
|
box-shadow: 0px 2px 9px rgba(80, 103, 132, 0.8);
|
||||||
|
background-color: white;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-header {
|
.dialog-header {
|
||||||
@@ -16,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dialog-body {
|
.dialog-body {
|
||||||
min-height: 60px;
|
min-height: 56px;
|
||||||
background-color: var(--color-bg-light);
|
background-color: var(--color-bg-light);
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
border-top: 1px solid var(--color-border-light);
|
border-top: 1px solid var(--color-border-light);
|
||||||
@@ -35,6 +42,6 @@
|
|||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm--overlay {
|
.vfm__overlay.vfm--overlay {
|
||||||
background-color: rgba(162, 177, 198, 0.5);
|
background-color: rgba(162, 177, 198, 0.5);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,14 +62,14 @@
|
|||||||
margin: 2px;
|
margin: 2px;
|
||||||
}
|
}
|
||||||
.sqliteviz-select .multiselect__tag-icon:after {
|
.sqliteviz-select .multiselect__tag-icon:after {
|
||||||
content: url('~@/assets/images/delete-tag.svg');
|
content: url('@/assets/images/delete-tag.svg');
|
||||||
height: 14px;
|
height: 14px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sqliteviz-select .multiselect__tag-icon:focus:after,
|
.sqliteviz-select .multiselect__tag-icon:focus:after,
|
||||||
.sqliteviz-select .multiselect__tag-icon:hover: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,
|
.sqliteviz-select .multiselect__tag-icon:focus,
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sqliteviz-select .multiselect__select:before {
|
.sqliteviz-select .multiselect__select:before {
|
||||||
content: url('~@/assets/images/arrow.svg');
|
content: url('@/assets/images/arrow.svg');
|
||||||
border: none;
|
border: none;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sqliteviz-select .multiselect__select:hover:before {
|
.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 {
|
.sqliteviz-select.multiselect--active .multiselect__tags {
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ table.sqliteviz-table {
|
|||||||
margin-top: -35px;
|
margin-top: -35px;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
.sqliteviz-table thead th, .fixed-header {
|
.sqliteviz-table thead th,
|
||||||
|
.fixed-header {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -107,3 +108,9 @@ table.sqliteviz-table {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--color-text-base);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
line-height: 19px;;
|
line-height: 19px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
height: 19px;
|
height: 19px;
|
||||||
border-radius: var(--border-radius-medium);
|
border-radius: var(--border-radius-medium);
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
:root {
|
:root {
|
||||||
--color-white: #ffffff;
|
--color-white: #ffffff;
|
||||||
--color-gray-light: #F3F6FA;
|
--color-gray-light: #f3f6fa;
|
||||||
--color-gray-light-2: #DFE8F3;
|
--color-gray-light-2: #dfe8f3;
|
||||||
--color-gray-light-3: #C8D4E3;
|
--color-gray-light-3: #c8d4e3;
|
||||||
--color-gray-light-4:#EBF0F8;
|
--color-gray-light-4: #ebf0f8;
|
||||||
--color-gray-light-5: #f8f8f9;
|
--color-gray-light-5: #f8f8f9;
|
||||||
--color-gray-medium: #A2B1C6;
|
--color-gray-medium: #a2b1c6;
|
||||||
--color-gray-dark: #506784;
|
--color-gray-dark: #506784;
|
||||||
--color-blue-medium: #119DFF;
|
--color-blue-medium: #119dff;
|
||||||
--color-blue-dark: #0D76BF;
|
--color-blue-dark: #0d76bf;
|
||||||
--color-blue-dark-2: #2A3F5F;
|
--color-blue-dark-2: #2a3f5f;
|
||||||
--color-red: #EF553B;
|
--color-red: #ef553b;
|
||||||
--color-red-2: #DE350B;
|
--color-red-2: #de350b;
|
||||||
--color-red-light: #FFBDAD;
|
--color-red-light: #ffbdad;
|
||||||
--color-yellow: #FBEFCB;
|
--color-yellow: #fbefcb;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--color-bg-light: var(--color-gray-light);
|
--color-bg-light: var(--color-gray-light);
|
||||||
--color-bg-light-2: var(--color-gray-light-2);
|
--color-bg-light-2: var(--color-gray-light-2);
|
||||||
@@ -48,6 +46,3 @@
|
|||||||
.plotly-editor--theme-provider {
|
.plotly-editor--theme-provider {
|
||||||
--sidebar-width: 112px;
|
--sidebar-width: 112px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="['checkbox-container', { 'checked': checked }, {'disabled': disabled}]"
|
:class="[
|
||||||
|
'checkbox-container',
|
||||||
|
{ checked: checked },
|
||||||
|
{ disabled: disabled }
|
||||||
|
]"
|
||||||
@click.stop="onClick"
|
@click.stop="onClick"
|
||||||
>
|
>
|
||||||
<div v-show="!checked" class="unchecked" />
|
<div v-show="!checked" class="unchecked" />
|
||||||
<img
|
<img
|
||||||
v-show="checked && !disabled"
|
v-show="checked && !disabled && theme === 'light'"
|
||||||
:src="theme === 'light'
|
class="checked-light"
|
||||||
? require('@/assets/images/checkbox_checked_light.svg')
|
src="~@/assets/images/checkbox_checked_light.svg"
|
||||||
: require('@/assets/images/checkbox_checked.svg')"
|
/>
|
||||||
|
<img
|
||||||
|
v-show="checked && !disabled && theme !== 'light'"
|
||||||
|
class="checked"
|
||||||
|
src="~@/assets/images/checkbox_checked.svg"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
v-show="checked && disabled"
|
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>
|
<span v-if="label" class="label">{{ label }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,7 +35,7 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'accent',
|
default: 'accent',
|
||||||
validator: (value) => {
|
validator: value => {
|
||||||
return ['accent', 'light'].includes(value)
|
return ['accent', 'light'].includes(value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -46,6 +55,7 @@ export default {
|
|||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
emits: ['click'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
checked: this.init
|
checked: this.init
|
||||||
|
|||||||
@@ -1,385 +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'
|
|
||||||
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</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>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="{ 'disabled': disabled }">
|
<div :class="{ disabled: disabled }">
|
||||||
<div class="text-field-label">Delimiter</div>
|
<div class="text-field-label">Delimiter</div>
|
||||||
<div
|
<div
|
||||||
class="delimiter-selector-container"
|
class="delimiter-selector-container"
|
||||||
@@ -8,21 +8,21 @@
|
|||||||
>
|
>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<input
|
<input
|
||||||
:class="{ 'filled': filled }"
|
|
||||||
ref="delimiterInput"
|
ref="delimiterInput"
|
||||||
|
v-model="inputValue"
|
||||||
|
:class="{ filled: filled }"
|
||||||
type="text"
|
type="text"
|
||||||
maxlength="1"
|
maxlength="1"
|
||||||
v-model="inputValue"
|
|
||||||
@click.stop
|
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
|
@click.stop
|
||||||
/>
|
/>
|
||||||
<div class="name">{{ getSymbolName(value) }}</div>
|
<div class="name">{{ getSymbolName(modelValue) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="controls" @click.stop>
|
<div class="controls" @click.stop>
|
||||||
<clear-icon @click.native="clear" :disabled="disabled"/>
|
<clear-icon :disabled="disabled" @click="clear" />
|
||||||
<drop-down-chevron
|
<drop-down-chevron
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@click.native="!disabled && (showOptions = !showOptions)"
|
@click="!disabled && (showOptions = !showOptions)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,10 +30,11 @@
|
|||||||
<div
|
<div
|
||||||
v-for="(option, index) in options"
|
v-for="(option, index) in options"
|
||||||
:key="index"
|
:key="index"
|
||||||
@click="chooseOption(option)"
|
|
||||||
class="option"
|
class="option"
|
||||||
|
@click="chooseOption(option)"
|
||||||
>
|
>
|
||||||
<pre>{{option}}</pre><div>{{ getSymbolName(option) }}</div>
|
<pre>{{ option }}</pre>
|
||||||
|
<div>{{ getSymbolName(option) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,8 +47,13 @@ import ClearIcon from '@/components/svg/clear'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DelimiterSelector',
|
name: 'DelimiterSelector',
|
||||||
props: ['value', 'width', 'disabled'],
|
|
||||||
components: { DropDownChevron, ClearIcon },
|
components: { DropDownChevron, ClearIcon },
|
||||||
|
props: {
|
||||||
|
modelValue: String,
|
||||||
|
width: String,
|
||||||
|
disabled: Boolean
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showOptions: false,
|
showOptions: false,
|
||||||
@@ -60,8 +66,8 @@ export default {
|
|||||||
inputValue() {
|
inputValue() {
|
||||||
if (this.inputValue) {
|
if (this.inputValue) {
|
||||||
this.filled = true
|
this.filled = true
|
||||||
if (this.inputValue !== this.value) {
|
if (this.inputValue !== this.modelValue) {
|
||||||
this.$emit('input', this.inputValue)
|
this.$emit('update:modelValue', this.inputValue)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.filled = false
|
this.filled = false
|
||||||
@@ -69,7 +75,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.inputValue = this.value
|
this.inputValue = this.modelValue
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getSymbolName(str) {
|
getSymbolName(str) {
|
||||||
@@ -82,7 +88,7 @@ export default {
|
|||||||
this.inputValue = option
|
this.inputValue = option
|
||||||
this.showOptions = false
|
this.showOptions = false
|
||||||
},
|
},
|
||||||
onContainerClick (event) {
|
onContainerClick() {
|
||||||
this.$refs.delimiterInput.focus()
|
this.$refs.delimiterInput.focus()
|
||||||
},
|
},
|
||||||
|
|
||||||
518
src/components/CsvJsonImport/index.vue
Normal file
518
src/components/CsvJsonImport/index.vue
Normal 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>
|
||||||
@@ -10,44 +10,45 @@
|
|||||||
@click="browse"
|
@click="browse"
|
||||||
>
|
>
|
||||||
<div class="text">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="type === 'illustrated'" id="img-container">
|
<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
|
<img
|
||||||
id="left-arm-img"
|
id="left-arm-img"
|
||||||
:class="{'swing': state === 'dragover'}"
|
:class="{ swing: state === 'dragover' }"
|
||||||
:src="require('@/assets/images/leftArm.svg')"
|
src="~@/assets/images/leftArm.svg"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
id="file-img"
|
id="file-img"
|
||||||
ref="fileImg"
|
ref="fileImg"
|
||||||
:class="{
|
:class="{
|
||||||
'swing': state === 'dragover',
|
swing: state === 'dragover',
|
||||||
'fly': state === 'dropping',
|
fly: state === 'dropping',
|
||||||
'hidden': state === 'dropped'
|
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="drop-file-bottom-img" src="~@/assets/images/bottom.svg" />
|
||||||
<img id="body-img" :src="require('@/assets/images/body.svg')" />
|
<img id="body-img" src="~@/assets/images/body.svg" />
|
||||||
<img
|
<img
|
||||||
id="right-arm-img"
|
id="right-arm-img"
|
||||||
:class="{'swing': state === 'dragover'}"
|
:class="{ swing: state === 'dragover' }"
|
||||||
:src="require('@/assets/images/rightArm.svg')"
|
src="~@/assets/images/rightArm.svg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div id="error" class="error"></div>
|
<div id="error" class="error"></div>
|
||||||
|
|
||||||
<!--Parse csv dialog -->
|
<!--Parse csv or json dialog -->
|
||||||
<csv-import
|
<csv-json-import
|
||||||
ref="addCsv"
|
ref="addCsvJson"
|
||||||
:file="file"
|
:file="file"
|
||||||
:db="newDb"
|
:db="newDb"
|
||||||
dialog-name="importFromCsv"
|
dialogName="importFromCsvJson"
|
||||||
@cancel="cancelCsvImport"
|
@cancel="cancelImport"
|
||||||
@finish="finish"
|
@finish="finish"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,16 +58,21 @@
|
|||||||
import fIo from '@/lib/utils/fileIo'
|
import fIo from '@/lib/utils/fileIo'
|
||||||
import ChangeDbIcon from '@/components/svg/changeDb'
|
import ChangeDbIcon from '@/components/svg/changeDb'
|
||||||
import database from '@/lib/database'
|
import database from '@/lib/database'
|
||||||
import CsvImport from '@/components/CsvImport'
|
import CsvJsonImport from '@/components/CsvJsonImport'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DbUploader',
|
name: 'DbUploader',
|
||||||
|
components: {
|
||||||
|
ChangeDbIcon,
|
||||||
|
CsvJsonImport
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'small',
|
default: 'small',
|
||||||
validator: (value) => {
|
validator: value => {
|
||||||
return ['illustrated', 'small'].includes(value)
|
return ['illustrated', 'small'].includes(value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -76,10 +82,7 @@ export default {
|
|||||||
default: 'unset'
|
default: 'unset'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
emits: [],
|
||||||
ChangeDbIcon,
|
|
||||||
CsvImport
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
state: '',
|
state: '',
|
||||||
@@ -90,7 +93,7 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.type === 'illustrated') {
|
if (this.type === 'illustrated') {
|
||||||
this.animationPromise = new Promise((resolve) => {
|
this.animationPromise = new Promise(resolve => {
|
||||||
this.$refs.fileImg.addEventListener('animationend', event => {
|
this.$refs.fileImg.addEventListener('animationend', event => {
|
||||||
if (event.animationName.startsWith('fly')) {
|
if (event.animationName.startsWith('fly')) {
|
||||||
this.state = 'dropped'
|
this.state = 'dropped'
|
||||||
@@ -101,7 +104,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
cancelCsvImport () {
|
cancelImport() {
|
||||||
if (this.newDb) {
|
if (this.newDb) {
|
||||||
this.newDb.shutDown()
|
this.newDb.shutDown()
|
||||||
this.newDb = null
|
this.newDb = null
|
||||||
@@ -116,8 +119,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
loadDb(file) {
|
loadDb(file) {
|
||||||
return Promise.all([this.newDb.loadDb(file), this.animationPromise])
|
return Promise.all([this.newDb.loadDb(file), this.animationPromise]).then(
|
||||||
.then(this.finish)
|
this.finish
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
async checkFile(file) {
|
async checkFile(file) {
|
||||||
@@ -127,16 +131,25 @@ export default {
|
|||||||
if (fIo.isDatabase(file)) {
|
if (fIo.isDatabase(file)) {
|
||||||
this.loadDb(file)
|
this.loadDb(file)
|
||||||
} else {
|
} else {
|
||||||
|
const isJson = fIo.isJSON(file) || fIo.isNDJSON(file)
|
||||||
|
events.send('database.import', file.size, {
|
||||||
|
from: isJson ? 'json' : 'csv',
|
||||||
|
new_db: true
|
||||||
|
})
|
||||||
|
|
||||||
this.file = file
|
this.file = file
|
||||||
await this.$nextTick()
|
await this.$nextTick()
|
||||||
const csvImport = this.$refs.addCsv
|
const csvJsonImportModal = this.$refs.addCsvJson
|
||||||
csvImport.reset()
|
csvJsonImportModal.reset()
|
||||||
return Promise.all([csvImport.previewCsv(), this.animationPromise])
|
return Promise.all([
|
||||||
.then(csvImport.open)
|
csvJsonImportModal.preview(),
|
||||||
|
this.animationPromise
|
||||||
|
]).then(csvJsonImportModal.open)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
browse() {
|
browse() {
|
||||||
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
|
fIo
|
||||||
|
.getFileFromUser('.db,.sqlite,.sqlite3,.csv,.json,.ndjson')
|
||||||
.then(this.checkFile)
|
.then(this.checkFile)
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -237,8 +250,12 @@ export default {
|
|||||||
transform-origin: -74px 139px;
|
transform-origin: -74px 139px;
|
||||||
}
|
}
|
||||||
@keyframes swing {
|
@keyframes swing {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(-7deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(-7deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#file-img.fly {
|
#file-img.fly {
|
||||||
|
|||||||
77
src/components/Graph/CirclePackLayoutSettings.vue
Normal file
77
src/components/Graph/CirclePackLayoutSettings.vue
Normal 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>
|
||||||
125
src/components/Graph/ForceAtlasLayoutSettings.vue
Normal file
125
src/components/Graph/ForceAtlasLayoutSettings.vue
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<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="Scaling ratio">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.scalingRatio"
|
||||||
|
@update="update('scalingRatio', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Slow down">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.slowDown"
|
||||||
|
:min="1"
|
||||||
|
@update="update('slowDown', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Edge weight influence">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.edgeWeightInfluence"
|
||||||
|
@update="update('edgeWeightInfluence', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Edge weight">
|
||||||
|
<Dropdown
|
||||||
|
:options="keyOptions"
|
||||||
|
:value="modelValue.weightSource"
|
||||||
|
@change="update('weightSource', $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>
|
||||||
34
src/components/Graph/RandomLayoutSettings.vue
Normal file
34
src/components/Graph/RandomLayoutSettings.vue
Normal 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>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<button
|
||||||
:class="['icon-btn', { active }, { disabled }]"
|
:class="['icon-btn', { active }]"
|
||||||
|
:disabled="disabled"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
@mouseenter="showTooltip($event, tooltipPosition)"
|
@mouseenter="showTooltip($event, tooltipPosition)"
|
||||||
@mouseleave="hideTooltip"
|
@mouseleave="hideTooltip"
|
||||||
@@ -9,10 +10,15 @@
|
|||||||
<div v-show="loading" class="icon-in-progress">
|
<div v-show="loading" class="icon-in-progress">
|
||||||
<loading-indicator />
|
<loading-indicator />
|
||||||
</div>
|
</div>
|
||||||
<span v-if="tooltip" class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
<span
|
||||||
|
v-if="tooltip"
|
||||||
|
ref="tooltip"
|
||||||
|
class="icon-tooltip"
|
||||||
|
:style="tooltipStyle"
|
||||||
|
>
|
||||||
{{ tooltip }}
|
{{ tooltip }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -21,9 +27,16 @@ import LoadingIndicator from '@/components/LoadingIndicator'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SideBarButton',
|
name: 'SideBarButton',
|
||||||
props: ['active', 'disabled', 'tooltip', 'tooltipPosition', 'loading'],
|
|
||||||
components: { LoadingIndicator },
|
components: { LoadingIndicator },
|
||||||
mixins: [tooltipMixin],
|
mixins: [tooltipMixin],
|
||||||
|
props: {
|
||||||
|
active: Boolean,
|
||||||
|
disabled: Boolean,
|
||||||
|
tooltip: String,
|
||||||
|
tooltipPosition: String,
|
||||||
|
loading: Boolean
|
||||||
|
},
|
||||||
|
emits: ['click'],
|
||||||
methods: {
|
methods: {
|
||||||
onClick() {
|
onClick() {
|
||||||
this.hideTooltip()
|
this.hideTooltip()
|
||||||
@@ -38,35 +51,36 @@ export default {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 26px;
|
width: 26px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
.icon-btn:hover {
|
.icon-btn:hover {
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--border-radius-medium-2);
|
border-radius: var(--border-radius-medium-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-btn:hover .icon >>> path,
|
.icon-btn:hover .icon :deep(path),
|
||||||
.icon-btn.active .icon >>> path,
|
.icon-btn.active .icon :deep(path),
|
||||||
.icon-btn:hover .icon >>> circle,
|
.icon-btn:hover .icon :deep(circle),
|
||||||
.icon-btn.active .icon >>> circle {
|
.icon-btn.active .icon :deep(circle) {
|
||||||
fill: var(--color-accent);
|
fill: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled.icon-btn .icon >>> path,
|
.icon-btn:disabled .icon :deep(path),
|
||||||
.disabled.icon-btn .icon >>> circle {
|
.icon-btn:disabled .icon :deep(circle) {
|
||||||
fill: var(--color-border);
|
fill: var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled.icon-btn {
|
.icon-btn:disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled.icon-btn:hover .icon >>> path {
|
.disabled.icon-btn:hover .icon :deep(path) {
|
||||||
fill: var(--color-border);
|
fill: var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<modal
|
<modal
|
||||||
:name="name"
|
:modalId="name"
|
||||||
classes="dialog"
|
class="dialog"
|
||||||
height="auto"
|
|
||||||
:clickToClose="false"
|
:clickToClose="false"
|
||||||
|
:contentTransition="{ name: 'loading-dialog' }"
|
||||||
|
:overlayTransition="{ name: 'loading-dialog' }"
|
||||||
>
|
>
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
<close-icon @click="$emit('cancel')" :disabled="loading"/>
|
<close-icon :disabled="loading" @click="$emit('cancel')" />
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
<div v-if="loading" class="loading-dialog-body">
|
<div v-if="loading" class="loading-dialog-body">
|
||||||
@@ -15,13 +16,17 @@
|
|||||||
{{ loadingMsg }}
|
{{ loadingMsg }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="loading-dialog-body">
|
<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 }}
|
{{ successMsg }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-buttons-container">
|
<div class="dialog-buttons-container">
|
||||||
<button
|
<button
|
||||||
class="secondary"
|
class="secondary"
|
||||||
|
type="button"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@click="$emit('cancel')"
|
@click="$emit('cancel')"
|
||||||
>
|
>
|
||||||
@@ -29,6 +34,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="primary"
|
class="primary"
|
||||||
|
type="button"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@click="$emit('action')"
|
@click="$emit('action')"
|
||||||
>
|
>
|
||||||
@@ -43,7 +49,8 @@ import LoadingIndicator from '@/components/LoadingIndicator'
|
|||||||
import CloseIcon from '@/components/svg/close'
|
import CloseIcon from '@/components/svg/close'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'loadingDialog',
|
name: 'LoadingDialog',
|
||||||
|
components: { LoadingIndicator, CloseIcon },
|
||||||
props: {
|
props: {
|
||||||
loadingMsg: String,
|
loadingMsg: String,
|
||||||
successMsg: String,
|
successMsg: String,
|
||||||
@@ -52,6 +59,7 @@ export default {
|
|||||||
title: String,
|
title: String,
|
||||||
loading: Boolean
|
loading: Boolean
|
||||||
},
|
},
|
||||||
|
emits: ['cancel', 'action'],
|
||||||
watch: {
|
watch: {
|
||||||
loading() {
|
loading() {
|
||||||
if (this.loading) {
|
if (this.loading) {
|
||||||
@@ -59,7 +67,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: { LoadingIndicator, CloseIcon },
|
|
||||||
methods: {
|
methods: {
|
||||||
cancel() {
|
cancel() {
|
||||||
this.$emit('cancel')
|
this.$emit('cancel')
|
||||||
@@ -68,6 +75,31 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</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>
|
<style scoped>
|
||||||
.loading-dialog-body {
|
.loading-dialog-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<template>
|
<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
|
<circle
|
||||||
class="loader-svg bg"
|
class="loader-svg bg"
|
||||||
:style="{ strokeWidth }"
|
:style="{ strokeWidth }"
|
||||||
@@ -9,7 +14,11 @@
|
|||||||
/>
|
/>
|
||||||
<circle
|
<circle
|
||||||
class="loader-svg front"
|
class="loader-svg front"
|
||||||
:style="{ strokeDasharray: circleProgress, strokeDashoffset: offset, strokeWidth }"
|
:style="{
|
||||||
|
strokeDasharray: circleProgress,
|
||||||
|
strokeDashoffset: offset,
|
||||||
|
strokeWidth
|
||||||
|
}"
|
||||||
:cx="size / 2"
|
:cx="size / 2"
|
||||||
:cy="size / 2"
|
:cy="size / 2"
|
||||||
:r="radius"
|
:r="radius"
|
||||||
@@ -31,10 +40,13 @@ export default {
|
|||||||
default: 20
|
default: 20
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
emits: [],
|
||||||
computed: {
|
computed: {
|
||||||
circleProgress() {
|
circleProgress() {
|
||||||
const circle = this.radius * 3.14 * 2
|
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
|
const space = circle - dash
|
||||||
return `${dash}px, ${space}px`
|
return `${dash}px, ${space}px`
|
||||||
},
|
},
|
||||||
@@ -45,7 +57,7 @@ export default {
|
|||||||
return this.size / 2 - this.strokeWidth
|
return this.size / 2 - this.strokeWidth
|
||||||
},
|
},
|
||||||
offset() {
|
offset() {
|
||||||
return this.radius * 3.14 / 2
|
return (this.radius * 3.14) / 2
|
||||||
},
|
},
|
||||||
strokeWidth() {
|
strokeWidth() {
|
||||||
return this.size / 10
|
return this.size / 10
|
||||||
@@ -57,7 +69,10 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.loader-svg {
|
.loader-svg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0; right: 0; top: 0; bottom: 0;
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
fill: none;
|
fill: none;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
stroke: var(--color-accent);
|
stroke: var(--color-accent);
|
||||||
@@ -111,5 +126,4 @@ export default {
|
|||||||
r: 8;
|
r: 8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="logs-container" ref="logsContainer">
|
<div ref="logsContainer" class="logs-container">
|
||||||
<div v-for="(msg, index) in messages" :key="index" class="msg">
|
<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 === 'error'" src="~@/assets/images/error.svg" />
|
||||||
<img v-if="msg.type === 'info'" :src="require('@/assets/images/info.svg')" width="20px">
|
<img
|
||||||
<img v-if="msg.type === 'success'" :src="require('@/assets/images/success.svg')">
|
v-if="msg.type === 'info'"
|
||||||
<loading-indicator v-if="msg.type === 'loading'" :progress="msg.progress" />
|
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>
|
<span class="msg-text">{{ serializeMessage(msg) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -14,9 +21,10 @@
|
|||||||
import LoadingIndicator from '@/components/LoadingIndicator'
|
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'logs',
|
name: 'Logs',
|
||||||
props: ['messages'],
|
|
||||||
components: { LoadingIndicator },
|
components: { LoadingIndicator },
|
||||||
|
props: { messages: Array },
|
||||||
|
emits: [],
|
||||||
watch: {
|
watch: {
|
||||||
'messages.length': 'scrollToBottom'
|
'messages.length': 'scrollToBottom'
|
||||||
},
|
},
|
||||||
@@ -43,7 +51,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
result += msg.message
|
result += msg.message
|
||||||
if (!(/(\.|!|\?)$/.test(result))) {
|
if (!/(\.|!|\?)$/.test(result)) {
|
||||||
result += '.'
|
result += '.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,14 @@
|
|||||||
{ 'splitpanes-dragging': dragging }
|
{ 'splitpanes-dragging': dragging }
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<div class="movable-splitter" ref="movableSplitter" :style="movableSplitterStyle" />
|
|
||||||
<div
|
<div
|
||||||
class="splitpanes-pane"
|
ref="movableSplitter"
|
||||||
|
class="movable-splitter"
|
||||||
|
:style="movableSplitterStyle"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
ref="left"
|
ref="left"
|
||||||
|
class="splitpanes-pane"
|
||||||
:size="paneBefore.size"
|
:size="paneBefore.size"
|
||||||
max-size="30"
|
max-size="30"
|
||||||
:style="styles.before"
|
:style="styles.before"
|
||||||
@@ -27,8 +31,11 @@
|
|||||||
:class="[
|
:class="[
|
||||||
'toggle-btns',
|
'toggle-btns',
|
||||||
{
|
{
|
||||||
'both': after.max === 100 && before.max === 100 &&
|
both:
|
||||||
paneAfter.size > 0 && paneBefore.size > 0
|
after.max === 100 &&
|
||||||
|
before.max === 100 &&
|
||||||
|
paneAfter.size > 0 &&
|
||||||
|
paneBefore.size > 0
|
||||||
}
|
}
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
@@ -39,9 +46,9 @@
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="direction-icon"
|
class="direction-icon"
|
||||||
:src="require('@/assets/images/chevron.svg')"
|
src="~@/assets/images/chevron.svg"
|
||||||
:style="directionBeforeIconStyle"
|
:style="directionBeforeIconStyle"
|
||||||
>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="before.max === 100 && paneBefore.size > 0"
|
v-if="before.max === 100 && paneBefore.size > 0"
|
||||||
@@ -50,18 +57,14 @@
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="direction-icon"
|
class="direction-icon"
|
||||||
:src="require('@/assets/images/chevron.svg')"
|
src="~@/assets/images/chevron.svg"
|
||||||
:style="directionAfterIconStyle"
|
:style="directionAfterIconStyle"
|
||||||
>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- splitter end -->
|
<!-- splitter end -->
|
||||||
<div
|
<div ref="right" class="splitpanes-pane" :style="styles.after">
|
||||||
class="splitpanes-pane"
|
|
||||||
ref="right"
|
|
||||||
:style="styles.after"
|
|
||||||
>
|
|
||||||
<slot name="right-pane" />
|
<slot name="right-pane" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,14 +78,27 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
horizontal: { type: Boolean, default: false },
|
horizontal: { type: Boolean, default: false },
|
||||||
before: { type: Object },
|
before: { type: Object },
|
||||||
after: { type: Object }
|
after: { type: Object },
|
||||||
|
default: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {
|
||||||
|
before: 50,
|
||||||
|
after: 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
emits: [],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
container: null,
|
container: null,
|
||||||
paneBefore: this.before,
|
paneBefore: this.before,
|
||||||
paneAfter: this.after,
|
paneAfter: this.after,
|
||||||
beforeMinimising: {
|
beforeMinimising:
|
||||||
|
!this.after.size || !this.before.size
|
||||||
|
? this.default
|
||||||
|
: {
|
||||||
before: this.before.size,
|
before: this.before.size,
|
||||||
after: this.after.size
|
after: this.after.size
|
||||||
},
|
},
|
||||||
@@ -97,8 +113,12 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
styles() {
|
styles() {
|
||||||
return {
|
return {
|
||||||
before: { [this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%` },
|
before: {
|
||||||
after: { [this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%` }
|
[this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%`
|
||||||
|
},
|
||||||
|
after: {
|
||||||
|
[this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
movableSplitterStyle() {
|
movableSplitterStyle() {
|
||||||
@@ -138,25 +158,36 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.container = this.$refs.container
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
// Passive: false to prevent scrolling while touch dragging.
|
// 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)
|
document.addEventListener('mouseup', this.onMouseUp)
|
||||||
|
|
||||||
if ('ontouchstart' in window) {
|
if ('ontouchstart' in window) {
|
||||||
document.addEventListener('touchmove', this.onMouseMove, { passive: false })
|
document.addEventListener('touchmove', this.onMouseMove, {
|
||||||
|
passive: false
|
||||||
|
})
|
||||||
document.addEventListener('touchend', this.onMouseUp)
|
document.addEventListener('touchend', this.onMouseUp)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
unbindEvents() {
|
unbindEvents() {
|
||||||
document.removeEventListener('mousemove', this.onMouseMove, { passive: false })
|
document.removeEventListener('mousemove', this.onMouseMove, {
|
||||||
|
passive: false
|
||||||
|
})
|
||||||
document.removeEventListener('mouseup', this.onMouseUp)
|
document.removeEventListener('mouseup', this.onMouseUp)
|
||||||
|
|
||||||
if ('ontouchstart' in window) {
|
if ('ontouchstart' in window) {
|
||||||
document.removeEventListener('touchmove', this.onMouseMove, { passive: false })
|
document.removeEventListener('touchmove', this.onMouseMove, {
|
||||||
|
passive: false
|
||||||
|
})
|
||||||
document.removeEventListener('touchend', this.onMouseUp)
|
document.removeEventListener('touchend', this.onMouseUp)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -206,16 +237,14 @@ export default {
|
|||||||
this.beforeMinimising.before = this.paneBefore.size
|
this.beforeMinimising.before = this.paneBefore.size
|
||||||
this.beforeMinimising.after = this.paneAfter.size
|
this.beforeMinimising.after = this.paneAfter.size
|
||||||
pane.size = 0
|
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
|
otherPane.size = 100 - pane.size
|
||||||
} else {
|
} else {
|
||||||
this.paneBefore.size = this.beforeMinimising.before
|
this.paneBefore.size = this.beforeMinimising.before
|
||||||
this.paneAfter.size = this.beforeMinimising.after
|
this.paneAfter.size = this.beforeMinimising.after
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
this.container = this.$refs.container
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -227,9 +256,15 @@ export default {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.splitpanes-vertical {flex-direction: row;}
|
.splitpanes-vertical {
|
||||||
.splitpanes-horizontal {flex-direction: column;}
|
flex-direction: row;
|
||||||
.splitpanes-dragging * {user-select: none;}
|
}
|
||||||
|
.splitpanes-horizontal {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.splitpanes-dragging * {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
.splitpanes-pane {
|
.splitpanes-pane {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -276,7 +311,7 @@ export default {
|
|||||||
.splitpanes-vertical > .movable-splitter {
|
.splitpanes-vertical > .movable-splitter {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
height: 100%
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.splitpanes-horizontal > .splitpanes-splitter,
|
.splitpanes-horizontal > .splitpanes-splitter,
|
||||||
@@ -327,20 +362,32 @@ export default {
|
|||||||
left: 50%;
|
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);
|
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;
|
border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0;
|
||||||
margin-left: -1px;
|
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;
|
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);
|
border-radius: 0 0 var(--border-radius-small) var(--border-radius-small);
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ export default {
|
|||||||
// Get the cursor position relative to the splitpane container.
|
// Get the cursor position relative to the splitpane container.
|
||||||
getCurrentMouseDrag(event, container) {
|
getCurrentMouseDrag(event, container) {
|
||||||
const rect = container.getBoundingClientRect()
|
const rect = container.getBoundingClientRect()
|
||||||
const { clientX, clientY } = ('ontouchstart' in window && event.touches)
|
const { clientX, clientY } =
|
||||||
? event.touches[0]
|
'ontouchstart' in window && event.touches ? event.touches[0] : event
|
||||||
: event
|
|
||||||
return {
|
return {
|
||||||
x: clientX - rect.left,
|
x: clientX - rect.left,
|
||||||
y: clientY - rect.top
|
y: clientY - rect.top
|
||||||
@@ -15,20 +14,32 @@ export default {
|
|||||||
getCurrentDragPercentage(event, container, isHorisontal) {
|
getCurrentDragPercentage(event, container, isHorisontal) {
|
||||||
let drag = this.getCurrentMouseDrag(event, container)
|
let drag = this.getCurrentMouseDrag(event, container)
|
||||||
drag = drag[isHorisontal ? 'y' : 'x']
|
drag = drag[isHorisontal ? 'y' : 'x']
|
||||||
const containerSize = container[isHorisontal ? 'clientHeight' : 'clientWidth']
|
const containerSize =
|
||||||
return drag * 100 / containerSize
|
container[isHorisontal ? 'clientHeight' : 'clientWidth']
|
||||||
|
return (drag * 100) / containerSize
|
||||||
},
|
},
|
||||||
|
|
||||||
// Returns the new position in percents.
|
// Returns the new position in percents.
|
||||||
calculateOffset (event, { container, isHorisontal, paneBeforeMax, paneAfterMax }) {
|
calculateOffset(
|
||||||
const dragPercentage = this.getCurrentDragPercentage(event, container, isHorisontal)
|
event,
|
||||||
|
{ container, isHorisontal, paneBeforeMax, paneAfterMax }
|
||||||
|
) {
|
||||||
|
const dragPercentage = this.getCurrentDragPercentage(
|
||||||
|
event,
|
||||||
|
container,
|
||||||
|
isHorisontal
|
||||||
|
)
|
||||||
|
|
||||||
const paneBeforeMaxReached = paneBeforeMax < 100 && (dragPercentage >= paneBeforeMax)
|
const paneBeforeMaxReached =
|
||||||
const paneAfterMaxReached = paneAfterMax < 100 && (dragPercentage <= 100 - paneAfterMax)
|
paneBeforeMax < 100 && dragPercentage >= paneBeforeMax
|
||||||
|
const paneAfterMaxReached =
|
||||||
|
paneAfterMax < 100 && dragPercentage <= 100 - paneAfterMax
|
||||||
|
|
||||||
// Prevent dragging beyond pane max.
|
// Prevent dragging beyond pane max.
|
||||||
if (paneBeforeMaxReached || paneAfterMaxReached) {
|
if (paneBeforeMaxReached || paneAfterMaxReached) {
|
||||||
return paneBeforeMaxReached ? paneBeforeMax : Math.max(100 - paneAfterMax, 0)
|
return paneBeforeMaxReached
|
||||||
|
? paneBeforeMax
|
||||||
|
: Math.max(100 - paneAfterMax, 0)
|
||||||
} else {
|
} else {
|
||||||
return Math.min(Math.max(dragPercentage, 0), paneBeforeMax)
|
return Math.min(Math.max(dragPercentage, 0), paneBeforeMax)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<paginate
|
<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"
|
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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Paginate from 'vuejs-paginate'
|
import Paginate from 'vuejs-paginate-next'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Pager',
|
name: 'Pager',
|
||||||
components: { Paginate },
|
components: { Paginate },
|
||||||
props: ['pageCount', 'value'],
|
props: {
|
||||||
|
pageCount: Number,
|
||||||
|
modelValue: Number
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
page: this.value,
|
page: this.modelValue,
|
||||||
chevron: `
|
chevron: `
|
||||||
<svg width="9" height="9" viewBox="0 0 8 12" fill="none">
|
<svg width="9" height="9" viewBox="0 0 8 12" fill="none">
|
||||||
<path
|
<path
|
||||||
@@ -39,10 +43,10 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
page() {
|
page() {
|
||||||
this.$emit('input', this.page)
|
this.$emit('update:modelValue', this.page)
|
||||||
},
|
},
|
||||||
value () {
|
modelValue() {
|
||||||
this.page = this.value
|
this.page = this.modelValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,48 +58,52 @@ export default {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 10px;
|
line-height: 10px;
|
||||||
}
|
}
|
||||||
>>> .paginator-page-link {
|
:deep(a) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.paginator-page-link) {
|
||||||
padding: 2px 3px;
|
padding: 2px 3px;
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
display: block;
|
display: block;
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
>>> .paginator-page-link:hover {
|
:deep(.paginator-page-link:hover) {
|
||||||
color: var(--color-text-active);
|
color: var(--color-text-active);
|
||||||
}
|
}
|
||||||
>>> .paginator-page-link:active,
|
:deep(.paginator-page-link:active),
|
||||||
>>> .paginator-page-link:visited,
|
:deep(.paginator-page-link:visited),
|
||||||
>>> .paginator-page-link:focus,
|
:deep(.paginator-page-link:focus),
|
||||||
>>> .paginator-next:active,
|
:deep(.paginator-next:active),
|
||||||
>>> .paginator-next:visited,
|
:deep(.paginator-next:visited),
|
||||||
>>> .paginator-next:focus,
|
:deep(.paginator-next:focus),
|
||||||
>>> .paginator-prev:active,
|
:deep(.paginator-prev:active),
|
||||||
>>> .paginator-prev:visited,
|
:deep(.paginator-prev:visited),
|
||||||
>>> .paginator-prev:focus {
|
:deep(.paginator-prev:focus) {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
>>> .paginator-active-page,
|
:deep(.paginator-active-page),
|
||||||
>>> .paginator-active-page:hover {
|
:deep(.paginator-active-page:hover) {
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
>>> .paginator-break:hover,
|
:deep(.paginator-break:hover),
|
||||||
>>> .paginator-disabled:hover {
|
:deep(.paginator-disabled:hover) {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
>>> .paginator-prev svg {
|
:deep(.paginator-prev svg) {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
>>> .paginator-next:hover path,
|
:deep(.paginator-next:hover path),
|
||||||
>>> .paginator-prev:hover path {
|
:deep(.paginator-prev:hover path) {
|
||||||
fill: var(--color-text-active);
|
fill: var(--color-text-active);
|
||||||
}
|
}
|
||||||
>>> .paginator-disabled path,
|
:deep(.paginator-disabled path),
|
||||||
>>> .paginator-disabled:hover path {
|
:deep(.paginator-disabled:hover path) {
|
||||||
fill: var(--color-text-light-2);
|
fill: var(--color-text-light-2);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,24 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="rounded-bg">
|
<div class="rounded-bg">
|
||||||
<div class="header-container" ref="header-container">
|
<div ref="header-container" class="header-container">
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
v-for="(th, index) in header"
|
v-for="(th, index) in header"
|
||||||
|
:key="index"
|
||||||
class="fixed-header"
|
class="fixed-header"
|
||||||
:style="{ width: `${th.width}px` }"
|
:style="{ width: `${th.width}px` }"
|
||||||
:key="index"
|
:title="th.name"
|
||||||
>
|
>
|
||||||
{{ th.name }}
|
{{ th.name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="table-container"
|
|
||||||
ref="table-container"
|
ref="table-container"
|
||||||
|
class="table-container"
|
||||||
@scroll="onScrollTable"
|
@scroll="onScrollTable"
|
||||||
>
|
>
|
||||||
<table ref="table" class="sqliteviz-table">
|
<table
|
||||||
|
ref="table"
|
||||||
|
class="sqliteviz-table"
|
||||||
|
tabindex="0"
|
||||||
|
@keydown="onTableKeydown"
|
||||||
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th v-for="(th, index) in columns" :key="index" ref="th">
|
<th v-for="(th, index) in columns" :key="index" ref="th">
|
||||||
@@ -28,9 +34,18 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="rowIndex in currentPageData.count" :key="rowIndex">
|
<tr v-for="rowIndex in currentPageData.count" :key="rowIndex">
|
||||||
<td v-for="(col, colIndex) in columns" :key="colIndex">
|
<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">
|
<div class="cell-data" :style="cellStyle">
|
||||||
{{ dataSet.values[col][rowIndex - 1 + currentPageData.start] }}
|
{{ getCellText(col, rowIndex) }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -44,32 +59,43 @@
|
|||||||
<span v-if="preview">for preview</span>
|
<span v-if="preview">for preview</span>
|
||||||
<span v-if="time">in {{ time }}</span>
|
<span v-if="time">in {{ time }}</span>
|
||||||
</div>
|
</div>
|
||||||
<pager v-show="pageCount > 1" :page-count="pageCount" v-model="currentPage" />
|
<pager
|
||||||
|
v-show="pageCount > 1"
|
||||||
|
v-model="currentPage"
|
||||||
|
:pageCount="pageCount"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Pager from './Pager'
|
import Pager from './Pager.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SqlTable',
|
name: 'SqlTable',
|
||||||
components: { Pager },
|
components: { Pager },
|
||||||
props: {
|
props: {
|
||||||
dataSet: Object,
|
dataSet: Object,
|
||||||
time: String,
|
time: [String, Number],
|
||||||
pageSize: {
|
pageSize: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 20
|
default: 20
|
||||||
},
|
},
|
||||||
preview: Boolean
|
page: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
},
|
},
|
||||||
|
preview: Boolean,
|
||||||
|
selectedCellCoordinates: Object
|
||||||
|
},
|
||||||
|
emits: ['updateSelectedCell'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
header: null,
|
header: null,
|
||||||
tableWidth: null,
|
tableWidth: null,
|
||||||
currentPage: 1,
|
currentPage: this.page,
|
||||||
resizeObserver: null
|
resizeObserver: null,
|
||||||
|
selectedCellElement: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -99,7 +125,53 @@ 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: {
|
methods: {
|
||||||
|
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() {
|
calculateHeadersWidth() {
|
||||||
this.tableWidth = this.$refs['table-container'].offsetWidth
|
this.tableWidth = this.$refs['table-container'].offsetWidth
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -109,25 +181,101 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onScrollTable() {
|
onScrollTable() {
|
||||||
this.$refs['header-container'].scrollLeft = this.$refs['table-container'].scrollLeft
|
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])
|
||||||
},
|
},
|
||||||
mounted () {
|
onCellClick(e) {
|
||||||
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
|
this.selectCell(e.target.closest('td'), false)
|
||||||
this.resizeObserver.observe(this.$refs.table)
|
|
||||||
this.calculateHeadersWidth()
|
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
selectCell(cell, scrollTo = true) {
|
||||||
this.resizeObserver.unobserve(this.$refs.table)
|
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)
|
||||||
},
|
},
|
||||||
watch: {
|
moveFocusInTable(initialCell, direction) {
|
||||||
currentPageData: 'calculateHeadersWidth',
|
const currentRowIndex = +initialCell.dataset.row
|
||||||
dataSet () {
|
const currentColIndex = +initialCell.dataset.col
|
||||||
this.currentPage = 1
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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>
|
</style>
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<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 }}
|
{{ 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>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:class="{ error: errorMsg }"
|
:class="{ error: errorMsg }"
|
||||||
:style="{ width: width }"
|
:style="{ width: width }"
|
||||||
:value="value"
|
:value="modelValue"
|
||||||
:disabled="disabled"
|
: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 v-show="errorMsg" class="text-field-error">{{ errorMsg }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,9 +28,19 @@
|
|||||||
<script>
|
<script>
|
||||||
import HintIcon from '@/components/svg/hint'
|
import HintIcon from '@/components/svg/hint'
|
||||||
export default {
|
export default {
|
||||||
name: 'textField',
|
name: 'TextField',
|
||||||
props: ['placeholder', 'label', 'errorMsg', 'value', 'width', 'hint', 'maxHintWidth', 'disabled'],
|
components: { HintIcon },
|
||||||
components: { HintIcon }
|
props: {
|
||||||
|
placeholder: String,
|
||||||
|
label: String,
|
||||||
|
errorMsg: String,
|
||||||
|
modelValue: String,
|
||||||
|
width: String,
|
||||||
|
hint: String,
|
||||||
|
maxHintWidth: String,
|
||||||
|
disabled: Boolean
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue']
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -33,8 +33,8 @@
|
|||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
<span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
|
||||||
Add new table from CSV
|
Add new table from CSV, JSON or NDJSON
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -45,7 +45,8 @@ import tooltipMixin from '@/tooltipMixin'
|
|||||||
export default {
|
export default {
|
||||||
name: 'AddTableIcon',
|
name: 'AddTableIcon',
|
||||||
mixins: [tooltipMixin],
|
mixins: [tooltipMixin],
|
||||||
props: ['tooltip'],
|
props: { tooltip: String },
|
||||||
|
emits: ['click'],
|
||||||
methods: {
|
methods: {
|
||||||
onClick() {
|
onClick() {
|
||||||
this.hideTooltip()
|
this.hideTooltip()
|
||||||
|
|||||||
19
src/components/svg/arrow.vue
Normal file
19
src/components/svg/arrow.vue
Normal 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>
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
fill="#A2B1C6"
|
fill="#A2B1C6"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
<span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
|
||||||
Load another database or CSV
|
Load another database, CSV, JSON or NDJSON
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -31,8 +31,9 @@
|
|||||||
import tooltipMixin from '@/tooltipMixin'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'changeDbIcon',
|
name: 'ChangeDbIcon',
|
||||||
mixins: [tooltipMixin],
|
mixins: [tooltipMixin],
|
||||||
|
emits: ['click'],
|
||||||
methods: {
|
methods: {
|
||||||
onClick() {
|
onClick() {
|
||||||
this.hideTooltip()
|
this.hideTooltip()
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 18 18"
|
|
||||||
fill="none"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
@@ -46,7 +41,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ChartIcon'
|
name: 'ChartIcon'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg
|
<svg
|
||||||
:class="['clear-icon', {'disabled': disabled}]"
|
:class="['clear-icon', { disabled: disabled }]"
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
@@ -21,10 +21,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ClearIcon',
|
name: 'ClearIcon',
|
||||||
props: ['disabled']
|
props: { disabled: Boolean }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -42,6 +41,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.disabled.clear-icon:hover path {
|
.disabled.clear-icon:hover path {
|
||||||
fill: #C8D4E3;
|
fill: #c8d4e3;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 18 18"
|
|
||||||
fill="none"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
d="M14.1917 1.3851H12.4806V0.703125C12.4806 0.314758 12.1658 0 11.7775 0H6.246C5.85764 0
|
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
|
5.54288 0.314758 5.54288 0.703125V1.3851H3.83203C2.86276 1.3851 2.07422 2.17365 2.07422
|
||||||
@@ -26,7 +21,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ClipboardIcon'
|
name: 'ClipboardIcon'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg
|
<svg
|
||||||
@click.stop="$emit('click')"
|
:class="['icon', { disabled: disabled }]"
|
||||||
:class="['icon', {'disabled': disabled }]"
|
|
||||||
:width="size"
|
:width="size"
|
||||||
:height="size"
|
:height="size"
|
||||||
viewBox="0 0 14 14"
|
viewBox="0 0 14 14"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
@click.stop="$emit('click')"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M14 1.41L12.59 0L7 5.59L1.41 0L0 1.41L5.59 7L0 12.59L1.41 14L7 8.41L12.59 14L14
|
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,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
emits: ['click']
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 18 18"
|
|
||||||
fill="none"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
@@ -31,7 +26,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DataViewIcon'
|
name: 'DataViewIcon'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg
|
<svg
|
||||||
:class="['chevron-icon', {'disabled': disabled}]"
|
:class="['chevron-icon', { disabled: disabled }]"
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
@@ -15,10 +15,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DropDownChevron',
|
name: 'DropDownChevron',
|
||||||
props: ['disabled']
|
props: { disabled: Boolean }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -36,6 +35,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.disabled.chevron-icon:hover path {
|
.disabled.chevron-icon:hover path {
|
||||||
fill: #C8D4E3;
|
fill: #c8d4e3;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
25
src/components/svg/edgeArrow.vue
Normal file
25
src/components/svg/edgeArrow.vue
Normal 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>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
fill="#A2B1C6"
|
fill="#A2B1C6"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
<span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
|
||||||
{{ tooltip }}
|
{{ tooltip }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -29,7 +29,11 @@ import tooltipMixin from '@/tooltipMixin'
|
|||||||
export default {
|
export default {
|
||||||
name: 'ExportIcon',
|
name: 'ExportIcon',
|
||||||
mixins: [tooltipMixin],
|
mixins: [tooltipMixin],
|
||||||
props: ['tooltip', 'tooltipPosition'],
|
props: {
|
||||||
|
tooltip: String,
|
||||||
|
tooltipPosition: String
|
||||||
|
},
|
||||||
|
emits: ['click'],
|
||||||
methods: {
|
methods: {
|
||||||
onClick() {
|
onClick() {
|
||||||
this.hideTooltip()
|
this.hideTooltip()
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg
|
<svg width="19" height="18" viewBox="0 0 19 18" fill="none">
|
||||||
width="19"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 19 18"
|
|
||||||
fill="none"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
d="M6.07959 13.5756C6.05908 14.0209 5.93896 14.415 5.71924 14.7578C5.49951 15.0976 5.19043
|
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
|
15.3613 4.79199 15.5488C4.39648 15.7363 3.94385 15.83 3.43408 15.83C2.59326 15.83
|
||||||
@@ -55,7 +50,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ExportToCsvIcon'
|
name: 'ExportToCsvIcon'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg
|
<svg width="19" height="18" viewBox="0 0 19 18" fill="none">
|
||||||
width="19"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 19 18"
|
|
||||||
fill="none"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
d="M4.28369 13.9966C4.28369 13.7711 4.20312 13.5953 4.04199 13.4693C3.88379 13.3433 3.604
|
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
|
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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ExportToSvgIcon'
|
name: 'ExportToSvgIcon'
|
||||||
}
|
}
|
||||||
|
|||||||
40
src/components/svg/graph.vue
Normal file
40
src/components/svg/graph.vue
Normal 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>
|
||||||
@@ -33,7 +33,11 @@
|
|||||||
fill="#A2B1C6"
|
fill="#A2B1C6"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="icon-tooltip" :style="{...tooltipStyle, maxWidth: maxWidth }" ref="tooltip">
|
<span
|
||||||
|
ref="tooltip"
|
||||||
|
class="icon-tooltip"
|
||||||
|
:style="{ ...tooltipStyle, maxWidth: maxWidth }"
|
||||||
|
>
|
||||||
{{ hint }}
|
{{ hint }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,8 +48,12 @@ import tooltipMixin from '@/tooltipMixin'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HintIcon',
|
name: 'HintIcon',
|
||||||
props: ['hint', 'maxWidth'],
|
|
||||||
mixins: [tooltipMixin],
|
mixins: [tooltipMixin],
|
||||||
|
props: {
|
||||||
|
hint: String,
|
||||||
|
maxWidth: String
|
||||||
|
},
|
||||||
|
emits: ['click'],
|
||||||
methods: {
|
methods: {
|
||||||
onClick() {
|
onClick() {
|
||||||
this.hideTooltip()
|
this.hideTooltip()
|
||||||
|
|||||||
49
src/components/svg/html.vue
Normal file
49
src/components/svg/html.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="19"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 19 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5.1626 10.0745L7.56641 10.8831V12.2322L3.68164 10.6501V9.4812L7.56641
|
||||||
|
7.89917V9.2439L5.1626 10.0745ZM8.99023 13.3H7.93994L10.124 6.35229H11.1787L8.99023
|
||||||
|
13.3ZM14.1099 10.0613L11.7192 9.24829V7.90356L15.582 9.4856V10.6545L11.7192
|
||||||
|
12.2366V10.8918L14.1099 10.0613Z"
|
||||||
|
fill="#A2B1C6"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M2.17041 0.0637207H16.2185V1.56372H2.17041V9.30354H0.67041V1.56372C0.67041 0.73872
|
||||||
|
1.34541 0.0637207 2.17041 0.0637207Z"
|
||||||
|
fill="#A2B1C6"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M17.1704 0.0637207H15.3052V1.56372H17.1704V9.84163H18.6704V1.56372C18.6704 0.73872
|
||||||
|
17.9954 0.0637207 17.1704 0.0637207Z"
|
||||||
|
fill="#A2B1C6"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M2.17041 17.1098H15.8754V15.6098H2.17041V8.78486H0.67041V15.6098C0.67041 16.4348
|
||||||
|
1.34541 17.1098 2.17041 17.1098Z"
|
||||||
|
fill="#A2B1C6"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M17.1704 17.1098H15.3052V15.6098H17.1704V8.55939H18.6704V15.6098C18.6704 16.4348
|
||||||
|
17.9954 17.1098 17.1704 17.1098Z"
|
||||||
|
fill="#A2B1C6"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M18.1197 4.13787H1.76172V3.03787H18.1197V4.13787Z"
|
||||||
|
fill="#A2B1C6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'HtmlIcon'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 18 18"
|
|
||||||
fill="none"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
@@ -21,7 +16,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PivotIcon'
|
name: 'PivotIcon'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 18 18"
|
|
||||||
fill="none"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
d="M9 5.51953C6.57686 5.51953 4.60547 7.49092 4.60547 9.91406C4.60547 12.3372 6.57686
|
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
|
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"
|
5.5195V15.0117Z"
|
||||||
fill="#A2B1C6"
|
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>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
47
src/components/svg/row.vue
Normal file
47
src/components/svg/row.vue
Normal 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>
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg
|
<svg width="12" height="13" viewBox="0 0 12 13" fill="none">
|
||||||
width="12"
|
<path
|
||||||
height="13"
|
d="M11.1624 6.94358L0.770043 12.9436L0.770043 0.943573L11.1624 6.94358Z"
|
||||||
viewBox="0 0 12 13"
|
fill="#A2B1C6"
|
||||||
fill="none"
|
/>
|
||||||
>
|
|
||||||
<path d="M11.1624 6.94358L0.770043 12.9436L0.770043 0.943573L11.1624 6.94358Z" fill="#A2B1C6"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RunIcon'
|
name: 'RunIcon'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SortIcon',
|
name: 'SortIcon',
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg
|
<svg width="18" height="19" viewBox="0 0 18 19" fill="none">
|
||||||
width="18"
|
|
||||||
height="19"
|
|
||||||
viewBox="0 0 18 19"
|
|
||||||
fill="none"
|
|
||||||
>
|
|
||||||
<g clip-path="url(#clip0)">
|
<g clip-path="url(#clip0)">
|
||||||
<path
|
<path
|
||||||
d="M4.5 1.51343H10.5L15 6.01343V8.45284H13.5V6.76343H9.75V3.01343H4.5V8.45284H3V3.01343C3
|
d="M4.5 1.51343H10.5L15 6.01343V8.45284H13.5V6.76343H9.75V3.01343H4.5V8.45284H3V3.01343C3
|
||||||
@@ -47,14 +42,18 @@
|
|||||||
</g>
|
</g>
|
||||||
<defs>
|
<defs>
|
||||||
<clipPath id="clip0">
|
<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>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SqlEditorIcon'
|
name: 'SqlEditorIcon'
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/components/svg/stop.vue
Normal file
20
src/components/svg/stop.vue
Normal 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>
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 18 18"
|
|
||||||
fill="none"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
@@ -41,7 +36,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TableIcon'
|
name: 'TableIcon'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'treeChevron',
|
name: 'TreeChevron',
|
||||||
props: {
|
props: {
|
||||||
expanded: {
|
expanded: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -31,7 +30,7 @@ export default {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chevron-icon {
|
.chevron-icon {
|
||||||
-webkit-transition: transform .15s ease-in-out;
|
-webkit-transition: transform 0.15s ease-in-out;
|
||||||
transition: transform .15s ease-in-out;
|
transition: transform 0.15s ease-in-out;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
44
src/components/svg/viewCellValue.vue
Normal file
44
src/components/svg/viewCellValue.vue
Normal 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>
|
||||||
52
src/lib/GraphEditorControls.jsx
Normal file
52
src/lib/GraphEditorControls.jsx
Normal 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
|
||||||
65
src/lib/ReactPlotlyEditorWithPlotRef.jsx
Normal file
65
src/lib/ReactPlotlyEditorWithPlotRef.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
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 plotly from 'plotly.js'
|
||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
|
||||||
export function getOptionsFromDataSources(dataSources) {
|
export function getOptionsFromDataSources(dataSources) {
|
||||||
if (!dataSources) {
|
if (!dataSources) {
|
||||||
@@ -20,7 +21,7 @@ export function getOptionsForSave (state, dataSources) {
|
|||||||
for (const key in dataSources) {
|
for (const key in dataSources) {
|
||||||
emptySources[key] = []
|
emptySources[key] = []
|
||||||
}
|
}
|
||||||
dereference(stateCopy.data, emptySources)
|
dereference.default(stateCopy.data, emptySources)
|
||||||
return stateCopy
|
return stateCopy
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,8 +34,43 @@ export async function getImageDataUrl (element, type) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getChartData(element) {
|
||||||
|
const chartElement = element.querySelector('.js-plotly-plot')
|
||||||
|
return {
|
||||||
|
data: chartElement.data,
|
||||||
|
layout: chartElement.layout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHtml(options) {
|
||||||
|
const chartId = nanoid()
|
||||||
|
return `
|
||||||
|
<script src="https://cdn.plot.ly/plotly-latest.js" charset="UTF-8"></script>
|
||||||
|
<div id="${chartId}"></div>
|
||||||
|
<script>
|
||||||
|
const el = document.getElementById("${chartId}")
|
||||||
|
|
||||||
|
let timeout
|
||||||
|
function debounceResize() {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
var r = el.getBoundingClientRect()
|
||||||
|
Plotly.relayout(el, {width: r.width, height: r.height})
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(debounceResize)
|
||||||
|
resizeObserver.observe(el)
|
||||||
|
|
||||||
|
Plotly.newPlot(el, ${JSON.stringify(options.data)}, ${JSON.stringify(options.layout)})
|
||||||
|
</script>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getOptionsFromDataSources,
|
getOptionsFromDataSources,
|
||||||
getOptionsForSave,
|
getOptionsForSave,
|
||||||
getImageDataUrl
|
getImageDataUrl,
|
||||||
|
getHtml,
|
||||||
|
getChartData
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ const hintsByCode = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getResult (source) {
|
getResult(source, columns) {
|
||||||
const result = {
|
const result = {
|
||||||
columns: []
|
columns: columns || []
|
||||||
}
|
}
|
||||||
const values = {}
|
const values = {}
|
||||||
if (source.meta.fields) {
|
if (source.meta.fields) {
|
||||||
@@ -24,8 +24,18 @@ export default {
|
|||||||
return value
|
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 {
|
} 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}`
|
const colName = `col${i + 1}`
|
||||||
result.columns.push(colName)
|
result.columns.push(colName)
|
||||||
values[colName] = source.data.map(row => {
|
values[colName] = source.data.map(row => {
|
||||||
@@ -51,7 +61,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let rowNumber = 0; rowNumber < rowCount; rowNumber++) {
|
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
|
return result
|
||||||
@@ -73,8 +85,10 @@ export default {
|
|||||||
comments: false,
|
comments: false,
|
||||||
step: undefined,
|
step: undefined,
|
||||||
complete: results => {
|
complete: results => {
|
||||||
const res = {
|
let res
|
||||||
data: this.getResult(results),
|
try {
|
||||||
|
res = {
|
||||||
|
data: this.getResult(results, config.columns),
|
||||||
delimiter: results.meta.delimiter,
|
delimiter: results.meta.delimiter,
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
rowCount: results.data.length
|
rowCount: results.data.length
|
||||||
@@ -85,9 +99,12 @@ export default {
|
|||||||
msg.hint = hintsByCode[msg.code]
|
msg.hint = hintsByCode[msg.code]
|
||||||
return msg
|
return msg
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
resolve(res)
|
resolve(res)
|
||||||
},
|
},
|
||||||
error: (error, file) => {
|
error: error => {
|
||||||
reject(error)
|
reject(error)
|
||||||
},
|
},
|
||||||
download: false,
|
download: false,
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
import initSqlJs from 'sql.js/dist/sql-wasm.js'
|
import initSqlJs from 'sql.js'
|
||||||
import dbUtils from './_statements'
|
import dbUtils from './_statements'
|
||||||
|
import wasmUrl from 'sql.js/dist/sql-wasm.wasm?url'
|
||||||
|
|
||||||
let SQL = null
|
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) {
|
if (!sqlResult) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
const dataSorces = {}
|
const dataSources = {}
|
||||||
sqlResult.columns.forEach((column, index) => {
|
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 {
|
export default class Sql {
|
||||||
@@ -21,8 +26,7 @@ export default class Sql {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static build() {
|
static build() {
|
||||||
return sqlModuleReady
|
return sqlModuleReady.then(() => {
|
||||||
.then(() => {
|
|
||||||
return new Sql()
|
return new Sql()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -77,7 +81,10 @@ export default class Sql {
|
|||||||
}
|
}
|
||||||
this.db.exec('COMMIT')
|
this.db.exec('COMMIT')
|
||||||
count++
|
count++
|
||||||
progressCallback({ progress: 100 * (count / chunksAmount), id: progressCounterId })
|
progressCallback({
|
||||||
|
progress: 100 * (count / chunksAmount),
|
||||||
|
id: progressCounterId
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ export default {
|
|||||||
*generateChunks(data, size) {
|
*generateChunks(data, size) {
|
||||||
const matrix = Object.keys(data).map(col => data[col])
|
const matrix = Object.keys(data).map(col => data[col])
|
||||||
const [row] = matrix
|
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)
|
const count = Math.ceil(transposedMatrix.length / size)
|
||||||
|
|
||||||
@@ -38,7 +40,8 @@ export default {
|
|||||||
type = 'TEXT'
|
type = 'TEXT'
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
default: type = 'TEXT'
|
default:
|
||||||
|
type = 'TEXT'
|
||||||
}
|
}
|
||||||
result += `"${col}" ${type}, `
|
result += `"${col}" ${type}, `
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,5 @@ function onError (error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
registerPromiseWorker(data => {
|
registerPromiseWorker(data => {
|
||||||
return sqlReady
|
return sqlReady.then(processMsg.bind(data)).catch(onError)
|
||||||
.then(processMsg.bind(data))
|
|
||||||
.catch(onError)
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import fu from '@/lib/utils/fileIo'
|
import fu from '@/lib/utils/fileIo'
|
||||||
// We can import workers like so because of worker-loader:
|
|
||||||
// https://webpack.js.org/loaders/worker-loader/
|
|
||||||
import Worker from './_worker.js'
|
|
||||||
|
|
||||||
// Use promise-worker in order to turn worker into the promise based one:
|
// Use promise-worker in order to turn worker into the promise based one:
|
||||||
// https://github.com/nolanlawson/promise-worker
|
// https://github.com/nolanlawson/promise-worker
|
||||||
import PromiseWorker from 'promise-worker'
|
import PromiseWorker from 'promise-worker'
|
||||||
|
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
function getNewDatabase() {
|
function getNewDatabase() {
|
||||||
const worker = new Worker()
|
const worker = new Worker(new URL('./_worker.js', import.meta.url), {
|
||||||
|
type: 'module'
|
||||||
|
})
|
||||||
return new Database(worker)
|
return new Database(worker)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,9 +30,11 @@ class Database {
|
|||||||
const progress = e.data.progress
|
const progress = e.data.progress
|
||||||
if (progress !== undefined) {
|
if (progress !== undefined) {
|
||||||
const id = e.data.id
|
const id = e.data.id
|
||||||
this.importProgresses[id].dispatchEvent(new CustomEvent('progress', {
|
this.importProgresses[id].dispatchEvent(
|
||||||
|
new CustomEvent('progress', {
|
||||||
detail: progress
|
detail: progress
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -43,7 +46,9 @@ class Database {
|
|||||||
createProgressCounter(callback) {
|
createProgressCounter(callback) {
|
||||||
const id = progressCounterIds++
|
const id = progressCounterIds++
|
||||||
this.importProgresses[id] = new EventTarget()
|
this.importProgresses[id] = new EventTarget()
|
||||||
this.importProgresses[id].addEventListener('progress', e => { callback(e.detail) })
|
this.importProgresses[id].addEventListener('progress', e => {
|
||||||
|
callback(e.detail)
|
||||||
|
})
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,14 +73,22 @@ class Database {
|
|||||||
|
|
||||||
async loadDb(file) {
|
async loadDb(file) {
|
||||||
const fileContent = file ? await fu.readAsArrayBuffer(file) : null
|
const fileContent = file ? await fu.readAsArrayBuffer(file) : null
|
||||||
const res = await this.pw.postMessage({ action: 'open', buffer: fileContent })
|
const res = await this.pw.postMessage({
|
||||||
|
action: 'open',
|
||||||
|
buffer: fileContent
|
||||||
|
})
|
||||||
|
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
throw new Error(res.error)
|
throw new Error(res.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dbName = file ? fu.getFileName(file) : 'database'
|
this.dbName = file ? fu.getFileName(file) : 'database'
|
||||||
this.refreshSchema()
|
await this.refreshSchema()
|
||||||
|
|
||||||
|
events.send('database.import', file ? file.size : 0, {
|
||||||
|
from: file ? 'sqlite' : 'none',
|
||||||
|
new_db: true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshSchema() {
|
async refreshSchema() {
|
||||||
@@ -114,6 +127,7 @@ class Database {
|
|||||||
throw new Error(data.error)
|
throw new Error(data.error)
|
||||||
}
|
}
|
||||||
fu.exportToFile(data, fileName)
|
fu.exportToFile(data, fileName)
|
||||||
|
events.send('database.export', data.byteLength, { to: 'sqlite' })
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateTableName(name) {
|
async validateTableName(name) {
|
||||||
@@ -122,7 +136,9 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (/[^\w]/.test(name)) {
|
if (/[^\w]/.test(name)) {
|
||||||
throw new Error('Table name can contain only letters, digits and underscores')
|
throw new Error(
|
||||||
|
'Table name can contain only letters, digits and underscores'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/^(\d)/.test(name)) {
|
if (/^(\d)/.test(name)) {
|
||||||
|
|||||||
8
src/lib/eventBus.js
Normal file
8
src/lib/eventBus.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import emitter from 'tiny-emitter/instance'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
$on: (...args) => emitter.on(...args),
|
||||||
|
$once: (...args) => emitter.once(...args),
|
||||||
|
$off: (...args) => emitter.off(...args),
|
||||||
|
$emit: (...args) => emitter.emit(...args)
|
||||||
|
}
|
||||||
364
src/lib/graphHelper.js
Normal file
364
src/lib/graphHelper.js
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
import { COLOR_PICKER_CONSTANTS } from 'react-colorscales'
|
||||||
|
import tinycolor from 'tinycolor2'
|
||||||
|
|
||||||
|
const TYPE_NODE = 0
|
||||||
|
const TYPE_EDGE = 1
|
||||||
|
const DEFAULT_SCALE = COLOR_PICKER_CONSTANTS.DEFAULT_SCALE
|
||||||
|
|
||||||
|
export function buildNodes(graph, dataSources, options) {
|
||||||
|
const docColumn = Object.keys(dataSources)[0] || 'doc'
|
||||||
|
const { objectType, nodeId } = options.structure
|
||||||
|
|
||||||
|
if (objectType && nodeId) {
|
||||||
|
const nodes = dataSources[docColumn]
|
||||||
|
.map(json => JSON.parse(json))
|
||||||
|
.filter(item => item[objectType] === TYPE_NODE)
|
||||||
|
|
||||||
|
nodes.forEach(node => {
|
||||||
|
graph.addNode(node[nodeId], {
|
||||||
|
data: node
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildEdges(graph, dataSources, options) {
|
||||||
|
const docColumn = Object.keys(dataSources)[0] || 'doc'
|
||||||
|
const { objectType, edgeSource, edgeTarget } = options.structure
|
||||||
|
|
||||||
|
if (objectType && edgeSource && edgeTarget) {
|
||||||
|
const edges = dataSources[docColumn]
|
||||||
|
.map(json => JSON.parse(json))
|
||||||
|
.filter(item => item[objectType] === TYPE_EDGE)
|
||||||
|
|
||||||
|
edges.forEach(edge => {
|
||||||
|
const source = edge[edgeSource]
|
||||||
|
const target = edge[edgeTarget]
|
||||||
|
if (graph.hasNode(source) && graph.hasNode(target)) {
|
||||||
|
graph.addEdge(source, target, {
|
||||||
|
data: edge
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateNodes(graph, attributeUpdates) {
|
||||||
|
const changeMethods = []
|
||||||
|
if (attributeUpdates.label) {
|
||||||
|
changeMethods.push(getUpdateLabelMethod(attributeUpdates.label))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributeUpdates.size) {
|
||||||
|
changeMethods.push(getUpdateSizeMethod(graph, attributeUpdates.size))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributeUpdates.color) {
|
||||||
|
changeMethods.push(getUpdateNodeColorMethod(graph, attributeUpdates.color))
|
||||||
|
}
|
||||||
|
graph.forEachNode(nodeId => {
|
||||||
|
graph.updateNode(nodeId, attributes => {
|
||||||
|
const newAttributes = { ...attributes }
|
||||||
|
changeMethods.forEach(method => method(newAttributes, nodeId))
|
||||||
|
return newAttributes
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateEdges(graph, attributeUpdates) {
|
||||||
|
const changeMethods = []
|
||||||
|
if (attributeUpdates.label) {
|
||||||
|
changeMethods.push(getUpdateLabelMethod(attributeUpdates.label))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributeUpdates.size) {
|
||||||
|
changeMethods.push(getUpdateSizeMethod(graph, attributeUpdates.size))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributeUpdates.color) {
|
||||||
|
changeMethods.push(getUpdateEdgeColorMethod(graph, attributeUpdates.color))
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('showDirection' in attributeUpdates) {
|
||||||
|
changeMethods.push(
|
||||||
|
attributes =>
|
||||||
|
(attributes.type = attributeUpdates.showDirection ? 'arrow' : 'line')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
graph.forEachEdge((edgeId, attributes, source, target) => {
|
||||||
|
graph.updateEdge(source, target, attributes => {
|
||||||
|
const newAttributes = { ...attributes }
|
||||||
|
changeMethods.forEach(method => method(newAttributes, edgeId))
|
||||||
|
return newAttributes
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUpdateLabelMethod(labelSettings) {
|
||||||
|
const { source } = labelSettings
|
||||||
|
return attributes => {
|
||||||
|
const label = attributes.data[source] ?? ''
|
||||||
|
attributes.label = label.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUpdateSizeMethod(graph, sizeSettings) {
|
||||||
|
const { type, value, source, scale, mode, min, method } = sizeSettings
|
||||||
|
if (type === 'constant') {
|
||||||
|
return attributes => (attributes.size = value)
|
||||||
|
} else if (type === 'variable') {
|
||||||
|
return getVariabledSizeMethod(mode, source, scale, min)
|
||||||
|
} else {
|
||||||
|
return (attributes, nodeId) =>
|
||||||
|
(attributes.size = Math.max(graph[method](nodeId) * scale, min))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDirectVariableColorUpdateMethod(source) {
|
||||||
|
return attributes =>
|
||||||
|
(attributes.color = tinycolor(attributes.data[source]).toHexString())
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUpdateNodeColorMethod(graph, colorSettings) {
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
value,
|
||||||
|
source,
|
||||||
|
sourceUsage,
|
||||||
|
colorscale,
|
||||||
|
colorscaleDirection,
|
||||||
|
mode,
|
||||||
|
method
|
||||||
|
} = colorSettings
|
||||||
|
if (type === 'constant') {
|
||||||
|
return attributes => (attributes.color = value)
|
||||||
|
} else if (type === 'variable') {
|
||||||
|
return sourceUsage === 'map_to'
|
||||||
|
? getColorMethod(
|
||||||
|
graph,
|
||||||
|
mode,
|
||||||
|
(nodeId, attributes) => attributes.data[source],
|
||||||
|
colorscale,
|
||||||
|
colorscaleDirection,
|
||||||
|
getNodeValueScale
|
||||||
|
)
|
||||||
|
: getDirectVariableColorUpdateMethod(source)
|
||||||
|
} else {
|
||||||
|
return getColorMethod(
|
||||||
|
graph,
|
||||||
|
mode,
|
||||||
|
nodeId => graph[method](nodeId),
|
||||||
|
colorscale,
|
||||||
|
colorscaleDirection,
|
||||||
|
getNodeValueScale
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUpdateEdgeColorMethod(graph, colorSettings) {
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
value,
|
||||||
|
source,
|
||||||
|
sourceUsage,
|
||||||
|
colorscale,
|
||||||
|
colorscaleDirection,
|
||||||
|
mode
|
||||||
|
} = colorSettings
|
||||||
|
if (type === 'constant') {
|
||||||
|
return attributes => (attributes.color = value)
|
||||||
|
} else {
|
||||||
|
return sourceUsage === 'map_to'
|
||||||
|
? getColorMethod(
|
||||||
|
graph,
|
||||||
|
mode,
|
||||||
|
(edgeId, attributes) => attributes.data[source],
|
||||||
|
colorscale,
|
||||||
|
colorscaleDirection,
|
||||||
|
getEdgeValueScale
|
||||||
|
)
|
||||||
|
: getDirectVariableColorUpdateMethod(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVariabledSizeMethod(mode, source, scale, min) {
|
||||||
|
if (mode === 'diameter') {
|
||||||
|
return attributes =>
|
||||||
|
(attributes.size = Math.max(
|
||||||
|
(attributes.data[source] / 2) * scale,
|
||||||
|
min / 2
|
||||||
|
))
|
||||||
|
} else if (mode === 'area') {
|
||||||
|
return attributes =>
|
||||||
|
(attributes.size = Math.max(
|
||||||
|
Math.sqrt((attributes.data[source] / 2) * scale),
|
||||||
|
min / 2
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
return attributes =>
|
||||||
|
(attributes.size = Math.max(attributes.data[source] * scale, min))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColorMethod(
|
||||||
|
graph,
|
||||||
|
mode,
|
||||||
|
sourceGetter,
|
||||||
|
selectedColorscale,
|
||||||
|
colorscaleDirection,
|
||||||
|
valueScaleGetter
|
||||||
|
) {
|
||||||
|
const valueScale = valueScaleGetter(graph, sourceGetter)
|
||||||
|
let colorscale = selectedColorscale || DEFAULT_SCALE
|
||||||
|
if (colorscaleDirection === 'reversed') {
|
||||||
|
colorscale = [...colorscale].reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'categorical') {
|
||||||
|
const colorMap = Object.fromEntries(
|
||||||
|
valueScale.map((value, index) => [
|
||||||
|
value,
|
||||||
|
colorscale[index % colorscale.length]
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
return (attributes, nodeId) => {
|
||||||
|
const category = sourceGetter(nodeId, attributes)
|
||||||
|
attributes.color = colorMap[category]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const min = valueScale[0]
|
||||||
|
const max = valueScale[valueScale.length - 1]
|
||||||
|
const normalizedColorscale = colorscale.map((color, index) => [
|
||||||
|
index / (colorscale.length - 1),
|
||||||
|
tinycolor(color).toRgb()
|
||||||
|
])
|
||||||
|
return (attributes, nodeId) => {
|
||||||
|
const value = sourceGetter(nodeId, attributes)
|
||||||
|
const normalizedValue = (value - min) / (max - min)
|
||||||
|
if (isNaN(normalizedValue)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const exactMatch = normalizedColorscale.find(
|
||||||
|
([value]) => value === normalizedValue
|
||||||
|
)
|
||||||
|
if (exactMatch) {
|
||||||
|
attributes.color = tinycolor(exactMatch[1]).toHexString()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const rightColorIndex = normalizedColorscale.findIndex(
|
||||||
|
([value]) => value >= normalizedValue
|
||||||
|
)
|
||||||
|
const leftColorIndex = (rightColorIndex || 1) - 1
|
||||||
|
const right = normalizedColorscale[rightColorIndex]
|
||||||
|
const left = normalizedColorscale[leftColorIndex]
|
||||||
|
const interpolationFactor =
|
||||||
|
(normalizedValue - left[0]) / (right[0] - left[0])
|
||||||
|
|
||||||
|
const r0 = left[1].r
|
||||||
|
const g0 = left[1].g
|
||||||
|
const b0 = left[1].b
|
||||||
|
const r1 = right[1].r
|
||||||
|
const g1 = right[1].g
|
||||||
|
const b1 = right[1].b
|
||||||
|
|
||||||
|
attributes.color = tinycolor({
|
||||||
|
r: r0 + interpolationFactor * (r1 - r0),
|
||||||
|
g: g0 + interpolationFactor * (g1 - g0),
|
||||||
|
b: b0 + interpolationFactor * (b1 - b0)
|
||||||
|
}).toHexString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNodeValueScale(graph, sourceGetter) {
|
||||||
|
const scaleSet = graph.reduceNodes((res, nodeId, attributes) => {
|
||||||
|
res.add(sourceGetter(nodeId, attributes))
|
||||||
|
return res
|
||||||
|
}, new Set())
|
||||||
|
return Array.from(scaleSet).sort((a, b) => a - b)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEdgeValueScale(graph, sourceGetter) {
|
||||||
|
const scaleSet = graph.reduceEdges((res, edgeId, attributes) => {
|
||||||
|
res.add(sourceGetter(edgeId, attributes))
|
||||||
|
return res
|
||||||
|
}, new Set())
|
||||||
|
return Array.from(scaleSet).sort((a, b) => a - b)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOptionsFromDataSources(dataSources) {
|
||||||
|
if (!dataSources) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(dataSources).map(name => ({
|
||||||
|
value: name,
|
||||||
|
label: name
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
const emptySources = {}
|
||||||
|
for (const key in dataSources) {
|
||||||
|
emptySources[key] = []
|
||||||
|
}
|
||||||
|
dereference.default(stateCopy.data, emptySources)
|
||||||
|
return stateCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getImageDataUrl(element, type) {
|
||||||
|
const chartElement = element.querySelector('.js-plotly-plot')
|
||||||
|
return await plotly.toImage(chartElement, {
|
||||||
|
format: type,
|
||||||
|
width: null,
|
||||||
|
height: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChartData(element) {
|
||||||
|
const chartElement = element.querySelector('.js-plotly-plot')
|
||||||
|
return {
|
||||||
|
data: chartElement.data,
|
||||||
|
layout: chartElement.layout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHtml(options) {
|
||||||
|
const chartId = nanoid()
|
||||||
|
return `
|
||||||
|
<script src="https://cdn.plot.ly/plotly-latest.js" charset="UTF-8"></script>
|
||||||
|
<div id="${chartId}"></div>
|
||||||
|
<script>
|
||||||
|
const el = document.getElementById("${chartId}")
|
||||||
|
|
||||||
|
let timeout
|
||||||
|
function debounceResize() {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
var r = el.getBoundingClientRect()
|
||||||
|
Plotly.relayout(el, {width: r.width, height: r.height})
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(debounceResize)
|
||||||
|
resizeObserver.observe(el)
|
||||||
|
|
||||||
|
Plotly.newPlot(el, ${JSON.stringify(options.data)}, ${JSON.stringify(options.layout)})
|
||||||
|
</script>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getOptionsFromDataSources,
|
||||||
|
getOptionsForSave,
|
||||||
|
getImageDataUrl,
|
||||||
|
getHtml,
|
||||||
|
getChartData
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import fu from '@/lib/utils/fileIo'
|
import fu from '@/lib/utils/fileIo'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
import migration from './_migrations'
|
import migration from './_migrations'
|
||||||
|
|
||||||
const migrate = migration._migrate
|
const migrate = migration._migrate
|
||||||
@@ -32,50 +33,24 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
isTabNeedName(inquiryTab) {
|
isTabNeedName(inquiryTab) {
|
||||||
const isFromScratch = !inquiryTab.initName
|
return inquiryTab.isPredefined || !inquiryTab.name
|
||||||
return inquiryTab.isPredefined || isFromScratch
|
|
||||||
},
|
|
||||||
|
|
||||||
save (inquiryTab, newName) {
|
|
||||||
const value = {
|
|
||||||
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
|
|
||||||
query: inquiryTab.query,
|
|
||||||
viewType: inquiryTab.$refs.dataView.mode,
|
|
||||||
viewOptions: inquiryTab.$refs.dataView.getOptionsForSave(),
|
|
||||||
name: newName || inquiryTab.initName
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get inquiries from local storage
|
|
||||||
const myInquiries = this.getStoredInquiries()
|
|
||||||
|
|
||||||
// Set createdAt
|
|
||||||
if (newName) {
|
|
||||||
value.createdAt = new Date()
|
|
||||||
} else {
|
|
||||||
var inquiryIndex = myInquiries.findIndex(oldInquiry => oldInquiry.id === inquiryTab.id)
|
|
||||||
value.createdAt = myInquiries[inquiryIndex].createdAt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert in inquiries list
|
|
||||||
if (newName) {
|
|
||||||
myInquiries.push(value)
|
|
||||||
} else {
|
|
||||||
myInquiries[inquiryIndex] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to local storage
|
|
||||||
this.updateStorage(myInquiries)
|
|
||||||
return value
|
|
||||||
},
|
},
|
||||||
|
|
||||||
updateStorage(inquiries) {
|
updateStorage(inquiries) {
|
||||||
localStorage.setItem('myInquiries', JSON.stringify({ version: this.version, inquiries }))
|
localStorage.setItem(
|
||||||
|
'myInquiries',
|
||||||
|
JSON.stringify({ version: this.version, inquiries })
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
serialiseInquiries(inquiryList) {
|
serialiseInquiries(inquiryList) {
|
||||||
const preparedData = JSON.parse(JSON.stringify(inquiryList))
|
const preparedData = JSON.parse(JSON.stringify(inquiryList))
|
||||||
preparedData.forEach(inquiry => delete inquiry.isPredefined)
|
preparedData.forEach(inquiry => delete inquiry.isPredefined)
|
||||||
return JSON.stringify({ version: this.version, inquiries: preparedData }, null, 4)
|
return JSON.stringify(
|
||||||
|
{ version: this.version, inquiries: preparedData },
|
||||||
|
null,
|
||||||
|
4
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
deserialiseInquiries(str) {
|
deserialiseInquiries(str) {
|
||||||
@@ -91,7 +66,9 @@ export default {
|
|||||||
|
|
||||||
// Generate new ids if they are the same as existing inquiries
|
// Generate new ids if they are the same as existing inquiries
|
||||||
inquiryList.forEach(inquiry => {
|
inquiryList.forEach(inquiry => {
|
||||||
const allInquiriesIds = this.getStoredInquiries().map(inquiry => inquiry.id)
|
const allInquiriesIds = this.getStoredInquiries().map(
|
||||||
|
inquiry => inquiry.id
|
||||||
|
)
|
||||||
if (allInquiriesIds.includes(inquiry.id)) {
|
if (allInquiriesIds.includes(inquiry.id)) {
|
||||||
inquiry.id = nanoid()
|
inquiry.id = nanoid()
|
||||||
}
|
}
|
||||||
@@ -101,11 +78,20 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
importInquiries() {
|
importInquiries() {
|
||||||
return fu.importFile()
|
return fu.importFile().then(str => {
|
||||||
.then(str => {
|
const inquires = this.deserialiseInquiries(str)
|
||||||
return this.deserialiseInquiries(str)
|
|
||||||
|
events.send('inquiry.import', inquires.length)
|
||||||
|
|
||||||
|
return inquires
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
export(inquiryList, fileName) {
|
||||||
|
const jsonStr = this.serialiseInquiries(inquiryList)
|
||||||
|
fu.exportToFile(jsonStr, fileName)
|
||||||
|
|
||||||
|
events.send('inquiry.export', inquiryList.length)
|
||||||
|
},
|
||||||
|
|
||||||
async readPredefinedInquiries() {
|
async readPredefinedInquiries() {
|
||||||
const res = await fu.readFile('./inquiries.json')
|
const res = await fu.readFile('./inquiries.json')
|
||||||
|
|||||||
62
src/lib/tab.js
Normal file
62
src/lib/tab.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
import time from '@/lib/utils/time'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
|
export default class Tab {
|
||||||
|
constructor(state, inquiry = {}) {
|
||||||
|
this.id = inquiry.id || nanoid()
|
||||||
|
this.name = inquiry.id ? inquiry.name : null
|
||||||
|
this.tempName =
|
||||||
|
inquiry.name ||
|
||||||
|
(state.untitledLastIndex
|
||||||
|
? `Untitled ${state.untitledLastIndex}`
|
||||||
|
: 'Untitled')
|
||||||
|
this.query = inquiry.query
|
||||||
|
this.viewOptions = inquiry.viewOptions || undefined
|
||||||
|
this.isPredefined = inquiry.isPredefined
|
||||||
|
this.viewType = inquiry.viewType || 'chart'
|
||||||
|
this.result = null
|
||||||
|
this.isGettingResults = false
|
||||||
|
this.error = null
|
||||||
|
this.time = 0
|
||||||
|
this.layout = inquiry.layout || {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
}
|
||||||
|
this.maximize = inquiry.maximize
|
||||||
|
|
||||||
|
this.isSaved = !!inquiry.id
|
||||||
|
this.state = state
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute() {
|
||||||
|
this.isGettingResults = true
|
||||||
|
this.result = null
|
||||||
|
this.error = null
|
||||||
|
const db = this.state.db
|
||||||
|
try {
|
||||||
|
const start = new Date()
|
||||||
|
this.result = await db.execute(this.query + ';')
|
||||||
|
this.time = time.getPeriod(start, new Date())
|
||||||
|
|
||||||
|
if (this.result && this.result.values) {
|
||||||
|
events.send(
|
||||||
|
'resultset.create',
|
||||||
|
this.result.values[this.result.columns[0]].length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
events.send('query.run', parseFloat(this.time), { status: 'success' })
|
||||||
|
} catch (err) {
|
||||||
|
this.error = {
|
||||||
|
type: 'error',
|
||||||
|
message: err
|
||||||
|
}
|
||||||
|
|
||||||
|
events.send('query.run', 0, { status: 'error' })
|
||||||
|
}
|
||||||
|
db.refreshSchema()
|
||||||
|
this.isGettingResults = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,11 @@ import Lib from 'plotly.js/src/lib'
|
|||||||
import dataUrlToBlob from 'dataurl-to-blob'
|
import dataUrlToBlob from 'dataurl-to-blob'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async copyCsv (str) {
|
async copyText(str, notifyMessage) {
|
||||||
await navigator.clipboard.writeText(str)
|
await navigator.clipboard.writeText(str)
|
||||||
Lib.notifier('CSV copied to clipboard successfully', 'long')
|
if (notifyMessage) {
|
||||||
|
Lib.notifier(notifyMessage, 'long')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async copyImage(source) {
|
async copyImage(source) {
|
||||||
@@ -17,7 +19,8 @@ export default {
|
|||||||
|
|
||||||
async _copyBlob(blob) {
|
async _copyBlob(blob) {
|
||||||
await navigator.clipboard.write([
|
await navigator.clipboard.write([
|
||||||
new ClipboardItem({ // eslint-disable-line no-undef
|
new ClipboardItem({
|
||||||
|
// eslint-disable-line no-undef
|
||||||
[blob.type]: blob
|
[blob.type]: blob
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
@@ -30,9 +33,13 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async _copyCanvas(canvas) {
|
async _copyCanvas(canvas) {
|
||||||
canvas.toBlob(async (blob) => {
|
canvas.toBlob(
|
||||||
|
async blob => {
|
||||||
await this._copyBlob(blob)
|
await this._copyBlob(blob)
|
||||||
Lib.notifier('Image copied to clipboard successfully', 'long')
|
Lib.notifier('Image copied to clipboard successfully', 'long')
|
||||||
}, 'image/png', 1)
|
},
|
||||||
|
'image/png',
|
||||||
|
1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/lib/utils/events.js
Normal file
12
src/lib/utils/events.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export default {
|
||||||
|
send(name, value, labels) {
|
||||||
|
const event = new CustomEvent('sqliteviz-app-event', {
|
||||||
|
detail: {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
labels: labels || {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
export default {
|
export default {
|
||||||
|
isJSON(file) {
|
||||||
|
return file && file.type === 'application/json'
|
||||||
|
},
|
||||||
|
isNDJSON(file) {
|
||||||
|
return file && file.name.endsWith('.ndjson')
|
||||||
|
},
|
||||||
isDatabase(file) {
|
isDatabase(file) {
|
||||||
const dbTypes = ['application/vnd.sqlite3', 'application/x-sqlite3']
|
const dbTypes = ['application/vnd.sqlite3', 'application/x-sqlite3']
|
||||||
return file.type
|
return file.type
|
||||||
@@ -51,16 +57,16 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
importFile() {
|
importFile() {
|
||||||
const reader = new FileReader()
|
return this.getFileFromUser('.json').then(file => {
|
||||||
|
return this.getFileContent(file)
|
||||||
return this.getFileFromUser('.json')
|
|
||||||
.then(file => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
reader.onload = e => {
|
|
||||||
resolve(e.target.result)
|
|
||||||
}
|
|
||||||
reader.readAsText(file)
|
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getFileContent(file) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
return new Promise(resolve => {
|
||||||
|
reader.onload = e => resolve(e.target.result)
|
||||||
|
reader.readAsText(file)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user