1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-06 10:08:52 +08:00
This commit is contained in:
lana-k
2025-03-20 22:04:15 +01:00
parent 5e2b34a856
commit 0c1b91ab2f
146 changed files with 3317 additions and 2438 deletions

View File

@@ -2,12 +2,9 @@ module.exports = {
root: true,
env: {
node: true,
es2022: true,
es2022: true
},
extends: [
'plugin:vue/essential',
'@vue/standard'
],
extends: ['eslint:recommended', 'plugin:vue/vue3-recommended', 'prettier'],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
@@ -20,10 +17,7 @@ module.exports = {
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/**/*.spec.{j,t}s?(x)'
],
files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/**/*.spec.{j,t}s?(x)'],
env: {
mocha: true
}

View File

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

View File

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

View File

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

7
.prettierrc Normal file
View File

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

View File

@@ -5,9 +5,10 @@
# sqliteviz
Sqliteviz is a single-page offline-first PWA for fully client-side visualisation
of SQLite databases, CSV, JSON or NDJSON files.
of SQLite databases, CSV, JSON or NDJSON files.
With sqliteviz you can:
- run SQL queries against a SQLite database and create [Plotly][11] charts and pivot tables based on the result sets
- import a CSV/JSON/NDJSON file into a SQLite database and visualize imported data
- export result set to CSV file
@@ -19,15 +20,19 @@ With sqliteviz you can:
https://user-images.githubusercontent.com/24638357/128249848-f8fab0f5-9add-46e0-a9c1-dd5085a8623e.mp4
## Quickstart
The latest release of sqliteviz is deployed on [sqliteviz.com/app][6].
## Wiki
For user documentation, check out sqliteviz [documentation][7].
## Motivation
It's a kind of middleground between [Plotly Falcon][1] and [Redash][2].
## Components
It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4] and [Vue-Codemirror][8] in [Vue.js][5]. CSV parsing is performed with [Papa Parse][9].
[1]: https://github.com/plotly/falcon

View File

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

View File

@@ -1,11 +1,11 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="favicon.png">
<link rel="manifest" href="manifest.webmanifest">
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="favicon.png" />
<link rel="manifest" href="manifest.webmanifest" />
<title>sqliteviz</title>
<style>
#sqliteviz-loading-wrapper {
@@ -38,15 +38,18 @@
#sqliteviz-loading-wrapper circle {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
left: 0;
right: 0;
top: 0;
bottom: 0;
fill: none;
stroke-width: 5px;
stroke-linecap: round;
stroke: #119DFF;
stroke: #119dff;
}
#sqliteviz-loading-wrapper circle.bg {
stroke: #C8D4E3;
stroke: #c8d4e3;
}
#sqliteviz-loading-wrapper circle.front {
@@ -74,24 +77,17 @@
</head>
<body>
<noscript>
<strong>We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
<strong>
We're sorry but this app doesn't work properly without JavaScript
enabled. Please enable it to continue.
</strong>
</noscript>
<div id="app">
<div id="sqliteviz-loading-wrapper">
<div id="sqliteviz-loading-text">LOADING</div>
<svg height="170" width="170" viewBox="0 0 170 170">
<circle
class="bg"
cx="85"
cy="85"
r="80"
/>
<circle
class="front"
cx="85"
cy="85"
r="80"
/>
<circle class="bg" cx="85" cy="85" r="80" />
<circle class="front" cx="85" cy="85" r="80" />
</svg>
</div>
</div>

10
jsconfig.json Normal file
View File

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

View File

@@ -4,7 +4,7 @@ module.exports = function (config) {
config: {
resolve: {
alias: {
'vue': 'vue/dist/vue.esm-bundler.js'
vue: 'vue/dist/vue.esm-bundler.js'
}
},
server: {
@@ -32,19 +32,19 @@ module.exports = function (config) {
pattern: 'test.setup.js',
type: 'module',
watched: false,
served: false,
served: false
},
{
pattern: 'tests/**/*.spec.js',
type: 'module',
watched: false,
served: false,
served: false
},
{
pattern: 'src/assets/styles/*.css',
type: 'css',
watched: false,
served: false,
served: false
}
],

View File

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

View File

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

View File

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

View File

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

58
package-lock.json generated
View File

@@ -42,6 +42,7 @@
"chai": "^4.1.2",
"chai-as-promised": "^8.0.1",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
@@ -56,6 +57,7 @@
"karma-spec-reporter": "^0.0.36",
"karma-vite": "^1.0.5",
"mocha": "^5.2.0",
"prettier": "3.5.3",
"process": "^0.11.10",
"url-loader": "^4.1.1",
"vite": "^5.4.14",
@@ -3421,6 +3423,22 @@
"url": "https://opencollective.com/postcss/"
}
},
"node_modules/@vue/component-compiler-utils/node_modules/prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
"optional": true,
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/@vue/component-compiler-utils/node_modules/yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
@@ -7967,6 +7985,18 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-config-prettier": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz",
"integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==",
"dev": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
"peerDependencies": {
"eslint": ">=7.0.0"
}
},
"node_modules/eslint-import-resolver-custom-alias": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-custom-alias/-/eslint-import-resolver-custom-alias-1.3.2.tgz",
@@ -14788,16 +14818,15 @@
}
},
"node_modules/prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"optional": true,
"bin": {
"prettier": "bin-prettier.js"
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=10.13.0"
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
@@ -20029,6 +20058,23 @@
"node": ">=0.10.0"
}
},
"node_modules/vue-cli-plugin-ui-karma/node_modules/prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
"optional": true,
"peer": true,
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/vue-cli-plugin-ui-karma/node_modules/read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",

View File

