Added table tab

This commit is contained in:
Fabio 2020-06-12 18:10:45 +02:00
parent 29812f7ee3
commit 68b128c550
13 changed files with 309 additions and 138 deletions

30
package-lock.json generated
View File

@ -10931,11 +10931,6 @@
"ajv-keywords": "^3.4.1" "ajv-keywords": "^3.4.1"
} }
}, },
"scrollparent": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/scrollparent/-/scrollparent-2.0.1.tgz",
"integrity": "sha1-cV1bnMV3YPsivczDvvtb/gaxoxc="
},
"scss-tokenizer": { "scss-tokenizer": {
"version": "0.2.3", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz",
@ -12848,16 +12843,6 @@
"vue-style-loader": "^4.1.0" "vue-style-loader": "^4.1.0"
} }
}, },
"vue-observe-visibility": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/vue-observe-visibility/-/vue-observe-visibility-0.4.6.tgz",
"integrity": "sha512-xo0CEVdkjSjhJoDdLSvoZoQrw/H2BlzB5jrCBKGZNXN2zdZgMuZ9BKrxXDjNP2AxlcCoKc8OahI3F3r3JGLv2Q=="
},
"vue-resize": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-0.4.5.tgz",
"integrity": "sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg=="
},
"vue-style-loader": { "vue-style-loader": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz", "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz",
@ -12884,21 +12869,6 @@
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true "dev": true
}, },
"vue-virtual-scroll-list": {
"version": "2.2.9",
"resolved": "https://registry.npmjs.org/vue-virtual-scroll-list/-/vue-virtual-scroll-list-2.2.9.tgz",
"integrity": "sha512-dPlvzIUUtxkaVBVea2/73sWsiTrsIpjWXd+7FWJPUEL+ME1i6LuwWNiMMqu2WVad82ONWeoXSiM5NMSDpMxYGA=="
},
"vue-virtual-scroller": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/vue-virtual-scroller/-/vue-virtual-scroller-1.0.10.tgz",
"integrity": "sha512-Hn4qSBDhRY4XdngPioYy/ykDjrLX/NMm1fQXm/4UQQ/Xv1x8JbHGFZNftQowTcfICgN7yc31AKnUk1UGLJ2ndA==",
"requires": {
"scrollparent": "^2.0.1",
"vue-observe-visibility": "^0.4.4",
"vue-resize": "^0.4.5"
}
},
"vuedraggable": { "vuedraggable": {
"version": "2.23.2", "version": "2.23.2",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.23.2.tgz", "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.23.2.tgz",

View File

@ -41,8 +41,6 @@
"spectre.css": "^0.5.8", "spectre.css": "^0.5.8",
"vue-click-outside": "^1.1.0", "vue-click-outside": "^1.1.0",
"vue-i18n": "^8.18.2", "vue-i18n": "^8.18.2",
"vue-virtual-scroll-list": "^2.2.9",
"vue-virtual-scroller": "^1.0.10",
"vuedraggable": "^2.23.2", "vuedraggable": "^2.23.2",
"vuex": "^3.4.0", "vuex": "^3.4.0",
"vuex-persist": "^2.2.0" "vuex-persist": "^2.2.0"

View File

@ -7,6 +7,17 @@ export class AntaresConnector {
this.params = args.params; this.params = args.params;
this.poolSize = args.poolSize || false; this.poolSize = args.poolSize || false;
this.connection = null; this.connection = null;
this.query = {
select: [],
from: '',
where: [],
groupBy: [],
orderBy: [],
join: [],
update: [],
insert: []
};
} }
connect () { connect () {
@ -28,6 +39,20 @@ export class AntaresConnector {
} }
} }
// select (args) {
// const type = typeof args;
// switch (type) {
// case string:
// case number:
// this.query.select;
// break;
// default:
// break;
// }
// }
async raw (sql) { async raw (sql) {
switch (this.client) { switch (this.client) {
case 'maria': case 'maria':

View File

@ -47,7 +47,7 @@ export default {
}, },
methods: { methods: {
checkScrollPosition (e = {}) { checkScrollPosition (e = {}) {
var el = this.$el; const el = this.$el;
// prevent parent scroll // prevent parent scroll
if ((el.scrollTop === 0 && e.deltaY < 0) || (Math.abs(el.scrollTop - (el.scrollHeight - el.clientHeight)) <= 1 && e.deltaY > 0)) if ((el.scrollTop === 0 && e.deltaY < 0) || (Math.abs(el.scrollTop - (el.scrollHeight - el.clientHeight)) <= 1 && e.deltaY > 0))

View File

@ -4,15 +4,37 @@
<div v-if="workspace.connected" class="workspace-tabs column columns col-gapless"> <div v-if="workspace.connected" class="workspace-tabs column columns col-gapless">
<ul class="tab tab-block column col-12"> <ul class="tab tab-block column col-12">
<li <li
v-for="(tab, key) of workspace.tabs" v-if="workspace.breadcrumbs.table"
class="tab-item"
:class="{'active': selectedTab === 1}"
@click="selectTab({uid: workspace.uid, tab: 1})"
>
<a class="tab-link">
<i class="material-icons md-18 mr-1">grid_on</i>
<span :title="workspace.breadcrumbs.table">{{ workspace.breadcrumbs.table }}</span>
</a>
</li>
<li
v-for="(tab, key) of queryTabs"
:key="tab.uid" :key="tab.uid"
class="tab-item" class="tab-item"
:class="{'active': selectedTab === tab.uid}" :class="{'active': selectedTab === tab.uid}"
@click="selectTab({uid: workspace.uid, tab: tab.uid})"
> >
<a href="#">Query #{{ key }} <span v-if="workspace.tabs.length > 1" class="btn btn-clear" /></a> <a><span>Query #{{ key+1 }} <span v-if="queryTabs.length > 1" class="btn btn-clear" /></span></a>
</li> </li>
</ul> </ul>
<WorkspaceQueryTab :connection="connection" /> <WorkspaceTableTab
v-show="selectedTab === 1"
:connection="connection"
:table="workspace.breadcrumbs.table"
/>
<WorkspaceQueryTab
v-for="tab of queryTabs"
v-show="selectedTab === tab.uid"
:key="tab.uid"
:connection="connection"
/>
</div> </div>
</div> </div>
</template> </template>
@ -22,12 +44,14 @@ import { mapGetters, mapActions } from 'vuex';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import WorkspaceExploreBar from '@/components/WorkspaceExploreBar'; import WorkspaceExploreBar from '@/components/WorkspaceExploreBar';
import WorkspaceQueryTab from '@/components/WorkspaceQueryTab'; import WorkspaceQueryTab from '@/components/WorkspaceQueryTab';
import WorkspaceTableTab from '@/components/WorkspaceTableTab';
export default { export default {
name: 'Workspace', name: 'Workspace',
components: { components: {
WorkspaceExploreBar, WorkspaceExploreBar,
WorkspaceQueryTab WorkspaceQueryTab,
WorkspaceTableTab
}, },
props: { props: {
connection: Object connection: Object
@ -44,7 +68,10 @@ export default {
return this.selectedWorkspace === this.connection.uid; return this.selectedWorkspace === this.connection.uid;
}, },
selectedTab () { selectedTab () {
return this.workspace.tabs.filter(tab => tab.selected).uid || this.workspace.tabs[0].uid; return this.workspace.selected_tab || this.queryTabs[0].uid;
},
queryTabs () {
return this.workspace.tabs.filter(tab => tab.type === 'query');
} }
}, },
async created () { async created () {
@ -58,7 +85,8 @@ export default {
addNotification: 'notifications/addNotification', addNotification: 'notifications/addNotification',
addWorkspace: 'workspaces/addWorkspace', addWorkspace: 'workspaces/addWorkspace',
connectWorkspace: 'workspaces/connectWorkspace', connectWorkspace: 'workspaces/connectWorkspace',
removeConnected: 'workspaces/removeConnected' removeConnected: 'workspaces/removeConnected',
selectTab: 'workspaces/selectTab'
}) })
} }
}; };
@ -78,7 +106,71 @@ export default {
margin-top: 0; margin-top: 0;
.tab-item{ .tab-item{
max-width: 6rem; max-width: 12rem;
width: fit-content;
flex: initial;
&.active a{
opacity: 1;
}
> a{
padding: .2rem .8rem;
color: $body-font-color;
cursor: pointer;
display: flex;
align-items: center;
opacity: .7;
transition: opacity .2s;
&:hover{
opacity: 1;
}
> span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
}
}
.workspace-query-results{
overflow: auto;
white-space: nowrap;
.table{
width: auto;
border-collapse: separate;
.tr:focus{
background: rgba($color: #000000, $alpha: .3);
}
.th{
position: sticky;
top: 0;
background: $bg-color;
border: 1px solid;
border-left: none;
border-color: $bg-color-light;
padding: .1rem .4rem;
font-weight: 700;
font-size: .7rem;
}
.td{
border-right: 1px solid;
border-bottom: 1px solid;
border-color: $bg-color-light;
padding: 0 .4rem;
text-overflow: ellipsis;
max-width: 200px;
white-space: nowrap;
overflow: hidden;
font-size: .7rem;
} }
} }
} }

View File

@ -161,7 +161,7 @@ export default {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
display: flex; display: block;
align-items: center; align-items: center;
} }

View File

@ -1,43 +0,0 @@
<template>
<div>
<div
v-for="(col, cKey) in source"
:key="cKey"
class="td"
:class="fieldType(col)"
>
{{ col }}
</div>
</div>
</template>
<script>
export default {
name: 'WorkspaceQueryRow',
props: {
index: { // index of current item
type: Number
},
source: { // here is: {uid: 'unique_1', text: 'abc'}
type: Object,
default () {
return {};
}
}
},
methods: {
fieldType (col) {
let type = typeof col;
if (type === 'object')
if (col instanceof Date) type = 'date';
if (col instanceof Uint8Array) type = 'blob';
if (col === null) type = 'null';
return `type-${type}`;
}
}
};
</script>
<style>
</style>

View File

@ -17,8 +17,13 @@
<i class="material-icons ml-1">save</i> <i class="material-icons ml-1">save</i>
</button> </button>
</div> </div>
<div v-if="workspace.breadcrumbs.database"> <div class="workspace-query-info">
{{ $t('word.schema') }}: <b>{{ workspace.breadcrumbs.database }}</b> <div v-if="results.rows">
{{ $t('word.results') }}: <b>{{ results.rows.length }}</b>
</div>
<div v-if="workspace.breadcrumbs.database">
{{ $t('word.schema') }}: <b>{{ workspace.breadcrumbs.database }}</b>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -112,40 +117,12 @@ export default {
margin-right: .4rem; margin-right: .4rem;
} }
} }
}
}
.workspace-query-results{ .workspace-query-info{
overflow: auto; display: flex;
white-space: nowrap;
.table{ > div + div{
width: auto; padding-left: .6rem;
.tr:focus{
background: rgba($color: #000000, $alpha: .3);
}
.th{
position: sticky;
top: 0;
background: $bg-color;
border-color: $bg-color-light;
padding: .1rem .4rem;
font-weight: 400;
}
.td{
border-left: 1px solid;
border-color: $bg-color-light;
padding: 0 .4rem;
text-overflow: ellipsis;
max-width: 200px;
white-space: nowrap;
overflow: hidden;
&:first-child{
border-left: none;
} }
} }
} }

View File

@ -2,7 +2,7 @@
<BaseVirtualScroll <BaseVirtualScroll
v-if="results.rows" v-if="results.rows"
ref="resultTable" ref="resultTable"
:items="results.rows" :items="rows"
:item-height="25" :item-height="25"
class="vscroll" class="vscroll"
:style="{'height': resultsSize+'px'}" :style="{'height': resultsSize+'px'}"
@ -22,8 +22,8 @@
</div> </div>
<div class="tbody"> <div class="tbody">
<div <div
v-for="(row, rKey) in items" v-for="row in items"
:key="rKey" :key="row._id"
class="tr" class="tr"
tabindex="0" tabindex="0"
> >
@ -32,6 +32,7 @@
:key="cKey" :key="cKey"
class="td" class="td"
:class="fieldType(col)" :class="fieldType(col)"
:style="{'display': cKey === '_id'? 'none' : ''}"
> >
{{ col }} {{ col }}
</div> </div>
@ -43,6 +44,7 @@
</template> </template>
<script> <script>
import { uidGen } from 'common/libs/utilities';
import BaseVirtualScroll from '@/components/BaseVirtualScroll'; import BaseVirtualScroll from '@/components/BaseVirtualScroll';
export default { export default {
@ -59,15 +61,15 @@ export default {
}; };
}, },
computed: { computed: {
rows () { rows () { // Adds uid to rows
return this.results.rows ? this.results.rows.map(item => Object.assign({}, item)) : []; return this.results.rows ? this.results.rows.map(item => {
return { ...item, _id: uidGen() };
}) : [];
} }
}, },
watch: { updated () {
results: function () { if (this.$refs.resultTable)
if (this.$refs.resultTable) this.resizeResults();
this.resizeResults();
}
}, },
mounted () { mounted () {
window.addEventListener('resize', this.resizeResults); window.addEventListener('resize', this.resizeResults);
@ -86,12 +88,14 @@ export default {
return `type-${type}`; return `type-${type}`;
}, },
resizeResults (e) { resizeResults (e) {
const el = this.$refs.resultTable.$el; if (this.$refs.resultTable) {
const footer = document.getElementById('footer'); const el = this.$refs.resultTable.$el;
const footer = document.getElementById('footer');
if (el) { if (el) {
const size = window.innerHeight - el.getBoundingClientRect().top - footer.offsetHeight; const size = window.innerHeight - el.getBoundingClientRect().top - footer.offsetHeight;
this.resultsSize = size; this.resultsSize = size;
}
} }
} }
} }

