mirror of
				https://github.com/Fabio286/antares.git
				synced 2025-06-05 21:59:22 +02:00 
			
		
		
		
	feat: index management
This commit is contained in:
		
							
								
								
									
										14
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								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 <fabio286@gmail.com>", | ||||
|   "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", | ||||
|   | ||||
							
								
								
									
										6
									
								
								src/common/index-types/mysql.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/common/index-types/mysql.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| module.exports = [ | ||||
|    'PRIMARY', | ||||
|    'INDEX', | ||||
|    'UNIQUE', | ||||
|    'FULLTEXT' | ||||
| ]; | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   | ||||
| @@ -6,6 +6,11 @@ | ||||
|       <div class="context-element"> | ||||
|          <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" /> | ||||
|          <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 class="context-element" @click="showEditModal"> | ||||
|          <span class="d-flex"><i class="mdi mdi-18px mdi-pencil text-light pr-1" /> {{ $t('word.edit') }}</span> | ||||
|   | ||||
							
								
								
									
										263
									
								
								src/renderer/components/WorkspacePropsIndexesModal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										263
									
								
								src/renderer/components/WorkspacePropsIndexesModal.vue
									
									
									
									
									
										Normal 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> | ||||
| @@ -96,7 +96,6 @@ export default { | ||||
|    }, | ||||
|    props: { | ||||
|       localOptions: Object, | ||||
|       tableOptions: Object, | ||||
|       table: String, | ||||
|       workspace: Object | ||||
|    }, | ||||
|   | ||||
| @@ -32,7 +32,11 @@ | ||||
|                   <span>{{ $t('word.add') }}</span> | ||||
|                   <i class="mdi mdi-24px mdi-playlist-plus ml-1" /> | ||||
|                </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> | ||||
|                   <i class="mdi mdi-24px mdi-key mdi-rotate-45 ml-1" /> | ||||
|                </button> | ||||
| @@ -50,26 +54,38 @@ | ||||
|       <div class="workspace-query-results column col-12"> | ||||
|          <WorkspacePropsTable | ||||
|             v-if="localFields" | ||||
|             ref="queryTable" | ||||
|             ref="indexTable" | ||||
|             :fields="localFields" | ||||
|             :indexes="localIndexes" | ||||
|             :tab-uid="tabUid" | ||||
|             :conn-uid="connection.uid" | ||||
|             :index-types="workspace.indexTypes" | ||||
|             :table="table" | ||||
|             :schema="schema" | ||||
|             mode="table" | ||||
|             @remove-field="removeField" | ||||
|             @add-new-index="addNewIndex" | ||||
|             @add-to-index="addToIndex" | ||||
|          /> | ||||
|       </div> | ||||
|       <WorkspacePropsOptionsModal | ||||
|          v-if="isOptionsModal" | ||||
|          :local-options="localOptions" | ||||
|          :table-options="tableOptions" | ||||
|          :table="table" | ||||
|          :workspace="workspace" | ||||
|          @hide="hideOptionsModal" | ||||
|          @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> | ||||
| </template> | ||||
|  | ||||
| @@ -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; | ||||
|       } | ||||
|    } | ||||
| }; | ||||
|   | ||||
| @@ -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)" | ||||
|       /> | ||||
|       <div ref="propTable" class="table table-hover"> | ||||
|          <div class="thead"> | ||||
| @@ -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; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -3,6 +3,36 @@ | ||||
|       :context-event="contextEvent" | ||||
|       @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"> | ||||
|          <span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('message.deleteField') }}</span> | ||||
|       </div> | ||||
| @@ -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(); | ||||
|       } | ||||
|    } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .disabled { | ||||
|   pointer-events: none; | ||||
|   filter: grayscale(100%); | ||||
|   opacity: 0.5; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -9,8 +9,8 @@ | ||||
|       <div class="td" tabindex="0"> | ||||
|          <div class="text-center"> | ||||
|             <i | ||||
|                v-for="index in indexes" | ||||
|                :key="index.name" | ||||
|                v-for="(index, i) in indexes" | ||||
|                :key="`${index.name}-${i}`" | ||||
|                :title="index.type" | ||||
|                class="d-inline-block mdi mdi-key column-key c-help" | ||||
|                :class="`key-${index.type}`" | ||||
|   | ||||
| @@ -44,7 +44,7 @@ | ||||
|                <div v-if="results.length && results[0].rows"> | ||||
|                   {{ $t('word.results') }}: <b>{{ results[0].rows.length.toLocaleString() }}</b> | ||||
|                </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> | ||||
|                </div> | ||||
|                <div v-if="workspace.breadcrumbs.database"> | ||||
|   | ||||
| @@ -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!', | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user