1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-07 18:48:55 +08:00

50 Commits
0.1.1 ... build

Author SHA1 Message Date
lana-k
10c49bda7f Deploying to build from @ lana-k/sqliteviz@c674bf11e3 🚀 2022-07-10 20:40:36 +00:00
lana-k
69445c1f5a Deploying to build from @ lana-k/sqliteviz@310a939109 🚀 2021-12-24 15:25:29 +00:00
lana-k
8cdaf45079 Deploying to build from @ lana-k/sqliteviz@9c55e76a41 🚀 2021-12-19 15:09:39 +00:00
lana-k
77ff2df17c Deploying to build from @ lana-k/sqliteviz@b2c2344951 🚀 2021-12-19 14:46:45 +00:00
lana-k
c8f8625486 Deploying to build from @ lana-k/sqliteviz@3e3a70430f 🚀 2021-09-03 14:12:08 +00:00
lana-k
1d741ef87e Deploying to build from @ lana-k/sqliteviz@4213e9df5c 🚀 2021-08-11 21:01:39 +00:00
lana-k
35a838af00 Deploying to build from @ lana-k/sqliteviz@4213e9df5c 🚀 2021-08-11 20:55:28 +00:00
lana-k
501aaf1a26 Deploying to build from @ lana-k/sqliteviz@4213e9df5c 🚀 2021-08-11 20:51:19 +00:00
lana-k
d580de1b50 Deploying to build from @ lana-k/sqliteviz@2ed5160f65 🚀 2021-08-05 16:49:57 +00:00
lana-k
20d6f4b854 Deploying to build from @ lana-k/sqliteviz@848112979b 🚀 2021-08-05 16:39:32 +00:00
lana-k
ac30abeb76 Deploying to build from @ lana-k/sqliteviz@2830df2adc 🚀 2021-08-04 20:57:44 +00:00
lana-k
e268ca7c99 Deploying to build from @ lana-k/sqliteviz@d07506266c 🚀 2021-07-15 20:45:21 +00:00
lana-k
3c54138494 Deploying to build from @ lana-k/sqliteviz@cea1d40797 🚀 2021-07-15 20:41:05 +00:00
lana-k
3b632af4b2 Deploying to build from @ lana-k/sqliteviz@fb930028de 🚀 2021-07-04 11:54:27 +00:00
lana-k
cbe7a1c3bd Deploying to build from @ lana-k/sqliteviz@f9edeafd40 🚀 2021-07-01 17:20:56 +00:00
lana-k
2fc500a86d Deploying to build from @ lana-k/sqliteviz@a37ed93306 🚀 2021-06-17 10:29:03 +00:00
lana-k
81901f6d87 Deploying to build from @ lana-k/sqliteviz@1251c542cb 🚀 2021-05-25 10:25:11 +00:00
lana-k
397dea2782 Deploying to build from @ lana-k/sqliteviz@8a9f4b3c0a 🚀 2021-05-18 13:11:33 +00:00
lana-k
658c7b1b12 Deploying to build from @ lana-k/sqliteviz@9b3dda6cff 🚀 2021-05-05 21:47:19 +00:00
lana-k
a565e4d841 Merge branch 'build' of github.com:lana-k/sqliteviz into build 2021-05-05 23:39:40 +02:00
lana-k
362ddae678 add .nojekyll 2021-05-05 23:39:06 +02:00
lana-k
ee369d351b Deploying to build from @ lana-k/sqliteviz@16868ef430 🚀 2021-05-05 21:34:18 +00:00
lana-k
ad3d411062 Deploying to build from @ 8e856063b8 🚀 2021-05-05 21:16:46 +00:00
lana-k
e3977f910e Deploying to build from @ 8684b4cef9 🚀 2021-05-05 20:52:12 +00:00
lana-k
d41bf8e44e Deploying to build from @ 8684b4cef9 🚀 2021-05-05 19:58:24 +00:00
lana-k
38d77319b5 Deploying to build from @ a07f2d3d99 🚀 2021-05-02 19:08:12 +00:00
lana-k
e0637cd44f Deploying to build from @ 297ea2c18a 🚀 2021-04-30 17:47:40 +00:00
lana-k
b64c69be59 Deploying to build from @ 1f2327a724 🚀 2021-04-30 17:07:01 +00:00
lana-k
450c15f233 Deploying to build from @ 6d512422cf 🚀 2021-04-30 16:43:29 +00:00
lana-k
7a47763510 Deploying to build from @ 9a91dd19bf 🚀 2021-04-23 15:02:51 +00:00
lana-k
61aaa766a8 Deploying to build from @ 1150b2e7c4 🚀 2020-12-25 17:43:08 +00:00
lana-k
e95ba853cd Deploying to build from @ e2db4e2634 🚀 2020-12-21 19:19:08 +00:00
lana-k
568c14940b Deploying to build from @ 5ed3266942 🚀 2020-11-20 18:21:54 +00:00
lana-k
bef8317379 Deploying to build from @ c991691fcb 🚀 2020-11-14 19:47:08 +00:00
lana-k
41a7645095 Deploying to build from @ 4c685b6d7c 🚀 2020-11-10 20:47:39 +00:00
lana-k
1b38a1027e Deploying to build from @ a5ba1cb821 🚀 2020-11-10 17:27:47 +00:00
lana-k
b9209aa119 Deploying to build from @ fec8fb5ac0 🚀 2020-10-25 21:34:02 +00:00
lana-k
816658a781 Deploying to build from @ 3e5e4b29c1 🚀 2020-10-20 12:57:54 +00:00
lana-k
d280b63490 Deploying to build from @ 518270e1f5 🚀 2020-10-15 18:56:37 +00:00
lana-k
b92976c2a9 Deploying to build from @ 36f3fa87e3 🚀 2020-10-12 11:48:39 +00:00
lana-k
b20ea14e63 Deploying to build from @ 7448d03c86 🚀 2020-10-12 11:35:51 +00:00
lana-k
ff8a420c40 clean build 2020-10-12 13:32:38 +02:00
lana-k
c845a5ec36 Deploying to build from @ 7448d03c86 🚀 2020-10-12 11:22:49 +00:00
lana-k
3556ace55d Update build to output generated at 3008609 2020-10-12 10:58:02 +00:00
lana-k
8b470a4a8f fix production mode with vuera 2020-10-11 16:21:02 +02:00
lana-k
84ddb7ca72 dev mode 2020-10-10 23:15:25 +02:00
lana-k
53dc479815 new build 2020-10-10 12:45:37 +02:00
lana-k
4e1aa70c2e fix path to worker 2020-10-09 22:19:22 +02:00
lana-k
851b2210b3 fix publicPath 2020-10-09 22:12:04 +02:00
lana-k
ae679859c1 0.0.1 2020-10-09 21:50:02 +02:00
82 changed files with 522 additions and 19453 deletions

View File

@@ -1,3 +0,0 @@
> 1%
last 2 versions
not dead

View File

@@ -1,5 +0,0 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

View File

@@ -1,28 +0,0 @@
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/essential',
'@vue/standard'
],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)'
],
env: {
mocha: true
}
}
]
}

View File

@@ -1,29 +0,0 @@
name: Deploy to GitHub Pages
on:
workflow_dispatch:
push:
tags:
- '*'
jobs:
deploy:
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 10.x
- name: npm install and build
run: |
npm install
npm run build
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@3.6.2
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: build # The branch the action should deploy to.
FOLDER: dist/ # The folder the action should deploy.
CLEAN: false # Automatically remove deleted files from the deploy branch

22
.gitignore vendored
View File

@@ -1,22 +0,0 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

0
.nojekyll Normal file
View File

201
LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

BIN
Logo192x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
Logo48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
Logo512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -1,21 +0,0 @@
# sqliteviz
**Note: This is a work-in-progress project.**
Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases.
It's a kind of middleground between [Plotly Falcon][1] and [Redash][2].
It is built on top of [react-chart-editor][3] and [sql.js][4] in [Vue.js][5].
## Features
1. Ad-hoc and prepared visualisation of a SQLite database using flexibility
of SQL and richness of Plotly's `react-chart-editor`
2. Visualisation pallete, SQL and chart definitions, kept in local
storage with import/export
3. Integration-side provided pallete
[1]: https://github.com/plotly/falcon
[2]: https://github.com/getredash/redash
[3]: https://github.com/plotly/react-chart-editor
[4]: https://github.com/sql-js/sql.js
[5]: https://github.com/vuejs/vue

View File

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

1
css/app.ec26c5dd.css Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
dist.zip Normal file

Binary file not shown.

BIN
favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
img/file.f5540c43.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

102
index.html Normal file
View File

@@ -0,0 +1,102 @@
<!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">
<title>sqliteviz</title>
<style>
#sqliteviz-loading-wrapper {
position: fixed;
width: 100%;
height: 100%;
left: 0;
top: 0;
background-color: white;
}
#sqliteviz-loading-text {
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #506784;
font-family: sans-serif;
font-size: 20px;
}
#sqliteviz-loading-wrapper svg {
display: block;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
#sqliteviz-loading-wrapper circle {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
fill: none;
stroke-width: 5px;
stroke-linecap: round;
stroke: #119DFF;
}
#sqliteviz-loading-wrapper circle.bg {
stroke: #C8D4E3;
}
#sqliteviz-loading-wrapper circle.front {
stroke-dasharray: 402px;
animation: sqliteviz-loading 2s linear 0s infinite;
}
@keyframes sqliteviz-loading {
0% {
stroke-dasharray: 100px 402px;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 251px;
stroke-dashoffset: -251px;
}
100% {
stroke-dasharray: 100px 402px;
stroke-dashoffset: -502px;
}
}
</style>
<!-- head extention slot start -->
<!-- head extention slot end -->
<link href="js/chunk-2d22998c.869ca7da.js" rel="prefetch"><link href="css/app.ec26c5dd.css" rel="preload" as="style"><link href="css/chunk-vendors.e843161b.css" rel="preload" as="style"><link href="js/app.925bc521.js" rel="preload" as="script"><link href="js/chunk-vendors.889e6f5b.js" rel="preload" as="script"><link href="css/chunk-vendors.e843161b.css" rel="stylesheet"><link href="css/app.ec26c5dd.css" rel="stylesheet"></head>
<body>
<noscript>
<strong>We're sorry but sqliteviz 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"
/>
</svg>
</div>
</div>
<!-- extention slot start -->
<!-- extention slot end -->
<!-- built files will be auto injected -->
<script type="text/javascript" src="js/chunk-vendors.889e6f5b.js"></script><script type="text/javascript" src="js/app.925bc521.js"></script></body>
</html>

