feat: stored routines edit

This commit is contained in:
Fabio Di Stasio 2021-01-05 17:25:18 +01:00
parent d695c9f8d2
commit 82fdc0bcd7
13 changed files with 937 additions and 15 deletions

View File

@ -2,6 +2,7 @@ import connection from './connection';
import tables from './tables';
import views from './views';
import triggers from './triggers';
import routines from './routines';
import updates from './updates';
import application from './application';
import database from './database';
@ -14,6 +15,7 @@ export default () => {
tables(connections);
views(connections);
triggers(connections);
routines(connections);
database(connections);
users(connections);
updates();

View File

@ -0,0 +1,43 @@
import { ipcMain } from 'electron';
export default (connections) => {
ipcMain.handle('get-routine-informations', async (event, params) => {
try {
const result = await connections[params.uid].getRoutineInformations(params);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('drop-routine', async (event, params) => {
try {
await connections[params.uid].dropRoutine(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('alter-routine', async (event, params) => {
try {
await connections[params.uid].alterRoutine(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('create-routine', async (event, params) => {
try {
await connections[params.uid].createRoutine(params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
};

View File

@ -104,6 +104,20 @@ export class MySQLClient extends AntaresCore {
};
});
// FUNCTIONS
const remappedFunctions = functions.filter(func => func.Db === db.Database).map(func => {
return {
name: func.Name,
type: func.Type,
definer: func.Definer,
created: func.Created,
updated: func.Modified,
comment: func.Comment,
charset: func.character_set_client,
security: func.Security_type
};
});
// SCHEDULERS
const remappedSchedulers = schedulers.filter(scheduler => scheduler.Db === db.Database).map(scheduler => {
return {
@ -148,7 +162,7 @@ export class MySQLClient extends AntaresCore {
return {
name: db.Database,
tables: remappedTables,
functions: functions.filter(func => func.Db === db.Database), // TODO: remap functions
functions: remappedFunctions,
procedures: remappedProcedures,
triggers: remappedTriggers,
schedulers: remappedSchedulers
@ -355,7 +369,7 @@ export class MySQLClient extends AntaresCore {
return results.rows.map(row => {
return {
definer: row['SQL Original Statement'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['SQL Original Statement'].match(/BEGIN(.*)END/gs)[0],
sql: row['SQL Original Statement'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
name: row.Trigger,
table: row['SQL Original Statement'].match(/(?<=ON `).*?(?=`)/gs)[0],
event1: row['SQL Original Statement'].match(/(BEFORE|AFTER)/gs)[0],
@ -405,9 +419,111 @@ export class MySQLClient extends AntaresCore {
*/
async createTrigger (trigger) {
const sql = `CREATE ${trigger.definer ? `DEFINER=${trigger.definer} ` : ''}TRIGGER \`${trigger.name}\` ${trigger.event1} ${trigger.event2} ON \`${trigger.table}\` FOR EACH ROW ${trigger.sql}`;
return await this.raw(sql, { split: false });
}
/**
* SHOW CREATE PROCEDURE
*
* @returns {Array.<Object>} view informations
* @memberof MySQLClient
*/
async getRoutineInformations ({ schema, routine }) {
const sql = `SHOW CREATE PROCEDURE \`${schema}\`.\`${routine}\``;
const results = await this.raw(sql);
return results.rows.map(row => {
const parameters = row['Create Procedure']
.match(/(?<=\().*?(?=\))/s)[0]
.replaceAll('\r', '')
.replaceAll('\t', '')
.split(',')
.map(el => {
const param = el.split(' ');
return {
name: param[1] ? param[1].replaceAll('`', '') : '',
type: param[2] ? param[2].replace(',', '') : '',
context: param[0] ? param[0].replace('\n', '') : ''
};
}).filter(el => el.name);
let dataAccess = 'CONTAINS SQL';
if (row['Create Procedure'].includes('NO SQL'))
dataAccess = 'NO SQL';
if (row['Create Procedure'].includes('READS SQL DATA'))
dataAccess = 'READS SQL DATA';
if (row['Create Procedure'].includes('MODIFIES SQL DATA'))
dataAccess = 'MODIFIES SQL DATA';
return {
definer: row['Create Procedure'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
sql: row['Create Procedure'].match(/(BEGIN|begin)(.*)(END|end)/gs)[0],
parameters,
name: row.Procedure,
comment: row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs) ? row['Create Procedure'].match(/(?<=COMMENT ').*?(?=')/gs)[0] : '',
security: row['Create Procedure'].includes('SQL SECURITY INVOKER') ? 'INVOKER' : 'DEFINER',
deterministic: row['Create Procedure'].includes('DETERMINISTIC'),
dataAccess
};
})[0];
}
/**
* DROP PROCEDURE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async dropRoutine (params) {
const sql = `DROP PROCEDURE \`${params.routine}\``;
return await this.raw(sql);
}
/**
* ALTER PROCEDURE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async alterRoutine (params) {
const { routine } = params;
const tempProcedure = Object.assign({}, routine);
tempProcedure.name = `Antares_${tempProcedure.name}_tmp`;
try {
await this.createRoutine(tempProcedure);
await this.dropRoutine({ routine: tempProcedure.name });
await this.dropRoutine({ routine: routine.oldName });
await this.createRoutine(routine);
}
catch (err) {
return Promise.reject(err);
}
}
/**
* CREATE PROCEDURE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async createRoutine (routine) {
const parameters = routine.parameters.reduce((acc, curr) => {
acc.push(`${curr.context} \`${curr.name}\` ${curr.type}`);
return acc;
}, []).join(',');
const sql = `CREATE ${routine.definer ? `DEFINER=${routine.definer} ` : ''}PROCEDURE \`${routine.name}\`(${parameters})
LANGUAGE SQL
${routine.deterministic ? 'DETERMINISTIC' : 'NOT DETERMINISTIC'}
${routine.dataAccess}
SQL SECURITY ${routine.security}
COMMENT '${routine.comment}'
${routine.sql}`;
return await this.raw(sql, { split: false });
}
/**
* SHOW COLLATION
*
@ -709,6 +825,7 @@ export class MySQLClient extends AntaresCore {
* @param {object} args
* @param {boolean} args.nest
* @param {boolean} args.details
* @param {boolean} args.split
* @returns {Promise}
* @memberof MySQLClient
*/
@ -716,13 +833,14 @@ export class MySQLClient extends AntaresCore {
args = {
nest: false,
details: false,
split: true,
...args
};
const nestTables = args.nest ? '.' : false;
const resultsArr = [];
let paramsArr = [];
let selectedFields = [];
const queries = sql.split(';');
const queries = args.split ? sql.split(';') : [sql];
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder

View File

@ -89,7 +89,6 @@
</template>
<script>
import { mapGetters } from 'vuex';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
@ -114,9 +113,6 @@ export default {
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected'
}),
schema () {
return this.workspace.breadcrumbs.schema;
},

View File

@ -83,6 +83,12 @@
:connection="connection"
:trigger="workspace.breadcrumbs.trigger"
/>
<WorkspacePropsTabRoutine
v-show="selectedTab === 'prop' && workspace.breadcrumbs.procedure"
:is-selected="selectedTab === 'prop'"
:connection="connection"
:routine="workspace.breadcrumbs.procedure"
/>
<WorkspaceTableTab
v-show="selectedTab === 'data'"
:connection="connection"
@ -108,6 +114,7 @@ import WorkspaceTableTab from '@/components/WorkspaceTableTab';
import WorkspacePropsTab from '@/components/WorkspacePropsTab';
import WorkspacePropsTabView from '@/components/WorkspacePropsTabView';
import WorkspacePropsTabTrigger from '@/components/WorkspacePropsTabTrigger';
import WorkspacePropsTabRoutine from '@/components/WorkspacePropsTabRoutine';
export default {
name: 'Workspace',
@ -117,7 +124,8 @@ export default {
WorkspaceTableTab,
WorkspacePropsTab,
WorkspacePropsTabView,
WorkspacePropsTabTrigger
WorkspacePropsTabTrigger,
WorkspacePropsTabRoutine
},
props: {
connection: Object
@ -144,6 +152,7 @@ export default {
this.workspace.breadcrumbs.view === null &&
this.workspace.breadcrumbs.trigger === null &&
this.workspace.breadcrumbs.procedure === null &&
this.workspace.breadcrumbs.function === null &&
this.workspace.breadcrumbs.scheduler === null &&
['data', 'prop'].includes(this.workspace.selected_tab)
)

View File

@ -67,7 +67,7 @@
<div v-if="database.procedures.length" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure}">
<i class="misc-icon mdi mdi-18px mdi-folder-move mr-1" />
<i class="misc-icon mdi mdi-18px mdi-folder-sync mr-1" />
{{ $tc('word.storedRoutine', 2) }}
</summary>
<div class="accordion-body">
@ -82,7 +82,7 @@
@contextmenu.prevent="showMiscContext($event, {...procedure, type: 'procedure'})"
>
<a class="table-name">
<i class="table-icon mdi mdi-arrow-right-bold-box mdi-18px mr-1" />
<i class="table-icon mdi mdi-sync-circle mdi-18px mr-1" />
<span>{{ procedure.name }}</span>
</a>
</li>
@ -92,6 +92,34 @@
</details>
</div>
<div v-if="database.functions.length" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function}">
<i class="misc-icon mdi mdi-18px mdi-folder-move mr-1" />
{{ $tc('word.function', 2) }}
</summary>
<div class="accordion-body">
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="func of database.functions"
:key="func.name"
class="menu-item"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
@click="setBreadcrumbs({schema: database.name, function: func.name})"
@contextmenu.prevent="showMiscContext($event, {...func, type: 'function'})"
>
<a class="table-name">
<i class="table-icon mdi mdi-arrow-right-bold-box mdi-18px mr-1" />
<span>{{ func.name }}</span>
</a>
</li>
</ul>
</div>
</div>
</details>
</div>
<div v-if="database.schedulers.length" class="database-misc">
<details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler}">

View File

@ -16,10 +16,13 @@
<div class="context-element" @click="showCreateTriggerModal">
<span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ $tc('word.trigger', 1) }}</span>
</div>
<div class="context-element d-none" @click="false">
<span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box pr-1" /> {{ $tc('word.storedRoutine', 1) }}</span>
<div class="context-element" @click="false">
<span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle pr-1" /> {{ $tc('word.storedRoutine', 1) }}</span>
</div>
<div class="context-element d-none" @click="false">
<div class="context-element" @click="false">
<span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box pr-1" /> {{ $tc('word.function', 1) }}</span>
</div>
<div class="context-element" @click="false">
<span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ $tc('word.scheduler', 1) }}</span>
</div>
</div>

View File

@ -0,0 +1,145 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="400"
@confirm="confirmOptionsChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-cogs mr-1" /> {{ $t('word.options') }} "{{ localOptions.name }}"
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.name') }}
</label>
<div class="column">
<input
ref="firstInput"
v-model="optionsProxy.name"
class="form-input"
:class="{'is-error': !isTableNameValid}"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.definer') }}
</label>
<div class="column">
<select
v-if="workspace.users.length"
v-model="optionsProxy.definer"
class="form-select"
>
<option value="">
{{ $t('message.currentUser') }}
</option>
<option
v-for="user in workspace.users"
:key="`${user.name}@${user.host}`"
:value="`\`${user.name}\`@\`${user.host}\``"
>
{{ user.name }}@{{ user.host }}
</option>
</select>
<select v-if="!workspace.users.length" class="form-select">
<option value="">
{{ $t('message.currentUser') }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('word.comment') }}
</label>
<div class="column">
<input
v-model="optionsProxy.comment"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.sqlSecurity') }}
</label>
<div class="column">
<select v-model="optionsProxy.security" class="form-select">
<option>DEFINER</option>
<option>INVOKER</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.dataAccess') }}
</label>
<div class="column">
<select v-model="optionsProxy.dataAccess" class="form-select">
<option>CONTAINS SQL</option>
<option>NO SQL</option>
<option>READS SQL DATA</option>
<option>MODIFIES SQL DATA</option>
</select>
</div>
</div>
<div class="form-group">
<div class="col-4" />
<div class="column">
<label class="form-checkbox form-inline">
<input v-model="optionsProxy.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }}
</label>
</div>
</div>
</form>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsRoutineOptionsModal',
components: {
ConfirmModal
},
props: {
localOptions: Object,
workspace: Object
},
data () {
return {
optionsProxy: {},
isOptionsChanging: false
};
},
computed: {
isTableNameValid () {
return this.optionsProxy.name !== '';
}
},
created () {
this.optionsProxy = JSON.parse(JSON.stringify(this.localOptions));
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
confirmOptionsChange () {
if (!this.isTableNameValid)
this.optionsProxy.name = this.localOptions.name;
this.$emit('options-update', this.optionsProxy);
}
}
};
</script>

View File

@ -0,0 +1,287 @@
<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-dots-horizontal mr-1" /> {{ $t('word.parameters') }} "{{ routine }}"
</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="addParameter">
<span>{{ $t('word.add') }}</span>
<i class="mdi mdi-24px mdi-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="parametersPanel" class="panel-body p-0 pr-1">
<div
v-for="param in parametersProxy"
:key="param.name"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-param': selectedParam === param.name}"
@click="selectParameter($event, param.name)"
>
<div class="tile-icon">
<div>
<i class="mdi mdi-hexagon mdi-24px" :class="`type-${param.type.toLowerCase()}`" />
</div>
</div>
<div class="tile-content">
<div class="tile-title">
{{ param.name }}
</div>
<small class="tile-subtitle text-gray">{{ param.type }} · {{ param.context }}</small>
</div>
<div class="tile-action">
<button
class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')"
@click.prevent="removeParameter(param.name)"
>
<i class="mdi mdi-close" />
</button>
</div>
</div>
</div>
</div>
</div>
<div class="column col-7 pl-2 editor-col">
<form
v-if="selectedParamObj"
:style="{ height: modalInnerHeight + 'px'}"
class="form-horizontal"
>
<div class="form-group">
<label class="form-label col-3">
{{ $t('word.name') }}
</label>
<div class="column">
<input
v-model="selectedParamObj.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label col-3">
{{ $t('word.type') }}
</label>
<div class="column">
<select v-model="selectedParamObj.type" class="form-select text-uppercase">
<optgroup
v-for="group in workspace.dataTypes"
:key="group.group"
:label="group.group"
>
<option
v-for="type in group.types"
:key="type.name"
:selected="selectedParamObj.type.toUpperCase() === type.name"
:value="type.name"
>
{{ type.name }}
</option>
</optgroup>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-3">
{{ $t('word.context') }}
</label>
<div class="column">
<label class="form-radio">
<input
v-model="selectedParamObj.context"
type="radio"
name="context"
value="IN"
> <i class="form-icon" /> IN
</label>
<label class="form-radio">
<input
v-model="selectedParamObj.context"
type="radio"
value="OUT"
name="context"
> <i class="form-icon" /> OUT
</label>
<label class="form-radio">
<input
v-model="selectedParamObj.context"
type="radio"
value="INOUT"
name="context"
> <i class="form-icon" /> INOUT
</label>
</div>
</div>
</form>
<div v-if="!parametersProxy.length" class="empty">
<div class="empty-icon">
<i class="mdi mdi-dots-horizontal mdi-48px" />
</div>
<p class="empty-title h5">
{{ $t('message.thereAreNoParameters') }}
</p>
<div class="empty-action">
<button class="btn btn-primary" @click="addParameter">
{{ $t('message.createNewParameter') }}
</button>
</div>
</div>
</div>
</div>
</div>
</ConfirmModal>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsRoutineParamsModal',
components: {
ConfirmModal
},
props: {
localParameters: Array,
routine: String,
workspace: Object
},
data () {
return {
parametersProxy: [],
isOptionsChanging: false,
selectedParam: '',
modalInnerHeight: 400,
i: 1
};
},
computed: {
selectedParamObj () {
return this.parametersProxy.find(param => param.name === this.selectedParam);
},
isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
}
},
mounted () {
this.parametersProxy = JSON.parse(JSON.stringify(this.localParameters));
this.i = this.parametersProxy.length + 1;
if (this.parametersProxy.length)
this.resetSelectedID();
this.getModalInnerHeight();
window.addEventListener('resize', this.getModalInnerHeight);
},
destroyed () {
window.removeEventListener('resize', this.getModalInnerHeight);
},
methods: {
confirmIndexesChange () {
this.$emit('parameters-update', this.parametersProxy);
},
selectParameter (event, name) {
if (this.selectedParam !== name && !event.target.classList.contains('remove-field'))
this.selectedParam = name;
},
getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body');
if (modalBody)
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
},
addParameter () {
this.parametersProxy = [...this.parametersProxy, {
name: `Param${this.i++}`,
type: 'INT',
context: 'IN'
}];
if (this.parametersProxy.length === 1)
this.resetSelectedID();
setTimeout(() => {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60;
}, 20);
},
removeParameter (name) {
this.parametersProxy = this.parametersProxy.filter(param => param.name !== name);
if (this.selectedParam === name && this.parametersProxy.length)
this.resetSelectedID();
},
clearChanges () {
this.parametersProxy = JSON.parse(JSON.stringify(this.localParameters));
this.i = this.parametersProxy.length + 1;
if (!this.parametersProxy.some(param => param.name === this.selectedParam))
this.resetSelectedID();
},
resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0].name : '';
}
}
};
</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-param {
background: $bg-color-light;
opacity: 1;
}
}
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list {
max-height: 300px;
overflow: auto;
}
.remove-field .mdi {
pointer-events: none;
}
</style>

View File

@ -0,0 +1,262 @@
<template>
<div class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
@click="saveChanges"
>
<span>{{ $t('word.save') }}</span>
<i class="mdi mdi-24px mdi-content-save ml-1" />
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
<div class="divider-vert py-3" />
<button
class="btn btn-dark btn-sm"
:disabled="isChanged"
@click="false"
>
<span>{{ $t('word.run') }}</span>
<i class="mdi mdi-24px mdi-play ml-1" />
</button>
<button class="btn btn-dark btn-sm" @click="showParamsModal">
<span>{{ $t('word.parameters') }}</span>
<i class="mdi mdi-24px mdi-dots-horizontal ml-1" />
</button>
<button class="btn btn-dark btn-sm" @click="showOptionsModal">
<span>{{ $t('word.options') }}</span>
<i class="mdi mdi-24px mdi-cogs ml-1" />
</button>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2">
<label class="form-label ml-2">{{ $t('message.routineBody') }}</label>
<QueryEditor
v-if="isSelected"
ref="queryEditor"
:value.sync="localRoutine.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
<WorkspacePropsRoutineOptionsModal
v-if="isOptionsModal"
:local-options="localRoutine"
:workspace="workspace"
@hide="hideOptionsModal"
@options-update="optionsUpdate"
/>
<WorkspacePropsRoutineParamsModal
v-if="isParamsModal"
:local-parameters="localRoutine.parameters"
:workspace="workspace"
:routine="localRoutine.name"
@hide="hideParamsModal"
@parameters-update="parametersUpdate"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import QueryEditor from '@/components/QueryEditor';
import WorkspacePropsRoutineOptionsModal from '@/components/WorkspacePropsRoutineOptionsModal';
import WorkspacePropsRoutineParamsModal from '@/components/WorkspacePropsRoutineParamsModal';
import Routines from '@/ipc-api/Routines';
export default {
name: 'WorkspacePropsTabRoutine',
components: {
QueryEditor,
WorkspacePropsRoutineOptionsModal,
WorkspacePropsRoutineParamsModal
},
props: {
connection: Object,
routine: String
},
data () {
return {
tabUid: 'prop',
isQuering: false,
isSaving: false,
isOptionsModal: false,
isParamsModal: false,
originalRoutine: null,
localRoutine: { sql: '' },
lastRoutine: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
...mapGetters({
getWorkspace: 'workspaces/getWorkspace'
}),
workspace () {
return this.getWorkspace(this.connection.uid);
},
isSelected () {
return this.workspace.selected_tab === 'prop';
},
schema () {
return this.workspace.breadcrumbs.schema;
},
isChanged () {
return JSON.stringify(this.originalRoutine) !== JSON.stringify(this.localRoutine);
},
isDefinerInUsers () {
return this.originalRoutine ? this.workspace.users.some(user => this.originalRoutine.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
}
},
watch: {
async routine () {
if (this.isSelected) {
await this.getRoutineData();
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql);
this.lastRoutine = this.routine;
}
},
async isSelected (val) {
if (val && this.lastRoutine !== this.routine) {
await this.getRoutineData();
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql);
this.lastRoutine = this.routine;
}
},
isChanged (val) {
if (this.isSelected && this.lastRoutine === this.routine && this.routine !== null)
this.setUnsavedChanges(val);
}
},
mounted () {
window.addEventListener('resize', this.resizeQueryEditor);
},
destroyed () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
refreshStructure: 'workspaces/refreshStructure',
setUnsavedChanges: 'workspaces/setUnsavedChanges',
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
}),
async getRoutineData () {
if (!this.routine) return;
this.isQuering = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
routine: this.workspace.breadcrumbs.procedure
};
try {
const { status, response } = await Routines.getRoutineInformations(params);
if (status === 'success') {
this.originalRoutine = response;
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
this.sqlProxy = this.localRoutine.sql;
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.resizeQueryEditor();
this.isQuering = false;
},
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
routine: {
...this.localRoutine,
oldName: this.originalRoutine.name
}
};
try {
const { status, response } = await Routines.alterRoutine(params);
if (status === 'success') {
const oldName = this.originalRoutine.name;
await this.refreshStructure(this.connection.uid);
if (oldName !== this.localRoutine.name) {
this.setUnsavedChanges(false);
this.changeBreadcrumbs({ schema: this.schema, procedure: this.localRoutine.name });
}
this.getRoutineData();
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
optionsUpdate (options) {
this.localRoutine = options;
},
parametersUpdate (parameters) {
this.localRoutine = { ...this.localRoutine, parameters };
},
showOptionsModal () {
this.isOptionsModal = true;
},
hideOptionsModal () {
this.isOptionsModal = false;
},
showParamsModal () {
this.isParamsModal = true;
},
hideParamsModal () {
this.isParamsModal = false;
}
}
};
</script>

View File

@ -74,7 +74,11 @@ module.exports = {
trigger: 'Trigger | Triggers',
storedRoutine: 'Stored routine | Stored routines',
scheduler: 'Scheduler | Schedulers',
event: 'Event'
event: 'Event',
parameters: 'Parameters',
function: 'Function | Functions',
deterministic: 'Deterministic',
context: 'Context'
},
message: {
appWelcome: 'Welcome to Antares SQL Client!',
@ -148,7 +152,11 @@ module.exports = {
createNewView: 'Create new view',
deleteTrigger: 'Delete trigger',
createNewTrigger: 'Create new trigger',
currentUser: 'Current user'
currentUser: 'Current user',
routineBody: 'Routine body',
dataAccess: 'Data access',
thereAreNoParameters: 'There are no parameters',
createNewParameter: 'Create new parameter'
},
// Date and Time
short: {

View File

@ -0,0 +1,20 @@
'use strict';
import { ipcRenderer } from 'electron';
export default class {
static getRoutineInformations (params) {
return ipcRenderer.invoke('get-routine-informations', params);
}
static dropRoutine (params) {
return ipcRenderer.invoke('drop-routine', params);
}
static alterRoutine (params) {
return ipcRenderer.invoke('alter-routine', params);
}
static createRoutine (params) {
return ipcRenderer.invoke('create-routine', params);
}
}

View File

@ -345,6 +345,7 @@ export default {
table: null,
trigger: null,
procedure: null,
function: null,
scheduler: null,
view: null
};