@@ -9,7 +9,8 @@
"build": "vite build",
"serve": "vite preview",
"test": "karma start karma.conf.cjs",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src"
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
"format": "prettier . --write"
},
"dependencies": {
"buffer": "^6.0.3",
@@ -45,6 +46,7 @@
"chai": "^4.1.2",
"chai-as-promised": "^8.0.1",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
@@ -59,6 +61,7 @@
"karma-spec-reporter": "^0.0.36",
"karma-vite": "^1.0.5",
"mocha": "^5.2.0",
"prettier": "3.5.3",
"process": "^0.11.10",
"url-loader": "^4.1.1",
"vite": "^5.4.14",

View File

@@ -1,7 +1,7 @@
<template>
<div id="app">
<router-view/>
<modals-container/>
<router-view />
<modals-container />
</div>
</template>
@@ -11,18 +11,18 @@ import { ModalsContainer } from 'vue-final-modal'
export default {
components: { ModalsContainer },
created () {
created() {
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
},
computed: {
inquiries () {
inquiries() {
return this.$store.state.inquiries
}
},
watch: {
inquiries: {
deep: true,
handler () {
handler() {
storedInquiries.updateStorage(this.inquiries)
}
}
@@ -32,43 +32,43 @@ export default {
<style>
@font-face {
font-family: "Open Sans";
src: url("@/assets/fonts/OpenSans-Regular.woff2");
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Regular.woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: "Open Sans";
src: url("@/assets/fonts/OpenSans-SemiBold.woff2");
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-SemiBold.woff2');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: "Open Sans";
src: url("@/assets/fonts/OpenSans-Bold.woff2");
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Bold.woff2');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: "Open Sans";
src: url("@/assets/fonts/OpenSans-Italic.woff2");
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Italic.woff2');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: "Open Sans";
src: url("@/assets/fonts/OpenSans-SemiBoldItalic.woff2");
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-SemiBoldItalic.woff2');
font-weight: 600;
font-style: italic;
}
@font-face {
font-family: "Open Sans";
src: url("@/assets/fonts/OpenSans-BoldItalic.woff2");
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-BoldItalic.woff2');
font-weight: 700;
font-style: italic;
}
@@ -80,7 +80,7 @@ label,
button,
.plotly_editor *,
.CodeMirror pre.CodeMirror-line {
font-family: "Open Sans", Helvetica, Arial, sans-serif;
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

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

View File

@@ -123,7 +123,7 @@
border-radius: var(--border-radius-medium-2);
}
.sqliteviz-select .multiselect__option .no-results {
.sqliteviz-select .multiselect__option .no-results {
color: var(--color-text-light-2);
}

View File

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

View File

@@ -4,7 +4,7 @@
text-align: center;
font-size: 12px;
padding: 0 6px;
line-height: 19px;;
line-height: 19px;
position: fixed;
height: 19px;
border-radius: var(--border-radius-medium);

View File

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

View File

@@ -1,6 +1,10 @@
<template>
<div
:class="['checkbox-container', { 'checked': checked }, {'disabled': disabled}]"
:class="[
'checkbox-container',
{ checked: checked },
{ disabled: disabled }
]"
@click.stop="onClick"
>
<div v-show="!checked" class="unchecked" />
@@ -31,7 +35,7 @@ export default {
type: String,
required: false,
default: 'accent',
validator: (value) => {
validator: value => {
return ['accent', 'light'].includes(value)
}
},
@@ -52,13 +56,13 @@ export default {
}
},
emits: ['click'],
data () {
data() {
return {
checked: this.init
}
},
methods: {
onClick () {
onClick() {
if (!this.disabled) {
this.checked = !this.checked
this.$emit('click', this.checked)
@@ -86,7 +90,7 @@ export default {
}
img {
display: block;
display: block;
}
.label {
margin-left: 6px;
@@ -106,6 +110,6 @@ img {
.disabled .unchecked,
.disabled .unchecked:hover {
background-color: var(--color-bg-light-2);
background-color: var(--color-bg-light-2);
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div :class="{ 'disabled': disabled }">
<div :class="{ disabled: disabled }">
<div class="text-field-label">Delimiter</div>
<div
class="delimiter-selector-container"
@@ -8,7 +8,7 @@
>
<div class="value">
<input
:class="{ 'filled': filled }"
:class="{ filled: filled }"
ref="delimiterInput"
type="text"
maxlength="1"
@@ -19,7 +19,7 @@
<div class="name">{{ getSymbolName(modelValue) }}</div>
</div>
<div class="controls" @click.stop>
<clear-icon @click="clear" :disabled="disabled"/>
<clear-icon @click="clear" :disabled="disabled" />
<drop-down-chevron
:disabled="disabled"
@click="!disabled && (showOptions = !showOptions)"
@@ -33,7 +33,8 @@
@click="chooseOption(option)"
class="option"
>
<pre>{{option}}</pre><div>{{ getSymbolName(option) }}</div>
<pre>{{ option }}</pre>
<div>{{ getSymbolName(option) }}</div>
</div>
</div>
</div>
@@ -49,7 +50,7 @@ export default {
props: ['modelValue', 'width', 'disabled'],
emits: ['update:modelValue'],
components: { DropDownChevron, ClearIcon },
data () {
data() {
return {
showOptions: false,
options: [',', '\t', ' ', '|', ';', '\u001F', '\u001E'],
@@ -58,7 +59,7 @@ export default {
}
},
watch: {
inputValue () {
inputValue() {
if (this.inputValue) {
this.filled = true
if (this.inputValue !== this.modelValue) {
@@ -69,25 +70,25 @@ export default {
}
}
},
created () {
created() {
this.inputValue = this.modelValue
},
methods: {
getSymbolName (str) {
getSymbolName(str) {
if (!str) {
return ''
}
return ascii[str.charCodeAt(0).toString()].name
},
chooseOption (option) {
chooseOption(option) {
this.inputValue = option
this.showOptions = false
},
onContainerClick (event) {
onContainerClick(event) {
this.$refs.delimiterInput.focus()
},
clear () {
clear() {
if (!this.disabled) {
this.inputValue = ''
this.$refs.delimiterInput.focus()

View File

@@ -8,7 +8,7 @@
>
<div class="dialog-header">
{{ typeName }} import
<close-icon @click="cancelImport" :disabled="disableDialog"/>
<close-icon @click="cancelImport" :disabled="disableDialog" />
</div>
<div class="dialog-body">
<text-field
@@ -66,10 +66,7 @@
class="preview-table"
/>
<div v-else class="no-data">No data</div>
<logs
class="import-errors"
:messages="importMessages"
/>
<logs class="import-errors" :messages="importMessages" />
</div>
<div class="dialog-buttons-container">
<button
@@ -130,7 +127,7 @@ export default {
dialogName: String
},
emits: ['cancel', 'finish'],
data () {
data() {
return {
disableDialog: false,
disableImport: false,
@@ -147,24 +144,24 @@ export default {
}
},
computed: {
isJson () {
isJson() {
return fIo.isJSON(this.file)
},
isNdJson () {
isNdJson() {
return fIo.isNDJSON(this.file)
},
typeName () {
typeName() {
return this.isJson || this.isNdJson ? 'JSON' : 'CSV'
}
},
watch: {
isJson () {
isJson() {
if (this.isJson) {
this.delimiter = '\u001E'
this.header = false
}
},
isNdJson () {
isNdJson() {
if (this.isNdJson) {
this.delimiter = '\u001E'
this.header = false
@@ -175,18 +172,17 @@ export default {
if (!this.tableName) {
return
}
this.db.validateTableName(this.tableName)
.catch(err => {
this.tableNameError = err.message + '. Try another table name.'
})
this.db.validateTableName(this.tableName).catch(err => {
this.tableNameError = err.message + '. Try another table name.'
})
}, 400)
},
methods: {
changeHeaderDisplaying (e) {
changeHeaderDisplaying(e) {
this.header = e
this.preview()
},
cancelImport () {
cancelImport() {
if (!this.disableDialog) {
if (this.addedTable) {
this.db.execute(`DROP TABLE "${this.addedTable}"`)
@@ -196,7 +192,7 @@ export default {
this.$emit('cancel')
}
},
reset () {
reset() {
this.header = !this.isJson && !this.isNdJson
this.quoteChar = '"'
this.escapeChar = '"'
@@ -210,11 +206,11 @@ export default {
this.addedTable = null
this.tableNameError = ''
},
open () {
open() {
this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file))
this.$modal.show(this.dialogName)
},
async preview () {
async preview() {
this.disableImport = false
if (!this.file) {
return
@@ -257,13 +253,15 @@ export default {
}
} catch (err) {
console.error(err)
this.importMessages = [{
message: err,
type: 'error'
}]
this.importMessages = [
{
message: err,
type: 'error'
}
]
}
},
async getJsonParseResult (file) {
async getJsonParseResult(file) {
const jsonContent = await fIo.getFileContent(file)
const isEmpty = !jsonContent.trim()
return {
@@ -273,10 +271,10 @@ export default {
},
hasErrors: false,
messages: [],
rowCount: +(!isEmpty)
rowCount: +!isEmpty
}
},
async loadToDb (file) {
async loadToDb(file) {
if (!this.tableName) {
this.tableNameError = "Table name can't be empty"
return
@@ -297,7 +295,9 @@ export default {
})
// Get *reactive* link to parsing message for later updates
parsingMsg = this.importMessages[this.importMessages.length - 1]
const parsingLoadingIndicator = setTimeout(() => { parsingMsg.type = 'loading' }, 1000)
const parsingLoadingIndicator = setTimeout(() => {
parsingMsg.type = 'loading'
}, 1000)
let importMsg = {}
let importLoadingIndicator = null
@@ -321,7 +321,9 @@ export default {
parsingMsg.type = 'success'
if (parseResult.messages.length > 0) {
this.importMessages = this.importMessages.concat(parseResult.messages)
this.importMessages = this.importMessages.concat(
parseResult.messages
)
parsingMsg.message = `${rowCount} rows are parsed in ${period}.`
} else {
// Inform about parsing success
@@ -345,14 +347,19 @@ export default {
// Add table
start = new Date()
await this.db.addTableFromCsv(this.tableName, parseResult.data, progressCounterId)
await this.db.addTableFromCsv(
this.tableName,
parseResult.data,
progressCounterId
)
end = new Date()
this.addedTable = this.tableName
// Inform about import success
period = time.getPeriod(start, end)
importMsg.message = `Importing ${this.typeName} ` +
`into a SQLite database is completed in ${period}.`
importMsg.message =
`Importing ${this.typeName} ` +
`into a SQLite database is completed in ${period}.`
importMsg.type = 'success'
// Loading indicator for import is not needed anymore
@@ -385,7 +392,7 @@ export default {
this.db.deleteProgressCounter(progressCounterId)
this.disableDialog = false
},
async finish () {
async finish() {
this.$modal.hide(this.dialogName)
const stmt = this.getQueryExample()
const tabId = await this.$store.dispatch('addTab', { query: stmt })
@@ -394,20 +401,20 @@ export default {
this.$emit('finish')
events.send('inquiry.create', null, { auto: true })
},
getQueryExample () {
getQueryExample() {
return this.isNdJson
? this.getNdJsonQueryExample()
: this.isJson
? this.getJsonQueryExample()
: [
'/*',
` * Your CSV file has been imported into ${this.addedTable} table.`,
' * You can run this SQL query to make all CSV records available for charting.',
' */',
`SELECT * FROM "${this.addedTable}"`
` * Your CSV file has been imported into ${this.addedTable} table.`,
' * You can run this SQL query to make all CSV records available for charting.',
' */',
`SELECT * FROM "${this.addedTable}"`
].join('\n')
},
getNdJsonQueryExample () {
getNdJsonQueryExample() {
try {
const firstRowJson = JSON.parse(this.previewData.values.doc[0])
const firstKey = Object.keys(firstRowJson)[0]
@@ -415,7 +422,7 @@ export default {
'/*',
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
` * Run this SQL query to get values of property ${firstKey} ` +
'and make them available for charting.',
'and make them available for charting.',
' */',
`SELECT doc->>'${firstKey}'`,
`FROM "${this.addedTable}"`
@@ -431,7 +438,7 @@ export default {
].join('\n')
}
},
getJsonQueryExample () {
getJsonQueryExample() {
try {
const firstRowJson = JSON.parse(this.previewData.values.doc[0])
const firstKey = Object.keys(firstRowJson)[0]
@@ -439,7 +446,7 @@ export default {
'/*',
` * Your JSON file has been imported into ${this.addedTable} table.`,
` * Run this SQL query to get values of property ${firstKey} ` +
'and make them available for charting.',
'and make them available for charting.',
' */',
'SELECT *',
`FROM "${this.addedTable}"`,
@@ -475,7 +482,7 @@ export default {
}
#csv-json-table-name {
margin-bottom: 24px;
margin-bottom: 24px;
}
.chars {
@@ -508,5 +515,4 @@ margin-bottom: 24px;
justify-content: center;
align-items: center;
}
</style>

View File

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

View File

@@ -10,7 +10,12 @@
<div v-show="loading" class="icon-in-progress">
<loading-indicator />
</div>
<span v-if="tooltip" class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
<span
v-if="tooltip"
class="icon-tooltip"
:style="tooltipStyle"
ref="tooltip"
>
{{ tooltip }}
</span>
</button>
@@ -27,7 +32,7 @@ export default {
components: { LoadingIndicator },
mixins: [tooltipMixin],
methods: {
onClick () {
onClick() {
this.hideTooltip()
this.$emit('click')
}
@@ -59,8 +64,8 @@ export default {
fill: var(--color-accent);
}
.icon-btn:disabled .icon :deep(path),
.icon-btn:disabled .icon :deep(circle) {
.icon-btn:disabled .icon :deep(path),
.icon-btn:disabled .icon :deep(circle) {
fill: var(--color-border);
}

View File

@@ -3,20 +3,23 @@
:modal-id="name"
class="dialog"
:clickToClose="false"
:contentTransition="{name: 'loading-dialog'}"
:overlayTransition="{name: 'loading-dialog'}"
:contentTransition="{ name: 'loading-dialog' }"
:overlayTransition="{ name: 'loading-dialog' }"
>
<div class="dialog-header">
{{ title }}
<close-icon @click="$emit('cancel')" :disabled="loading"/>
<close-icon @click="$emit('cancel')" :disabled="loading" />
</div>
<div class="dialog-body">
<div v-if="loading" class="loading-dialog-body">
<loading-indicator :size="30" class="state-icon"/>
<loading-indicator :size="30" class="state-icon" />
{{ loadingMsg }}
</div>
<div v-else class="loading-dialog-body">
<img src="~@/assets/images/success.svg" class="success-icon state-icon" />
<img
src="~@/assets/images/success.svg"
class="success-icon state-icon"
/>
{{ successMsg }}
</div>
</div>
@@ -57,7 +60,7 @@ export default {
},
emits: ['cancel', 'action'],
watch: {
loading () {
loading() {
if (this.loading) {
this.$modal.show(this.name)
}
@@ -65,7 +68,7 @@ export default {
},
components: { LoadingIndicator, CloseIcon },
methods: {
cancel () {
cancel() {
this.$emit('cancel')
}
}

View File

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

View File

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

View File

@@ -7,7 +7,11 @@
{ 'splitpanes-dragging': dragging }
]"
>
<div class="movable-splitter" ref="movableSplitter" :style="movableSplitterStyle" />
<div
class="movable-splitter"
ref="movableSplitter"
:style="movableSplitterStyle"
/>
<div
class="splitpanes-pane"
ref="left"
@@ -27,8 +31,11 @@
:class="[
'toggle-btns',
{
'both': after.max === 100 && before.max === 100 &&
paneAfter.size > 0 && paneBefore.size > 0
both:
after.max === 100 &&
before.max === 100 &&
paneAfter.size > 0 &&
paneBefore.size > 0
}
]"
>
@@ -41,7 +48,7 @@
class="direction-icon"
src="~@/assets/images/chevron.svg"
:style="directionBeforeIconStyle"
>
/>
</div>
<div
v-if="before.max === 100 && paneBefore.size > 0"
@@ -52,16 +59,12 @@
class="direction-icon"
src="~@/assets/images/chevron.svg"
:style="directionAfterIconStyle"
>
/>
</div>
</div>
</div>
<!-- splitter end -->
<div
class="splitpanes-pane"
ref="right"
:style="styles.after"
>
<div class="splitpanes-pane" ref="right" :style="styles.after">
<slot name="right-pane" />
</div>
</div>
@@ -87,17 +90,18 @@ export default {
}
},
emits: [],
data () {
data() {
return {
container: null,
paneBefore: this.before,
paneAfter: this.after,
beforeMinimising: !this.after.size || !this.before.size
? this.default
: {
before: this.before.size,
after: this.after.size
},
beforeMinimising:
!this.after.size || !this.before.size
? this.default
: {
before: this.before.size,
after: this.after.size
},
dragging: false,
movableSplitter: {
top: 0,
@@ -107,19 +111,23 @@ export default {
}
},
computed: {
styles () {
styles() {
return {
before: { [this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%` },
after: { [this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%` }
before: {
[this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%`
},
after: {
[this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%`
}
}
},
movableSplitterStyle () {
movableSplitterStyle() {
const style = { ...this.movableSplitter }
style.top += '%'
style.left += '%'
return style
},
directionBeforeIconStyle () {
directionBeforeIconStyle() {
const expanded = this.paneBefore.size !== 0
const translation = 'translate(-50%, -50%) '
let rotation = ''
@@ -134,7 +142,7 @@ export default {
transform: translation + rotation
}
},
directionAfterIconStyle () {
directionAfterIconStyle() {
const expanded = this.paneAfter.size !== 0
const translation = 'translate(-50%, -50%)'
let rotation = ''
@@ -152,35 +160,43 @@ export default {
},
methods: {
bindEvents () {
bindEvents() {
// Passive: false to prevent scrolling while touch dragging.
document.addEventListener('mousemove', this.onMouseMove, { passive: false })
document.addEventListener('mousemove', this.onMouseMove, {
passive: false
})
document.addEventListener('mouseup', this.onMouseUp)
if ('ontouchstart' in window) {
document.addEventListener('touchmove', this.onMouseMove, { passive: false })
document.addEventListener('touchmove', this.onMouseMove, {
passive: false
})
document.addEventListener('touchend', this.onMouseUp)
}
},
unbindEvents () {
document.removeEventListener('mousemove', this.onMouseMove, { passive: false })
unbindEvents() {
document.removeEventListener('mousemove', this.onMouseMove, {
passive: false
})
document.removeEventListener('mouseup', this.onMouseUp)
if ('ontouchstart' in window) {
document.removeEventListener('touchmove', this.onMouseMove, { passive: false })
document.removeEventListener('touchmove', this.onMouseMove, {
passive: false
})
document.removeEventListener('touchend', this.onMouseUp)
}
},
onMouseMove (event) {
onMouseMove(event) {
event.preventDefault()
this.dragging = true
this.movableSplitter.visibility = 'visible'
this.moveSplitter(event)
},
onMouseUp () {
onMouseUp() {
if (this.dragging) {
const dragPercentage = this.horizontal
? this.movableSplitter.top
@@ -201,7 +217,7 @@ export default {
this.unbindEvents()
},
moveSplitter (event) {
moveSplitter(event) {
const splitterInfo = {
container: this.container,
paneBeforeMax: this.paneBefore.max,
@@ -213,12 +229,13 @@ export default {
this.movableSplitter[dir] = offset
},
togglePane (pane) {
togglePane(pane) {
if (pane.size > 0) {
this.beforeMinimising.before = this.paneBefore.size
this.beforeMinimising.after = this.paneAfter.size
pane.size = 0
const otherPane = pane === this.paneBefore ? this.paneAfter : this.paneBefore
const otherPane =
pane === this.paneBefore ? this.paneAfter : this.paneBefore
otherPane.size = 100 - pane.size
} else {
this.paneBefore.size = this.beforeMinimising.before
@@ -226,7 +243,7 @@ export default {
}
}
},
mounted () {
mounted() {
this.container = this.$refs.container
}
}
@@ -239,9 +256,15 @@ export default {
position: relative;
}
.splitpanes-vertical {flex-direction: row;}
.splitpanes-horizontal {flex-direction: column;}
.splitpanes-dragging * {user-select: none;}
.splitpanes-vertical {
flex-direction: row;
}
.splitpanes-horizontal {
flex-direction: column;
}
.splitpanes-dragging * {
user-select: none;
}
.splitpanes-pane {
width: 100%;
@@ -281,14 +304,14 @@ export default {
.movable-splitter {
position: absolute;
background-color:rgba(162, 177, 198, 0.5);
background-color: rgba(162, 177, 198, 0.5);
}
.splitpanes-vertical > .splitpanes-splitter,
.splitpanes-vertical > .movable-splitter {
width: 8px;
z-index: 5;
height: 100%
height: 100%;
}
.splitpanes-horizontal > .splitpanes-splitter,
@@ -339,20 +362,32 @@ export default {
left: 50%;
}
.splitpanes-horizontal > .splitpanes-splitter .toggle-btns.both .toggle-btn:first-child {
.splitpanes-horizontal
> .splitpanes-splitter
.toggle-btns.both
.toggle-btn:first-child {
border-radius: var(--border-radius-small) 0 0 var(--border-radius-small);
}
.splitpanes-horizontal > .splitpanes-splitter .toggle-btns.both .toggle-btn:last-child {
.splitpanes-horizontal
> .splitpanes-splitter
.toggle-btns.both
.toggle-btn:last-child {
border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0;
margin-left: -1px;
}
.splitpanes-vertical > .splitpanes-splitter .toggle-btns.both .toggle-btn:first-child {
.splitpanes-vertical
> .splitpanes-splitter
.toggle-btns.both
.toggle-btn:first-child {
border-radius: var(--border-radius-small) var(--border-radius-small) 0 0;
}
.splitpanes-vertical > .splitpanes-splitter .toggle-btns.both .toggle-btn:last-child {
.splitpanes-vertical
> .splitpanes-splitter
.toggle-btns.both
.toggle-btn:last-child {
border-radius: 0 0 var(--border-radius-small) var(--border-radius-small);
margin-top: -1px;
}

View File

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

View File

@@ -25,7 +25,7 @@ export default {
components: { Paginate },
props: ['pageCount', 'modelValue'],
emits: ['update:modelValue'],
data () {
data() {
return {
page: this.modelValue,
chevron: `
@@ -39,10 +39,10 @@ export default {
}
},
watch: {
page () {
page() {
this.$emit('update:modelValue', this.page)
},
modelValue () {
modelValue() {
this.page = this.modelValue
}
}
@@ -93,7 +93,7 @@ export default {
:deep(.paginator-next:hover path),
:deep(.paginator-prev:hover path) {
fill: var(--color-text-active);
fill: var(--color-text-active);
}
:deep(.paginator-disabled path),
:deep(.paginator-disabled:hover path) {

View File

@@ -11,50 +11,50 @@
>
{{ th.name }}
</div>
</div>
</div>
</div>
<div
class="table-container"
ref="table-container"
@scroll="onScrollTable"
>
<table
ref="table"
class="sqliteviz-table"
tabindex="0"
@keydown="onTableKeydown"
>
<thead>
<tr>
<th v-for="(th, index) in columns" :key="index" ref="th">
<div class="cell-data" :style="cellStyle">{{ th }}</div>
</th>
</tr>
</thead>
<tbody>
<tr v-for="rowIndex in currentPageData.count" :key="rowIndex">
<td
v-for="(col, colIndex) in columns"
:data-col="colIndex"
:data-row="pageSize * (currentPage - 1) + rowIndex - 1"
:data-isNull="isNull(getCellValue(col, rowIndex))"
:data-isBlob="isBlob(getCellValue(col, rowIndex))"
:key="colIndex"
:aria-selected="false"
@click="onCellClick"
>
<div class="cell-data" :style="cellStyle">
{{ getCellText(col, rowIndex) }}
</div>
</td>
</tr>
</tbody>
</table>
<table
ref="table"
class="sqliteviz-table"
tabindex="0"
@keydown="onTableKeydown"
>
<thead>
<tr>
<th v-for="(th, index) in columns" :key="index" ref="th">
<div class="cell-data" :style="cellStyle">{{ th }}</div>
</th>
</tr>
</thead>
<tbody>
<tr v-for="rowIndex in currentPageData.count" :key="rowIndex">
<td
v-for="(col, colIndex) in columns"
:data-col="colIndex"
:data-row="pageSize * (currentPage - 1) + rowIndex - 1"
:data-isNull="isNull(getCellValue(col, rowIndex))"
:data-isBlob="isBlob(getCellValue(col, rowIndex))"
:key="colIndex"
:aria-selected="false"
@click="onCellClick"
>
<div class="cell-data" :style="cellStyle">
{{ getCellText(col, rowIndex) }}
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="table-footer">
<div class="table-footer-count">
{{ rowCount }} {{rowCount === 1 ? 'row' : 'rows'}} retrieved
{{ rowCount }} {{ rowCount === 1 ? 'row' : 'rows' }} retrieved
<span v-if="preview">for preview</span>
<span v-if="time">in {{ time }}</span>
</div>
@@ -88,7 +88,7 @@ export default {
selectedCellCoordinates: Object
},
emits: ['updateSelectedCell'],
data () {
data() {
return {
header: null,
tableWidth: null,
@@ -98,20 +98,20 @@ export default {
}
},
computed: {
columns () {
columns() {
return this.dataSet.columns
},
rowCount () {
rowCount() {
return this.dataSet.values[this.columns[0]].length
},
cellStyle () {
cellStyle() {
const eq = this.tableWidth / this.columns.length
return { maxWidth: `${Math.max(eq, 100)}px` }
},
pageCount () {
pageCount() {
return Math.ceil(this.rowCount / this.pageSize)
},
currentPageData () {
currentPageData() {
const start = (this.currentPage - 1) * this.pageSize
let end = start + this.pageSize
if (end > this.rowCount - 1) {
@@ -124,31 +124,32 @@ export default {
}
}
},
mounted () {
mounted() {
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
this.resizeObserver.observe(this.$refs.table)
this.calculateHeadersWidth()
if (this.selectedCellCoordinates) {
const { row, col } = this.selectedCellCoordinates
const cell = this.$refs.table
.querySelector(`td[data-col="${col}"][data-row="${row}"]`)
const cell = this.$refs.table.querySelector(
`td[data-col="${col}"][data-row="${row}"]`
)
if (cell) {
this.selectCell(cell)
}
}
},
methods: {
isBlob (value) {
isBlob(value) {
return value && ArrayBuffer.isView(value)
},
isNull (value) {
isNull(value) {
return value === null
},
getCellValue (col, rowIndex) {
getCellValue(col, rowIndex) {
return this.dataSet.values[col][rowIndex - 1 + this.currentPageData.start]
},
getCellText (col, rowIndex) {
getCellText(col, rowIndex) {
const value = this.getCellValue(col, rowIndex)
if (this.isNull(value)) {
return 'NULL'
@@ -158,7 +159,7 @@ export default {
}
return value
},
calculateHeadersWidth () {
calculateHeadersWidth() {
this.tableWidth = this.$refs['table-container'].offsetWidth
this.$nextTick(() => {
this.header = this.$refs.th.map(th => {
@@ -166,10 +167,11 @@ export default {
})
})
},
onScrollTable () {
this.$refs['header-container'].scrollLeft = this.$refs['table-container'].scrollLeft
onScrollTable() {
this.$refs['header-container'].scrollLeft =
this.$refs['table-container'].scrollLeft
},
onTableKeydown (e) {
onTableKeydown(e) {
const keyCodeMap = {
37: 'left',
39: 'right',
@@ -187,10 +189,10 @@ export default {
this.moveFocusInTable(this.selectedCellElement, keyCodeMap[e.keyCode])
},
onCellClick (e) {
onCellClick(e) {
this.selectCell(e.target.closest('td'), false)
},
selectCell (cell, scrollTo = true) {
selectCell(cell, scrollTo = true) {
if (!cell) {
if (this.selectedCellElement) {
this.selectedCellElement.ariaSelected = 'false'
@@ -213,7 +215,7 @@ export default {
this.$emit('updateSelectedCell', this.selectedCellElement)
},
moveFocusInTable (initialCell, direction) {
moveFocusInTable(initialCell, direction) {
const currentRowIndex = +initialCell.dataset.row
const currentColIndex = +initialCell.dataset.col
let newRowIndex, newColIndex
@@ -242,22 +244,23 @@ export default {
newColIndex = currentColIndex
}
const newCell = this.$refs.table
.querySelector(`td[data-col="${newColIndex}"][data-row="${newRowIndex}"]`)
const newCell = this.$refs.table.querySelector(
`td[data-col="${newColIndex}"][data-row="${newRowIndex}"]`
)
if (newCell) {
this.selectCell(newCell)
}
}
},
beforeUnmount () {
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.table)
},
watch: {
currentPageData () {
currentPageData() {
this.calculateHeadersWidth()
this.selectCell(null)
},
dataSet () {
dataSet() {
this.currentPage = 1
}
}
@@ -271,7 +274,7 @@ table.sqliteviz-table:focus {
.sqliteviz-table tbody td:hover {
background-color: var(--color-bg-light-3);
}
.sqliteviz-table tbody td[aria-selected="true"] {
box-shadow:inset 0 0 0 1px var(--color-accent);
.sqliteviz-table tbody td[aria-selected='true'] {
box-shadow: inset 0 0 0 1px var(--color-accent);
}
</style>

View File

@@ -1,8 +1,16 @@
<template>
<div>
<div v-if="label" :class="['text-field-label', { error: errorMsg }, {'disabled': disabled}]">
<div
v-if="label"
:class="['text-field-label', { error: errorMsg }, { disabled: disabled }]"
>
{{ label }}
<hint-icon class="hint" v-if="hint" :hint="hint" :max-width="maxHintWidth || '149px'"/>
<hint-icon
class="hint"
v-if="hint"
:hint="hint"
:max-width="maxHintWidth || '149px'"
/>
</div>
<input
type="text"
@@ -75,7 +83,7 @@ input.error {
position: relative;
}
.text-field-label .hint{
.text-field-label .hint {
position: absolute;
top: -2px;
right: -22px;

View File

@@ -29,7 +29,7 @@
</g>
<defs>
<clipPath id="clip0">
<rect width="18" height="18" fill="white"/>
<rect width="18" height="18" fill="white" />
</clipPath>
</defs>
</svg>
@@ -48,7 +48,7 @@ export default {
props: ['tooltip'],
emits: ['click'],
methods: {
onClick () {
onClick() {
this.hideTooltip()
this.$emit('click')
}

View File

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

View File

@@ -1,30 +1,30 @@
<template>
<div>
<svg
class="db-edit-icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
@click.stop="onClick"
@mouseenter="showTooltip"
@mouseleave="hideTooltip"
>
<path
d="M3 10.5V12.75C3 14.25 5.2875 15.54 8.25 15.75V13.5825L8.3475 13.5C5.34 13.32 3 12.045 3
<div>
<svg
class="db-edit-icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
@click.stop="onClick"
@mouseenter="showTooltip"
@mouseleave="hideTooltip"
>
<path
d="M3 10.5V12.75C3 14.25 5.2875 15.54 8.25 15.75V13.5825L8.3475 13.5C5.34 13.32 3 12.045 3
10.5ZM9 9.75C5.685 9.75 3 8.4075 3 6.75V9C3 10.6575 5.685 12 9 12C9.2925 12 9.5775 12
9.87 12L12.75 9.09C11.55 9.54 10.2825 9.75 9 9.75ZM9 2.25C5.685 2.25 3 3.5925 3 5.25C3
6.9075 5.685 8.25 9 8.25C12.315 8.25 15 6.9075 15 5.25C15 3.5925 12.315 2.25 9 2.25ZM15.75
8.3475C15.6375 8.3475 15.5325 8.3925 15.4575 8.475L14.7075 9.225L16.245 10.725L16.995
9.975C17.1525 9.825 17.16 9.57 16.995 9.3975L16.065 8.475C15.99 8.3925 15.885 8.3475 15.78
8.3475H15.75ZM14.28 9.66L9.75 14.205V15.75H11.295L15.84 11.1975L14.28 9.66Z"
fill="#A2B1C6"
/>
</svg>
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
Load another database, CSV, JSON or NDJSON
</span>
</div>
fill="#A2B1C6"
/>
</svg>
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
Load another database, CSV, JSON or NDJSON
</span>
</div>
</template>
<script>
@@ -35,7 +35,7 @@ export default {
mixins: [tooltipMixin],
emits: ['click'],
methods: {
onClick () {
onClick() {
this.hideTooltip()
this.$emit('click')
}

View File

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

View File

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

View File

@@ -1,10 +1,5 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
d="M14.1917 1.3851H12.4806V0.703125C12.4806 0.314758 12.1658 0 11.7775 0H6.246C5.85764 0
5.54288 0.314758 5.54288 0.703125V1.3851H3.83203C2.86276 1.3851 2.07422 2.17365 2.07422
@@ -26,7 +21,6 @@
</template>
<script>
export default {
name: 'ClipboardIcon'
}

View File

@@ -1,7 +1,7 @@
<template>
<svg
@click.stop="$emit('click')"
:class="['icon', {'disabled': disabled }]"
:class="['icon', { disabled: disabled }]"
:width="size"
:height="size"
viewBox="0 0 14 14"

View File

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

View File

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

View File

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

View File

@@ -32,7 +32,7 @@ export default {
props: ['tooltip', 'tooltipPosition'],
emits: ['click'],
methods: {
onClick () {
onClick() {
this.hideTooltip()
this.$emit('click')
}

View File

@@ -1,10 +1,5 @@
<template>
<svg
width="19"
height="18"
viewBox="0 0 19 18"
fill="none"
>
<svg width="19" height="18" viewBox="0 0 19 18" fill="none">
<path
d="M6.07959 13.5756C6.05908 14.0209 5.93896 14.415 5.71924 14.7578C5.49951 15.0976 5.19043
15.3613 4.79199 15.5488C4.39648 15.7363 3.94385 15.83 3.43408 15.83C2.59326 15.83
@@ -34,7 +29,7 @@
14.6699C9.53809 14.6699 9.73877 14.6157 9.88525 14.5073C10.0347 14.3959 10.1094 14.2407
10.1094 14.0414ZM14.9258 14.0019L16.2002 9.34369H17.9229L15.7695 15.7421H14.082L11.9463
9.34369H13.6558L14.9258 14.0019Z"
fill="#A2B1C6"
fill="#A2B1C6"
/>
<path
d="M3.03345 0.991333H4.89869V2.49133H3.03345V7.93074H1.53345V2.49133C1.53345 1.66633
@@ -55,7 +50,6 @@
</template>
<script>
export default {
name: 'ExportToCsvIcon'
}

View File

@@ -1,10 +1,5 @@
<template>
<svg
width="19"
height="18"
viewBox="0 0 19 18"
fill="none"
>
<svg width="19" height="18" viewBox="0 0 19 18" fill="none">
<path
d="M4.28369 13.9966C4.28369 13.7711 4.20312 13.5953 4.04199 13.4693C3.88379 13.3433 3.604
13.213 3.20264 13.0782C2.80127 12.9434 2.47314 12.813 2.21826 12.6871C1.38916 12.2798
@@ -54,7 +49,6 @@
</template>
<script>
export default {
name: 'ExportToSvgIcon'
}

View File

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

View File

@@ -11,8 +11,8 @@
7.89917V9.2439L5.1626 10.0745ZM8.99023 13.3H7.93994L10.124 6.35229H11.1787L8.99023
13.3ZM14.1099 10.0613L11.7192 9.24829V7.90356L15.582 9.4856V10.6545L11.7192
12.2366V10.8918L14.1099 10.0613Z"
fill="#A2B1C6"
/>
fill="#A2B1C6"
/>
<path
d="M2.17041 0.0637207H16.2185V1.56372H2.17041V9.30354H0.67041V1.56372C0.67041 0.73872
1.34541 0.0637207 2.17041 0.0637207Z"

View File

@@ -1,10 +1,5 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
@@ -14,14 +9,13 @@
14.1914C14.8372 13.965 15.0161 13.5645 15.0161 12.8467V9.43008H13.1914L15.7661 5.13901Z"
fill="#A2B1C6"
/>
<path d="M6.41943 0H18.4194V4H6.41943V0Z" fill="#A2B1C6"/>
<path d="M0.419434 6H4.41943V18H0.419434V6Z" fill="#A2B1C6"/>
<path d="M0.419434 0H4.41943V4H0.419434V0Z" fill="#A2B1C6"/>
<path d="M6.41943 0H18.4194V4H6.41943V0Z" fill="#A2B1C6" />
<path d="M0.419434 6H4.41943V18H0.419434V6Z" fill="#A2B1C6" />
<path d="M0.419434 0H4.41943V4H0.419434V0Z" fill="#A2B1C6" />
</svg>
</template>
<script>
export default {
name: 'PivotIcon'
}

View File

@@ -1,10 +1,5 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<path
d="M9 5.51953C6.57686 5.51953 4.60547 7.49092 4.60547 9.91406C4.60547 12.3372 6.57686
14.3086 9 14.3086C11.4231 14.3086 13.3945 12.3372 13.3945 9.91406C13.3945 7.49092 11.4231
@@ -30,7 +25,10 @@
5.5195V15.0117Z"
fill="#A2B1C6"
/>
<path d="M15.1875 6.22266H13.7812V7.62891H15.1875V6.22266Z" fill="#A2B1C6"/>
<path
d="M15.1875 6.22266H13.7812V7.62891H15.1875V6.22266Z"
fill="#A2B1C6"
/>
</svg>
</template>

View File

@@ -1,15 +1,10 @@
<template>
<svg
width="19"
height="19"
viewBox="0 0 19 19"
fill="none"
>
<svg width="19" height="19" viewBox="0 0 19 19" fill="none">
<g clip-path="url(#clip0_2130_5292)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M1.85303 11.3794L1.85303 7.80371L5.86304 7.80371L5.86304
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M1.85303 11.3794L1.85303 7.80371L5.86304 7.80371L5.86304
11.3794L1.85303 11.3794ZM7.36304 11.3794L7.36304 7.80371L11.3428
7.80371L11.3428 11.3794L7.36304 11.3794ZM12.8428 11.3794L16.853
11.3794L16.853 7.80371L12.8428 7.80371L12.8428 11.3794ZM15.353
@@ -29,24 +24,23 @@
14.3121L6.76685 12.8794L4.67027 12.8794ZM8.26685 12.8794L8.26685
14.3121L10.387 14.3121L10.387 12.8794L8.26685 12.8794ZM11.887
12.8794L11.887 14.3121L14.0315 14.3121L14.0315 12.8794L11.887 12.8794Z"
fill="#A2B1C6"
/>
fill="#A2B1C6"
/>
</g>
<defs>
<clipPath id="clip0_2130_5292">
<rect
width="18"
height="18"
fill="white"
transform="translate(0.353027 18.5916) rotate(-90)"
/>
width="18"
height="18"
fill="white"
transform="translate(0.353027 18.5916) rotate(-90)"
/>
</clipPath>
</defs>
</svg>
</svg>
</template>
<script>
export default {
name: 'RowIcon'
}

View File

@@ -1,16 +1,13 @@
<template>
<svg
width="12"
height="13"
viewBox="0 0 12 13"
fill="none"
>
<path d="M11.1624 6.94358L0.770043 12.9436L0.770043 0.943573L11.1624 6.94358Z" fill="#A2B1C6"/>
<svg width="12" height="13" viewBox="0 0 12 13" fill="none">
<path
d="M11.1624 6.94358L0.770043 12.9436L0.770043 0.943573L11.1624 6.94358Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'RunIcon'
}

View File

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

View File

@@ -1,10 +1,5 @@
<template>
<svg
width="18"
height="19"
viewBox="0 0 18 19"
fill="none"
>
<svg width="18" height="19" viewBox="0 0 18 19" fill="none">
<g clip-path="url(#clip0)">
<path
d="M4.5 1.51343H10.5L15 6.01343V8.45284H13.5V6.76343H9.75V3.01343H4.5V8.45284H3V3.01343C3
@@ -47,14 +42,18 @@
</g>
<defs>
<clipPath id="clip0">
<rect width="18" height="18" fill="white" transform="translate(0 0.0134277)"/>
<rect
width="18"
height="18"
fill="white"
transform="translate(0 0.0134277)"
/>
</clipPath>
</defs>
</svg>
</template>
<script>
export default {
name: 'SqlEditorIcon'
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,22 @@
export default {
isJSON (file) {
isJSON(file) {
return file && file.type === 'application/json'
},
isNDJSON (file) {
isNDJSON(file) {
return file && file.name.endsWith('.ndjson')
},
isDatabase (file) {
isDatabase(file) {
const dbTypes = ['application/vnd.sqlite3', 'application/x-sqlite3']
return file.type
? dbTypes.includes(file.type)
: /\.(db|sqlite(3)?)+$/.test(file.name)
},
getFileName (file) {
getFileName(file) {
return file.name.replace(/\.[^.]+$/, '')
},
downloadFromUrl (url, fileName) {
downloadFromUrl(url, fileName) {
// Create downloader
const downloader = document.createElement('a')
downloader.href = url
@@ -29,7 +29,7 @@ export default {
URL.revokeObjectURL(url)
},
async exportToFile (str, fileName, type = 'octet/stream') {
async exportToFile(str, fileName, type = 'octet/stream') {
const blob = new Blob([str], { type })
const url = URL.createObjectURL(blob)
this.downloadFromUrl(url, fileName)
@@ -40,7 +40,7 @@ export default {
* it will be an unsettled promise. But it's grabbed by
* the garbage collector (tested with FinalizationRegistry).
*/
getFileFromUser (type) {
getFileFromUser(type) {
return new Promise(resolve => {
const uploader = document.createElement('input')
@@ -56,14 +56,13 @@ export default {
})
},
importFile () {
return this.getFileFromUser('.json')
.then(file => {
return this.getFileContent(file)
})
importFile() {
return this.getFileFromUser('.json').then(file => {
return this.getFileContent(file)
})
},
getFileContent (file) {
getFileContent(file) {
const reader = new FileReader()
return new Promise(resolve => {
reader.onload = e => resolve(e.target.result)
@@ -71,11 +70,11 @@ export default {
})
},
readFile (path) {
readFile(path) {
return fetch(path)
},
readAsArrayBuffer (file) {
readAsArrayBuffer(file) {
const fileReader = new FileReader()
return new Promise((resolve, reject) => {

View File

@@ -1,11 +1,11 @@
export default {
getPeriod (start, end) {
getPeriod(start, end) {
const diff = end.getTime() - start.getTime()
const seconds = diff / 1000
return seconds.toFixed(3) + 's'
},
debounce (func, ms) {
debounce(func, ms) {
let timeout
return function () {
clearTimeout(timeout)
@@ -13,9 +13,11 @@ export default {
}
},
sleep (ms) {
sleep(ms) {
return new Promise(resolve => {
setTimeout(() => { resolve() }, ms)
setTimeout(() => {
resolve()
}, ms)
})
}
}

View File

@@ -1,7 +1,7 @@
import events from '@/lib/utils/events'
let refresh = false
function invokeServiceWorkerUpdateFlow (registration) {
function invokeServiceWorkerUpdateFlow(registration) {
const agree = confirm('New version of the app is available. Refresh now?')
if (agree) {
if (registration.waiting) {
@@ -14,7 +14,8 @@ function invokeServiceWorkerUpdateFlow (registration) {
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
const registration = await navigator.serviceWorker.register('service-worker.js')
const registration =
await navigator.serviceWorker.register('service-worker.js')
// ensure the case when the updatefound event was missed is also handled
// by re-invoking the prompt when there's a waiting Service Worker
if (registration.waiting) {

View File

@@ -2,7 +2,7 @@ import Tab from '@/lib/tab'
import { nanoid } from 'nanoid'
export default {
async addTab ({ state }, inquiry = {}) {
async addTab({ state }, inquiry = {}) {
// add new tab only if it was not already opened
if (!state.tabs.some(openedTab => openedTab.id === inquiry.id)) {
const tab = new Tab(state, JSON.parse(JSON.stringify(inquiry)))
@@ -15,7 +15,7 @@ export default {
return inquiry.id
},
async saveInquiry ({ state }, { inquiryTab, newName }) {
async saveInquiry({ state }, { inquiryTab, newName }) {
const value = {
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
query: inquiryTab.query,
@@ -31,7 +31,9 @@ export default {
if (newName) {
value.createdAt = new Date()
} else {
var inquiryIndex = myInquiries.findIndex(oldInquiry => oldInquiry.id === inquiryTab.id)
var inquiryIndex = myInquiries.findIndex(
oldInquiry => oldInquiry.id === inquiryTab.id
)
value.createdAt = myInquiries[inquiryIndex].createdAt
}
@@ -44,10 +46,10 @@ export default {
return value
},
addInquiry ({ state }, newInquiry) {
addInquiry({ state }, newInquiry) {
state.inquiries.push(newInquiry)
},
deleteInquiries ({ state, commit }, inquiryIdSet) {
deleteInquiries({ state, commit }, inquiryIdSet) {
state.inquiries = state.inquiries.filter(
inquiry => !inquiryIdSet.has(inquiry.id)
)
@@ -62,9 +64,10 @@ export default {
i--
}
},
renameInquiry ({ state, commit }, { inquiryId, newName }) {
const renamingInquiry = state.inquiries
.find(inquiry => inquiry.id === inquiryId)
renameInquiry({ state, commit }, { inquiryId, newName }) {
const renamingInquiry = state.inquiries.find(
inquiry => inquiry.id === inquiryId
)
renamingInquiry.name = newName

View File

@@ -1,12 +1,12 @@
export default {
setDb (state, db) {
setDb(state, db) {
if (state.db) {
state.db.shutDown()
}
state.db = db
},
updateTab (state, { tab, newValues }) {
updateTab(state, { tab, newValues }) {
const { name, id, query, viewType, viewOptions, isSaved } = newValues
const oldId = tab.id
@@ -14,19 +14,31 @@ export default {
state.currentTabId = id
}
if (id) { tab.id = id }
if (name) { tab.name = name }
if (query) { tab.query = query }
if (viewType) { tab.viewType = viewType }
if (viewOptions) { tab.viewOptions = viewOptions }
if (isSaved !== undefined) { tab.isSaved = isSaved }
if (id) {
tab.id = id
}
if (name) {
tab.name = name
}
if (query) {
tab.query = query
}
if (viewType) {
tab.viewType = viewType
}
if (viewOptions) {
tab.viewOptions = viewOptions
}
if (isSaved !== undefined) {
tab.isSaved = isSaved
}
if (isSaved) {
// Saved inquiry is not predefined
delete tab.isPredefined
}
},
deleteTab (state, tab) {
deleteTab(state, tab) {
const index = state.tabs.indexOf(tab)
// If closing tab is the current opened
if (tab.id === state.currentTabId) {
@@ -44,27 +56,29 @@ export default {
}
state.tabs.splice(index, 1)
},
setCurrentTabId (state, id) {
setCurrentTabId(state, id) {
try {
state.currentTabId = id
state.currentTab = state.tabs.find(tab => tab.id === id)
} catch (e) {
console.error('Can\'t open a tab id:' + id)
console.error("Can't open a tab id:" + id)
}
},
updatePredefinedInquiries (state, inquiries) {
state.predefinedInquiries = Array.isArray(inquiries) ? inquiries : [inquiries]
updatePredefinedInquiries(state, inquiries) {
state.predefinedInquiries = Array.isArray(inquiries)
? inquiries
: [inquiries]
},
setLoadingPredefinedInquiries (state, value) {
setLoadingPredefinedInquiries(state, value) {
state.loadingPredefinedInquiries = value
},
setPredefinedInquiriesLoaded (state, value) {
setPredefinedInquiriesLoaded(state, value) {
state.predefinedInquiriesLoaded = value
},
setInquiries (state, value) {
setInquiries(state, value) {
state.inquiries = value
},
setIsWorkspaceVisible (state, value) {
setIsWorkspaceVisible(state, value) {
state.isWorkspaceVisible = value
}
}

View File

@@ -1,5 +1,5 @@
export default {
data () {
data() {
return {
tooltipStyle: {
visibility: 'hidden'
@@ -7,13 +7,15 @@ export default {
}
},
computed: {
tooltipElement () {
tooltipElement() {
return this.$refs.tooltip
}
},
methods: {
showTooltip (e, tooltipPosition) {
const position = tooltipPosition ? tooltipPosition.split('-') : ['top', 'right']
showTooltip(e, tooltipPosition) {
const position = tooltipPosition
? tooltipPosition.split('-')
: ['top', 'right']
const offset = 12
if (position[0] === 'top') {
@@ -25,12 +27,13 @@ export default {
if (position[1] === 'right') {
this.tooltipStyle.left = e.clientX + offset + 'px'
} else {
this.tooltipStyle.left = e.clientX - offset - this.tooltipElement.offsetWidth + 'px'
this.tooltipStyle.left =
e.clientX - offset - this.tooltipElement.offsetWidth + 'px'
}
this.tooltipStyle.visibility = 'visible'
},
hideTooltip () {
hideTooltip() {
this.tooltipStyle.visibility = 'hidden'
}
}

View File

@@ -1,17 +1,15 @@
<template>
<div>
<logs
id="logs"
:messages="messages"
/>
<button
v-if="hasErrors"
id="open-workspace-btn"
class="secondary"
@click="$router.push('/workspace?hide_schema=1')">
Open workspace
</button>
</div>
<div>
<logs id="logs" :messages="messages" />
<button
v-if="hasErrors"
id="open-workspace-btn"
class="secondary"
@click="$router.push('/workspace?hide_schema=1')"
>
Open workspace
</button>
</div>
</template>
<script>
@@ -25,7 +23,7 @@ export default {
components: {
Logs
},
data () {
data() {
return {
newDb: null,
messages: [],
@@ -34,11 +32,11 @@ export default {
}
},
computed: {
hasErrors () {
hasErrors() {
return this.dataMsg.type === 'error' || this.inquiryMsg.type === 'error'
}
},
async created () {
async created() {
const {
data_url: dataUrl,
data_format: dataFormat,
@@ -65,7 +63,7 @@ export default {
}
},
methods: {
async loadData (dataUrl, dataFormat) {
async loadData(dataUrl, dataFormat) {
this.newDb = database.getNewDatabase()
if (dataUrl) {
this.dataMsg = {
@@ -95,7 +93,7 @@ export default {
}
this.$store.commit('setDb', this.newDb)
},
async getSqliteDb (dataUrl) {
async getSqliteDb(dataUrl) {
try {
const filename = new URL(dataUrl).pathname.split('/').pop()
const res = await fu.readFile(dataUrl)
@@ -114,7 +112,7 @@ export default {
this.dataMsg.type = 'error'
}
},
async loadInquiries (inquiryUrl, inquiryIds = []) {
async loadInquiries(inquiryUrl, inquiryIds = []) {
if (!inquiryUrl) {
return []
}
@@ -148,7 +146,7 @@ export default {
// Loading indicator is not needed anymore
clearTimeout(loadingInquiriesIndicator)
},
async openInquiries (inquiries, maximize) {
async openInquiries(inquiries, maximize) {
let tabToOpen = null
const layout = maximize ? this.getLayout(maximize) : undefined
for (const inquiry of inquiries) {
@@ -167,7 +165,7 @@ export default {
this.$store.state.currentTab.execute()
},
getLayout (panelToMaximize) {
getLayout(panelToMaximize) {
if (panelToMaximize === 'dataView') {
return {
sqlEditor: 'hidden',
@@ -190,7 +188,6 @@ export default {
#logs {
margin: 8px auto;
max-width: 800px;
}
#open-workspace-btn {

View File

@@ -5,22 +5,18 @@
src="~@/assets/images/info.svg"
@click="$modal.show('app-info')"
/>
<modal
modal-id="app-info"
class="dialog"
content-class="app-info-modal"
>
<modal modal-id="app-info" class="dialog" content-class="app-info-modal">
<div class="dialog-header">
App info
<close-icon @click="$modal.hide('app-info')"/>
<close-icon @click="$modal.hide('app-info')" />
</div>
<div class="dialog-body">
<div v-for="(item, index) in info" :key="index" class="info-item">
{{item.name}}
<div class="divider"/>
{{ item.name }}
<div class="divider" />
<div class="options">
<div v-for="(opt, index) in item.info" :key="index">
{{opt}}
{{ opt }}
</div>
</div>
</div>
@@ -36,7 +32,7 @@ import { version } from '../../../package.json'
export default {
name: 'AppDiagnosticInfo',
components: { CloseIcon },
data () {
data() {
return {
info: [
{
@@ -47,7 +43,7 @@ export default {
}
},
async created () {
async created() {
const state = this.$store.state
let result = (await state.db.execute('select sqlite_version()')).values
this.info.push({
@@ -94,7 +90,7 @@ export default {
}
.info-item {
margin-bottom: 32px;
font-size: 14px;
font-size: 14px;
}
.info-item:last-child {
margin-bottom: 0;

View File

@@ -10,13 +10,21 @@
id="loading-predefined-status"
v-if="$store.state.loadingPredefinedInquiries"
>
<loading-indicator/>
<loading-indicator />
Loading predefined inquiries...
</div>
<div id="my-inquiries-content" ref="my-inquiries-content" v-show="allInquiries.length > 0">
<div
id="my-inquiries-content"
ref="my-inquiries-content"
v-show="allInquiries.length > 0"
>
<div id="my-inquiries-toolbar">
<div id="toolbar-buttons">
<button id="toolbar-btns-import" class="toolbar" @click="importInquiries">
<button
id="toolbar-btns-import"
class="toolbar"
@click="importInquiries"
>
Import
</button>
<button
@@ -37,7 +45,11 @@
</button>
</div>
<div id="toolbar-search">
<text-field placeholder="Search inquiry by name" width="300px" v-model="filter"/>
<text-field
placeholder="Search inquiry by name"
width="300px"
v-model="filter"
/>
</div>
</div>
@@ -46,27 +58,32 @@
</div>
<div v-show="showedInquiries.length > 0" class="rounded-bg">
<div class="header-container">
<div>
<div class="fixed-header" ref="name-th">
<check-box ref="mainCheckBox" theme="light" @click="toggleSelectAll"/>
<div class="name-th">Name</div>
</div>
<div class="fixed-header">
Created at
<div class="header-container">
<div>
<div class="fixed-header" ref="name-th">
<check-box
ref="mainCheckBox"
theme="light"
@click="toggleSelectAll"
/>
<div class="name-th">Name</div>
</div>
<div class="fixed-header">Created at</div>
</div>
</div>
</div>
<div class="table-container" :style="{ 'max-height': `${maxTableHeight}px` }">
<table ref="table" class="sqliteviz-table">
<tbody>
<tr
v-for="(inquiry, index) in showedInquiries"
:key="inquiry.id"
@click="openInquiry(index)"
>
<td ref="name-td">
<div class="cell-data">
<div
class="table-container"
:style="{ 'max-height': `${maxTableHeight}px` }"
>
<table ref="table" class="sqliteviz-table">
<tbody>
<tr
v-for="(inquiry, index) in showedInquiries"
:key="inquiry.id"
@click="openInquiry(index)"
>
<td ref="name-td">
<div class="cell-data">
<check-box
ref="rowCheckBox"
:init="selectAll || selectedInquiriesIds.has(inquiry.id)"
@@ -81,82 +98,89 @@
@mouseleave="hideTooltip"
>
Predefined
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
Predefined inquiries come from the server.
These inquiries cant be deleted or renamed.
<span
class="icon-tooltip"
:style="tooltipStyle"
ref="tooltip"
>
Predefined inquiries come from the server. These
inquiries cant be deleted or renamed.
</span>
</div>
</div>
</td>
<td>
<div class="second-column">
<div class="date-container">
{{ createdAtFormatted(inquiry.createdAt) }}
</div>
<div class="icons-container">
<rename-icon
v-if="!inquiry.isPredefined"
@click="showRenameDialog(inquiry.id)"
/>
<copy-icon @click="duplicateInquiry(index)"/>
<export-icon
@click="exportToFile([inquiry], `${inquiry.name}.json`)"
tooltip="Export inquiry to file"
tooltip-position="top-left"
/>
<delete-icon
v-if="!inquiry.isPredefined"
@click="showDeleteDialog((new Set()).add(inquiry.id))"
/>
</td>
<td>
<div class="second-column">
<div class="date-container">
{{ createdAtFormatted(inquiry.createdAt) }}
</div>
<div class="icons-container">
<rename-icon
v-if="!inquiry.isPredefined"
@click="showRenameDialog(inquiry.id)"
/>
<copy-icon @click="duplicateInquiry(index)" />
<export-icon
@click="exportToFile([inquiry], `${inquiry.name}.json`)"
tooltip="Export inquiry to file"
tooltip-position="top-left"
/>
<delete-icon
v-if="!inquiry.isPredefined"
@click="showDeleteDialog(new Set().add(inquiry.id))"
/>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!--Rename Inquiry dialog -->
<modal modal-id="rename" class="dialog" content-style="width: 560px;">
<div class="dialog-header">
Rename inquiry
<close-icon @click="$modal.hide('rename')" />
</div>
<div class="dialog-body">
<text-field
label="New inquiry name"
:error-msg="errorMsg"
v-model="newName"
width="100%"
/>
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="$modal.hide('rename')">Cancel</button>
<button class="primary" @click="renameInquiry">Rename</button>
</div>
</modal>
<!--Delete Inquiry dialog -->
<modal modal-id="delete" class="dialog" content-style="width: 480px;">
<div class="dialog-header">
Delete {{ deleteGroup ? 'inquiries' : 'inquiry' }}
<close-icon @click="$modal.hide('delete')" />
</div>
<div class="dialog-body">
{{ deleteDialogMsg }}
<div
v-show="selectedInquiriesCount > selectedNotPredefinedCount"
id="note"
>
<img src="~@/assets/images/info.svg" />
Note: Predefined inquiries you've selected won't be deleted
</div>
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="$modal.hide('delete')">Cancel</button>
<button class="primary" @click="deleteInquiry">Delete</button>
</div>
</modal>
</div>
<!--Rename Inquiry dialog -->
<modal modal-id="rename" class="dialog" content-style="width: 560px;">
<div class="dialog-header">
Rename inquiry
<close-icon @click="$modal.hide('rename')"/>
</div>
<div class="dialog-body">
<text-field
label="New inquiry name"
:error-msg="errorMsg"
v-model="newName"
width="100%"
/>
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="$modal.hide('rename')">Cancel</button>
<button class="primary" @click="renameInquiry">Rename</button>
</div>
</modal>
<!--Delete Inquiry dialog -->
<modal modal-id="delete" class="dialog" content-style="width: 480px;">
<div class="dialog-header">
Delete {{ deleteGroup ? 'inquiries' : 'inquiry' }}
<close-icon @click="$modal.hide('delete')"/>
</div>
<div class="dialog-body">
{{ deleteDialogMsg }}
<div v-show="selectedInquiriesCount > selectedNotPredefinedCount" id="note">
<img src="~@/assets/images/info.svg">
Note: Predefined inquiries you've selected won't be deleted
</div>
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="$modal.hide('delete')">Cancel</button>
<button class="primary" @click="deleteInquiry">Delete</button>
</div>
</modal>
</div>
</template>
<script>
@@ -185,7 +209,7 @@ export default {
LoadingIndicator
},
mixins: [tooltipMixin],
data () {
data() {
return {
filter: null,
newName: null,
@@ -201,47 +225,51 @@ export default {
}
},
computed: {
inquiries () {
inquiries() {
return this.$store.state.inquiries
},
predefinedInquiries () {
predefinedInquiries() {
return this.$store.state.predefinedInquiries.map(inquiry => {
inquiry.isPredefined = true
return inquiry
})
},
predefinedInquiriesIds () {
predefinedInquiriesIds() {
return new Set(this.predefinedInquiries.map(inquiry => inquiry.id))
},
showedInquiries () {
showedInquiries() {
let showedInquiries = this.allInquiries
if (this.filter) {
showedInquiries = showedInquiries.filter(
inquiry => inquiry.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0
inquiry =>
inquiry.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0
)
}
return showedInquiries
},
allInquiries () {
allInquiries() {
return this.predefinedInquiries.concat(this.inquiries)
},
processedInquiryIndex () {
return this.inquiries.findIndex(inquiry => inquiry.id === this.processedInquiryId)
processedInquiryIndex() {
return this.inquiries.findIndex(
inquiry => inquiry.id === this.processedInquiryId
)
},
deleteDialogMsg () {
if (!this.deleteGroup && (
this.processedInquiryIndex === null ||
deleteDialogMsg() {
if (
!this.deleteGroup &&
(this.processedInquiryIndex === null ||
this.processedInquiryIndex < 0 ||
this.processedInquiryIndex > this.inquiries.length
)) {
this.processedInquiryIndex > this.inquiries.length)
) {
return ''
}
const deleteItem = this.deleteGroup
? `${this.selectedNotPredefinedCount} ${this.selectedNotPredefinedCount > 1
? 'inquiries'
: 'inquiry'}`
? `${this.selectedNotPredefinedCount} ${
this.selectedNotPredefinedCount > 1 ? 'inquiries' : 'inquiry'
}`
: `"${this.inquiries[this.processedInquiryIndex].name}"`
return `Are you sure you want to delete ${deleteItem}?`
@@ -249,14 +277,16 @@ export default {
},
watch: {
showedInquiries: {
handler () {
this.selectedInquiriesIds = new Set(this.showedInquiries
.filter(inquiry => this.selectedInquiriesIds.has(inquiry.id))
.map(inquiry => inquiry.id)
handler() {
this.selectedInquiriesIds = new Set(
this.showedInquiries
.filter(inquiry => this.selectedInquiriesIds.has(inquiry.id))
.map(inquiry => inquiry.id)
)
this.selectedInquiriesCount = this.selectedInquiriesIds.size
this.selectedNotPredefinedCount = ([...this.selectedInquiriesIds]
.filter(id => !this.predefinedInquiriesIds.has(id))).length
this.selectedNotPredefinedCount = [...this.selectedInquiriesIds].filter(
id => !this.predefinedInquiriesIds.has(id)
).length
if (this.selectedInquiriesIds.size < this.showedInquiries.length) {
if (this.$refs.mainCheckBox) {
@@ -268,9 +298,11 @@ export default {
deep: true
}
},
async created () {
const loadingPredefinedInquiries = this.$store.state.loadingPredefinedInquiries
const predefinedInquiriesLoaded = this.$store.state.predefinedInquiriesLoaded
async created() {
const loadingPredefinedInquiries =
this.$store.state.loadingPredefinedInquiries
const predefinedInquiriesLoaded =
this.$store.state.predefinedInquiriesLoaded
if (!predefinedInquiriesLoaded && !loadingPredefinedInquiries) {
try {
this.$store.commit('setLoadingPredefinedInquiries', true)
@@ -283,7 +315,7 @@ export default {
this.$store.commit('setLoadingPredefinedInquiries', false)
}
},
mounted () {
mounted() {
this.resizeObserver = new ResizeObserver(this.calcMaxTableHeight)
this.resizeObserver.observe(this.$refs['my-inquiries-content'])
@@ -292,15 +324,15 @@ export default {
this.calcNameWidth()
this.calcMaxTableHeight()
},
beforeUnmount () {
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs['my-inquiries-content'])
this.tableResizeObserver.unobserve(this.$refs.table)
},
methods: {
emitCreateTabEvent () {
emitCreateTabEvent() {
eventBus.$emit('createNewInquiry')
},
createdAtFormatted (value) {
createdAtFormatted(value) {
if (!value) {
return ''
}
@@ -310,20 +342,24 @@ export default {
hour: '2-digit',
minute: '2-digit'
}
return new Date(value).toLocaleDateString('en-GB', dateOptions) + ' ' +
new Date(value).toLocaleTimeString('en-GB', timeOptions)
return (
new Date(value).toLocaleDateString('en-GB', dateOptions) +
' ' +
new Date(value).toLocaleTimeString('en-GB', timeOptions)
)
},
calcNameWidth () {
const nameWidth = this.$refs['name-td'] && this.$refs['name-td'][0]
? this.$refs['name-td'][0].getBoundingClientRect().width
: 0
calcNameWidth() {
const nameWidth =
this.$refs['name-td'] && this.$refs['name-td'][0]
? this.$refs['name-td'][0].getBoundingClientRect().width
: 0
this.$refs['name-th'].style = `width: ${nameWidth}px`
},
calcMaxTableHeight () {
calcMaxTableHeight() {
const freeSpace = this.$refs['my-inquiries-content'].offsetHeight - 200
this.maxTableHeight = freeSpace - (freeSpace % 40) + 1
},
openInquiry (index) {
openInquiry(index) {
const tab = this.showedInquiries[index]
setTimeout(() => {
this.$store.dispatch('addTab', tab).then(id => {
@@ -332,13 +368,13 @@ export default {
})
})
},
showRenameDialog (id) {
showRenameDialog(id) {
this.errorMsg = null
this.processedInquiryId = id
this.newName = this.inquiries[this.processedInquiryIndex].name
this.$modal.show('rename')
},
renameInquiry () {
renameInquiry() {
if (!this.newName) {
this.errorMsg = "Inquiry name can't be empty"
return
@@ -351,21 +387,26 @@ export default {
// hide dialog
this.$modal.hide('rename')
},
duplicateInquiry (index) {
const newInquiry = storedInquiries.duplicateInquiry(this.showedInquiries[index])
duplicateInquiry(index) {
const newInquiry = storedInquiries.duplicateInquiry(
this.showedInquiries[index]
)
this.$store.dispatch('addInquiry', newInquiry)
},
showDeleteDialog (idsSet) {
showDeleteDialog(idsSet) {
this.deleteGroup = idsSet.size > 1
if (!this.deleteGroup) {
this.processedInquiryId = idsSet.values().next().value
}
this.$modal.show('delete')
},
deleteInquiry () {
deleteInquiry() {
this.$modal.hide('delete')
if (!this.deleteGroup) {
this.$store.dispatch('deleteInquiries', new Set().add(this.processedInquiryId))
this.$store.dispatch(
'deleteInquiries',
new Set().add(this.processedInquiryId)
)
// Clear checkbox
if (this.selectedInquiriesIds.has(this.processedInquiryId)) {
@@ -379,27 +420,31 @@ export default {
}
this.selectedInquiriesCount = this.selectedInquiriesIds.size
},
exportToFile (inquiryList, fileName) {
exportToFile(inquiryList, fileName) {
storedInquiries.export(inquiryList, fileName)
},
exportSelectedInquiries () {
const inquiryList = this.allInquiries.filter(
inquiry => this.selectedInquiriesIds.has(inquiry.id)
exportSelectedInquiries() {
const inquiryList = this.allInquiries.filter(inquiry =>
this.selectedInquiriesIds.has(inquiry.id)
)
this.exportToFile(inquiryList, 'My sqliteviz inquiries.json')
},
importInquiries () {
storedInquiries.importInquiries()
.then(importedInquiries => {
this.$store.commit('setInquiries', this.inquiries.concat(importedInquiries))
})
importInquiries() {
storedInquiries.importInquiries().then(importedInquiries => {
this.$store.commit(
'setInquiries',
this.inquiries.concat(importedInquiries)
)
})
},
toggleSelectAll (checked) {
toggleSelectAll(checked) {
this.selectAll = checked
this.$refs.rowCheckBox.forEach(item => { item.checked = checked })
this.$refs.rowCheckBox.forEach(item => {
item.checked = checked
})
this.selectedInquiriesIds = checked
? new Set(this.showedInquiries.map(inquiry => inquiry.id))
@@ -407,12 +452,13 @@ export default {
this.selectedInquiriesCount = this.selectedInquiriesIds.size
this.selectedNotPredefinedCount = checked
? ([...this.selectedInquiriesIds].filter(id => !this.predefinedInquiriesIds.has(id)))
.length
? [...this.selectedInquiriesIds].filter(
id => !this.predefinedInquiriesIds.has(id)
).length
: 0
},
toggleRow (checked, id) {
toggleRow(checked, id) {
const isPredefined = this.predefinedInquiriesIds.has(id)
if (checked) {
this.selectedInquiriesIds.add(id)

View File

@@ -34,7 +34,7 @@ export default {
emits: ['click'],
mixins: [tooltipMixin],
methods: {
onClick () {
onClick() {
this.hideTooltip()
this.$emit('click')
}

View File

@@ -32,7 +32,7 @@ export default {
emits: ['click'],
mixins: [tooltipMixin],
methods: {
onClick () {
onClick() {
this.hideTooltip()
this.$emit('click')
}

View File

@@ -32,7 +32,7 @@ export default {
emits: ['click'],
mixins: [tooltipMixin],
methods: {
onClick () {
onClick() {
this.hideTooltip()
this.$emit('click')
}

View File

@@ -2,7 +2,7 @@
<nav>
<div id="nav-links">
<a href="https://sqliteviz.com">
<img src="~@/assets/images/logo_simple.svg">
<img src="~@/assets/images/logo_simple.svg" />
</a>
<router-link to="/workspace">Workspace</router-link>
<router-link to="/inquiries">Inquiries</router-link>
@@ -18,11 +18,7 @@
>
Save
</button>
<button
id="create-btn"
class="primary"
@click="createNewInquiry"
>
<button id="create-btn" class="primary" @click="createNewInquiry">
Create
</button>
<app-diagnostic-info />
@@ -32,13 +28,13 @@
<modal modal-id="save" class="dialog" content-style="width: 560px;">
<div class="dialog-header">
Save inquiry
<close-icon @click="cancelSave"/>
<close-icon @click="cancelSave" />
</div>
<div class="dialog-body">
<div v-show="isPredefined" id="save-note">
<img src="~@/assets/images/info.svg">
Note: Predefined inquiries can't be edited.
That's why your modifications will be saved as a new inquiry. Enter the name for it.
<img src="~@/assets/images/info.svg" />
Note: Predefined inquiries can't be edited. That's why your
modifications will be saved as a new inquiry. Enter the name for it.
</div>
<text-field
label="Inquiry name"
@@ -70,36 +66,39 @@ export default {
CloseIcon,
AppDiagnosticInfo
},
data () {
data() {
return {
name: '',
errorMsg: null
}
},
computed: {
currentInquiry () {
currentInquiry() {
return this.$store.state.currentTab
},
isSaved () {
isSaved() {
return this.currentInquiry && this.currentInquiry.isSaved
},
isPredefined () {
isPredefined() {
return this.currentInquiry && this.currentInquiry.isPredefined
},
runDisabled () {
return this.currentInquiry && (!this.$store.state.db || !this.currentInquiry.query)
runDisabled() {
return (
this.currentInquiry &&
(!this.$store.state.db || !this.currentInquiry.query)
)
}
},
created () {
created() {
eventBus.$on('createNewInquiry', this.createNewInquiry)
eventBus.$on('saveInquiry', this.checkInquiryBeforeSave)
document.addEventListener('keydown', this._keyListener)
},
beforeUnmount () {
beforeUnmount() {
document.removeEventListener('keydown', this._keyListener)
},
methods: {
createNewInquiry () {
createNewInquiry() {
this.$store.dispatch('addTab').then(id => {
this.$store.commit('setCurrentTabId', id)
if (this.$route.path !== '/workspace') {
@@ -109,11 +108,11 @@ export default {
events.send('inquiry.create', null, { auto: false })
},
cancelSave () {
cancelSave() {
this.$modal.hide('save')
eventBus.$off('inquirySaved')
},
checkInquiryBeforeSave () {
checkInquiryBeforeSave() {
this.errorMsg = null
this.name = ''
@@ -123,10 +122,10 @@ export default {
this.saveInquiry()
}
},
async saveInquiry () {
async saveInquiry() {
const isNeedName = storedInquiries.isTabNeedName(this.currentInquiry)
if (isNeedName && !this.name) {
this.errorMsg = 'Inquiry name can\'t be empty'
this.errorMsg = "Inquiry name can't be empty"
return
}
const dataSet = this.currentInquiry.result
@@ -168,7 +167,7 @@ export default {
eventBus.$emit('inquirySaved')
events.send('inquiry.save')
},
_keyListener (e) {
_keyListener(e) {
if (this.$route.path === '/workspace') {
// Run query Ctrl+R or Ctrl+Enter
if ((e.key === 'r' || e.key === 'Enter') && (e.ctrlKey || e.metaKey)) {

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div @click="colVisible = !colVisible" class="table-name">
<tree-chevron :expanded="colVisible"/>
<tree-chevron :expanded="colVisible" />
{{ name }}
</div>
<div v-show="colVisible" class="columns">
@@ -20,7 +20,7 @@ export default {
name: 'TableDescription',
components: { TreeChevron },
props: ['name', 'columns'],
data () {
data() {
return {
colVisible: false
}
@@ -29,7 +29,8 @@ export default {
</script>
<style scoped>
.table-name, .column {
.table-name,
.column {
margin-top: 11px;
}

View File

@@ -1,16 +1,16 @@
<template>
<div id="schema-container">
<div id="schema-filter">
<text-field placeholder="Search table" width="100%" v-model="filter"/>
<text-field placeholder="Search table" width="100%" v-model="filter" />
</div>
<div id="db">
<div @click="schemaVisible = !schemaVisible" class="db-name">
<tree-chevron v-show="schema.length > 0" :expanded="schemaVisible"/>
<tree-chevron v-show="schema.length > 0" :expanded="schemaVisible" />
{{ dbName }}
</div>
<db-uploader id="db-edit" type="small" />
<export-icon tooltip="Export database" @click="exportToFile"/>
<add-table-icon @click="addCsvJson"/>
<export-icon tooltip="Export database" @click="exportToFile" />
<add-table-icon @click="addCsvJson" />
</div>
<div v-show="schemaVisible" class="schema">
<table-description
@@ -53,7 +53,7 @@ export default {
AddTableIcon,
CsvJsonImport
},
data () {
data() {
return {
schemaVisible: true,
filter: null,
@@ -61,7 +61,7 @@ export default {
}
},
computed: {
schema () {
schema() {
if (!this.$store.state.db.schema) {
return []
}
@@ -69,18 +69,19 @@ export default {
return !this.filter
? this.$store.state.db.schema
: this.$store.state.db.schema.filter(
table => table.name.toUpperCase().indexOf(this.filter.toUpperCase()) !== -1
)
table =>
table.name.toUpperCase().indexOf(this.filter.toUpperCase()) !== -1
)
},
dbName () {
dbName() {
return this.$store.state.db.dbName
}
},
methods: {
exportToFile () {
exportToFile() {
this.$store.state.db.export(`${this.dbName}.sqlite`)
},
async addCsvJson () {
async addCsvJson() {
this.file = await fIo.getFileFromUser('.csv,.json,.ndjson')
await this.$nextTick()
const csvJsonImportModal = this.$refs.addCsvJson
@@ -119,7 +120,8 @@ export default {
background-image: linear-gradient(white 73%, rgba(255, 255, 255, 0));
z-index: 2;
}
.schema, .db-name {
.schema,
.db-name {
color: var(--color-text-base);
font-size: 13px;
white-space: nowrap;

View File

@@ -1,15 +1,23 @@
<template>
<div v-show="visible" class="chart-container" ref="chartContainer">
<div class="warning chart-warning" v-show="!dataSources && visible">
There is no data to build a chart. Run your SQL query and make sure the result is not empty.
There is no data to build a chart. Run your SQL query and make sure the
result is not empty.
</div>
<div class="chart" :style="{ height: !dataSources ? 'calc(100% - 40px)' : '100%' }">
<div
class="chart"
:style="{ height: !dataSources ? 'calc(100% - 40px)' : '100%' }"
>
<PlotlyEditor
v-show="visible"
v-show="visible"
:data="state.data"
:layout="state.layout"
:frames="state.frames"
:config="{ editable: true, displaylogo: false, modeBarButtonsToRemove: ['toImage'] }"
:config="{
editable: true,
displaylogo: false,
modeBarButtonsToRemove: ['toImage']
}"
:dataSources="dataSources"
:dataSourceOptions="dataSourceOptions"
:plotly="plotly"
@@ -21,7 +29,7 @@
@render="onRender"
/>
</div>
</div>
</div>
</template>
<script>
@@ -38,15 +46,17 @@ import events from '@/lib/utils/events'
export default {
name: 'Chart',
props: [
'dataSources', 'initOptions',
'importToPngEnabled', 'importToSvgEnabled',
'dataSources',
'initOptions',
'importToPngEnabled',
'importToSvgEnabled',
'forPivot'
],
emits: ['update:importToSvgEnabled', 'update', 'loadingImageCompleted'],
components: {
PlotlyEditor: applyPureReactInVue(ReactPlotlyEditor)
},
data () {
data() {
return {
plotly,
state: this.initOptions || {
@@ -60,20 +70,23 @@ export default {
}
},
computed: {
dataSourceOptions () {
dataSourceOptions() {
return chartHelper.getOptionsFromDataSources(this.dataSources)
}
},
created () {
created() {
// https://github.com/plotly/plotly.js/issues/4555
plotly.setPlotConfig({
notifyOnLogging: 1
})
this.$watch(
() => this.state && this.state.data && this.state.data
.map(trace => `${trace.type}${trace.mode ? '-' + trace.mode : ''}`)
.join(','),
(value) => {
() =>
this.state &&
this.state.data &&
this.state.data
.map(trace => `${trace.type}${trace.mode ? '-' + trace.mode : ''}`)
.join(','),
value => {
events.send('viz_plotly.render', null, {
type: value,
pivot: !!this.forPivot
@@ -83,21 +96,21 @@ export default {
)
this.$emit('update:importToSvgEnabled', true)
},
mounted () {
mounted() {
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.chartContainer)
},
activated () {
activated() {
this.useResizeHandler = true
},
deactivated () {
deactivated() {
this.useResizeHandler = false
},
beforeUnmount () {
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.chartContainer)
},
watch: {
dataSources () {
dataSources() {
// we need to update state.data in order to update the graph
// https://github.com/plotly/react-chart-editor/issues/948
if (this.dataSources) {
@@ -106,41 +119,44 @@ export default {
}
},
methods: {
async handleResize () {
async handleResize() {
this.visible = false
await this.$nextTick()
this.visible = true
},
onRender (data, layout, frames) {
onRender(data, layout, frames) {
// TODO: check changes and enable Save button if needed
},
update (data, layout, frames) {
update(data, layout, frames) {
this.state = { data, layout, frames }
this.$emit('update')
},
getOptionsForSave () {
getOptionsForSave() {
return chartHelper.getOptionsForSave(this.state, this.dataSources)
},
async saveAsPng () {
async saveAsPng() {
const url = await this.prepareCopy()
this.$emit('loadingImageCompleted')
fIo.downloadFromUrl(url, 'chart')
},
async saveAsSvg () {
async saveAsSvg() {
const url = await this.prepareCopy('svg')
fIo.downloadFromUrl(url, 'chart')
},
saveAsHtml () {
saveAsHtml() {
fIo.exportToFile(
chartHelper.getHtml(this.state),
'chart.html',
'text/html'
)
},
async prepareCopy (type = 'png') {
return await chartHelper.getImageDataUrl(this.$refs.plotlyEditor.$el, type)
async prepareCopy(type = 'png') {
return await chartHelper.getImageDataUrl(
this.$refs.plotlyEditor.$el,
type
)
}
}
}

View File

@@ -1,11 +1,11 @@
<template>
<div :class="['pivot-sort-btn', direction] " @click="changeSorting">
{{ modelValue.includes('key') ? 'key' : 'value' }}
<sort-icon
class="sort-icon"
:horizontal="direction === 'col'"
:asc="modelValue.includes('a_to_z')"
/>
<div :class="['pivot-sort-btn', direction]" @click="changeSorting">
{{ modelValue.includes('key') ? 'key' : 'value' }}
<sort-icon
class="sort-icon"
:horizontal="direction === 'col'"
:asc="modelValue.includes('a_to_z')"
/>
</div>
</template>
@@ -20,7 +20,7 @@ export default {
SortIcon
},
methods: {
changeSorting () {
changeSorting() {
if (this.modelValue === 'key_a_to_z') {
this.$emit('update:modelValue', 'value_a_to_z')
} else if (this.modelValue === 'value_a_to_z') {

View File

@@ -1,6 +1,6 @@
<template>
<div class="pivot-ui">
<div :class="{collapsed}">
<div :class="{ collapsed }">
<div class="row">
<label>Columns</label>
<multiselect
@@ -139,7 +139,12 @@
import $ from 'jquery'
import Multiselect from 'vue-multiselect'
import PivotSortBtn from './PivotSortBtn'
import { renderers, aggregators, zeroValAggregators, twoValAggregators } from '../pivotHelper'
import {
renderers,
aggregators,
zeroValAggregators,
twoValAggregators
} from '../pivotHelper'
export default {
name: 'pivotUi',
@@ -149,23 +154,35 @@ export default {
Multiselect,
PivotSortBtn
},
data () {
const aggregatorName = (this.modelValue && this.modelValue.aggregatorName) || 'Count'
const rendererName = (this.modelValue && this.modelValue.rendererName) || 'Table'
data() {
const aggregatorName =
(this.modelValue && this.modelValue.aggregatorName) || 'Count'
const rendererName =
(this.modelValue && this.modelValue.rendererName) || 'Table'
return {
collapsed: false,
renderer: { name: rendererName, fun: $.pivotUtilities.renderers[rendererName] },
aggregator: { name: aggregatorName, fun: $.pivotUtilities.aggregators[aggregatorName] },
renderer: {
name: rendererName,
fun: $.pivotUtilities.renderers[rendererName]
},
aggregator: {
name: aggregatorName,
fun: $.pivotUtilities.aggregators[aggregatorName]
},
rows: (this.modelValue && this.modelValue.rows) || [],
cols: (this.modelValue && this.modelValue.cols) || [],
val1: (this.modelValue && this.modelValue.vals && this.modelValue.vals[0]) || '',
val2: (this.modelValue && this.modelValue.vals && this.modelValue.vals[1]) || '',
val1:
(this.modelValue && this.modelValue.vals && this.modelValue.vals[0]) ||
'',
val2:
(this.modelValue && this.modelValue.vals && this.modelValue.vals[1]) ||
'',
colOrder: (this.modelValue && this.modelValue.colOrder) || 'key_a_to_z',
rowOrder: (this.modelValue && this.modelValue.rowOrder) || 'key_a_to_z'
}
},
computed: {
valCount () {
valCount() {
if (zeroValAggregators.includes(this.aggregator.name)) {
return 0
}
@@ -176,47 +193,47 @@ export default {
return 1
},
renderers () {
renderers() {
return renderers
},
aggregators () {
aggregators() {
return aggregators
},
rowsToSelect () {
rowsToSelect() {
return this.keyNames.filter(key => !this.cols.includes(key))
},
colsToSelect () {
colsToSelect() {
return this.keyNames.filter(key => !this.rows.includes(key))
}
},
watch: {
renderer () {
renderer() {
this.returnValue()
},
aggregator () {
aggregator() {
this.returnValue()
},
rows () {
rows() {
this.returnValue()
},
cols () {
cols() {
this.returnValue()
},
val1 () {
val1() {
this.returnValue()
},
val2 () {
val2() {
this.returnValue()
},
colOrder () {
colOrder() {
this.returnValue()
},
rowOrder () {
rowOrder() {
this.returnValue()
}
},
methods: {
returnValue () {
returnValue() {
const vals = []
for (let i = 1; i <= this.valCount; i++) {
vals.push(this[`val${i}`])
@@ -281,7 +298,6 @@ export default {
white-space: nowrap;
margin: auto;
cursor: pointer;
}
.switcher:hover {

View File

@@ -1,27 +1,28 @@
<template>
<div class="pivot-container">
<div class="warning pivot-warning" v-show="!dataSources">
There is no data to build a pivot. Run your SQL query and make sure the result is not empty.
</div>
<pivot-ui
:key-names="columns"
v-model="pivotOptions"
@update="$emit('update')"
/>
<div ref="pivotOutput" class="pivot-output"/>
<div
ref="customChartOutput"
v-show="viewCustomChart"
class="custom-chart-output"
>
<chart
ref="customChart"
v-bind="customChartComponentProps"
<div class="pivot-container">
<div class="warning pivot-warning" v-show="!dataSources">
There is no data to build a pivot. Run your SQL query and make sure the
result is not empty.
</div>
<pivot-ui
:key-names="columns"
v-model="pivotOptions"
@update="$emit('update')"
@loadingImageCompleted="$emit('loadingImageCompleted')"
/>
<div ref="pivotOutput" class="pivot-output" />
<div
ref="customChartOutput"
v-show="viewCustomChart"
class="custom-chart-output"
>
<chart
ref="customChart"
v-bind="customChartComponentProps"
@update="$emit('update')"
@loadingImageCompleted="$emit('loadingImageCompleted')"
/>
</div>
</div>
</div>
</template>
<script>
@@ -37,7 +38,12 @@ import events from '@/lib/utils/events'
export default {
name: 'pivot',
props: ['dataSources', 'initOptions', 'importToPngEnabled', 'importToSvgEnabled'],
props: [
'dataSources',
'initOptions',
'importToPngEnabled',
'importToSvgEnabled'
],
emits: [
'loadingImageCompleted',
'update',
@@ -48,7 +54,7 @@ export default {
PivotUi,
Chart
},
data () {
data() {
return {
resizeObserver: null,
pivotOptions: !this.initOptions
@@ -83,25 +89,25 @@ export default {
}
},
computed: {
columns () {
columns() {
return Object.keys(this.dataSources || {})
},
viewStandartChart () {
viewStandartChart() {
return this.pivotOptions.rendererName in $.pivotUtilities.plotly_renderers
},
viewCustomChart () {
viewCustomChart() {
return this.pivotOptions.rendererName === 'Custom chart'
}
},
watch: {
dataSources () {
dataSources() {
this.show()
},
'pivotOptions.rendererName': {
immediate: true,
handler () {
handler() {
this.$emit(
'update:importToPngEnabled',
this.pivotOptions.rendererName !== 'TSV Export'
@@ -115,22 +121,22 @@ export default {
})
}
},
pivotOptions () {
pivotOptions() {
this.show()
}
},
mounted () {
mounted() {
this.show()
// We need to detect resizing because plotly doesn't resize when resize its container
// but it resize on window.resize (we will trigger it manualy in order to make plotly resize)
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.customChartOutput)
},
beforeUnmount () {
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.customChartOutput)
},
methods: {
handleResize () {
handleResize() {
// hack: plotly changes size only on window.resize event,
// so, we trigger it when container resizes (e.g. when move splitter)
if (this.viewStandartChart) {
@@ -138,7 +144,7 @@ export default {
}
},
show () {
show() {
const options = { ...this.pivotOptions }
if (this.viewStandartChart) {
options.rendererOptions = {
@@ -163,7 +169,9 @@ export default {
$(this.$refs.pivotOutput).pivot(
function (callback) {
const rowCount = !this.dataSources ? 0 : this.dataSources[this.columns[0]].length
const rowCount = !this.dataSources
? 0
: this.dataSources[this.columns[0]].length
for (let i = 1; i <= rowCount; i++) {
const row = {}
this.columns.forEach(col => {
@@ -181,7 +189,7 @@ export default {
}
},
getOptionsForSave () {
getOptionsForSave() {
const options = { ...this.pivotOptions }
if (this.viewCustomChart) {
const chartComponent = this.$refs.customChart
@@ -192,20 +200,22 @@ export default {
return options
},
async saveAsPng () {
async saveAsPng() {
if (this.viewCustomChart) {
this.$refs.customChart.saveAsPng()
} else {
const source = this.viewStandartChart
? await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
: (await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)).toDataURL('image/png')
: (
await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
).toDataURL('image/png')
this.$emit('loadingImageCompleted')
fIo.downloadFromUrl(source, 'pivot')
}
},
async prepareCopy () {
async prepareCopy() {
if (this.viewCustomChart) {
return await this.$refs.customChart.prepareCopy()
}
@@ -215,16 +225,19 @@ export default {
return await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
},
async saveAsSvg () {
async saveAsSvg() {
if (this.viewCustomChart) {
this.$refs.customChart.saveAsSvg()
} else if (this.viewStandartChart) {
const url = await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'svg')
const url = await chartHelper.getImageDataUrl(
this.$refs.pivotOutput,
'svg'
)
fIo.downloadFromUrl(url, 'pivot')
}
},
saveAsHtml () {
saveAsHtml() {
if (this.viewCustomChart) {
this.$refs.customChart.saveAsHtml()
return

View File

@@ -17,7 +17,7 @@ export const twoValAggregators = [
'80% Lower Bound'
]
export function _getDataSources (pivotData) {
export function _getDataSources(pivotData) {
const rowKeys = pivotData.getRowKeys()
const colKeys = pivotData.getColKeys()
@@ -49,7 +49,7 @@ export function _getDataSources (pivotData) {
return Object.assign(dataSources, dataSourcesByCols, dataSourcesByRows)
}
function customChartRenderer (data, options) {
function customChartRenderer(data, options) {
const propsRef = options.getCustomComponentsProps()
propsRef.dataSources = _getDataSources(data)
return null
@@ -69,19 +69,21 @@ export const renderers = Object.keys($.pivotUtilities.renderers).map(key => {
}
})
export const aggregators = Object.keys($.pivotUtilities.aggregators).map(key => {
return {
name: key,
fun: $.pivotUtilities.aggregators[key]
export const aggregators = Object.keys($.pivotUtilities.aggregators).map(
key => {
return {
name: key,
fun: $.pivotUtilities.aggregators[key]
}
}
})
)
export async function getPivotCanvas (pivotOutput) {
export async function getPivotCanvas(pivotOutput) {
const tableElement = pivotOutput.querySelector('.pvtTable')
return await html2canvas(tableElement, { logging: false })
}
export function getPivotHtml (pivotOutput) {
export function getPivotHtml(pivotOutput) {
return `
<style>
table.pvtTable {

View File

@@ -31,7 +31,7 @@
<pivot-icon />
</icon-button>
<div class="side-tool-bar-divider"/>
<div class="side-tool-bar-divider" />
<icon-button
:disabled="!importToPngEnabled || loadingImage"
@@ -67,20 +67,20 @@
tooltip-position="top-left"
@click="prepareCopy"
>
<clipboard-icon/>
<clipboard-icon />
</icon-button>
</side-tool-bar>
<loading-dialog
loadingMsg="Rendering the visualisation..."
successMsg="Image is ready"
actionBtnName="Copy"
name="prepareCopy"
title="Copy to clipboard"
:loading="preparingCopy"
@action="copyToClipboard"
@cancel="cancelCopy"
/>
<loading-dialog
loadingMsg="Rendering the visualisation..."
successMsg="Image is ready"
actionBtnName="Copy"
name="prepareCopy"
title="Copy to clipboard"
:loading="preparingCopy"
@action="copyToClipboard"
@cancel="cancelCopy"
/>
</div>
</template>
@@ -117,7 +117,7 @@ export default {
ClipboardIcon,
loadingDialog
},
data () {
data() {
return {
mode: this.initMode || 'chart',
importToPngEnabled: true,
@@ -129,18 +129,18 @@ export default {
}
},
computed: {
plotlyInPivot () {
plotlyInPivot() {
return this.mode === 'pivot' && this.$refs.viewComponent.viewCustomChart
}
},
watch: {
mode () {
mode() {
this.$emit('update')
this.importToPngEnabled = true
}
},
methods: {
async saveAsPng () {
async saveAsPng() {
this.loadingImage = true
/*
setTimeout does its thing by putting its callback on the callback queue.
@@ -160,10 +160,10 @@ export default {
this.$refs.viewComponent.saveAsPng()
this.exportSignal('png')
},
getOptionsForSave () {
getOptionsForSave() {
return this.$refs.viewComponent.getOptionsForSave()
},
async prepareCopy () {
async prepareCopy() {
if ('ClipboardItem' in window) {
this.preparingCopy = true
this.$modal.show('prepareCopy')
@@ -172,7 +172,7 @@ export default {
await time.sleep(0)
this.dataToCopy = await this.$refs.viewComponent.prepareCopy()
const t1 = performance.now()
if ((t1 - t0) < 950) {
if (t1 - t0 < 950) {
this.$modal.hide('prepareCopy')
this.copyToClipboard()
} else {
@@ -181,30 +181,30 @@ export default {
} else {
alert(
"Your browser doesn't support copying images into the clipboard. " +
'If you use Firefox you can enable it ' +
'by setting dom.events.asyncClipboard.clipboardItem to true.'
'If you use Firefox you can enable it ' +
'by setting dom.events.asyncClipboard.clipboardItem to true.'
)
}
},
async copyToClipboard () {
async copyToClipboard() {
cIo.copyImage(this.dataToCopy)
this.$modal.hide('prepareCopy')
this.exportSignal('clipboard')
},
cancelCopy () {
cancelCopy() {
this.dataToCopy = null
this.$modal.hide('prepareCopy')
},
saveAsSvg () {
saveAsSvg() {
this.$refs.viewComponent.saveAsSvg()
this.exportSignal('svg')
},
saveAsHtml () {
saveAsHtml() {
this.$refs.viewComponent.saveAsHtml()
this.exportSignal('html')
},
exportSignal (to) {
exportSignal(to) {
const eventLabels = { type: to }
if (this.mode === 'chart' || this.plotlyInPivot) {

View File

@@ -1,42 +1,42 @@
<template>
<div class="record-navigator">
<icon-button
:disabled="modelValue === 0"
tooltip="First row"
tooltip-position="top-left"
class="first"
@click="$emit('update:modelValue', 0)"
>
<edge-arrow-icon :disabled="false" />
</icon-button>
<icon-button
:disabled="modelValue === 0"
tooltip="Previous row"
tooltip-position="top-left"
class="prev"
@click="$emit('update:modelValue', modelValue - 1)"
>
<arrow-icon :disabled="false" />
</icon-button>
<icon-button
:disabled="modelValue === total - 1"
tooltip="Next row"
tooltip-position="top-left"
class="next"
@click="$emit('update:modelValue', modelValue + 1)"
>
<arrow-icon :disabled="false" />
</icon-button>
<icon-button
:disabled="modelValue === total - 1"
tooltip="Last row"
tooltip-position="top-left"
class="last"
@click="$emit('update:modelValue', total - 1)"
>
<edge-arrow-icon :disabled="false" />
</icon-button>
</div>
<div class="record-navigator">
<icon-button
:disabled="modelValue === 0"
tooltip="First row"
tooltip-position="top-left"
class="first"
@click="$emit('update:modelValue', 0)"
>
<edge-arrow-icon :disabled="false" />
</icon-button>
<icon-button
:disabled="modelValue === 0"
tooltip="Previous row"
tooltip-position="top-left"
class="prev"
@click="$emit('update:modelValue', modelValue - 1)"
>
<arrow-icon :disabled="false" />
</icon-button>
<icon-button
:disabled="modelValue === total - 1"
tooltip="Next row"
tooltip-position="top-left"
class="next"
@click="$emit('update:modelValue', modelValue + 1)"
>
<arrow-icon :disabled="false" />
</icon-button>
<icon-button
:disabled="modelValue === total - 1"
tooltip="Last row"
tooltip-position="top-left"
class="last"
@click="$emit('update:modelValue', total - 1)"
>
<edge-arrow-icon :disabled="false" />
</icon-button>
</div>
</template>
<script>
@@ -59,7 +59,7 @@ export default {
<style scoped>
.record-navigator {
display: flex;
display: flex;
}
.record-navigator .next,

View File

@@ -9,11 +9,9 @@
>
<thead>
<tr>
<th/>
<th />
<th>
<div class="cell-data">
Row #{{ currentRowIndex + 1 }}
</div>
<div class="cell-data">Row #{{ currentRowIndex + 1 }}</div>
</th>
</tr>
</thead>
@@ -39,11 +37,11 @@
</div>
<div class="table-footer">
<div class="table-footer-count">
{{ rowCount }} {{rowCount === 1 ? 'row' : 'rows'}} retrieved
{{ rowCount }} {{ rowCount === 1 ? 'row' : 'rows' }} retrieved
<span v-if="time">in {{ time }}</span>
</div>
<row-navigator v-model="currentRowIndex" :total="rowCount"/>
<row-navigator v-model="currentRowIndex" :total="rowCount" />
</div>
</div>
</template>
@@ -61,31 +59,32 @@ export default {
selectedColumnIndex: Number
},
emits: ['updateSelectedCell'],
data () {
data() {
return {
selectedCellElement: null,
currentRowIndex: this.rowIndex
}
},
computed: {
columns () {
columns() {
return this.dataSet.columns
},
rowCount () {
rowCount() {
return this.dataSet.values[this.columns[0]].length
}
},
mounted () {
mounted() {
const col = this.selectedColumnIndex
const row = this.currentRowIndex
const cell = this.$refs.table
.querySelector(`td[data-col="${col}"][data-row="${row}"]`)
const cell = this.$refs.table.querySelector(
`td[data-col="${col}"][data-row="${row}"]`
)
if (cell) {
this.selectCell(cell)
}
},
watch: {
async currentRowIndex () {
async currentRowIndex() {
await nextTick()
if (this.selectedCellElement) {
const previouslySelected = this.selectedCellElement
@@ -95,16 +94,16 @@ export default {
}
},
methods: {
isBlob (value) {
isBlob(value) {
return value && ArrayBuffer.isView(value)
},
isNull (value) {
isNull(value) {
return value === null
},
getCellValue (col) {
getCellValue(col) {
return this.dataSet.values[col][this.currentRowIndex]
},
getCellText (col) {
getCellText(col) {
const value = this.getCellValue(col)
if (this.isNull(value)) {
return 'NULL'
@@ -114,7 +113,7 @@ export default {
}
return value
},
onTableKeydown (e) {
onTableKeydown(e) {
const keyCodeMap = {
38: 'up',
40: 'down'
@@ -130,10 +129,10 @@ export default {
this.moveFocusInTable(this.selectedCellElement, keyCodeMap[e.keyCode])
},
onCellClick (e) {
onCellClick(e) {
this.selectCell(e.target.closest('td'), false)
},
selectCell (cell, scrollTo = true) {
selectCell(cell, scrollTo = true) {
if (!cell) {
if (this.selectedCellElement) {
this.selectedCellElement.ariaSelected = 'false'
@@ -152,19 +151,21 @@ export default {
if (this.selectedCellElement && scrollTo) {
this.selectedCellElement.scrollIntoView()
this.selectedCellElement.closest('.table-container').scrollTo({ left: 0 })
this.selectedCellElement
.closest('.table-container')
.scrollTo({ left: 0 })
}
this.$emit('updateSelectedCell', this.selectedCellElement)
},
moveFocusInTable (initialCell, direction) {
moveFocusInTable(initialCell, direction) {
const currentColIndex = +initialCell.dataset.col
const newColIndex = direction === 'up'
? currentColIndex - 1
: currentColIndex + 1
const newColIndex =
direction === 'up' ? currentColIndex - 1 : currentColIndex + 1
const newCell = this.$refs.table
.querySelector(`td[data-col="${newColIndex}"][data-row="${this.currentRowIndex}"]`)
const newCell = this.$refs.table.querySelector(
`td[data-col="${newColIndex}"][data-row="${this.currentRowIndex}"]`
)
if (newCell) {
this.selectCell(newCell)
}
@@ -180,7 +181,7 @@ table.sqliteviz-table:focus {
.sqliteviz-table tbody td:hover {
background-color: var(--color-bg-light-3);
}
.sqliteviz-table tbody td[aria-selected="true"] {
.sqliteviz-table tbody td[aria-selected='true'] {
box-shadow: inset 0 0 0 1px var(--color-accent);
}

View File

@@ -12,13 +12,7 @@
{{ format.text }}
</button>
<button
type="button"
class="copy"
@click="copyToClipboard"
>
Copy
</button>
<button type="button" class="copy" @click="copyToClipboard">Copy</button>
</div>
<div class="value-body">
<codemirror
@@ -30,7 +24,8 @@
<pre
v-if="currentFormat === 'text'"
:class="['text-value', { 'meta-value': isNull || isBlob }]"
>{{ cellText }}</pre>
>{{ cellText }}</pre
>
<logs
v-if="messages && messages.length > 0"
:messages="messages"
@@ -60,7 +55,7 @@ export default {
props: {
cellValue: [String, Number, Uint8Array]
},
data () {
data() {
return {
formats: [
{ text: 'Text', value: 'text' },
@@ -82,13 +77,13 @@ export default {
}
},
computed: {
isBlob () {
isBlob() {
return this.cellValue && ArrayBuffer.isView(this.cellValue)
},
isNull () {
isNull() {
return this.cellValue === null
},
cellText () {
cellText() {
const value = this.cellValue
if (this.isNull) {
return 'NULL'
@@ -100,14 +95,14 @@ export default {
}
},
watch: {
currentFormat () {
currentFormat() {
this.messages = []
this.formattedJson = ''
if (this.currentFormat === 'json') {
this.formatJson(this.cellValue)
}
},
cellValue () {
cellValue() {
this.messages = []
if (this.currentFormat === 'json') {
this.formatJson(this.cellValue)
@@ -115,25 +110,24 @@ export default {
}
},
methods: {
formatJson (jsonStr) {
formatJson(jsonStr) {
try {
this.formattedJson = JSON.stringify(
JSON.parse(jsonStr), null, 4
)
this.formattedJson = JSON.stringify(JSON.parse(jsonStr), null, 4)
} catch (e) {
console.error(e)
this.formattedJson = ''
this.messages = [{
type: 'error',
message: 'Can\'t parse JSON.'
}]
this.messages = [
{
type: 'error',
message: "Can't parse JSON."
}
]
}
},
copyToClipboard () {
cIo.copyText(this.currentFormat === 'json'
? this.formattedJson
: this.cellValue,
'The value is copied to clipboard.'
copyToClipboard() {
cIo.copyText(
this.currentFormat === 'json' ? this.formattedJson : this.cellValue,
'The value is copied to clipboard.'
)
}
}
@@ -188,7 +182,7 @@ export default {
background-color: var(--color-bg-light);
}
.value-viewer-toolbar button[aria-selected="true"] {
.value-viewer-toolbar button[aria-selected='true'] {
color: var(--color-accent);
}

View File

@@ -1,23 +1,33 @@
<template>
<div class="run-result-panel" ref="runResultPanel">
<component
:is="viewValuePanelVisible ? 'splitpanes':'div'"
<component
:is="viewValuePanelVisible ? 'splitpanes' : 'div'"
:before="{ size: 50, max: 100 }"
:after="{ size: 50, max: 100 }"
:default="{ before: 50, after: 50 }"
class="run-result-panel-content"
>
<template #left-pane>
<div :id="'run-result-left-pane-'+tab.id" class="result-set-container"/>
<template #left-pane>
<div
:id="'run-result-left-pane-' + tab.id"
class="result-set-container"
/>
</template>
<div :id="'run-result-result-set-'+tab.id" class="result-set-container"/>
<div
:id="'run-result-result-set-' + tab.id"
class="result-set-container"
/>
<template #right-pane v-if="viewValuePanelVisible">
<div class="value-viewer-container">
<value-viewer
v-show="selectedCell"
:cellValue="selectedCell
? result.values[result.columns[selectedCell.dataset.col]][selectedCell.dataset.row]
: ''"
:cellValue="
selectedCell
? result.values[result.columns[selectedCell.dataset.col]][
selectedCell.dataset.row
]
: ''
"
/>
<div v-show="!selectedCell" class="table-preview">
No cell selected to view
@@ -33,7 +43,7 @@
tooltip-position="top-left"
@click="exportToCsv"
>
<export-to-csv-icon/>
<export-to-csv-icon />
</icon-button>
<icon-button
@@ -43,7 +53,7 @@
tooltip-position="top-left"
@click="prepareCopy"
>
<clipboard-icon/>
<clipboard-icon />
</icon-button>
<icon-button
@@ -54,7 +64,7 @@
:active="viewRecord"
@click="toggleViewRecord"
>
<row-icon/>
<row-icon />
</icon-button>
<icon-button
@@ -65,7 +75,7 @@
:active="viewValuePanelVisible"
@click="toggleViewValuePanel"
>
<view-cell-value-icon/>
<view-cell-value-icon />
</icon-button>
</side-tool-bar>
@@ -80,50 +90,46 @@
@cancel="cancelCopy"
/>
<teleport
defer
:to="resultSetTeleportTarget"
:disabled="!enableTeleport"
>
<teleport defer :to="resultSetTeleportTarget" :disabled="!enableTeleport">
<div>
<div
v-show="result === null && !isGettingResults && !error"
class="table-preview result-before"
>
Run your query and get results here
</div>
<div v-if="isGettingResults" class="table-preview result-in-progress">
<loading-indicator :size="30"/>
Fetching results...
</div>
<div
v-show="result === undefined && !isGettingResults && !error"
class="table-preview result-empty"
>
No rows retrieved according to your query
</div>
<logs v-if="error" :messages="[error]"/>
<sql-table
v-if="result && !viewRecord"
:data-set="result"
:time="time"
:pageSize="pageSize"
:page="defaultPage"
:selected-cell-coordinates="defaultSelectedCell"
class="straight"
@updateSelectedCell="onUpdateSelectedCell"
/>
<div
v-show="result === null && !isGettingResults && !error"
class="table-preview result-before"
>
Run your query and get results here
</div>
<div v-if="isGettingResults" class="table-preview result-in-progress">
<loading-indicator :size="30" />
Fetching results...
</div>
<div
v-show="result === undefined && !isGettingResults && !error"
class="table-preview result-empty"
>
No rows retrieved according to your query
</div>
<logs v-if="error" :messages="[error]" />
<sql-table
v-if="result && !viewRecord"
:data-set="result"
:time="time"
:pageSize="pageSize"
:page="defaultPage"
:selected-cell-coordinates="defaultSelectedCell"
class="straight"
@updateSelectedCell="onUpdateSelectedCell"
/>
<record
ref="recordView"
v-if="result && viewRecord"
:data-set="result"
:time="time"
:selected-column-index="selectedCell ? +selectedCell.dataset.col : 0"
:rowIndex="selectedCell ? +selectedCell.dataset.row : 0"
@updateSelectedCell="onUpdateSelectedCell"
/>
</div>
<record
ref="recordView"
v-if="result && viewRecord"
:data-set="result"
:time="time"
:selected-column-index="selectedCell ? +selectedCell.dataset.col : 0"
:rowIndex="selectedCell ? +selectedCell.dataset.row : 0"
@updateSelectedCell="onUpdateSelectedCell"
/>
</div>
</teleport>
</div>
</template>
@@ -158,7 +164,7 @@ export default {
time: [String, Number]
},
emits: ['switchTo'],
data () {
data() {
return {
resizeObserver: null,
pageSize: 20,
@@ -188,44 +194,45 @@ export default {
Splitpanes
},
computed: {
resultSetTeleportTarget () {
resultSetTeleportTarget() {
if (!this.enableTeleport) {
return undefined
}
const base = `#${this.viewValuePanelVisible
? 'run-result-left-pane'
: 'run-result-result-set'
const base = `#${
this.viewValuePanelVisible
? 'run-result-left-pane'
: 'run-result-result-set'
}`
const tabIdPostfix = `-${this.tab.id}`
return base + tabIdPostfix
}
},
activated () {
activated() {
this.enableTeleport = true
},
deactivated () {
deactivated() {
this.enableTeleport = false
},
mounted () {
mounted() {
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.runResultPanel)
this.calculatePageSize()
},
beforeUnmount () {
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.runResultPanel)
},
watch: {
result () {
result() {
this.defaultSelectedCell = null
this.selectedCell = null
}
},
methods: {
handleResize () {
handleResize() {
this.calculatePageSize()
},
calculatePageSize () {
calculatePageSize() {
const runResultPanel = this.$refs.runResultPanel
// 27 - table footer hight
// 5 - padding-bottom of rounded table container
@@ -234,9 +241,10 @@ export default {
this.pageSize = Math.max(Math.floor(freeSpace / 35), 20)
},
exportToCsv () {
exportToCsv() {
if (this.result && this.result.values) {
events.send('resultset.export',
events.send(
'resultset.export',
this.result.values[this.result.columns[0]].length,
{ to: 'csv' }
)
@@ -245,9 +253,10 @@ export default {
fIo.exportToFile(csv.serialize(this.result), 'result_set.csv', 'text/csv')
},
async prepareCopy () {
async prepareCopy() {
if (this.result && this.result.values) {
events.send('resultset.export',
events.send(
'resultset.export',
this.result.values[this.result.columns[0]].length,
{ to: 'clipboard' }
)
@@ -261,7 +270,7 @@ export default {
await time.sleep(0)
this.dataToCopy = csv.serialize(this.result)
const t1 = performance.now()
if ((t1 - t0) < 950) {
if (t1 - t0 < 950) {
this.$modal.hide('prepareCSVCopy')
this.copyToClipboard()
} else {
@@ -270,27 +279,27 @@ export default {
} else {
alert(
"Your browser doesn't support copying into the clipboard. " +
'If you use Firefox you can enable it ' +
'by setting dom.events.asyncClipboard.clipboardItem to true.'
'If you use Firefox you can enable it ' +
'by setting dom.events.asyncClipboard.clipboardItem to true.'
)
}
},
copyToClipboard () {
copyToClipboard() {
cIo.copyText(this.dataToCopy, 'CSV copied to clipboard successfully')
this.$modal.hide('prepareCSVCopy')
},
cancelCopy () {
cancelCopy() {
this.dataToCopy = null
this.$modal.hide('prepareCSVCopy')
},
toggleViewValuePanel () {
toggleViewValuePanel() {
this.viewValuePanelVisible = !this.viewValuePanelVisible
},
toggleViewRecord () {
toggleViewRecord() {
if (this.viewRecord) {
this.defaultSelectedCell = {
row: this.$refs.recordView.currentRowIndex,
@@ -304,7 +313,7 @@ export default {
this.viewRecord = !this.viewRecord
},
onUpdateSelectedCell (e) {
onUpdateSelectedCell(e) {
this.selectedCell = e
}
}

View File

@@ -17,7 +17,7 @@
tooltip-position="top-left"
@click="$emit('switchTo', 'table')"
>
<table-icon/>
<table-icon />
</icon-button>
<icon-button
@@ -30,9 +30,9 @@
<data-view-icon />
</icon-button>
<div class="side-tool-bar-divider" v-if="$slots.default"/>
<div class="side-tool-bar-divider" v-if="$slots.default" />
<slot/>
<slot />
</div>
</template>

View File

@@ -3,17 +3,19 @@ import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/hint/sql-hint.js'
import store from '@/store'
function _getHintText (hint) {
function _getHintText(hint) {
return typeof hint === 'string' ? hint : hint.text
}
export function getHints (cm, options) {
export function getHints(cm, options) {
const result = CM.hint.sql(cm, options)
// Don't show the hint if there is only one option
// and the replacingText is already equals to this option
const replacedText = cm.getRange(result.from, result.to).toUpperCase()
if (result.list.length === 1 &&
_getHintText(result.list[0]).toUpperCase() === replacedText) {
if (
result.list.length === 1 &&
_getHintText(result.list[0]).toUpperCase() === replacedText
) {
result.list = []
}
@@ -21,7 +23,7 @@ export function getHints (cm, options) {
}
const hintOptions = {
get tables () {
get tables() {
const tables = {}
if (store.state.db.schema) {
store.state.db.schema.forEach(table => {
@@ -30,7 +32,7 @@ const hintOptions = {
}
return tables
},
get defaultTable () {
get defaultTable() {
const schema = store.state.db.schema
return schema && schema.length === 1 ? schema[0].name : null
},
@@ -39,11 +41,11 @@ const hintOptions = {
alignWithWord: false
}
export function showHintOnDemand (editor) {
export function showHintOnDemand(editor) {
CM.showHint(editor, getHints, hintOptions)
}
export default function showHint (editor) {
export default function showHint(editor) {
// Don't show autocomplete after a space or semicolon or in string literals
const token = editor.getTokenAt(editor.getCursor())
const ch = token.string.slice(-1)

View File

@@ -18,7 +18,7 @@
tooltip-position="top-left"
@click="$emit('run')"
>
<run-icon :disabled="runDisabled"/>
<run-icon :disabled="runDisabled" />
</icon-button>
</side-tool-bar>
</div>
@@ -47,7 +47,7 @@ export default {
IconButton,
RunIcon
},
data () {
data() {
return {
query: this.modelValue,
cmOptions: {
@@ -63,18 +63,18 @@ export default {
}
},
computed: {
runDisabled () {
return (!this.$store.state.db || !this.query || this.isGettingResults)
runDisabled() {
return !this.$store.state.db || !this.query || this.isGettingResults
}
},
watch: {
query () {
query() {
this.$emit('update:modelValue', this.query)
}
},
methods: {
onChange: time.debounce((value, editor) => showHint(editor), 400),
focus () {
focus() {
this.$refs.cm.cminstance?.focus()
}
}

View File

@@ -11,15 +11,15 @@
<div :id="'above-' + tab.id" class="above" />
</template>
<template #right-pane>
<div :id="'bottom-'+ tab.id" ref="bottomPane" class="bottomPane" />
<div :id="'bottom-' + tab.id" ref="bottomPane" class="bottomPane" />
</template>
</splitpanes>
<div :id="'hidden-'+ tab.id" class="hidden-part" />
<div :id="'hidden-' + tab.id" class="hidden-part" />
<teleport
defer
:to="enableTeleport ? `#${tab.layout.sqlEditor}-${tab.id}`: undefined"
:to="enableTeleport ? `#${tab.layout.sqlEditor}-${tab.id}` : undefined"
:disabled="!enableTeleport"
>
<sql-editor
@@ -33,7 +33,7 @@
<teleport
defer
:to="enableTeleport ? `#${tab.layout.table}-${tab.id}`: undefined"
:to="enableTeleport ? `#${tab.layout.table}-${tab.id}` : undefined"
:disabled="!enableTeleport"
>
<run-result
@@ -84,51 +84,53 @@ export default {
RunResult,
Splitpanes
},
data () {
data() {
return {
topPaneSize: this.tab.maximize
? this.tab.layout[this.tab.maximize] === 'above' ? 100 : 0
? this.tab.layout[this.tab.maximize] === 'above'
? 100
: 0
: 50,
enableTeleport: this.$store.state.isWorkspaceVisible
}
},
computed: {
isActive () {
isActive() {
return this.tab.id === this.$store.state.currentTabId
}
},
watch: {
isActive: {
immediate: true,
async handler () {
async handler() {
if (this.isActive) {
await nextTick()
this.$refs.sqlEditor?.focus()
}
}
},
'tab.query' () {
'tab.query'() {
this.$store.commit('updateTab', {
tab: this.tab,
newValues: { isSaved: false }
})
}
},
async activated () {
async activated() {
this.enableTeleport = true
if (this.isActive) {
await nextTick()
this.$refs.sqlEditor.focus()
}
},
deactivated () {
deactivated() {
this.enableTeleport = false
},
async mounted () {
async mounted() {
this.tab.dataView = this.$refs.dataView
},
methods: {
onSwitchView (from, to) {
onSwitchView(from, to) {
const fromPosition = this.tab.layout[from]
this.tab.layout[from] = this.tab.layout[to]
this.tab.layout[to] = fromPosition
@@ -136,7 +138,7 @@ export default {
events.send('inquiry.panel', null, { panel: to })
},
onDataViewUpdate () {
onDataViewUpdate() {
this.$store.commit('updateTab', {
tab: this.tab,
newValues: { isSaved: false }

View File

@@ -1,11 +1,11 @@
<template>
<div id="tabs">
<div id="tabs">
<div id="tabs-header" v-if="tabs.length > 0">
<div
v-for="(tab, index) in tabs"
:key="index"
@click="selectTab(tab.id)"
:class="[{'tab-selected': (tab.id === selectedTabId)}, 'tab']"
:class="[{ 'tab-selected': tab.id === selectedTabId }, 'tab']"
>
<div class="tab-name">
<span v-show="!tab.isSaved" class="star">*</span>
@@ -13,15 +13,15 @@
<span v-else class="tab-untitled">{{ tab.tempName }}</span>
</div>
<div>
<close-icon class="close-icon" :size="10" @click="beforeCloseTab(tab)"/>
<close-icon
class="close-icon"
:size="10"
@click="beforeCloseTab(tab)"
/>
</div>
</div>
</div>
<tab
v-for="tab in tabs"
:key="tab.id"
:tab="tab"
/>
<tab v-for="tab in tabs" :key="tab.id" :tab="tab" />
<div v-show="tabs.length === 0" id="start-guide">
<span class="link" @click="emitCreateTabEvent">Create</span>
new inquiry from scratch or open one from
@@ -31,26 +31,33 @@
<!--Close tab warning dialog -->
<modal modal-id="close-warn" class="dialog" content-style="width: 560px;">
<div class="dialog-header">
Close tab {{
Close tab
{{
closingTab !== null
? (closingTab.name || `[${closingTab.tempName}]`)
: ''
? closingTab.name || `[${closingTab.tempName}]`
: ''
}}
<close-icon @click="$modal.hide('close-warn')"/>
<close-icon @click="$modal.hide('close-warn')" />
</div>
<div class="dialog-body">
You have unsaved changes. Save changes in {{
You have unsaved changes. Save changes in
{{
closingTab !== null
? (closingTab.name || `[${closingTab.tempName}]`)
: ''
}} before closing?
? closingTab.name || `[${closingTab.tempName}]`
: ''
}}
before closing?
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="closeTab(closingTab)">
Close without saving
</button>
<button class="secondary" @click="$modal.hide('close-warn')">Don't close</button>
<button class="primary" @click="saveAndClose(closingTab)">Save and close</button>
<button class="secondary" @click="$modal.hide('close-warn')">
Don't close
</button>
<button class="primary" @click="saveAndClose(closingTab)">
Save and close
</button>
</div>
</modal>
</div>
@@ -67,36 +74,36 @@ export default {
Tab,
CloseIcon
},
data () {
data() {
return {
closingTab: null
}
},
computed: {
tabs () {
tabs() {
return this.$store.state.tabs
},
selectedTabId () {
selectedTabId() {
return this.$store.state.currentTabId
}
},
created () {
created() {
window.addEventListener('beforeunload', this.leavingSqliteviz)
},
methods: {
emitCreateTabEvent () {
emitCreateTabEvent() {
eventBus.$emit('createNewInquiry')
},
leavingSqliteviz (event) {
leavingSqliteviz(event) {
if (this.tabs.some(tab => !tab.isSaved)) {
event.preventDefault()
event.returnValue = ''
}
},
selectTab (id) {
selectTab(id) {
this.$store.commit('setCurrentTabId', id)
},
beforeCloseTab (tab) {
beforeCloseTab(tab) {
this.closingTab = tab
if (!tab.isSaved) {
this.$modal.show('close-warn')
@@ -104,11 +111,11 @@ export default {
this.closeTab(tab)
}
},
closeTab (tab) {
closeTab(tab) {
this.$modal.hide('close-warn')
this.$store.commit('deleteTab', tab)
},
saveAndClose (tab) {
saveAndClose(tab) {
eventBus.$on('inquirySaved', () => {
this.closeTab(tab)
eventBus.$off('inquirySaved')

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