0.0.1
@@ -1,3 +0,0 @@
|
|||||||
> 1%
|
|
||||||
last 2 versions
|
|
||||||
not dead
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
[*.{js,jsx,ts,tsx,vue}]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
|
||||||
28
.eslintrc.js
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
22
.gitignore
vendored
@@ -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?
|
|
||||||
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"git.ignoreLimitWarning": true
|
||||||
|
}
|
||||||
21
README.md
@@ -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
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@vue/cli-plugin-babel/preset'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 243 B After Width: | Height: | Size: 243 B |
16869
package-lock.json
generated
46
package.json
@@ -1,46 +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",
|
|
||||||
"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-react": "^1.2.0",
|
|
||||||
"vue-router": "^3.2.0",
|
|
||||||
"vuejs-paginate": "^2.1.0",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -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>
|
|
||||||
16
src/App.vue
@@ -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>
|
|
||||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
|
Before Width: | Height: | Size: 6.7 KiB |
@@ -1,54 +0,0 @@
|
|||||||
button:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.primary {
|
|
||||||
background: var(--color-accent);
|
|
||||||
border: 1px solid var(--color-accent-shade);
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: var(--border-radius-big);
|
|
||||||
height: 36px;
|
|
||||||
padding: 0 12px;
|
|
||||||
min-width: 83px;
|
|
||||||
color: var(--color-text-light);
|
|
||||||
text-shadow: var(--shadow);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.primary:hover {
|
|
||||||
background: var(--color-accent-shade);
|
|
||||||
border: 1px solid var(--color-accent-shade);
|
|
||||||
color: var(--color-text-light);
|
|
||||||
text-shadow: var(--shadow);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.toolbar {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: 36px;
|
|
||||||
padding: 0 12px;
|
|
||||||
color: var(--color-text-base);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.toolbar:hover {
|
|
||||||
color: var(--color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
input[type="text"] {
|
|
||||||
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: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"]::placeholder {
|
|
||||||
color: var(--color-text-light-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="text"]:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,38 +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-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);
|
|
||||||
|
|
||||||
--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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,100 +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>
|
|
||||||
export default {
|
|
||||||
name: 'MainMenu',
|
|
||||||
methods: {
|
|
||||||
createNewQuery () {
|
|
||||||
const tab = {
|
|
||||||
id: Number(new Date()),
|
|
||||||
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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<input type="text" placeholder="Search table"/>
|
|
||||||
<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'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'Schema',
|
|
||||||
components: { TableDescription },
|
|
||||||
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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,247 +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'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'TabContent',
|
|
||||||
props: ['id', 'initName', 'initQuery', 'initPlotly', 'tabIndex'],
|
|
||||||
components: {
|
|
||||||
codemirror,
|
|
||||||
SqlTable,
|
|
||||||
Splitpanes,
|
|
||||||
ViewSwitcher
|
|
||||||
},
|
|
||||||
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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg 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>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg 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>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg 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>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg 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>
|
|
||||||
22
src/main.js
@@ -1,22 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import App from './App.vue'
|
|
||||||
import router from './router'
|
|
||||||
import store from './store'
|
|
||||||
import VueReact from 'vue-react'
|
|
||||||
import PlotlyEditor from 'react-chart-editor'
|
|
||||||
|
|
||||||
import '@/assets/styles/variables.css'
|
|
||||||
import '@/assets/styles/buttons.css'
|
|
||||||
import '@/assets/styles/textFields.css'
|
|
||||||
import '@/assets/styles/tables.css'
|
|
||||||
|
|
||||||
Vue.use(VueReact)
|
|
||||||
Vue.react('PlotlyEditor', PlotlyEditor)
|
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
router,
|
|
||||||
store,
|
|
||||||
render: h => h(App)
|
|
||||||
}).$mount('#app')
|
|
||||||
@@ -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
|
|
||||||
@@ -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: {
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div id="my-queries-content">
|
|
||||||
<div id="my-queries-toolbar">
|
|
||||||
<div id="toolbar-buttons">
|
|
||||||
<button class="toolbar">Import</button>
|
|
||||||
<button class="toolbar">Export</button>
|
|
||||||
<button class="toolbar">Delete</button>
|
|
||||||
</div>
|
|
||||||
<div id="toolbar-search">
|
|
||||||
<input type="text" placeholder="Search query by name"/>
|
|
||||||
</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 />
|
|
||||||
<copy-icon />
|
|
||||||
<export-icon />
|
|
||||||
<delete-icon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import RenameIcon from '@/components/svg/rename.vue'
|
|
||||||
import CopyIcon from '@/components/svg/copy.vue'
|
|
||||||
import ExportIcon from '@/components/svg/export.vue'
|
|
||||||
import DeleteIcon from '@/components/svg/delete.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'MyQueries',
|
|
||||||
components: {
|
|
||||||
RenameIcon,
|
|
||||||
CopyIcon,
|
|
||||||
ExportIcon,
|
|
||||||
DeleteIcon
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
queries: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
#my-queries-content {
|
|
||||||
padding: 52px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#my-queries-toolbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rounded-bg,
|
|
||||||
#my-queries-toolbar {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
const CopyPlugin = require('copy-webpack-plugin')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
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/' }
|
|
||||||
])
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||