mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-06 18:18:53 +08:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c685b6d7c | ||
|
|
a37edc501b | ||
|
|
46a9d1613b | ||
|
|
b08265fab9 | ||
|
|
f1b5f5e3c7 | ||
|
|
e08fdbecbc | ||
|
|
a5ba1cb821 | ||
|
|
7da1cb36fa | ||
|
|
57c8fe5bc8 | ||
|
|
e7d3da8869 | ||
|
|
5dc80751c4 | ||
|
|
3e52dcac2c | ||
|
|
1037185a6a | ||
|
|
fec8fb5ac0 | ||
|
|
621a41844e | ||
|
|
0a3a94444e | ||
|
|
37aa2d35d5 | ||
|
|
5f91180a8c | ||
|
|
b8c5a2bfd7 | ||
|
|
880c15762b | ||
|
|
df54c9086b | ||
|
|
fdd50b2f86 | ||
|
|
b39a6bdb86 | ||
|
|
5a8b2584ff | ||
|
|
aae47eff86 | ||
|
|
65db2556c0 | ||
|
|
d132127143 | ||
|
|
3e5e4b29c1 | ||
|
|
71c70e0232 | ||
|
|
8f49c0509f | ||
|
|
5e29a051b2 | ||
|
|
8b76258260 | ||
|
|
518270e1f5 | ||
|
|
f16fab2d7a | ||
|
|
769c146d95 | ||
|
|
39d958de86 | ||
|
|
fbccb3d9be | ||
|
|
f25a4d5c07 | ||
|
|
805f2861aa | ||
|
|
9c6aae7c02 | ||
|
|
d7782733ed | ||
|
|
343dea6ba8 | ||
|
|
6a178a6436 | ||
|
|
dbc2e3d0f3 | ||
|
|
24e8e3e520 |
21
.github/workflows/main.yml
vendored
21
.github/workflows/main.yml
vendored
@@ -1,18 +1,16 @@
|
||||
name: Deploy to GitHub Pages
|
||||
name: Deploy to GitHub Pages and create release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
branches:
|
||||
- master
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy to GitHub Pages
|
||||
name: Deploy to GitHub Pages and create release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
@@ -22,6 +20,17 @@ jobs:
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Create archive
|
||||
run: |
|
||||
cd dist
|
||||
zip -9 -r dist.zip . -x "js/*.map"
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "dist/dist.zip"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@3.6.2
|
||||
with:
|
||||
|
||||
1215
package-lock.json
generated
1215
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -1,24 +1,26 @@
|
||||
{
|
||||
"name": "sqliteviz",
|
||||
"version": "0.0.1",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"build": "NODE_OPTIONS=--max_old_space_size=4096 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",
|
||||
"nanoid": "^3.1.12",
|
||||
"plotly.js": "^1.57.1",
|
||||
"react": "^16.13.1",
|
||||
"react-chart-editor": "^0.41.7",
|
||||
"react-chart-editor": "^0.42.0",
|
||||
"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",
|
||||
|
||||
47
public/queries.json
Normal file
47
public/queries.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"query": "select * from invoices",
|
||||
"chart": {
|
||||
"data": [
|
||||
{
|
||||
"type": "scatter",
|
||||
"mode": "lines",
|
||||
"x": null,
|
||||
"xsrc": "InvoiceId",
|
||||
"meta": {
|
||||
"columnNames": {
|
||||
"x": "InvoiceId",
|
||||
"y": "Total"
|
||||
}
|
||||
},
|
||||
"y": null,
|
||||
"ysrc": "Total"
|
||||
}
|
||||
],
|
||||
"layout": {
|
||||
"xaxis": {
|
||||
"range": [
|
||||
1,
|
||||
412
|
||||
],
|
||||
"autorange": true,
|
||||
"type": "linear"
|
||||
},
|
||||
"yaxis": {
|
||||
"range": [
|
||||
-0.39166666666666683,
|
||||
27.241666666666667
|
||||
],
|
||||
"autorange": true,
|
||||
"type": "linear"
|
||||
},
|
||||
"autosize": true,
|
||||
"mapbox": {
|
||||
"style": "open-street-map"
|
||||
}
|
||||
},
|
||||
"frames": []
|
||||
},
|
||||
"name": "Invoices",
|
||||
"id": "ieZfcITwDUTADwOmQlYyL",
|
||||
"createdAt": "2020-11-03T14:17:49.524Z"
|
||||
}
|
||||
17
src/assets/images/checkbox_checked.svg
Normal file
17
src/assets/images/checkbox_checked.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="17" height="17" rx="2.5" fill="#119DFF" stroke="#0D76BF"/>
|
||||
<g filter="url(#filter0_d)">
|
||||
<path d="M15.75 5.25L6.75 14.25L2.625 10.125L3.6825 9.0675L6.75 12.1275L14.6925 4.1925L15.75 5.25Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d" x="0.625" y="3.1925" width="17.125" height="14.0575" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.164706 0 0 0 0 0.247059 0 0 0 0 0.372549 0 0 0 0.7 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 981 B |
17
src/assets/images/checkbox_checked_light.svg
Normal file
17
src/assets/images/checkbox_checked_light.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="17" height="17" rx="2.5" fill="#F3F6FA" stroke="#C8D4E3"/>
|
||||
<g filter="url(#filter0_d)">
|
||||
<path d="M15.75 5.24988L6.75 14.2499L2.625 10.1249L3.6825 9.06738L6.75 12.1274L14.6925 4.19238L15.75 5.24988Z" fill="#119DFF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d" x="0.625" y="3.19238" width="17.125" height="14.0575" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.164706 0 0 0 0 0.247059 0 0 0 0 0.372549 0 0 0 0.45 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 996 B |
3
src/assets/images/info.svg
Normal file
3
src/assets/images/info.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 9H13V7H11V9ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20ZM12 2C10.6868 2 9.38642 2.25866 8.17317 2.7612C6.95991 3.26375 5.85752 4.00035 4.92893 4.92893C3.05357 6.8043 2 9.34784 2 12C2 14.6522 3.05357 17.1957 4.92893 19.0711C5.85752 19.9997 6.95991 20.7362 8.17317 21.2388C9.38642 21.7413 10.6868 22 12 22C14.6522 22 17.1957 20.9464 19.0711 19.0711C20.9464 17.1957 22 14.6522 22 12C22 10.6868 21.7413 9.38642 21.2388 8.17317C20.7362 6.95991 19.9997 5.85752 19.0711 4.92893C18.1425 4.00035 17.0401 3.26375 15.8268 2.7612C14.6136 2.25866 13.3132 2 12 2V2ZM11 17H13V11H11V17Z" fill="#A2B1C6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 754 B |
@@ -1,3 +1,12 @@
|
||||
button {
|
||||
box-sizing: border-box;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
outline: none;
|
||||
}
|
||||
@@ -5,16 +14,10 @@ button:focus {
|
||||
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 {
|
||||
@@ -22,8 +25,6 @@ button.primary:hover {
|
||||
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 {
|
||||
@@ -34,16 +35,24 @@ button.primary:disabled {
|
||||
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;
|
||||
box-sizing: border-box;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
color: var(--color-text-base);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button.toolbar:hover {
|
||||
|
||||
40
src/assets/styles/dialogs.css
Normal file
40
src/assets/styles/dialogs.css
Normal file
@@ -0,0 +1,40 @@
|
||||
.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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
15
src/assets/styles/tooltips.css
Normal file
15
src/assets/styles/tooltips.css
Normal file
@@ -0,0 +1,15 @@
|
||||
.icon-tooltip {
|
||||
visibility: hidden;
|
||||
background-color: rgba(80, 103, 132, 0.85);
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
padding: 0 6px;
|
||||
line-height: 19px;;
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
height: 19px;
|
||||
border-radius: var(--border-radius-medium);
|
||||
white-space: nowrap;
|
||||
z-index: 999;
|
||||
}
|
||||
@@ -10,6 +10,8 @@
|
||||
--color-blue-medium: #119DFF;
|
||||
--color-blue-dark: #0D76BF;
|
||||
--color-blue-dark-2: #2A3F5F;
|
||||
--color-red: #EF553B;
|
||||
--color-yellow: #FBEFCB;
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +19,7 @@
|
||||
--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-bg-warning: var(--color-yellow);
|
||||
--color-accent: var(--color-blue-medium);
|
||||
--color-accent-shade: var(--color-blue-dark);
|
||||
--color-border-light: var(--color-gray-light-2);
|
||||
@@ -25,6 +28,7 @@
|
||||
--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);
|
||||
|
||||
|
||||
131
src/components/Chart.vue
Normal file
131
src/components/Chart.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div v-show="visible" class="chart-container">
|
||||
<div class="chart-worning" v-show="!sqlResult && visible">
|
||||
There is no data to build a chart. Run your sql query and make sure the result is not empty.
|
||||
</div>
|
||||
<PlotlyEditor
|
||||
:data="state.data"
|
||||
:layout="state.layout"
|
||||
:frames="state.frames"
|
||||
:config="{ editable: true, displaylogo: false }"
|
||||
:dataSources="dataSources"
|
||||
:dataSourceOptions="dataSourceOptions"
|
||||
:plotly="plotly"
|
||||
@onUpdate="update"
|
||||
:useResizeHandler="true"
|
||||
:debug="true"
|
||||
:advancedTraceTypeSelector="true"
|
||||
class="chart"
|
||||
:style="{ height: !sqlResult ? 'calc(100% - 40px)' : '100%' }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import plotly from 'plotly.js/dist/plotly'
|
||||
import 'react-chart-editor/lib/react-chart-editor.min.css'
|
||||
|
||||
import PlotlyEditor from 'react-chart-editor'
|
||||
import dereference from 'react-chart-editor/lib/lib/dereference'
|
||||
|
||||
export default {
|
||||
name: 'Chart',
|
||||
props: ['sqlResult', 'initChart', 'visible'],
|
||||
components: {
|
||||
PlotlyEditor
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
plotly: plotly,
|
||||
state: this.initChart || {
|
||||
data: [],
|
||||
layout: {},
|
||||
frames: []
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dataSources () {
|
||||
if (!this.sqlResult) {
|
||||
return {}
|
||||
}
|
||||
const dataSorces = {}
|
||||
const matrix = this.sqlResult.values
|
||||
const [row] = matrix
|
||||
const transposedMatrix = row.map((value, column) => matrix.map(row => row[column]))
|
||||
this.sqlResult.columns.forEach((column, index) => {
|
||||
dataSorces[column] = transposedMatrix[index]
|
||||
})
|
||||
return dataSorces
|
||||
},
|
||||
dataSourceOptions () {
|
||||
return Object.keys(this.dataSources).map(name => ({
|
||||
value: name,
|
||||
label: name
|
||||
}))
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
dataSources () {
|
||||
// we need to update state.data in order to update the graph
|
||||
// https://github.com/plotly/react-chart-editor/issues/948
|
||||
dereference(this.state.data, this.dataSources)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update (data, layout, frames) {
|
||||
this.state = { data, layout, frames }
|
||||
this.$emit('update')
|
||||
},
|
||||
getChartSatateForSave () {
|
||||
// we don't need to save the data, only settings
|
||||
// so we modify state.data using dereference
|
||||
const stateCopy = JSON.parse(JSON.stringify(this.state))
|
||||
const emptySources = {}
|
||||
for (const key in this.dataSources) {
|
||||
emptySources[key] = []
|
||||
}
|
||||
dereference(stateCopy.data, emptySources)
|
||||
return stateCopy
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.chart-container {
|
||||
height: calc(100% - 89px);
|
||||
}
|
||||
.chart-worning {
|
||||
background-color: var(--color-bg-warning);
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
color: var(--color-text-base);
|
||||
font-size: 13px;
|
||||
padding: 0 24px;
|
||||
}
|
||||
.chart {
|
||||
border-top: 1px solid var(--color-border);
|
||||
min-height: 242px;
|
||||
}
|
||||
|
||||
>>> .editor_controls .sidebar__item:before {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
>>> .sidebar {
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
>>> .editor_controls__wrapper>.panel,
|
||||
>>> .editor_controls .panel__empty {
|
||||
width: 315px;
|
||||
}
|
||||
>>> .editor_controls .sidebar__group__title {
|
||||
padding-left: 10px;
|
||||
}
|
||||
>>> .editor_controls .sidebar__item {
|
||||
padding-left: 32px;
|
||||
}
|
||||
</style>
|
||||
67
src/components/CheckBox.vue
Normal file
67
src/components/CheckBox.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="checkbox-container" @click.stop="onClick">
|
||||
<div v-show="!checked" class="unchecked" />
|
||||
<img
|
||||
v-show="checked && theme === 'accent'"
|
||||
:src="require('@/assets/images/checkbox_checked.svg')"
|
||||
/>
|
||||
<img
|
||||
v-show="checked && theme === 'light'"
|
||||
:src="require('@/assets/images/checkbox_checked_light.svg')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'checkBox',
|
||||
props: {
|
||||
theme: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'accent',
|
||||
validator: (value) => {
|
||||
return ['accent', 'light'].includes(value)
|
||||
}
|
||||
},
|
||||
init: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
checked: this.init
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick () {
|
||||
this.checked = !this.checked
|
||||
this.$emit('click', this.checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.checkbox-container {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
.unchecked {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: white;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-medium);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.unchecked:hover {
|
||||
background-color: var(--color-bg-light);
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -8,23 +8,85 @@
|
||||
<button
|
||||
v-if="$store.state.tabs.length > 0"
|
||||
class="primary"
|
||||
:disabled="!$store.state.currentTab.isUnsaved"
|
||||
@click="saveQuery"
|
||||
:disabled="currentQuery && !$store.state.schema || !currentQuery.query"
|
||||
@click="currentQuery.execute"
|
||||
>
|
||||
Run
|
||||
</button>
|
||||
<button
|
||||
v-if="$store.state.tabs.length > 0"
|
||||
class="primary"
|
||||
:disabled="currentQuery && !currentQuery.isUnsaved"
|
||||
@click="checkQueryBeforeSave"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button class="primary" @click="createNewQuery">Create</button>
|
||||
</div>
|
||||
|
||||
<!--Save Query dialog -->
|
||||
<modal name="save" classes="dialog" height="auto">
|
||||
<div class="dialog-header">
|
||||
Save query
|
||||
<close-icon @click="$modal.hide('save')"/>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<div v-show="isPredefined" id="save-note">
|
||||
<img :src="require('@/assets/images/info.svg')">
|
||||
Note: Predefined queries can't be edited.
|
||||
That's why your modifications will be saved as a new query. Enter the name for it.
|
||||
</div>
|
||||
<text-field
|
||||
label="Query name"
|
||||
:error-msg="errorMsg"
|
||||
v-model="name"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button class="secondary" @click="$modal.hide('save')">Cancel</button>
|
||||
<button class="primary" @click="saveQuery">Save</button>
|
||||
</div>
|
||||
</modal>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { nanoid } from 'nanoid'
|
||||
import TextField from '@/components/TextField'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
|
||||
export default {
|
||||
name: 'MainMenu',
|
||||
components: {
|
||||
TextField,
|
||||
CloseIcon
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
name: '',
|
||||
errorMsg: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentQuery () {
|
||||
return this.$store.state.currentTab
|
||||
},
|
||||
isPredefined () {
|
||||
if (this.currentQuery) {
|
||||
return this.currentQuery.isPredefined
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$root.$on('createNewQuery', this.createNewQuery)
|
||||
},
|
||||
methods: {
|
||||
createNewQuery () {
|
||||
const tab = {
|
||||
id: Number(new Date()),
|
||||
id: nanoid(),
|
||||
name: null,
|
||||
tempName: this.$store.state.untitledLastIndex
|
||||
? `Untitled ${this.$store.state.untitledLastIndex}`
|
||||
@@ -33,42 +95,74 @@ export default {
|
||||
}
|
||||
this.$store.commit('addTab', tab)
|
||||
this.$store.commit('setCurrentTabId', tab.id)
|
||||
this.$store.commit('updateUntitledLastIndex')
|
||||
},
|
||||
checkQueryBeforeSave () {
|
||||
this.errorMsg = null
|
||||
const isFromScratch = !this.currentQuery.initName
|
||||
|
||||
if (isFromScratch || this.isPredefined) {
|
||||
this.$modal.show('save')
|
||||
} else {
|
||||
this.saveQuery()
|
||||
}
|
||||
},
|
||||
saveQuery () {
|
||||
const currentQuery = this.$store.state.currentTab
|
||||
const isFromScratch = !this.$store.state.currentTab.initName
|
||||
const isFromScratch = !this.currentQuery.initName
|
||||
if ((isFromScratch || this.isPredefined) && !this.name) {
|
||||
this.errorMsg = 'Query name can\'t be empty'
|
||||
return
|
||||
}
|
||||
const dataSet = this.currentQuery.result
|
||||
const tabView = this.currentQuery.view
|
||||
// Prepare query
|
||||
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
|
||||
id: this.isPredefined ? nanoid() : this.currentQuery.id,
|
||||
query: this.currentQuery.query,
|
||||
chart: this.currentQuery.getChartSatateForSave(),
|
||||
name: (!this.isPredefined && this.currentQuery.initName) || this.name,
|
||||
createdAt: new Date()
|
||||
}
|
||||
|
||||
// Save query
|
||||
let myQueries = JSON.parse(localStorage.getItem('myQueries'))
|
||||
if (!myQueries) {
|
||||
myQueries = [value]
|
||||
} else if (isFromScratch) {
|
||||
} else if (isFromScratch || this.isPredefined) {
|
||||
myQueries.push(value)
|
||||
} else {
|
||||
const queryIndex = myQueries.findIndex(query => query.id === currentQuery.id)
|
||||
const queryIndex = myQueries.findIndex(query => query.id === this.currentQuery.id)
|
||||
value.createdAt = myQueries[queryIndex].createdAt
|
||||
myQueries[queryIndex] = value
|
||||
}
|
||||
localStorage.setItem('myQueries', JSON.stringify(myQueries))
|
||||
currentQuery.isUnsaved = false
|
||||
|
||||
// Update tab
|
||||
this.$store.commit('updateTab', {
|
||||
index: this.currentQuery.tabIndex,
|
||||
name: value.name,
|
||||
id: value.id,
|
||||
query: value.query,
|
||||
chart: value.chart,
|
||||
isUnsaved: false
|
||||
})
|
||||
|
||||
// Restore data:
|
||||
// e.g. if we save predefined query the tab will be created again
|
||||
// (because of new id) and
|
||||
// it will be without sql result and has default view - table.
|
||||
// That's why we need to restore data and view
|
||||
this.$nextTick(() => {
|
||||
this.currentQuery.result = dataSet
|
||||
this.currentQuery.view = tabView
|
||||
})
|
||||
|
||||
// Hide dialog
|
||||
this.$modal.hide('save')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
nav {
|
||||
height: 68px;
|
||||
@@ -83,6 +177,7 @@ nav {
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
padding: 0 52px;
|
||||
z-index: 999;
|
||||
}
|
||||
a {
|
||||
font-size: 18px;
|
||||
@@ -97,4 +192,13 @@ a.router-link-active {
|
||||
button {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
#save-note {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
#save-note img {
|
||||
margin: -3px 6px 0 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<input type="text" placeholder="Search table"/>
|
||||
<div id="schema-container">
|
||||
<div id="schema-filter">
|
||||
<text-field placeholder="Search table" width="100%" v-model="filter"/>
|
||||
</div>
|
||||
<div id="db">
|
||||
<div @click="schemaVisible = !schemaVisible" class="db-name">
|
||||
<svg
|
||||
:style="{transform: schemaVisible ? 'rotate(90deg)' : 'rotate(0)'}"
|
||||
:style="{ transform: schemaVisible ? 'rotate(90deg)' : 'rotate(0)' }"
|
||||
class="chevron-icon"
|
||||
width="9"
|
||||
height="9"
|
||||
@@ -22,25 +24,14 @@
|
||||
<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>
|
||||
<change-db-icon />
|
||||
</label>
|
||||
<span class="db-edit-tooltip">Change database</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="schemaVisible" class="schema">
|
||||
<div v-if="schemaVisible" class="schema">
|
||||
<table-description
|
||||
v-for="(table, index) in schema"
|
||||
:key="index"
|
||||
v-for="table in schema"
|
||||
:key="table[0]"
|
||||
:name="table[0]"
|
||||
:sql="table[1]"
|
||||
/>
|
||||
@@ -50,19 +41,33 @@
|
||||
|
||||
<script>
|
||||
import TableDescription from '@/components/TableDescription'
|
||||
import TextField from '@/components/TextField'
|
||||
import ChangeDbIcon from '@/components/svg/changeDb'
|
||||
|
||||
export default {
|
||||
name: 'Schema',
|
||||
components: { TableDescription },
|
||||
components: {
|
||||
TableDescription,
|
||||
TextField,
|
||||
ChangeDbIcon
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
schemaVisible: true,
|
||||
worker: this.$store.state.worker
|
||||
filter: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
schema () {
|
||||
return this.$store.state.schema
|
||||
if (!this.$store.state.schema) {
|
||||
return []
|
||||
}
|
||||
|
||||
return !this.filter
|
||||
? this.$store.state.schema
|
||||
: this.$store.state.schema.filter(
|
||||
table => table[0].toUpperCase().indexOf(this.filter.toUpperCase()) !== -1
|
||||
)
|
||||
},
|
||||
dbName () {
|
||||
return this.$store.state.dbName
|
||||
@@ -70,37 +75,32 @@ export default {
|
||||
},
|
||||
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)
|
||||
this.$db.loadDb(this.$refs.dbfile.files[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#schema-container {
|
||||
position: relative;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.schema {
|
||||
margin-left: 12px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
#schema-filter {
|
||||
padding: 32px 12px;
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
box-sizing: border-box;
|
||||
background-image: linear-gradient(white 73%, transparent);;
|
||||
z-index: 2;
|
||||
}
|
||||
.schema, .db-name {
|
||||
color: var(--color-text-base);
|
||||
@@ -110,6 +110,8 @@ export default {
|
||||
#db {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: -5px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.db-name {
|
||||
@@ -123,36 +125,4 @@ export default {
|
||||
>>> .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,8 +1,13 @@
|
||||
<template>
|
||||
<div
|
||||
ref="container"
|
||||
:class="['splitpanes', `splitpanes--${horizontal ? 'horizontal' : 'vertical'}`, { 'splitpanes--dragging': touch.dragging }]"
|
||||
:class="[
|
||||
'splitpanes',
|
||||
`splitpanes--${horizontal ? 'horizontal' : 'vertical'}`,
|
||||
{ 'splitpanes--dragging': touch.dragging }
|
||||
]"
|
||||
>
|
||||
<div class="movable-splitter" ref="movableSplitter" :style="movableSplitterStyle" />
|
||||
<div
|
||||
class="splitpanes__pane"
|
||||
ref="left"
|
||||
@@ -12,13 +17,21 @@
|
||||
>
|
||||
<slot name="left-pane" />
|
||||
</div>
|
||||
|
||||
<splitter
|
||||
<!-- Splitter start-->
|
||||
<div
|
||||
class="splitpanes__splitter"
|
||||
@mousedown="onMouseDown"
|
||||
@toggle="toggleFirstPane"
|
||||
:expanded="paneBefore.size !== 0"
|
||||
/>
|
||||
|
||||
@touchstart="onMouseDown"
|
||||
>
|
||||
<div class="toggle-btn" @click="toggleFirstPane">
|
||||
<img
|
||||
class="direction-icon"
|
||||
:src="require('@/assets/images/chevron.svg')"
|
||||
:style="directionIconStyle"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<!-- splitter end -->
|
||||
<div
|
||||
class="splitpanes__pane"
|
||||
ref="right"
|
||||
@@ -30,33 +43,58 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Splitter from '@/components/splitter'
|
||||
|
||||
export default {
|
||||
name: 'splitpanes',
|
||||
components: { Splitter },
|
||||
name: 'Splitpanes',
|
||||
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
|
||||
data () {
|
||||
return {
|
||||
container: null,
|
||||
paneBefore: this.before,
|
||||
paneAfter: this.after,
|
||||
beforeMinimising: this.before.size,
|
||||
touch: {
|
||||
mouseDown: false,
|
||||
dragging: false
|
||||
},
|
||||
movableSplitter: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
visibility: 'hidden'
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
},
|
||||
computed: {
|
||||
styles () {
|
||||
return [
|
||||
{ [this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%` },
|
||||
{ [this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%` }
|
||||
]
|
||||
},
|
||||
movableSplitterStyle () {
|
||||
const style = { ...this.movableSplitter }
|
||||
style.top += '%'
|
||||
style.left += '%'
|
||||
return style
|
||||
},
|
||||
expanded () {
|
||||
return this.paneBefore.size !== 0
|
||||
},
|
||||
directionIconStyle () {
|
||||
const translation = 'translate(-50%, -50%)'
|
||||
if (this.horizontal) {
|
||||
return {
|
||||
transform: `${translation} ${this.expanded ? 'rotate(-90deg)' : 'rotate(90deg)'}`
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
transform: `${translation} ${this.expanded ? 'rotate(180deg)' : ''}`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -92,12 +130,28 @@ export default {
|
||||
// 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))
|
||||
this.$set(this.movableSplitter, 'visibility', 'visible')
|
||||
this.moveSplitter(event)
|
||||
}
|
||||
},
|
||||
|
||||
onMouseUp () {
|
||||
this.touch.mouseDown = false
|
||||
if (this.touch.dragging) {
|
||||
const dragPercentage = this.horizontal
|
||||
? this.movableSplitter.top
|
||||
: this.movableSplitter.left
|
||||
|
||||
this.paneBefore.size = dragPercentage
|
||||
this.paneAfter.size = 100 - dragPercentage
|
||||
|
||||
this.movableSplitter = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
visibility: 'hidden'
|
||||
}
|
||||
}
|
||||
|
||||
// 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(() => {
|
||||
@@ -109,8 +163,9 @@ export default {
|
||||
// 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
|
||||
|
||||
const { clientX, clientY } = ('ontouchstart' in window && event.touches)
|
||||
? event.touches[0]
|
||||
: event
|
||||
return {
|
||||
x: clientX - rect.left,
|
||||
y: clientY - rect.top
|
||||
@@ -126,27 +181,26 @@ export default {
|
||||
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.
|
||||
moveSplitter (event) {
|
||||
const dragPercentage = this.getCurrentDragPercentage(this.getCurrentMouseDrag(event))
|
||||
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)
|
||||
|
||||
const dir = this.horizontal ? 'top' : 'left'
|
||||
|
||||
// Prevent dragging beyond pane max.
|
||||
if (paneBeforeMaxReached || paneAfterMaxReached) {
|
||||
if (paneBeforeMaxReached) {
|
||||
paneBefore.size = paneBefore.max
|
||||
paneAfter.size = Math.max(100 - paneBefore.max, 0)
|
||||
this.$set(this.movableSplitter, dir, paneBefore.max)
|
||||
} else {
|
||||
paneBefore.size = Math.max(100 - paneAfter.max, 0)
|
||||
paneAfter.size = paneAfter.max
|
||||
this.$set(this.movableSplitter, dir, Math.max(100 - paneAfter.max, 0))
|
||||
}
|
||||
return
|
||||
} else {
|
||||
this.$set(this.movableSplitter, dir, Math.min(Math.max(dragPercentage, 0), paneBefore.max))
|
||||
}
|
||||
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) {
|
||||
@@ -160,10 +214,6 @@ export default {
|
||||
},
|
||||
mounted () {
|
||||
this.container = this.$refs.container
|
||||
},
|
||||
created () {
|
||||
this.paneBefore = this.before
|
||||
this.paneAfter = this.after
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -172,6 +222,7 @@ export default {
|
||||
.splitpanes {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.splitpanes--vertical {flex-direction: row;}
|
||||
@@ -183,4 +234,68 @@ export default {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Splitter */
|
||||
|
||||
.splitpanes--vertical > .splitpanes__splitter,
|
||||
.splitpanes--vertical.splitpanes--dragging {
|
||||
cursor: col-resize;
|
||||
}
|
||||
.splitpanes--horizontal > .splitpanes__splitter,
|
||||
.splitpanes--horizontal.splitpanes--dragging {
|
||||
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;
|
||||
}
|
||||
|
||||
.movable-splitter {
|
||||
position: absolute;
|
||||
background-color:rgba(162, 177, 198, 0.5);
|
||||
}
|
||||
|
||||
.splitpanes--vertical > .splitpanes__splitter,
|
||||
.splitpanes--vertical .movable-splitter {
|
||||
width: 3px;
|
||||
z-index: 5;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.splitpanes--horizontal > .splitpanes__splitter,
|
||||
.splitpanes--horizontal .movable-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>
|
||||
82
src/components/SqlEditor.vue
Normal file
82
src/components/SqlEditor.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="codemirror-container">
|
||||
<codemirror v-model="query" :options="cmOptions" @changes="onCmChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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: 'SqlEditor',
|
||||
props: ['value'],
|
||||
components: {
|
||||
codemirror
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
query: this.value,
|
||||
cmOptions: {
|
||||
// codemirror options
|
||||
tabSize: 4,
|
||||
mode: 'text/x-mysql',
|
||||
theme: 'neo',
|
||||
lineNumbers: true,
|
||||
line: true
|
||||
},
|
||||
result: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
query () {
|
||||
this.$emit('input', this.query)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.codemirror-container {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
>>> .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%;
|
||||
}
|
||||
</style>
|
||||
@@ -17,12 +17,12 @@
|
||||
class="table-container"
|
||||
ref="table-container"
|
||||
@scroll="onScrollTable"
|
||||
:style="{height: `${height}px`}"
|
||||
:style="{maxHeight: `${height}px`}"
|
||||
>
|
||||
<table ref="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="(th,index) in data.columns" :key="index" ref="th">
|
||||
<th v-for="(th,index) in dataSet.columns" :key="index" ref="th">
|
||||
<div class="cell-data" :style="cellStyle">{{ th }}</div>
|
||||
</th>
|
||||
</tr>
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
<div class="table-footer">
|
||||
<div class="table-footer-count">
|
||||
{{ data.values.length}} {{data.values.length === 1 ? 'row' : 'rows'}} retrieved
|
||||
{{ dataSet.values.length}} {{dataSet.values.length === 1 ? 'row' : 'rows'}} retrieved
|
||||
</div>
|
||||
<pager v-show="pageCount > 1" :page-count="pageCount" v-model="currentPage" />
|
||||
</div>
|
||||
@@ -52,17 +52,18 @@ import Pager from '@/components/Pager'
|
||||
export default {
|
||||
name: 'SqlTable',
|
||||
components: { Pager },
|
||||
props: ['data', 'height'],
|
||||
props: ['dataSet', 'height'],
|
||||
data () {
|
||||
return {
|
||||
header: null,
|
||||
tableWidth: null,
|
||||
currentPage: 1
|
||||
currentPage: 1,
|
||||
resizeObserver: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
cellStyle () {
|
||||
const eq = this.tableWidth / this.data.columns.length
|
||||
const eq = this.tableWidth / this.dataSet.columns.length
|
||||
|
||||
return { maxWidth: `${Math.max(eq, 100)}px` }
|
||||
},
|
||||
@@ -70,11 +71,11 @@ export default {
|
||||
return Math.max(Math.floor(this.height / 40), 20)
|
||||
},
|
||||
pageCount () {
|
||||
return Math.ceil(this.data.values.length / this.pageSize)
|
||||
return Math.ceil(this.dataSet.values.length / this.pageSize)
|
||||
},
|
||||
currentPageData () {
|
||||
const start = (this.currentPage - 1) * this.pageSize
|
||||
return this.data.values.slice(start, start + this.pageSize)
|
||||
return this.dataSet.values.slice(start, start + this.pageSize)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -94,12 +95,16 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
new ResizeObserver(this.calculateHeadersWidth).observe(this.$refs.table)
|
||||
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
|
||||
this.resizeObserver.observe(this.$refs.table)
|
||||
this.calculateHeadersWidth()
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.resizeObserver.unobserve(this.$refs.table)
|
||||
},
|
||||
watch: {
|
||||
currentPageData: 'calculateHeadersWidth',
|
||||
data () {
|
||||
dataSet () {
|
||||
this.currentPage = 1
|
||||
}
|
||||
}
|
||||
|
||||
197
src/components/Tab.vue
Normal file
197
src/components/Tab.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<div class="tab-content-container" v-show="isActive">
|
||||
<splitpanes
|
||||
class="query-results-splitter"
|
||||
horizontal
|
||||
:before="{ size: 50, max: 70 }"
|
||||
:after="{ size: 50, max: 100 }"
|
||||
>
|
||||
<template #left-pane>
|
||||
<div class="query-editor">
|
||||
<sql-editor v-model="query" />
|
||||
</div>
|
||||
</template>
|
||||
<template #right-pane>
|
||||
<div id="bottomPane" ref="bottomPane">
|
||||
<view-switcher :view.sync="view" />
|
||||
<div v-show="view === 'table'" class="table-view">
|
||||
<div v-show="result === null && !isGettingResults && !error" class="table-preview">
|
||||
Run your query and get results here
|
||||
</div>
|
||||
<div v-show="isGettingResults" class="table-preview">
|
||||
Fetching results...
|
||||
</div>
|
||||
<div v-show="result === undefined && !isGettingResults && !error" class="table-preview">
|
||||
No rows retrieved according to your query
|
||||
</div>
|
||||
<div v-show="error" class="table-preview error">
|
||||
{{ error }}
|
||||
</div>
|
||||
<sql-table v-if="result" :data-set="result" :height="tableViewHeight" />
|
||||
</div>
|
||||
<chart
|
||||
:visible="view === 'chart'"
|
||||
:sql-result="result"
|
||||
:init-chart="initChart"
|
||||
ref="chart"
|
||||
@update="isUnsaved = true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</splitpanes>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SqlTable from '@/components/SqlTable'
|
||||
import SqlEditor from '@/components/SqlEditor'
|
||||
import Splitpanes from '@/components/Splitpanes'
|
||||
import ViewSwitcher from '@/components/ViewSwitcher'
|
||||
import Chart from '@/components/Chart'
|
||||
|
||||
export default {
|
||||
name: 'Tab',
|
||||
props: ['id', 'initName', 'initQuery', 'initChart', 'tabIndex', 'isPredefined'],
|
||||
components: {
|
||||
SqlEditor,
|
||||
SqlTable,
|
||||
Splitpanes,
|
||||
ViewSwitcher,
|
||||
Chart
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
query: this.initQuery,
|
||||
result: null,
|
||||
view: 'table',
|
||||
tableViewHeight: 0,
|
||||
isUnsaved: !this.initName,
|
||||
isGettingResults: false,
|
||||
error: null,
|
||||
resizeObserver: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isActive () {
|
||||
return this.id === this.$store.state.currentTabId
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.commit('setCurrentTab', this)
|
||||
},
|
||||
mounted () {
|
||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||
this.resizeObserver.observe(this.$refs.bottomPane)
|
||||
this.calculateTableHeight()
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.resizeObserver.unobserve(this.$refs.bottomPane)
|
||||
},
|
||||
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: {
|
||||
// Run a command in the database
|
||||
execute () {
|
||||
// this.$refs.output.textContent = 'Fetching results...' */
|
||||
this.isGettingResults = true
|
||||
this.result = null
|
||||
this.error = null
|
||||
this.$db.execute(this.query + ';')
|
||||
.then(result => {
|
||||
this.result = result
|
||||
})
|
||||
.catch(err => {
|
||||
this.error = err
|
||||
})
|
||||
.finally(() => {
|
||||
this.isGettingResults = false
|
||||
})
|
||||
},
|
||||
handleResize () {
|
||||
if (this.view === 'chart') {
|
||||
// hack react-chart editor: hidden and show in order to make the graph resize
|
||||
this.view = 'not chart'
|
||||
this.$nextTick(() => {
|
||||
this.view = 'chart'
|
||||
})
|
||||
}
|
||||
this.calculateTableHeight()
|
||||
},
|
||||
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)
|
||||
},
|
||||
getChartSatateForSave () {
|
||||
return this.$refs.chart.getChartSatateForSave()
|
||||
}
|
||||
}
|
||||
}
|
||||
</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);
|
||||
}
|
||||
|
||||
.query-editor {
|
||||
padding: 52px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 190px;
|
||||
}
|
||||
|
||||
.table-view {
|
||||
margin: 0 52px;
|
||||
height: calc(100% - 88px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table-preview {
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: var(--color-text-base);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.table-preview.error {
|
||||
color: var(--color-text-error);
|
||||
}
|
||||
|
||||
.table-preview.error::first-letter {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<div id="tabs__header">
|
||||
<div id="tabs-container">
|
||||
<div id="tabs__header" v-if="tabs.length > 0">
|
||||
<div
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="tab.id"
|
||||
:key="index"
|
||||
@click="selectTab(tab.id)"
|
||||
:class="[{'tab__selected': (tab.id === selectedIndex)}, 'tab']"
|
||||
>
|
||||
@@ -29,22 +29,30 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<tab-content
|
||||
<tab
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="tab.id"
|
||||
:id="tab.id"
|
||||
:init-name="tab.name"
|
||||
:init-query="tab.query"
|
||||
:init-chart="tab.chart"
|
||||
:is-predefined="tab.isPredefined"
|
||||
:tab-index="index"
|
||||
/>
|
||||
<div v-if="tabs.length === 0" id="start-guide">
|
||||
<span class="link" @click="$root.$emit('createNewQuery')">Create</span>
|
||||
a new query from scratch or open the one from
|
||||
<router-link class="link" to="/my-queries">My queries</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TabContent from '@/components/TabContent'
|
||||
import Tab from '@/components/Tab'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TabContent
|
||||
Tab
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -70,6 +78,10 @@ export default {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#tabs-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
#tabs__header {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
@@ -129,4 +141,19 @@ export default {
|
||||
fill: var(--color-text-base);
|
||||
cursor: pointer;
|
||||
}
|
||||
#start-guide {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: var(--color-text-base);
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
.link {
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
63
src/components/TextField.vue
Normal file
63
src/components/TextField.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<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>
|
||||
@@ -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>
|
||||
41
src/components/svg/changeDb.vue
Normal file
41
src/components/svg/changeDb.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div>
|
||||
<svg
|
||||
class="db-edit-icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@mouseover="showTooltip"
|
||||
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
|
||||
>
|
||||
<path
|
||||
d="M3 10.5V12.75C3 14.25 5.2875 15.54 8.25 15.75V13.5825L8.3475 13.5C5.34 13.32 3 12.045 3 10.5ZM9 9.75C5.685 9.75 3 8.4075 3 6.75V9C3 10.6575 5.685 12 9 12C9.2925 12 9.5775 12 9.87 12L12.75 9.09C11.55 9.54 10.2825 9.75 9 9.75ZM9 2.25C5.685 2.25 3 3.5925 3 5.25C3 6.9075 5.685 8.25 9 8.25C12.315 8.25 15 6.9075 15 5.25C15 3.5925 12.315 2.25 9 2.25ZM15.75 8.3475C15.6375 8.3475 15.5325 8.3925 15.4575 8.475L14.7075 9.225L16.245 10.725L16.995 9.975C17.1525 9.825 17.16 9.57 16.995 9.3975L16.065 8.475C15.99 8.3925 15.885 8.3475 15.78 8.3475H15.75ZM14.28 9.66L9.75 14.205V15.75H11.295L15.84 11.1975L14.28 9.66Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Change database
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
|
||||
export default {
|
||||
name: 'changeDbIcon',
|
||||
mixins: [tooltipMixin]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.db-edit-icon {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
.db-edit-icon:hover path {
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
</style>
|
||||
20
src/components/svg/close.vue
Normal file
20
src/components/svg/close.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<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>
|
||||
@@ -1,12 +1,33 @@
|
||||
<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>
|
||||
<span>
|
||||
<svg
|
||||
class="icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@click.stop="$emit('click')"
|
||||
@mouseover="showTooltip"
|
||||
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
|
||||
>
|
||||
<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>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Duplicate query
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
|
||||
export default {
|
||||
name: 'CopyIcon'
|
||||
name: 'CopyIcon',
|
||||
mixins: [tooltipMixin]
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,19 +1,40 @@
|
||||
<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>
|
||||
<span>
|
||||
<svg
|
||||
class="icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@click.stop="$emit('click')"
|
||||
@mouseover="showTooltip"
|
||||
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
|
||||
>
|
||||
<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>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Delete query
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
|
||||
export default {
|
||||
name: 'DeleteIcon'
|
||||
name: 'DeleteIcon',
|
||||
mixins: [tooltipMixin]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
margin: 0 0 0 12px;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.icon:hover path {
|
||||
|
||||
@@ -1,12 +1,33 @@
|
||||
<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>
|
||||
<span>
|
||||
<svg
|
||||
class="icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@click.stop="$emit('click')"
|
||||
@mouseover="showTooltip"
|
||||
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
|
||||
>
|
||||
<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>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Export query to file
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
|
||||
export default {
|
||||
name: 'ExportIcon'
|
||||
name: 'ExportIcon',
|
||||
mixins: [tooltipMixin]
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,19 +1,40 @@
|
||||
<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>
|
||||
<span>
|
||||
<svg
|
||||
class="icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@click.stop="$emit('click')"
|
||||
@mouseover="showTooltip"
|
||||
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
|
||||
>
|
||||
<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>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Rename query
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
|
||||
export default {
|
||||
name: 'RenameIcon'
|
||||
name: 'RenameIcon',
|
||||
mixins: [tooltipMixin]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
margin: 0 12px 0 6px;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.icon:hover path {
|
||||
|
||||
46
src/dataBase.js
Normal file
46
src/dataBase.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import store from '@/store'
|
||||
import router from '@/router'
|
||||
const worker = new Worker('js/worker.sql-wasm.js')
|
||||
|
||||
export default {
|
||||
loadDb (file) {
|
||||
const dbName = file.name
|
||||
store.commit('saveDbName', dbName)
|
||||
const f = file
|
||||
const r = new FileReader()
|
||||
r.onload = () => {
|
||||
worker.onmessage = () => {
|
||||
const getSchemaSql = `
|
||||
SELECT name, sql
|
||||
FROM sqlite_master
|
||||
WHERE type='table' AND name NOT LIKE 'sqlite_%';`
|
||||
worker.onmessage = event => {
|
||||
store.commit('saveSchema', event.data.results[0].values)
|
||||
if (router.currentRoute.path !== '/editor') {
|
||||
router.push('/editor')
|
||||
}
|
||||
}
|
||||
worker.postMessage({ action: 'exec', sql: getSchemaSql })
|
||||
}
|
||||
store.commit('saveDbFile', r.result)
|
||||
try {
|
||||
worker.postMessage({ action: 'open', buffer: r.result }, [r.result])
|
||||
} catch (exception) {
|
||||
worker.postMessage({ action: 'open', buffer: r.result })
|
||||
}
|
||||
}
|
||||
r.readAsArrayBuffer(f)
|
||||
},
|
||||
execute (commands) {
|
||||
return new Promise((resolve, reject) => {
|
||||
worker.onmessage = (event) => {
|
||||
if (event.data.error) {
|
||||
reject(event.data.error)
|
||||
}
|
||||
// if it was more than one select - take only the first one
|
||||
resolve(event.data.results[0])
|
||||
}
|
||||
worker.postMessage({ action: 'exec', sql: commands })
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,20 @@ import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import { VuePlugin } from 'vuera'
|
||||
import VModal from 'vue-js-modal'
|
||||
import db from '@/dataBase'
|
||||
|
||||
import '@/assets/styles/variables.css'
|
||||
import '@/assets/styles/buttons.css'
|
||||
import '@/assets/styles/textFields.css'
|
||||
import '@/assets/styles/tables.css'
|
||||
import '@/assets/styles/dialogs.css'
|
||||
import '@/assets/styles/tooltips.css'
|
||||
|
||||
Vue.use(VuePlugin)
|
||||
Vue.use(VModal)
|
||||
|
||||
Vue.config.productionTip = false
|
||||
Vue.prototype.$db = db
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
|
||||
17
src/mixins/tooltips.js
Normal file
17
src/mixins/tooltips.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
tooltipStyle: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showTooltip (e) {
|
||||
this.tooltipStyle = {
|
||||
visibility: 'visible',
|
||||
position: 'fixed',
|
||||
top: e.clientY - 12 + 'px',
|
||||
left: e.clientX + 12 + 'px'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,11 @@ export default new Vuex.Store({
|
||||
schema: null,
|
||||
dbFile: null,
|
||||
dbName: null,
|
||||
worker: new Worker('js/worker.sql-wasm.js'),
|
||||
tabs: [],
|
||||
currentTab: null,
|
||||
currentTabId: null,
|
||||
untitledLastIndex: 0
|
||||
untitledLastIndex: 0,
|
||||
predefinedQueries: []
|
||||
},
|
||||
mutations: {
|
||||
saveSchema (state, schema) {
|
||||
@@ -26,14 +26,29 @@ export default new Vuex.Store({
|
||||
},
|
||||
addTab (state, tab) {
|
||||
state.tabs.push(tab)
|
||||
|
||||
if (!tab.name) {
|
||||
state.untitledLastIndex += 1
|
||||
}
|
||||
},
|
||||
updateTabName (state, { index, newName }) {
|
||||
updateTab (state, { index, name, id, query, chart, isUnsaved }) {
|
||||
const tab = state.tabs[index]
|
||||
tab.name = newName
|
||||
const oldId = tab.id
|
||||
|
||||
if (state.currentTabId === oldId) {
|
||||
state.currentTabId = id
|
||||
}
|
||||
|
||||
tab.id = id
|
||||
if (name) { tab.name = name }
|
||||
if (query) { tab.query = query }
|
||||
if (chart) { tab.chart = chart }
|
||||
if (isUnsaved !== undefined) { tab.isUnsaved = isUnsaved }
|
||||
delete tab.isPredefined
|
||||
|
||||
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)
|
||||
@@ -46,6 +61,7 @@ export default new Vuex.Store({
|
||||
state.currentTabId = state.tabs[index - 1].id
|
||||
} else {
|
||||
state.currentTabId = null
|
||||
state.currentTab = null
|
||||
state.untitledLastIndex = 0
|
||||
}
|
||||
state.tabs.splice(index, 1)
|
||||
@@ -56,12 +72,14 @@ export default new Vuex.Store({
|
||||
setCurrentTab (state, tab) {
|
||||
state.currentTab = tab
|
||||
},
|
||||
updateUntitledLastIndex (state) {
|
||||
state.untitledLastIndex += 1
|
||||
updatePredefinedQueries (state, queries) {
|
||||
if (Array.isArray(queries)) {
|
||||
state.predefinedQueries = queries
|
||||
} else {
|
||||
state.predefinedQueries = [queries]
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
},
|
||||
modules: {
|
||||
}
|
||||
})
|
||||
|
||||
@@ -16,47 +16,22 @@
|
||||
</div>
|
||||
</label>
|
||||
<div id="error" class="error"></div>
|
||||
<button id ="skip" class="secondary" @click="$router.push('/editor')">
|
||||
Skip database connection for now
|
||||
</button>
|
||||
</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)
|
||||
this.$db.loadDb(this.$refs.file.files[0])
|
||||
},
|
||||
dragover (event) {
|
||||
event.preventDefault()
|
||||
// TODO: Add some visual fluff to show the user can drop its files
|
||||
// TODO: Add some visual stuff 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')
|
||||
@@ -98,17 +73,21 @@ label {
|
||||
border-radius: var(--border-radius-big);
|
||||
}
|
||||
#drop-area {
|
||||
width: 231px;
|
||||
height: 153px;
|
||||
width: 628px;
|
||||
height: 490px;
|
||||
background-color: var(--color-bg-light-3);
|
||||
border-radius: var(--border-radius-big);
|
||||
color: var(--color-text-base);
|
||||
font-size: 13px;
|
||||
padding: 44px 15px;
|
||||
padding: 200px 144px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
#skip {
|
||||
position: absolute;
|
||||
bottom: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,18 +5,18 @@
|
||||
:before="{ size: 20, max: 30 }"
|
||||
:after="{ size: 80, max: 100 }"
|
||||
>
|
||||
<div slot="left-pane">
|
||||
<template #left-pane>
|
||||
<schema />
|
||||
</div>
|
||||
<div slot="right-pane">
|
||||
</template>
|
||||
<template #right-pane>
|
||||
<tabs />
|
||||
</div>
|
||||
</template>
|
||||
</splitpanes>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Splitpanes from '@/components/splitpanes'
|
||||
import Splitpanes from '@/components/Splitpanes'
|
||||
import Schema from '@/components/Schema'
|
||||
import Tabs from '@/components/Tabs'
|
||||
|
||||
|
||||
@@ -13,7 +13,35 @@ import '@/assets/styles/scrollbars.css'
|
||||
|
||||
export default {
|
||||
name: 'MainView',
|
||||
components: { MainMenu }
|
||||
components: { MainMenu },
|
||||
created () {
|
||||
this.readPredefinedQueries()
|
||||
.then(queries => {
|
||||
this.$store.commit('updatePredefinedQueries', queries)
|
||||
})
|
||||
.catch(console.error)
|
||||
},
|
||||
methods: {
|
||||
readPredefinedQueries () {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('GET', './queries.json')
|
||||
xhr.onload = () => {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status === 200) {
|
||||
resolve(JSON.parse(xhr.responseText))
|
||||
} else {
|
||||
reject(xhr.statusText)
|
||||
}
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => {
|
||||
reject(xhr.statusText)
|
||||
}
|
||||
xhr.send()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
@@ -3,19 +3,43 @@
|
||||
<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>
|
||||
<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="selectedQueriesCount > 0"
|
||||
@click="exportQuery(selectedQueriesIds)"
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
<button
|
||||
class="toolbar"
|
||||
v-show="selectedNotPredefinedCount > 0"
|
||||
@click="showDeleteDialog(selectedQueriesIds)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<div id="toolbar-search">
|
||||
<input type="text" placeholder="Search query by name"/>
|
||||
<text-field placeholder="Search query by name" width="300px" v-model="filter"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-bg">
|
||||
<div class="header-container">
|
||||
<div>
|
||||
<div class="fixed-header" ref="name-th">
|
||||
Name
|
||||
<check-box ref="mainCheckBox" theme="light" @click="toggleSelectAll"/>
|
||||
<div class="name-th">Name</div>
|
||||
</div>
|
||||
<div class="fixed-header">
|
||||
Created at
|
||||
@@ -28,18 +52,41 @@
|
||||
>
|
||||
<table ref="table">
|
||||
<tbody>
|
||||
<tr v-for="(query, index) in queries" :key="query.id" @click="openQuery(index)">
|
||||
<tr
|
||||
v-for="(query, index) in showedQueries"
|
||||
:key="query.id"
|
||||
:class="{ 'predefined': query.isPredefined }"
|
||||
@click="openQuery(index)"
|
||||
>
|
||||
<td ref="name-td">
|
||||
{{ query.name }}
|
||||
<div class="cell-data">
|
||||
<check-box
|
||||
ref="rowCheckBox"
|
||||
:init="selectAll || selectedQueriesIds.has(query.id)"
|
||||
@click="toggleRow($event, query.id)"
|
||||
/>
|
||||
<div class="name">{{ query.name }}</div>
|
||||
<div
|
||||
class="badge"
|
||||
@mouseover="showTooltip"
|
||||
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
|
||||
>
|
||||
Predefined
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Predefined queries come from the server.
|
||||
These queries can’t be deleted or renamed.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</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 />
|
||||
<rename-icon v-if="!query.isPredefined" @click="showRenameDialog(query.id)" />
|
||||
<copy-icon @click="duplicateQuery(index)"/>
|
||||
<export-icon @click="exportQuery(index)"/>
|
||||
<delete-icon v-if="!query.isPredefined" @click="showDeleteDialog(query.id)"/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -49,14 +96,72 @@
|
||||
</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 {{ deleteGroup ? 'queries' : 'query' }}
|
||||
<close-icon @click="$modal.hide('delete')"/>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
deleteGroup || (
|
||||
currentQueryIndex !== null
|
||||
&& currentQueryIndex >= 0
|
||||
&& currentQueryIndex < queries.length
|
||||
)
|
||||
"
|
||||
class="dialog-body"
|
||||
>
|
||||
Are you sure you want to delete
|
||||
{{ deleteGroup
|
||||
? `${selectedNotPredefinedCount} ${selectedNotPredefinedCount > 1 ? 'queries' : 'query'}`
|
||||
: `"${queries[currentQueryIndex].name}"`
|
||||
}}?
|
||||
<div v-show="selectedQueriesCount > selectedNotPredefinedCount" id="note">
|
||||
<img :src="require('@/assets/images/info.svg')">
|
||||
Note: Predefined queries you've selected won't be deleted
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button class="secondary" @click="$modal.hide('delete')">Cancel</button>
|
||||
<button class="primary" @click="deleteQuery">Delete</button>
|
||||
</div>
|
||||
</modal>
|
||||
<a ref="downloader" />
|
||||
</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'
|
||||
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 CheckBox from '@/components/CheckBox'
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
export default {
|
||||
name: 'MyQueries',
|
||||
@@ -64,20 +169,64 @@ export default {
|
||||
RenameIcon,
|
||||
CopyIcon,
|
||||
ExportIcon,
|
||||
DeleteIcon
|
||||
DeleteIcon,
|
||||
CloseIcon,
|
||||
TextField,
|
||||
CheckBox
|
||||
},
|
||||
mixins: [tooltipMixin],
|
||||
data () {
|
||||
return {
|
||||
queries: []
|
||||
queries: [],
|
||||
filter: null,
|
||||
newName: null,
|
||||
currentQueryId: null,
|
||||
errorMsg: null,
|
||||
selectedQueriesIds: new Set(),
|
||||
selectedQueriesCount: 0,
|
||||
selectedNotPredefinedCount: 0,
|
||||
selectAll: false,
|
||||
deleteGroup: false,
|
||||
resizeObserver: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
predefinedQueries () {
|
||||
return this.$store.state.predefinedQueries.map(query => {
|
||||
query.isPredefined = true
|
||||
return query
|
||||
})
|
||||
},
|
||||
predefinedQueriesIds () {
|
||||
return new Set(this.predefinedQueries.map(query => query.id))
|
||||
},
|
||||
showedQueries () {
|
||||
let showedQueries = this.allQueries
|
||||
if (this.filter) {
|
||||
showedQueries = showedQueries.filter(
|
||||
query => query.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0
|
||||
)
|
||||
}
|
||||
return showedQueries
|
||||
},
|
||||
allQueries () {
|
||||
return this.predefinedQueries.concat(this.queries)
|
||||
},
|
||||
currentQueryIndex () {
|
||||
return this.queries.findIndex(query => query.id === this.currentQueryId)
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.queries = JSON.parse(localStorage.getItem('myQueries'))
|
||||
this.queries = JSON.parse(localStorage.getItem('myQueries')) || []
|
||||
},
|
||||
mounted () {
|
||||
new ResizeObserver(this.calcNameWidth).observe(this.$refs.table)
|
||||
this.resizeObserver = new ResizeObserver(this.calcNameWidth)
|
||||
this.resizeObserver.observe(this.$refs.table)
|
||||
this.calcNameWidth()
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.resizeObserver.unobserve(this.$refs.table)
|
||||
},
|
||||
filters: {
|
||||
date (value) {
|
||||
if (!value) {
|
||||
@@ -98,11 +247,181 @@ export default {
|
||||
this.$refs['name-th'].style = `width: ${this.$refs['name-td'][0].offsetWidth}px`
|
||||
},
|
||||
openQuery (index) {
|
||||
const tab = this.queries[index]
|
||||
const tab = JSON.parse(JSON.stringify(this.showedQueries[index]))
|
||||
tab.isUnsaved = false
|
||||
this.$store.commit('addTab', tab)
|
||||
this.$store.commit('setCurrentTabId', tab.id)
|
||||
this.$router.push('/editor')
|
||||
},
|
||||
showRenameDialog (id) {
|
||||
this.errorMsg = null
|
||||
this.currentQueryId = id
|
||||
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)
|
||||
|
||||
// update queries in local storage
|
||||
this.saveQueriesInLocalStorage()
|
||||
|
||||
// update tab, if renamed query is opened
|
||||
const tabIndex = this.findTabIndex(currentQuery.id)
|
||||
if (tabIndex >= 0) {
|
||||
this.$store.commit('updateTab', {
|
||||
index: tabIndex,
|
||||
name: this.newName,
|
||||
id: currentQuery.id
|
||||
})
|
||||
}
|
||||
// hide dialog
|
||||
this.$modal.hide('rename')
|
||||
},
|
||||
duplicateQuery (index) {
|
||||
const newQuery = JSON.parse(JSON.stringify(this.showedQueries[index]))
|
||||
newQuery.name = newQuery.name + ' Copy'
|
||||
newQuery.id = nanoid()
|
||||
newQuery.createdAt = new Date()
|
||||
delete newQuery.isPredefined
|
||||
if (this.selectAll) {
|
||||
this.selectedQueriesIds.add(newQuery.id)
|
||||
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||
}
|
||||
this.queries.push(newQuery)
|
||||
this.saveQueriesInLocalStorage()
|
||||
},
|
||||
showDeleteDialog (id) {
|
||||
this.deleteGroup = typeof id !== 'string'
|
||||
if (!this.deleteGroup) {
|
||||
this.currentQueryId = id
|
||||
}
|
||||
this.$modal.show('delete')
|
||||
},
|
||||
deleteQuery () {
|
||||
this.$modal.hide('delete')
|
||||
if (!this.deleteGroup) {
|
||||
this.queries.splice(this.currentQueryIndex, 1)
|
||||
const tabIndex = this.findTabIndex(this.currentQueryId)
|
||||
if (tabIndex >= 0) {
|
||||
this.$store.commit('deleteTab', tabIndex)
|
||||
}
|
||||
if (this.selectedQueriesIds.has(this.currentQueryId)) {
|
||||
this.selectedQueriesIds.delete(this.currentQueryId)
|
||||
}
|
||||
} else {
|
||||
this.queries = this.selectAll
|
||||
? []
|
||||
: this.queries.filter(query => !this.selectedQueriesIds.has(query.id))
|
||||
const tabs = this.$store.state.tabs
|
||||
for (let i = tabs.length - 1; i >= 0; i--) {
|
||||
if (this.selectedQueriesIds.has(tabs[i].id)) {
|
||||
this.$store.commit('deleteTab', i)
|
||||
}
|
||||
}
|
||||
this.selectedQueriesIds.clear()
|
||||
}
|
||||
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||
this.saveQueriesInLocalStorage()
|
||||
},
|
||||
findTabIndex (id) {
|
||||
return this.$store.state.tabs.findIndex(tab => tab.id === id)
|
||||
},
|
||||
exportQuery (index) {
|
||||
let data
|
||||
let name
|
||||
|
||||
// single operation
|
||||
if (typeof index === 'number') {
|
||||
data = JSON.parse(JSON.stringify(this.showedQueries[index]))
|
||||
name = data.name
|
||||
delete data.isPredefined
|
||||
} else {
|
||||
// group operation
|
||||
data = this.selectAll
|
||||
? JSON.parse(JSON.stringify(this.allQueries))
|
||||
: this.allQueries.filter(query => this.selectedQueriesIds.has(query.id))
|
||||
name = 'My sqliteviz queries'
|
||||
data.forEach(query => delete query.isPredefined)
|
||||
}
|
||||
|
||||
// export data to file
|
||||
const downloader = this.$refs.downloader
|
||||
const json = JSON.stringify(data, null, 4)
|
||||
const blob = new Blob([json], { type: 'octet/stream' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
downloader.href = url
|
||||
downloader.download = `${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 => {
|
||||
const allQueriesIds = this.allQueries.map(query => query.id)
|
||||
if (new Set(allQueriesIds).has(query.id)) {
|
||||
query.id = nanoid()
|
||||
}
|
||||
})
|
||||
|
||||
if (this.selectAll) {
|
||||
importedQueries.forEach(query => {
|
||||
this.selectedQueriesIds.add(query.id)
|
||||
})
|
||||
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||
}
|
||||
|
||||
this.queries = this.queries.concat(importedQueries)
|
||||
this.saveQueriesInLocalStorage()
|
||||
this.$refs.importFile.value = null
|
||||
}
|
||||
reader.readAsText(file)
|
||||
},
|
||||
saveQueriesInLocalStorage () {
|
||||
localStorage.setItem('myQueries', JSON.stringify(this.queries))
|
||||
},
|
||||
toggleSelectAll (checked) {
|
||||
this.selectAll = checked
|
||||
this.$refs.rowCheckBox.forEach(item => { item.checked = checked })
|
||||
|
||||
this.selectedQueriesIds = checked
|
||||
? new Set(this.allQueries.map(query => query.id))
|
||||
: new Set()
|
||||
|
||||
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||
this.selectedNotPredefinedCount = checked ? this.queries.length : 0
|
||||
},
|
||||
toggleRow (checked, id) {
|
||||
const isPredefined = this.predefinedQueriesIds.has(id)
|
||||
if (checked) {
|
||||
this.selectedQueriesIds.add(id)
|
||||
if (!isPredefined) {
|
||||
this.selectedNotPredefinedCount += 1
|
||||
}
|
||||
} else {
|
||||
if (this.selectedQueriesIds.size === this.allQueries.length) {
|
||||
this.$refs.mainCheckBox.checked = false
|
||||
this.selectAll = false
|
||||
}
|
||||
this.selectedQueriesIds.delete(id)
|
||||
if (!isPredefined) {
|
||||
this.selectedNotPredefinedCount -= 1
|
||||
}
|
||||
}
|
||||
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,32 +435,55 @@ export default {
|
||||
#my-queries-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 18px;
|
||||
margin: 0 auto 8px;
|
||||
max-width: 1500px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rounded-bg,
|
||||
#my-queries-toolbar {
|
||||
.rounded-bg {
|
||||
margin: 0 auto;
|
||||
max-width: 1500px;
|
||||
width: 100%;
|
||||
}
|
||||
.fixed-header:first-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 12px;
|
||||
}
|
||||
.fixed-header:first-child .name-th {
|
||||
margin-left: 24px;
|
||||
}
|
||||
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;
|
||||
padding: 0 12px;
|
||||
}
|
||||
tbody tr td:last-child {
|
||||
width: 30%;
|
||||
max-width: 0;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
tbody .cell-data {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
tbody .cell-data div.name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
tbody tr:hover td {
|
||||
@@ -162,6 +504,7 @@ tbody tr:hover td {
|
||||
|
||||
.icons-container {
|
||||
display: none;
|
||||
margin-right: -12px;
|
||||
}
|
||||
.date-container {
|
||||
flex-shrink: 1;
|
||||
@@ -172,4 +515,51 @@ tbody tr:hover td {
|
||||
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;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: none;
|
||||
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;
|
||||
line-height: normal;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
tbody tr.predefined:hover .badge {
|
||||
display: block;
|
||||
}
|
||||
#note {
|
||||
margin-top: 24px;
|
||||
}
|
||||
#note img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.icon-tooltip {
|
||||
display: block;
|
||||
width: 149px;
|
||||
white-space: normal;
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
padding: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user