1
inquiries.json Normal file
View File

@@ -0,0 +1 @@
[]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
js/app.925bc521.js Normal file

File diff suppressed because one or more lines are too long

1
js/app.925bc521.js.map Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d22998c"],{ddb8:function(e,n,t){"use strict";t.r(n);var a=t("c7eb"),i=t("1da1"),r=t("28b7"),o=!1;function c(e){var n=confirm("New version of the app is available. Refresh now?");n&&e.waiting&&(o=!0,e.waiting.postMessage({type:"SKIP_WAITING"}))}"serviceWorker"in navigator&&(window.addEventListener("load",Object(i["a"])(Object(a["a"])().mark((function e(){var n;return Object(a["a"])().wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.next=2,navigator.serviceWorker.register("service-worker.js");case 2:n=e.sent,n.waiting&&c(n),n.addEventListener("updatefound",(function(){var e=n.installing;e&&e.addEventListener("statechange",(function(){n.waiting&&navigator.serviceWorker.controller&&c(n)}))})),navigator.serviceWorker.addEventListener("controllerchange",(function(){o&&(window.location.reload(),o=!1)}));case 6:case"end":return e.stop()}}),e)})))),window.addEventListener("appinstalled",(function(){Object(r["a"])("pwa.install")})))}}]);
//# sourceMappingURL=chunk-2d22998c.869ca7da.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["webpack:///./src/registerServiceWorker.js"],"names":["refresh","invokeServiceWorkerUpdateFlow","registration","agree","confirm","waiting","postMessage","type","navigator","window","addEventListener","serviceWorker","register","newRegestration","installing","controller","location","reload","send"],"mappings":"+JACIA,GAAU,EAEd,SAASC,EAA+BC,GACtC,IAAMC,EAAQC,QAAQ,qDAClBD,GACED,EAAaG,UAEfL,GAAU,EACVE,EAAaG,QAAQC,YAAY,CAAEC,KAAM,kBAK3C,kBAAmBC,YACrBC,OAAOC,iBAAiB,OAAxB,sCAAgC,kHACHF,UAAUG,cAAcC,SAAS,qBAD9B,OACxBV,EADwB,OAI1BA,EAAaG,SACfJ,EAA8BC,GAIhCA,EAAaQ,iBAAiB,eAAe,WAC3C,IAAMG,EAAkBX,EAAaY,WACjCD,GAEFA,EAAgBH,iBAAiB,eAAe,WAC1CR,EAAaG,SAAWG,UAAUG,cAAcI,YAClDd,EAA8BC,SAOtCM,UAAUG,cAAcD,iBAAiB,oBAAoB,WACvDV,IACFS,OAAOO,SAASC,SAChBjB,GAAU,MAzBgB,4CA8BhCS,OAAOC,iBAAiB,gBAAgB,WACtCQ,eAAK","file":"js/chunk-2d22998c.869ca7da.js","sourcesContent":["import { send } from '@/lib/utils/events'\nlet refresh = false\n\nfunction invokeServiceWorkerUpdateFlow (registration) {\n const agree = confirm('New version of the app is available. Refresh now?')\n if (agree) {\n if (registration.waiting) {\n // let waiting Service Worker know it should became active\n refresh = true\n registration.waiting.postMessage({ type: 'SKIP_WAITING' })\n }\n }\n}\n\nif ('serviceWorker' in navigator) {\n window.addEventListener('load', async () => {\n const registration = await navigator.serviceWorker.register('service-worker.js')\n // ensure the case when the updatefound event was missed is also handled\n // by re-invoking the prompt when there's a waiting Service Worker\n if (registration.waiting) {\n invokeServiceWorkerUpdateFlow(registration)\n }\n\n // detect Service Worker update available and wait for it to become installed\n registration.addEventListener('updatefound', () => {\n const newRegestration = registration.installing\n if (newRegestration) {\n // wait until the new Service worker is actually installed (ready to take over)\n newRegestration.addEventListener('statechange', () => {\n if (registration.waiting && navigator.serviceWorker.controller) {\n invokeServiceWorkerUpdateFlow(registration)\n }\n })\n }\n })\n\n // detect controller change and refresh the page\n navigator.serviceWorker.addEventListener('controllerchange', () => {\n if (refresh) {\n window.location.reload()\n refresh = false\n }\n })\n })\n\n window.addEventListener('appinstalled', () => {\n send('pwa.install')\n })\n}\n"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
js/sql-wasm.wasm Normal file

Binary file not shown.

30
manifest.webmanifest Normal file
View File