View File

@ -0,0 +1,140 @@
<template>
<div class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-link btn-sm"
:class="{'loading':isQuering}"
@click="runQuery"
>
<span>{{ $t('word.refresh') }}</span>
<i class="material-icons ml-1">refresh</i>
</button>
<button class="btn btn-link btn-sm">
<span>{{ $t('word.save') }}</span>
<i class="material-icons ml-1">save</i>
</button>
</div>
<div class="workspace-query-info">
<div v-if="results.rows">
{{ $t('word.results') }}: <b>{{ results.rows.length }}</b>
</div>
<div v-if="workspace.breadcrumbs.database">
{{ $t('word.schema') }}: <b>{{ workspace.breadcrumbs.database }}</b>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12">
<WorkspaceQueryTable v-if="results" :results="results" />
</div>
</div>
</template>
<script>
import Connection from '@/ipc-api/Connection';
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
import { mapGetters, mapActions } from 'vuex';
export default {
name: 'WorkspaceQueryTab',
components: {
WorkspaceQueryTable
},
props: {
connection: Object,
table: String
},
data () {
return {
isQuering: false,
results: {}
};
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
query () {
return `SELECT * FROM \`${this.table}\` LIMIT 1000`;// TODO: use query builder
}
},
watch: {
table: function () {
this.runQuery();
}
},
created () {
this.runQuery();
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
async runQuery () {
if (!this.table) return;
this.isQuering = true;
this.results = {};
const params = {
uid: this.connection.uid,
query: this.query,
database: this.workspace.breadcrumbs.database
};
try {
const { status, response } = await Connection.rawQuery(params);
if (status === 'success')
this.results = response;
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
}
}
};
</script>
<style lang="scss">
.workspace-tabs{
align-content: baseline;
.workspace-query-runner{
.workspace-query-runner-footer{
display: flex;
justify-content: space-between;
padding: .3rem .6rem .4rem;
align-items: center;
.workspace-query-buttons{
display: flex;
.btn{
display: flex;
align-self: center;
color: $body-font-color;
margin-right: .4rem;
}
}
.workspace-query-info{
display: flex;
> div + div{
padding-left: .6rem;
}
}
}
}
}
</style>

View File

@ -28,7 +28,8 @@ module.exports = {
version: 'Version', version: 'Version',
donate: 'Donate', donate: 'Donate',
run: 'Run', run: 'Run',
schema: 'Schema' schema: 'Schema',
results: 'Results'
}, },
message: { message: {
appWelcome: 'Welcome to Antares SQL Client!', appWelcome: 'Welcome to Antares SQL Client!',

View File

@ -2,7 +2,6 @@
import Vue from 'vue'; import Vue from 'vue';
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
import 'material-design-icons/iconfont/material-icons.css'; import 'material-design-icons/iconfont/material-icons.css';
import '@/scss/main.scss'; import '@/scss/main.scss';

View File

@ -59,7 +59,8 @@ export default {
NEW_TAB (state, uid) { NEW_TAB (state, uid) {
const newTab = { const newTab = {
uid: uidGen(), uid: uidGen(),
selected: false selected: false,
type: 'query'
}; };
state.workspaces = state.workspaces.map(workspace => { state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === uid) { if (workspace.uid === uid) {
@ -71,6 +72,9 @@ export default {
else else
return workspace; return workspace;
}); });
},
SELECT_TAB (state, { uid, tab }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, selected_tab: tab } : workspace);
} }
}, },
actions: { actions: {
@ -109,14 +113,15 @@ export default {
const workspace = { const workspace = {
uid, uid,
connected: false, connected: false,
tabs: [], selectedTab: 0,
tabs: [{ uid: 1, type: 'table' }],
structure: {}, structure: {},
breadcrumbs: {} breadcrumbs: {}
}; };
commit('ADD_WORKSPACE', workspace); commit('ADD_WORKSPACE', workspace);
if (!getters.getWorkspace(uid).tabs.length) if (getters.getWorkspace(uid).tabs.length < 2)
dispatch('newTab', uid); dispatch('newTab', uid);
}, },
changeBreadcrumbs ({ commit, getters }, payload) { changeBreadcrumbs ({ commit, getters }, payload) {
@ -124,6 +129,9 @@ export default {
}, },
newTab ({ commit }, uid) { newTab ({ commit }, uid) {
commit('NEW_TAB', uid); commit('NEW_TAB', uid);
},
selectTab ({ commit }, payload) {
commit('SELECT_TAB', payload);
} }
} }
}; };