feat: index management

This commit is contained in:
Fabio Di Stasio 2020-12-01 16:48:20 +01:00
parent 8ebc3bce92
commit 41505bde65
14 changed files with 550 additions and 41 deletions

View File

@ -4,7 +4,7 @@
"version": "0.0.9", "version": "0.0.9",
"description": "A cross-platform easy to use SQL client.", "description": "A cross-platform easy to use SQL client.",
"license": "MIT", "license": "MIT",
"repository": "https://github.com/EStarium/antares.git", "repository": "https://github.com/Fabio286/antares.git",
"scripts": { "scripts": {
"dev": "cross-env NODE_ENV=development electron-webpack dev", "dev": "cross-env NODE_ENV=development electron-webpack dev",
"compile": "electron-webpack", "compile": "electron-webpack",
@ -17,7 +17,7 @@
}, },
"author": "Fabio Di Stasio <fabio286@gmail.com>", "author": "Fabio Di Stasio <fabio286@gmail.com>",
"build": { "build": {
"appId": "com.estarium.antares", "appId": "com.fabio286.antares",
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}", "artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
"dmg": { "dmg": {
"contents": [ "contents": [
@ -58,10 +58,10 @@
"pg": "^8.5.1", "pg": "^8.5.1",
"source-map-support": "^0.5.16", "source-map-support": "^0.5.16",
"spectre.css": "^0.5.9", "spectre.css": "^0.5.9",
"vue-i18n": "^8.22.1", "vue-i18n": "^8.22.2",
"vue-the-mask": "^0.11.1", "vue-the-mask": "^0.11.1",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",
"vuex": "^3.5.1", "vuex": "^3.6.0",
"vuex-persist": "^3.1.3" "vuex-persist": "^3.1.3"
}, },
"devDependencies": { "devDependencies": {
@ -72,8 +72,8 @@
"electron-devtools-installer": "^3.1.1", "electron-devtools-installer": "^3.1.1",
"electron-webpack": "^2.8.2", "electron-webpack": "^2.8.2",
"electron-webpack-vue": "^2.4.0", "electron-webpack-vue": "^2.4.0",
"eslint": "^7.13.0", "eslint": "^7.14.0",
"eslint-config-standard": "^16.0.1", "eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1", "eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.2.1",
@ -82,7 +82,7 @@
"node-sass": "^5.0.0", "node-sass": "^5.0.0",
"sass-loader": "^10.1.0", "sass-loader": "^10.1.0",
"standard-version": "^9.0.0", "standard-version": "^9.0.0",
"stylelint": "^13.7.2", "stylelint": "^13.8.0",
"stylelint-config-standard": "^20.0.0", "stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0", "stylelint-scss": "^3.18.0",
"vue": "^2.6.12", "vue": "^2.6.12",

View File

@ -0,0 +1,6 @@
module.exports = [
'PRIMARY',
'INDEX',
'UNIQUE',
'FULLTEXT'
];

View File

@ -325,9 +325,12 @@ export class MySQLClient extends AntaresCore {
additions, additions,
deletions, deletions,
changes, changes,
indexChanges,
options options
} = params; } = params;
console.log(params);
let sql = `ALTER TABLE \`${table}\` `; let sql = `ALTER TABLE \`${table}\` `;
const alterColumns = []; const alterColumns = [];
@ -337,7 +340,7 @@ export class MySQLClient extends AntaresCore {
if ('autoIncrement' in options) alterColumns.push(`AUTO_INCREMENT=${+options.autoIncrement}`); if ('autoIncrement' in options) alterColumns.push(`AUTO_INCREMENT=${+options.autoIncrement}`);
if ('collation' in options) alterColumns.push(`COLLATE='${options.collation}'`); if ('collation' in options) alterColumns.push(`COLLATE='${options.collation}'`);
// ADD // ADD FIELDS
additions.forEach(addition => { additions.forEach(addition => {
const length = addition.numLength || addition.charLength || addition.datePrecision; const length = addition.numLength || addition.charLength || addition.datePrecision;
@ -354,7 +357,22 @@ export class MySQLClient extends AntaresCore {
${addition.after ? `AFTER \`${addition.after}\`` : 'FIRST'}`); ${addition.after ? `AFTER \`${addition.after}\`` : 'FIRST'}`);
}); });
// CHANGE // ADD INDEX
indexChanges.additions.forEach(addition => {
const fields = addition.fields.map(field => `\`${field}\``).join(',');
let type = addition.type;
if (type === 'PRIMARY')
alterColumns.push(`ADD PRIMARY KEY (${fields})`);
else {
if (type === 'UNIQUE')
type = 'UNIQUE INDEX';
alterColumns.push(`ADD ${type} \`${addition.name}\` (${fields})`);
}
});
// CHANGE FIELDS
changes.forEach(change => { changes.forEach(change => {
const length = change.numLength || change.charLength || change.datePrecision; const length = change.numLength || change.charLength || change.datePrecision;
@ -371,11 +389,39 @@ export class MySQLClient extends AntaresCore {
${change.after ? `AFTER \`${change.after}\`` : 'FIRST'}`); ${change.after ? `AFTER \`${change.after}\`` : 'FIRST'}`);
}); });
// DROP // CHANGE INDEX
indexChanges.changes.forEach(change => {
if (change.oldType === 'PRIMARY')
alterColumns.push('DROP PRIMARY KEY');
else
alterColumns.push(`DROP INDEX \`${change.oldName}\``);
const fields = change.fields.map(field => `\`${field}\``).join(',');
let type = change.type;
if (type === 'PRIMARY')
alterColumns.push(`ADD PRIMARY KEY (${fields})`);
else {
if (type === 'UNIQUE')
type = 'UNIQUE INDEX';
alterColumns.push(`ADD ${type} \`${change.name}\` (${fields})`);
}
});
// DROP FIELDS
deletions.forEach(deletion => { deletions.forEach(deletion => {
alterColumns.push(`DROP COLUMN \`${deletion.name}\``); alterColumns.push(`DROP COLUMN \`${deletion.name}\``);
}); });
// DROP INDEX
indexChanges.deletions.forEach(deletion => {
if (deletion.type === 'PRIMARY')
alterColumns.push('DROP PRIMARY KEY');
else
alterColumns.push(`DROP INDEX \`${deletion.name}\``);
});
sql += alterColumns.join(', '); sql += alterColumns.join(', ');
// RENAME // RENAME

View File

@ -86,9 +86,8 @@ export default {
.context-container { .context-container {
min-width: 100px; min-width: 100px;
max-width: 150px;
z-index: 10; z-index: 10;
box-shadow: 0 0 1px 0 #000; box-shadow: 0 0 2px 0 #000;
padding: 0; padding: 0;
background: #1d1d1d; background: #1d1d1d;
border-radius: 0.1rem; border-radius: 0.1rem;
@ -103,9 +102,28 @@ export default {
padding: 0.1rem 0.3rem; padding: 0.1rem 0.3rem;
cursor: pointer; cursor: pointer;
justify-content: space-between; justify-content: space-between;
position: relative;
.context-submenu {
opacity: 0;
visibility: hidden;
transition: opacity 0.2s;
position: absolute;
left: 100%;
top: 0;
background: #1d1d1d;
box-shadow: 0 0 2px 0 #000;
min-width: 100px;
}
&:hover { &:hover {
background: $primary-color; background: $primary-color;
.context-submenu {
display: block;
visibility: visible;
opacity: 1;
}
} }
} }
} }

View File

@ -6,6 +6,11 @@
<div class="context-element"> <div class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-plus text-light pr-1" /> {{ $t('word.add') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-plus text-light pr-1" /> {{ $t('word.add') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" /> <i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu">
<div class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-table text-light pr-1" /> {{ $t('word.table') }}</span>
</div>
</div>
</div> </div>
<div class="context-element" @click="showEditModal"> <div class="context-element" @click="showEditModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-pencil text-light pr-1" /> {{ $t('word.edit') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-pencil text-light pr-1" /> {{ $t('word.edit') }}</span>

View File

@ -0,0 +1,263 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
@confirm="confirmIndexesChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-key mdi-rotate-45 mr-1" /> {{ $t('word.indexes') }} "{{ table }}"
</div>
</template>
<div :slot="'body'">
<div class="columns col-gapless">
<div class="column col-5">
<div class="panel" :style="{ height: modalInnerHeight + 'px'}">
<div class="panel-header pt-0 pl-0">
<div class="d-flex">
<button class="btn btn-dark btn-sm d-flex" @click="addIndex">
<span>{{ $t('word.add') }}</span>
<i class="mdi mdi-24px mdi-key-plus ml-1" />
</button>
<button
class="btn btn-dark btn-sm d-flex ml-2 mr-0"
:title="$t('message.clearChanges')"
:disabled="!isChanged"
@click.prevent="clearChanges"
>
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
</div>
</div>
<div ref="indexesPanel" class="panel-body p-0 pr-1">
<div
v-for="index in indexesProxy"
:key="index._id"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-index': selectedIndexID === index._id}"
@click="selectIndex($event, index._id)"
>
<div class="tile-icon">
<div>
<i class="mdi mdi-key mdi-24px column-key" :class="`key-${index.type}`" />
</div>
</div>
<div class="tile-content">
<div class="tile-title">
{{ index.name }}
</div>
<small class="tile-subtitle text-gray">{{ index.type }} · {{ index.fields.length }} {{ $tc('word.field', index.fields.length) }}</small>
</div>
<div class="tile-action">
<button
class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')"
@click.prevent="removeIndex(index._id)"
>
<i class="mdi mdi-close" />
</button>
</div>
</div>
</div>
</div>
</div>
<div class="column col-7 pl-2 editor-col">
<form v-if="selectedIndexObj" :style="{ height: modalInnerHeight + 'px'}">
<div class="form-group">
<label class="form-label">
{{ $t('word.name') }}
</label>
<input
v-model="selectedIndexObj.name"
class="form-input"
type="text"
>
</div>
<div class="form-group">
<label class="form-label">
{{ $t('word.type') }}
</label>
<select v-model="selectedIndexObj.type" class="form-select">
<option
v-for="index in indexTypes"
:key="index"
:value="index"
:disabled="index === 'PRIMARY' && hasPrimary"
>
{{ index }}
</option>
</select>
</div>
<div class="form-group">
<label class="form-label">
{{ $tc('word.field', fields.length) }}
</label>
<div class="fields-list">
<label
v-for="(field, i) in fields"
:key="`${field.name}-${i}`"
class="form-checkbox m-0"
@click.prevent="toggleField(field.name)"
>
<input type="checkbox" :checked="selectedIndexObj.fields.some(f => f === field.name)">
<i class="form-icon" /> {{ field.name }}
</label>
</div>
</div>
</form>
</div>
</div>
</div>
</ConfirmModal>
</template>
<script>
import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsIndexesModal',
components: {
ConfirmModal
},
props: {
localIndexes: Array,
table: String,
fields: Array,
workspace: Object,
indexTypes: Array
},
data () {
return {
indexesProxy: [],
isOptionsChanging: false,
selectedIndexID: '',
modalInnerHeight: 400
};
},
computed: {
selectedIndexObj () {
return this.indexesProxy.find(index => index._id === this.selectedIndexID);
},
isChanged () {
return JSON.stringify(this.localIndexes) !== JSON.stringify(this.indexesProxy);
},
hasPrimary () {
return this.indexesProxy.some(index => index.type === 'PRIMARY');
}
},
mounted () {
this.indexesProxy = JSON.parse(JSON.stringify(this.localIndexes));
if (this.indexesProxy.length)
this.resetSelectedID();
this.getModalInnerHeight();
window.addEventListener('resize', this.getModalInnerHeight);
},
destroyed () {
window.removeEventListener('resize', this.getModalInnerHeight);
},
methods: {
confirmIndexesChange () {
this.$emit('indexes-update', this.indexesProxy);
},
selectIndex (event, id) {
if (this.selectedIndexID !== id && !event.target.classList.contains('remove-field'))
this.selectedIndexID = id;
},
getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body');
if (modalBody)
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
},
addIndex () {
this.indexesProxy = [...this.indexesProxy, {
_id: uidGen(),
name: 'NEW_INDEX',
fields: [],
type: 'INDEX',
comment: '',
indexType: 'BTREE',
indexComment: '',
cardinality: 0
}];
if (this.indexesProxy.length === 1)
this.resetSelectedID();
setTimeout(() => {
this.$refs.indexesPanel.scrollTop = this.$refs.indexesPanel.scrollHeight + 60;
}, 20);
},
removeIndex (id) {
this.indexesProxy = this.indexesProxy.filter(index => index._id !== id);
if (this.selectedIndexID === id && this.indexesProxy.length)
this.resetSelectedID();
},
clearChanges () {
this.indexesProxy = JSON.parse(JSON.stringify(this.localIndexes));
if (!this.indexesProxy.some(index => index._id === this.selectedIndexID))
this.resetSelectedID();
},
toggleField (field) {
this.indexesProxy = this.indexesProxy.map(index => {
if (index._id === this.selectedIndexID) {
if (index.fields.includes(field))
index.fields = index.fields.filter(f => f !== field);
else
index.fields.push(field);
}
return index;
});
},
resetSelectedID () {
this.selectedIndexID = this.indexesProxy[0]._id;
}
}
};
</script>
<style lang="scss" scoped>
.tile {
border-radius: 2px;
opacity: 0.5;
transition: background 0.2s;
transition: opacity 0.2s;
.tile-action {
opacity: 0;
transition: opacity 0.2s;
}
&:hover {
background: $bg-color-light;
.tile-action {
opacity: 1;
}
}
&.selected-index {
background: $bg-color-light;
opacity: 1;
}
}
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list {
max-height: 200px;
overflow: auto;
}
.remove-field .mdi {
pointer-events: none;
}
</style>

View File

@ -96,7 +96,6 @@ export default {
}, },
props: { props: {
localOptions: Object, localOptions: Object,
tableOptions: Object,
table: String, table: String,
workspace: Object workspace: Object
}, },

View File

@ -32,7 +32,11 @@
<span>{{ $t('word.add') }}</span> <span>{{ $t('word.add') }}</span>
<i class="mdi mdi-24px mdi-playlist-plus ml-1" /> <i class="mdi mdi-24px mdi-playlist-plus ml-1" />
</button> </button>
<button class="btn btn-dark btn-sm" :title="$t('message.manageIndexes')"> <button
class="btn btn-dark btn-sm"
:title="$t('message.manageIndexes')"
@click="showIntdexesModal"
>
<span>{{ $t('word.indexes') }}</span> <span>{{ $t('word.indexes') }}</span>
<i class="mdi mdi-24px mdi-key mdi-rotate-45 ml-1" /> <i class="mdi mdi-24px mdi-key mdi-rotate-45 ml-1" />
</button> </button>
@ -50,26 +54,38 @@
<div class="workspace-query-results column col-12"> <div class="workspace-query-results column col-12">
<WorkspacePropsTable <WorkspacePropsTable
v-if="localFields" v-if="localFields"
ref="queryTable" ref="indexTable"
:fields="localFields" :fields="localFields"
:indexes="localIndexes" :indexes="localIndexes"
:tab-uid="tabUid" :tab-uid="tabUid"
:conn-uid="connection.uid" :conn-uid="connection.uid"
:index-types="workspace.indexTypes"
:table="table" :table="table"
:schema="schema" :schema="schema"
mode="table" mode="table"
@remove-field="removeField" @remove-field="removeField"
@add-new-index="addNewIndex"
@add-to-index="addToIndex"
/> />
</div> </div>
<WorkspacePropsOptionsModal <WorkspacePropsOptionsModal
v-if="isOptionsModal" v-if="isOptionsModal"
:local-options="localOptions" :local-options="localOptions"
:table-options="tableOptions"
:table="table" :table="table"
:workspace="workspace" :workspace="workspace"
@hide="hideOptionsModal" @hide="hideOptionsModal"
@options-update="optionsUpdate" @options-update="optionsUpdate"
/> />
<WorkspacePropsIndexesModal
v-if="isIndexesModal"
:local-indexes="localIndexes"
:table="table"
:fields="localFields"
:index-types="workspace.indexTypes"
:workspace="workspace"
@hide="hideIndexesModal"
@indexes-update="indexesUpdate"
/>
</div> </div>
</template> </template>
@ -79,12 +95,14 @@ import { uidGen } from 'common/libs/uidGen';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import WorkspacePropsTable from '@/components/WorkspacePropsTable'; import WorkspacePropsTable from '@/components/WorkspacePropsTable';
import WorkspacePropsOptionsModal from '@/components/WorkspacePropsOptionsModal'; import WorkspacePropsOptionsModal from '@/components/WorkspacePropsOptionsModal';
import WorkspacePropsIndexesModal from '@/components/WorkspacePropsIndexesModal';
export default { export default {
name: 'WorkspacePropsTab', name: 'WorkspacePropsTab',
components: { components: {
WorkspacePropsTable, WorkspacePropsTable,
WorkspacePropsOptionsModal WorkspacePropsOptionsModal,
WorkspacePropsIndexesModal
}, },
props: { props: {
connection: Object, connection: Object,
@ -95,8 +113,8 @@ export default {
tabUid: 'prop', tabUid: 'prop',
isQuering: false, isQuering: false,
isSaving: false, isSaving: false,
isAddModal: false,
isOptionsModal: false, isOptionsModal: false,
isIndexesModal: false,
isOptionsChanging: false, isOptionsChanging: false,
originalFields: [], originalFields: [],
localFields: [], localFields: [],
@ -105,7 +123,8 @@ export default {
originalIndexes: [], originalIndexes: [],
localIndexes: [], localIndexes: [],
localOptions: {}, localOptions: {},
lastTable: null lastTable: null,
newFieldsCounter: 0
}; };
}, },
computed: { computed: {
@ -132,6 +151,7 @@ export default {
isChanged () { isChanged () {
return JSON.stringify(this.originalFields) !== JSON.stringify(this.localFields) || return JSON.stringify(this.originalFields) !== JSON.stringify(this.localFields) ||
JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage) || JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage) ||
JSON.stringify(this.originalIndexes) !== JSON.stringify(this.localIndexes) ||
JSON.stringify(this.tableOptions) !== JSON.stringify(this.localOptions); JSON.stringify(this.tableOptions) !== JSON.stringify(this.localOptions);
} }
}, },
@ -156,6 +176,7 @@ export default {
}), }),
async getFieldsData () { async getFieldsData () {
if (!this.table) return; if (!this.table) return;
this.newFieldsCounter = 0;
this.isQuering = true; this.isQuering = true;
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions)); this.localOptions = JSON.parse(JSON.stringify(this.tableOptions));
@ -184,8 +205,26 @@ export default {
const { status, response } = await Tables.getTableIndexes(params); const { status, response } = await Tables.getTableIndexes(params);
if (status === 'success') { if (status === 'success') {
this.originalIndexes = response; const indexesObj = response.reduce((acc, curr) => {
this.localIndexes = JSON.parse(JSON.stringify(response)); acc[curr.name] = acc[curr.name] || [];
acc[curr.name].push(curr);
return acc;
}, {});
this.originalIndexes = Object.keys(indexesObj).map(index => {
return {
_id: uidGen(),
name: index,
fields: indexesObj[index].map(field => field.column),
type: indexesObj[index][0].type,
comment: indexesObj[index][0].comment,
indexType: indexesObj[index][0].indexType,
indexComment: indexesObj[index][0].indexComment,
cardinality: indexesObj[index][0].cardinality
};
});
this.localIndexes = JSON.parse(JSON.stringify(this.originalIndexes));
} }
else else
this.addNotification({ status: 'error', message: response }); this.addNotification({ status: 'error', message: response });
@ -214,20 +253,21 @@ export default {
if (this.isSaving) return; if (this.isSaving) return;
this.isSaving = true; this.isSaving = true;
// FIELDS
const originalIDs = this.originalFields.reduce((acc, curr) => [...acc, curr._id], []); const originalIDs = this.originalFields.reduce((acc, curr) => [...acc, curr._id], []);
const localIDs = this.localFields.reduce((acc, curr) => [...acc, curr._id], []); const localIDs = this.localFields.reduce((acc, curr) => [...acc, curr._id], []);
// Additions // Fields Additions
const additions = this.localFields.filter((field, i) => !originalIDs.includes(field._id)).map(field => { const additions = this.localFields.filter((field, i) => !originalIDs.includes(field._id)).map(field => {
const lI = this.localFields.findIndex(localField => localField._id === field._id); const lI = this.localFields.findIndex(localField => localField._id === field._id);
const after = lI > 0 ? this.localFields[lI - 1].name : false; const after = lI > 0 ? this.localFields[lI - 1].name : false;
return { ...field, after }; return { ...field, after };
}); });
// Deletions // Fields Deletions
const deletions = this.originalFields.filter(field => !localIDs.includes(field._id)); const deletions = this.originalFields.filter(field => !localIDs.includes(field._id));
// Changes // Fields Changes
const changes = []; const changes = [];
this.originalFields.forEach((originalField, oI) => { this.originalFields.forEach((originalField, oI) => {
const lI = this.localFields.findIndex(localField => localField._id === originalField._id); const lI = this.localFields.findIndex(localField => localField._id === originalField._id);
@ -247,6 +287,33 @@ export default {
return acc; return acc;
}, {}); }, {});
// INDEXES
const indexChanges = {
additions: [],
changes: [],
deletions: []
};
const originalIndexIDs = this.originalIndexes.reduce((acc, curr) => [...acc, curr._id], []);
const localIndexIDs = this.localIndexes.reduce((acc, curr) => [...acc, curr._id], []);
// Index Additions
indexChanges.additions = this.localIndexes.filter(index => !originalIndexIDs.includes(index._id));
// Index Changes
this.originalIndexes.forEach(originalIndex => {
const lI = this.localIndexes.findIndex(localIndex => localIndex._id === originalIndex._id);
if (JSON.stringify(originalIndex) !== JSON.stringify(this.localIndexes[lI])) {
indexChanges.changes.push({
...this.localIndexes[lI],
oldName: originalIndex.name,
oldType: originalIndex.type
});
}
});
// Index Deletions
indexChanges.deletions = this.originalIndexes.filter(index => !localIndexIDs.includes(index._id));
const params = { const params = {
uid: this.connection.uid, uid: this.connection.uid,
schema: this.schema, schema: this.schema,
@ -254,6 +321,7 @@ export default {
additions, additions,
changes, changes,
deletions, deletions,
indexChanges,
options options
}; };
@ -272,16 +340,19 @@ export default {
} }
this.isSaving = false; this.isSaving = false;
this.newFieldsCounter = 0;
}, },
clearChanges () { clearChanges () {
this.localFields = JSON.parse(JSON.stringify(this.originalFields)); this.localFields = JSON.parse(JSON.stringify(this.originalFields));
this.localIndexes = JSON.parse(JSON.stringify(this.originalIndexes));
this.localKeyUsage = JSON.parse(JSON.stringify(this.originalKeyUsage)); this.localKeyUsage = JSON.parse(JSON.stringify(this.originalKeyUsage));
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions)); this.localOptions = JSON.parse(JSON.stringify(this.tableOptions));
this.newFieldsCounter = 0;
}, },
addField () { addField () {
this.localFields.push({ this.localFields.push({
_id: uidGen(), _id: uidGen(),
name: '', name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`,
key: '', key: '',
type: 'int', type: 'int',
schema: this.schema, schema: this.schema,
@ -301,10 +372,33 @@ export default {
onUpdate: '', onUpdate: '',
comment: '' comment: ''
}); });
setTimeout(() => {
const scrollable = this.$refs.indexTable.$refs.tableWrapper;
scrollable.scrollTop = scrollable.scrollHeight + 30;
}, 20);
}, },
removeField (uid) { removeField (uid) {
this.localFields = this.localFields.filter(field => field._id !== uid); this.localFields = this.localFields.filter(field => field._id !== uid);
}, },
addNewIndex (payload) {
this.localIndexes = [...this.localIndexes, {
_id: uidGen(),
name: payload.index === 'PRIMARY' ? 'PRIMARY' : payload.field,
fields: [payload.field],
type: payload.index,
comment: '',
indexType: 'BTREE',
indexComment: '',
cardinality: 0
}];
},
addToIndex (payload) {
this.localIndexes = this.localIndexes.map(index => {
if (index._id === payload.index) index.fields.push(payload.field);
return index;
});
},
showOptionsModal () { showOptionsModal () {
this.isOptionsModal = true; this.isOptionsModal = true;
}, },
@ -313,6 +407,15 @@ export default {
}, },
optionsUpdate (options) { optionsUpdate (options) {
this.localOptions = options; this.localOptions = options;
},
showIntdexesModal () {
this.isIndexesModal = true;
},
hideIndexesModal () {
this.isIndexesModal = false;
},
indexesUpdate (indexes) {
this.localIndexes = indexes;
} }
} }
}; };

View File

@ -8,8 +8,12 @@
v-if="isContext" v-if="isContext"
:context-event="contextEvent" :context-event="contextEvent"
:selected-field="selectedField" :selected-field="selectedField"
:index-types="indexTypes"
:indexes="indexes"
@delete-selected="removeField" @delete-selected="removeField"
@close-context="isContext = false" @close-context="isContext = false"
@add-new-index="$emit('add-new-index', $event)"
@add-to-index="$emit('add-to-index', $event)"
/> />
<div ref="propTable" class="table table-hover"> <div ref="propTable" class="table table-hover">
<div class="thead"> <div class="thead">
@ -124,6 +128,7 @@ export default {
props: { props: {
fields: Array, fields: Array,
indexes: Array, indexes: Array,
indexTypes: Array,
tabUid: [String, Number], tabUid: [String, Number],
connUid: String, connUid: String,
table: String, table: String,
@ -133,7 +138,6 @@ export default {
data () { data () {
return { return {
resultsSize: 1000, resultsSize: 1000,
localResults: [],
isContext: false, isContext: false,
contextEvent: null, contextEvent: null,
selectedField: null, selectedField: null,
@ -156,6 +160,14 @@ export default {
}, },
tabProperties () { tabProperties () {
return this.getWorkspaceTab(this.tabUid); return this.getWorkspaceTab(this.tabUid);
},
fieldsLength () {
return this.fields.length;
}
},
watch: {
fieldsLength () {
this.refreshScroller();
} }
}, },
updated () { updated () {
@ -184,22 +196,24 @@ export default {
const size = window.innerHeight - el.getBoundingClientRect().top - footer.offsetHeight; const size = window.innerHeight - el.getBoundingClientRect().top - footer.offsetHeight;
this.resultsSize = size; this.resultsSize = size;
} }
// this.$refs.resultTable.updateWindow();
} }
}, },
refreshScroller () { refreshScroller () {
this.resizeResults(); this.resizeResults();
}, },
contextMenu (event, uid) { contextMenu (event, uid) {
this.selectedField = uid; this.selectedField = this.fields.find(field => field._id === uid);
this.contextEvent = event; this.contextEvent = event;
this.isContext = true; this.isContext = true;
}, },
removeField () { removeField () {
this.$emit('remove-field', this.selectedField); this.$emit('remove-field', this.selectedField._id);
}, },
getIndexes (field) { getIndexes (field) {
return this.indexes.filter(index => index.column === field); return this.indexes.reduce((acc, curr) => {
acc.push(...curr.fields.map(f => ({ name: f, type: curr.type })));
return acc;
}, []).filter(f => f.name === field);
} }
} }
}; };
@ -213,4 +227,8 @@ export default {
overflow: hidden; overflow: hidden;
} }
} }
.vscroll {
overflow: auto;
}
</style> </style>

View File

@ -3,6 +3,36 @@
:context-event="contextEvent" :context-event="contextEvent"
@close-context="closeContext" @close-context="closeContext"
> >
<div class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-key-plus text-light pr-1" /> {{ $t('message.createNewIndex') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu">
<div
v-for="index in indexTypes"
:key="index"
class="context-element"
:class="{'disabled': index === 'PRIMARY' && hasPrimary}"
@click="addNewIndex(index)"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-key column-key pr-1" :class="`key-${index}`" /> {{ index }}</span>
</div>
</div>
</div>
<div class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-key-arrow-right text-light pr-1" /> {{ $t('message.addToIndex') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu">
<div
v-for="index in indexes"
:key="index.name"
class="context-element"
:class="{'disabled': index.fields.includes(selectedField.name)}"
@click="addToIndex(index._id)"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-key column-key pr-1" :class="`key-${index.type}`" /> {{ index.name }}</span>
</div>
</div>
</div>
<div class="context-element" @click="deleteField"> <div class="context-element" @click="deleteField">
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('message.deleteField') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('message.deleteField') }}</span>
</div> </div>
@ -19,14 +49,14 @@ export default {
}, },
props: { props: {
contextEvent: MouseEvent, contextEvent: MouseEvent,
selectedField: String indexes: Array,
}, indexTypes: Array,
data () { selectedField: Object
return {
isConfirmModal: false
};
}, },
computed: { computed: {
hasPrimary () {
return this.indexes.some(index => index.type === 'PRIMARY');
}
}, },
methods: { methods: {
closeContext () { closeContext () {
@ -35,7 +65,23 @@ export default {
deleteField () { deleteField () {
this.$emit('delete-selected'); this.$emit('delete-selected');
this.closeContext(); this.closeContext();
},
addNewIndex (index) {
this.$emit('add-new-index', { field: this.selectedField.name, index });
this.closeContext();
},
addToIndex (index) {
this.$emit('add-to-index', { field: this.selectedField.name, index });
this.closeContext();
} }
} }
}; };
</script> </script>
<style lang="scss" scoped>
.disabled {
pointer-events: none;
filter: grayscale(100%);
opacity: 0.5;
}
</style>

View File

@ -9,8 +9,8 @@
<div class="td" tabindex="0"> <div class="td" tabindex="0">
<div class="text-center"> <div class="text-center">
<i <i
v-for="index in indexes" v-for="(index, i) in indexes"
:key="index.name" :key="`${index.name}-${i}`"
:title="index.type" :title="index.type"
class="d-inline-block mdi mdi-key column-key c-help" class="d-inline-block mdi mdi-key column-key c-help"
:class="`key-${index.type}`" :class="`key-${index.type}`"

View File

@ -44,7 +44,7 @@
<div v-if="results.length && results[0].rows"> <div v-if="results.length && results[0].rows">
{{ $t('word.results') }}: <b>{{ results[0].rows.length.toLocaleString() }}</b> {{ $t('word.results') }}: <b>{{ results[0].rows.length.toLocaleString() }}</b>
</div> </div>
<div v-if="results.length && results[0].rows && results[0].rows.length < tableInfo.rows"> <div v-if="results.length && results[0].rows && tableInfo && results[0].rows.length < tableInfo.rows">
{{ $t('word.total') }}: <b>{{ tableInfo.rows.toLocaleString() }}</b> <small>({{ $t('word.approximately') }})</small> {{ $t('word.total') }}: <b>{{ tableInfo.rows.toLocaleString() }}</b> <small>({{ $t('word.approximately') }})</small>
</div> </div>
<div v-if="workspace.breadcrumbs.database"> <div v-if="workspace.breadcrumbs.database">

View File

@ -58,7 +58,8 @@ module.exports = {
engine: 'Engine', engine: 'Engine',
field: 'Field | Fields', field: 'Field | Fields',
approximately: 'Approximately', approximately: 'Approximately',
total: 'Total' total: 'Total',
table: 'Table'
}, },
message: { message: {
appWelcome: 'Welcome to Antares SQL Client!', appWelcome: 'Welcome to Antares SQL Client!',

View File

@ -41,12 +41,13 @@ export default {
SELECT_WORKSPACE (state, uid) { SELECT_WORKSPACE (state, uid) {
state.selected_workspace = uid; state.selected_workspace = uid;
}, },
ADD_CONNECTED (state, { uid, client, dataTypes, structure }) { ADD_CONNECTED (state, { uid, client, dataTypes, indexTypes, structure }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? { ? {
...workspace, ...workspace,
client, client,
dataTypes, dataTypes,
indexTypes,
structure, structure,
connected: true connected: true
} }
@ -187,17 +188,20 @@ export default {
dispatch('notifications/addNotification', { status, message: response }, { root: true }); dispatch('notifications/addNotification', { status, message: response }, { root: true });
else { else {
let dataTypes = []; let dataTypes = [];
let indexTypes = [];
switch (connection.client) { switch (connection.client) {
case 'mysql': case 'mysql':
case 'maria': case 'maria':
dataTypes = require('common/data-types/mysql'); dataTypes = require('common/data-types/mysql');
indexTypes = require('common/index-types/mysql');
break; break;
} }
commit('ADD_CONNECTED', { commit('ADD_CONNECTED', {
uid: connection.uid, uid: connection.uid,
client: connection.client, client: connection.client,
dataTypes, dataTypes,
indexTypes,
structure: response structure: response
}); });
dispatch('refreshCollations', connection.uid); dispatch('refreshCollations', connection.uid);