@@ -0,0 +1,30 @@
{
"background_color": "white",
"description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases or CSV.",
"display": "fullscreen",
"icons": [
{
"src": "favicon.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "Logo48x48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "Logo192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "Logo512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"name": "sqliteviz",
"short_name": "sqliteviz",
"start_url": "index.html"
}

16882
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +0,0 @@
{
"name": "sqliteviz",
"version": "0.0.1",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint"
},
"dependencies": {
"codemirror": "^5.57.0",
"core-js": "^3.6.5",
"nanoid": "^3.1.12",
"plotly.js": "^1.54.6",
"react": "^16.13.1",
"react-chart-editor": "^0.41.7",
"react-dom": "^16.13.1",
"sql.js": "^1.3.0",
"sqlite-parser": "^1.0.1",
"vue": "^2.6.11",
"vue-codemirror": "^4.0.6",
"vue-js-modal": "^2.0.0-rc.6",
"vue-router": "^3.2.0",
"vuejs-paginate": "^2.1.0",
"vuera": "^0.2.7",
"vuex": "^3.4.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.4.0",
"@vue/cli-plugin-eslint": "^4.4.0",
"@vue/cli-plugin-router": "^4.4.0",
"@vue/cli-plugin-unit-mocha": "^4.4.0",
"@vue/cli-plugin-vuex": "^4.4.0",
"@vue/cli-service": "^4.4.0",
"@vue/eslint-config-standard": "^5.1.2",
"@vue/test-utils": "^1.0.3",
"babel-eslint": "^10.1.0",
"chai": "^4.1.2",
"eslint": "^6.7.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^6.2.2",
"vue-template-compiler": "^2.6.11"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

2
service-worker.js Normal file
View File

@@ -0,0 +1,2 @@
if(!self.define){let e,n={};const s=(s,i)=>(s=new URL(s+".js",i).href,n[s]||new Promise((n=>{if("document"in self){const e=document.createElement("script");e.src=s,e.onload=n,document.head.appendChild(e)}else e=s,importScripts(s),n()})).then((()=>{let e=n[s];if(!e)throw new Error(`Module ${s} didnt register its module`);return e})));self.define=(i,o)=>{const r=e||("document"in self?document.currentScript.src:"")||location.href;if(n[r])return;let c={};const f=e=>s(e,r),l={module:{uri:r},exports:c,require:f};n[r]=Promise.all(i.map((e=>l[e]||f(e)))).then((e=>(o(...e),c)))}}define(["./workbox-6716fad7"],(function(e){"use strict";self.addEventListener("message",(e=>{e.data&&"SKIP_WAITING"===e.data.type&&self.skipWaiting()})),e.clientsClaim(),e.precacheAndRoute([{url:"Logo192x192.png",revision:"15e06927b5e6e2b55c5ae6fb783d2618"},{url:"Logo48x48.png",revision:"65270f0df7353ad8c75ba5140d6d7c6a"},{url:"Logo512x512.png",revision:"50991847b13d0d0de738dc34d15e0920"},{url:"css/app.ec26c5dd.css",revision:null},{url:"css/chunk-vendors.e843161b.css",revision:null},{url:"favicon.png",revision:"f3da3682ba8d648ebaa9e09a27875c20"},{url:"fonts/OpenSans-Bold.21b37d1a.woff2",revision:"21b37d1abf90816560781f286c7dfa90"},{url:"fonts/OpenSans-BoldItalic.95149f80.woff2",revision:"95149f80a181cfb7c63078e3f44134cb"},{url:"fonts/OpenSans-Italic.ee451d9a.woff2",revision:"ee451d9aa59d5cf65407f17971d12090"},{url:"fonts/OpenSans-Regular.8abbb9d9.woff2",revision:"8abbb9d98c0c7304060190592408ab78"},{url:"fonts/OpenSans-SemiBold.2595cae6.woff2",revision:"2595cae6483bb50cbeb0cb40e3292231"},{url:"fonts/OpenSans-SemiBoldItalic.40ca9121.woff2",revision:"40ca91217266fa60c6e81c539a059190"},{url:"img/file.f5540c43.png",revision:"f5540c433396049050945be85ee9c4b1"},{url:"index.html",revision:"7cdc52e674f6a0449389f124d2151994"},{url:"js/_worker.6822ecb4.worker.js",revision:null},{url:"js/app.925bc521.js",revision:null},{url:"js/chunk-2d22998c.869ca7da.js",revision:null},{url:"js/chunk-vendors.889e6f5b.js",revision:null},{url:"js/sql-wasm.wasm",revision:"0597550407880d9778b14164b8120d0f"},{url:"manifest.webmanifest",revision:"6b085b7ef6a4cd86a1cf5ad97b0d7160"}],{})}));
//# sourceMappingURL=service-worker.js.map

1
service-worker.js.map Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"service-worker.js","sources":["../../../../../tmp/928ffc6beb002e6d4c4789fb1451e0a8/service-worker.js"],"sourcesContent":["import {clientsClaim as workbox_core_clientsClaim} from '/home/runner/work/sqliteviz/sqliteviz/node_modules/workbox-core/clientsClaim.mjs';\nimport {precacheAndRoute as workbox_precaching_precacheAndRoute} from '/home/runner/work/sqliteviz/sqliteviz/node_modules/workbox-precaching/precacheAndRoute.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\n\n\n\n\n\n\nself.addEventListener('message', (event) => {\n if (event.data && event.data.type === 'SKIP_WAITING') {\n self.skipWaiting();\n }\n});\n\nworkbox_core_clientsClaim();\n\n\n/**\n * The precacheAndRoute() method efficiently caches and responds to\n * requests for URLs in the manifest.\n * See https://goo.gl/S9QRab\n */\nworkbox_precaching_precacheAndRoute([\n {\n \"url\": \"Logo192x192.png\",\n \"revision\": \"15e06927b5e6e2b55c5ae6fb783d2618\"\n },\n {\n \"url\": \"Logo48x48.png\",\n \"revision\": \"65270f0df7353ad8c75ba5140d6d7c6a\"\n },\n {\n \"url\": \"Logo512x512.png\",\n \"revision\": \"50991847b13d0d0de738dc34d15e0920\"\n },\n {\n \"url\": \"css/app.ec26c5dd.css\",\n \"revision\": null\n },\n {\n \"url\": \"css/chunk-vendors.e843161b.css\",\n \"revision\": null\n },\n {\n \"url\": \"favicon.png\",\n \"revision\": \"f3da3682ba8d648ebaa9e09a27875c20\"\n },\n {\n \"url\": \"fonts/OpenSans-Bold.21b37d1a.woff2\",\n \"revision\": \"21b37d1abf90816560781f286c7dfa90\"\n },\n {\n \"url\": \"fonts/OpenSans-BoldItalic.95149f80.woff2\",\n \"revision\": \"95149f80a181cfb7c63078e3f44134cb\"\n },\n {\n \"url\": \"fonts/OpenSans-Italic.ee451d9a.woff2\",\n \"revision\": \"ee451d9aa59d5cf65407f17971d12090\"\n },\n {\n \"url\": \"fonts/OpenSans-Regular.8abbb9d9.woff2\",\n \"revision\": \"8abbb9d98c0c7304060190592408ab78\"\n },\n {\n \"url\": \"fonts/OpenSans-SemiBold.2595cae6.woff2\",\n \"revision\": \"2595cae6483bb50cbeb0cb40e3292231\"\n },\n {\n \"url\": \"fonts/OpenSans-SemiBoldItalic.40ca9121.woff2\",\n \"revision\": \"40ca91217266fa60c6e81c539a059190\"\n },\n {\n \"url\": \"img/file.f5540c43.png\",\n \"revision\": \"f5540c433396049050945be85ee9c4b1\"\n },\n {\n \"url\": \"index.html\",\n \"revision\": \"7cdc52e674f6a0449389f124d2151994\"\n },\n {\n \"url\": \"js/_worker.6822ecb4.worker.js\",\n \"revision\": null\n },\n {\n \"url\": \"js/app.925bc521.js\",\n \"revision\": null\n },\n {\n \"url\": \"js/chunk-2d22998c.869ca7da.js\",\n \"revision\": null\n },\n {\n \"url\": \"js/chunk-vendors.889e6f5b.js\",\n \"revision\": null\n },\n {\n \"url\": \"js/sql-wasm.wasm\",\n \"revision\": \"0597550407880d9778b14164b8120d0f\"\n },\n {\n \"url\": \"manifest.webmanifest\",\n \"revision\": \"6b085b7ef6a4cd86a1cf5ad97b0d7160\"\n }\n], {});\n\n\n\n\n\n\n\n\n"],"names":["self","addEventListener","event","data","type","skipWaiting","workbox_core_clientsClaim","workbox_precaching_precacheAndRoute","url","revision"],"mappings":"0nBAoBAA,KAAKC,iBAAiB,WAAYC,IAC5BA,EAAMC,MAA4B,iBAApBD,EAAMC,KAAKC,MAC3BJ,KAAKK,iBAITC,EAAAA,eAQAC,EAAAA,iBAAoC,CAClC,CACEC,IAAO,kBACPC,SAAY,oCAEd,CACED,IAAO,gBACPC,SAAY,oCAEd,CACED,IAAO,kBACPC,SAAY,oCAEd,CACED,IAAO,uBACPC,SAAY,MAEd,CACED,IAAO,iCACPC,SAAY,MAEd,CACED,IAAO,cACPC,SAAY,oCAEd,CACED,IAAO,qCACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,uCACPC,SAAY,oCAEd,CACED,IAAO,wCACPC,SAAY,oCAEd,CACED,IAAO,yCACPC,SAAY,oCAEd,CACED,IAAO,+CACPC,SAAY,oCAEd,CACED,IAAO,wBACPC,SAAY,oCAEd,CACED,IAAO,aACPC,SAAY,oCAEd,CACED,IAAO,gCACPC,SAAY,MAEd,CACED,IAAO,qBACPC,SAAY,MAEd,CACED,IAAO,gCACPC,SAAY,MAEd,CACED,IAAO,+BACPC,SAAY,MAEd,CACED,IAAO,mBACPC,SAAY,oCAEd,CACED,IAAO,uBACPC,SAAY,qCAEb"}

View File

@@ -1,16 +0,0 @@
<template>
<div id="app">
<router-view/>
</div>
</template>
<style>
#app, * {
font-family: Open-Sans, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
}
</style>

View File

@@ -1,3 +0,0 @@
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.721924 9.93097L4.85292 5.79997L0.721924 1.66897L1.99992 0.399973L7.39992 5.79997L1.99992 11.2L0.721924 9.93097Z" fill="#506784"/>
</svg>

Before

Width:  |  Height:  |  Size: 243 B

View File

@@ -1,3 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 1.41L12.59 0L7 5.59L1.41 0L0 1.41L5.59 7L0 12.59L1.41 14L7 8.41L12.59 14L14 12.59L8.41 7L14 1.41Z" fill="#A2B1C6"/>
</svg>

Before

Width:  |  Height:  |  Size: 232 B

View File

@@ -1,3 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.25 15.75H6V5.25H14.25V15.75ZM14.25 3.75H6C5.60218 3.75 5.22064 3.90804 4.93934 4.18934C4.65804 4.47064 4.5 4.85218 4.5 5.25V15.75C4.5 16.1478 4.65804 16.5294 4.93934 16.8107C5.22064 17.092 5.60218 17.25 6 17.25H14.25C14.6478 17.25 15.0294 17.092 15.3107 16.8107C15.592 16.5294 15.75 16.1478 15.75 15.75V5.25C15.75 4.85218 15.592 4.47064 15.3107 4.18934C15.0294 3.90804 14.6478 3.75 14.25 3.75ZM12 0.75H3C2.60218 0.75 2.22064 0.908035 1.93934 1.18934C1.65804 1.47064 1.5 1.85218 1.5 2.25V12.75H3V2.25H12V0.75Z" fill="#A2B1C6"/>
</svg>

Before

Width:  |  Height:  |  Size: 643 B

View File

@@ -1,3 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<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>

Before

Width:  |  Height:  |  Size: 735 B

View File

@@ -1,3 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.75 2.25V3H3V4.5H3.75V14.25C3.75 14.6478 3.90804 15.0294 4.18934 15.3107C4.47064 15.592 4.85218 15.75 5.25 15.75H12.75C13.1478 15.75 13.5294 15.592 13.8107 15.3107C14.092 15.0294 14.25 14.6478 14.25 14.25V4.5H15V3H11.25V2.25H6.75ZM5.25 4.5H12.75V14.25H5.25V4.5ZM6.75 6V12.75H8.25V6H6.75ZM9.75 6V12.75H11.25V6H9.75Z" fill="#A2B1C6"/>
</svg>

Before

Width:  |  Height:  |  Size: 447 B

View File

@@ -1,3 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 1.5H4.5C3.675 1.5 3 2.175 3 3V15C3 15.825 3.675 16.5 4.5 16.5H13.5C14.325 16.5 15 15.825 15 15V6L10.5 1.5ZM13.5 15H4.5V3H9.75V6.75H13.5V15ZM12 8.25V13.575L10.425 12L8.325 14.1L6.225 12L8.325 9.9L6.675 8.25H12Z" fill="#A2B1C6"/>
</svg>

Before

Width:  |  Height:  |  Size: 346 B

View File

@@ -1,3 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.545 6.75L11.25 7.455L4.44 14.25H3.75V13.56L10.545 6.75ZM13.245 2.25C13.0575 2.25 12.8625 2.325 12.72 2.4675L11.3475 3.84L14.16 6.6525L15.5325 5.28C15.825 4.9875 15.825 4.5 15.5325 4.2225L13.7775 2.4675C13.6275 2.3175 13.44 2.25 13.245 2.25ZM10.545 4.6425L2.25 12.9375V15.75H5.0625L13.3575 7.455L10.545 4.6425Z" fill="#A2B1C6"/>
</svg>

Before

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -1,63 +0,0 @@
button {
box-sizing: border-box;
height: 36px;
padding: 0 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
button:focus {
outline: none;
}
button.primary {
background: var(--color-accent);
border: 1px solid var(--color-accent-shade);
border-radius: var(--border-radius-big);
min-width: 83px;
color: var(--color-text-light);
text-shadow: var(--shadow);
}
button.primary:hover {
background: var(--color-accent-shade);
border: 1px solid var(--color-accent-shade);
color: var(--color-text-light);
text-shadow: var(--shadow);
}
button.primary:disabled {
background: var(--color-bg-light-2);
border: 1px solid var(--color-border);
color: var(--color-text-light-2);
text-shadow: none;
cursor: default;
}
button.secondary {
background: white;
border: 1px solid var(--color-border);
border-radius: var(--border-radius-big);
min-width: 83px;
color: var(--color-text-base);
}
button.secondary:hover {
border: 1px solid var(--color-text-light-2);
color: var(--color-text-active);
}
button.toolbar {
background: transparent;
border: none;
color: var(--color-text-base);
padding: 0;
}
button.toolbar:hover {
color: var(--color-accent);
}

View File

@@ -1,40 +0,0 @@
.dialog {
border-radius: var(--border-radius-big);
box-shadow: 0px 2px 9px rgba(80, 103, 132, 0.8);
}
.dialog-header {
height: 46px;
line-height: 46px;
padding: 0 22px 0 12px;
color: var(--color-text-base);
font-size: 16px;
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
}
.dialog-body {
min-height: 60px;
background-color: var(--color-bg-light);
padding: 24px;
border-top: 1px solid var(--color-border-light);
color: var(--color-text-base);
font-size: 13px;
}
.dialog-buttons-container {
display: flex;
justify-content: flex-end;
background-color: var(--color-bg-light);
padding: 24px;
}
.dialog-buttons-container button {
margin-left: 16px;
}
.vm--overlay {
background-color: rgba(162, 177, 198, 0.5);
}

View File

@@ -1,17 +0,0 @@
/* width */
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
/* Track */
::-webkit-scrollbar-track {
background: transparent;
border-radius: 5px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: var(--color-accent);
border-radius: 10px;
}

View File

@@ -1,78 +0,0 @@
.rounded-bg {
padding: 40px 5px 5px;
background-color: white;
border-radius: 5px;
position: relative;
border: 1px solid var(--color-border-light);
box-sizing: border-box;
}
.header-container {
overflow: hidden;
position: absolute;
top: -1px;;
left: -1px;
width: calc(100% + 2px);
padding-left: 7px;
box-sizing: border-box;
background-color: var(--color-bg-dark);
border-radius: 5px 5px 0 0;
}
.header-container > div {
display: flex;
width: fit-content;
padding-right: 10px;
}
.table-container {
width: 100%;
overflow: auto;
}
table {
min-width: 100%;
margin-top: -40px;
border-collapse: collapse;
}
thead th, .fixed-header {
font-size: 14px;
font-weight: 600;
box-sizing: border-box;
background-color: var(--color-bg-dark);
color: var(--color-text-light);
border-right: 1px solid var(--color-border-light);
}
tbody td {
font-size: 13px;
background-color:white;
color: var(--color-text-base);
box-sizing: border-box;
border-bottom: 1px solid var(--color-border-light);
border-right: 1px solid var(--color-border-light);
}
td, th, .fixed-header {
padding: 12px 24px;
white-space: nowrap;
}
tbody tr td:last-child,
thead tr th:last-child,
.header-container div .fixed-header:last-child {
border-right: none;
}
td > div.cell-data {
width: -webkit-max-content;
width: -moz-max-content;
width: max-content;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.table-footer {
display: flex;
justify-content: space-between;
padding: 6px 12px;
}
.table-footer-count {
font-size: 11px;
color: var(--color-text-base);
}

View File

@@ -1,40 +0,0 @@
: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-dark: #506784;
--color-blue-medium: #119DFF;
--color-blue-dark: #0D76BF;
--color-blue-dark-2: #2A3F5F;
--color-red: #EF553B;
--color-bg-light: var(--color-gray-light);
--color-bg-light-2: var(--color-gray-light-2);
--color-bg-light-3: var(--color-gray-light-5);
--color-bg-dark: var(--color-gray-dark);
--color-accent: var(--color-blue-medium);
--color-accent-shade: var(--color-blue-dark);
--color-border-light: var(--color-gray-light-2);
--color-border: var(--color-gray-light-3);
--color-text-light: var(--color-white);
--color-text-light-2: var(--color-gray-medium);
--color-text-base: var(--color-gray-dark);
--color-text-active: var(--color-blue-dark-2);
--color-text-error: var(--color-red);
--shadow: 0 1px 2px rgba(42, 63, 95, 0.7);
--border-radius-big: 5px;
--border-radius-medium: 3px;
--border-radius-medium-2: 4px;
--border-radius-small: 2px;
}

View File

@@ -1,103 +0,0 @@
<template>
<nav>
<div>
<router-link to="/editor">Editor</router-link>
<router-link to="/my-queries">My queries</router-link>
</div>
<div>
<button
v-if="$store.state.tabs.length > 0"
class="primary"
:disabled="!$store.state.currentTab.isUnsaved"
@click="saveQuery"
>
Save
</button>
<button class="primary" @click="createNewQuery">Create</button>
</div>
</nav>
</template>
<script>
import { nanoid } from 'nanoid'
export default {
name: 'MainMenu',
methods: {
createNewQuery () {
const tab = {
id: nanoid(),
name: null,
tempName: this.$store.state.untitledLastIndex
? `Untitled ${this.$store.state.untitledLastIndex}`
: 'Untitled',
isUnsaved: true
}
this.$store.commit('addTab', tab)
this.$store.commit('setCurrentTabId', tab.id)
this.$store.commit('updateUntitledLastIndex')
},
saveQuery () {
const currentQuery = this.$store.state.currentTab
const isFromScratch = !this.$store.state.currentTab.initName
const value = {
id: currentQuery.id,
query: currentQuery.query
// TODO: save plotly settings
}
if (isFromScratch) {
value.name = prompt('query name')
// TODO: create dialog
this.$store.commit('updateTabName', { index: currentQuery.tabIndex, newName: value.name })
value.createdAt = new Date()
} else {
value.name = currentQuery.initName
}
let myQueries = JSON.parse(localStorage.getItem('myQueries'))
if (!myQueries) {
myQueries = [value]
} else if (isFromScratch) {
myQueries.push(value)
} else {
const queryIndex = myQueries.findIndex(query => query.id === currentQuery.id)
value.createdAt = myQueries[queryIndex].createdAt
myQueries[queryIndex] = value
}
localStorage.setItem('myQueries', JSON.stringify(myQueries))
currentQuery.isUnsaved = false
}
}
}
</script>
<style scoped>
nav {
height: 68px;
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--color-bg-light);
border-bottom: 1px solid var(--color-border-light);
box-sizing: border-box;
position: fixed;
top: 0;
left: 0;
width: 100vw;
padding: 0 52px;
}
a {
font-size: 18px;
color: var(--color-text-base);
text-transform: none;
text-decoration: none;
margin-right: 28px;
}
a.router-link-active {
color: var(--color-accent);
}
button {
margin-left: 16px;
}
</style>

View File

@@ -1,97 +0,0 @@
<template>
<paginate
:page-count="pageCount"
:page-range="5"
:margin-pages="1"
:prev-text="chevron"
:next-text="chevron"
:no-li-surround="true"
container-class="paginator-continer"
page-link-class="paginator-page-link"
active-class="paginator-active-page"
break-view-link-class="paginator-break"
next-link-class="paginator-next"
prev-link-class="paginator-prev"
disabled-class="paginator-disabled"
v-model="page"
/>
</template>
<script>
import Paginate from 'vuejs-paginate'
export default {
name: 'Pager',
components: { Paginate },
props: ['pageCount', 'value'],
data () {
return {
page: this.value,
chevron: `
<svg width="9" height="9" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.721924 9.93097L4.85292 5.79997L0.721924 1.66897L1.99992 0.399973L7.39992 5.79997L1.99992 11.2L0.721924 9.93097Z" fill="#506784"/>
</svg>
`
}
},
watch: {
page () {
this.$emit('input', this.page)
},
value () {
this.page = this.value
}
}
}
</script>
<style scoped>
.paginator-continer {
display: flex;
align-items: center;
}
>>> .paginator-page-link {
padding: 2px 3px;
margin: 0 5px;
display: block;
color: var(--color-text-base);
font-size: 11px;
}
>>> .paginator-page-link:hover {
color: var(--color-text-active);
}
>>> .paginator-page-link:active,
>>> .paginator-page-link:visited,
>>> .paginator-page-link:focus,
>>> .paginator-next:active,
>>> .paginator-next:visited,
>>> .paginator-next:focus,
>>> .paginator-prev:active,
>>> .paginator-prev:visited,
>>> .paginator-prev:focus {
outline: none;
}
>>> .paginator-active-page,
>>> .paginator-active-page:hover {
color: var(--color-accent);
}
>>> .paginator-break:hover,
>>> .paginator-disabled:hover {
cursor: default;
}
>>> .paginator-prev svg {
transform: rotate(180deg);
}
>>> .paginator-next:hover path,
>>> .paginator-prev:hover path {
fill: var(--color-text-active);
}
>>> .paginator-disabled path,
>>> .paginator-disabled:hover path {
fill: var(--color-text-light-2);
}
</style>

View File

@@ -1,159 +0,0 @@
<template>
<div>
<text-field placeholder="Search table" width="100%"/>
<div id="db">
<div @click="schemaVisible = !schemaVisible" class="db-name">
<svg
:style="{transform: schemaVisible ? 'rotate(90deg)' : 'rotate(0)'}"
class="chevron-icon"
width="9"
height="9"
viewBox="0 0 8 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0.721924 9.93097L4.85292 5.79997L0.721924 1.66897L1.99992 0.399973L7.39992 5.79997L1.99992 11.2L0.721924 9.93097Z"
:fill="schemaVisible ? '#506784' : 'rgba(80, 103, 132, 0.5)'"
/>
</svg>
{{ dbName }}
</div>
<div class="db-edit">
<input type="file" id="actual-btn" ref="dbfile" hidden @change="changeDb"/>
<label for="actual-btn">
<svg
class="db-edit-icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<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>
</label>
<span class="db-edit-tooltip">Change database</span>
</div>
</div>
<div v-show="schemaVisible" class="schema">
<table-description
v-for="(table, index) in schema"
:key="index"
:name="table[0]"
:sql="table[1]"
/>
</div>
</div>
</template>
<script>
import TableDescription from '@/components/TableDescription'
import TextField from '@/components/TextField'
export default {
name: 'Schema',
components: { TableDescription, TextField },
data () {
return {
schemaVisible: true,
worker: this.$store.state.worker
}
},
computed: {
schema () {
return this.$store.state.schema
},
dbName () {
return this.$store.state.dbName
}
},
methods: {
changeDb () {
const dbName = this.$refs.dbfile.value.substr(this.$refs.dbfile.value.lastIndexOf('\\') + 1)
this.$store.commit('saveDbName', dbName)
const f = this.$refs.dbfile.files[0]
const r = new FileReader()
r.onload = () => {
this.worker.onmessage = () => {
const getSchemaSql = `
SELECT name, sql
FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%';`
this.worker.onmessage = event => {
this.$store.commit('saveSchema', event.data.results[0].values)
}
this.worker.postMessage({ action: 'exec', sql: getSchemaSql })
}
this.$store.commit('saveDbFile', r.result)
try {
this.worker.postMessage({ action: 'open', buffer: r.result }, [r.result])
} catch (exception) {
this.worker.postMessage({ action: 'open', buffer: r.result })
}
}
r.readAsArrayBuffer(f)
}
}
}
</script>
<style scoped>
.schema {
margin-left: 12px;
}
.schema, .db-name {
color: var(--color-text-base);
font-size: 13px;
white-space: nowrap;
}
#db {
display: flex;
align-items: center;
}
.db-name {
cursor: pointer;
}
>>> .chevron-icon {
-webkit-transition: transform .15s ease-in-out;
transition: transform .15s ease-in-out;
}
.db-name:hover .chevron-icon path,
>>> .table-name:hover .chevron-icon path {
fill: #506784;
}
.db-edit {
position: relative;
}
.db-edit-icon {
display: block;
}
.db-edit-icon:hover path{
fill: var(--color-accent);
cursor: pointer;
}
.db-edit-tooltip {
visibility: hidden;
background-color: rgba(80, 103, 132, 0.75);
color: #fff;
text-align: center;
font-size: 11px;
padding: 0 6px;
line-height: 19px;;
position: absolute;
z-index: 5;
height: 19px;
left: 24px;
top: -12px;
border-radius: var(--border-radius-medium);
white-space: nowrap;
}
.db-edit:hover .db-edit-tooltip {
visibility: visible;
}
</style>

