mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-07 02:28:54 +08:00
Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b59c21c14e | ||
|
|
4ed4b54a28 | ||
|
|
2c2bb7d6d3 | ||
|
|
efbd985b36 | ||
|
|
9cf7d0e5dc | ||
|
|
0a8c09b58d | ||
|
|
931cf380bc | ||
|
|
f0f96ac663 | ||
|
|
45530cc9d6 | ||
|
|
6fbf75b601 | ||
|
|
d3fbf08569 | ||
|
|
be6a19a30f | ||
|
|
07d7a9d54b | ||
|
|
cdd925b8af | ||
|
|
12fa0749b1 | ||
|
|
75bf849823 | ||
|
|
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 |
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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
12
.github/workflows/main.yml
vendored
12
.github/workflows/main.yml
vendored
@@ -14,10 +14,10 @@ jobs:
|
|||||||
- 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: |
|
||||||
@@ -27,19 +27,19 @@ jobs:
|
|||||||
- name: Create archives
|
- 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 .
|
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.zip,dist_map.zip"
|
artifacts: 'dist.zip,dist_map.zip'
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
bodyFile: "CHANGELOG.md"
|
bodyFile: 'CHANGELOG.md'
|
||||||
|
|||||||
9
.github/workflows/test.yml
vendored
9
.github/workflows/test.yml
vendored
@@ -11,21 +11,22 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: Run tests
|
name: Run tests
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-24.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
|
||||||
|
sudo add-apt-repository -y ppa:mozillateam/ppa
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y chromium-browser firefox
|
sudo apt-get install -y chromium-browser firefox-esr
|
||||||
|
|
||||||
- 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"
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
# docker build -t sqliteviz/test -f Dockerfile.test .
|
# docker build -t sqliteviz/test -f Dockerfile.test .
|
||||||
#
|
#
|
||||||
|
|
||||||
FROM node:12
|
FROM node:12.22-bullseye
|
||||||
|
|
||||||
RUN set -ex; \
|
RUN set -ex; \
|
||||||
apt update; \
|
apt update; \
|
||||||
|
|||||||
10
README.md
10
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 [sqliteviz.com/app][6].
|
The latest release of sqliteviz is deployed on [sqliteviz.com/app][6].
|
||||||
|
|
||||||
## Wiki
|
## Wiki
|
||||||
|
|
||||||
For user documentation, check out sqliteviz [documentation][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
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -74,29 +77,22 @@
|
|||||||
</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>
|
||||||
<!-- extention slot start -->
|
<!-- extention slot start -->
|
||||||
<!-- extention slot end -->
|
<!-- extention slot end -->
|
||||||
<!-- built files will be auto injected -->
|
<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)
|
|
||||||
@@ -45,6 +45,8 @@ 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]
|
2. `pearson` correlation coefficient function extension from [sqlean][21]
|
||||||
(which is part of [squib][20])
|
(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.
|
||||||
@@ -103,3 +105,4 @@ described in [this message from SQLite Forum][12]:
|
|||||||
[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
|
[20]: https://github.com/mrwilson/squib/blob/master/pearson.c
|
||||||
[21]: https://github.com/nalgeon/sqlean/blob/incubator/src/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:14-bullseye
|
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,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' }
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,19 @@
|
|||||||
],
|
],
|
||||||
"metadata": {}
|
"metadata": {}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"!du -b lib | head -n 2"
|
||||||
|
],
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"collapsed": false,
|
||||||
|
"outputHidden": false,
|
||||||
|
"inputHidden": true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"source": [
|
"source": [
|
||||||
@@ -176,7 +189,7 @@
|
|||||||
},
|
},
|
||||||
"language_info": {
|
"language_info": {
|
||||||
"name": "python",
|
"name": "python",
|
||||||
"version": "3.10.7",
|
"version": "3.10.14",
|
||||||
"mimetype": "text/x-python",
|
"mimetype": "text/x-python",
|
||||||
"codemirror_mode": {
|
"codemirror_mode": {
|
||||||
"name": "ipython",
|
"name": "ipython",
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ cflags = (
|
|||||||
# Compile-time optimisation
|
# Compile-time optimisation
|
||||||
'-Os', # reduces the code size about in half comparing to -O2
|
'-Os', # reduces the code size about in half comparing to -O2
|
||||||
'-flto',
|
'-flto',
|
||||||
|
'-Isrc', '-Isrc/lua',
|
||||||
)
|
)
|
||||||
emflags = (
|
emflags = (
|
||||||
# Base
|
# Base
|
||||||
@@ -61,6 +62,15 @@ def build(src: Path, dst: Path):
|
|||||||
'-c', src / 'extension-functions.c',
|
'-c', src / 'extension-functions.c',
|
||||||
'-o', out / 'extension-functions.o',
|
'-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([
|
||||||
@@ -68,6 +78,7 @@ def build(src: Path, dst: Path):
|
|||||||
*emflags,
|
*emflags,
|
||||||
out / 'sqlite3.o',
|
out / 'sqlite3.o',
|
||||||
out / 'extension-functions.o',
|
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/2023/sqlite-amalgamation-3410000.zip'
|
amalgamation_url = 'https://sqlite.org/2025/sqlite-amalgamation-3500300.zip'
|
||||||
|
|
||||||
# Extension-functions
|
# Extension-functions
|
||||||
# ===================
|
# ===================
|
||||||
@@ -20,18 +22,24 @@ contrib_functions_url = 'https://sqlite.org/contrib/download/extension-functions
|
|||||||
extension_urls = (
|
extension_urls = (
|
||||||
# Miscellaneous extensions
|
# Miscellaneous extensions
|
||||||
# ========================
|
# ========================
|
||||||
('https://sqlite.org/src/raw/8d79354f?at=series.c', 'sqlite3_series_init'),
|
('https://sqlite.org/src/raw/e212edb2?at=series.c', 'sqlite3_series_init'),
|
||||||
('https://sqlite.org/src/raw/dbfd8543?at=closure.c', 'sqlite3_closure_init'),
|
('https://sqlite.org/src/raw/5559daf1?at=closure.c', 'sqlite3_closure_init'),
|
||||||
('https://sqlite.org/src/raw/5bb2264c?at=uuid.c', 'sqlite3_uuid_init'),
|
('https://sqlite.org/src/raw/5bb2264c?at=uuid.c', 'sqlite3_uuid_init'),
|
||||||
('https://sqlite.org/src/raw/5853b0e5?at=regexp.c', 'sqlite3_regexp_init'),
|
('https://sqlite.org/src/raw/388e7f23?at=regexp.c', 'sqlite3_regexp_init'),
|
||||||
('https://sqlite.org/src/raw/b9086e22?at=percentile.c', 'sqlite3_percentile_init'),
|
('https://sqlite.org/src/raw/72e05a21?at=percentile.c', 'sqlite3_percentile_init'),
|
||||||
('https://sqlite.org/src/raw/09f967dc?at=decimal.c', 'sqlite3_decimal_init'),
|
('https://sqlite.org/src/raw/228d47e9?at=decimal.c', 'sqlite3_decimal_init'),
|
||||||
# Third-party extension
|
# Third-party extension
|
||||||
# =====================
|
# =====================
|
||||||
('https://github.com/jakethaw/pivot_vtab/raw/9323ef93/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
|
('https://github.com/jakethaw/pivot_vtab/raw/e7705f34/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
|
||||||
('https://github.com/nalgeon/sqlean/raw/95e8d21a/src/pearson.c', 'sqlite3_pearson_init'),
|
('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'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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'
|
sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.7.0.zip'
|
||||||
|
|
||||||
|
|
||||||
@@ -59,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')
|
||||||
|
|
||||||
@@ -70,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)
|
||||||
|
|
||||||
@@ -90,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.
46026
package-lock.json
generated
46026
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
90
package.json
90
package.json
@@ -1,16 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "sqliteviz",
|
"name": "sqliteviz",
|
||||||
"version": "0.23.1",
|
"version": "0.27.1",
|
||||||
"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",
|
||||||
"html2canvas": "^1.1.4",
|
"html2canvas": "^1.1.4",
|
||||||
@@ -18,46 +23,57 @@
|
|||||||
"nanoid": "^3.1.12",
|
"nanoid": "^3.1.12",
|
||||||
"papaparse": "^5.4.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",
|
||||||
"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",
|
||||||
"flush-promises": "^1.0.2",
|
"flush-promises": "^1.0.2",
|
||||||
"karma": "^3.1.4",
|
"karma": "^6.4.4",
|
||||||
"karma-firefox-launcher": "^2.1.0",
|
"karma-coverage": "^2.2.1",
|
||||||
"karma-webpack": "^4.0.2",
|
"karma-coverage-istanbul-reporter": "^3.0.3",
|
||||||
"vue-cli-plugin-ui-karma": "^0.2.5",
|
"karma-firefox-launcher": "^2.1.3",
|
||||||
"vue-template-compiler": "^2.6.11",
|
"karma-mocha": "^1.3.0",
|
||||||
"workbox-webpack-plugin": "^6.1.5",
|
"karma-spec-reporter": "^0.0.36",
|
||||||
"worker-loader": "^3.0.8"
|
"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": [
|
||||||
{
|
{
|
||||||
|
|||||||
55
src/App.vue
55
src/App.vue
@@ -4,55 +4,84 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import storedInquiries from '@/lib/storedInquiries'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
inquiries() {
|
||||||
|
return this.$store.state.inquiries
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
inquiries: {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
storedInquiries.updateStorage(this.inquiries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
|
||||||
|
addEventListener('storage', event => {
|
||||||
|
if (event.key === storedInquiries.myInquiriesKey) {
|
||||||
|
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,387 +0,0 @@
|
|||||||
<template>
|
|
||||||
<modal
|
|
||||||
:name="dialogName"
|
|
||||||
classes="dialog"
|
|
||||||
height="auto"
|
|
||||||
width="80%"
|
|
||||||
scrollable
|
|
||||||
:clickToClose="false"
|
|
||||||
>
|
|
||||||
<div class="dialog-header">
|
|
||||||
CSV import
|
|
||||||
<close-icon @click="cancelCsvImport" :disabled="disableDialog"/>
|
|
||||||
</div>
|
|
||||||
<div class="dialog-body">
|
|
||||||
<text-field
|
|
||||||
label="Table name"
|
|
||||||
v-model="tableName"
|
|
||||||
width="484px"
|
|
||||||
:disabled="disableDialog"
|
|
||||||
:error-msg="tableNameError"
|
|
||||||
id="csv-table-name"
|
|
||||||
/>
|
|
||||||
<div class="chars">
|
|
||||||
<delimiter-selector
|
|
||||||
v-model="delimiter"
|
|
||||||
width="210px"
|
|
||||||
class="char-input"
|
|
||||||
@input="previewCsv"
|
|
||||||
:disabled="disableDialog"
|
|
||||||
/>
|
|
||||||
<text-field
|
|
||||||
label="Quote char"
|
|
||||||
hint="The character used to quote fields."
|
|
||||||
v-model="quoteChar"
|
|
||||||
width="93px"
|
|
||||||
:disabled="disableDialog"
|
|
||||||
class="char-input"
|
|
||||||
id="quote-char"
|
|
||||||
/>
|
|
||||||
<text-field
|
|
||||||
label="Escape char"
|
|
||||||
hint='
|
|
||||||
The character used to escape the quote character within a field
|
|
||||||
(e.g. "column with ""quotes"" in text").
|
|
||||||
'
|
|
||||||
max-hint-width="242px"
|
|
||||||
v-model="escapeChar"
|
|
||||||
width="93px"
|
|
||||||
:disabled="disableDialog"
|
|
||||||
class="char-input"
|
|
||||||
id="escape-char"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<check-box
|
|
||||||
@click="header = $event"
|
|
||||||
:init="true"
|
|
||||||
label="Use first row as column headers"
|
|
||||||
:disabled="disableDialog"
|
|
||||||
/>
|
|
||||||
<sql-table
|
|
||||||
v-if="previewData
|
|
||||||
&& (previewData.rowCount > 0 || Object.keys(previewData).length > 0)
|
|
||||||
"
|
|
||||||
:data-set="previewData"
|
|
||||||
class="preview-table"
|
|
||||||
:preview="true"
|
|
||||||
/>
|
|
||||||
<div v-else class="no-data">No data</div>
|
|
||||||
<logs
|
|
||||||
class="import-csv-errors"
|
|
||||||
:messages="importCsvMessages"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="dialog-buttons-container">
|
|
||||||
<button
|
|
||||||
class="secondary"
|
|
||||||
:disabled="disableDialog"
|
|
||||||
@click="cancelCsvImport"
|
|
||||||
id="csv-cancel"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-show="!importCsvCompleted"
|
|
||||||
class="primary"
|
|
||||||
:disabled="disableDialog"
|
|
||||||
@click="loadFromCsv(file)"
|
|
||||||
id="csv-import"
|
|
||||||
>
|
|
||||||
Import
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-show="importCsvCompleted"
|
|
||||||
class="primary"
|
|
||||||
:disabled="disableDialog"
|
|
||||||
@click="finish"
|
|
||||||
id="csv-finish"
|
|
||||||
>
|
|
||||||
Finish
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import csv from '@/lib/csv'
|
|
||||||
import CloseIcon from '@/components/svg/close'
|
|
||||||
import TextField from '@/components/TextField'
|
|
||||||
import DelimiterSelector from './DelimiterSelector'
|
|
||||||
import CheckBox from '@/components/CheckBox'
|
|
||||||
import SqlTable from '@/components/SqlTable'
|
|
||||||
import Logs from '@/components/Logs'
|
|
||||||
import time from '@/lib/utils/time'
|
|
||||||
import fIo from '@/lib/utils/fileIo'
|
|
||||||
import events from '@/lib/utils/events'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'CsvImport',
|
|
||||||
components: {
|
|
||||||
CloseIcon,
|
|
||||||
TextField,
|
|
||||||
DelimiterSelector,
|
|
||||||
CheckBox,
|
|
||||||
SqlTable,
|
|
||||||
Logs
|
|
||||||
},
|
|
||||||
props: ['file', 'db', 'dialogName'],
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
disableDialog: false,
|
|
||||||
tableName: '',
|
|
||||||
delimiter: '',
|
|
||||||
quoteChar: '"',
|
|
||||||
escapeChar: '"',
|
|
||||||
header: true,
|
|
||||||
importCsvCompleted: false,
|
|
||||||
importCsvMessages: [],
|
|
||||||
previewData: null,
|
|
||||||
addedTable: null,
|
|
||||||
tableNameError: ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
quoteChar () {
|
|
||||||
this.previewCsv()
|
|
||||||
},
|
|
||||||
|
|
||||||
escapeChar () {
|
|
||||||
this.previewCsv()
|
|
||||||
},
|
|
||||||
|
|
||||||
header () {
|
|
||||||
this.previewCsv()
|
|
||||||
},
|
|
||||||
tableName: time.debounce(function () {
|
|
||||||
this.tableNameError = ''
|
|
||||||
if (!this.tableName) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.db.validateTableName(this.tableName)
|
|
||||||
.catch(err => {
|
|
||||||
this.tableNameError = err.message + '. Try another table name.'
|
|
||||||
})
|
|
||||||
}, 400)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
cancelCsvImport () {
|
|
||||||
if (!this.disableDialog) {
|
|
||||||
if (this.addedTable) {
|
|
||||||
this.db.execute(`DROP TABLE "${this.addedTable}"`)
|
|
||||||
this.db.refreshSchema()
|
|
||||||
}
|
|
||||||
this.$modal.hide(this.dialogName)
|
|
||||||
this.$emit('cancel')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
reset () {
|
|
||||||
this.header = true
|
|
||||||
this.quoteChar = '"'
|
|
||||||
this.escapeChar = '"'
|
|
||||||
this.delimiter = ''
|
|
||||||
this.tableName = ''
|
|
||||||
this.disableDialog = false
|
|
||||||
this.importCsvCompleted = false
|
|
||||||
this.importCsvMessages = []
|
|
||||||
this.previewData = null
|
|
||||||
this.addedTable = null
|
|
||||||
this.tableNameError = ''
|
|
||||||
},
|
|
||||||
open () {
|
|
||||||
this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file))
|
|
||||||
this.$modal.show(this.dialogName)
|
|
||||||
},
|
|
||||||
async previewCsv () {
|
|
||||||
this.importCsvCompleted = false
|
|
||||||
const config = {
|
|
||||||
preview: 3,
|
|
||||||
quoteChar: this.quoteChar || '"',
|
|
||||||
escapeChar: this.escapeChar,
|
|
||||||
header: this.header,
|
|
||||||
delimiter: this.delimiter
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const start = new Date()
|
|
||||||
const parseResult = await csv.parse(this.file, config)
|
|
||||||
const end = new Date()
|
|
||||||
this.previewData = parseResult.data
|
|
||||||
this.delimiter = parseResult.delimiter
|
|
||||||
|
|
||||||
// In parseResult.messages we can get parse errors
|
|
||||||
this.importCsvMessages = parseResult.messages || []
|
|
||||||
|
|
||||||
if (!parseResult.hasErrors) {
|
|
||||||
this.importCsvMessages.push({
|
|
||||||
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
|
|
||||||
type: 'success'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.importCsvMessages = [{
|
|
||||||
message: err,
|
|
||||||
type: 'error'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async loadFromCsv (file) {
|
|
||||||
if (!this.tableName) {
|
|
||||||
this.tableNameError = "Table name can't be empty"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.disableDialog = true
|
|
||||||
const config = {
|
|
||||||
quoteChar: this.quoteChar || '"',
|
|
||||||
escapeChar: this.escapeChar,
|
|
||||||
header: this.header,
|
|
||||||
delimiter: this.delimiter
|
|
||||||
}
|
|
||||||
const parseCsvMsg = {
|
|
||||||
message: 'Parsing CSV...',
|
|
||||||
type: 'info'
|
|
||||||
}
|
|
||||||
this.importCsvMessages.push(parseCsvMsg)
|
|
||||||
const parseCsvLoadingIndicator = setTimeout(() => { parseCsvMsg.type = 'loading' }, 1000)
|
|
||||||
|
|
||||||
const importMsg = {
|
|
||||||
message: 'Importing CSV into a SQLite database...',
|
|
||||||
type: 'info'
|
|
||||||
}
|
|
||||||
let importLoadingIndicator = null
|
|
||||||
|
|
||||||
const updateProgress = progress => {
|
|
||||||
this.$set(importMsg, 'progress', progress)
|
|
||||||
}
|
|
||||||
const progressCounterId = this.db.createProgressCounter(updateProgress)
|
|
||||||
|
|
||||||
try {
|
|
||||||
let start = new Date()
|
|
||||||
const parseResult = await csv.parse(this.file, config)
|
|
||||||
let end = new Date()
|
|
||||||
|
|
||||||
if (!parseResult.hasErrors) {
|
|
||||||
const rowCount = parseResult.rowCount
|
|
||||||
let period = time.getPeriod(start, end)
|
|
||||||
parseCsvMsg.type = 'success'
|
|
||||||
|
|
||||||
if (parseResult.messages.length > 0) {
|
|
||||||
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
|
||||||
parseCsvMsg.message = `${rowCount} rows are parsed in ${period}.`
|
|
||||||
} else {
|
|
||||||
// Inform about csv parsing success
|
|
||||||
parseCsvMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loading indicator for csv parsing is not needed anymore
|
|
||||||
clearTimeout(parseCsvLoadingIndicator)
|
|
||||||
|
|
||||||
// Add info about import start
|
|
||||||
this.importCsvMessages.push(importMsg)
|
|
||||||
|
|
||||||
// Show import progress after 1 second
|
|
||||||
importLoadingIndicator = setTimeout(() => {
|
|
||||||
importMsg.type = 'loading'
|
|
||||||
}, 1000)
|
|
||||||
|
|
||||||
// Add table
|
|
||||||
start = new Date()
|
|
||||||
await this.db.addTableFromCsv(this.tableName, parseResult.data, progressCounterId)
|
|
||||||
end = new Date()
|
|
||||||
|
|
||||||
this.addedTable = this.tableName
|
|
||||||
// Inform about import success
|
|
||||||
period = time.getPeriod(start, end)
|
|
||||||
importMsg.message = `Importing CSV into a SQLite database is completed in ${period}.`
|
|
||||||
importMsg.type = 'success'
|
|
||||||
|
|
||||||
// Loading indicator for import is not needed anymore
|
|
||||||
clearTimeout(importLoadingIndicator)
|
|
||||||
|
|
||||||
this.importCsvCompleted = true
|
|
||||||
} else {
|
|
||||||
parseCsvMsg.message = 'Parsing ended with errors.'
|
|
||||||
parseCsvMsg.type = 'info'
|
|
||||||
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (parseCsvMsg.type === 'loading') {
|
|
||||||
parseCsvMsg.type = 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (importMsg.type === 'loading') {
|
|
||||||
importMsg.type = 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
this.importCsvMessages.push({
|
|
||||||
message: err,
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(parseCsvLoadingIndicator)
|
|
||||||
clearTimeout(importLoadingIndicator)
|
|
||||||
this.db.deleteProgressCounter(progressCounterId)
|
|
||||||
this.disableDialog = false
|
|
||||||
},
|
|
||||||
async finish () {
|
|
||||||
this.$modal.hide(this.dialogName)
|
|
||||||
const stmt = [
|
|
||||||
'/*',
|
|
||||||
` * Your CSV file has been imported into ${this.addedTable} table.`,
|
|
||||||
' * You can run this SQL query to make all CSV records available for charting.',
|
|
||||||
' */',
|
|
||||||
`SELECT * FROM "${this.addedTable}"`
|
|
||||||
].join('\n')
|
|
||||||
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
|
||||||
this.$store.commit('setCurrentTabId', tabId)
|
|
||||||
this.importCsvCompleted = false
|
|
||||||
this.$emit('finish')
|
|
||||||
events.send('inquiry.create', null, { auto: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.dialog-body {
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chars {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
margin: 24px 0 20px;
|
|
||||||
}
|
|
||||||
.char-input {
|
|
||||||
margin-right: 44px;
|
|
||||||
}
|
|
||||||
.preview-table {
|
|
||||||
margin-top: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.import-csv-errors {
|
|
||||||
height: 136px;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
.no-data {
|
|
||||||
margin-top: 32px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 5px;
|
|
||||||
position: relative;
|
|
||||||
border: 1px solid var(--color-border-light);
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: 147px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--color-text-base);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* https://github.com/euvl/vue-js-modal/issues/623 */
|
|
||||||
>>> .vm--modal {
|
|
||||||
max-width: 1152px;
|
|
||||||
margin: auto;
|
|
||||||
left: 0 !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -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,17 +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'
|
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)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -77,10 +82,7 @@ export default {
|
|||||||
default: 'unset'
|
default: 'unset'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
emits: [],
|
||||||
ChangeDbIcon,
|
|
||||||
CsvImport
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
state: '',
|
state: '',
|
||||||
@@ -91,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'
|
||||||
@@ -102,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
|
||||||
@@ -117,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) {
|
||||||
@@ -128,21 +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, {
|
events.send('database.import', file.size, {
|
||||||
from: 'csv',
|
from: isJson ? 'json' : 'csv',
|
||||||
new_db: true
|
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)
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -243,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 {
|
||||||
|
|||||||
@@ -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,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<modal
|
<modal
|
||||||
:name="name"
|
v-model="show"
|
||||||
classes="dialog"
|
class="dialog"
|
||||||
height="auto"
|
|
||||||
:clickToClose="false"
|
:clickToClose="false"
|
||||||
|
:contentTransition="{ name: 'loading-dialog' }"
|
||||||
|
:overlayTransition="{ name: 'loading-dialog' }"
|
||||||
|
@update:model-value="$emit('update:modelValue', $event)"
|
||||||
>
|
>
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
<close-icon @click="$emit('cancel')" :disabled="loading"/>
|
<close-icon :disabled="loading" @click="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,20 +17,25 @@
|
|||||||
{{ 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="cancel"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="primary"
|
class="primary"
|
||||||
|
type="button"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@click="$emit('action')"
|
@click="$emit('action')"
|
||||||
>
|
>
|
||||||
@@ -43,31 +50,66 @@ 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: {
|
||||||
|
modelValue: Boolean,
|
||||||
loadingMsg: String,
|
loadingMsg: String,
|
||||||
successMsg: String,
|
successMsg: String,
|
||||||
actionBtnName: String,
|
actionBtnName: String,
|
||||||
name: String,
|
|
||||||
title: String,
|
title: String,
|
||||||
loading: Boolean
|
loading: Boolean
|
||||||
},
|
},
|
||||||
|
emits: ['cancel', 'action', 'update:modelValue'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show: this.modelValue
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
modelValue() {
|
||||||
|
this.show = this.modelValue
|
||||||
|
},
|
||||||
loading() {
|
loading() {
|
||||||
if (this.loading) {
|
if (this.loading) {
|
||||||
this.$modal.show(this.name)
|
this.$emit('update:modelValue', true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: { LoadingIndicator, CloseIcon },
|
|
||||||
methods: {
|
methods: {
|
||||||
cancel() {
|
cancel() {
|
||||||
this.$emit('cancel')
|
this.$emit('cancel')
|
||||||
|
this.$emit('update:modelValue', false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</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>
|
||||||
@@ -86,12 +89,16 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
emits: [],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
container: null,
|
container: null,
|
||||||
paneBefore: this.before,
|
paneBefore: this.before,
|
||||||
paneAfter: this.after,
|
paneAfter: this.after,
|
||||||
beforeMinimising: !this.after.size || !this.before.size ? this.default : {
|
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
|
||||||
},
|
},
|
||||||
@@ -106,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() {
|
||||||
@@ -147,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)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -215,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>
|
||||||
@@ -236,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%;
|
||||||
@@ -285,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,
|
||||||
@@ -336,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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
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,4 +1,4 @@
|
|||||||
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'
|
import { nanoid } from 'nanoid'
|
||||||
|
|
||||||
@@ -21,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -76,7 +88,7 @@ export default {
|
|||||||
let res
|
let res
|
||||||
try {
|
try {
|
||||||
res = {
|
res = {
|
||||||
data: this.getResult(results),
|
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
|
||||||
|
|||||||
@@ -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,7 +1,4 @@
|
|||||||
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
|
||||||
@@ -10,7 +7,9 @@ import PromiseWorker from 'promise-worker'
|
|||||||
import events from '@/lib/utils/events'
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,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
|
||||||
}))
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -45,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +73,10 @@ 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)
|
||||||
@@ -130,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)
|
||||||
|
}
|
||||||
@@ -4,11 +4,13 @@ import events from '@/lib/utils/events'
|
|||||||
import migration from './_migrations'
|
import migration from './_migrations'
|
||||||
|
|
||||||
const migrate = migration._migrate
|
const migrate = migration._migrate
|
||||||
|
const myInquiriesKey = 'myInquiries'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
version: 2,
|
version: 2,
|
||||||
|
myInquiriesKey,
|
||||||
getStoredInquiries() {
|
getStoredInquiries() {
|
||||||
let myInquiries = JSON.parse(localStorage.getItem('myInquiries'))
|
let myInquiries = JSON.parse(localStorage.getItem(myInquiriesKey))
|
||||||
if (!myInquiries) {
|
if (!myInquiries) {
|
||||||
const oldInquiries = localStorage.getItem('myQueries')
|
const oldInquiries = localStorage.getItem('myQueries')
|
||||||
if (oldInquiries) {
|
if (oldInquiries) {
|
||||||
@@ -26,7 +28,8 @@ export default {
|
|||||||
const newInquiry = JSON.parse(JSON.stringify(baseInquiry))
|
const newInquiry = JSON.parse(JSON.stringify(baseInquiry))
|
||||||
newInquiry.name = newInquiry.name + ' Copy'
|
newInquiry.name = newInquiry.name + ' Copy'
|
||||||
newInquiry.id = nanoid()
|
newInquiry.id = nanoid()
|
||||||
newInquiry.createdAt = new Date()
|
newInquiry.createdAt = new Date().toJSON()
|
||||||
|
newInquiry.updatedAt = new Date().toJSON()
|
||||||
delete newInquiry.isPredefined
|
delete newInquiry.isPredefined
|
||||||
|
|
||||||
return newInquiry
|
return newInquiry
|
||||||
@@ -36,46 +39,21 @@ export default {
|
|||||||
return inquiryTab.isPredefined || !inquiryTab.name
|
return inquiryTab.isPredefined || !inquiryTab.name
|
||||||
},
|
},
|
||||||
|
|
||||||
save (inquiryTab, newName) {
|
|
||||||
const value = {
|
|
||||||
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
|
|
||||||
query: inquiryTab.query,
|
|
||||||
viewType: inquiryTab.dataView.mode,
|
|
||||||
viewOptions: inquiryTab.dataView.getOptionsForSave(),
|
|
||||||
name: newName || inquiryTab.name
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(
|
||||||
|
myInquiriesKey,
|
||||||
|
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 +69,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,8 +81,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
importInquiries() {
|
importInquiries() {
|
||||||
return fu.importFile()
|
return fu.importFile().then(str => {
|
||||||
.then(str => {
|
|
||||||
const inquires = this.deserialiseInquiries(str)
|
const inquires = this.deserialiseInquiries(str)
|
||||||
|
|
||||||
events.send('inquiry.import', inquires.length)
|
events.send('inquiry.import', inquires.length)
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ export default class Tab {
|
|||||||
constructor(state, inquiry = {}) {
|
constructor(state, inquiry = {}) {
|
||||||
this.id = inquiry.id || nanoid()
|
this.id = inquiry.id || nanoid()
|
||||||
this.name = inquiry.id ? inquiry.name : null
|
this.name = inquiry.id ? inquiry.name : null
|
||||||
this.tempName = inquiry.name || (state.untitledLastIndex
|
this.tempName =
|
||||||
|
inquiry.name ||
|
||||||
|
(state.untitledLastIndex
|
||||||
? `Untitled ${state.untitledLastIndex}`
|
? `Untitled ${state.untitledLastIndex}`
|
||||||
: 'Untitled')
|
: 'Untitled')
|
||||||
this.query = inquiry.query
|
this.query = inquiry.query
|
||||||
@@ -26,6 +28,7 @@ export default class Tab {
|
|||||||
|
|
||||||
this.isSaved = !!inquiry.id
|
this.isSaved = !!inquiry.id
|
||||||
this.state = state
|
this.state = state
|
||||||
|
this.updatedAt = inquiry.updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute() {
|
async execute() {
|
||||||
@@ -39,7 +42,8 @@ export default class Tab {
|
|||||||
this.time = time.getPeriod(start, new Date())
|
this.time = time.getPeriod(start, new Date())
|
||||||
|
|
||||||
if (this.result && this.result.values) {
|
if (this.result && this.result.values) {
|
||||||
events.send('resultset.create',
|
events.send(
|
||||||
|
'resultset.create',
|
||||||
this.result.values[this.result.columns[0]].length
|
this.result.values[this.result.columns[0]].length
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ export default {
|
|||||||
|
|
||||||
sleep(ms) {
|
sleep(ms) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
setTimeout(() => { resolve() }, ms)
|
setTimeout(() => {
|
||||||
|
resolve()
|
||||||
|
}, ms)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
src/main.js
28
src/main.js
@@ -1,9 +1,8 @@
|
|||||||
import Vue from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from '@/App.vue'
|
import App from '@/App.vue'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
import { VuePlugin } from 'vuera'
|
import { createVfm, VueFinalModal, useVfm } from 'vue-final-modal'
|
||||||
import VModal from 'vue-js-modal'
|
|
||||||
|
|
||||||
import '@/assets/styles/variables.css'
|
import '@/assets/styles/variables.css'
|
||||||
import '@/assets/styles/buttons.css'
|
import '@/assets/styles/buttons.css'
|
||||||
@@ -11,20 +10,23 @@ import '@/assets/styles/tables.css'
|
|||||||
import '@/assets/styles/dialogs.css'
|
import '@/assets/styles/dialogs.css'
|
||||||
import '@/assets/styles/tooltips.css'
|
import '@/assets/styles/tooltips.css'
|
||||||
import '@/assets/styles/messages.css'
|
import '@/assets/styles/messages.css'
|
||||||
import 'vue-multiselect/dist/vue-multiselect.min.css'
|
import 'vue-multiselect/dist/vue-multiselect.css'
|
||||||
import '@/assets/styles/multiselect.css'
|
import '@/assets/styles/multiselect.css'
|
||||||
|
import 'vue-final-modal/style.css'
|
||||||
|
|
||||||
if (!['localhost', '127.0.0.1'].includes(location.hostname)) {
|
if (!['localhost', '127.0.0.1'].includes(location.hostname)) {
|
||||||
import('./registerServiceWorker') // eslint-disable-line no-unused-expressions
|
import('./registerServiceWorker') // eslint-disable-line no-unused-expressions
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.use(VuePlugin)
|
const app = createApp(App)
|
||||||
Vue.use(VModal)
|
.use(router)
|
||||||
|
.use(store)
|
||||||
|
.use(createVfm())
|
||||||
|
.component('modal', VueFinalModal)
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
const vfm = useVfm()
|
||||||
|
app.config.globalProperties.$modal = {
|
||||||
new Vue({
|
show: modalId => vfm.open(modalId),
|
||||||
router,
|
hide: modalId => vfm.close(modalId)
|
||||||
store,
|
}
|
||||||
render: h => h(App)
|
app.mount('#app')
|
||||||
}).$mount('#app')
|
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ function invokeServiceWorkerUpdateFlow (registration) {
|
|||||||
|
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
window.addEventListener('load', async () => {
|
window.addEventListener('load', async () => {
|
||||||
const registration = await navigator.serviceWorker.register('service-worker.js')
|
const registration =
|
||||||
|
await navigator.serviceWorker.register('service-worker.js')
|
||||||
// ensure the case when the updatefound event was missed is also handled
|
// ensure the case when the updatefound event was missed is also handled
|
||||||
// by re-invoking the prompt when there's a waiting Service Worker
|
// by re-invoking the prompt when there's a waiting Service Worker
|
||||||
if (registration.waiting) {
|
if (registration.waiting) {
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import Vue from 'vue'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
import VueRouter from 'vue-router'
|
import Workspace from '@/views/MainView/Workspace'
|
||||||
import Workspace from '@/views/Main/Workspace'
|
import Inquiries from '@/views/MainView/Inquiries'
|
||||||
import Inquiries from '@/views/Main/Inquiries'
|
|
||||||
import Welcome from '@/views/Welcome'
|
import Welcome from '@/views/Welcome'
|
||||||
import Main from '@/views/Main'
|
import MainView from '@/views/MainView'
|
||||||
import LoadView from '@/views/LoadView'
|
import LoadView from '@/views/LoadView'
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
import database from '@/lib/database'
|
import database from '@/lib/database'
|
||||||
|
|
||||||
Vue.use(VueRouter)
|
export const routes = [
|
||||||
|
|
||||||
const routes = [
|
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'Welcome',
|
name: 'Welcome',
|
||||||
@@ -18,8 +15,8 @@ const routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'Main',
|
name: 'MainView',
|
||||||
component: Main,
|
component: MainView,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '/workspace',
|
path: '/workspace',
|
||||||
@@ -40,7 +37,8 @@ const routes = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = new VueRouter({
|
const router = createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
routes
|
routes
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Tab from '@/lib/tab'
|
import Tab from '@/lib/tab'
|
||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async addTab({ state }, inquiry = {}) {
|
async addTab({ state }, inquiry = {}) {
|
||||||
@@ -13,5 +14,77 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return inquiry.id
|
return inquiry.id
|
||||||
|
},
|
||||||
|
async saveInquiry({ state }, { inquiryTab, newName }) {
|
||||||
|
const value = {
|
||||||
|
id: inquiryTab.isPredefined || newName ? nanoid() : inquiryTab.id,
|
||||||
|
query: inquiryTab.query,
|
||||||
|
viewType: inquiryTab.dataView.mode,
|
||||||
|
viewOptions: inquiryTab.dataView.getOptionsForSave(),
|
||||||
|
name: newName || inquiryTab.name,
|
||||||
|
updatedAt: new Date().toJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get inquiries from local storage
|
||||||
|
const myInquiries = state.inquiries
|
||||||
|
let inquiryIndex
|
||||||
|
// Set createdAt
|
||||||
|
if (newName) {
|
||||||
|
value.createdAt = new Date().toJSON()
|
||||||
|
} else {
|
||||||
|
inquiryIndex = myInquiries.findIndex(
|
||||||
|
oldInquiry => oldInquiry.id === inquiryTab.id
|
||||||
|
)
|
||||||
|
|
||||||
|
value.createdAt =
|
||||||
|
inquiryIndex !== -1
|
||||||
|
? myInquiries[inquiryIndex].createdAt
|
||||||
|
: new Date().toJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert in inquiries list
|
||||||
|
if (newName || inquiryIndex === -1) {
|
||||||
|
myInquiries.push(value)
|
||||||
|
} else {
|
||||||
|
myInquiries.splice(inquiryIndex, 1, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
addInquiry({ state }, newInquiry) {
|
||||||
|
state.inquiries.push(newInquiry)
|
||||||
|
},
|
||||||
|
deleteInquiries({ state, commit }, inquiryIdSet) {
|
||||||
|
state.inquiries = state.inquiries.filter(
|
||||||
|
inquiry => !inquiryIdSet.has(inquiry.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Close deleted inquiries if it was opened
|
||||||
|
const tabs = state.tabs
|
||||||
|
let i = tabs.length - 1
|
||||||
|
while (i > -1) {
|
||||||
|
if (inquiryIdSet.has(tabs[i].id)) {
|
||||||
|
commit('deleteTab', tabs[i])
|
||||||
|
}
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renameInquiry({ state, commit }, { inquiryId, newName }) {
|
||||||
|
const renamingInquiry = state.inquiries.find(
|
||||||
|
inquiry => inquiry.id === inquiryId
|
||||||
|
)
|
||||||
|
|
||||||
|
renamingInquiry.name = newName
|
||||||
|
|
||||||
|
// update tab, if renamed inquiry is opened
|
||||||
|
const tab = state.tabs.find(tab => tab.id === renamingInquiry.id)
|
||||||
|
if (tab) {
|
||||||
|
commit('updateTab', {
|
||||||
|
tab,
|
||||||
|
newValues: {
|
||||||
|
name: newName
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import Vue from 'vue'
|
import { createStore } from 'vuex'
|
||||||
import Vuex from 'vuex'
|
|
||||||
import state from '@/store/state'
|
import state from '@/store/state'
|
||||||
import mutations from '@/store/mutations'
|
import mutations from '@/store/mutations'
|
||||||
import actions from '@/store/actions'
|
import actions from '@/store/actions'
|
||||||
|
|
||||||
Vue.use(Vuex)
|
export default createStore({
|
||||||
|
|
||||||
export default new Vuex.Store({
|
|
||||||
state,
|
state,
|
||||||
mutations,
|
mutations,
|
||||||
actions
|
actions
|
||||||
|
|||||||
@@ -7,23 +7,39 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateTab(state, { tab, newValues }) {
|
updateTab(state, { tab, newValues }) {
|
||||||
const { name, id, query, viewType, viewOptions, isSaved } = newValues
|
const { name, id, query, viewType, viewOptions, isSaved, updatedAt } =
|
||||||
|
newValues
|
||||||
const oldId = tab.id
|
const oldId = tab.id
|
||||||
|
|
||||||
if (id && state.currentTabId === oldId) {
|
if (id && state.currentTabId === oldId) {
|
||||||
state.currentTabId = id
|
state.currentTabId = id
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id) { tab.id = id }
|
if (id) {
|
||||||
if (name) { tab.name = name }
|
tab.id = id
|
||||||
if (query) { tab.query = query }
|
}
|
||||||
if (viewType) { tab.viewType = viewType }
|
if (name) {
|
||||||
if (viewOptions) { tab.viewOptions = viewOptions }
|
tab.name = name
|
||||||
if (isSaved !== undefined) { tab.isSaved = isSaved }
|
}
|
||||||
|
if (query) {
|
||||||
|
tab.query = query
|
||||||
|
}
|
||||||
|
if (viewType) {
|
||||||
|
tab.viewType = viewType
|
||||||
|
}
|
||||||
|
if (viewOptions) {
|
||||||
|
tab.viewOptions = viewOptions
|
||||||
|
}
|
||||||
|
if (isSaved !== undefined) {
|
||||||
|
tab.isSaved = isSaved
|
||||||
|
}
|
||||||
if (isSaved) {
|
if (isSaved) {
|
||||||
// Saved inquiry is not predefined
|
// Saved inquiry is not predefined
|
||||||
delete tab.isPredefined
|
delete tab.isPredefined
|
||||||
}
|
}
|
||||||
|
if (updatedAt) {
|
||||||
|
tab.updatedAt = updatedAt
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteTab(state, tab) {
|
deleteTab(state, tab) {
|
||||||
@@ -36,9 +52,11 @@ export default {
|
|||||||
state.currentTabId = state.tabs[index - 1].id
|
state.currentTabId = state.tabs[index - 1].id
|
||||||
} else {
|
} else {
|
||||||
state.currentTabId = null
|
state.currentTabId = null
|
||||||
state.currentTab = null
|
|
||||||
state.untitledLastIndex = 0
|
state.untitledLastIndex = 0
|
||||||
}
|
}
|
||||||
|
state.currentTab = state.currentTabId
|
||||||
|
? state.tabs.find(tab => tab.id === state.currentTabId)
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
state.tabs.splice(index, 1)
|
state.tabs.splice(index, 1)
|
||||||
},
|
},
|
||||||
@@ -47,16 +65,24 @@ export default {
|
|||||||
state.currentTabId = id
|
state.currentTabId = id
|
||||||
state.currentTab = state.tabs.find(tab => tab.id === id)
|
state.currentTab = state.tabs.find(tab => tab.id === id)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Can\'t open a tab id:' + id)
|
console.error("Can't open a tab id:" + id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updatePredefinedInquiries(state, inquiries) {
|
updatePredefinedInquiries(state, inquiries) {
|
||||||
state.predefinedInquiries = Array.isArray(inquiries) ? inquiries : [inquiries]
|
state.predefinedInquiries = Array.isArray(inquiries)
|
||||||
|
? inquiries
|
||||||
|
: [inquiries]
|
||||||
},
|
},
|
||||||
setLoadingPredefinedInquiries(state, value) {
|
setLoadingPredefinedInquiries(state, value) {
|
||||||
state.loadingPredefinedInquiries = value
|
state.loadingPredefinedInquiries = value
|
||||||
},
|
},
|
||||||
setPredefinedInquiriesLoaded(state, value) {
|
setPredefinedInquiriesLoaded(state, value) {
|
||||||
state.predefinedInquiriesLoaded = value
|
state.predefinedInquiriesLoaded = value
|
||||||
|
},
|
||||||
|
setInquiries(state, value) {
|
||||||
|
state.inquiries = value
|
||||||
|
},
|
||||||
|
setIsWorkspaceVisible(state, value) {
|
||||||
|
state.isWorkspaceVisible = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ export default {
|
|||||||
currentTab: null,
|
currentTab: null,
|
||||||
currentTabId: null,
|
currentTabId: null,
|
||||||
untitledLastIndex: 0,
|
untitledLastIndex: 0,
|
||||||
|
inquiries: [],
|
||||||
predefinedInquiries: [],
|
predefinedInquiries: [],
|
||||||
loadingPredefinedInquiries: false,
|
loadingPredefinedInquiries: false,
|
||||||
predefinedInquiriesLoaded: false,
|
predefinedInquiriesLoaded: false,
|
||||||
db: null
|
db: null,
|
||||||
|
isWorkspaceVisible: false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showTooltip(e, tooltipPosition) {
|
showTooltip(e, tooltipPosition) {
|
||||||
const position = tooltipPosition ? tooltipPosition.split('-') : ['top', 'right']
|
const position = tooltipPosition
|
||||||
|
? tooltipPosition.split('-')
|
||||||
|
: ['top', 'right']
|
||||||
const offset = 12
|
const offset = 12
|
||||||
|
|
||||||
if (position[0] === 'top') {
|
if (position[0] === 'top') {
|
||||||
@@ -25,7 +27,8 @@ export default {
|
|||||||
if (position[1] === 'right') {
|
if (position[1] === 'right') {
|
||||||
this.tooltipStyle.left = e.clientX + offset + 'px'
|
this.tooltipStyle.left = e.clientX + offset + 'px'
|
||||||
} else {
|
} else {
|
||||||
this.tooltipStyle.left = e.clientX - offset - this.tooltipElement.offsetWidth + 'px'
|
this.tooltipStyle.left =
|
||||||
|
e.clientX - offset - this.tooltipElement.offsetWidth + 'px'
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tooltipStyle.visibility = 'visible'
|
this.tooltipStyle.visibility = 'visible'
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<logs
|
<logs id="logs" :messages="messages" />
|
||||||
id="logs"
|
|
||||||
:messages="messages"
|
|
||||||
/>
|
|
||||||
<button
|
<button
|
||||||
v-if="hasErrors"
|
v-if="hasErrors"
|
||||||
id="open-workspace-btn"
|
id="open-workspace-btn"
|
||||||
class="secondary"
|
class="secondary"
|
||||||
@click="$router.push('/workspace?hide_schema=1')">
|
@click="$router.push('/workspace?hide_schema=1')"
|
||||||
|
>
|
||||||
Open workspace
|
Open workspace
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,7 +188,6 @@ export default {
|
|||||||
#logs {
|
#logs {
|
||||||
margin: 8px auto;
|
margin: 8px auto;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#open-workspace-btn {
|
#open-workspace-btn {
|
||||||
|
|||||||
@@ -1,248 +0,0 @@
|
|||||||
<template>
|
|
||||||
<nav>
|
|
||||||
<div id="nav-links">
|
|
||||||
<a href="https://sqliteviz.com">
|
|
||||||
<img :src="require('@/assets/images/logo_simple.svg')">
|
|
||||||
</a>
|
|
||||||
<router-link to="/workspace">Workspace</router-link>
|
|
||||||
<router-link to="/inquiries">Inquiries</router-link>
|
|
||||||
<a href="https://sqliteviz.com/docs" target="_blank">Help</a>
|
|
||||||
</div>
|
|
||||||
<div id="nav-buttons">
|
|
||||||
<button
|
|
||||||
id="save-btn"
|
|
||||||
v-show="currentInquiry && $route.path === '/workspace'"
|
|
||||||
class="primary"
|
|
||||||
:disabled="isSaved"
|
|
||||||
@click="checkInquiryBeforeSave"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
id="create-btn"
|
|
||||||
class="primary"
|
|
||||||
@click="createNewInquiry"
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
<app-diagnostic-info />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--Save Inquiry dialog -->
|
|
||||||
<modal name="save" classes="dialog" height="auto">
|
|
||||||
<div class="dialog-header">
|
|
||||||
Save inquiry
|
|
||||||
<close-icon @click="cancelSave"/>
|
|
||||||
</div>
|
|
||||||
<div class="dialog-body">
|
|
||||||
<div v-show="isPredefined" id="save-note">
|
|
||||||
<img :src="require('@/assets/images/info.svg')">
|
|
||||||
Note: Predefined inquiries can't be edited.
|
|
||||||
That's why your modifications will be saved as a new inquiry. Enter the name for it.
|
|
||||||
</div>
|
|
||||||
<text-field
|
|
||||||
label="Inquiry name"
|
|
||||||
:error-msg="errorMsg"
|
|
||||||
v-model="name"
|
|
||||||
width="100%"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="dialog-buttons-container">
|
|
||||||
<button class="secondary" @click="cancelSave">Cancel</button>
|
|
||||||
<button class="primary" @click="saveInquiry">Save</button>
|
|
||||||
</div>
|
|
||||||
</modal>
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import TextField from '@/components/TextField'
|
|
||||||
import CloseIcon from '@/components/svg/close'
|
|
||||||
import storedInquiries from '@/lib/storedInquiries'
|
|
||||||
import AppDiagnosticInfo from './AppDiagnosticInfo'
|
|
||||||
import events from '@/lib/utils/events'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'MainMenu',
|
|
||||||
components: {
|
|
||||||
TextField,
|
|
||||||
CloseIcon,
|
|
||||||
AppDiagnosticInfo
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
name: '',
|
|
||||||
errorMsg: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
currentInquiry () {
|
|
||||||
return this.$store.state.currentTab
|
|
||||||
},
|
|
||||||
isSaved () {
|
|
||||||
return this.currentInquiry && this.currentInquiry.isSaved
|
|
||||||
},
|
|
||||||
isPredefined () {
|
|
||||||
return this.currentInquiry && this.currentInquiry.isPredefined
|
|
||||||
},
|
|
||||||
runDisabled () {
|
|
||||||
return this.currentInquiry && (!this.$store.state.db || !this.currentInquiry.query)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.$root.$on('createNewInquiry', this.createNewInquiry)
|
|
||||||
this.$root.$on('saveInquiry', this.checkInquiryBeforeSave)
|
|
||||||
document.addEventListener('keydown', this._keyListener)
|
|
||||||
},
|
|
||||||
beforeDestroy () {
|
|
||||||
document.removeEventListener('keydown', this._keyListener)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
createNewInquiry () {
|
|
||||||
this.$store.dispatch('addTab').then(id => {
|
|
||||||
this.$store.commit('setCurrentTabId', id)
|
|
||||||
if (this.$route.path !== '/workspace') {
|
|
||||||
this.$router.push('/workspace')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
events.send('inquiry.create', null, { auto: false })
|
|
||||||
},
|
|
||||||
cancelSave () {
|
|
||||||
this.$modal.hide('save')
|
|
||||||
this.$root.$off('inquirySaved')
|
|
||||||
},
|
|
||||||
checkInquiryBeforeSave () {
|
|
||||||
this.errorMsg = null
|
|
||||||
this.name = ''
|
|
||||||
|
|
||||||
if (storedInquiries.isTabNeedName(this.currentInquiry)) {
|
|
||||||
this.$modal.show('save')
|
|
||||||
} else {
|
|
||||||
this.saveInquiry()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
saveInquiry () {
|
|
||||||
const isNeedName = storedInquiries.isTabNeedName(this.currentInquiry)
|
|
||||||
if (isNeedName && !this.name) {
|
|
||||||
this.errorMsg = 'Inquiry name can\'t be empty'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const dataSet = this.currentInquiry.result
|
|
||||||
const tabView = this.currentInquiry.view
|
|
||||||
|
|
||||||
// Save inquiry
|
|
||||||
const value = storedInquiries.save(this.currentInquiry, this.name)
|
|
||||||
|
|
||||||
// Update tab in store
|
|
||||||
this.$store.commit('updateTab', {
|
|
||||||
tab: this.currentInquiry,
|
|
||||||
newValues: {
|
|
||||||
name: value.name,
|
|
||||||
id: value.id,
|
|
||||||
query: value.query,
|
|
||||||
viewType: value.viewType,
|
|
||||||
viewOptions: value.viewOptions,
|
|
||||||
isSaved: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Restore data:
|
|
||||||
// e.g. if we save predefined inquiry the tab will be created again
|
|
||||||
// (because of new id) and
|
|
||||||
// it will be without sql result and has default view - table.
|
|
||||||
// That's why we need to restore data and view
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.currentInquiry.result = dataSet
|
|
||||||
this.currentInquiry.view = tabView
|
|
||||||
})
|
|
||||||
|
|
||||||
// Hide dialog
|
|
||||||
this.$modal.hide('save')
|
|
||||||
|
|
||||||
// Signal about saving
|
|
||||||
this.$root.$emit('inquirySaved')
|
|
||||||
events.send('inquiry.save')
|
|
||||||
},
|
|
||||||
_keyListener (e) {
|
|
||||||
if (this.$route.path === '/workspace') {
|
|
||||||
// Run query Ctrl+R or Ctrl+Enter
|
|
||||||
if ((e.key === 'r' || e.key === 'Enter') && (e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault()
|
|
||||||
if (!this.runDisabled) {
|
|
||||||
this.currentInquiry.execute()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save inquiry Ctrl+S
|
|
||||||
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault()
|
|
||||||
if (!this.isSaved) {
|
|
||||||
this.checkInquiryBeforeSave()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// New (blank) inquiry Ctrl+B
|
|
||||||
if (e.key === 'b' && (e.ctrlKey || e.metaKey)) {
|
|
||||||
e.preventDefault()
|
|
||||||
this.createNewInquiry()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
nav {
|
|
||||||
height: 68px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
background-color: var(--color-bg-light);
|
|
||||||
border-bottom: 1px solid var(--color-border-light);
|
|
||||||
box-shadow: var(--shadow-1);
|
|
||||||
box-sizing: border-box;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
padding: 0 16px 0 52px;
|
|
||||||
z-index: 999;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
font-size: 18px;
|
|
||||||
color: var(--color-text-base);
|
|
||||||
text-transform: none;
|
|
||||||
text-decoration: none;
|
|
||||||
margin-right: 28px;
|
|
||||||
}
|
|
||||||
a.router-link-active {
|
|
||||||
color: var(--color-accent);
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
margin-left: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#save-note {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
#save-note img {
|
|
||||||
margin: -3px 6px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav-buttons {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav-links {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
#nav-links img {
|
|
||||||
width: 32px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-show="visible" class="chart-container" ref="chartContainer">
|
|
||||||
<div class="warning chart-warning" v-show="!dataSources && visible">
|
|
||||||
There is no data to build a chart. Run your SQL query and make sure the result is not empty.
|
|
||||||
</div>
|
|
||||||
<PlotlyEditor
|
|
||||||
:data="state.data"
|
|
||||||
:layout="state.layout"
|
|
||||||
:frames="state.frames"
|
|
||||||
:config="{ editable: true, displaylogo: false, modeBarButtonsToRemove: ['toImage'] }"
|
|
||||||
:dataSources="dataSources"
|
|
||||||
:dataSourceOptions="dataSourceOptions"
|
|
||||||
:plotly="plotly"
|
|
||||||
@onUpdate="update"
|
|
||||||
@onRender="onRender"
|
|
||||||
:useResizeHandler="true"
|
|
||||||
:debug="true"
|
|
||||||
:advancedTraceTypeSelector="true"
|
|
||||||
class="chart"
|
|
||||||
ref="plotlyEditor"
|
|
||||||
:style="{ height: !dataSources ? 'calc(100% - 40px)' : '100%' }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import plotly from 'plotly.js'
|
|
||||||
import 'react-chart-editor/lib/react-chart-editor.min.css'
|
|
||||||
|
|
||||||
import PlotlyEditor from 'react-chart-editor'
|
|
||||||
import chartHelper from '@/lib/chartHelper'
|
|
||||||
import dereference from 'react-chart-editor/lib/lib/dereference'
|
|
||||||
import fIo from '@/lib/utils/fileIo'
|
|
||||||
import events from '@/lib/utils/events'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'Chart',
|
|
||||||
props: [
|
|
||||||
'dataSources', 'initOptions',
|
|
||||||
'importToPngEnabled', 'importToSvgEnabled',
|
|
||||||
'forPivot'
|
|
||||||
],
|
|
||||||
components: {
|
|
||||||
PlotlyEditor
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
plotly: plotly,
|
|
||||||
state: this.initOptions || {
|
|
||||||
data: [],
|
|
||||||
layout: {},
|
|
||||||
frames: []
|
|
||||||
},
|
|
||||||
visible: true,
|
|
||||||
resizeObserver: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
dataSourceOptions () {
|
|
||||||
return chartHelper.getOptionsFromDataSources(this.dataSources)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
// https://github.com/plotly/plotly.js/issues/4555
|
|
||||||
plotly.setPlotConfig({
|
|
||||||
notifyOnLogging: 1
|
|
||||||
})
|
|
||||||
this.$watch(
|
|
||||||
() => this.state && this.state.data && this.state.data
|
|
||||||
.map(trace => `${trace.type}${trace.mode ? '-' + trace.mode : ''}`)
|
|
||||||
.join(','),
|
|
||||||
(value) => {
|
|
||||||
events.send('viz_plotly.render', null, {
|
|
||||||
type: value,
|
|
||||||
pivot: !!this.forPivot
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
)
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
|
||||||
this.resizeObserver.observe(this.$refs.chartContainer)
|
|
||||||
},
|
|
||||||
beforeDestroy () {
|
|
||||||
this.resizeObserver.unobserve(this.$refs.chartContainer)
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
dataSources () {
|
|
||||||
// we need to update state.data in order to update the graph
|
|
||||||
// https://github.com/plotly/react-chart-editor/issues/948
|
|
||||||
if (this.dataSources) {
|
|
||||||
dereference(this.state.data, this.dataSources)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleResize () {
|
|
||||||
this.visible = false
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.visible = true
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onRender (data, layout, frames) {
|
|
||||||
// TODO: check changes and enable Save button if needed
|
|
||||||
},
|
|
||||||
update (data, layout, frames) {
|
|
||||||
this.state = { data, layout, frames }
|
|
||||||
this.$emit('update')
|
|
||||||
},
|
|
||||||
getOptionsForSave () {
|
|
||||||
return chartHelper.getOptionsForSave(this.state, this.dataSources)
|
|
||||||
},
|
|
||||||
async saveAsPng () {
|
|
||||||
const url = await this.prepareCopy()
|
|
||||||
this.$emit('loadingImageCompleted')
|
|
||||||
fIo.downloadFromUrl(url, 'chart')
|
|
||||||
},
|
|
||||||
|
|
||||||
async saveAsSvg () {
|
|
||||||
const url = await this.prepareCopy('svg')
|
|
||||||
fIo.downloadFromUrl(url, 'chart')
|
|
||||||
},
|
|
||||||
|
|
||||||
saveAsHtml () {
|
|
||||||
fIo.exportToFile(
|
|
||||||
chartHelper.getHtml(this.state),
|
|
||||||
'chart.html',
|
|
||||||
'text/html'
|
|
||||||
)
|
|
||||||
},
|
|
||||||
async prepareCopy (type = 'png') {
|
|
||||||
return await chartHelper.getImageDataUrl(this.$refs.plotlyEditor.$el, type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.chart-container {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-warning {
|
|
||||||
height: 40px;
|
|
||||||
line-height: 40px;
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart {
|
|
||||||
min-height: 242px;
|
|
||||||
}
|
|
||||||
|
|
||||||
>>> .editor_controls .sidebar__item:before {
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="run-result-panel" ref="runResultPanel">
|
|
||||||
<div class="run-result-panel-content">
|
|
||||||
<div
|
|
||||||
v-show="result === null && !isGettingResults && !error"
|
|
||||||
class="table-preview result-before"
|
|
||||||
>
|
|
||||||
Run your query and get results here
|
|
||||||
</div>
|
|
||||||
<div v-if="isGettingResults" class="table-preview result-in-progress">
|
|
||||||
<loading-indicator :size="30"/>
|
|
||||||
Fetching results...
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-show="result === undefined && !isGettingResults && !error"
|
|
||||||
class="table-preview result-empty"
|
|
||||||
>
|
|
||||||
No rows retrieved according to your query
|
|
||||||
</div>
|
|
||||||
<logs v-if="error" :messages="[error]"/>
|
|
||||||
<sql-table
|
|
||||||
v-if="result"
|
|
||||||
:data-set="result"
|
|
||||||
:time="time"
|
|
||||||
:pageSize="pageSize"
|
|
||||||
class="straight"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<side-tool-bar @switchTo="$emit('switchTo', $event)" panel="table">
|
|
||||||
<icon-button
|
|
||||||
:disabled="!result"
|
|
||||||
tooltip="Export result set to CSV file"
|
|
||||||
tooltip-position="top-left"
|
|
||||||
@click="exportToCsv"
|
|
||||||
>
|
|
||||||
<export-to-csv-icon/>
|
|
||||||
</icon-button>
|
|
||||||
|
|
||||||
<icon-button
|
|
||||||
:disabled="!result"
|
|
||||||
tooltip="Copy result set to clipboard"
|
|
||||||
tooltip-position="top-left"
|
|
||||||
@click="prepareCopy"
|
|
||||||
>
|
|
||||||
<clipboard-icon/>
|
|
||||||
</icon-button>
|
|
||||||
</side-tool-bar>
|
|
||||||
|
|
||||||
<loading-dialog
|
|
||||||
loadingMsg="Building CSV..."
|
|
||||||
successMsg="CSV is ready"
|
|
||||||
actionBtnName="Copy"
|
|
||||||
name="prepareCSVCopy"
|
|
||||||
title="Copy to clipboard"
|
|
||||||
:loading="preparingCopy"
|
|
||||||
@action="copyToClipboard"
|
|
||||||
@cancel="cancelCopy"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Logs from '@/components/Logs'
|
|
||||||
import SqlTable from '@/components/SqlTable'
|
|
||||||
import LoadingIndicator from '@/components/LoadingIndicator'
|
|
||||||
import SideToolBar from './SideToolBar'
|
|
||||||
import ExportToCsvIcon from '@/components/svg/exportToCsv'
|
|
||||||
import ClipboardIcon from '@/components/svg/clipboard'
|
|
||||||
import IconButton from '@/components/IconButton'
|
|
||||||
import csv from '@/lib/csv'
|
|
||||||
import fIo from '@/lib/utils/fileIo'
|
|
||||||
import cIo from '@/lib/utils/clipboardIo'
|
|
||||||
import time from '@/lib/utils/time'
|
|
||||||
import loadingDialog from '@/components/LoadingDialog'
|
|
||||||
import events from '@/lib/utils/events'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'RunResult',
|
|
||||||
props: ['result', 'isGettingResults', 'error', 'time'],
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
resizeObserver: null,
|
|
||||||
pageSize: 20,
|
|
||||||
preparingCopy: false,
|
|
||||||
dataToCopy: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
SqlTable,
|
|
||||||
LoadingIndicator,
|
|
||||||
Logs,
|
|
||||||
SideToolBar,
|
|
||||||
ExportToCsvIcon,
|
|
||||||
IconButton,
|
|
||||||
ClipboardIcon,
|
|
||||||
loadingDialog
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
|
||||||
this.resizeObserver.observe(this.$refs.runResultPanel)
|
|
||||||
this.calculatePageSize()
|
|
||||||
},
|
|
||||||
beforeDestroy () {
|
|
||||||
this.resizeObserver.unobserve(this.$refs.runResultPanel)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleResize () {
|
|
||||||
this.calculatePageSize()
|
|
||||||
},
|
|
||||||
|
|
||||||
calculatePageSize () {
|
|
||||||
const runResultPanel = this.$refs.runResultPanel
|
|
||||||
// 27 - table footer hight
|
|
||||||
// 5 - padding-bottom of rounded table container
|
|
||||||
// 35 - height of table header
|
|
||||||
const freeSpace = runResultPanel.offsetHeight - 27 - 5 - 35
|
|
||||||
this.pageSize = Math.max(Math.floor(freeSpace / 35), 20)
|
|
||||||
},
|
|
||||||
|
|
||||||
exportToCsv () {
|
|
||||||
if (this.result && this.result.values) {
|
|
||||||
events.send('resultset.export',
|
|
||||||
this.result.values[this.result.columns[0]].length,
|
|
||||||
{ to: 'csv' }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fIo.exportToFile(csv.serialize(this.result), 'result_set.csv', 'text/csv')
|
|
||||||
},
|
|
||||||
|
|
||||||
async prepareCopy () {
|
|
||||||
if (this.result && this.result.values) {
|
|
||||||
events.send('resultset.export',
|
|
||||||
this.result.values[this.result.columns[0]].length,
|
|
||||||
{ to: 'clipboard' }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('ClipboardItem' in window) {
|
|
||||||
this.preparingCopy = true
|
|
||||||
this.$modal.show('prepareCSVCopy')
|
|
||||||
const t0 = performance.now()
|
|
||||||
|
|
||||||
await time.sleep(0)
|
|
||||||
this.dataToCopy = csv.serialize(this.result)
|
|
||||||
const t1 = performance.now()
|
|
||||||
if ((t1 - t0) < 950) {
|
|
||||||
this.$modal.hide('prepareCSVCopy')
|
|
||||||
this.copyToClipboard()
|
|
||||||
} else {
|
|
||||||
this.preparingCopy = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
alert(
|
|
||||||
"Your browser doesn't support copying into the clipboard. " +
|
|
||||||
'If you use Firefox you can enable it ' +
|
|
||||||
'by setting dom.events.asyncClipboard.clipboardItem to true.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
copyToClipboard () {
|
|
||||||
cIo.copyCsv(this.dataToCopy)
|
|
||||||
this.$modal.hide('prepareCSVCopy')
|
|
||||||
},
|
|
||||||
|
|
||||||
cancelCopy () {
|
|
||||||
this.dataToCopy = null
|
|
||||||
this.$modal.hide('prepareCSVCopy')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.run-result-panel {
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.run-result-panel-content {
|
|
||||||
position: relative;
|
|
||||||
flex-grow: 1;
|
|
||||||
height: 100%;
|
|
||||||
width: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-preview {
|
|
||||||
position: absolute;
|
|
||||||
top: 40%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
color: var(--color-text-base);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-in-progress {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
will-change: opacity;
|
|
||||||
/*
|
|
||||||
We need to show loader in 1 sec after starting query execution. We can't do that with
|
|
||||||
setTimeout because the main thread can be busy by getting a result set from the web worker.
|
|
||||||
But we can use CSS animation for opacity. Opacity triggers changes only in the Composite Layer
|
|
||||||
stage in rendering waterfall. Hence it can be processed only with Compositor Thread while
|
|
||||||
the Main Thread processes a result set.
|
|
||||||
https://www.viget.com/articles/animation-performance-101-browser-under-the-hood/
|
|
||||||
*/
|
|
||||||
animation: show-loader 1s linear 0s 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes show-loader {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
99% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
>>>.vm--container {
|
|
||||||
animation: show-modal 1s linear 0s 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes show-modal {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
99% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
<div id="app-info-container">
|
<div id="app-info-container">
|
||||||
<img
|
<img
|
||||||
id="app-info-icon"
|
id="app-info-icon"
|
||||||
:src="require('@/assets/images/info.svg')"
|
src="~@/assets/images/info.svg"
|
||||||
@click="$modal.show('app-info')"
|
@click="$modal.show('app-info')"
|
||||||
/>
|
/>
|
||||||
<modal name="app-info" classes="dialog" height="auto" width="400px">
|
<modal modalId="app-info" class="dialog" contentClass="app-info-modal">
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
App info
|
App info
|
||||||
<close-icon @click="$modal.hide('app-info')" />
|
<close-icon @click="$modal.hide('app-info')" />
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
<div class="divider" />
|
<div class="divider" />
|
||||||
<div class="options">
|
<div class="options">
|
||||||
<div v-for="(opt, index) in item.info" :key="index">
|
<div v-for="(opt, optIndex) in item.info" :key="optIndex">
|
||||||
{{ opt }}
|
{{ opt }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import CloseIcon from '@/components/svg/close'
|
import CloseIcon from '@/components/svg/close'
|
||||||
|
import { version } from '../../../package.json'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AppDiagnosticInfo',
|
name: 'AppDiagnosticInfo',
|
||||||
@@ -36,7 +37,7 @@ export default {
|
|||||||
info: [
|
info: [
|
||||||
{
|
{
|
||||||
name: 'sqliteviz version',
|
name: 'sqliteviz version',
|
||||||
info: [require('../../../package.json').version]
|
info: [version]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -59,6 +60,12 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.app-info-modal {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
#app-info-icon {
|
#app-info-icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -1,43 +1,55 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="my-inquiries-container">
|
<div id="my-inquiries-container">
|
||||||
<div id="start-guide" v-if="allInquiries.length === 0">
|
<div v-if="allInquiries.length === 0" id="start-guide">
|
||||||
You don't have saved inquiries so far.
|
You don't have saved inquiries so far.
|
||||||
<span class="link" @click="$root.$emit('createNewInquiry')">Create</span>
|
<span class="link" @click="emitCreateTabEvent">Create</span>
|
||||||
the one from scratch or
|
the one from scratch or
|
||||||
<span @click="importInquiries" class="link">import</span> from a file.
|
<span class="link" @click="importInquiries">import</span> from a file.
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
id="loading-predefined-status"
|
|
||||||
v-if="$store.state.loadingPredefinedInquiries"
|
v-if="$store.state.loadingPredefinedInquiries"
|
||||||
|
id="loading-predefined-status"
|
||||||
>
|
>
|
||||||
<loading-indicator />
|
<loading-indicator />
|
||||||
Loading predefined inquiries...
|
Loading predefined inquiries...
|
||||||
</div>
|
</div>
|
||||||
<div id="my-inquiries-content" ref="my-inquiries-content" v-show="allInquiries.length > 0">
|
<div
|
||||||
|
v-show="allInquiries.length > 0"
|
||||||
|
id="my-inquiries-content"
|
||||||
|
ref="my-inquiries-content"
|
||||||
|
>
|
||||||
<div id="my-inquiries-toolbar">
|
<div id="my-inquiries-toolbar">
|
||||||
<div id="toolbar-buttons">
|
<div id="toolbar-buttons">
|
||||||
<button id="toolbar-btns-import" class="toolbar" @click="importInquiries">
|
<button
|
||||||
|
id="toolbar-btns-import"
|
||||||
|
class="toolbar"
|
||||||
|
@click="importInquiries"
|
||||||
|
>
|
||||||
Import
|
Import
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-show="selectedInquiriesCount > 0"
|
||||||
id="toolbar-btns-export"
|
id="toolbar-btns-export"
|
||||||
class="toolbar"
|
class="toolbar"
|
||||||
v-show="selectedInquiriesCount > 0"
|
|
||||||
@click="exportSelectedInquiries()"
|
@click="exportSelectedInquiries()"
|
||||||
>
|
>
|
||||||
Export
|
Export
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-show="selectedNotPredefinedCount > 0"
|
||||||
id="toolbar-btns-delete"
|
id="toolbar-btns-delete"
|
||||||
class="toolbar"
|
class="toolbar"
|
||||||
v-show="selectedNotPredefinedCount > 0"
|
|
||||||
@click="showDeleteDialog(selectedInquiriesIds)"
|
@click="showDeleteDialog(selectedInquiriesIds)"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="toolbar-search">
|
<div id="toolbar-search">
|
||||||
<text-field placeholder="Search inquiry by name" width="300px" v-model="filter"/>
|
<text-field
|
||||||
|
v-model="filter"
|
||||||
|
placeholder="Search inquiry by name"
|
||||||
|
width="300px"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -48,16 +60,21 @@
|
|||||||
<div v-show="showedInquiries.length > 0" class="rounded-bg">
|
<div v-show="showedInquiries.length > 0" class="rounded-bg">
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<div>
|
<div>
|
||||||
<div class="fixed-header" ref="name-th">
|
<div ref="name-th" class="fixed-header">
|
||||||
<check-box ref="mainCheckBox" theme="light" @click="toggleSelectAll"/>
|
<check-box
|
||||||
|
ref="mainCheckBox"
|
||||||
|
theme="light"
|
||||||
|
@click="toggleSelectAll"
|
||||||
|
/>
|
||||||
<div class="name-th">Name</div>
|
<div class="name-th">Name</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fixed-header">
|
<div class="fixed-header">Created at</div>
|
||||||
Created at
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div
|
||||||
<div class="table-container" :style="{ 'max-height': `${maxTableHeight}px` }">
|
class="table-container"
|
||||||
|
:style="{ 'max-height': `${maxTableHeight}px` }"
|
||||||
|
>
|
||||||
<table ref="table" class="sqliteviz-table">
|
<table ref="table" class="sqliteviz-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr
|
||||||
@@ -70,6 +87,7 @@
|
|||||||
<check-box
|
<check-box
|
||||||
ref="rowCheckBox"
|
ref="rowCheckBox"
|
||||||
:init="selectAll || selectedInquiriesIds.has(inquiry.id)"
|
:init="selectAll || selectedInquiriesIds.has(inquiry.id)"
|
||||||
|
data-test="rowCheckBox"
|
||||||
@click="toggleRow($event, inquiry.id)"
|
@click="toggleRow($event, inquiry.id)"
|
||||||
/>
|
/>
|
||||||
<div class="name">{{ inquiry.name }}</div>
|
<div class="name">{{ inquiry.name }}</div>
|
||||||
@@ -80,16 +98,22 @@
|
|||||||
@mouseleave="hideTooltip"
|
@mouseleave="hideTooltip"
|
||||||
>
|
>
|
||||||
Predefined
|
Predefined
|
||||||
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
<span
|
||||||
Predefined inquiries come from the server.
|
ref="tooltip"
|
||||||
These inquiries can’t be deleted or renamed.
|
class="icon-tooltip"
|
||||||
|
:style="tooltipStyle"
|
||||||
|
>
|
||||||
|
Predefined inquiries come from the server. These
|
||||||
|
inquiries can’t be deleted or renamed.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="second-column">
|
<div class="second-column">
|
||||||
<div class="date-container">{{ inquiry.createdAt | date }}</div>
|
<div class="date-container">
|
||||||
|
{{ createdAtFormatted(inquiry.createdAt) }}
|
||||||
|
</div>
|
||||||
<div class="icons-container">
|
<div class="icons-container">
|
||||||
<rename-icon
|
<rename-icon
|
||||||
v-if="!inquiry.isPredefined"
|
v-if="!inquiry.isPredefined"
|
||||||
@@ -97,13 +121,13 @@
|
|||||||
/>
|
/>
|
||||||
<copy-icon @click="duplicateInquiry(index)" />
|
<copy-icon @click="duplicateInquiry(index)" />
|
||||||
<export-icon
|
<export-icon
|
||||||
@click="exportToFile([inquiry], `${inquiry.name}.json`)"
|
|
||||||
tooltip="Export inquiry to file"
|
tooltip="Export inquiry to file"
|
||||||
tooltip-position="top-left"
|
tooltipPosition="top-left"
|
||||||
|
@click="exportToFile([inquiry], `${inquiry.name}.json`)"
|
||||||
/>
|
/>
|
||||||
<delete-icon
|
<delete-icon
|
||||||
v-if="!inquiry.isPredefined"
|
v-if="!inquiry.isPredefined"
|
||||||
@click="showDeleteDialog((new Set()).add(inquiry.id))"
|
@click="showDeleteDialog(new Set().add(inquiry.id))"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,16 +140,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--Rename Inquiry dialog -->
|
<!--Rename Inquiry dialog -->
|
||||||
<modal name="rename" classes="dialog" height="auto">
|
<modal modalId="rename" class="dialog" contentStyle="width: 560px;">
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
Rename inquiry
|
Rename inquiry
|
||||||
<close-icon @click="$modal.hide('rename')" />
|
<close-icon @click="$modal.hide('rename')" />
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
<text-field
|
<text-field
|
||||||
label="New inquiry name"
|
|
||||||
:error-msg="errorMsg"
|
|
||||||
v-model="newName"
|
v-model="newName"
|
||||||
|
label="New inquiry name"
|
||||||
|
:errorMsg="errorMsg"
|
||||||
width="100%"
|
width="100%"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,15 +160,18 @@
|
|||||||
</modal>
|
</modal>
|
||||||
|
|
||||||
<!--Delete Inquiry dialog -->
|
<!--Delete Inquiry dialog -->
|
||||||
<modal name="delete" classes="dialog" height="auto">
|
<modal modalId="delete" class="dialog" contentStyle="width: 480px;">
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
Delete {{ deleteGroup ? 'inquiries' : 'inquiry' }}
|
Delete {{ deleteGroup ? 'inquiries' : 'inquiry' }}
|
||||||
<close-icon @click="$modal.hide('delete')" />
|
<close-icon @click="$modal.hide('delete')" />
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
{{ deleteDialogMsg }}
|
{{ deleteDialogMsg }}
|
||||||
<div v-show="selectedInquiriesCount > selectedNotPredefinedCount" id="note">
|
<div
|
||||||
<img :src="require('@/assets/images/info.svg')">
|
v-show="selectedInquiriesCount > selectedNotPredefinedCount"
|
||||||
|
id="note"
|
||||||
|
>
|
||||||
|
<img src="~@/assets/images/info.svg" />
|
||||||
Note: Predefined inquiries you've selected won't be deleted
|
Note: Predefined inquiries you've selected won't be deleted
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,6 +194,7 @@ import CheckBox from '@/components/CheckBox'
|
|||||||
import LoadingIndicator from '@/components/LoadingIndicator'
|
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||||
import tooltipMixin from '@/tooltipMixin'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
import storedInquiries from '@/lib/storedInquiries'
|
import storedInquiries from '@/lib/storedInquiries'
|
||||||
|
import eventBus from '@/lib/eventBus'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Inquiries',
|
name: 'Inquiries',
|
||||||
@@ -183,7 +211,6 @@ export default {
|
|||||||
mixins: [tooltipMixin],
|
mixins: [tooltipMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
inquiries: [],
|
|
||||||
filter: null,
|
filter: null,
|
||||||
newName: null,
|
newName: null,
|
||||||
processedInquiryId: null,
|
processedInquiryId: null,
|
||||||
@@ -198,6 +225,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
inquiries() {
|
||||||
|
return this.$store.state.inquiries
|
||||||
|
},
|
||||||
predefinedInquiries() {
|
predefinedInquiries() {
|
||||||
return this.$store.state.predefinedInquiries.map(inquiry => {
|
return this.$store.state.predefinedInquiries.map(inquiry => {
|
||||||
inquiry.isPredefined = true
|
inquiry.isPredefined = true
|
||||||
@@ -211,7 +241,8 @@ export default {
|
|||||||
let showedInquiries = this.allInquiries
|
let showedInquiries = this.allInquiries
|
||||||
if (this.filter) {
|
if (this.filter) {
|
||||||
showedInquiries = showedInquiries.filter(
|
showedInquiries = showedInquiries.filter(
|
||||||
inquiry => inquiry.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0
|
inquiry =>
|
||||||
|
inquiry.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return showedInquiries
|
return showedInquiries
|
||||||
@@ -221,46 +252,57 @@ export default {
|
|||||||
return this.predefinedInquiries.concat(this.inquiries)
|
return this.predefinedInquiries.concat(this.inquiries)
|
||||||
},
|
},
|
||||||
processedInquiryIndex() {
|
processedInquiryIndex() {
|
||||||
return this.inquiries.findIndex(inquiry => inquiry.id === this.processedInquiryId)
|
return this.inquiries.findIndex(
|
||||||
|
inquiry => inquiry.id === this.processedInquiryId
|
||||||
|
)
|
||||||
},
|
},
|
||||||
deleteDialogMsg() {
|
deleteDialogMsg() {
|
||||||
if (!this.deleteGroup && (
|
if (
|
||||||
this.processedInquiryIndex === null ||
|
!this.deleteGroup &&
|
||||||
|
(this.processedInquiryIndex === null ||
|
||||||
this.processedInquiryIndex < 0 ||
|
this.processedInquiryIndex < 0 ||
|
||||||
this.processedInquiryIndex > this.inquiries.length
|
this.processedInquiryIndex > this.inquiries.length)
|
||||||
)) {
|
) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteItem = this.deleteGroup
|
const deleteItem = this.deleteGroup
|
||||||
? `${this.selectedNotPredefinedCount} ${this.selectedNotPredefinedCount > 1
|
? `${this.selectedNotPredefinedCount} ${
|
||||||
? 'inquiries'
|
this.selectedNotPredefinedCount > 1 ? 'inquiries' : 'inquiry'
|
||||||
: 'inquiry'}`
|
}`
|
||||||
: `"${this.inquiries[this.processedInquiryIndex].name}"`
|
: `"${this.inquiries[this.processedInquiryIndex].name}"`
|
||||||
|
|
||||||
return `Are you sure you want to delete ${deleteItem}?`
|
return `Are you sure you want to delete ${deleteItem}?`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
showedInquiries () {
|
showedInquiries: {
|
||||||
this.selectedInquiriesIds = new Set(this.showedInquiries
|
handler() {
|
||||||
|
this.selectedInquiriesIds = new Set(
|
||||||
|
this.showedInquiries
|
||||||
.filter(inquiry => this.selectedInquiriesIds.has(inquiry.id))
|
.filter(inquiry => this.selectedInquiriesIds.has(inquiry.id))
|
||||||
.map(inquiry => inquiry.id)
|
.map(inquiry => inquiry.id)
|
||||||
)
|
)
|
||||||
this.selectedInquiriesCount = this.selectedInquiriesIds.size
|
this.selectedInquiriesCount = this.selectedInquiriesIds.size
|
||||||
this.selectedNotPredefinedCount = ([...this.selectedInquiriesIds]
|
this.selectedNotPredefinedCount = [...this.selectedInquiriesIds].filter(
|
||||||
.filter(id => !this.predefinedInquiriesIds.has(id))).length
|
id => !this.predefinedInquiriesIds.has(id)
|
||||||
|
).length
|
||||||
|
|
||||||
if (this.selectedInquiriesIds.size < this.showedInquiries.length) {
|
if (this.selectedInquiriesIds.size < this.showedInquiries.length) {
|
||||||
|
if (this.$refs.mainCheckBox) {
|
||||||
this.$refs.mainCheckBox.checked = false
|
this.$refs.mainCheckBox.checked = false
|
||||||
|
}
|
||||||
this.selectAll = false
|
this.selectAll = false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
this.inquiries = storedInquiries.getStoredInquiries()
|
const loadingPredefinedInquiries =
|
||||||
const loadingPredefinedInquiries = this.$store.state.loadingPredefinedInquiries
|
this.$store.state.loadingPredefinedInquiries
|
||||||
const predefinedInquiriesLoaded = this.$store.state.predefinedInquiriesLoaded
|
const predefinedInquiriesLoaded =
|
||||||
|
this.$store.state.predefinedInquiriesLoaded
|
||||||
if (!predefinedInquiriesLoaded && !loadingPredefinedInquiries) {
|
if (!predefinedInquiriesLoaded && !loadingPredefinedInquiries) {
|
||||||
try {
|
try {
|
||||||
this.$store.commit('setLoadingPredefinedInquiries', true)
|
this.$store.commit('setLoadingPredefinedInquiries', true)
|
||||||
@@ -282,12 +324,15 @@ export default {
|
|||||||
this.calcNameWidth()
|
this.calcNameWidth()
|
||||||
this.calcMaxTableHeight()
|
this.calcMaxTableHeight()
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeUnmount() {
|
||||||
this.resizeObserver.unobserve(this.$refs['my-inquiries-content'])
|
this.resizeObserver.unobserve(this.$refs['my-inquiries-content'])
|
||||||
this.tableResizeObserver.unobserve(this.$refs.table)
|
this.tableResizeObserver.unobserve(this.$refs.table)
|
||||||
},
|
},
|
||||||
filters: {
|
methods: {
|
||||||
date (value) {
|
emitCreateTabEvent() {
|
||||||
|
eventBus.$emit('createNewInquiry')
|
||||||
|
},
|
||||||
|
createdAtFormatted(value) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -297,13 +342,15 @@ export default {
|
|||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit'
|
minute: '2-digit'
|
||||||
}
|
}
|
||||||
return new Date(value).toLocaleDateString('en-GB', dateOptions) + ' ' +
|
return (
|
||||||
|
new Date(value).toLocaleDateString('en-GB', dateOptions) +
|
||||||
|
' ' +
|
||||||
new Date(value).toLocaleTimeString('en-GB', timeOptions)
|
new Date(value).toLocaleTimeString('en-GB', timeOptions)
|
||||||
}
|
)
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
calcNameWidth() {
|
calcNameWidth() {
|
||||||
const nameWidth = this.$refs['name-td'] && this.$refs['name-td'][0]
|
const nameWidth =
|
||||||
|
this.$refs['name-td'] && this.$refs['name-td'][0]
|
||||||
? this.$refs['name-td'][0].getBoundingClientRect().width
|
? this.$refs['name-td'][0].getBoundingClientRect().width
|
||||||
: 0
|
: 0
|
||||||
this.$refs['name-th'].style = `width: ${nameWidth}px`
|
this.$refs['name-th'].style = `width: ${nameWidth}px`
|
||||||
@@ -314,10 +361,12 @@ export default {
|
|||||||
},
|
},
|
||||||
openInquiry(index) {
|
openInquiry(index) {
|
||||||
const tab = this.showedInquiries[index]
|
const tab = this.showedInquiries[index]
|
||||||
|
setTimeout(() => {
|
||||||
this.$store.dispatch('addTab', tab).then(id => {
|
this.$store.dispatch('addTab', tab).then(id => {
|
||||||
this.$store.commit('setCurrentTabId', id)
|
this.$store.commit('setCurrentTabId', id)
|
||||||
this.$router.push('/workspace')
|
this.$router.push('/workspace')
|
||||||
})
|
})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
showRenameDialog(id) {
|
showRenameDialog(id) {
|
||||||
this.errorMsg = null
|
this.errorMsg = null
|
||||||
@@ -330,31 +379,19 @@ export default {
|
|||||||
this.errorMsg = "Inquiry name can't be empty"
|
this.errorMsg = "Inquiry name can't be empty"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const processedInquiry = this.inquiries[this.processedInquiryIndex]
|
this.$store.dispatch('renameInquiry', {
|
||||||
processedInquiry.name = this.newName
|
inquiryId: this.processedInquiryId,
|
||||||
this.$set(this.inquiries, this.processedInquiryIndex, processedInquiry)
|
newName: this.newName
|
||||||
|
|
||||||
// update inquiries in local storage
|
|
||||||
storedInquiries.updateStorage(this.inquiries)
|
|
||||||
|
|
||||||
// update tab, if renamed inquiry is opened
|
|
||||||
const tab = this.$store.state.tabs
|
|
||||||
.find(tab => tab.id === processedInquiry.id)
|
|
||||||
if (tab) {
|
|
||||||
this.$store.commit('updateTab', {
|
|
||||||
tab,
|
|
||||||
newValues: {
|
|
||||||
name: this.newName
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
|
||||||
// hide dialog
|
// hide dialog
|
||||||
this.$modal.hide('rename')
|
this.$modal.hide('rename')
|
||||||
},
|
},
|
||||||
duplicateInquiry(index) {
|
duplicateInquiry(index) {
|
||||||
const newInquiry = storedInquiries.duplicateInquiry(this.showedInquiries[index])
|
const newInquiry = storedInquiries.duplicateInquiry(
|
||||||
this.inquiries.push(newInquiry)
|
this.showedInquiries[index]
|
||||||
storedInquiries.updateStorage(this.inquiries)
|
)
|
||||||
|
this.$store.dispatch('addInquiry', newInquiry)
|
||||||
},
|
},
|
||||||
showDeleteDialog(idsSet) {
|
showDeleteDialog(idsSet) {
|
||||||
this.deleteGroup = idsSet.size > 1
|
this.deleteGroup = idsSet.size > 1
|
||||||
@@ -366,62 +403,48 @@ export default {
|
|||||||
deleteInquiry() {
|
deleteInquiry() {
|
||||||
this.$modal.hide('delete')
|
this.$modal.hide('delete')
|
||||||
if (!this.deleteGroup) {
|
if (!this.deleteGroup) {
|
||||||
this.inquiries.splice(this.processedInquiryIndex, 1)
|
this.$store.dispatch(
|
||||||
|
'deleteInquiries',
|
||||||
// Close deleted inquiry tab if it was opened
|
new Set().add(this.processedInquiryId)
|
||||||
const tab = this.$store.state.tabs
|
)
|
||||||
.find(tab => tab.id === this.processedInquiryId)
|
|
||||||
if (tab) {
|
|
||||||
this.$store.commit('deleteTab', tab)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear checkbox
|
// Clear checkbox
|
||||||
if (this.selectedInquiriesIds.has(this.processedInquiryId)) {
|
if (this.selectedInquiriesIds.has(this.processedInquiryId)) {
|
||||||
this.selectedInquiriesIds.delete(this.processedInquiryId)
|
this.selectedInquiriesIds.delete(this.processedInquiryId)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.inquiries = this.inquiries.filter(
|
this.$store.dispatch('deleteInquiries', this.selectedInquiriesIds)
|
||||||
inquiry => !this.selectedInquiriesIds.has(inquiry.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Close deleted inquiries if it was opened
|
|
||||||
const tabs = this.$store.state.tabs
|
|
||||||
let i = tabs.length - 1
|
|
||||||
while (i > -1) {
|
|
||||||
if (this.selectedInquiriesIds.has(tabs[i].id)) {
|
|
||||||
this.$store.commit('deleteTab', tabs[i])
|
|
||||||
}
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear checkboxes
|
// Clear checkboxes
|
||||||
this.selectedInquiriesIds.clear()
|
this.selectedInquiriesIds.clear()
|
||||||
}
|
}
|
||||||
this.selectedInquiriesCount = this.selectedInquiriesIds.size
|
this.selectedInquiriesCount = this.selectedInquiriesIds.size
|
||||||
storedInquiries.updateStorage(this.inquiries)
|
|
||||||
},
|
},
|
||||||
exportToFile(inquiryList, fileName) {
|
exportToFile(inquiryList, fileName) {
|
||||||
storedInquiries.export(inquiryList, fileName)
|
storedInquiries.export(inquiryList, fileName)
|
||||||
},
|
},
|
||||||
exportSelectedInquiries() {
|
exportSelectedInquiries() {
|
||||||
const inquiryList = this.allInquiries.filter(
|
const inquiryList = this.allInquiries.filter(inquiry =>
|
||||||
inquiry => this.selectedInquiriesIds.has(inquiry.id)
|
this.selectedInquiriesIds.has(inquiry.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
this.exportToFile(inquiryList, 'My sqliteviz inquiries.json')
|
this.exportToFile(inquiryList, 'My sqliteviz inquiries.json')
|
||||||
},
|
},
|
||||||
|
|
||||||
importInquiries() {
|
importInquiries() {
|
||||||
storedInquiries.importInquiries()
|
storedInquiries.importInquiries().then(importedInquiries => {
|
||||||
.then(importedInquiries => {
|
this.$store.commit(
|
||||||
this.inquiries = this.inquiries.concat(importedInquiries)
|
'setInquiries',
|
||||||
storedInquiries.updateStorage(this.inquiries)
|
this.inquiries.concat(importedInquiries)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleSelectAll(checked) {
|
toggleSelectAll(checked) {
|
||||||
this.selectAll = checked
|
this.selectAll = checked
|
||||||
this.$refs.rowCheckBox.forEach(item => { item.checked = checked })
|
this.$refs.rowCheckBox.forEach(item => {
|
||||||
|
item.checked = checked
|
||||||
|
})
|
||||||
|
|
||||||
this.selectedInquiriesIds = checked
|
this.selectedInquiriesIds = checked
|
||||||
? new Set(this.showedInquiries.map(inquiry => inquiry.id))
|
? new Set(this.showedInquiries.map(inquiry => inquiry.id))
|
||||||
@@ -429,8 +452,9 @@ export default {
|
|||||||
|
|
||||||
this.selectedInquiriesCount = this.selectedInquiriesIds.size
|
this.selectedInquiriesCount = this.selectedInquiriesIds.size
|
||||||
this.selectedNotPredefinedCount = checked
|
this.selectedNotPredefinedCount = checked
|
||||||
? ([...this.selectedInquiriesIds].filter(id => !this.predefinedInquiriesIds.has(id)))
|
? [...this.selectedInquiriesIds].filter(
|
||||||
.length
|
id => !this.predefinedInquiriesIds.has(id)
|
||||||
|
).length
|
||||||
: 0
|
: 0
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
fill="#A2B1C6"
|
fill="#A2B1C6"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
<span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
|
||||||
Duplicate inquiry
|
Duplicate inquiry
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -32,6 +32,7 @@ import tooltipMixin from '@/tooltipMixin'
|
|||||||
export default {
|
export default {
|
||||||
name: 'CopyIcon',
|
name: 'CopyIcon',
|
||||||
mixins: [tooltipMixin],
|
mixins: [tooltipMixin],
|
||||||
|
emits: ['click'],
|
||||||
methods: {
|
methods: {
|
||||||
onClick() {
|
onClick() {
|
||||||
this.hideTooltip()
|
this.hideTooltip()
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user