From 41505bde6547c0af3c3413248ad8a0d182838bb1 Mon Sep 17 00:00:00 2001 From: Fabio Di Stasio Date: Tue, 1 Dec 2020 16:48:20 +0100 Subject: [PATCH] feat: index management --- package.json | 14 +- src/common/index-types/mysql.js | 6 + src/main/libs/clients/MySQLClient.js | 52 +++- src/renderer/components/BaseContextMenu.vue | 22 +- .../WorkspaceExploreBarDatabaseContext.vue | 5 + .../components/WorkspacePropsIndexesModal.vue | 263 ++++++++++++++++++ .../components/WorkspacePropsOptionsModal.vue | 1 - src/renderer/components/WorkspacePropsTab.vue | 127 ++++++++- .../components/WorkspacePropsTable.vue | 28 +- .../components/WorkspacePropsTableContext.vue | 58 +++- .../components/WorkspacePropsTableRow.vue | 4 +- src/renderer/components/WorkspaceTableTab.vue | 2 +- src/renderer/i18n/en-US.js | 3 +- .../store/modules/workspaces.store.js | 6 +- 14 files changed, 550 insertions(+), 41 deletions(-) create mode 100644 src/common/index-types/mysql.js create mode 100644 src/renderer/components/WorkspacePropsIndexesModal.vue diff --git a/package.json b/package.json index 59d8a6cb..ce3490a1 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.9", "description": "A cross-platform easy to use SQL client.", "license": "MIT", - "repository": "https://github.com/EStarium/antares.git", + "repository": "https://github.com/Fabio286/antares.git", "scripts": { "dev": "cross-env NODE_ENV=development electron-webpack dev", "compile": "electron-webpack", @@ -17,7 +17,7 @@ }, "author": "Fabio Di Stasio ", "build": { - "appId": "com.estarium.antares", + "appId": "com.fabio286.antares", "artifactName": "${productName}-${version}-${os}_${arch}.${ext}", "dmg": { "contents": [ @@ -58,10 +58,10 @@ "pg": "^8.5.1", "source-map-support": "^0.5.16", "spectre.css": "^0.5.9", - "vue-i18n": "^8.22.1", + "vue-i18n": "^8.22.2", "vue-the-mask": "^0.11.1", "vuedraggable": "^2.24.3", - "vuex": "^3.5.1", + "vuex": "^3.6.0", "vuex-persist": "^3.1.3" }, "devDependencies": { @@ -72,8 +72,8 @@ "electron-devtools-installer": "^3.1.1", "electron-webpack": "^2.8.2", "electron-webpack-vue": "^2.4.0", - "eslint": "^7.13.0", - "eslint-config-standard": "^16.0.1", + "eslint": "^7.14.0", + "eslint-config-standard": "^16.0.2", "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.2.1", @@ -82,7 +82,7 @@ "node-sass": "^5.0.0", "sass-loader": "^10.1.0", "standard-version": "^9.0.0", - "stylelint": "^13.7.2", + "stylelint": "^13.8.0", "stylelint-config-standard": "^20.0.0", "stylelint-scss": "^3.18.0", "vue": "^2.6.12", diff --git a/src/common/index-types/mysql.js b/src/common/index-types/mysql.js new file mode 100644 index 00000000..a9e52124 --- /dev/null +++ b/src/common/index-types/mysql.js @@ -0,0 +1,6 @@ +module.exports = [ + 'PRIMARY', + 'INDEX', + 'UNIQUE', + 'FULLTEXT' +]; diff --git a/src/main/libs/clients/MySQLClient.js b/src/main/libs/clients/MySQLClient.js index 64b6b9fb..1396f0aa 100644 --- a/src/main/libs/clients/MySQLClient.js +++ b/src/main/libs/clients/MySQLClient.js @@ -325,9 +325,12 @@ export class MySQLClient extends AntaresCore { additions, deletions, changes, + indexChanges, options } = params; + console.log(params); + let sql = `ALTER TABLE \`${table}\` `; const alterColumns = []; @@ -337,7 +340,7 @@ export class MySQLClient extends AntaresCore { if ('autoIncrement' in options) alterColumns.push(`AUTO_INCREMENT=${+options.autoIncrement}`); if ('collation' in options) alterColumns.push(`COLLATE='${options.collation}'`); - // ADD + // ADD FIELDS additions.forEach(addition => { const length = addition.numLength || addition.charLength || addition.datePrecision; @@ -354,7 +357,22 @@ export class MySQLClient extends AntaresCore { ${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 => { const length = change.numLength || change.charLength || change.datePrecision; @@ -371,11 +389,39 @@ export class MySQLClient extends AntaresCore { ${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 => { 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(', '); // RENAME diff --git a/src/renderer/components/BaseContextMenu.vue b/src/renderer/components/BaseContextMenu.vue index a57d3565..b200f74c 100644 --- a/src/renderer/components/BaseContextMenu.vue +++ b/src/renderer/components/BaseContextMenu.vue @@ -86,9 +86,8 @@ export default { .context-container { min-width: 100px; - max-width: 150px; z-index: 10; - box-shadow: 0 0 1px 0 #000; + box-shadow: 0 0 2px 0 #000; padding: 0; background: #1d1d1d; border-radius: 0.1rem; @@ -103,9 +102,28 @@ export default { padding: 0.1rem 0.3rem; cursor: pointer; 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 { background: $primary-color; + + .context-submenu { + display: block; + visibility: visible; + opacity: 1; + } } } } diff --git a/src/renderer/components/WorkspaceExploreBarDatabaseContext.vue b/src/renderer/components/WorkspaceExploreBarDatabaseContext.vue index 7a25afe6..2515d5c1 100644 --- a/src/renderer/components/WorkspaceExploreBarDatabaseContext.vue +++ b/src/renderer/components/WorkspaceExploreBarDatabaseContext.vue @@ -6,6 +6,11 @@
{{ $t('word.add') }} +
+
+ {{ $t('word.table') }} +
+
{{ $t('word.edit') }} diff --git a/src/renderer/components/WorkspacePropsIndexesModal.vue b/src/renderer/components/WorkspacePropsIndexesModal.vue new file mode 100644 index 00000000..e29884d0 --- /dev/null +++ b/src/renderer/components/WorkspacePropsIndexesModal.vue @@ -0,0 +1,263 @@ + + + + + diff --git a/src/renderer/components/WorkspacePropsOptionsModal.vue b/src/renderer/components/WorkspacePropsOptionsModal.vue index 29d5e1fa..d02ceeb1 100644 --- a/src/renderer/components/WorkspacePropsOptionsModal.vue +++ b/src/renderer/components/WorkspacePropsOptionsModal.vue @@ -96,7 +96,6 @@ export default { }, props: { localOptions: Object, - tableOptions: Object, table: String, workspace: Object }, diff --git a/src/renderer/components/WorkspacePropsTab.vue b/src/renderer/components/WorkspacePropsTab.vue index 9aa19d4d..780fa9fb 100644 --- a/src/renderer/components/WorkspacePropsTab.vue +++ b/src/renderer/components/WorkspacePropsTab.vue @@ -32,7 +32,11 @@ {{ $t('word.add') }} - @@ -50,26 +54,38 @@
+
@@ -79,12 +95,14 @@ import { uidGen } from 'common/libs/uidGen'; import Tables from '@/ipc-api/Tables'; import WorkspacePropsTable from '@/components/WorkspacePropsTable'; import WorkspacePropsOptionsModal from '@/components/WorkspacePropsOptionsModal'; +import WorkspacePropsIndexesModal from '@/components/WorkspacePropsIndexesModal'; export default { name: 'WorkspacePropsTab', components: { WorkspacePropsTable, - WorkspacePropsOptionsModal + WorkspacePropsOptionsModal, + WorkspacePropsIndexesModal }, props: { connection: Object, @@ -95,8 +113,8 @@ export default { tabUid: 'prop', isQuering: false, isSaving: false, - isAddModal: false, isOptionsModal: false, + isIndexesModal: false, isOptionsChanging: false, originalFields: [], localFields: [], @@ -105,7 +123,8 @@ export default { originalIndexes: [], localIndexes: [], localOptions: {}, - lastTable: null + lastTable: null, + newFieldsCounter: 0 }; }, computed: { @@ -132,6 +151,7 @@ export default { isChanged () { return JSON.stringify(this.originalFields) !== JSON.stringify(this.localFields) || JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage) || + JSON.stringify(this.originalIndexes) !== JSON.stringify(this.localIndexes) || JSON.stringify(this.tableOptions) !== JSON.stringify(this.localOptions); } }, @@ -156,6 +176,7 @@ export default { }), async getFieldsData () { if (!this.table) return; + this.newFieldsCounter = 0; this.isQuering = true; this.localOptions = JSON.parse(JSON.stringify(this.tableOptions)); @@ -184,8 +205,26 @@ export default { const { status, response } = await Tables.getTableIndexes(params); if (status === 'success') { - this.originalIndexes = response; - this.localIndexes = JSON.parse(JSON.stringify(response)); + const indexesObj = response.reduce((acc, curr) => { + 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 this.addNotification({ status: 'error', message: response }); @@ -214,20 +253,21 @@ export default { if (this.isSaving) return; this.isSaving = true; + // FIELDS const originalIDs = this.originalFields.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 lI = this.localFields.findIndex(localField => localField._id === field._id); const after = lI > 0 ? this.localFields[lI - 1].name : false; return { ...field, after }; }); - // Deletions + // Fields Deletions const deletions = this.originalFields.filter(field => !localIDs.includes(field._id)); - // Changes + // Fields Changes const changes = []; this.originalFields.forEach((originalField, oI) => { const lI = this.localFields.findIndex(localField => localField._id === originalField._id); @@ -247,6 +287,33 @@ export default { 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 = { uid: this.connection.uid, schema: this.schema, @@ -254,6 +321,7 @@ export default { additions, changes, deletions, + indexChanges, options }; @@ -272,16 +340,19 @@ export default { } this.isSaving = false; + this.newFieldsCounter = 0; }, clearChanges () { 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.localOptions = JSON.parse(JSON.stringify(this.tableOptions)); + this.newFieldsCounter = 0; }, addField () { this.localFields.push({ _id: uidGen(), - name: '', + name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`, key: '', type: 'int', schema: this.schema, @@ -301,10 +372,33 @@ export default { onUpdate: '', comment: '' }); + + setTimeout(() => { + const scrollable = this.$refs.indexTable.$refs.tableWrapper; + scrollable.scrollTop = scrollable.scrollHeight + 30; + }, 20); }, removeField (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 () { this.isOptionsModal = true; }, @@ -313,6 +407,15 @@ export default { }, optionsUpdate (options) { this.localOptions = options; + }, + showIntdexesModal () { + this.isIndexesModal = true; + }, + hideIndexesModal () { + this.isIndexesModal = false; + }, + indexesUpdate (indexes) { + this.localIndexes = indexes; } } }; diff --git a/src/renderer/components/WorkspacePropsTable.vue b/src/renderer/components/WorkspacePropsTable.vue index e8505805..1866c8ef 100644 --- a/src/renderer/components/WorkspacePropsTable.vue +++ b/src/renderer/components/WorkspacePropsTable.vue @@ -8,8 +8,12 @@ v-if="isContext" :context-event="contextEvent" :selected-field="selectedField" + :index-types="indexTypes" + :indexes="indexes" @delete-selected="removeField" @close-context="isContext = false" + @add-new-index="$emit('add-new-index', $event)" + @add-to-index="$emit('add-to-index', $event)" />
@@ -124,6 +128,7 @@ export default { props: { fields: Array, indexes: Array, + indexTypes: Array, tabUid: [String, Number], connUid: String, table: String, @@ -133,7 +138,6 @@ export default { data () { return { resultsSize: 1000, - localResults: [], isContext: false, contextEvent: null, selectedField: null, @@ -156,6 +160,14 @@ export default { }, tabProperties () { return this.getWorkspaceTab(this.tabUid); + }, + fieldsLength () { + return this.fields.length; + } + }, + watch: { + fieldsLength () { + this.refreshScroller(); } }, updated () { @@ -184,22 +196,24 @@ export default { const size = window.innerHeight - el.getBoundingClientRect().top - footer.offsetHeight; this.resultsSize = size; } - // this.$refs.resultTable.updateWindow(); } }, refreshScroller () { this.resizeResults(); }, contextMenu (event, uid) { - this.selectedField = uid; + this.selectedField = this.fields.find(field => field._id === uid); this.contextEvent = event; this.isContext = true; }, removeField () { - this.$emit('remove-field', this.selectedField); + this.$emit('remove-field', this.selectedField._id); }, 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; } } + +.vscroll { + overflow: auto; +} diff --git a/src/renderer/components/WorkspacePropsTableContext.vue b/src/renderer/components/WorkspacePropsTableContext.vue index 29744157..3cd1a74f 100644 --- a/src/renderer/components/WorkspacePropsTableContext.vue +++ b/src/renderer/components/WorkspacePropsTableContext.vue @@ -3,6 +3,36 @@ :context-event="contextEvent" @close-context="closeContext" > +
+ {{ $t('message.createNewIndex') }} + +
+
+ {{ index }} +
+
+
+
+ {{ $t('message.addToIndex') }} + +
+
+ {{ index.name }} +
+
+
{{ $t('message.deleteField') }}
@@ -19,14 +49,14 @@ export default { }, props: { contextEvent: MouseEvent, - selectedField: String - }, - data () { - return { - isConfirmModal: false - }; + indexes: Array, + indexTypes: Array, + selectedField: Object }, computed: { + hasPrimary () { + return this.indexes.some(index => index.type === 'PRIMARY'); + } }, methods: { closeContext () { @@ -35,7 +65,23 @@ export default { deleteField () { this.$emit('delete-selected'); 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(); } } }; + + diff --git a/src/renderer/components/WorkspacePropsTableRow.vue b/src/renderer/components/WorkspacePropsTableRow.vue index c2ecfdff..4b34ed90 100644 --- a/src/renderer/components/WorkspacePropsTableRow.vue +++ b/src/renderer/components/WorkspacePropsTableRow.vue @@ -9,8 +9,8 @@
{{ $t('word.results') }}: {{ results[0].rows.length.toLocaleString() }}
-
+
{{ $t('word.total') }}: {{ tableInfo.rows.toLocaleString() }} ({{ $t('word.approximately') }})
diff --git a/src/renderer/i18n/en-US.js b/src/renderer/i18n/en-US.js index dfda9449..0122a479 100644 --- a/src/renderer/i18n/en-US.js +++ b/src/renderer/i18n/en-US.js @@ -58,7 +58,8 @@ module.exports = { engine: 'Engine', field: 'Field | Fields', approximately: 'Approximately', - total: 'Total' + total: 'Total', + table: 'Table' }, message: { appWelcome: 'Welcome to Antares SQL Client!', diff --git a/src/renderer/store/modules/workspaces.store.js b/src/renderer/store/modules/workspaces.store.js index 28e3a493..b0ccbe73 100644 --- a/src/renderer/store/modules/workspaces.store.js +++ b/src/renderer/store/modules/workspaces.store.js @@ -41,12 +41,13 @@ export default { SELECT_WORKSPACE (state, 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 ? { ...workspace, client, dataTypes, + indexTypes, structure, connected: true } @@ -187,17 +188,20 @@ export default { dispatch('notifications/addNotification', { status, message: response }, { root: true }); else { let dataTypes = []; + let indexTypes = []; switch (connection.client) { case 'mysql': case 'maria': dataTypes = require('common/data-types/mysql'); + indexTypes = require('common/index-types/mysql'); break; } commit('ADD_CONNECTED', { uid: connection.uid, client: connection.client, dataTypes, + indexTypes, structure: response }); dispatch('refreshCollations', connection.uid);