View File

@@ -1,110 +0,0 @@
<template>
<div>
<div class="rounded-bg">
<div class="header-container" ref="header-container">
<div>
<div
v-for="(th, index) in header"
class="fixed-header"
:style="{ width: `${th.width}px` }"
:key="index"
>
{{ th.name }}
</div>
</div>
</div>
<div
class="table-container"
ref="table-container"
@scroll="onScrollTable"
:style="{height: `${height}px`}"
>
<table ref="table">
<thead>
<tr>
<th v-for="(th,index) in data.columns" :key="index" ref="th">
<div class="cell-data" :style="cellStyle">{{ th }}</div>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row,index) in currentPageData" :key="index">
<td v-for="(value, valIndex) in row" :key="valIndex">
<div class="cell-data" :style="cellStyle">{{ value }}</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="table-footer">
<div class="table-footer-count">
{{ data.values.length}} {{data.values.length === 1 ? 'row' : 'rows'}} retrieved
</div>
<pager v-show="pageCount > 1" :page-count="pageCount" v-model="currentPage" />
</div>
</div>
</template>
<script>
import Pager from '@/components/Pager'
export default {
name: 'SqlTable',
components: { Pager },
props: ['data', 'height'],
data () {
return {
header: null,
tableWidth: null,
currentPage: 1
}
},
computed: {
cellStyle () {
const eq = this.tableWidth / this.data.columns.length
return { maxWidth: `${Math.max(eq, 100)}px` }
},
pageSize () {
return Math.max(Math.floor(this.height / 40), 20)
},
pageCount () {
return Math.ceil(this.data.values.length / this.pageSize)
},
currentPageData () {
const start = (this.currentPage - 1) * this.pageSize
return this.data.values.slice(start, start + this.pageSize)
}
},
methods: {
calculateHeadersWidth () {
this.tableWidth = this.$refs['table-container'].offsetWidth
this.$nextTick(() => {
this.header = this.$refs.th.map(th => {
return { name: th.innerText, width: th.offsetWidth }
})
})
},
onScrollTable () {
this.$refs['header-container'].scrollLeft = this.$refs['table-container'].scrollLeft
},
functionName () {
}
},
mounted () {
new ResizeObserver(this.calculateHeadersWidth).observe(this.$refs.table)
this.calculateHeadersWidth()
},
watch: {
currentPageData: 'calculateHeadersWidth',
data () {
this.currentPage = 1
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,250 +0,0 @@
<template>
<div class="tab-content-container" v-show="isActive">
<splitpanes
class="query-results-splitter"
horizontal
:before="{ size: 50, max: 50 }"
:after="{ size: 50, max: 100 }"
>
<div slot="left-pane" class="query-editor">
<div class="codemirror-container">
<codemirror v-model="query" :options="cmOptions" @changes="onCmChange" ref="codemirror" />
</div>
<div class="run-btn-container">
<button class="primary run-btn" @click="execEditorContents">Run</button>
</div>
</div>
<div slot="right-pane" id="bottomPane" ref="bottomPane">
<view-switcher :view.sync="view" />
<div v-show="view === 'table'" class="table-view">
<!-- <div id="error" class="error"></div>
<pre ref="output" id="output">Results will be displayed here</pre> -->
<sql-table v-if="result" :data="result" :height="tableViewHeight" />
</div>
<PlotlyEditor
v-show="view === 'chart'"
:data="state.data"
:layout="state.layout"
:frames="state.frames"
:config="{ editable: true }"
:dataSources="dataSources"
:dataSourceOptions="dataSourceOptions"
:plotly="plotly"
@onUpdate="update"
:useResizeHandler="true"
:debug="true"
:advancedTraceTypeSelector="true"
/>
</div>
</splitpanes>
</div>
</template>
<script>
import SqlTable from '@/components/SqlTable'
import Splitpanes from '@/components/splitpanes'
import ViewSwitcher from '@/components/ViewSwitcher'
import plotly from 'plotly.js/dist/plotly'
import 'react-chart-editor/lib/react-chart-editor.min.css'
import CM from 'codemirror'
import { codemirror } from 'vue-codemirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/mode/sql/sql.js'
import 'codemirror/theme/neo.css'
import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/hint/show-hint.css'
import 'codemirror/addon/hint/sql-hint.js'
import PlotlyEditor from 'react-chart-editor'
export default {
name: 'TabContent',
props: ['id', 'initName', 'initQuery', 'initPlotly', 'tabIndex'],
components: {
codemirror,
SqlTable,
Splitpanes,
ViewSwitcher,
PlotlyEditor
},
data () {
return {
plotly: plotly,
state: {
data: [],
layout: {},
frames: []
},
query: 'select * from albums',
cmOptions: {
// codemirror options
tabSize: 4,
mode: 'text/x-mysql',
theme: 'neo',
lineNumbers: true,
line: true
},
result: null,
view: 'table',
tableViewHeight: 0,
worker: this.$store.state.worker,
isUnsaved: !this.name
}
},
computed: {
isActive () {
return this.id === this.$store.state.currentTabId
},
dataSources () {
if (!this.result) {
return {}
}
const dataSorces = {}
const matrix = this.result.values
const [row] = matrix
const transposedMatrix = row.map((value, column) => matrix.map(row => row[column]))
this.result.columns.forEach((column, index) => {
dataSorces[column] = transposedMatrix[index]
})
return dataSorces
},
dataSourceOptions () {
return Object.keys(this.dataSources).map(name => ({
value: name,
label: name
}))
}
},
created () {
this.$store.commit('setCurrentTab', this)
},
mounted () {
new ResizeObserver(this.calculateTableHeight).observe(this.$refs.bottomPane)
this.calculateTableHeight()
},
watch: {
isActive () {
if (this.isActive) {
this.$store.commit('setCurrentTab', this)
}
},
query () {
this.isUnsaved = true
},
isUnsaved () {
this.$store.commit('updateTabState', { index: this.tabIndex, newValue: this.isUnsaved })
}
},
methods: {
update (data, layout, frames) {
this.state = { data, layout, frames }
this.isUnsaved = true
console.log(this.state)
},
onCmChange (editor) {
// Don't show autocomplete after a space or semicolon
const ch = editor.getTokenAt(editor.getCursor()).string.slice(-1)
if (!ch || ch === ' ' || ch === ';') {
return
}
const hintOptions = {
// tables: this.state.tables,
completeSingle: false,
completeOnSingleClick: true
}
// editor.hint.sql is defined when importing codemirror/addon/hint/sql-hint
// (this is mentioned in codemirror addon documentation)
// Reference the hint function imported here when including other hint addons
// or supply your own
CM.showHint(editor, CM.hint.sql, hintOptions)
},
// Run a command in the database
execute (commands) {
this.worker.onmessage = (event) => {
// if it was more than one select - take only the first one
this.result = event.data.results[0]
if (!this.result) {
console.log(event.data.error)
// return
}
// this.$refs.output.innerHTML = ''
}
this.worker.postMessage({ action: 'exec', sql: commands })
// this.$refs.output.textContent = 'Fetching results...'
},
execEditorContents () {
this.execute(this.query + ';')
},
calculateTableHeight () {
const bottomPane = this.$refs.bottomPane
// 88 - view swittcher height
// 42 - table footer width
// 30 - desirable space after the table
// 5 - padding-bottom of rounded table container
// 40 - height of table header
const freeSpace = bottomPane.offsetHeight - 88 - 42 - 30 - 5 - 40
this.tableViewHeight = freeSpace - (freeSpace % 40)
}
}
}
</script>
<style scoped>
.tab-content-container {
padding-top: 6px;
background-color: var(--color-bg-light);
border-top: 1px solid var(--color-border-light);
margin-top: -1px;
}
#bottomPane {
height: 100%;
}
.query-results-splitter {
height: calc(100vh - 110px);
background-color: var(--color-bg-light);
}
.run-btn {
margin-top: 24px;
}
.query-editor {
padding: 52px 52px 24px;
display: flex;
flex-direction: column;
height: 100%;
max-height: 100%;
box-sizing: border-box;
min-height: 150px;
}
.codemirror-container {
flex-grow: 1;
min-height: 0;
}
.run-btn-container {
text-align: right;
}
>>> .vue-codemirror {
height: 100%;
max-height: 100%;
}
>>> .CodeMirror {
border: 1px solid var(--color-border);
border-radius: var(--border-radius-big);
height: 100%;
max-height: 100%;
}
.table-view {
margin: 0 52px;
}
</style>

View File

@@ -1,84 +0,0 @@
<template>
<div>
<div @click="colVisible = !colVisible" class="table-name">
<svg
:style="{transform: colVisible ? 'rotate(90deg)' : 'rotate(0)'}"
class="chevron-icon"
width="9"
height="9"
viewBox="0 0 8 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0.721924 9.93097L4.85292 5.79997L0.721924 1.66897L1.99992 0.399973L7.39992 5.79997L1.99992 11.2L0.721924 9.93097Z"
:fill="colVisible ? '#506784' : 'rgba(80, 103, 132, 0.5)'"
/>
</svg>
{{ name }}
</div>
<div v-show="colVisible" class="columns">
<div v-for="(col, index) in columns" :key="index" class="column">
{{ col.name }}
<span class="column-type">{{ col.type }}</span>
</div>
</div>
</div>
</template>
<script>
import sqliteParser from 'sqlite-parser'
export default {
name: 'TableDescription',
props: ['name', 'sql'],
data () {
return {
colVisible: false
}
},
computed: {
ast () {
return sqliteParser(this.sql)
},
columns () {
const columns = []
this.ast.statement[0].definition.forEach(item => {
if (item.variant === 'column') {
let type = item.datatype.variant
if (item.datatype.args) {
type = type + '(' + item.datatype.args.expression[0].value
if (item.datatype.args.expression.length === 2) {
type = type + ', ' + item.datatype.args.expression[1].value
}
type = type + ')'
}
columns.push({ name: item.name, type: type })
}
})
return columns
}
}
}
</script>
<style scoped>
.table-name, .column {
margin-top: 11px;
}
.table-name:hover {
cursor: pointer;
}
.columns {
margin-left: 24px;
}
.column-type {
display: inline-block;
background-color: var(--color-gray-light-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-small);
padding: 2px 6px;
font-size: 11px;
text-transform: uppercase;
}
</style>

View File

@@ -1,132 +0,0 @@
<template>
<div>
<div id="tabs__header">
<div
v-for="(tab, index) in tabs"
:key="tab.id"
@click="selectTab(tab.id)"
:class="[{'tab__selected': (tab.id === selectedIndex)}, 'tab']"
>
<div class="tab-name">
<span v-show="tab.isUnsaved">*</span>
<span v-if="tab.name">{{ tab.name }}</span>
<span v-else class="tab-untitled">{{ tab.tempName }}</span>
</div>
<div>
<svg
class="close-icon"
@click.stop="closeTab(index)"
width="10"
height="10"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14 1.41L12.59 0L7 5.59L1.41 0L0 1.41L5.59 7L0 12.59L1.41 14L7 8.41L12.59 14L14 12.59L8.41 7L14 1.41Z"
fill="#A2B1C6"/>
</svg>
</div>
</div>
</div>
<tab-content
v-for="(tab, index) in tabs"
:key="tab.id"
:id="tab.id"
:init-name="tab.name"
:tab-index="index"
/>
</div>
</template>
<script>
import TabContent from '@/components/TabContent'
export default {
components: {
TabContent
},
data () {
return {
}
},
computed: {
tabs () {
return this.$store.state.tabs
},
selectedIndex () {
return this.$store.state.currentTabId
}
},
methods: {
selectTab (id) {
this.$store.commit('setCurrentTabId', id)
},
closeTab (index) {
this.$store.commit('deleteTab', index)
}
}
}
</script>
<style>
#tabs__header {
display: flex;
margin: 0;
max-width: 100%;
overflow: hidden;
}
#tabs__header .tab {
height: 36px;
background-color: var(--color-bg-light);
border-right: 1px solid var(--color-border-light);
border-bottom: 1px solid var(--color-border-light);
line-height: 36px;
font-size: 14px;
color: var(--color-text-base);
padding: 0 12px;
box-sizing: border-box;
position: relative;
max-width: 200px;
display: flex;
flex-shrink: 1;
min-width: 0;
}
#tabs__header .tab-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 1;
}
#tabs__header div:hover {
cursor: pointer;
}
#tabs__header .tab__selected {
color: var(--color-text-active);
font-weight: 600;
border-bottom: none;
}
#tabs__header .tab__selected:hover {
cursor: default;
}
#tabs__header .tab__selected:before {
content: '';
position: absolute;
width: 100%;
height: 5px;
background-color: var(--color-accent);
top: 0;
left: 0;
}
.close-icon {
margin-left: 5px;
}
.close-icon:hover path {
fill: var(--color-text-base);
cursor: pointer;
}
</style>

