├── .browserslistrc
├── .editorconfig
├── .eslintrc.cjs
├── .github
└── workflows
│ ├── config.grenrc.cjs
│ ├── main.yml
│ └── test.yml
├── .gitignore
├── .prettierrc
├── Dockerfile.test
├── LICENSE
├── README.md
├── babel.config.js
├── index.html
├── jsconfig.json
├── karma.conf.cjs
├── lib
└── sql-js
│ ├── .dockerignore
│ ├── Dockerfile
│ ├── README.md
│ ├── benchmark
│ ├── .gitignore
│ ├── Dockerfile
│ ├── README.md
│ ├── karma.conf.js
│ ├── lib
│ │ └── package.json
│ ├── make.sh
│ ├── package.json
│ ├── procpath
│ │ ├── karma_docker.procpath
│ │ ├── top2_cpu.sql
│ │ └── top2_rss.sql
│ ├── result-analysis.ipynb
│ └── suite.js
│ ├── build.py
│ ├── configure.py
│ ├── dist
│ ├── sql-wasm.js
│ └── sql-wasm.wasm
│ ├── make.sh
│ └── package.json
├── package-lock.json
├── package.json
├── public
├── Logo192x192.png
├── Logo48x48.png
├── Logo512x512.png
├── favicon.png
├── inquiries.json
└── manifest.webmanifest
├── src
├── App.vue
├── assets
│ ├── fonts
│ │ ├── OpenSans-Bold.woff2
│ │ ├── OpenSans-BoldItalic.woff2
│ │ ├── OpenSans-Italic.woff2
│ │ ├── OpenSans-Regular.woff2
│ │ ├── OpenSans-SemiBold.woff2
│ │ └── OpenSans-SemiBoldItalic.woff2
│ ├── images
│ │ ├── Logo.svg
│ │ ├── Screenshot_editor.png
│ │ ├── arrow-hover.svg
│ │ ├── arrow.svg
│ │ ├── body.svg
│ │ ├── bottom.svg
│ │ ├── checkbox_checked.svg
│ │ ├── checkbox_checked_disabled.svg
│ │ ├── checkbox_checked_light.svg
│ │ ├── chevron.svg
│ │ ├── close.svg
│ │ ├── copy.svg
│ │ ├── database-edit.svg
│ │ ├── delete-tag-hover.svg
│ │ ├── delete-tag.svg
│ │ ├── delete.svg
│ │ ├── error.svg
│ │ ├── file-export.svg
│ │ ├── file.png
│ │ ├── info.svg
│ │ ├── leftArm.svg
│ │ ├── logo_simple.svg
│ │ ├── rename.svg
│ │ ├── rightArm.svg
│ │ ├── sort.svg
│ │ ├── success.svg
│ │ └── top.svg
│ └── styles
│ │ ├── buttons.css
│ │ ├── dialogs.css
│ │ ├── messages.css
│ │ ├── multiselect.css
│ │ ├── scrollbars.css
│ │ ├── tables.css
│ │ ├── tooltips.css
│ │ └── variables.css
├── components
│ ├── CheckBox.vue
│ ├── CsvJsonImport
│ │ ├── DelimiterSelector
│ │ │ ├── ascii.js
│ │ │ └── index.vue
│ │ └── index.vue
│ ├── DbUploader.vue
│ ├── IconButton.vue
│ ├── LoadingDialog.vue
│ ├── LoadingIndicator.vue
│ ├── Logs.vue
│ ├── Splitpanes
│ │ ├── index.vue
│ │ └── splitter.js
│ ├── SqlTable
│ │ ├── Pager.vue
│ │ └── index.vue
│ ├── TextField.vue
│ └── svg
│ │ ├── addTable.vue
│ │ ├── arrow.vue
│ │ ├── changeDb.vue
│ │ ├── chart.vue
│ │ ├── clear.vue
│ │ ├── clipboard.vue
│ │ ├── close.vue
│ │ ├── dataView.vue
│ │ ├── dropDownChevron.vue
│ │ ├── edgeArrow.vue
│ │ ├── export.vue
│ │ ├── exportToCsv.vue
│ │ ├── exportToSvg.vue
│ │ ├── hint.vue
│ │ ├── html.vue
│ │ ├── pivot.vue
│ │ ├── png.vue
│ │ ├── row.vue
│ │ ├── run.vue
│ │ ├── sort.vue
│ │ ├── sqlEditor.vue
│ │ ├── table.vue
│ │ ├── treeChevron.vue
│ │ └── viewCellValue.vue
├── lib
│ ├── ReactPlotlyEditorWithPlotRef.jsx
│ ├── chartHelper.js
│ ├── csv.js
│ ├── database
│ │ ├── _sql.js
│ │ ├── _statements.js
│ │ ├── _worker.js
│ │ └── index.js
│ ├── eventBus.js
│ ├── storedInquiries
│ │ ├── _migrations.js
│ │ └── index.js
│ ├── tab.js
│ └── utils
│ │ ├── clipboardIo.js
│ │ ├── events.js
│ │ ├── fileIo.js
│ │ └── time.js
├── main.js
├── registerServiceWorker.js
├── router.js
├── store
│ ├── actions.js
│ ├── index.js
│ ├── mutations.js
│ └── state.js
├── tooltipMixin.js
└── views
│ ├── LoadView.vue
│ ├── MainView
│ ├── AppDiagnosticInfo.vue
│ ├── Inquiries
│ │ ├── index.vue
│ │ └── svg
│ │ │ ├── copy.vue
│ │ │ ├── delete.vue
│ │ │ └── rename.vue
│ ├── MainMenu.vue
│ ├── Workspace
│ │ ├── Schema
│ │ │ ├── TableDescription.vue
│ │ │ └── index.vue
│ │ ├── Tabs
│ │ │ ├── Tab
│ │ │ │ ├── DataView
│ │ │ │ │ ├── Chart
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── Pivot
│ │ │ │ │ │ ├── PivotUi
│ │ │ │ │ │ │ ├── PivotSortBtn.vue
│ │ │ │ │ │ │ └── index.vue
│ │ │ │ │ │ ├── index.vue
│ │ │ │ │ │ └── pivotHelper.js
│ │ │ │ │ └── index.vue
│ │ │ │ ├── RunResult
│ │ │ │ │ ├── Record
│ │ │ │ │ │ ├── RowNavigator.vue
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── ValueViewer.vue
│ │ │ │ │ └── index.vue
│ │ │ │ ├── SideToolBar.vue
│ │ │ │ ├── SqlEditor
│ │ │ │ │ ├── hint.js
│ │ │ │ │ └── index.vue
│ │ │ │ └── index.vue
│ │ │ └── index.vue
│ │ └── index.vue
│ └── index.vue
│ └── Welcome.vue
├── test.setup.js
├── tests
├── App.spec.js
├── components
│ ├── CheckBox.spec.js
│ ├── CsvImport
│ │ ├── CsvImport.spec.js
│ │ └── DelimiterSelector.spec.js
│ ├── DbUploader.spec.js
│ ├── LoadingIndicator.spec.js
│ ├── Logs.spec.js
│ ├── Splitpanes
│ │ ├── Splitpanes.spec.js
│ │ └── splitter.spec.js
│ └── SqlTable
│ │ └── Pager.spec.js
├── lib
│ ├── chartHelper.spec.js
│ ├── csv.spec.js
│ ├── database
│ │ ├── _sql.spec.js
│ │ ├── _statements.spec.js
│ │ ├── database.spec.js
│ │ └── sqliteExtensions.spec.js
│ ├── storedInquiries
│ │ ├── _migrations.spec.js
│ │ └── storedInquiries.spec.js
│ ├── tab.spec.js
│ └── utils
│ │ ├── clipboardIo.spec.js
│ │ ├── fileIo.spec.js
│ │ └── time.spec.js
├── store
│ ├── actions.spec.js
│ └── mutations.spec.js
├── tooltipMixin.spec.js
└── views
│ ├── LoadView.spec.js
│ └── MainView
│ ├── Inquiries
│ └── Inquiries.spec.js
│ ├── MainMenu.spec.js
│ └── Workspace
│ ├── Schema
│ ├── Schema.spec.js
│ └── TableDescription.spec.js
│ ├── Tabs
│ ├── Tab
│ │ ├── DataView
│ │ │ ├── Chart
│ │ │ │ └── Chart.spec.js
│ │ │ ├── DataView.spec.js
│ │ │ └── Pivot
│ │ │ │ ├── Pivot.spec.js
│ │ │ │ ├── PivotUi
│ │ │ │ ├── PivotSortBtn.spec.js
│ │ │ │ └── PivotUi.spec.js
│ │ │ │ └── pivotHelper.spec.js
│ │ ├── RunResult
│ │ │ ├── Record.spec.js
│ │ │ ├── RunResult.spec.js
│ │ │ └── ValueViewer.spec.js
│ │ ├── SqlEditor
│ │ │ ├── SqlEditor.spec.js
│ │ │ └── hint.spec.js
│ │ └── Tab.spec.js
│ └── Tabs.spec.js
│ └── Workspace.spec.js
├── vite.config.js
└── vue.config.js
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not dead
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,ts,tsx,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | trim_trailing_whitespace = true
5 | insert_final_newline = true
6 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | es2022: true
6 | },
7 | extends: ['eslint:recommended', 'plugin:vue/vue3-recommended', 'prettier'],
8 | rules: {
9 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
10 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
11 | 'no-case-declarations': 'off',
12 | 'max-len': [2, 100, 4, { ignoreUrls: true }],
13 | 'vue/multi-word-component-names': 'off',
14 | 'vue/no-mutating-props': 'warn',
15 | 'vue/no-reserved-component-names': 'warn',
16 | 'vue/no-v-model-argument': 'off',
17 | 'vue/require-default-prop': 'off',
18 | 'vue/custom-event-name-casing': ['error', 'camelCase'],
19 | 'vue/attribute-hyphenation': ['error', 'never']
20 | },
21 | overrides: [
22 | {
23 | files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/**/*.spec.{j,t}s?(x)'],
24 | env: {
25 | mocha: true
26 | }
27 | }
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/.github/workflows/config.grenrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | dataSource: 'milestones',
3 | ignoreIssuesWith: ['wontfix', 'duplicate'],
4 | milestoneMatch: 'v{{tag_name}}',
5 | template: {
6 | issue: '- {{name}} [{{text}}]({{url}})',
7 | changelogTitle: '',
8 | release: '{{body}}'
9 | },
10 | groupBy: {
11 | Enhancements: ['enhancement', 'internal'],
12 | 'Bug fixes': ['bug']
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Create release
2 | on:
3 | workflow_dispatch:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 | deploy:
10 | name: Create release
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Use Node.js
15 | uses: actions/setup-node@v1
16 | with:
17 | node-version: 18.x
18 |
19 | - name: Update npm
20 | run: npm install -g npm@10
21 |
22 | - name: npm install and build
23 | run: |
24 | npm install
25 | npm run build
26 |
27 | - name: Create archives
28 | run: |
29 | cd dist
30 | zip -9 -r ../dist.zip . -x "*.map"
31 | zip -9 -r ../dist_map.zip .
32 |
33 | - name: Create Release Notes
34 | run: |
35 | npm install github-release-notes@0.16.0 -g
36 | gren changelog --generate --config="/.github/workflows/config.grenrc.cjs"
37 | env:
38 | GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39 |
40 | - name: Create release
41 | uses: ncipollo/release-action@v1
42 | with:
43 | artifacts: 'dist.zip,dist_map.zip'
44 | token: ${{ secrets.GITHUB_TOKEN }}
45 | bodyFile: 'CHANGELOG.md'
46 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Run tests
2 | on:
3 | workflow_dispatch:
4 | push:
5 | branches:
6 | - 'master'
7 | pull_request:
8 | branches:
9 | - 'master'
10 |
11 | jobs:
12 | test:
13 | name: Run tests
14 | runs-on: ubuntu-20.04
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Use Node.js
18 | uses: actions/setup-node@v1
19 | with:
20 | node-version: 18.x
21 | - name: Install browsers
22 | run: |
23 | export DEBIAN_FRONTEND=noninteractive
24 | sudo apt-get update
25 | sudo apt-get install -y chromium-browser firefox
26 |
27 | - name: Update npm
28 | run: npm install -g npm@10
29 |
30 | - name: Install the project
31 | run: npm install
32 |
33 | - name: Run lint
34 | run: npm run lint -- --no-fix
35 |
36 | - name: Run karma tests
37 | run: npm run test
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 | /coverage
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "tabWidth": 2,
4 | "semi": false,
5 | "singleQuote": true,
6 | "arrowParens": "avoid"
7 | }
8 |
--------------------------------------------------------------------------------
/Dockerfile.test:
--------------------------------------------------------------------------------
1 | # An easy way to run tests locally without Nodejs installed:
2 | #
3 | # docker build -t sqliteviz/test -f Dockerfile.test .
4 | #
5 |
6 | FROM node:12.22-buster
7 |
8 | RUN set -ex; \
9 | apt update; \
10 | apt install -y chromium firefox-esr; \
11 | npm install -g npm@7
12 |
13 | WORKDIR /tmp/build
14 |
15 | COPY package.json package-lock.json ./
16 | COPY lib lib
17 | RUN npm install
18 |
19 | COPY . .
20 |
21 | RUN set -ex; \
22 | sed -i 's/browsers: \[.*\],/browsers: ['"'FirefoxHeadlessTouch'"'],/' karma.conf.js
23 |
24 | RUN npm run lint -- --no-fix && npm run test
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # sqliteviz
6 |
7 | Sqliteviz is a single-page offline-first PWA for fully client-side visualisation
8 | of SQLite databases, CSV, JSON or NDJSON files.
9 |
10 | With sqliteviz you can:
11 |
12 | - run SQL queries against a SQLite database and create [Plotly][11] charts and pivot tables based on the result sets
13 | - import a CSV/JSON/NDJSON file into a SQLite database and visualize imported data
14 | - export result set to CSV file
15 | - manage inquiries and run them against different databases
16 | - import/export inquiries from/to a JSON file
17 | - export a modified SQLite database
18 | - use it offline from your OS application menu like any other desktop app
19 |
20 | https://user-images.githubusercontent.com/24638357/128249848-f8fab0f5-9add-46e0-a9c1-dd5085a8623e.mp4
21 |
22 | ## Quickstart
23 |
24 | The latest release of sqliteviz is deployed on [sqliteviz.com/app][6].
25 |
26 | ## Wiki
27 |
28 | For user documentation, check out sqliteviz [documentation][7].
29 |
30 | ## Motivation
31 |
32 | It's a kind of middleground between [Plotly Falcon][1] and [Redash][2].
33 |
34 | ## Components
35 |
36 | 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].
37 |
38 | [1]: https://github.com/plotly/falcon
39 | [2]: https://github.com/getredash/redash
40 | [3]: https://github.com/plotly/react-chart-editor
41 | [4]: https://github.com/sql-js/sql.js
42 | [5]: https://github.com/vuejs/vue
43 | [6]: https://sqliteviz.com/app/
44 | [7]: https://sqliteviz.com/docs
45 | [8]: https://github.com/surmon-china/vue-codemirror#readme
46 | [9]: https://www.papaparse.com/
47 | [10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
48 | [11]: https://github.com/plotly/plotly.js
49 | [12]: https://github.com/nicolaskruchten/pivottable
50 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@vue/cli-plugin-babel/preset']
3 | }
4 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | sqliteviz
10 |
75 |
76 |
77 |
78 |
79 |
85 |
86 |
87 |
LOADING
88 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src/**/*", "tests/**/*"],
3 | "exclude": ["node_modules", "dist"],
4 | "compilerOptions": {
5 | "baseUrl": "./",
6 | "paths": {
7 | "@*": ["./src/*"]
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/karma.conf.cjs:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | config.set({
3 | vite: {
4 | config: {
5 | resolve: {
6 | alias: {
7 | vue: 'vue/dist/vue.esm-bundler.js'
8 | }
9 | },
10 | server: {
11 | preTransformRequests: false
12 | }
13 | },
14 | coverage: {
15 | enable: true,
16 | include: 'src/*',
17 | exclude: ['node_modules', 'src/components/svg/*'],
18 | extension: ['.js', '.vue'],
19 | requireEnv: false
20 | }
21 | },
22 | // base path that will be used to resolve all patterns (eg. files, exclude)
23 | basePath: '',
24 |
25 | // frameworks to use
26 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
27 | frameworks: ['mocha', 'sinon-chai', 'vite'],
28 |
29 | // list of files / patterns to load in the browser
30 | files: [
31 | {
32 | pattern: 'test.setup.js',
33 | type: 'module',
34 | watched: false,
35 | served: false
36 | },
37 | {
38 | pattern: 'tests/**/*.spec.js',
39 | type: 'module',
40 | watched: false,
41 | served: false
42 | },
43 | {
44 | pattern: 'src/assets/styles/*.css',
45 | type: 'css',
46 | watched: false,
47 | served: false
48 | }
49 | ],
50 |
51 | plugins: [
52 | 'karma-vite',
53 | 'karma-mocha',
54 | 'karma-sinon-chai',
55 | 'karma-firefox-launcher',
56 | 'karma-chrome-launcher',
57 | 'karma-spec-reporter',
58 | 'karma-coverage'
59 | ],
60 |
61 | // list of files / patterns to exclude
62 | exclude: [],
63 |
64 | // test results reporter to use
65 | // possible values: 'dots', 'progress'
66 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
67 | reporters: ['spec', 'coverage'],
68 |
69 | coverageReporter: {
70 | dir: 'coverage',
71 | reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }]
72 | },
73 |
74 | // web server port
75 | port: 9876,
76 |
77 | // enable / disable colors in the output (reporters and logs)
78 | colors: true,
79 |
80 | // level of logging
81 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN ||
82 | // config.LOG_INFO || config.LOG_DEBUG
83 | logLevel: config.LOG_INFO,
84 |
85 | // enable / disable watching file and executing tests whenever any file changes
86 | autoWatch: false,
87 |
88 | customLaunchers: {
89 | FirefoxHeadlessTouch: {
90 | base: 'FirefoxHeadless',
91 | prefs: {
92 | 'dom.w3c_touch_events.enabled': 1,
93 | 'dom.events.asyncClipboard.clipboardItem': true
94 | }
95 | }
96 | },
97 | // start these browsers
98 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
99 | browsers: ['ChromiumHeadless', 'FirefoxHeadlessTouch'],
100 |
101 | // Continuous Integration mode
102 | // if true, Karma captures browsers, runs the tests and exits
103 | singleRun: true,
104 |
105 | // Concurrency level
106 | // how many browser should be started simultaneous
107 | concurrency: 2,
108 |
109 | client: {
110 | captureConsole: true,
111 | mocha: {
112 | timeout: 7000
113 | }
114 | },
115 | browserConsoleLogOptions: {
116 | terminal: true,
117 | level: ''
118 | }
119 | })
120 | // Fix the timezone
121 | process.env.TZ = 'Europe/Amsterdam'
122 | }
123 |
--------------------------------------------------------------------------------
/lib/sql-js/.dockerignore:
--------------------------------------------------------------------------------
1 | benchmark
2 | dist
3 |
--------------------------------------------------------------------------------
/lib/sql-js/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM emscripten/emsdk:3.0.1
2 |
3 | WORKDIR /tmp/build
4 |
5 | COPY configure.py .
6 | RUN python3.8 configure.py
7 |
8 | COPY build.py .
9 | RUN python3.8 build.py
10 |
--------------------------------------------------------------------------------
/lib/sql-js/benchmark/.gitignore:
--------------------------------------------------------------------------------
1 | /lib/build-*
2 | /lib/dist
3 | /build-*-result.json
4 | /sample.csv
5 |
--------------------------------------------------------------------------------
/lib/sql-js/benchmark/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20.14-bookworm
2 |
3 | RUN set -ex; \
4 | apt-get update; \
5 | apt-get install -y firefox-esr; \
6 | apt-get install -y chromium
7 |
8 | WORKDIR /tmp/build
9 |
10 | COPY package.json ./
11 | COPY lib/dist lib/dist
12 | COPY lib/package.json lib/package.json
13 | RUN npm install
14 |
15 | COPY . .
16 |
17 | CMD npm run benchmark
18 |
--------------------------------------------------------------------------------
/lib/sql-js/benchmark/README.md:
--------------------------------------------------------------------------------
1 | # SQLite WebAssembly build micro-benchmark
2 |
3 | This directory contains a micro-benchmark for evaluating SQLite WebAssembly
4 | builds performance on read and write SQL queries, run from `make.sh` script. If
5 | the script has permission to `nice` processes and [Procpath][1] is installed,
6 | e.g. it is run with `sudo -E env PATH=$PATH ./make.sh`, it'll `renice` all
7 | processes running inside the benchmark containers. It can also serve as a smoke
8 | test (e.g. for memory leaks).
9 |
10 | The benchmark operates on a set of SQLite WebAssembly builds expected in
11 | `lib/build-$NAME` directories each containing `sql-wasm.js` and
12 | `sql-wasm.wasm`. Then it creates a Docker image for each, and runs the
13 | benchmark in Firefox and Chromium using Karma in the container.
14 |
15 | After successful run, the benchmark produces the following per each build:
16 |
17 | - `build-$NAME-result.json`
18 | - `build-$NAME.sqlite` (if Procpath is installed)
19 | - `build-$NAME.svg` (if Procpath is installed)
20 |
21 | These files can be analysed using `result-analysis.ipynb` Jupyter notebook.
22 | The SVG is a chart with CPU and RSS usage of each test container (i.e. Chromium
23 | run, then Firefox run per container).
24 |
25 | [1]: https://pypi.org/project/Procpath/
26 |
--------------------------------------------------------------------------------
/lib/sql-js/benchmark/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | const timeout = 15 * 60 * 1000
3 | config.set({
4 | frameworks: ['mocha'],
5 |
6 | files: [
7 | 'suite.js',
8 | {
9 | pattern: 'node_modules/sql.js/dist/sql-wasm.wasm',
10 | served: true,
11 | included: false
12 | },
13 | { pattern: 'sample.csv', served: true, included: false }
14 | ],
15 |
16 | reporters: ['progress', 'json-to-file'],
17 |
18 | singleRun: true,
19 |
20 | customLaunchers: {
21 | ChromiumHeadlessNoSandbox: {
22 | base: 'ChromiumHeadless',
23 | flags: ['--no-sandbox']
24 | }
25 | },
26 | browsers: ['ChromiumHeadlessNoSandbox', 'FirefoxHeadless'],
27 | concurrency: 1,
28 |
29 | browserDisconnectTimeout: timeout,
30 | browserNoActivityTimeout: timeout,
31 | captureTimeout: timeout,
32 | browserSocketTimeout: timeout,
33 | pingTimeout: timeout,
34 | client: {
35 | captureConsole: true,
36 | mocha: { timeout: timeout }
37 | },
38 |
39 | logLevel: config.LOG_INFO,
40 | browserConsoleLogOptions: { terminal: true, level: config.LOG_INFO },
41 |
42 | preprocessors: { 'suite.js': ['webpack'] },
43 | webpack: {
44 | mode: 'development',
45 | module: {
46 | noParse: [__dirname + '/node_modules/benchmark/benchmark.js']
47 | },
48 | node: { fs: 'empty' }
49 | },
50 |
51 | proxies: {
52 | '/sql-wasm.wasm': '/base/node_modules/sql.js/dist/sql-wasm.wasm'
53 | },
54 |
55 | jsonToFileReporter: { outputPath: '.', fileName: 'suite-result.json' }
56 | })
57 | }
58 |
--------------------------------------------------------------------------------
/lib/sql-js/benchmark/lib/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sql.js",
3 | "main": "./dist/sql-wasm.js",
4 | "private": true
5 | }
6 |
--------------------------------------------------------------------------------
/lib/sql-js/benchmark/make.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | cleanup () {
4 | rm -rf lib/dist "$renice_flag_file"
5 | docker rm -f sqljs-benchmark-run 2> /dev/null || true
6 | }
7 | trap cleanup EXIT
8 |
9 | if [ ! -f sample.csv ]; then
10 | wget --header="accept-encoding: gzip" -q -O- \
11 | https://github.com/plotly/datasets/raw/547090bd/wellspublic.csv \
12 | | gunzip -c > sample.csv
13 | fi
14 |
15 | PLAYBOOK=procpath/karma_docker.procpath
16 |
17 | # for renice to work run like "sudo -E env PATH=$PATH ./make.sh"
18 | test_ni=$(nice -n -5 nice)
19 | if [ $test_ni == -5 ]; then
20 | renice_flag_file=$(mktemp)
21 | fi
22 | {
23 | while [ -f $renice_flag_file ]; do
24 | procpath --logging-level ERROR play -f $PLAYBOOK renice:watch
25 | done
26 | } &
27 |
28 | shopt -s nullglob
29 | for d in lib/build-* ; do
30 | rm -rf lib/dist
31 | cp -r $d lib/dist
32 | sample_name=$(basename $d)
33 |
34 | docker build -t sqliteviz/sqljs-benchmark .
35 | docker rm sqljs-benchmark-run 2> /dev/null || true
36 | docker run -d -it --cpus 2 --name sqljs-benchmark-run sqliteviz/sqljs-benchmark
37 | {
38 | rm -f ${sample_name}.sqlite
39 | procpath play -f $PLAYBOOK -o database_file=${sample_name}.sqlite track:record
40 | procpath play -f $PLAYBOOK -o database_file=${sample_name}.sqlite \
41 | -o plot_file=${sample_name}.svg track:plot
42 | } &
43 |
44 | docker attach sqljs-benchmark-run
45 | docker cp sqljs-benchmark-run:/tmp/build/suite-result.json ${sample_name}-result.json
46 | docker rm sqljs-benchmark-run
47 | done
48 |
--------------------------------------------------------------------------------
/lib/sql-js/benchmark/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sqlite-webassembly-microbenchmark",
3 | "private": true,
4 | "dependencies": {
5 | "@babel/core": "^7.14.8",
6 | "babel-loader": "^8.2.2",
7 | "benchmark": "^2.1.4",
8 | "lodash": "^4.17.4",
9 | "papaparse": "^5.3.1",
10 | "mocha": "^9.0.3",
11 | "karma": "^6.3.4",
12 | "karma-chrome-launcher": "^3.1.0",
13 | "karma-firefox-launcher": "^2.1.1",
14 | "karma-json-to-file-reporter": "^1.0.1",
15 | "karma-mocha": "^2.0.1",
16 | "karma-webpack": "^4.0.2",
17 | "webpack": "^4.46.0",
18 | "sql.js": "file:./lib"
19 | },
20 | "scripts": {
21 | "benchmark": "karma start karma.conf.js"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/sql-js/benchmark/procpath/karma_docker.procpath:
--------------------------------------------------------------------------------
1 | # This command may run when "sqljs-benchmark-run" does not yet exist or run
2 | [renice:watch]
3 | interval: 2
4 | repeat: 30
5 | environment:
6 | ROOT_PID=docker inspect -f "{{.State.Pid}}" sqljs-benchmark-run 2> /dev/null || true
7 | query:
8 | PIDS=$..children[?(@.stat.pid in [$ROOT_PID])]..pid
9 | command:
10 | echo $PIDS | tr , '\n' | xargs --no-run-if-empty -I{} -- renice -n -5 -p {}
11 |
12 | # Expected input arguments: database_file
13 | [track:record]
14 | interval: 1
15 | stop_without_result: 1
16 | environment:
17 | ROOT_PID=docker inspect -f "{{.State.Pid}}" sqljs-benchmark-run
18 | query:
19 | $..children[?(@.stat.pid == $ROOT_PID)]
20 | pid_list: $ROOT_PID
21 |
22 | # Expected input arguments: database_file, plot_file
23 | [track:plot]
24 | moving_average_window: 5
25 | title: Chromium vs Firefox (№1 RSS, №2 CPU)
26 | custom_query_file:
27 | procpath/top2_rss.sql
28 | procpath/top2_cpu.sql
29 |
--------------------------------------------------------------------------------
/lib/sql-js/benchmark/procpath/top2_cpu.sql:
--------------------------------------------------------------------------------
1 | WITH diff_all AS (
2 | SELECT
3 | record_id,
4 | ts,
5 | stat_pid,
6 | stat_utime + stat_stime - LAG(stat_utime + stat_stime) OVER (
7 | PARTITION BY stat_pid
8 | ORDER BY record_id
9 | ) tick_diff,
10 | ts - LAG(ts) OVER (
11 | PARTITION BY stat_pid
12 | ORDER BY record_id
13 | ) ts_diff
14 | FROM record
15 | ), diff AS (
16 | SELECT * FROM diff_all WHERE tick_diff IS NOT NULL
17 | ), one_time_pid_condition AS (
18 | SELECT stat_pid
19 | FROM record
20 | GROUP BY 1
21 | ORDER BY SUM(stat_utime + stat_stime) DESC
22 | LIMIT 2
23 | )
24 | SELECT
25 | ts,
26 | stat_pid pid,
27 | 100.0 * tick_diff / (SELECT value FROM meta WHERE key = 'clock_ticks') / ts_diff value
28 | FROM diff
29 | JOIN one_time_pid_condition USING(stat_pid)
30 |
--------------------------------------------------------------------------------
/lib/sql-js/benchmark/procpath/top2_rss.sql:
--------------------------------------------------------------------------------
1 | WITH one_time_pid_condition AS (
2 | SELECT stat_pid
3 | FROM record
4 | GROUP BY 1
5 | ORDER BY SUM(stat_rss) DESC
6 | LIMIT 2
7 | )
8 | SELECT
9 | ts,
10 | stat_pid pid,
11 | stat_rss / 1024.0 / 1024 * (SELECT value FROM meta WHERE key = 'page_size') value
12 | FROM record
13 | JOIN one_time_pid_condition USING(stat_pid)
14 |
--------------------------------------------------------------------------------
/lib/sql-js/build.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import subprocess
3 | from pathlib import Path
4 |
5 | # See the setting descriptions on these pages:
6 | # - https://emscripten.org/docs/optimizing/Optimizing-Code.html
7 | # - https://github.com/emscripten-core/emscripten/blob/main/src/settings.js
8 | cflags = (
9 | # SQLite configuration
10 | '-DSQLITE_DEFAULT_CACHE_SIZE=-65536', # 64 MiB
11 | '-DSQLITE_DEFAULT_MEMSTATUS=0',
12 | '-DSQLITE_DEFAULT_SYNCHRONOUS=0',
13 | '-DSQLITE_DISABLE_LFS',
14 | '-DSQLITE_DQS=0',
15 | '-DSQLITE_ENABLE_FTS3',
16 | '-DSQLITE_ENABLE_FTS3_PARENTHESIS',
17 | '-DSQLITE_ENABLE_FTS5',
18 | '-DSQLITE_ENABLE_NORMALIZE',
19 | '-DSQLITE_EXTRA_INIT=extra_init',
20 | '-DSQLITE_OMIT_DEPRECATED',
21 | '-DSQLITE_OMIT_LOAD_EXTENSION',
22 | '-DSQLITE_OMIT_SHARED_CACHE',
23 | '-DSQLITE_THREADSAFE=0',
24 | # Compile-time optimisation
25 | '-Os', # reduces the code size about in half comparing to -O2
26 | '-flto',
27 | '-Isrc', '-Isrc/lua',
28 | )
29 | emflags = (
30 | # Base
31 | '--memory-init-file', '0',
32 | '-s', 'ALLOW_TABLE_GROWTH=1',
33 | # WASM
34 | '-s', 'WASM=1',
35 | '-s', 'ALLOW_MEMORY_GROWTH=1',
36 | '-s', 'ENVIRONMENT=web,worker',
37 | # Link-time optimisation
38 | '-Os',
39 | '-flto',
40 | # sql.js
41 | '-s', 'EXPORTED_FUNCTIONS=@src/sqljs/exported_functions.json',
42 | '-s', 'EXPORTED_RUNTIME_METHODS=@src/sqljs/exported_runtime_methods.json',
43 | '--pre-js', 'src/sqljs/api.js',
44 | )
45 |
46 |
47 | def build(src: Path, dst: Path):
48 | out = Path('out')
49 | out.mkdir()
50 |
51 | logging.info('Building LLVM bitcode for sqlite3.c')
52 | subprocess.check_call([
53 | 'emcc',
54 | *cflags,
55 | '-c', src / 'sqlite3.c',
56 | '-o', out / 'sqlite3.o',
57 | ])
58 | logging.info('Building LLVM bitcode for extension-functions.c')
59 | subprocess.check_call([
60 | 'emcc',
61 | *cflags,
62 | '-c', src / 'extension-functions.c',
63 | '-o', out / 'extension-functions.o',
64 | ])
65 | logging.info('Building LLVM bitcode for SQLite Lua extension')
66 | subprocess.check_call([
67 | 'emcc',
68 | *cflags,
69 | '-shared',
70 | *(src / 'lua').glob('*.c'),
71 | *(src / 'sqlitelua').glob('*.c'),
72 | '-o', out / 'sqlitelua.o',
73 | ])
74 |
75 | logging.info('Building WASM from bitcode')
76 | subprocess.check_call([
77 | 'emcc',
78 | *emflags,
79 | out / 'sqlite3.o',
80 | out / 'extension-functions.o',
81 | out / 'sqlitelua.o',
82 | '-o', out / 'sql-wasm.js',
83 | ])
84 |
85 | logging.info('Post-processing build and copying to dist')
86 | (out / 'sql-wasm.wasm').rename(dst / 'sql-wasm.wasm')
87 | with (dst / 'sql-wasm.js').open('w') as f:
88 | f.write((src / 'sqljs' / 'shell-pre.js').read_text())
89 | f.write((out / 'sql-wasm.js').read_text())
90 | f.write((src / 'sqljs' / 'shell-post.js').read_text())
91 |
92 |
93 | if __name__ == '__main__':
94 | logging.basicConfig(level='INFO', format='%(asctime)s %(levelname)s %(name)s %(message)s')
95 |
96 | src = Path('src')
97 | dst = Path('dist')
98 | dst.mkdir()
99 | build(src, dst)
100 |
--------------------------------------------------------------------------------
/lib/sql-js/dist/sql-wasm.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lana-k/sqliteviz/3ee825defe91e240a996f308df2eb4ed4d0621e1/lib/sql-js/dist/sql-wasm.wasm
--------------------------------------------------------------------------------
/lib/sql-js/make.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | docker build -t sqliteviz/sqljs .
4 |
5 | rm -r dist || true
6 |
7 | CONTAINER=$(docker create sqliteviz/sqljs)
8 | docker cp $CONTAINER:/tmp/build/dist .
9 | docker rm $CONTAINER
10 |
--------------------------------------------------------------------------------
/lib/sql-js/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sql.js",
3 | "main": "./dist/sql-wasm.js",
4 | "private": true
5 | }
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sqliteviz",
3 | "version": "0.26.0",
4 | "license": "Apache-2.0",
5 | "private": true,
6 | "type": "module",
7 | "scripts": {
8 | "dev": "vite",
9 | "build": "vite build",
10 | "serve": "vite preview",
11 | "test": "karma start karma.conf.cjs",
12 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
13 | "format": "prettier . --write"
14 | },
15 | "dependencies": {
16 | "buffer": "^6.0.3",
17 | "codemirror": "^5.65.18",
18 | "codemirror-editor-vue3": "^2.8.0",
19 | "core-js": "^3.6.5",
20 | "dataurl-to-blob": "^0.0.1",
21 | "html2canvas": "^1.1.4",
22 | "jquery": "^3.6.0",
23 | "nanoid": "^3.1.12",
24 | "papaparse": "^5.4.1",
25 | "pivottable": "^2.23.0",
26 | "plotly.js": "^2.35.2",
27 | "promise-worker": "^2.0.1",
28 | "react": "^16.14.0",
29 | "react-chart-editor": "^0.46.1",
30 | "react-dom": "^16.14.0",
31 | "sql.js": "file:./lib/sql-js",
32 | "tiny-emitter": "^2.1.0",
33 | "veaury": "^2.5.1",
34 | "vue": "^3.5.11",
35 | "vue-final-modal": "^4.5.5",
36 | "vue-multiselect": "^3.0.0-beta.3",
37 | "vue-router": "^4.4.5",
38 | "vuejs-paginate-next": "^1.0.2",
39 | "vuex": "^4.1.0"
40 | },
41 | "devDependencies": {
42 | "@babel/core": "^7.25.7",
43 | "@vitejs/plugin-vue": "^5.2.1",
44 | "@vue/eslint-config-standard": "^8.0.1",
45 | "@vue/test-utils": "^2.4.6",
46 | "chai": "^4.1.2",
47 | "chai-as-promised": "^8.0.1",
48 | "eslint": "^8.57.1",
49 | "eslint-config-prettier": "^10.1.1",
50 | "eslint-plugin-import": "^2.20.2",
51 | "eslint-plugin-node": "^11.1.0",
52 | "eslint-plugin-promise": "^4.2.1",
53 | "eslint-plugin-standard": "^4.0.0",
54 | "eslint-plugin-vue": "^9.28.0",
55 | "flush-promises": "^1.0.2",
56 | "karma": "^6.4.4",
57 | "karma-coverage": "^2.2.1",
58 | "karma-coverage-istanbul-reporter": "^3.0.3",
59 | "karma-firefox-launcher": "^2.1.3",
60 | "karma-mocha": "^1.3.0",
61 | "karma-spec-reporter": "^0.0.36",
62 | "karma-vite": "^1.0.5",
63 | "mocha": "^5.2.0",
64 | "prettier": "3.5.3",
65 | "process": "^0.11.10",
66 | "url-loader": "^4.1.1",
67 | "vite": "^5.4.14",
68 | "vite-plugin-istanbul": "^5.0.0",
69 | "vite-plugin-node-polyfills": "^0.23.0",
70 | "vite-plugin-pwa": "^0.21.1",
71 | "vite-plugin-static-copy": "^2.2.0",
72 | "vue-cli-plugin-ui-karma": "^0.2.5"
73 | },
74 | "overrides": {
75 | "karma-vite": {
76 | "vite-plugin-istanbul": "$vite-plugin-istanbul"
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/public/Logo192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lana-k/sqliteviz/3ee825defe91e240a996f308df2eb4ed4d0621e1/public/Logo192x192.png
--------------------------------------------------------------------------------
/public/Logo48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lana-k/sqliteviz/3ee825defe91e240a996f308df2eb4ed4d0621e1/public/Logo48x48.png
--------------------------------------------------------------------------------
/public/Logo512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lana-k/sqliteviz/3ee825defe91e240a996f308df2eb4ed4d0621e1/public/Logo512x512.png
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lana-k/sqliteviz/3ee825defe91e240a996f308df2eb4ed4d0621e1/public/favicon.png
--------------------------------------------------------------------------------
/public/inquiries.json:
--------------------------------------------------------------------------------
1 | []
2 |
--------------------------------------------------------------------------------
/public/manifest.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "background_color": "white",
3 | "description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases, CSV, JSON or NDJSON.",
4 | "display": "fullscreen",
5 | "icons": [
6 | {
7 | "src": "favicon.png",
8 | "sizes": "32x32",
9 | "type": "image/png"
10 | },
11 | {
12 | "src": "Logo48x48.png",
13 | "sizes": "48x48",
14 | "type": "image/png"
15 | },
16 | {
17 | "src": "Logo192x192.png",
18 | "sizes": "192x192",
19 | "type": "image/png"
20 | },
21 | {
22 | "src": "Logo512x512.png",
23 | "sizes": "512x512",
24 | "type": "image/png"
25 | }
26 | ],
27 | "name": "sqliteviz",
28 | "short_name": "sqliteviz",
29 | "start_url": "index.html"
30 | }
31 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
32 |
33 |
94 |
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lana-k/sqliteviz/3ee825defe91e240a996f308df2eb4ed4d0621e1/src/assets/fonts/OpenSans-Bold.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-BoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lana-k/sqliteviz/3ee825defe91e240a996f308df2eb4ed4d0621e1/src/assets/fonts/OpenSans-BoldItalic.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lana-k/sqliteviz/3ee825defe91e240a996f308df2eb4ed4d0621e1/src/assets/fonts/OpenSans-Italic.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lana-k/sqliteviz/3ee825defe91e240a996f308df2eb4ed4d0621e1/src/assets/fonts/OpenSans-Regular.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lana-k/sqliteviz/3ee825defe91e240a996f308df2eb4ed4d0621e1/src/assets/fonts/OpenSans-SemiBold.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/OpenSans-SemiBoldItalic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lana-k/sqliteviz/3ee825defe91e240a996f308df2eb4ed4d0621e1/src/assets/fonts/OpenSans-SemiBoldItalic.woff2
--------------------------------------------------------------------------------
/src/assets/images/Screenshot_editor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lana-k/sqliteviz/3ee825defe91e240a996f308df2eb4ed4d0621e1/src/assets/images/Screenshot_editor.png
--------------------------------------------------------------------------------
/src/assets/images/arrow-hover.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/body.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/src/assets/images/checkbox_checked.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/src/assets/images/checkbox_checked_disabled.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/src/assets/images/checkbox_checked_light.svg:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/src/assets/images/chevron.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/close.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/copy.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/database-edit.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/delete-tag-hover.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/delete-tag.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/delete.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/error.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/images/file-export.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lana-k/sqliteviz/3ee825defe91e240a996f308df2eb4ed4d0621e1/src/assets/images/file.png
--------------------------------------------------------------------------------
/src/assets/images/info.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/leftArm.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/images/logo_simple.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/rename.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/images/rightArm.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/assets/images/sort.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/assets/images/success.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/assets/images/top.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/src/assets/styles/buttons.css:
--------------------------------------------------------------------------------
1 | button {
2 | box-sizing: border-box;
3 | height: 36px;
4 | padding: 0 12px;
5 | font-size: 14px;
6 | font-weight: 600;
7 | cursor: pointer;
8 | }
9 |
10 | button:focus {
11 | outline: none;
12 | }
13 |
14 | button.primary {
15 | background: var(--color-accent);
16 | border: 1px solid var(--color-accent-shade);
17 | border-radius: var(--border-radius-big);
18 | min-width: 83px;
19 | color: var(--color-text-light);
20 | text-shadow: var(--shadow);
21 | }
22 |
23 | button.primary:hover {
24 | background: var(--color-accent-shade);
25 | border: 1px solid var(--color-accent-shade);
26 | color: var(--color-text-light);
27 | text-shadow: var(--shadow);
28 | }
29 |
30 | button.secondary {
31 | background: white;
32 | border: 1px solid var(--color-border);
33 | border-radius: var(--border-radius-big);
34 | min-width: 83px;
35 | color: var(--color-text-base);
36 | }
37 |
38 | button.secondary:hover {
39 | border: 1px solid var(--color-text-light-2);
40 | color: var(--color-text-active);
41 | }
42 |
43 | button.toolbar {
44 | background: transparent;
45 | border: none;
46 | color: var(--color-text-base);
47 | padding: 0;
48 | }
49 |
50 | button.toolbar:hover {
51 | color: var(--color-accent);
52 | }
53 |
54 | button.primary:disabled,
55 | button.secondary:disabled {
56 | background: var(--color-bg-light-2);
57 | border: 1px solid var(--color-border);
58 | color: var(--color-text-light-2);
59 | text-shadow: none;
60 | cursor: default;
61 | }
62 |
--------------------------------------------------------------------------------
/src/assets/styles/dialogs.css:
--------------------------------------------------------------------------------
1 | .dialog {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | }
6 | .dialog .vfm__content {
7 | border-radius: var(--border-radius-big);
8 | box-shadow: 0px 2px 9px rgba(80, 103, 132, 0.8);
9 | background-color: white;
10 | overflow: hidden;
11 | }
12 |
13 | .dialog-header {
14 | height: 46px;
15 | line-height: 46px;
16 | padding: 0 22px 0 12px;
17 | color: var(--color-text-base);
18 | font-size: 16px;
19 | font-weight: 600;
20 | display: flex;
21 | justify-content: space-between;
22 | align-items: center;
23 | }
24 |
25 | .dialog-body {
26 | min-height: 56px;
27 | background-color: var(--color-bg-light);
28 | padding: 24px;
29 | border-top: 1px solid var(--color-border-light);
30 | color: var(--color-text-base);
31 | font-size: 13px;
32 | }
33 |
34 | .dialog-buttons-container {
35 | display: flex;
36 | justify-content: flex-end;
37 | background-color: var(--color-bg-light);
38 | padding: 24px;
39 | }
40 |
41 | .dialog-buttons-container button {
42 | margin-left: 16px;
43 | }
44 |
45 | .vfm__overlay.vfm--overlay {
46 | background-color: rgba(162, 177, 198, 0.5);
47 | }
48 |
--------------------------------------------------------------------------------
/src/assets/styles/messages.css:
--------------------------------------------------------------------------------
1 | .warning {
2 | background-color: var(--color-bg-warning);
3 | color: var(--color-text-base);
4 | font-size: 13px;
5 | padding: 0 24px;
6 | }
7 |
--------------------------------------------------------------------------------
/src/assets/styles/scrollbars.css:
--------------------------------------------------------------------------------
1 | /* width */
2 | ::-webkit-scrollbar {
3 | width: 5px;
4 | height: 5px;
5 | }
6 |
7 | /* Track */
8 | ::-webkit-scrollbar-track {
9 | background: transparent;
10 | border-radius: 5px;
11 | }
12 |
13 | /* Handle */
14 | ::-webkit-scrollbar-thumb {
15 | background: var(--color-accent);
16 | border-radius: 10px;
17 | }
18 |
--------------------------------------------------------------------------------
/src/assets/styles/tables.css:
--------------------------------------------------------------------------------
1 | .rounded-bg {
2 | padding: 35px 5px 5px;
3 | background-color: white;
4 | border-radius: 5px;
5 | position: relative;
6 | border: 1px solid var(--color-border-light);
7 | box-sizing: border-box;
8 | }
9 |
10 | .straight .rounded-bg {
11 | border-radius: 0;
12 | border-width: 0 0 1px 0;
13 | }
14 |
15 | .header-container {
16 | overflow: hidden;
17 | position: absolute;
18 | top: -1px;
19 | left: -1px;
20 | width: calc(100% + 2px);
21 | padding-left: 7px;
22 | box-sizing: border-box;
23 | background-color: var(--color-bg-dark);
24 | border-radius: 5px 5px 0 0;
25 | }
26 |
27 | .straight .header-container {
28 | border-radius: 0;
29 | }
30 |
31 | .straight {
32 | height: 100%;
33 | }
34 |
35 | .straight .rounded-bg {
36 | /* 27 - height of table footer */
37 | height: calc(100% - 27px);
38 | }
39 |
40 | @supports (-moz-appearance: none) {
41 | .header-container {
42 | top: 0;
43 | padding-left: 6px;
44 | }
45 | }
46 |
47 | .header-container > div {
48 | display: flex;
49 | width: fit-content;
50 | padding-right: 30px;
51 | }
52 | .table-container {
53 | width: 100%;
54 | max-height: 100%;
55 | overflow: auto;
56 | }
57 | table.sqliteviz-table {
58 | min-width: 100%;
59 | margin-top: -35px;
60 | border-collapse: collapse;
61 | }
62 | .sqliteviz-table thead th,
63 | .fixed-header {
64 | font-size: 14px;
65 | font-weight: 600;
66 | box-sizing: border-box;
67 | background-color: var(--color-bg-dark);
68 | color: var(--color-text-light);
69 | border-right: 1px solid var(--color-border-light);
70 | overflow: hidden;
71 | text-overflow: ellipsis;
72 | }
73 | .sqliteviz-table tbody td {
74 | font-size: 13px;
75 | background-color: white;
76 | color: var(--color-text-base);
77 | box-sizing: border-box;
78 | border-bottom: 1px solid var(--color-border-light);
79 | border-right: 1px solid var(--color-border-light);
80 | }
81 | .sqliteviz-table td,
82 | .sqliteviz-table th,
83 | .fixed-header {
84 | padding: 8px 24px;
85 | white-space: nowrap;
86 | }
87 |
88 | .sqliteviz-table tbody tr td:last-child,
89 | .sqliteviz-table thead tr th:last-child,
90 | .header-container div .fixed-header:last-child {
91 | border-right: none;
92 | }
93 |
94 | .sqliteviz-table td > div.cell-data {
95 | width: -webkit-max-content;
96 | width: -moz-max-content;
97 | width: max-content;
98 | white-space: nowrap;
99 | overflow: hidden;
100 | text-overflow: ellipsis;
101 | }
102 | .table-footer {
103 | display: flex;
104 | justify-content: space-between;
105 | padding: 6px 12px;
106 | }
107 | .table-footer-count {
108 | font-size: 11px;
109 | color: var(--color-text-base);
110 | }
111 |
112 | .sqliteviz-table tbody td[data-isNull='true'],
113 | .sqliteviz-table tbody td[data-isBlob='true'] {
114 | color: var(--color-text-light-2);
115 | font-style: italic;
116 | }
117 |
--------------------------------------------------------------------------------
/src/assets/styles/tooltips.css:
--------------------------------------------------------------------------------
1 | .icon-tooltip {
2 | background-color: rgba(80, 103, 132, 0.85);
3 | color: #fff;
4 | text-align: center;
5 | font-size: 12px;
6 | padding: 0 6px;
7 | line-height: 19px;
8 | position: fixed;
9 | height: 19px;
10 | border-radius: var(--border-radius-medium);
11 | white-space: nowrap;
12 | z-index: 999;
13 | }
14 |
--------------------------------------------------------------------------------
/src/assets/styles/variables.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --color-white: #ffffff;
3 | --color-gray-light: #f3f6fa;
4 | --color-gray-light-2: #dfe8f3;
5 | --color-gray-light-3: #c8d4e3;
6 | --color-gray-light-4: #ebf0f8;
7 | --color-gray-light-5: #f8f8f9;
8 | --color-gray-medium: #a2b1c6;
9 | --color-gray-dark: #506784;
10 | --color-blue-medium: #119dff;
11 | --color-blue-dark: #0d76bf;
12 | --color-blue-dark-2: #2a3f5f;
13 | --color-red: #ef553b;
14 | --color-red-2: #de350b;
15 | --color-red-light: #ffbdad;
16 | --color-yellow: #fbefcb;
17 |
18 | --color-bg-light: var(--color-gray-light);
19 | --color-bg-light-2: var(--color-gray-light-2);
20 | --color-bg-light-3: var(--color-gray-light-5);
21 | --color-bg-light-4: var(--color-gray-light-4);
22 | --color-bg-dark: var(--color-gray-dark);
23 | --color-bg-warning: var(--color-yellow);
24 | --color-bg-danger: var(--color-red-light);
25 | --color-danger: var(--color-red-2);
26 | --color-accent: var(--color-blue-medium);
27 | --color-accent-shade: var(--color-blue-dark);
28 | --color-border-light: var(--color-gray-light-2);
29 | --color-border: var(--color-gray-light-3);
30 | --color-border-dark: var(--color-gray-medium);
31 | --color-text-light: var(--color-white);
32 | --color-text-light-2: var(--color-gray-medium);
33 | --color-text-base: var(--color-gray-dark);
34 | --color-text-active: var(--color-blue-dark-2);
35 | --color-text-error: var(--color-red);
36 |
37 | --shadow: 0 1px 2px rgba(42, 63, 95, 0.7);
38 | --shadow-1: 0 2px 9px rgba(80, 103, 132, 0.2);
39 |
40 | --border-radius-big: 5px;
41 | --border-radius-medium: 3px;
42 | --border-radius-medium-2: 4px;
43 | --border-radius-small: 2px;
44 | }
45 |
46 | .plotly-editor--theme-provider {
47 | --sidebar-width: 112px;
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/CheckBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |

16 |

21 |

26 |
{{ label }}
27 |
28 |
29 |
30 |
74 |
75 |
116 |
--------------------------------------------------------------------------------
/src/components/IconButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
22 |
23 |
24 |
48 |
49 |
125 |
--------------------------------------------------------------------------------
/src/components/LoadingDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
13 |
14 |
15 |
16 | {{ loadingMsg }}
17 |
18 |
19 |

23 | {{ successMsg }}
24 |
25 |
26 |
27 |
35 |
43 |
44 |
45 |
46 |
47 |
77 |
78 |
102 |
103 |
116 |
--------------------------------------------------------------------------------
/src/components/LoadingIndicator.vue:
--------------------------------------------------------------------------------
1 |
2 |
27 |
28 |
29 |
68 |
69 |
130 |
--------------------------------------------------------------------------------
/src/components/Logs.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

5 |

10 |

11 |
15 |
{{ serializeMessage(msg) }}
16 |
17 |
18 |
19 |
20 |
66 |
67 |
95 |
--------------------------------------------------------------------------------
/src/components/Splitpanes/splitter.js:
--------------------------------------------------------------------------------
1 | export default {
2 | // Get the cursor position relative to the splitpane container.
3 | getCurrentMouseDrag(event, container) {
4 | const rect = container.getBoundingClientRect()
5 | const { clientX, clientY } =
6 | 'ontouchstart' in window && event.touches ? event.touches[0] : event
7 | return {
8 | x: clientX - rect.left,
9 | y: clientY - rect.top
10 | }
11 | },
12 |
13 | // Returns the drag percentage of the splitter relative to the 2 panes it's inbetween.
14 | getCurrentDragPercentage(event, container, isHorisontal) {
15 | let drag = this.getCurrentMouseDrag(event, container)
16 | drag = drag[isHorisontal ? 'y' : 'x']
17 | const containerSize =
18 | container[isHorisontal ? 'clientHeight' : 'clientWidth']
19 | return (drag * 100) / containerSize
20 | },
21 |
22 | // Returns the new position in percents.
23 | calculateOffset(
24 | event,
25 | { container, isHorisontal, paneBeforeMax, paneAfterMax }
26 | ) {
27 | const dragPercentage = this.getCurrentDragPercentage(
28 | event,
29 | container,
30 | isHorisontal
31 | )
32 |
33 | const paneBeforeMaxReached =
34 | paneBeforeMax < 100 && dragPercentage >= paneBeforeMax
35 | const paneAfterMaxReached =
36 | paneAfterMax < 100 && dragPercentage <= 100 - paneAfterMax
37 |
38 | // Prevent dragging beyond pane max.
39 | if (paneBeforeMaxReached || paneAfterMaxReached) {
40 | return paneBeforeMaxReached
41 | ? paneBeforeMax
42 | : Math.max(100 - paneAfterMax, 0)
43 | } else {
44 | return Math.min(Math.max(dragPercentage, 0), paneBeforeMax)
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/SqlTable/Pager.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
54 |
55 |
110 |
--------------------------------------------------------------------------------
/src/components/TextField.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | {{ label }}
8 |
14 |
15 |
24 |
{{ errorMsg }}
25 |
26 |
27 |
28 |
46 |
47 |
112 |
--------------------------------------------------------------------------------
/src/components/svg/addTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
36 |
37 | Add new table from CSV, JSON or NDJSON
38 |
39 |
40 |
41 |
42 |
58 |
59 |
70 |
--------------------------------------------------------------------------------
/src/components/svg/arrow.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
20 |
--------------------------------------------------------------------------------
/src/components/svg/changeDb.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
24 |
25 | Load another database, CSV, JSON or NDJSON
26 |
27 |
28 |
29 |
30 |
45 |
46 |
55 |
--------------------------------------------------------------------------------
/src/components/svg/chart.vue:
--------------------------------------------------------------------------------
1 |
2 |
41 |
42 |
43 |
48 |
--------------------------------------------------------------------------------
/src/components/svg/clear.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 |
29 |
30 |
47 |
--------------------------------------------------------------------------------
/src/components/svg/clipboard.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 |
28 |
--------------------------------------------------------------------------------
/src/components/svg/close.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
37 |
38 |
49 |
--------------------------------------------------------------------------------
/src/components/svg/dataView.vue:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
33 |
--------------------------------------------------------------------------------
/src/components/svg/dropDownChevron.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
23 |
24 |
41 |
--------------------------------------------------------------------------------
/src/components/svg/edgeArrow.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 |
26 |
--------------------------------------------------------------------------------
/src/components/svg/export.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
20 |
21 | {{ tooltip }}
22 |
23 |
24 |
25 |
26 |
45 |
46 |
57 |
--------------------------------------------------------------------------------
/src/components/svg/exportToCsv.vue:
--------------------------------------------------------------------------------
1 |
2 |
50 |
51 |
52 |
57 |
--------------------------------------------------------------------------------
/src/components/svg/exportToSvg.vue:
--------------------------------------------------------------------------------
1 |
2 |
49 |
50 |
51 |
56 |
--------------------------------------------------------------------------------
/src/components/svg/hint.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
36 |
41 | {{ hint }}
42 |
43 |
44 |
45 |
46 |
65 |
66 |
83 |
--------------------------------------------------------------------------------
/src/components/svg/html.vue:
--------------------------------------------------------------------------------
1 |
2 |
43 |
44 |
45 |
50 |
--------------------------------------------------------------------------------
/src/components/svg/pivot.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
23 |
--------------------------------------------------------------------------------
/src/components/svg/png.vue:
--------------------------------------------------------------------------------
1 |
2 |
33 |
34 |
35 |
40 |
--------------------------------------------------------------------------------
/src/components/svg/row.vue:
--------------------------------------------------------------------------------
1 |
2 |
41 |
42 |
43 |
48 |
--------------------------------------------------------------------------------
/src/components/svg/run.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
15 |
--------------------------------------------------------------------------------
/src/components/svg/sort.vue:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
26 |
43 |
44 |
57 |
--------------------------------------------------------------------------------
/src/components/svg/sqlEditor.vue:
--------------------------------------------------------------------------------
1 |
2 |
54 |
55 |
56 |
61 |
--------------------------------------------------------------------------------
/src/components/svg/table.vue:
--------------------------------------------------------------------------------
1 |
2 |
36 |
37 |
38 |
43 |
--------------------------------------------------------------------------------
/src/components/svg/treeChevron.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
30 |
31 |
37 |
--------------------------------------------------------------------------------
/src/components/svg/viewCellValue.vue:
--------------------------------------------------------------------------------
1 |
2 |
38 |
39 |
40 |
45 |
--------------------------------------------------------------------------------
/src/lib/ReactPlotlyEditorWithPlotRef.jsx:
--------------------------------------------------------------------------------
1 | import ReactPlotlyEditor from 'react-chart-editor'
2 | import React, { createRef } from 'react'
3 | import EditorControls from 'react-chart-editor/lib/EditorControls'
4 |
5 | /**
6 | * This extended ReactPlotlyEditor has a reference to PlotComponent.
7 | * The reference makes it possible to call updatePlotly method of PlotComponent.
8 | * updatePlotly method allows smoothly resize the plot
9 | * when resize chart editor container.
10 | */
11 | export default class ReactPlotlyEditorWithPlotRef extends ReactPlotlyEditor {
12 | constructor(props) {
13 | super(props)
14 | this.plotComponentRef = createRef()
15 | }
16 | render() {
17 | return (
18 |
19 | {!this.props.hideControls && (
20 |
41 | {this.props.children}
42 |
43 | )}
44 |
48 |
61 |
62 |
63 | )
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/lib/chartHelper.js:
--------------------------------------------------------------------------------
1 | import * as dereference from 'react-chart-editor/lib/lib/dereference'
2 | import plotly from 'plotly.js'
3 | import { nanoid } from 'nanoid'
4 |
5 | export function getOptionsFromDataSources(dataSources) {
6 | if (!dataSources) {
7 | return []
8 | }
9 |
10 | return Object.keys(dataSources).map(name => ({
11 | value: name,
12 | label: name
13 | }))
14 | }
15 |
16 | export function getOptionsForSave(state, dataSources) {
17 | // we don't need to save the data, only settings
18 | // so we modify state.data using dereference
19 | const stateCopy = JSON.parse(JSON.stringify(state))
20 | const emptySources = {}
21 | for (const key in dataSources) {
22 | emptySources[key] = []
23 | }
24 | dereference.default(stateCopy.data, emptySources)
25 | return stateCopy
26 | }
27 |
28 | export async function getImageDataUrl(element, type) {
29 | const chartElement = element.querySelector('.js-plotly-plot')
30 | return await plotly.toImage(chartElement, {
31 | format: type,
32 | width: null,
33 | height: null
34 | })
35 | }
36 |
37 | export function getChartData(element) {
38 | const chartElement = element.querySelector('.js-plotly-plot')
39 | return {
40 | data: chartElement.data,
41 | layout: chartElement.layout
42 | }
43 | }
44 |
45 | export function getHtml(options) {
46 | const chartId = nanoid()
47 | return `
48 |
49 |
50 |
67 | `
68 | }
69 |
70 | export default {
71 | getOptionsFromDataSources,
72 | getOptionsForSave,
73 | getImageDataUrl,
74 | getHtml,
75 | getChartData
76 | }
77 |
--------------------------------------------------------------------------------
/src/lib/database/_sql.js:
--------------------------------------------------------------------------------
1 | import initSqlJs from 'sql.js'
2 | import dbUtils from './_statements'
3 | import wasmUrl from 'sql.js/dist/sql-wasm.wasm?url'
4 |
5 | let SQL = null
6 | const sqlModuleReady = initSqlJs({
7 | locateFile: () => wasmUrl
8 | }).then(sqlModule => {
9 | SQL = sqlModule
10 | })
11 |
12 | function _getDataSourcesFromSqlResult(sqlResult) {
13 | if (!sqlResult) {
14 | return {}
15 | }
16 | const dataSources = {}
17 | sqlResult.columns.forEach((column, index) => {
18 | dataSources[column] = sqlResult.values.map(row => row[index])
19 | })
20 | return dataSources
21 | }
22 |
23 | export default class Sql {
24 | constructor() {
25 | this.db = null
26 | }
27 |
28 | static build() {
29 | return sqlModuleReady.then(() => {
30 | return new Sql()
31 | })
32 | }
33 |
34 | createDb(buffer) {
35 | if (this.db != null) this.db.close()
36 | this.db = new SQL.Database(buffer)
37 | return this.db
38 | }
39 |
40 | open(buffer) {
41 | this.createDb(buffer && new Uint8Array(buffer))
42 | return {
43 | ready: true
44 | }
45 | }
46 |
47 | exec(sql, params) {
48 | if (this.db === null) {
49 | this.createDb()
50 | }
51 | if (!sql) {
52 | throw new Error('exec: Missing query string')
53 | }
54 | const sqlResults = this.db.exec(sql, params)
55 | return sqlResults.map(result => {
56 | return {
57 | columns: result.columns,
58 | values: _getDataSourcesFromSqlResult(result)
59 | }
60 | })
61 | }
62 |
63 | import(tabName, data, progressCounterId, progressCallback, chunkSize = 1500) {
64 | if (this.db === null) {
65 | this.createDb()
66 | }
67 | const columns = data.columns
68 | const rowCount = data.values[columns[0]].length
69 | this.db.exec(dbUtils.getCreateStatement(tabName, data.values))
70 | const chunks = dbUtils.generateChunks(data.values, chunkSize)
71 | const chunksAmount = Math.ceil(rowCount / chunkSize)
72 | let count = 0
73 | const insertStr = dbUtils.getInsertStmt(tabName, columns)
74 | const insertStmt = this.db.prepare(insertStr)
75 |
76 | progressCallback({ progress: 0, id: progressCounterId })
77 | for (const chunk of chunks) {
78 | this.db.exec('BEGIN')
79 | for (const row of chunk) {
80 | insertStmt.run(row)
81 | }
82 | this.db.exec('COMMIT')
83 | count++
84 | progressCallback({
85 | progress: 100 * (count / chunksAmount),
86 | id: progressCounterId
87 | })
88 | }
89 |
90 | return {
91 | finish: true
92 | }
93 | }
94 |
95 | export() {
96 | return this.db.export()
97 | }
98 |
99 | close() {
100 | if (this.db) {
101 | this.db.close()
102 | }
103 | return {
104 | finished: true
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/lib/database/_statements.js:
--------------------------------------------------------------------------------
1 | export default {
2 | *generateChunks(data, size) {
3 | const matrix = Object.keys(data).map(col => data[col])
4 | const [row] = matrix
5 | const transposedMatrix = row.map((value, column) =>
6 | matrix.map(row => row[column])
7 | )
8 |
9 | const count = Math.ceil(transposedMatrix.length / size)
10 |
11 | for (let i = 0; i <= count - 1; i++) {
12 | const start = size * i
13 | const end = start + size
14 | yield transposedMatrix.slice(start, end)
15 | }
16 | },
17 |
18 | getInsertStmt(tabName, columns) {
19 | const colList = `"${columns.join('", "')}"`
20 | const params = columns.map(() => '?').join(', ')
21 | return `INSERT INTO "${tabName}" (${colList}) VALUES (${params});`
22 | },
23 |
24 | getCreateStatement(tabName, data) {
25 | let result = `CREATE table "${tabName}"(`
26 | for (const col in data) {
27 | // Get the first row of values to determine types
28 | const value = data[col][0]
29 | let type = ''
30 | switch (typeof value) {
31 | case 'number': {
32 | type = 'REAL'
33 | break
34 | }
35 | case 'boolean': {
36 | type = 'INTEGER'
37 | break
38 | }
39 | case 'string': {
40 | type = 'TEXT'
41 | break
42 | }
43 | default:
44 | type = 'TEXT'
45 | }
46 | result += `"${col}" ${type}, `
47 | }
48 |
49 | result = result.replace(/,\s$/, ');')
50 | return result
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/lib/database/_worker.js:
--------------------------------------------------------------------------------
1 | import registerPromiseWorker from 'promise-worker/register'
2 | import Sql from './_sql'
3 |
4 | const sqlReady = Sql.build()
5 |
6 | function processMsg(sql) {
7 | const data = this
8 | switch (data && data.action) {
9 | case 'open':
10 | return sql.open(data.buffer)
11 | case 'reopen':
12 | return sql.open(sql.export())
13 | case 'exec':
14 | return sql.exec(data.sql, data.params)
15 | case 'import':
16 | return sql.import(
17 | data.tabName,
18 | data.data,
19 | data.progressCounterId,
20 | postMessage
21 | )
22 | case 'export':
23 | return sql.export()
24 | case 'close':
25 | return sql.close()
26 | default:
27 | throw new Error('Invalid action : ' + (data && data.action))
28 | }
29 | }
30 |
31 | function onError(error) {
32 | return {
33 | error: error.message
34 | }
35 | }
36 |
37 | registerPromiseWorker(data => {
38 | return sqlReady.then(processMsg.bind(data)).catch(onError)
39 | })
40 |
--------------------------------------------------------------------------------
/src/lib/eventBus.js:
--------------------------------------------------------------------------------
1 | import emitter from 'tiny-emitter/instance'
2 |
3 | export default {
4 | $on: (...args) => emitter.on(...args),
5 | $once: (...args) => emitter.once(...args),
6 | $off: (...args) => emitter.off(...args),
7 | $emit: (...args) => emitter.emit(...args)
8 | }
9 |
--------------------------------------------------------------------------------
/src/lib/storedInquiries/_migrations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | _migrate(installedVersion, inquiries) {
3 | if (installedVersion === 1) {
4 | inquiries.forEach(inquire => {
5 | inquire.viewType = 'chart'
6 | inquire.viewOptions = inquire.chart
7 | delete inquire.chart
8 | })
9 | return inquiries
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/lib/storedInquiries/index.js:
--------------------------------------------------------------------------------
1 | import { nanoid } from 'nanoid'
2 | import fu from '@/lib/utils/fileIo'
3 | import events from '@/lib/utils/events'
4 | import migration from './_migrations'
5 |
6 | const migrate = migration._migrate
7 |
8 | export default {
9 | version: 2,
10 | getStoredInquiries() {
11 | let myInquiries = JSON.parse(localStorage.getItem('myInquiries'))
12 | if (!myInquiries) {
13 | const oldInquiries = localStorage.getItem('myQueries')
14 | if (oldInquiries) {
15 | myInquiries = migrate(1, JSON.parse(oldInquiries))
16 | this.updateStorage(myInquiries)
17 | return myInquiries
18 | }
19 | return []
20 | }
21 |
22 | return (myInquiries && myInquiries.inquiries) || []
23 | },
24 |
25 | duplicateInquiry(baseInquiry) {
26 | const newInquiry = JSON.parse(JSON.stringify(baseInquiry))
27 | newInquiry.name = newInquiry.name + ' Copy'
28 | newInquiry.id = nanoid()
29 | newInquiry.createdAt = new Date()
30 | delete newInquiry.isPredefined
31 |
32 | return newInquiry
33 | },
34 |
35 | isTabNeedName(inquiryTab) {
36 | return inquiryTab.isPredefined || !inquiryTab.name
37 | },
38 |
39 | updateStorage(inquiries) {
40 | localStorage.setItem(
41 | 'myInquiries',
42 | JSON.stringify({ version: this.version, inquiries })
43 | )
44 | },
45 |
46 | serialiseInquiries(inquiryList) {
47 | const preparedData = JSON.parse(JSON.stringify(inquiryList))
48 | preparedData.forEach(inquiry => delete inquiry.isPredefined)
49 | return JSON.stringify(
50 | { version: this.version, inquiries: preparedData },
51 | null,
52 | 4
53 | )
54 | },
55 |
56 | deserialiseInquiries(str) {
57 | const inquiries = JSON.parse(str)
58 | let inquiryList = []
59 | if (!inquiries.version) {
60 | // Turn data into array if they are not
61 | inquiryList = !Array.isArray(inquiries) ? [inquiries] : inquiries
62 | inquiryList = migrate(1, inquiryList)
63 | } else {
64 | inquiryList = inquiries.inquiries || []
65 | }
66 |
67 | // Generate new ids if they are the same as existing inquiries
68 | inquiryList.forEach(inquiry => {
69 | const allInquiriesIds = this.getStoredInquiries().map(
70 | inquiry => inquiry.id
71 | )
72 | if (allInquiriesIds.includes(inquiry.id)) {
73 | inquiry.id = nanoid()
74 | }
75 | })
76 |
77 | return inquiryList
78 | },
79 |
80 | importInquiries() {
81 | return fu.importFile().then(str => {
82 | const inquires = this.deserialiseInquiries(str)
83 |
84 | events.send('inquiry.import', inquires.length)
85 |
86 | return inquires
87 | })
88 | },
89 | export(inquiryList, fileName) {
90 | const jsonStr = this.serialiseInquiries(inquiryList)
91 | fu.exportToFile(jsonStr, fileName)
92 |
93 | events.send('inquiry.export', inquiryList.length)
94 | },
95 |
96 | async readPredefinedInquiries() {
97 | const res = await fu.readFile('./inquiries.json')
98 | const data = await res.json()
99 |
100 | if (!data.version) {
101 | return data.length > 0 ? migrate(1, data) : []
102 | } else {
103 | return data.inquiries
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/lib/tab.js:
--------------------------------------------------------------------------------
1 | import { nanoid } from 'nanoid'
2 | import time from '@/lib/utils/time'
3 | import events from '@/lib/utils/events'
4 |
5 | export default class Tab {
6 | constructor(state, inquiry = {}) {
7 | this.id = inquiry.id || nanoid()
8 | this.name = inquiry.id ? inquiry.name : null
9 | this.tempName =
10 | inquiry.name ||
11 | (state.untitledLastIndex
12 | ? `Untitled ${state.untitledLastIndex}`
13 | : 'Untitled')
14 | this.query = inquiry.query
15 | this.viewOptions = inquiry.viewOptions || undefined
16 | this.isPredefined = inquiry.isPredefined
17 | this.viewType = inquiry.viewType || 'chart'
18 | this.result = null
19 | this.isGettingResults = false
20 | this.error = null
21 | this.time = 0
22 | this.layout = inquiry.layout || {
23 | sqlEditor: 'above',
24 | table: 'bottom',
25 | dataView: 'hidden'
26 | }
27 | this.maximize = inquiry.maximize
28 |
29 | this.isSaved = !!inquiry.id
30 | this.state = state
31 | }
32 |
33 | async execute() {
34 | this.isGettingResults = true
35 | this.result = null
36 | this.error = null
37 | const db = this.state.db
38 | try {
39 | const start = new Date()
40 | this.result = await db.execute(this.query + ';')
41 | this.time = time.getPeriod(start, new Date())
42 |
43 | if (this.result && this.result.values) {
44 | events.send(
45 | 'resultset.create',
46 | this.result.values[this.result.columns[0]].length
47 | )
48 | }
49 |
50 | events.send('query.run', parseFloat(this.time), { status: 'success' })
51 | } catch (err) {
52 | this.error = {
53 | type: 'error',
54 | message: err
55 | }
56 |
57 | events.send('query.run', 0, { status: 'error' })
58 | }
59 | db.refreshSchema()
60 | this.isGettingResults = false
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/lib/utils/clipboardIo.js:
--------------------------------------------------------------------------------
1 | import Lib from 'plotly.js/src/lib'
2 | import dataUrlToBlob from 'dataurl-to-blob'
3 |
4 | export default {
5 | async copyText(str, notifyMessage) {
6 | await navigator.clipboard.writeText(str)
7 | if (notifyMessage) {
8 | Lib.notifier(notifyMessage, 'long')
9 | }
10 | },
11 |
12 | async copyImage(source) {
13 | if (source instanceof HTMLCanvasElement) {
14 | return this._copyCanvas(source)
15 | } else {
16 | return this._copyFromDataUrl(source)
17 | }
18 | },
19 |
20 | async _copyBlob(blob) {
21 | await navigator.clipboard.write([
22 | new ClipboardItem({
23 | // eslint-disable-line no-undef
24 | [blob.type]: blob
25 | })
26 | ])
27 | },
28 |
29 | async _copyFromDataUrl(url) {
30 | const blob = dataUrlToBlob(url)
31 | await this._copyBlob(blob)
32 | Lib.notifier('Image copied to clipboard successfully', 'long')
33 | },
34 |
35 | async _copyCanvas(canvas) {
36 | canvas.toBlob(
37 | async blob => {
38 | await this._copyBlob(blob)
39 | Lib.notifier('Image copied to clipboard successfully', 'long')
40 | },
41 | 'image/png',
42 | 1
43 | )
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/lib/utils/events.js:
--------------------------------------------------------------------------------
1 | export default {
2 | send(name, value, labels) {
3 | const event = new CustomEvent('sqliteviz-app-event', {
4 | detail: {
5 | name,
6 | value,
7 | labels: labels || {}
8 | }
9 | })
10 | window.dispatchEvent(event)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/lib/utils/fileIo.js:
--------------------------------------------------------------------------------
1 | export default {
2 | isJSON(file) {
3 | return file && file.type === 'application/json'
4 | },
5 | isNDJSON(file) {
6 | return file && file.name.endsWith('.ndjson')
7 | },
8 | isDatabase(file) {
9 | const dbTypes = ['application/vnd.sqlite3', 'application/x-sqlite3']
10 | return file.type
11 | ? dbTypes.includes(file.type)
12 | : /\.(db|sqlite(3)?)+$/.test(file.name)
13 | },
14 |
15 | getFileName(file) {
16 | return file.name.replace(/\.[^.]+$/, '')
17 | },
18 |
19 | downloadFromUrl(url, fileName) {
20 | // Create downloader
21 | const downloader = document.createElement('a')
22 | downloader.href = url
23 | downloader.download = fileName
24 |
25 | // Trigger click
26 | downloader.click()
27 |
28 | // Clean up
29 | URL.revokeObjectURL(url)
30 | },
31 |
32 | async exportToFile(str, fileName, type = 'octet/stream') {
33 | const blob = new Blob([str], { type })
34 | const url = URL.createObjectURL(blob)
35 | this.downloadFromUrl(url, fileName)
36 | },
37 |
38 | /**
39 | * Note: if user press Cancel in file choosing dialog
40 | * it will be an unsettled promise. But it's grabbed by
41 | * the garbage collector (tested with FinalizationRegistry).
42 | */
43 | getFileFromUser(type) {
44 | return new Promise(resolve => {
45 | const uploader = document.createElement('input')
46 |
47 | uploader.type = 'file'
48 | uploader.accept = type
49 |
50 | uploader.addEventListener('change', () => {
51 | const file = uploader.files[0]
52 | resolve(file)
53 | })
54 |
55 | uploader.click()
56 | })
57 | },
58 |
59 | importFile() {
60 | return this.getFileFromUser('.json').then(file => {
61 | return this.getFileContent(file)
62 | })
63 | },
64 |
65 | getFileContent(file) {
66 | const reader = new FileReader()
67 | return new Promise(resolve => {
68 | reader.onload = e => resolve(e.target.result)
69 | reader.readAsText(file)
70 | })
71 | },
72 |
73 | readFile(path) {
74 | return fetch(path)
75 | },
76 |
77 | readAsArrayBuffer(file) {
78 | const fileReader = new FileReader()
79 |
80 | return new Promise((resolve, reject) => {
81 | fileReader.onerror = () => {
82 | fileReader.abort()
83 | reject(new Error('Problem parsing input file.'))
84 | }
85 |
86 | fileReader.onload = () => {
87 | resolve(fileReader.result)
88 | }
89 | fileReader.readAsArrayBuffer(file)
90 | })
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/lib/utils/time.js:
--------------------------------------------------------------------------------
1 | export default {
2 | getPeriod(start, end) {
3 | const diff = end.getTime() - start.getTime()
4 | const seconds = diff / 1000
5 | return seconds.toFixed(3) + 's'
6 | },
7 |
8 | debounce(func, ms) {
9 | let timeout
10 | return function () {
11 | clearTimeout(timeout)
12 | timeout = setTimeout(() => func.apply(this, arguments), ms)
13 | }
14 | },
15 |
16 | sleep(ms) {
17 | return new Promise(resolve => {
18 | setTimeout(() => {
19 | resolve()
20 | }, ms)
21 | })
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import App from '@/App.vue'
3 | import router from '@/router'
4 | import store from '@/store'
5 | import { createVfm, VueFinalModal, useVfm } from 'vue-final-modal'
6 |
7 | import '@/assets/styles/variables.css'
8 | import '@/assets/styles/buttons.css'
9 | import '@/assets/styles/tables.css'
10 | import '@/assets/styles/dialogs.css'
11 | import '@/assets/styles/tooltips.css'
12 | import '@/assets/styles/messages.css'
13 | import 'vue-multiselect/dist/vue-multiselect.css'
14 | import '@/assets/styles/multiselect.css'
15 | import 'vue-final-modal/style.css'
16 |
17 | if (!['localhost', '127.0.0.1'].includes(location.hostname)) {
18 | import('./registerServiceWorker') // eslint-disable-line no-unused-expressions
19 | }
20 |
21 | const app = createApp(App)
22 | .use(router)
23 | .use(store)
24 | .use(createVfm())
25 | .component('modal', VueFinalModal)
26 |
27 | const vfm = useVfm()
28 | app.config.globalProperties.$modal = {
29 | show: modalId => vfm.open(modalId),
30 | hide: modalId => vfm.close(modalId)
31 | }
32 | app.mount('#app')
33 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | import events from '@/lib/utils/events'
2 | let refresh = false
3 |
4 | function invokeServiceWorkerUpdateFlow(registration) {
5 | const agree = confirm('New version of the app is available. Refresh now?')
6 | if (agree) {
7 | if (registration.waiting) {
8 | // let waiting Service Worker know it should became active
9 | refresh = true
10 | registration.waiting.postMessage({ type: 'SKIP_WAITING' })
11 | }
12 | }
13 | }
14 |
15 | if ('serviceWorker' in navigator) {
16 | window.addEventListener('load', async () => {
17 | const registration =
18 | await navigator.serviceWorker.register('service-worker.js')
19 | // ensure the case when the updatefound event was missed is also handled
20 | // by re-invoking the prompt when there's a waiting Service Worker
21 | if (registration.waiting) {
22 | invokeServiceWorkerUpdateFlow(registration)
23 | }
24 |
25 | // detect Service Worker update available and wait for it to become installed
26 | registration.addEventListener('updatefound', () => {
27 | const newRegestration = registration.installing
28 | if (newRegestration) {
29 | // wait until the new Service worker is actually installed (ready to take over)
30 | newRegestration.addEventListener('statechange', () => {
31 | if (registration.waiting && navigator.serviceWorker.controller) {
32 | invokeServiceWorkerUpdateFlow(registration)
33 | }
34 | })
35 | }
36 | })
37 |
38 | // detect controller change and refresh the page
39 | navigator.serviceWorker.addEventListener('controllerchange', () => {
40 | if (refresh) {
41 | window.location.reload()
42 | refresh = false
43 | }
44 | })
45 | })
46 |
47 | window.addEventListener('appinstalled', () => {
48 | events.send('pwa.install')
49 | })
50 | }
51 |
--------------------------------------------------------------------------------
/src/router.js:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHashHistory } from 'vue-router'
2 | import Workspace from '@/views/MainView/Workspace'
3 | import Inquiries from '@/views/MainView/Inquiries'
4 | import Welcome from '@/views/Welcome'
5 | import MainView from '@/views/MainView'
6 | import LoadView from '@/views/LoadView'
7 | import store from '@/store'
8 | import database from '@/lib/database'
9 |
10 | const routes = [
11 | {
12 | path: '/',
13 | name: 'Welcome',
14 | component: Welcome
15 | },
16 | {
17 | path: '/',
18 | name: 'MainView',
19 | component: MainView,
20 | children: [
21 | {
22 | path: '/workspace',
23 | name: 'Workspace',
24 | component: Workspace
25 | },
26 | {
27 | path: '/inquiries',
28 | name: 'Inquiries',
29 | component: Inquiries
30 | }
31 | ]
32 | },
33 | {
34 | path: '/load',
35 | name: 'Load',
36 | component: LoadView
37 | }
38 | ]
39 |
40 | const router = createRouter({
41 | history: createWebHashHistory(),
42 | routes
43 | })
44 |
45 | router.beforeEach(async (to, from, next) => {
46 | if (!store.state.db && to.name !== 'Load') {
47 | const newDb = database.getNewDatabase()
48 | await newDb.loadDb()
49 | store.commit('setDb', newDb)
50 | }
51 | next()
52 | })
53 |
54 | export default router
55 |
--------------------------------------------------------------------------------
/src/store/actions.js:
--------------------------------------------------------------------------------
1 | import Tab from '@/lib/tab'
2 | import { nanoid } from 'nanoid'
3 |
4 | export default {
5 | async addTab({ state }, inquiry = {}) {
6 | // add new tab only if it was not already opened
7 | if (!state.tabs.some(openedTab => openedTab.id === inquiry.id)) {
8 | const tab = new Tab(state, JSON.parse(JSON.stringify(inquiry)))
9 | state.tabs.push(tab)
10 | if (!tab.name) {
11 | state.untitledLastIndex += 1
12 | }
13 | return tab.id
14 | }
15 |
16 | return inquiry.id
17 | },
18 | async saveInquiry({ state }, { inquiryTab, newName }) {
19 | const value = {
20 | id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
21 | query: inquiryTab.query,
22 | viewType: inquiryTab.dataView.mode,
23 | viewOptions: inquiryTab.dataView.getOptionsForSave(),
24 | name: newName || inquiryTab.name
25 | }
26 |
27 | // Get inquiries from local storage
28 | const myInquiries = state.inquiries
29 |
30 | // Set createdAt
31 | if (newName) {
32 | value.createdAt = new Date()
33 | } else {
34 | var inquiryIndex = myInquiries.findIndex(
35 | oldInquiry => oldInquiry.id === inquiryTab.id
36 | )
37 | value.createdAt = myInquiries[inquiryIndex].createdAt
38 | }
39 |
40 | // Insert in inquiries list
41 | if (newName) {
42 | myInquiries.push(value)
43 | } else {
44 | myInquiries.splice(inquiryIndex, 1, value)
45 | }
46 |
47 | return value
48 | },
49 | addInquiry({ state }, newInquiry) {
50 | state.inquiries.push(newInquiry)
51 | },
52 | deleteInquiries({ state, commit }, inquiryIdSet) {
53 | state.inquiries = state.inquiries.filter(
54 | inquiry => !inquiryIdSet.has(inquiry.id)
55 | )
56 |
57 | // Close deleted inquiries if it was opened
58 | const tabs = state.tabs
59 | let i = tabs.length - 1
60 | while (i > -1) {
61 | if (inquiryIdSet.has(tabs[i].id)) {
62 | commit('deleteTab', tabs[i])
63 | }
64 | i--
65 | }
66 | },
67 | renameInquiry({ state, commit }, { inquiryId, newName }) {
68 | const renamingInquiry = state.inquiries.find(
69 | inquiry => inquiry.id === inquiryId
70 | )
71 |
72 | renamingInquiry.name = newName
73 |
74 | // update tab, if renamed inquiry is opened
75 | const tab = state.tabs.find(tab => tab.id === renamingInquiry.id)
76 | if (tab) {
77 | commit('updateTab', {
78 | tab,
79 | newValues: {
80 | name: newName
81 | }
82 | })
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'vuex'
2 | import state from '@/store/state'
3 | import mutations from '@/store/mutations'
4 | import actions from '@/store/actions'
5 |
6 | export default createStore({
7 | state,
8 | mutations,
9 | actions
10 | })
11 |
--------------------------------------------------------------------------------
/src/store/mutations.js:
--------------------------------------------------------------------------------
1 | export default {
2 | setDb(state, db) {
3 | if (state.db) {
4 | state.db.shutDown()
5 | }
6 | state.db = db
7 | },
8 |
9 | updateTab(state, { tab, newValues }) {
10 | const { name, id, query, viewType, viewOptions, isSaved } = newValues
11 | const oldId = tab.id
12 |
13 | if (id && state.currentTabId === oldId) {
14 | state.currentTabId = id
15 | }
16 |
17 | if (id) {
18 | tab.id = id
19 | }
20 | if (name) {
21 | tab.name = name
22 | }
23 | if (query) {
24 | tab.query = query
25 | }
26 | if (viewType) {
27 | tab.viewType = viewType
28 | }
29 | if (viewOptions) {
30 | tab.viewOptions = viewOptions
31 | }
32 | if (isSaved !== undefined) {
33 | tab.isSaved = isSaved
34 | }
35 | if (isSaved) {
36 | // Saved inquiry is not predefined
37 | delete tab.isPredefined
38 | }
39 | },
40 |
41 | deleteTab(state, tab) {
42 | const index = state.tabs.indexOf(tab)
43 | // If closing tab is the current opened
44 | if (tab.id === state.currentTabId) {
45 | if (index < state.tabs.length - 1) {
46 | state.currentTabId = state.tabs[index + 1].id
47 | } else if (index > 0) {
48 | state.currentTabId = state.tabs[index - 1].id
49 | } else {
50 | state.currentTabId = null
51 | state.untitledLastIndex = 0
52 | }
53 | state.currentTab = state.currentTabId
54 | ? state.tabs.find(tab => tab.id === state.currentTabId)
55 | : null
56 | }
57 | state.tabs.splice(index, 1)
58 | },
59 | setCurrentTabId(state, id) {
60 | try {
61 | state.currentTabId = id
62 | state.currentTab = state.tabs.find(tab => tab.id === id)
63 | } catch (e) {
64 | console.error("Can't open a tab id:" + id)
65 | }
66 | },
67 | updatePredefinedInquiries(state, inquiries) {
68 | state.predefinedInquiries = Array.isArray(inquiries)
69 | ? inquiries
70 | : [inquiries]
71 | },
72 | setLoadingPredefinedInquiries(state, value) {
73 | state.loadingPredefinedInquiries = value
74 | },
75 | setPredefinedInquiriesLoaded(state, value) {
76 | state.predefinedInquiriesLoaded = value
77 | },
78 | setInquiries(state, value) {
79 | state.inquiries = value
80 | },
81 | setIsWorkspaceVisible(state, value) {
82 | state.isWorkspaceVisible = value
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/store/state.js:
--------------------------------------------------------------------------------
1 | export default {
2 | tabs: [],
3 | currentTab: null,
4 | currentTabId: null,
5 | untitledLastIndex: 0,
6 | inquiries: [],
7 | predefinedInquiries: [],
8 | loadingPredefinedInquiries: false,
9 | predefinedInquiriesLoaded: false,
10 | db: null,
11 | isWorkspaceVisible: false
12 | }
13 |
--------------------------------------------------------------------------------
/src/tooltipMixin.js:
--------------------------------------------------------------------------------
1 | export default {
2 | data() {
3 | return {
4 | tooltipStyle: {
5 | visibility: 'hidden'
6 | }
7 | }
8 | },
9 | computed: {
10 | tooltipElement() {
11 | return this.$refs.tooltip
12 | }
13 | },
14 | methods: {
15 | showTooltip(e, tooltipPosition) {
16 | const position = tooltipPosition
17 | ? tooltipPosition.split('-')
18 | : ['top', 'right']
19 | const offset = 12
20 |
21 | if (position[0] === 'top') {
22 | this.tooltipStyle.top = e.clientY - offset + 'px'
23 | } else {
24 | this.tooltipStyle.top = e.clientY + offset + 'px'
25 | }
26 |
27 | if (position[1] === 'right') {
28 | this.tooltipStyle.left = e.clientX + offset + 'px'
29 | } else {
30 | this.tooltipStyle.left =
31 | e.clientX - offset - this.tooltipElement.offsetWidth + 'px'
32 | }
33 |
34 | this.tooltipStyle.visibility = 'visible'
35 | },
36 | hideTooltip() {
37 | this.tooltipStyle.visibility = 'hidden'
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/views/MainView/AppDiagnosticInfo.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |

8 |
9 |
13 |
14 |
15 | {{ item.name }}
16 |
17 |
18 |
19 | {{ opt }}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
62 |
63 |
68 |
69 |
99 |
--------------------------------------------------------------------------------
/src/views/MainView/Inquiries/svg/copy.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
23 |
24 | Duplicate inquiry
25 |
26 |
27 |
28 |
29 |
44 |
45 |
54 |
--------------------------------------------------------------------------------
/src/views/MainView/Inquiries/svg/delete.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
21 |
22 | Delete inquiry
23 |
24 |
25 |
26 |
27 |
42 |
43 |
53 |
--------------------------------------------------------------------------------
/src/views/MainView/Inquiries/svg/rename.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
21 |
22 | Rename inquiry
23 |
24 |
25 |
26 |
27 |
42 |
43 |
53 |
--------------------------------------------------------------------------------
/src/views/MainView/Workspace/Schema/TableDescription.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ name }}
6 |
7 |
8 |
9 | {{ col.name }}
10 | {{ col.type }}
11 |
12 |
13 |
14 |
15 |
16 |
33 |
34 |
56 |
--------------------------------------------------------------------------------
/src/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/PivotUi/PivotSortBtn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ modelValue.includes('key') ? 'key' : 'value' }}
4 |
9 |
10 |
11 |
12 |
38 |
39 |
76 |
--------------------------------------------------------------------------------
/src/views/MainView/Workspace/Tabs/Tab/RunResult/Record/RowNavigator.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
19 |
20 |
21 |
28 |
29 |
30 |
37 |
38 |
39 |
40 |
41 |
42 |
60 |
61 |
71 |
--------------------------------------------------------------------------------
/src/views/MainView/Workspace/Tabs/Tab/SideToolBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
37 |
38 |
39 |
59 |
60 |
74 |
--------------------------------------------------------------------------------
/src/views/MainView/Workspace/Tabs/Tab/SqlEditor/hint.js:
--------------------------------------------------------------------------------
1 | import CM from 'codemirror'
2 | import 'codemirror/addon/hint/show-hint.js'
3 | import 'codemirror/addon/hint/sql-hint.js'
4 | import store from '@/store'
5 |
6 | function _getHintText(hint) {
7 | return typeof hint === 'string' ? hint : hint.text
8 | }
9 | export function getHints(cm, options) {
10 | const result = CM.hint.sql(cm, options)
11 |
12 | // Don't show the hint if there is only one option
13 | // and the replacingText is already equals to this option
14 | const replacedText = cm.getRange(result.from, result.to).toUpperCase()
15 | if (
16 | result.list.length === 1 &&
17 | _getHintText(result.list[0]).toUpperCase() === replacedText
18 | ) {
19 | result.list = []
20 | }
21 |
22 | return result
23 | }
24 |
25 | const hintOptions = {
26 | get tables() {
27 | const tables = {}
28 | if (store.state.db.schema) {
29 | store.state.db.schema.forEach(table => {
30 | tables[table.name] = table.columns.map(column => column.name)
31 | })
32 | }
33 | return tables
34 | },
35 | get defaultTable() {
36 | const schema = store.state.db.schema
37 | return schema && schema.length === 1 ? schema[0].name : null
38 | },
39 | completeSingle: false,
40 | completeOnSingleClick: true,
41 | alignWithWord: false
42 | }
43 |
44 | export function showHintOnDemand(editor) {
45 | CM.showHint(editor, getHints, hintOptions)
46 | }
47 |
48 | export default function showHint(editor) {
49 | // Don't show autocomplete after a space or semicolon or in string literals
50 | const token = editor.getTokenAt(editor.getCursor())
51 | const ch = token.string.slice(-1)
52 | const tokenType = token.type
53 | if (tokenType === 'string' || !ch || ch === ' ' || ch === ';') {
54 | return
55 | }
56 |
57 | CM.showHint(editor, getHints, hintOptions)
58 | }
59 |
--------------------------------------------------------------------------------
/src/views/MainView/Workspace/Tabs/Tab/SqlEditor/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
83 |
84 |
113 |
--------------------------------------------------------------------------------
/src/views/MainView/Workspace/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
74 |
75 |
81 |
--------------------------------------------------------------------------------
/src/views/MainView/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
21 |
28 |
--------------------------------------------------------------------------------
/src/views/Welcome.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Sqliteviz is fully client-side. Your database never leaves your computer.
6 |
7 |
10 |
11 |
12 |
13 |
21 |
22 |
55 |
--------------------------------------------------------------------------------
/test.setup.js:
--------------------------------------------------------------------------------
1 | import { config } from '@vue/test-utils'
2 | import { createVfm, VueFinalModal, useVfm } from 'vue-final-modal'
3 |
4 | config.global.plugins = [createVfm()]
5 | config.global.components = {
6 | modal: VueFinalModal
7 | }
8 | const vfm = useVfm()
9 | config.global.mocks = {
10 | $modal: {
11 | show: modalId => vfm.open(modalId),
12 | hide: modalId => vfm.close(modalId)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/App.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import sinon from 'sinon'
3 | import { shallowMount } from '@vue/test-utils'
4 | import { createStore } from 'vuex'
5 | import App from '@/App'
6 | import storedInquiries from '@/lib/storedInquiries'
7 | import mutations from '@/store/mutations'
8 | import { nextTick } from 'vue'
9 |
10 | describe('App.vue', () => {
11 | afterEach(() => {
12 | sinon.restore()
13 | })
14 |
15 | it('Gets inquiries', () => {
16 | sinon
17 | .stub(storedInquiries, 'getStoredInquiries')
18 | .returns([{ id: 1 }, { id: 2 }, { id: 3 }])
19 | const state = {
20 | predefinedInquiries: [],
21 | inquiries: []
22 | }
23 | const store = createStore({ state, mutations })
24 | shallowMount(App, {
25 | global: {
26 | stubs: ['router-view'],
27 | plugins: [store]
28 | }
29 | })
30 |
31 | expect(state.inquiries).to.eql([{ id: 1 }, { id: 2 }, { id: 3 }])
32 | })
33 |
34 | it('Updates inquiries when they change in store', async () => {
35 | sinon.stub(storedInquiries, 'getStoredInquiries').returns([
36 | { id: 1, name: 'foo' },
37 | { id: 2, name: 'baz' },
38 | { id: 3, name: 'bar' }
39 | ])
40 | sinon.spy(storedInquiries, 'updateStorage')
41 |
42 | const state = {
43 | predefinedInquiries: [],
44 | inquiries: []
45 | }
46 | const store = createStore({ state, mutations })
47 | shallowMount(App, {
48 | global: { stubs: ['router-view'], plugins: [store] }
49 | })
50 |
51 | store.state.inquiries.splice(0, 1, { id: 1, name: 'new foo name' })
52 | await nextTick()
53 |
54 | expect(storedInquiries.updateStorage.calledTwice).to.equal(true)
55 |
56 | expect(storedInquiries.updateStorage.args[1][0]).to.eql([
57 | { id: 1, name: 'new foo name' },
58 | { id: 2, name: 'baz' },
59 | { id: 3, name: 'bar' }
60 | ])
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/tests/components/CheckBox.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import { shallowMount } from '@vue/test-utils'
3 | import CheckBox from '@/components/CheckBox'
4 |
5 | describe('CheckBox', () => {
6 | it('unchecked by default', () => {
7 | const wrapper = shallowMount(CheckBox, {
8 | props: { init: false },
9 | attachTo: document.body
10 | })
11 | expect(wrapper.find('img').isVisible()).to.equal(false)
12 | wrapper.unmount()
13 | })
14 |
15 | it('gets init state according to passed props', () => {
16 | const wrapperChecked = shallowMount(CheckBox, {
17 | props: { init: true },
18 | attachTo: document.body
19 | })
20 | expect(wrapperChecked.find('img.checked').isVisible()).to.equal(true)
21 | wrapperChecked.unmount()
22 |
23 | const wrapperUnchecked = shallowMount(CheckBox, {
24 | props: { init: false },
25 | attachTo: document.body
26 | })
27 | expect(wrapperUnchecked.find('img').isVisible()).to.equal(false)
28 | wrapperUnchecked.unmount()
29 | })
30 |
31 | it('checked on click', async () => {
32 | const wrapper = shallowMount(CheckBox, { attachTo: document.body })
33 | await wrapper.trigger('click')
34 | expect(wrapper.find('img.checked').isVisible()).to.equal(true)
35 | wrapper.unmount()
36 | })
37 |
38 | it('emits event on click', async () => {
39 | const wrapper = shallowMount(CheckBox)
40 | await wrapper.trigger('click')
41 | expect(wrapper.emitted().click).to.have.lengthOf(1)
42 | expect(wrapper.emitted().click[0]).to.eql([true])
43 | await wrapper.trigger('click')
44 | expect(wrapper.emitted().click).to.have.lengthOf(2)
45 | expect(wrapper.emitted().click[1]).to.eql([false])
46 | })
47 |
48 | it('disabled', async () => {
49 | const wrapper = shallowMount(CheckBox, {
50 | props: { disabled: true }
51 | })
52 | expect(wrapper.find('.checkbox-container').classes()).to.include('disabled')
53 | expect(wrapper.find('.checkbox-container').classes()).to.not.include(
54 | 'checked'
55 | )
56 | await wrapper.trigger('click')
57 | expect(wrapper.emitted().click).to.equal(undefined)
58 | expect(wrapper.find('.checkbox-container').classes()).to.not.include(
59 | 'checked'
60 | )
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/tests/components/LoadingIndicator.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import { shallowMount } from '@vue/test-utils'
3 | import LoadingIndicator from '@/components/LoadingIndicator'
4 |
5 | describe('LoadingIndicator.vue', () => {
6 | it('Calculates animation class', async () => {
7 | const wrapper = shallowMount(LoadingIndicator, {
8 | props: { progress: 0 }
9 | })
10 | expect(wrapper.find('svg').classes()).to.contain('progress')
11 | await wrapper.setProps({ progress: undefined })
12 | expect(wrapper.find('svg').classes()).to.not.contain('progress')
13 | expect(wrapper.find('svg').classes()).to.contain('loading')
14 | })
15 |
16 | it('Calculates arc', async () => {
17 | const wrapper = shallowMount(LoadingIndicator, {
18 | props: { progress: 50 }
19 | })
20 | // The lendth of circle in the component is 50.24. If progress is 50% then resulting arc
21 | // should be 25.12
22 | expect(
23 | wrapper.find('.loader-svg.front').element.style.strokeDasharray
24 | ).to.equal('25.12px, 25.12px')
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/tests/components/Logs.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import { shallowMount } from '@vue/test-utils'
3 | import Logs from '@/components/Logs'
4 | import { nextTick } from 'vue'
5 |
6 | let place
7 | describe('Logs.vue', () => {
8 | beforeEach(() => {
9 | place = document.createElement('div')
10 | document.body.appendChild(place)
11 | })
12 |
13 | afterEach(() => {
14 | place.remove()
15 | })
16 |
17 | it('Scrolled to bottom on mounted', async () => {
18 | const messages = [
19 | { type: 'error', message: 'msg 1' },
20 | { type: 'error', message: 'msg 2' },
21 | { type: 'error', message: 'msg 3' },
22 | { type: 'error', message: 'msg 4' }
23 | ]
24 |
25 | const containerHeight = 160
26 | const borderWidth = 1
27 | const viewHeight = containerHeight - 2 * borderWidth
28 | const wrapper = shallowMount(Logs, {
29 | attachTo: place,
30 | props: { messages, style: `height: ${containerHeight}px` }
31 | })
32 | await nextTick()
33 | const height = wrapper.find('.logs-container').element.scrollHeight
34 | expect(wrapper.find('.logs-container').element.scrollTop).to.equal(
35 | height - viewHeight
36 | )
37 | wrapper.unmount()
38 | })
39 |
40 | it('Scrolled to bottom when a message added', async () => {
41 | const messages = [
42 | { type: 'error', message: 'msg 1' },
43 | { type: 'error', message: 'msg 2' },
44 | { type: 'error', message: 'msg 3' },
45 | { type: 'error', message: 'msg 4' }
46 | ]
47 |
48 | const containerHeight = 160
49 | const borderWidth = 1
50 | const viewHeight = containerHeight - 2 * borderWidth
51 | const wrapper = shallowMount(Logs, {
52 | attachTo: place,
53 | props: { messages, style: `height: ${containerHeight}px` }
54 | })
55 |
56 | await nextTick()
57 | messages.push({ type: 'error', message: 'msg 5' })
58 |
59 | await nextTick()
60 | await nextTick()
61 | const height = wrapper.find('.logs-container').element.scrollHeight
62 | expect(wrapper.find('.logs-container').element.scrollTop).to.equal(
63 | height - viewHeight
64 | )
65 | wrapper.unmount()
66 | })
67 |
68 | it('Serializes messages', async () => {
69 | const messages = [
70 | { type: 'error', message: 'msg 1.', row: 0, hint: 'Try again later.' },
71 | { type: 'error', message: 'msg 2!', row: 2, hint: undefined },
72 | { type: 'error', message: 'msg 3?', hint: 'Be happy!' },
73 | { type: 'error', message: 'msg 4' }
74 | ]
75 |
76 | const wrapper = shallowMount(Logs, {
77 | props: { messages }
78 | })
79 |
80 | const logs = wrapper.findAll('.msg')
81 | expect(logs[0].text()).to.equal('Error in row 0. msg 1. Try again later.')
82 | expect(logs[1].text()).to.equal('Error in row 2. msg 2!')
83 | expect(logs[2].text()).to.equal('msg 3? Be happy!')
84 | expect(logs[3].text()).to.equal('msg 4.')
85 | })
86 | })
87 |
--------------------------------------------------------------------------------
/tests/components/Splitpanes/splitter.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import sinon from 'sinon'
3 | import splitter from '@/components/Splitpanes/splitter'
4 |
5 | describe('splitter.js', () => {
6 | afterEach(() => {
7 | sinon.restore()
8 | })
9 |
10 | it('getCurrentMouseDrag', () => {
11 | const container = document.createElement('div')
12 | container.style.width = '100px'
13 | container.style.height = '100px'
14 | container.style.position = 'fixed'
15 | container.style.top = '10px'
16 | container.style.left = '20px'
17 |
18 | document.body.appendChild(container)
19 |
20 | const event = new MouseEvent('mousemove', {
21 | clientX: 70,
22 | clientY: 80
23 | })
24 |
25 | const mouseDrag = splitter.getCurrentMouseDrag(event, container)
26 | expect(mouseDrag.x).to.equal(50)
27 | expect(mouseDrag.y).to.equal(70)
28 | })
29 |
30 | it('getCurrentDragPercentage - horisontal', () => {
31 | sinon.stub(splitter, 'getCurrentMouseDrag').returns({ x: 50, y: 70 })
32 |
33 | const event = {}
34 | const isHorisontal = true
35 | const container = document.createElement('div')
36 | container.style.width = '200px'
37 | container.style.height = '140px'
38 |
39 | document.body.appendChild(container)
40 |
41 | const dragPercentage = splitter.getCurrentDragPercentage(
42 | event,
43 | container,
44 | isHorisontal
45 | )
46 | expect(dragPercentage).to.equal(50)
47 | })
48 |
49 | it('getCurrentDragPercentage - vertical', () => {
50 | sinon.stub(splitter, 'getCurrentMouseDrag').returns({ x: 50, y: 70 })
51 |
52 | const event = {}
53 | const isHorisontal = false
54 | const container = document.createElement('div')
55 | container.style.width = '200px'
56 | container.style.height = '140px'
57 |
58 | document.body.appendChild(container)
59 |
60 | const dragPercentage = splitter.getCurrentDragPercentage(
61 | event,
62 | container,
63 | isHorisontal
64 | )
65 | expect(dragPercentage).to.equal(25)
66 | })
67 |
68 | it('calculateOffset', () => {
69 | sinon.stub(splitter, 'getCurrentDragPercentage').returns(25)
70 |
71 | const event = {}
72 | const container = {}
73 |
74 | const splitterInfo = {
75 | container,
76 | paneBeforeMax: 70,
77 | paneAfterMax: 80,
78 | isHorisontal: true
79 | }
80 | const offset = splitter.calculateOffset(event, splitterInfo)
81 |
82 | expect(offset).to.equal(25)
83 | })
84 |
85 | it('calculateOffset prevents dragging beyond paneBefore max', () => {
86 | sinon.stub(splitter, 'getCurrentDragPercentage').returns(75)
87 |
88 | const event = {}
89 | const container = {}
90 | const splitterInfo = {
91 | container,
92 | paneBeforeMax: 70,
93 | paneAfterMax: 80,
94 | isHorisontal: true
95 | }
96 | const offset = splitter.calculateOffset(event, splitterInfo)
97 |
98 | expect(offset).to.equal(70)
99 | })
100 |
101 | it('calculateOffset prevents dragging beyond paneAfter max', () => {
102 | sinon.stub(splitter, 'getCurrentDragPercentage').returns(10)
103 |
104 | const event = {}
105 | const container = {}
106 | const splitterInfo = {
107 | container,
108 | paneBeforeMax: 70,
109 | paneAfterMax: 80,
110 | isHorisontal: true
111 | }
112 | const offset = splitter.calculateOffset(event, splitterInfo)
113 |
114 | expect(offset).to.equal(20)
115 | })
116 | })
117 |
--------------------------------------------------------------------------------
/tests/components/SqlTable/Pager.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import sinon from 'sinon'
3 | import { mount } from '@vue/test-utils'
4 | import Pager from '@/components/SqlTable/Pager'
5 |
6 | describe('Pager.vue', () => {
7 | afterEach(() => {
8 | sinon.restore()
9 | })
10 |
11 | it('emits update:modelValue event with a page', async () => {
12 | const wrapper = mount(Pager, {
13 | props: {
14 | pageCount: 5,
15 | 'onUpdate:modelValue': e => wrapper.setProps({ modelValue: e })
16 | }
17 | })
18 |
19 | // click on 'next page' link
20 | await wrapper.find('.paginator-next').trigger('click')
21 | expect(wrapper.props('modelValue')).to.eql(2)
22 |
23 | // click on the link to page 3 (it has index 2)
24 | await wrapper.findAll('.paginator-page-link')[2].trigger('click')
25 | expect(wrapper.props('modelValue')).to.eql(3)
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/tests/lib/chartHelper.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import sinon from 'sinon'
3 | import chartHelper from '@/lib/chartHelper'
4 | import * as dereference from 'react-chart-editor/lib/lib/dereference'
5 |
6 | describe('chartHelper.js', () => {
7 | afterEach(() => {
8 | sinon.restore()
9 | })
10 |
11 | it('getOptionsFromDataSources', () => {
12 | const dataSources = {
13 | id: [1, 2],
14 | name: ['foo', 'bar']
15 | }
16 |
17 | const ds = chartHelper.getOptionsFromDataSources(dataSources)
18 | expect(ds).to.eql([
19 | { value: 'id', label: 'id' },
20 | { value: 'name', label: 'name' }
21 | ])
22 | })
23 |
24 | it('getOptionsForSave', () => {
25 | const state = {
26 | data: {
27 | foo: {},
28 | bar: {}
29 | },
30 | layout: {},
31 | frames: {}
32 | }
33 | const dataSources = {
34 | id: [1, 2],
35 | name: ['foo', 'bar']
36 | }
37 | sinon.stub(dereference, 'default')
38 | sinon.spy(JSON, 'parse')
39 |
40 | const ds = chartHelper.getOptionsForSave(state, dataSources)
41 |
42 | expect(dereference.default.calledOnce).to.equal(true)
43 |
44 | const args = dereference.default.firstCall.args
45 | expect(args[0]).to.eql({
46 | foo: {},
47 | bar: {}
48 | })
49 | expect(args[1]).to.eql({
50 | id: [],
51 | name: []
52 | })
53 |
54 | expect(ds).to.equal(JSON.parse.returnValues[0])
55 | })
56 |
57 | it('getImageDataUrl returns dataUrl', async () => {
58 | const element = document.createElement('div')
59 | const child = document.createElement('div')
60 | element.append(child)
61 | child.classList.add('js-plotly-plot')
62 |
63 | let url = await chartHelper.getImageDataUrl(element, 'png')
64 | expect(/^data:image\/png/.test(url)).to.equal(true)
65 |
66 | url = await chartHelper.getImageDataUrl(element, 'svg')
67 | expect(/^data:image\/svg\+xml/.test(url)).to.equal(true)
68 | })
69 |
70 | it('getChartData returns plotly data and layout from element', async () => {
71 | const element = document.createElement('div')
72 | const child = document.createElement('div')
73 | element.append(child)
74 | child.classList.add('js-plotly-plot')
75 | child.data = 'plotly data'
76 | child.layout = 'plotly layout'
77 |
78 | const chartData = chartHelper.getChartData(element)
79 | expect(chartData).to.eql({
80 | data: 'plotly data',
81 | layout: 'plotly layout'
82 | })
83 | })
84 |
85 | it('getHtml returns valid html', async () => {
86 | const options = {
87 | data: 'plotly data',
88 | layout: 'plotly layout'
89 | }
90 |
91 | const html = chartHelper.getHtml(options)
92 | const doc = document.createElement('div')
93 | doc.innerHTML = html
94 |
95 | expect(doc.innerHTML).to.equal(html)
96 | expect(doc.children).to.have.lengthOf(3)
97 | expect(doc.children[0].src).to.includes('plotly-latest.js')
98 | expect(doc.children[1].id).to.have.lengthOf(21)
99 | expect(doc.children[2].innerHTML).to.includes(doc.children[1].id)
100 | expect(doc.children[2].innerHTML).to.includes(
101 | 'Plotly.newPlot(el, "plotly data", "plotly layout"'
102 | )
103 | })
104 | })
105 |
--------------------------------------------------------------------------------
/tests/lib/database/_statements.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import stmts from '@/lib/database/_statements'
3 |
4 | describe('_statements.js', () => {
5 | it('generateChunks', () => {
6 | const source = {
7 | id: ['1', '2', '3', '4', '5']
8 | }
9 | const size = 2
10 | const chunks = stmts.generateChunks(source, size)
11 | const output = []
12 | for (const chunk of chunks) {
13 | output.push(chunk)
14 | }
15 | expect(output[0]).to.eql([['1'], ['2']])
16 | expect(output[1]).to.eql([['3'], ['4']])
17 | expect(output[2]).to.eql([['5']])
18 | })
19 |
20 | it('getInsertStmt', () => {
21 | const columns = ['id', 'name']
22 | expect(stmts.getInsertStmt('foo', columns)).to.equal(
23 | 'INSERT INTO "foo" ("id", "name") VALUES (?, ?);'
24 | )
25 | })
26 |
27 | it('getCreateStatement', () => {
28 | const data = {
29 | id: [1, 2],
30 | name: ['foo', 'bar'],
31 | isAdmin: [true, false],
32 | startDate: [new Date(), new Date()]
33 | }
34 |
35 | expect(stmts.getCreateStatement('foo', data)).to.equal(
36 | 'CREATE table "foo"("id" REAL, "name" TEXT, "isAdmin" INTEGER, "startDate" TEXT);'
37 | )
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/tests/lib/storedInquiries/_migrations.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import migrations from '@/lib/storedInquiries/_migrations'
3 |
4 | describe('_migrations.js', () => {
5 | it('migrates from version 1 to the current', () => {
6 | const oldInquiries = [
7 | {
8 | id: '123',
9 | name: 'foo',
10 | query: 'SELECT * FROM foo',
11 | chart: { here_are: 'foo chart settings' },
12 | createdAt: '2021-05-06T11:05:50.877Z'
13 | },
14 | {
15 | id: '456',
16 | name: 'bar',
17 | query: 'SELECT * FROM bar',
18 | chart: { here_are: 'bar chart settings' },
19 | createdAt: '2021-05-07T11:05:50.877Z'
20 | }
21 | ]
22 |
23 | expect(migrations._migrate(1, oldInquiries)).to.eql([
24 | {
25 | id: '123',
26 | name: 'foo',
27 | query: 'SELECT * FROM foo',
28 | viewType: 'chart',
29 | viewOptions: { here_are: 'foo chart settings' },
30 | createdAt: '2021-05-06T11:05:50.877Z'
31 | },
32 | {
33 | id: '456',
34 | name: 'bar',
35 | query: 'SELECT * FROM bar',
36 | viewType: 'chart',
37 | viewOptions: { here_are: 'bar chart settings' },
38 | createdAt: '2021-05-07T11:05:50.877Z'
39 | }
40 | ])
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/tests/lib/utils/clipboardIo.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import cIo from '@/lib/utils/clipboardIo'
3 | import sinon from 'sinon'
4 |
5 | describe('clipboardIo.js', async () => {
6 | afterEach(() => {
7 | sinon.restore()
8 | })
9 |
10 | it('copyText', async () => {
11 | sinon.stub(navigator.clipboard, 'writeText').resolves(true)
12 | await cIo.copyText('id\tname\r\n1\t2')
13 | expect(navigator.clipboard.writeText.calledOnceWith('id\tname\r\n1\t2'))
14 | })
15 |
16 | it('copyImage for canvas calls _copyCanvas', async () => {
17 | sinon.stub(cIo, '_copyCanvas').resolves(true)
18 | const canvas = document.createElement('canvas')
19 |
20 | await cIo.copyImage(canvas)
21 | expect(cIo._copyCanvas.calledOnceWith(canvas))
22 | })
23 |
24 | it('copyImage for dataUrl calls _copyFromDataUrl', async () => {
25 | sinon.stub(cIo, '_copyFromDataUrl').resolves(true)
26 | const url = document.createElement('canvas').toDataURL()
27 | await cIo.copyImage(url)
28 | expect(cIo._copyFromDataUrl.calledOnceWith(url))
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/tests/lib/utils/time.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import time from '@/lib/utils/time'
3 |
4 | describe('time.js', () => {
5 | it('getPeriod', () => {
6 | // 1.01.2021 13:00:00 000
7 | let start = new Date(2021, 0, 1, 13, 0, 0, 0)
8 |
9 | // 1.01.2021 13:01:00 500
10 | let end = new Date(2021, 0, 1, 13, 1, 0, 500)
11 |
12 | expect(time.getPeriod(start, end)).to.equal('60.500s')
13 |
14 | // 1.01.2021 13:00:00 000
15 | start = new Date(2021, 0, 1, 13, 0, 0, 0)
16 |
17 | // 1.01.2021 13:00:20 500
18 | end = new Date(2021, 0, 1, 13, 0, 20, 500)
19 |
20 | expect(time.getPeriod(start, end)).to.equal('20.500s')
21 |
22 | // 1.01.2021 13:00:00 000
23 | start = new Date(2021, 0, 1, 13, 0, 0, 0)
24 |
25 | // 1.01.2021 13:00:00 45
26 | end = new Date(2021, 0, 1, 13, 0, 0, 45)
27 |
28 | expect(time.getPeriod(start, end)).to.equal('0.045s')
29 | })
30 |
31 | it('sleep resolves after n ms', async () => {
32 | let before = performance.now()
33 | await time.sleep(10)
34 | expect(performance.now() - before).to.be.least(10)
35 |
36 | before = performance.now()
37 | await time.sleep(30)
38 | expect(performance.now() - before).to.be.least(30)
39 | })
40 | })
41 |
--------------------------------------------------------------------------------
/tests/views/MainView/Workspace/Schema/TableDescription.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import { shallowMount } from '@vue/test-utils'
3 | import TableDescription from '@/views/MainView/Workspace/Schema/TableDescription'
4 |
5 | describe('TableDescription.vue', () => {
6 | it('Initially the columns are hidden and table name is rendered', () => {
7 | const wrapper = shallowMount(TableDescription, {
8 | attachTo: document.body,
9 | props: {
10 | name: 'Test table',
11 | columns: [
12 | { name: 'id', type: 'number' },
13 | { name: 'title', type: 'nvarchar(24)' }
14 | ]
15 | }
16 | })
17 | expect(wrapper.find('.table-name').text()).to.equal('Test table')
18 | expect(wrapper.find('.columns').isVisible()).to.equal(false)
19 | wrapper.unmount()
20 | })
21 |
22 | it('Columns are visible and correct when click on table name', async () => {
23 | const wrapper = shallowMount(TableDescription, {
24 | global: {
25 | stubs: ['router-link']
26 | },
27 | props: {
28 | name: 'Test table',
29 | columns: [
30 | { name: 'id', type: 'number' },
31 | { name: 'title', type: 'nvarchar(24)' }
32 | ]
33 | }
34 | })
35 | await wrapper.find('.table-name').trigger('click')
36 |
37 | expect(wrapper.find('.columns').isVisible()).to.equal(true)
38 | expect(wrapper.findAll('.column').length).to.equal(2)
39 | expect(wrapper.findAll('.column')[0].text())
40 | .to.include('id')
41 | .and.include('number')
42 | expect(wrapper.findAll('.column')[1].text())
43 | .to.include('title')
44 | .and.include('nvarchar(24)')
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/tests/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/PivotUi/PivotSortBtn.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import { shallowMount } from '@vue/test-utils'
3 | import PivotSortBtn from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/PivotUi/PivotSortBtn'
4 |
5 | describe('PivotSortBtn.vue', () => {
6 | it('switches order', async () => {
7 | const wrapper = shallowMount(PivotSortBtn, {
8 | props: {
9 | modelValue: 'key_a_to_z',
10 | 'onUpdate:modelValue': e => wrapper.setProps({ modelValue: e })
11 | }
12 | })
13 |
14 | expect(wrapper.props('modelValue')).to.equal('key_a_to_z')
15 | await wrapper.find('.pivot-sort-btn').trigger('click')
16 | expect(wrapper.props('modelValue')).to.equal('value_a_to_z')
17 |
18 | await wrapper.setValue('value_a_to_z')
19 | await wrapper.find('.pivot-sort-btn').trigger('click')
20 | expect(wrapper.props('modelValue')).to.equal('value_z_to_a')
21 |
22 | await wrapper.setValue('value_z_to_a')
23 | await wrapper.find('.pivot-sort-btn').trigger('click')
24 | expect(wrapper.props('modelValue')).to.equal('key_a_to_z')
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/tests/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/pivotHelper.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import {
3 | _getDataSources,
4 | getPivotCanvas,
5 | getPivotHtml
6 | } from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/pivotHelper'
7 |
8 | describe('pivotHelper.js', () => {
9 | it('_getDataSources returns data sources', () => {
10 | /*
11 | +---+---+---------+---------+
12 | | | x | 5 | 10 |
13 | | +---+----+----+----+----+
14 | | | z | 2 | 3 | 1 | 6 |
15 | +---+---+ | | | |
16 | | y | | | | | |
17 | +---+---+----+----+----+----+
18 | | 3 | 5 | 6 | 4 | 9 |
19 | +-------+----+----+----+----+
20 | | 6 | 8 | 9 | 7 | 12 |
21 | +-------+----+----+----+----+
22 | | 9 | 11 | 12 | 10 | 15 |
23 | +-------+----+----+----+----+
24 | */
25 | const pivotData = {
26 | rowAttrs: ['y'],
27 | colAttrs: ['x', 'z'],
28 | getRowKeys() {
29 | return [[3], [6], [9]]
30 | },
31 | getColKeys() {
32 | return [
33 | [5, 2],
34 | [5, 3],
35 | [10, 1],
36 | [10, 6]
37 | ]
38 | },
39 | getAggregator(row, col) {
40 | return {
41 | value() {
42 | return +row + +col[1]
43 | }
44 | }
45 | }
46 | }
47 |
48 | expect(_getDataSources(pivotData)).to.eql({
49 | 'Column keys': ['5-2', '5-3', '10-1', '10-6'],
50 | 'Row keys': ['3', '6', '9'],
51 | 'x-z:5-2': [5, 8, 11],
52 | 'x-z:5-3': [6, 9, 12],
53 | 'x-z:10-1': [4, 7, 10],
54 | 'x-z:10-6': [9, 12, 15],
55 | 'y:3': [5, 6, 4, 9],
56 | 'y:6': [8, 9, 7, 12],
57 | 'y:9': [11, 12, 10, 15]
58 | })
59 | })
60 |
61 | it('getPivotCanvas returns canvas', async () => {
62 | const pivotOutput = document.body
63 | const child = document.createElement('div')
64 | child.classList.add('pvtTable')
65 | pivotOutput.append(child)
66 |
67 | expect(await getPivotCanvas(pivotOutput)).to.be.instanceof(
68 | HTMLCanvasElement
69 | )
70 | })
71 |
72 | it('getPivotHtml returns html with styles', async () => {
73 | const pivotOutput = document.createElement('div')
74 | pivotOutput.append('test')
75 |
76 | const html = getPivotHtml(pivotOutput)
77 | const doc = document.createElement('div')
78 | doc.innerHTML = html
79 |
80 | expect(doc.innerHTML).to.equal(html)
81 | expect(doc.children).to.have.lengthOf(2)
82 | expect(doc.children[0].tagName).to.equal('STYLE')
83 | expect(doc.children[1].tagName).to.equal('DIV')
84 | expect(doc.children[1].innerHTML).to.equal('test')
85 | })
86 | })
87 |
--------------------------------------------------------------------------------
/tests/views/MainView/Workspace/Tabs/Tab/RunResult/ValueViewer.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import { mount } from '@vue/test-utils'
3 | import ValueViewer from '@/views/MainView/Workspace/Tabs/Tab/RunResult/ValueViewer.vue'
4 | import sinon from 'sinon'
5 |
6 | describe('ValueViewer.vue', () => {
7 | afterEach(() => {
8 | sinon.restore()
9 | })
10 |
11 | it('shows value in text mode', () => {
12 | const wrapper = mount(ValueViewer, {
13 | props: {
14 | cellValue: 'foo'
15 | }
16 | })
17 |
18 | expect(wrapper.find('.value-body').text()).to.equals('foo')
19 | })
20 |
21 | it('shows error in json mode if the value is not json', async () => {
22 | const wrapper = mount(ValueViewer, {
23 | props: {
24 | cellValue: 'foo'
25 | }
26 | })
27 | await wrapper.find('button.json').trigger('click')
28 | expect(wrapper.find('.value-body').text()).to.equals("Can't parse JSON.")
29 | })
30 |
31 | it('copy to clipboard', async () => {
32 | sinon.stub(window.navigator.clipboard, 'writeText').resolves()
33 | const wrapper = mount(ValueViewer, {
34 | props: {
35 | cellValue: 'foo'
36 | }
37 | })
38 |
39 | await wrapper.find('button.copy').trigger('click')
40 |
41 | expect(window.navigator.clipboard.writeText.calledOnceWith('foo')).to.equal(
42 | true
43 | )
44 | })
45 |
46 | it('wraps lines', async () => {
47 | const wrapper = mount(ValueViewer, {
48 | attachTo: document.body,
49 | props: {
50 | cellValue: 'foo'
51 | }
52 | })
53 |
54 | wrapper.wrapperElement.parentElement.style.width = '50px'
55 | const valueBody = wrapper.find('.value-body').wrapperElement
56 | expect(valueBody.scrollWidth).to.equal(valueBody.clientWidth)
57 |
58 | await wrapper.setProps({ cellValue: 'foo'.repeat(100) })
59 | expect(valueBody.scrollWidth).not.to.equal(valueBody.clientWidth)
60 |
61 | await wrapper.find('button.line-wrap').trigger('click')
62 | expect(valueBody.scrollWidth).to.equal(valueBody.clientWidth)
63 | wrapper.unmount()
64 | })
65 |
66 | it('wraps lines in code mirror', async () => {
67 | const wrapper = mount(ValueViewer, {
68 | attachTo: document.body,
69 | props: {
70 | cellValue: '{"foo": "foofoofoofoofoofoofoofoofoofoo"}'
71 | }
72 | })
73 |
74 | await wrapper.find('button.json').trigger('click')
75 |
76 | wrapper.wrapperElement.parentElement.style.width = '50px'
77 | const codeMirrorScroll = wrapper.find('.CodeMirror-scroll').wrapperElement
78 | expect(codeMirrorScroll.scrollWidth).not.to.equal(
79 | codeMirrorScroll.clientWidth
80 | )
81 |
82 | await wrapper.find('button.line-wrap').trigger('click')
83 | expect(codeMirrorScroll.scrollWidth).to.equal(codeMirrorScroll.clientWidth)
84 | wrapper.unmount()
85 | })
86 | })
87 |
--------------------------------------------------------------------------------
/tests/views/MainView/Workspace/Tabs/Tab/SqlEditor/SqlEditor.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import { mount } from '@vue/test-utils'
3 | import { createStore } from 'vuex'
4 | import SqlEditor from '@/views/MainView/Workspace/Tabs/Tab/SqlEditor'
5 | import { nextTick } from 'vue'
6 |
7 | describe('SqlEditor.vue', () => {
8 | it('Emits update:modelValue event when a query is changed', async () => {
9 | // mock store state
10 | const state = {
11 | db: {}
12 | }
13 |
14 | const store = createStore({ state })
15 |
16 | const wrapper = mount(SqlEditor, {
17 | global: {
18 | plugins: [store]
19 | }
20 | })
21 | await wrapper
22 | .findComponent({ ref: 'cm' })
23 | .setValue('SELECT * FROM foo', 'value')
24 | expect(wrapper.emitted()['update:modelValue'][0]).to.eql([
25 | 'SELECT * FROM foo'
26 | ])
27 | })
28 |
29 | it('Run is disabled if there is no db or no query or is getting result set', async () => {
30 | const state = {
31 | db: null
32 | }
33 | const store = createStore({ state })
34 |
35 | const wrapper = mount(SqlEditor, {
36 | global: { plugins: [store] },
37 | props: { isGettingResults: false }
38 | })
39 | await wrapper
40 | .findComponent({ ref: 'cm' })
41 | .setValue('SELECT * FROM foo', 'value')
42 | const runButton = wrapper.findComponent({ ref: 'runBtn' })
43 |
44 | expect(runButton.props('disabled')).to.equal(true)
45 |
46 | store.state.db = {}
47 | await nextTick()
48 | expect(runButton.props('disabled')).to.equal(false)
49 |
50 | await wrapper.findComponent({ ref: 'cm' }).setValue('', 'value')
51 | expect(runButton.props('disabled')).to.equal(true)
52 |
53 | await wrapper
54 | .findComponent({ ref: 'cm' })
55 | .setValue('SELECT * FROM foo', 'value')
56 | expect(runButton.props('disabled')).to.equal(false)
57 |
58 | await wrapper.setProps({ isGettingResults: true })
59 | expect(runButton.props('disabled')).to.equal(true)
60 | })
61 | })
62 |
--------------------------------------------------------------------------------
/tests/views/MainView/Workspace/Workspace.spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai'
2 | import { mount } from '@vue/test-utils'
3 | import actions from '@/store/actions'
4 | import mutations from '@/store/mutations'
5 | import { createStore } from 'vuex'
6 | import Workspace from '@/views/MainView/Workspace'
7 |
8 | describe('Workspace.vue', () => {
9 | it('Creates a tab with example if schema is empty', () => {
10 | const state = {
11 | db: {},
12 | tabs: []
13 | }
14 | const store = createStore({ state, actions, mutations })
15 | const $route = { path: '/workspace', query: {} }
16 | mount(Workspace, {
17 | global: {
18 | stubs: ['router-link', 'modal'],
19 | mocks: { $route },
20 | plugins: [store]
21 | }
22 | })
23 |
24 | expect(state.tabs[0].query).to.include('Your database is empty.')
25 | expect(state.tabs[0].tempName).to.equal('Untitled')
26 | expect(state.tabs[0].name).to.equal(null)
27 | expect(state.tabs[0].viewType).to.equal('chart')
28 | expect(state.tabs[0].viewOptions).to.equal(undefined)
29 | expect(state.tabs[0].isSaved).to.equal(false)
30 | })
31 |
32 | it('Collapse schema if hide_schema is 1', () => {
33 | const state = {
34 | db: {},
35 | tabs: []
36 | }
37 | const store = createStore({ state, actions, mutations })
38 | const $route = { path: '/workspace', query: { hide_schema: '1' } }
39 | const vm = mount(Workspace, {
40 | global: {
41 | stubs: ['router-link', 'modal'],
42 | mocks: { $route },
43 | plugins: [store]
44 | }
45 | })
46 |
47 | expect(vm.find('#schema-container').element.offsetWidth).to.equal(0)
48 | })
49 | })
50 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { fileURLToPath, URL } from 'node:url'
2 | import { defineConfig } from 'vite'
3 | import vue from '@vitejs/plugin-vue'
4 | import { nodePolyfills } from 'vite-plugin-node-polyfills'
5 | import { viteStaticCopy } from 'vite-plugin-static-copy'
6 | import { VitePWA } from 'vite-plugin-pwa'
7 |
8 | // https://vitejs.dev/config/
9 | export default defineConfig({
10 | base: './',
11 | plugins: [
12 | vue(),
13 | nodePolyfills({
14 | include: ['process', 'util', 'stream', 'buffer'],
15 | globals: { global: true, process: true }
16 | }),
17 | viteStaticCopy({
18 | targets: [
19 | {
20 | src: 'LICENSE',
21 | dest: './'
22 | }
23 | ]
24 | }),
25 | VitePWA({
26 | filename: 'service-worker.js',
27 | manifest: false,
28 | injectRegister: false,
29 | workbox: {
30 | globPatterns: ['**\/*.{js,wasm,css,html,woff2,png}'],
31 | globIgnores: ['*.map', 'LICENSE', 'inquiries.json'],
32 | clientsClaim: true,
33 | skipWaiting: false,
34 | maximumFileSizeToCacheInBytes: 40000000
35 | }
36 | })
37 | ],
38 | resolve: {
39 | alias: {
40 | '@': fileURLToPath(new URL('./src', import.meta.url))
41 | },
42 | extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
43 | },
44 | optimizeDeps: {
45 | include: ['sql.js'],
46 | esbuildOptions: {
47 | define: {
48 | global: 'globalThis'
49 | }
50 | }
51 | },
52 | build: {
53 | sourcemap: true,
54 | assetsInlineLimit: 10000,
55 | commonjsOptions: {
56 | include: ['sql.js', /sql-wasm.js/, /node_modules/]
57 | },
58 | rollupOptions: {
59 | output: {
60 | manualChunks: id => {
61 | if (id.includes('maplibre-gl') || id.includes('mapbox-gl')) {
62 | return 'maps'
63 | } else if (id.includes('node_modules')) {
64 | return 'vendor'
65 | } else {
66 | return null
67 | }
68 | }
69 | }
70 | }
71 | }
72 | })
73 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const { defineConfig } = require('@vue/cli-service')
2 |
3 | module.exports = defineConfig({
4 | parallel: false,
5 | transpileDependencies: true,
6 | publicPath: '',
7 | // Workaround for https://github.com/vuejs/vue-cli/issues/5399 as described
8 | // in https://stackoverflow.com/a/63185174
9 | lintOnSave: process.env.NODE_ENV === 'development'
10 | })
11 |
--------------------------------------------------------------------------------