View File

@@ -1,63 +0,0 @@
<template>
<div>
<div :class="['text-field-label', { error: errorMsg }]">{{ label }}</div>
<input
type="text"
:placeholder="placeholder"
:class="{ error: errorMsg }"
:style="{ width: width }"
:value="value"
@input="$emit('input', $event.target.value)"
/>
<div class="text-field-error">{{ errorMsg }}</div>
</div>
</template>
<script>
export default {
name: 'textField',
props: ['placeholder', 'label', 'errorMsg', 'value', 'width']
}
</script>
<style scoped>
input {
background: var(--color-white);
border: 1px solid var(--color-border);
color: var(--color-text-base);
border-radius: var(--border-radius-medium-2);
height: 36px;
padding: 0 8px;
font-size: 13px;
box-sizing: border-box;
}
input::placeholder {
color: var(--color-text-light-2);
}
input:focus {
outline: none;
}
input.error {
border-color: var(--color-text-error);
}
.text-field-label {
font-size: 12px;
color: var(--color-text-base);
padding-left: 8px;
margin-bottom: 2px;
}
.text-field-label.error {
color: var(--color-text-error);
}
.text-field-error {
color: var(--color-text-error);
font-size: 12px;
padding-left: 8px;
margin-top: 2px;
}
</style>

View File

@@ -1,67 +0,0 @@
<template>
<div class="view-switcher">
<div
:class="['table-mode', {'active-mode': view === 'table'}]"
@click="$emit('update:view','table')"
>
Table
</div>
<div
:class="['chart-mode', {'active-mode': view === 'chart'}]"
@click="$emit('update:view','chart')"
>
Chart
</div>
</div>
</template>
<script>
export default {
name: 'ViewSwitcher',
props: ['view']
}
</script>
<style scoped>
.view-switcher {
height: 28px;
display: flex;
padding: 30px;
justify-content: center;
}
.view-switcher div {
height: 100%;
width: 136px;
box-sizing: border-box;
line-height: 28px;
font-size: 12px;
cursor: pointer;
background: var(--color-white);
border: 1px solid var(--color-border);
color: var(--color-text-base);
text-align: center;
font-weight: 400;
}
.view-switcher div:hover {
background-color: var(--color-bg-light);
color: var(--color-text-active);
}
.view-switcher div.active-mode {
background: var(--color-accent);
border: 1px solid var(--color-accent-shade);
color: var(--color-text-light);
text-shadow: var(--shadow);
z-index: 1;
font-weight: 600;
}
.view-switcher div.active-mode:hover {
background: var(--color-accent-shade);
}
.table-mode {
border-radius: var(--border-radius-medium) 0 0 var(--border-radius-medium);
}
.chart-mode {
margin-left: -1px;
border-radius: 0 var(--border-radius-medium) var(--border-radius-medium) 0;
}
</style>

View File

@@ -1,186 +0,0 @@
<template>
<div
ref="container"
:class="['splitpanes', `splitpanes--${horizontal ? 'horizontal' : 'vertical'}`, { 'splitpanes--dragging': touch.dragging }]"
>
<div
class="splitpanes__pane"
ref="left"
:size="paneBefore.size"
max-size="30"
:style="styles[0]"
>
<slot name="left-pane" />
</div>
<splitter
@mousedown="onMouseDown"
@toggle="toggleFirstPane"
:expanded="paneBefore.size !== 0"
/>
<div
class="splitpanes__pane"
ref="right"
:style="styles[1]"
>
<slot name="right-pane" />
</div>
</div>
</template>
<script>
import Splitter from '@/components/splitter'
export default {
name: 'splitpanes',
components: { Splitter },
props: {
horizontal: { type: Boolean, default: false },
before: { type: Object },
after: { type: Object }
},
data: () => ({
container: null,
paneBefore: null,
paneAfter: null,
beforeMinimising: 20,
touch: {
mouseDown: false,
dragging: false
}
}),
computed: {
styles () {
return [
{ [this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%` },
{ [this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%` }
]
}
},
methods: {
bindEvents () {
document.addEventListener('mousemove', this.onMouseMove, { passive: false })
document.addEventListener('mouseup', this.onMouseUp)
// Passive: false to prevent scrolling while touch dragging.
if ('ontouchstart' in window) {
document.addEventListener('touchmove', this.onMouseMove, { passive: false })
document.addEventListener('touchend', this.onMouseUp)
}
},
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('touchend', this.onMouseUp)
}
},
onMouseDown () {
this.bindEvents()
this.touch.mouseDown = true
},
onMouseMove (event) {
if (this.touch.mouseDown) {
// Prevent scrolling while touch dragging (only works with an active event, eg. passive: false).
event.preventDefault()
this.touch.dragging = true
this.calculatePanesSize(this.getCurrentMouseDrag(event))
}
},
onMouseUp () {
this.touch.mouseDown = false
// Keep dragging flag until click event is finished (click happens immediately after mouseup)
// in order to prevent emitting `splitter-click` event if splitter was dragged.
setTimeout(() => {
this.touch.dragging = false
this.unbindEvents()
}, 100)
},
// Get the cursor position relative to the splitpane container.
getCurrentMouseDrag (event) {
const rect = this.container.getBoundingClientRect()
const { clientX, clientY } = ('ontouchstart' in window && event.touches) ? event.touches[0] : event
return {
x: clientX - rect.left,
y: clientY - rect.top
}
},
// Returns the drag percentage of the splitter relative to the 2 panes it's inbetween.
// if the sum of size of the 2 cells is 60%, the dragPercentage range will be 0 to 100% of this 60%.
getCurrentDragPercentage (drag) {
drag = drag[this.horizontal ? 'y' : 'x']
// In the code bellow 'size' refers to 'width' for vertical and 'height' for horizontal layout.
const containerSize = this.container[this.horizontal ? 'clientHeight' : 'clientWidth']
return drag * 100 / containerSize
},
calculatePanesSize (drag) {
const dragPercentage = this.getCurrentDragPercentage(drag)
// If not pushing other panes, panes to resize are right before and right after splitter.
const paneBefore = this.paneBefore
const paneAfter = this.paneAfter
const paneBeforeMaxReached = paneBefore.max < 100 && (dragPercentage >= paneBefore.max)
const paneAfterMaxReached = paneAfter.max < 100 && (dragPercentage <= 100 - paneAfter.max)
// Prevent dragging beyond pane max.
if (paneBeforeMaxReached || paneAfterMaxReached) {
if (paneBeforeMaxReached) {
paneBefore.size = paneBefore.max
paneAfter.size = Math.max(100 - paneBefore.max, 0)
} else {
paneBefore.size = Math.max(100 - paneAfter.max, 0)
paneAfter.size = paneAfter.max
}
return
}
paneBefore.size = Math.min(Math.max(dragPercentage, 0), paneBefore.max)
paneAfter.size = Math.min(Math.max(100 - dragPercentage, 0), paneAfter.max)
},
toggleFirstPane () {
if (this.paneBefore.size > 0) {
this.beforeMinimising = this.paneBefore.size
this.paneBefore.size = 0
} else {
this.paneBefore.size = this.beforeMinimising
}
this.paneAfter.size = 100 - this.paneBefore.size
}
},
mounted () {
this.container = this.$refs.container
},
created () {
this.paneBefore = this.before
this.paneAfter = this.after
}
}
</script>
<style>
.splitpanes {
display: flex;
height: 100%;
}
.splitpanes--vertical {flex-direction: row;}
.splitpanes--horizontal {flex-direction: column;}
.splitpanes--dragging * {user-select: none;}
.splitpanes__pane {
width: 100%;
height: 100%;
overflow: auto;
}
</style>

View File

@@ -1,85 +0,0 @@
<template>
<div
class="splitpanes__splitter"
@mousedown="$emit('mousedown')"
@touchstart="$emit('mousedown')"
>
<div class="toggle-btn" @click="$emit('toggle')">
<img
class="direction-icon"
:src="require('@/assets/images/chevron.svg')"
:style="directionIconStyle"
>
</div>
</div>
</template>
<script>
export default {
name: 'splitter',
props: ['expanded'],
computed: {
directionIconStyle () {
const translation = 'translate(-50%, -50%)'
if (this.$parent.horizontal) {
return {
transform: `${translation} ${this.expanded ? 'rotate(-90deg)' : 'rotate(90deg)'}`
}
} else {
return {
transform: `${translation} ${this.expanded ? 'rotate(180deg)' : ''}`
}
}
}
}
}
</script>
<style>
.splitpanes--vertical > .splitpanes__splitter {min-width: 1px;cursor: col-resize;}
.splitpanes--horizontal > .splitpanes__splitter {min-height: 1px; cursor: row-resize;}
.splitpanes__splitter {
touch-action: none;
background-color: var(--color-bg-light-2);
box-sizing: border-box;
position: relative;
flex-shrink: 0;
z-index: 1;
}
.splitpanes--vertical > .splitpanes__splitter {
width: 3px;
z-index: 3;
}
.splitpanes--horizontal > .splitpanes__splitter {
height: 3px;
width: 100%;
}
.splitpanes__splitter .toggle-btn {
background-color: var(--color-bg-light-2);
border-radius: var(--border-radius-small);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.splitpanes__splitter .toggle-btn:hover {
cursor: pointer;
}
.splitpanes--vertical .toggle-btn {
height: 68px;
width: 15px;
}
.splitpanes--horizontal .toggle-btn {
width: 68px;
height: 15px;
}
.splitpanes__splitter .toggle-btn .direction-icon {
position: absolute;
top: 50%;
left: 50%;
}
</style>

View File

@@ -1,20 +0,0 @@
<template>
<svg @click.stop="$emit('click')" class="icon" width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 1.41L12.59 0L7 5.59L1.41 0L0 1.41L5.59 7L0 12.59L1.41 14L7 8.41L12.59 14L14 12.59L8.41 7L14 1.41Z" fill="#A2B1C6"/>
</svg>
</template>
<script>
export default {
name: 'CloseIcon'
}
</script>
<style scoped>
.icon {
cursor: pointer;
}
.icon:hover path {
fill: var(--color-text-active);
}
</style>

View File

@@ -1,21 +0,0 @@
<template>
<svg @click.stop="$emit('click')" class="icon" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.25 15.75H6V5.25H14.25V15.75ZM14.25 3.75H6C5.60218 3.75 5.22064 3.90804 4.93934 4.18934C4.65804 4.47064 4.5 4.85218 4.5 5.25V15.75C4.5 16.1478 4.65804 16.5294 4.93934 16.8107C5.22064 17.092 5.60218 17.25 6 17.25H14.25C14.6478 17.25 15.0294 17.092 15.3107 16.8107C15.592 16.5294 15.75 16.1478 15.75 15.75V5.25C15.75 4.85218 15.592 4.47064 15.3107 4.18934C15.0294 3.90804 14.6478 3.75 14.25 3.75ZM12 0.75H3C2.60218 0.75 2.22064 0.908035 1.93934 1.18934C1.65804 1.47064 1.5 1.85218 1.5 2.25V12.75H3V2.25H12V0.75Z" fill="#A2B1C6"/>
</svg>
</template>
<script>
export default {
name: 'CopyIcon'
}
</script>
<style scoped>
.icon {
vertical-align: middle;
margin: 0 12px;
}
.icon:hover path {
fill: var(--color-accent);
}
</style>

View File

@@ -1,22 +0,0 @@
<template>
<svg @click.stop="$emit('click')" class="icon" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.75 2.25V3H3V4.5H3.75V14.25C3.75 14.6478 3.90804 15.0294 4.18934 15.3107C4.47064 15.592 4.85218 15.75 5.25 15.75H12.75C13.1478 15.75 13.5294 15.592 13.8107 15.3107C14.092 15.0294 14.25 14.6478 14.25 14.25V4.5H15V3H11.25V2.25H6.75ZM5.25 4.5H12.75V14.25H5.25V4.5ZM6.75 6V12.75H8.25V6H6.75ZM9.75 6V12.75H11.25V6H9.75Z" fill="#A2B1C6"/>
</svg>
</template>
<script>
export default {
name: 'DeleteIcon'
}
</script>
<style scoped>
.icon {
vertical-align: middle;
margin: 0 0 0 12px;
}
.icon:hover path {
fill: var(--color-accent);
}
</style>

View File

@@ -1,22 +0,0 @@
<template>
<svg @click.stop="$emit('click')" class="icon" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 1.5H4.5C3.675 1.5 3 2.175 3 3V15C3 15.825 3.675 16.5 4.5 16.5H13.5C14.325 16.5 15 15.825 15 15V6L10.5 1.5ZM13.5 15H4.5V3H9.75V6.75H13.5V15ZM12 8.25V13.575L10.425 12L8.325 14.1L6.225 12L8.325 9.9L6.675 8.25H12Z" fill="#A2B1C6"/>
</svg>
</template>
<script>
export default {
name: 'ExportIcon'
}
</script>
<style scoped>
.icon {
vertical-align: middle;
margin: 0 12px;
}
.icon:hover path {
fill: var(--color-accent);
}
</style>

View File

@@ -1,22 +0,0 @@
<template>
<svg @click.stop="$emit('click')" class="icon" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.545 6.75L11.25 7.455L4.44 14.25H3.75V13.56L10.545 6.75ZM13.245 2.25C13.0575 2.25 12.8625 2.325 12.72 2.4675L11.3475 3.84L14.16 6.6525L15.5325 5.28C15.825 4.9875 15.825 4.5 15.5325 4.2225L13.7775 2.4675C13.6275 2.3175 13.44 2.25 13.245 2.25ZM10.545 4.6425L2.25 12.9375V15.75H5.0625L13.3575 7.455L10.545 4.6425Z" fill="#A2B1C6"/>
</svg>
</template>
<script>
export default {
name: 'RenameIcon'
}
</script>
<style scoped>
.icon {
vertical-align: middle;
margin: 0 12px 0 6px;
}
.icon:hover path {
fill: var(--color-accent);
}
</style>

View File

@@ -1,22 +0,0 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import { VuePlugin } from 'vuera'
import VModal from 'vue-js-modal'
import '@/assets/styles/variables.css'
import '@/assets/styles/buttons.css'
import '@/assets/styles/tables.css'
import '@/assets/styles/dialogs.css'
Vue.use(VuePlugin)
Vue.use(VModal)
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')

View File

@@ -1,39 +0,0 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Editor from '../views/Editor'
import MyQueries from '../views/MyQueries'
import DbUpload from '../views/DbUpload'
import MainView from '../views/MainView'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Welcome',
component: DbUpload
},
{
path: '/',
name: 'MainView',
component: MainView,
children: [
{
path: '/editor',
name: 'Editor',
component: Editor
},
{
path: '/my-queries',
name: 'MyQueries',
component: MyQueries
}
]
}
]
const router = new VueRouter({
routes
})
export default router

View File

@@ -1,67 +0,0 @@
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
schema: null,
dbFile: null,
dbName: null,
worker: new Worker('js/worker.sql-wasm.js'),
tabs: [],
currentTab: null,
currentTabId: null,
untitledLastIndex: 0
},
mutations: {
saveSchema (state, schema) {
state.schema = schema
},
saveDbFile (state, file) {
state.dbFile = file
},
saveDbName (state, name) {
state.dbName = name
},
addTab (state, tab) {
state.tabs.push(tab)
},
updateTabName (state, { index, newName }) {
const tab = state.tabs[index]
tab.name = newName
Vue.set(state.tabs, index, tab)
},
updateTabState (state, { index, newValue }) {
console.log(index, newValue)
const tab = state.tabs[index]
tab.isUnsaved = newValue
Vue.set(state.tabs, index, tab)
},
deleteTab (state, index) {
if (state.tabs[index].id !== state.currentTabId) {
} else if (index < state.tabs.length - 1) {
state.currentTabId = state.tabs[index + 1].id
} else if (index > 0) {
state.currentTabId = state.tabs[index - 1].id
} else {
state.currentTabId = null
state.untitledLastIndex = 0
}
state.tabs.splice(index, 1)
},
setCurrentTabId (state, id) {
state.currentTabId = id
},
setCurrentTab (state, tab) {
state.currentTab = tab
},
updateUntitledLastIndex (state) {
state.untitledLastIndex += 1
}
},
actions: {
},
modules: {
}
})

View File

@@ -1,114 +0,0 @@
<template>
<div id="dbloader-container">
<h1>Sqliteviz</h1>
<label for="assetsFieldHandle">
<div id="drop-area" @dragover="dragover" @dragleave="dragleave" @drop="drop">
<input
type="file"
id="assetsFieldHandle"
@change="loadDb"
ref="file"
accept=".db"
/>
<div>
Drop the database file to upload here or click to choose a file from your computer.
</div>
</div>
</label>
<div id="error" class="error"></div>
</div>
</template>
<script>
export default {
name: 'DbUpload',
data () {
return {
worker: this.$store.state.worker
}
},
methods: {
loadDb () {
const dbName = this.$refs.file.value.substr(this.$refs.file.value.lastIndexOf('\\') + 1)
this.$store.commit('saveDbName', dbName)
const f = this.$refs.file.files[0]
const r = new FileReader()
r.onload = () => {
this.worker.onmessage = () => {
const getSchemaSql = `
SELECT name, sql
FROM sqlite_master
WHERE type='table' AND name NOT LIKE 'sqlite_%';`
this.worker.onmessage = event => {
this.$store.commit('saveSchema', event.data.results[0].values)
this.$router.push('/editor')
}
this.worker.postMessage({ action: 'exec', sql: getSchemaSql })
}
this.$store.commit('saveDbFile', r.result)
try {
this.worker.postMessage({ action: 'open', buffer: r.result }, [r.result])
} catch (exception) {
this.worker.postMessage({ action: 'open', buffer: r.result })
}
}
r.readAsArrayBuffer(f)
},
dragover (event) {
event.preventDefault()
// TODO: Add some visual fluff to show the user can drop its files
if (!event.currentTarget.classList.contains('bg-green-300')) {
event.currentTarget.classList.remove('bg-gray-100')
event.currentTarget.classList.add('bg-green-300')
}
},
dragleave (event) {
// Clean up
event.currentTarget.classList.add('bg-gray-100')
event.currentTarget.classList.remove('bg-green-300')
},
drop (event) {
event.preventDefault()
this.$refs.file.files = event.dataTransfer.files
this.loadDb()
// Clean up
event.currentTarget.classList.add('bg-gray-100')
event.currentTarget.classList.remove('bg-green-300')
}
}
}
</script>
<style scoped>
#dbloader-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
h1 {
color: var(--color-accent);
}
label {
display: inline-block;
border: 1px dashed var(--color-border);
padding: 8px;
border-radius: var(--border-radius-big);
}
#drop-area {
width: 231px;
height: 153px;
background-color: var(--color-bg-light-3);
border-radius: var(--border-radius-big);
color: var(--color-text-base);
font-size: 13px;
padding: 44px 15px;
text-align: center;
box-sizing: border-box;
}
input {
display: none;
}
</style>

View File

@@ -1,38 +0,0 @@
<template>
<div>
<splitpanes
class="schema-tabs-splitter"
:before="{ size: 20, max: 30 }"
:after="{ size: 80, max: 100 }"
>
<div slot="left-pane">
<schema />
</div>
<div slot="right-pane">
<tabs />
</div>
</splitpanes>
</div>
</template>
<script>
import Splitpanes from '@/components/splitpanes'
import Schema from '@/components/Schema'
import Tabs from '@/components/Tabs'
export default {
name: 'Editor',
components: {
Schema,
Splitpanes,
Tabs
}
}
</script>
<style>
.schema-tabs-splitter {
height: 100%;
margin-left: 6px;
}
</style>

View File

@@ -1,25 +0,0 @@
<template>
<div>
<main-menu />
<keep-alive include="Editor">
<router-view id="main-view" />
</keep-alive>
</div>
</template>
<script>
import MainMenu from '@/components/MainMenu'
import '@/assets/styles/scrollbars.css'
export default {
name: 'MainView',
components: { MainMenu }
}
</script>
<style scoped>
#main-view {
margin-top: 68px;
height: calc(100vh - 68px);
overflow-y: auto;
}
</style>

View File

@@ -1,342 +0,0 @@
<template>
<div>
<div id="my-queries-content">
<div id="my-queries-toolbar">
<div id="toolbar-buttons">
<input
ref="importFile"
type="file"
accept=".json"
id="import-file"
@change="importQueries"
/>
<button class="toolbar">
<label for="import-file">
Import
</label>
</button>
<button class="toolbar" v-show="selectedQueries.length > 0">Export</button>
<button class="toolbar" v-show="selectedQueries.length > 0">Delete</button>
</div>
<div id="toolbar-search">
<text-field placeholder="Search query by name" width="300px"/>
</div>
</div>
<div class="rounded-bg">
<div class="header-container">
<div>
<div class="fixed-header" ref="name-th">
Name
</div>
<div class="fixed-header">
Created at
</div>
</div>
</div>
<div
class="table-container"
ref="table-container"
>
<table ref="table">
<tbody>
<tr v-for="(query, index) in queries" :key="query.id" @click="openQuery(index)">
<td ref="name-td">
{{ query.name }}
</td>
<td>
<div class="second-column">
<div class="date-container">{{ query.createdAt | date }}</div>
<div class="icons-container">
<rename-icon @click="showRenameDialog(index)" />
<copy-icon @click="duplicateQuery(index)"/>
<export-icon @click="exportQuery(index)"/>
<delete-icon @click="showDeleteDialog(index)"/>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!--Rename Query dialog -->
<modal name="rename" classes="dialog" height="auto">
<div class="dialog-header">
Rename query
<close-icon @click="$modal.hide('rename')"/>
</div>
<div class="dialog-body">
<text-field
label="New query 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="renameQuery">Rename</button>
</div>
</modal>
<!--Delete Query dialog -->
<modal name="delete" classes="dialog" height="auto">
<div class="dialog-header">
Delete query
<close-icon @click="$modal.hide('delete')"/>
</div>
<div
v-if="
currentQueryIndex !== null
&& currentQueryIndex >= 0
&& currentQueryIndex < queries.length
"
class="dialog-body"
>
Are you sure you want to delete
"{{ queries[currentQueryIndex].name }}"?
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="$modal.hide('delete')">Cancel</button>
<button class="primary" @click="deleteQuery">Delete</button>
</div>
</modal>
<a ref="downloader" />
</div>
</template>
<script>
import RenameIcon from '@/components/svg/rename'
import CopyIcon from '@/components/svg/copy'
import ExportIcon from '@/components/svg/export'
import DeleteIcon from '@/components/svg/delete'
import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField'
import { nanoid } from 'nanoid'
export default {
name: 'MyQueries',
components: {
RenameIcon,
CopyIcon,
ExportIcon,
DeleteIcon,
CloseIcon,
TextField
},
data () {
return {
queries: [],
newName: null,
currentQueryIndex: null,
errorMsg: null,
selectedQueries: []
}
},
created () {
this.queries = JSON.parse(localStorage.getItem('myQueries'))
},
mounted () {
new ResizeObserver(this.calcNameWidth).observe(this.$refs.table)
this.calcNameWidth()
},
filters: {
date (value) {
if (!value) {
return ''
}
const dateOptions = { year: 'numeric', month: 'long', day: 'numeric' }
const timeOptions = {
hour12: false,
hour: '2-digit',
minute: '2-digit'
}
return new Date(value).toLocaleDateString('en-GB', dateOptions) + ' ' +
new Date(value).toLocaleTimeString('en-GB', timeOptions)
}
},
methods: {
calcNameWidth () {
this.$refs['name-th'].style = `width: ${this.$refs['name-td'][0].offsetWidth}px`
},
openQuery (index) {
const tab = this.queries[index]
tab.isUnsaved = false
this.$store.commit('addTab', tab)
this.$store.commit('setCurrentTabId', tab.id)
this.$router.push('/editor')
},
showRenameDialog (index) {
this.errorMsg = null
this.currentQueryIndex = index
this.newName = this.queries[this.currentQueryIndex].name
this.$modal.show('rename')
},
renameQuery () {
if (!this.newName) {
this.errorMsg = 'Query name can\'t be empty'
return
}
const currentQuery = this.queries[this.currentQueryIndex]
currentQuery.name = this.newName
this.$set(this.queries, this.currentQueryIndex, currentQuery)
this.$modal.hide('rename')
this.saveQueriesInLocalStorage()
const tabIndex = this.findTabIndex(currentQuery.id)
if (tabIndex >= 0) {
this.$store.commit('updateTabName', { index: tabIndex, newName: this.newName })
}
},
duplicateQuery (index) {
const newQuery = JSON.parse(JSON.stringify(this.queries[index]))
newQuery.name = newQuery.name + ' Copy'
newQuery.id = nanoid()
newQuery.createdAt = new Date()
this.queries.push(newQuery)
this.saveQueriesInLocalStorage()
},
showDeleteDialog (index) {
this.currentQueryIndex = index
this.$modal.show('delete')
},
deleteQuery () {
this.$modal.hide('delete')
const id = this.queries[this.currentQueryIndex].id
this.queries.splice(this.currentQueryIndex, 1)
this.saveQueriesInLocalStorage()
const tabIndex = this.findTabIndex(id)
if (tabIndex >= 0) {
this.$store.commit('deleteTab', tabIndex)
}
},
findTabIndex (id) {
return this.$store.state.tabs.findIndex(tab => tab.id === id)
},
exportQuery (index) {
this.currentQueryIndex = index
const downloader = this.$refs.downloader
const currentQuery = JSON.parse(JSON.stringify(this.queries[this.currentQueryIndex]))
delete currentQuery.id
delete currentQuery.createdAt
const json = JSON.stringify(currentQuery)
const blob = new Blob([json], { type: 'octet/stream' })
const url = window.URL.createObjectURL(blob)
downloader.href = url
downloader.download = `${currentQuery.name}.json`
downloader.click()
window.URL.revokeObjectURL(url)
},
importQueries () {
const file = this.$refs.importFile.files[0]
const reader = new FileReader()
reader.onload = () => {
let importedQueries = JSON.parse(event.target.result)
if (!Array.isArray(importedQueries)) {
importedQueries = [importedQueries]
}
importedQueries.forEach(query => {
query.id = nanoid()
query.createdAt = new Date()
})
this.queries = this.queries.concat(importedQueries)
this.saveQueriesInLocalStorage()
this.$refs.importFile.value = null
}
reader.readAsText(file)
},
saveQueriesInLocalStorage () {
localStorage.setItem('myQueries', JSON.stringify(this.queries))
}
}
}
</script>
<style scoped>
#my-queries-content {
padding: 52px;
}
#my-queries-toolbar {
display: flex;
justify-content: space-between;
margin-bottom: 18px;
margin: 0 auto 8px;
max-width: 1500px;
width: 100%;
}
.rounded-bg {
margin: 0 auto;
max-width: 1500px;
width: 100%;
}
table {
margin-top: 0;
}
tbody tr td {
overflow: hidden;
min-width: 0;
text-overflow: ellipsis;
padding: 0 24px;
line-height: 40px;
}
tbody tr td:first-child {
width: 70%;
max-width: 0;
}
tbody tr td:last-child {
width: 30%;
max-width: 0;
}
tbody tr:hover td {
cursor: pointer;
}
tbody tr:hover td {
color: var(--color-text-active);
}
.second-column {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 100%;
}
.icons-container {
display: none;
}
.date-container {
flex-shrink: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
tbody tr:hover .icons-container {
display: block;
}
.dialog input {
width: 100%;
}
a, #import-file {
display: none;
}
button.toolbar {
margin-right: 16px;
}
button label {
display: block;
line-height: 36px;
}
button label:hover {
cursor: pointer;
}
</style>

View File

@@ -1,13 +0,0 @@
import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message'
const wrapper = shallowMount(HelloWorld, {
propsData: { msg }
})
expect(wrapper.text()).to.include(msg)
})
})

View File

@@ -1,15 +0,0 @@
const CopyPlugin = require('copy-webpack-plugin')
module.exports = {
publicPath: '',
configureWebpack: {
plugins: [
new CopyPlugin([
// This wasm file will be fetched dynamically when we initialize sql.js
// It is important that we do not change its name, and that it is in the same folder as the js
{ from: 'node_modules/sql.js/dist/sql-wasm.wasm', to: 'js/' },
{ from: 'node_modules/sql.js/dist/worker.sql-wasm.js', to: 'js/' }
])
]
}
}

2
workbox-6716fad7.js Normal file

File diff suppressed because one or more lines are too long

1
workbox-6716fad7.js.map Normal file

File diff suppressed because one or more lines are too long