refactor: ts and composition api on more components

This commit is contained in:
Fabio Di Stasio 2022-06-04 18:37:16 +02:00
parent 2007305ff0
commit 7fc01227e7
25 changed files with 1256 additions and 1476 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "antares", "name": "antares",
"version": "0.5.4", "version": "0.5.5",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "antares", "name": "antares",
"version": "0.5.4", "version": "0.5.5",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@ -1,10 +1,16 @@
import * as mysql from 'common/customizations/mysql'; import * as mysql from 'common/customizations/mysql';
import * as postgresql from 'common/customizations/postgresql'; import * as postgresql from 'common/customizations/postgresql';
import * as sqlite from 'common/customizations/sqlite'; import * as sqlite from 'common/customizations/sqlite';
import { Customizations } from 'common/interfaces/customizations';
export default { export default {
maria: mysql.customizations, maria: mysql.customizations,
mysql: mysql.customizations, mysql: mysql.customizations,
pg: postgresql.customizations, pg: postgresql.customizations,
sqlite: sqlite.customizations sqlite: sqlite.customizations
} as {
maria: Customizations;
mysql: Customizations;
pg: Customizations;
sqlite: Customizations;
}; };

View File

@ -104,8 +104,6 @@ export default (connections: {[key: string]: antares.Client}) => {
escapedParam = `"${sqlEscaper(params.content)}"`; escapedParam = `"${sqlEscaper(params.content)}"`;
break; break;
case 'pg': case 'pg':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break;
case 'sqlite': case 'sqlite':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`; escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break; break;
@ -248,8 +246,7 @@ export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('insert-table-rows', async (event, params) => { ipcMain.handle('insert-table-rows', async (event, params) => {
try { // TODO: move to client classes try { // TODO: move to client classes
// eslint-disable-next-line @typescript-eslint/no-explicit-any const insertObj: {[key: string]: string | number | boolean | Date | Buffer} = {};
const insertObj: {[key: string]: any} = {};
for (const key in params.row) { for (const key in params.row) {
const type = params.fields[key]; const type = params.fields[key];
let escapedParam; let escapedParam;
@ -318,12 +315,10 @@ export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('insert-table-fake-rows', async (event, params: InsertRowsParams) => { ipcMain.handle('insert-table-fake-rows', async (event, params: InsertRowsParams) => {
try { // TODO: move to client classes try { // TODO: move to client classes
// eslint-disable-next-line @typescript-eslint/no-explicit-any const rows: {[key: string]: string | number | boolean | Date | Buffer}[] = [];
const rows: {[key: string]: any}[] = [];
for (let i = 0; i < +params.repeat; i++) { for (let i = 0; i < +params.repeat; i++) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any const insertObj: {[key: string]: string | number | boolean | Date | Buffer} = {};
const insertObj: {[key: string]: any} = {};
for (const key in params.row) { for (const key in params.row) {
const type = params.fields[key]; const type = params.fields[key];
@ -341,6 +336,7 @@ export default (connections: {[key: string]: antares.Client}) => {
escapedParam = `"${sqlEscaper(params.row[key].value)}"`; escapedParam = `"${sqlEscaper(params.row[key].value)}"`;
break; break;
case 'pg': case 'pg':
case 'sqlite':
escapedParam = `'${params.row[key].value.replaceAll('\'', '\'\'')}'`; escapedParam = `'${params.row[key].value.replaceAll('\'', '\'\'')}'`;
break; break;
} }
@ -381,8 +377,7 @@ export default (connections: {[key: string]: antares.Client}) => {
insertObj[key] = escapedParam; insertObj[key] = escapedParam;
} }
else { // Faker value else { // Faker value
// eslint-disable-next-line @typescript-eslint/no-explicit-any const parsedParams: {[key: string]: string | number | boolean | Date | Buffer} = {};
const parsedParams: {[key: string]: any} = {};
let fakeValue; let fakeValue;
if (params.locale) if (params.locale)
@ -402,7 +397,7 @@ export default (connections: {[key: string]: antares.Client}) => {
if (typeof fakeValue === 'string') { if (typeof fakeValue === 'string') {
if (params.row[key].length) if (params.row[key].length)
fakeValue = fakeValue.substr(0, params.row[key].length); fakeValue = fakeValue.substring(0, params.row[key].length);
fakeValue = `'${sqlEscaper(fakeValue)}'`; fakeValue = `'${sqlEscaper(fakeValue)}'`;
} }
else if ([...DATE, ...DATETIME].includes(type)) else if ([...DATE, ...DATETIME].includes(type))

View File

@ -1,7 +1,7 @@
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import Store from 'electron-store'; import * as Store from 'electron-store';
const persistentStore = new Store({ name: 'settings' }); const persistentStore = new Store({ name: 'settings', clearInvalidConfig: true });
const isMacOS = process.platform === 'darwin'; const isMacOS = process.platform === 'darwin';
let mainWindow: Electron.IpcMainEvent; let mainWindow: Electron.IpcMainEvent;

View File

@ -37,6 +37,8 @@
v-if="isOpen" v-if="isOpen"
ref="optionList" ref="optionList"
:class="`select__list-wrapper ${dropdownClass ? dropdownClass : '' }`" :class="`select__list-wrapper ${dropdownClass ? dropdownClass : '' }`"
@mousedown="isMouseDown = true"
@mouseup="handleMouseUpEvent()"
> >
<ul class="select__list" @mousedown.prevent> <ul class="select__list" @mousedown.prevent>
<li <li
@ -136,6 +138,7 @@ export default defineComponent({
setup (props, { emit }) { setup (props, { emit }) {
const hightlightedIndex = ref(0); const hightlightedIndex = ref(0);
const isOpen = ref(false); const isOpen = ref(false);
const isMouseDown = ref(false);
const internalValue = ref(props.modelValue || props.value); const internalValue = ref(props.modelValue || props.value);
const el = ref(null); const el = ref(null);
const searchInput = ref(null); const searchInput = ref(null);
@ -151,7 +154,7 @@ export default defineComponent({
if (typeof prop === 'function') if (typeof prop === 'function')
return prop(item); return prop(item);
return item[prop] || item; return item[prop] !== undefined ? item[prop] : item;
}; };
const flattenOptions = computed(() => { const flattenOptions = computed(() => {
@ -319,12 +322,24 @@ export default defineComponent({
}; };
const handleBlurEvent = () => { const handleBlurEvent = () => {
if (isMouseDown.value) return;
deactivate(); deactivate();
emit('blur'); emit('blur');
}; };
const handleMouseUpEvent = () => {
isMouseDown.value = false;
searchInput.value.focus();
};
const handleWheelEvent = (e) => {
if (!e.target.className.includes('select__')) deactivate();
};
onMounted(() => { onMounted(() => {
window.addEventListener('resize', adjustListPosition); window.addEventListener('resize', adjustListPosition);
window.addEventListener('wheel', handleWheelEvent);
nextTick(() => { nextTick(() => {
// fix position when the component is created and opened at the same time // fix position when the component is created and opened at the same time
if (isOpen.value) { if (isOpen.value) {
@ -336,6 +351,7 @@ export default defineComponent({
}); });
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('resize', adjustListPosition); window.removeEventListener('resize', adjustListPosition);
window.removeEventListener('wheel', handleWheelEvent);
}); });
return { return {
@ -351,10 +367,12 @@ export default defineComponent({
isSelected, isSelected,
keyArrows, keyArrows,
isOpen, isOpen,
isMouseDown,
hightlightedIndex, hightlightedIndex,
optionList, optionList,
optionRefs, optionRefs,
handleBlurEvent handleBlurEvent,
handleMouseUpEvent
}; };
} }
}); });

View File

@ -101,17 +101,10 @@
import { Component, computed, ComputedRef, onBeforeUnmount, onMounted, onUpdated, Prop, Ref, ref, watch } from 'vue'; import { Component, computed, ComputedRef, onBeforeUnmount, onMounted, onUpdated, Prop, Ref, ref, watch } from 'vue';
import * as moment from 'moment'; import * as moment from 'moment';
import { ConnectionParams } from 'common/interfaces/antares'; import { ConnectionParams } from 'common/interfaces/antares';
import { useHistoryStore } from '@/stores/history'; import { HistoryRecord, useHistoryStore } from '@/stores/history';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue'; import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
interface HistoryRow {
uid:string;
sql: string;
schema: string;
date: string;
}
const { getHistoryByWorkspace, deleteQueryFromHistory } = useHistoryStore(); const { getHistoryByWorkspace, deleteQueryFromHistory } = useHistoryStore();
const { getConnectionName } = useConnectionsStore(); const { getConnectionName } = useConnectionsStore();
@ -132,7 +125,7 @@ const searchTerm = ref('');
const localSearchTerm = ref(''); const localSearchTerm = ref('');
const connectionName = computed(() => getConnectionName(props.connection.uid)); const connectionName = computed(() => getConnectionName(props.connection.uid));
const history: ComputedRef<HistoryRow[]> = computed(() => (getHistoryByWorkspace(props.connection.uid) || [])); const history: ComputedRef<HistoryRecord[]> = computed(() => (getHistoryByWorkspace(props.connection.uid) || []));
const filteredHistory = computed(() => history.value.filter(q => q.sql.toLowerCase().search(searchTerm.value.toLowerCase()) >= 0)); const filteredHistory = computed(() => history.value.filter(q => q.sql.toLowerCase().search(searchTerm.value.toLowerCase()) >= 0));
watch(searchTerm, () => { watch(searchTerm, () => {
@ -147,7 +140,7 @@ const copyQuery = (sql: string) => {
navigator.clipboard.writeText(sql); navigator.clipboard.writeText(sql);
}; };
const deleteQuery = (query: HistoryRow[]) => { const deleteQuery = (query: HistoryRecord[]) => {
deleteQueryFromHistory({ deleteQueryFromHistory({
workspace: props.connection.uid, workspace: props.connection.uid,
...query ...query

View File

@ -8,7 +8,8 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed, onMounted, Prop, Ref, ref, toRef, watch } from 'vue';
import * as ace from 'ace-builds'; import * as ace from 'ace-builds';
import 'ace-builds/webpack-resolver'; import 'ace-builds/webpack-resolver';
import '../libs/ext-language_tools'; import '../libs/ext-language_tools';
@ -16,329 +17,330 @@ import { storeToRefs } from 'pinia';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { Workspace } from '@/stores/workspaces';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
export default { const editor: Ref<ace.Ace.Editor> = ref(null);
name: 'QueryEditor', const applicationStore = useApplicationStore();
props: { const settingsStore = useSettingsStore();
modelValue: String,
workspace: Object,
isSelected: Boolean,
schema: { type: String, default: '' },
autoFocus: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false },
height: { type: Number, default: 200 }
},
emits: ['update:modelValue'],
setup () {
const editor = null;
const applicationStore = useApplicationStore();
const settingsStore = useSettingsStore();
const { setBaseCompleters } = applicationStore; const { setBaseCompleters } = applicationStore;
const { baseCompleter } = storeToRefs(applicationStore); const { baseCompleter } = storeToRefs(applicationStore);
const { const {
editorTheme, editorTheme,
editorFontSize, editorFontSize,
autoComplete, autoComplete,
lineWrap lineWrap
} = storeToRefs(settingsStore); } = storeToRefs(settingsStore);
return { const props = defineProps({
editor, modelValue: String,
baseCompleter, workspace: Object as Prop<Workspace>,
setBaseCompleters, isSelected: Boolean,
editorTheme, schema: { type: String, default: '' },
editorFontSize, autoFocus: { type: Boolean, default: false },
autoComplete, readOnly: { type: Boolean, default: false },
lineWrap height: { type: Number, default: 200 }
}; });
},
data () { const emit = defineEmits(['update:modelValue']);
return {
cursorPosition: 0, const cursorPosition = ref(0);
fields: [], const fields = ref([]);
customCompleter: [], const customCompleter = ref([]);
id: uidGen(), const id = ref(uidGen());
lastSchema: null const lastSchema: Ref<string> = ref(null);
};
}, const tables = computed(() => {
computed: { return props.workspace
tables () { ? props.workspace.structure.filter(schema => schema.name === props.schema)
return this.workspace .reduce((acc, curr) => {
? this.workspace.structure.filter(schema => schema.name === this.schema) acc.push(...curr.tables);
.reduce((acc, curr) => { return acc;
acc.push(...curr.tables); }, []).map(table => {
return acc; return {
}, []).map(table => { name: table.name as string,
return { type: table.type as string,
name: table.name, fields: []
type: table.type, };
fields: [] })
}; : [];
}) });
: [];
}, const triggers = computed(() => {
triggers () { return props.workspace
return this.workspace ? props.workspace.structure.filter(schema => schema.name === props.schema)
? this.workspace.structure.filter(schema => schema.name === this.schema) .reduce((acc, curr) => {
.reduce((acc, curr) => { acc.push(...curr.triggers);
acc.push(...curr.triggers); return acc;
return acc; }, []).map(trigger => {
}, []).map(trigger => { return {
return { name: trigger.name as string,
name: trigger.name, type: 'trigger'
type: 'trigger' };
}; })
}) : [];
: []; });
},
procedures () { const procedures = computed(() => {
return this.workspace return props.workspace
? this.workspace.structure.filter(schema => schema.name === this.schema) ? props.workspace.structure.filter(schema => schema.name === props.schema)
.reduce((acc, curr) => { .reduce((acc, curr) => {
acc.push(...curr.procedures); acc.push(...curr.procedures);
return acc; return acc;
}, []).map(procedure => { }, []).map(procedure => {
return { return {
name: `${procedure.name}()`, name: `${procedure.name}()`,
type: 'routine' type: 'routine'
}; };
}) })
: []; : [];
}, });
functions () {
return this.workspace const functions = computed(() => {
? this.workspace.structure.filter(schema => schema.name === this.schema) return props.workspace
.reduce((acc, curr) => { ? props.workspace.structure.filter(schema => schema.name === props.schema)
acc.push(...curr.functions); .reduce((acc, curr) => {
return acc; acc.push(...curr.functions);
}, []).map(func => { return acc;
return { }, []).map(func => {
name: `${func.name}()`, return {
type: 'function' name: `${func.name}()`,
}; type: 'function'
}) };
: []; })
}, : [];
schedulers () { });
return this.workspace
? this.workspace.structure.filter(schema => schema.name === this.schema) const schedulers = computed(() => {
.reduce((acc, curr) => { return props.workspace
acc.push(...curr.schedulers); ? props.workspace.structure.filter(schema => schema.name === props.schema)
return acc; .reduce((acc, curr) => {
}, []).map(scheduler => { acc.push(...curr.schedulers);
return { return acc;
name: scheduler.name, }, []).map(scheduler => {
type: 'scheduler' return {
}; name: scheduler.name as string,
}) type: 'scheduler'
: []; };
}, })
mode () { : [];
switch (this.workspace.client) { });
case 'mysql':
case 'maria': const mode = computed(() => {
return 'mysql'; switch (props.workspace.client) {
case 'mssql': case 'mysql':
return 'sqlserver'; case 'maria':
case 'pg': return 'mysql';
return 'pgsql'; case 'mssql':
default: return 'sqlserver';
return 'sql'; case 'pg':
} return 'pgsql';
}, default:
lastWord () { return 'sql';
const charsBefore = this.modelValue.slice(0, this.cursorPosition); }
const words = charsBefore.replaceAll('\n', ' ').split(' ').filter(Boolean); });
return words.pop();
}, const lastWord = computed(() => {
isLastWordATable () { const charsBefore = props.modelValue.slice(0, cursorPosition.value);
return /\w+\.\w*/gm.test(this.lastWord); const words = charsBefore.replaceAll('\n', ' ').split(' ').filter(Boolean);
}, return words.pop();
fieldsCompleter () { });
return {
getCompletions: (editor, session, pos, prefix, callback) => { const isLastWordATable = computed(() => /\w+\.\w*/gm.test(lastWord.value));
const completions = [];
this.fields.forEach(field => { const fieldsCompleter = computed(() => {
completions.push({ return {
value: field, getCompletions: (editor: never, session: never, pos: never, prefix: never, callback: (err: null, response: ace.Ace.Completion[]) => void) => {
meta: 'column', const completions: ace.Ace.Completion[] = [];
score: 1000 fields.value.forEach(field => {
}); completions.push({
}); value: field,
callback(null, completions); meta: 'column',
} score: 1000
}; });
});
callback(null, completions);
} }
}, };
watch: { });
modelValue () {
this.cursorPosition = this.editor.session.doc.positionToIndex(this.editor.getCursorPosition());
},
editorTheme () {
if (this.editor)
this.editor.setTheme(`ace/theme/${this.editorTheme}`);
},
editorFontSize () {
const sizes = {
small: '12px',
medium: '14px',
large: '16px'
};
if (this.editor) { const setCustomCompleter = () => {
this.editor.setOptions({ editor.value.completers.push({
fontSize: sizes[this.editorFontSize] getCompletions: (editor, session, pos, prefix, callback: (err: null, response: ace.Ace.Completion[]) => void) => {
const completions: ace.Ace.Completion[] = [];
[
...tables.value,
...triggers.value,
...procedures.value,
...functions.value,
...schedulers.value
].forEach(el => {
completions.push({
value: el.name,
meta: el.type,
score: 1000
}); });
} });
}, callback(null, completions);
autoComplete () {
if (this.editor) {
this.editor.setOptions({
enableLiveAutocompletion: this.autoComplete
});
}
},
lineWrap () {
if (this.editor) {
this.editor.setOptions({
wrap: this.lineWrap
});
}
},
isSelected () {
if (this.isSelected) {
this.lastSchema = this.schema;
this.editor.resize();
}
},
height () {
setTimeout(() => {
this.editor.resize();
}, 20);
},
lastSchema () {
if (this.editor) {
this.editor.completers = this.baseCompleter.map(el => Object.assign({}, el));
this.setCustomCompleter();
}
} }
}, });
created () {
this.lastSchema = this.schema; customCompleter.value = editor.value.completers;
}, };
mounted () {
this.editor = ace.edit(`editor-${this.id}`, { watch(() => props.modelValue, () => {
mode: `ace/mode/${this.mode}`, // eslint-disable-next-line @typescript-eslint/no-explicit-any
theme: `ace/theme/${this.editorTheme}`, cursorPosition.value = (editor.value.session as any).doc.positionToIndex(editor.value.getCursorPosition());
value: this.modelValue, });
fontSize: '14px',
printMargin: false, watch(editorTheme, () => {
readOnly: this.readOnly if (editor.value)
editor.value.setTheme(`ace/theme/${editorTheme.value}`);
});
watch(editorFontSize, () => {
const sizes = {
small: '12px',
medium: '14px',
large: '16px'
};
if (editor.value) {
editor.value.setOptions({
fontSize: sizes[editorFontSize.value]
}); });
}
});
this.editor.setOptions({ watch(autoComplete, () => {
enableBasicAutocompletion: true, if (editor.value) {
wrap: this.lineWrap, editor.value.setOptions({
enableSnippets: true, enableLiveAutocompletion: autoComplete.value
enableLiveAutocompletion: this.autoComplete
}); });
}
});
if (!this.baseCompleter.length) watch(lineWrap, () => {
this.setBaseCompleters(this.editor.completers.map(el => Object.assign({}, el))); if (editor.value) {
editor.value.setOptions({
wrap: lineWrap.value
});
}
});
this.setCustomCompleter(); watch(() => props.isSelected, () => {
if (props.isSelected) {
lastSchema.value = props.schema;
editor.value.resize();
}
});
this.editor.commands.on('afterExec', e => { watch(() => props.height, () => {
if (['insertstring', 'backspace', 'del'].includes(e.command.name)) { setTimeout(() => {
if (this.isLastWordATable || e.args === '.') { editor.value.resize();
if (e.args !== ' ') { }, 20);
const table = this.tables.find(t => t.name === this.lastWord.split('.').pop().trim()); });
if (table) { watch(lastSchema, () => {
const params = { if (editor.value) {
uid: this.workspace.uid, editor.value.completers = baseCompleter.value.map(el => Object.assign({}, el));
schema: this.schema, setCustomCompleter();
table: table.name }
}; });
Tables.getTableColumns(params).then(res => { lastSchema.value = toRef(props, 'schema').value;
if (res.response.length)
this.fields = res.response.map(field => field.name); onMounted(() => {
this.editor.completers = [this.fieldsCompleter]; editor.value = ace.edit(`editor-${id.value}`, {
this.editor.execCommand('startAutocomplete'); mode: `ace/mode/${mode.value}`,
}).catch(console.log); theme: `ace/theme/${editorTheme.value}`,
} value: props.modelValue,
else fontSize: 14,
this.editor.completers = this.customCompleter; printMargin: false,
readOnly: props.readOnly
});
editor.value.setOptions({
enableBasicAutocompletion: true,
wrap: lineWrap.value,
enableSnippets: true,
enableLiveAutocompletion: autoComplete.value
});
if (!baseCompleter.value.length)
setBaseCompleters(editor.value.completers.map(el => Object.assign({}, el)));
setCustomCompleter();
editor.value.commands.on('afterExec', (e: { args: string; command: { name: string } }) => {
if (['insertstring', 'backspace', 'del'].includes(e.command.name)) {
if (isLastWordATable.value || e.args === '.') {
if (e.args !== ' ') {
const table = tables.value.find(t => t.name === lastWord.value.split('.').pop().trim());
if (table) {
const params = {
uid: props.workspace.uid,
schema: props.schema,
table: table.name
};
Tables.getTableColumns(params).then(res => {
if (res.response.length)
fields.value = res.response.map((field: { name: string }) => field.name);
editor.value.completers = [fieldsCompleter.value];
editor.value.execCommand('startAutocomplete');
}).catch(console.log);
} }
else else
this.editor.completers = this.customCompleter; editor.value.completers = customCompleter.value;
} }
else else
this.editor.completers = this.customCompleter; editor.value.completers = customCompleter.value;
} }
});
this.editor.session.on('change', () => {
const content = this.editor.getValue();
this.$emit('update:modelValue', content);
});
this.editor.on('guttermousedown', e => {
const target = e.domEvent.target;
if (target.className.indexOf('ace_gutter-cell') === -1)
return;
if (e.clientX > 25 + target.getBoundingClientRect().left)
return;
const row = e.getDocumentPosition().row;
const breakpoints = e.editor.session.getBreakpoints(row, 0);
if (typeof breakpoints[row] === typeof undefined)
e.editor.session.setBreakpoint(row);
else else
e.editor.session.clearBreakpoint(row); editor.value.completers = customCompleter.value;
e.stop();
});
if (this.autoFocus) {
setTimeout(() => {
this.editor.focus();
this.editor.resize();
}, 20);
} }
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(editor.value.session as any).on('change', () => {
const content = editor.value.getValue();
emit('update:modelValue', content);
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(editor.value as any).on('guttermousedown', (e: any) => {
const target = e.domEvent.target;
if (target.className.indexOf('ace_gutter-cell') === -1)
return;
if (e.clientX > 25 + target.getBoundingClientRect().left)
return;
const row = e.getDocumentPosition().row;
const breakpoints = e.editor.value.session.getBreakpoints(row, 0);
if (typeof breakpoints[row] === typeof undefined)
e.editor.value.session.setBreakpoint(row);
else
e.editor.value.session.clearBreakpoint(row);
e.stop();
});
if (props.autoFocus) {
setTimeout(() => { setTimeout(() => {
this.editor.resize(); editor.value.focus();
editor.value.resize();
}, 20); }, 20);
},
methods: {
setCustomCompleter () {
this.editor.completers.push({
getCompletions: (editor, session, pos, prefix, callback) => {
const completions = [];
[
...this.tables,
...this.triggers,
...this.procedures,
...this.functions,
...this.schedulers
].forEach(el => {
completions.push({
value: el.name,
meta: el.type
});
});
callback(null, completions);
}
});
this.customCompleter = this.editor.completers;
}
} }
};
setTimeout(() => {
editor.value.resize();
}, 20);
});
defineExpose({ editor });
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -29,83 +29,66 @@
</BaseContextMenu> </BaseContextMenu>
</template> </template>
<script> <script setup lang="ts">
import { computed, Prop, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu.vue';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import { storeToRefs } from 'pinia'; import { ConnectionParams } from 'common/interfaces/antares';
export default { const {
name: 'SettingBarContext', getConnectionName,
components: { addConnection,
BaseContextMenu, deleteConnection
ConfirmModal } = useConnectionsStore();
}, const workspacesStore = useWorkspacesStore();
props: { const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
contextEvent: MouseEvent,
contextConnection: Object
},
emits: ['close-context'],
setup () {
const {
getConnectionName,
addConnection,
deleteConnection
} = useConnectionsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { selectWorkspace } = workspacesStore; const { selectWorkspace } = workspacesStore;
return { const props = defineProps({
getConnectionName, contextEvent: MouseEvent,
addConnection, contextConnection: Object as Prop<ConnectionParams>
deleteConnection, });
selectedWorkspace,
selectWorkspace
};
},
data () {
return {
isConfirmModal: false,
isEditModal: false
};
},
computed: {
connectionName () {
return this.getConnectionName(this.contextConnection.uid);
}
},
methods: {
confirmDeleteConnection () {
if (this.selectedWorkspace === this.contextConnection.uid)
this.selectWorkspace();
this.deleteConnection(this.contextConnection);
this.closeContext();
},
duplicateConnection () {
let connectionCopy = Object.assign({}, this.contextConnection);
connectionCopy = {
...connectionCopy,
uid: uidGen('C'),
name: connectionCopy.name ? `${connectionCopy?.name}_copy` : ''
};
this.addConnection(connectionCopy); const emit = defineEmits(['close-context']);
this.closeContext();
}, const isConfirmModal = ref(false);
showConfirmModal () {
this.isConfirmModal = true; const connectionName = computed(() => getConnectionName(props.contextConnection.uid));
},
hideConfirmModal () { const confirmDeleteConnection = () => {
this.isConfirmModal = false; if (selectedWorkspace.value === props.contextConnection.uid)
this.closeContext(); selectWorkspace(null);
}, deleteConnection(props.contextConnection);
closeContext () { closeContext();
this.$emit('close-context'); };
}
} const duplicateConnection = () => {
let connectionCopy = Object.assign({}, props.contextConnection);
connectionCopy = {
...connectionCopy,
uid: uidGen('C'),
name: connectionCopy.name ? `${connectionCopy?.name}_copy` : ''
};
addConnection(connectionCopy);
closeContext();
};
const showConfirmModal = () => {
isConfirmModal.value = true;
};
const hideConfirmModal = () => {
isConfirmModal.value = false;
closeContext();
};
const closeContext = () => {
emit('close-context');
}; };
</script> </script>

View File

@ -29,7 +29,7 @@ const settingsStore = useSettingsStore();
const { removeNotification } = notificationsStore; const { removeNotification } = notificationsStore;
const { notifications } = storeToRefs(notificationsStore); const { notifications } = storeToRefs(notificationsStore);
const { notificationsTimeout } = storeToRefs(settingsStore) as {notificationsTimeout: Ref<number>};// TODO: temp const { notificationsTimeout } = storeToRefs(settingsStore);
const timeouts: Ref<{[key: string]: NodeJS.Timeout}> = ref({}); const timeouts: Ref<{[key: string]: NodeJS.Timeout}> = ref({});

View File

@ -43,7 +43,7 @@ const { notes } = storeToRefs(scratchpadStore);
const { changeNotes } = scratchpadStore; const { changeNotes } = scratchpadStore;
const { hideScratchpad } = applicationStore; const { hideScratchpad } = applicationStore;
const localNotes: Ref<string> = ref(notes.value as string);// TODO: temp const localNotes = ref(notes.value);
const debounceTimeout: Ref<NodeJS.Timeout> = ref(null); const debounceTimeout: Ref<NodeJS.Timeout> = ref(null);
watch(localNotes, () => { watch(localNotes, () => {

View File

@ -86,7 +86,7 @@ const connections = computed({
get () { get () {
return getConnections.value; return getConnections.value;
}, },
set (value) { set (value: ConnectionParams[]) {
updateConnections(value); updateConnections(value);
} }
}); });

View File

@ -484,246 +484,196 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed, onBeforeUnmount, Prop, ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import Draggable from 'vuedraggable'; import * as Draggable from 'vuedraggable';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore, WorkspaceTab } from '@/stores/workspaces';
import { ConnectionParams } from 'common/interfaces/antares';
import WorkspaceEmptyState from '@/components/WorkspaceEmptyState'; import WorkspaceEmptyState from '@/components/WorkspaceEmptyState.vue';
import WorkspaceExploreBar from '@/components/WorkspaceExploreBar'; import WorkspaceExploreBar from '@/components/WorkspaceExploreBar.vue';
import WorkspaceEditConnectionPanel from '@/components/WorkspaceEditConnectionPanel'; import WorkspaceEditConnectionPanel from '@/components/WorkspaceEditConnectionPanel.vue';
import WorkspaceTabQuery from '@/components/WorkspaceTabQuery'; import WorkspaceTabQuery from '@/components/WorkspaceTabQuery.vue';
import WorkspaceTabTable from '@/components/WorkspaceTabTable'; import WorkspaceTabTable from '@/components/WorkspaceTabTable.vue';
import WorkspaceTabNewTable from '@/components/WorkspaceTabNewTable'; import WorkspaceTabNewTable from '@/components/WorkspaceTabNewTable.vue';
import WorkspaceTabNewView from '@/components/WorkspaceTabNewView'; import WorkspaceTabNewView from '@/components/WorkspaceTabNewView.vue';
import WorkspaceTabNewTrigger from '@/components/WorkspaceTabNewTrigger'; import WorkspaceTabNewTrigger from '@/components/WorkspaceTabNewTrigger.vue';
import WorkspaceTabNewRoutine from '@/components/WorkspaceTabNewRoutine'; import WorkspaceTabNewRoutine from '@/components/WorkspaceTabNewRoutine.vue';
import WorkspaceTabNewFunction from '@/components/WorkspaceTabNewFunction'; import WorkspaceTabNewFunction from '@/components/WorkspaceTabNewFunction.vue';
import WorkspaceTabNewScheduler from '@/components/WorkspaceTabNewScheduler'; import WorkspaceTabNewScheduler from '@/components/WorkspaceTabNewScheduler.vue';
import WorkspaceTabNewTriggerFunction from '@/components/WorkspaceTabNewTriggerFunction'; import WorkspaceTabNewTriggerFunction from '@/components/WorkspaceTabNewTriggerFunction.vue';
import WorkspaceTabPropsTable from '@/components/WorkspaceTabPropsTable'; import WorkspaceTabPropsTable from '@/components/WorkspaceTabPropsTable.vue';
import WorkspaceTabPropsView from '@/components/WorkspaceTabPropsView'; import WorkspaceTabPropsView from '@/components/WorkspaceTabPropsView.vue';
import WorkspaceTabPropsTrigger from '@/components/WorkspaceTabPropsTrigger'; import WorkspaceTabPropsTrigger from '@/components/WorkspaceTabPropsTrigger.vue';
import WorkspaceTabPropsTriggerFunction from '@/components/WorkspaceTabPropsTriggerFunction'; import WorkspaceTabPropsTriggerFunction from '@/components/WorkspaceTabPropsTriggerFunction.vue';
import WorkspaceTabPropsRoutine from '@/components/WorkspaceTabPropsRoutine'; import WorkspaceTabPropsRoutine from '@/components/WorkspaceTabPropsRoutine.vue';
import WorkspaceTabPropsFunction from '@/components/WorkspaceTabPropsFunction'; import WorkspaceTabPropsFunction from '@/components/WorkspaceTabPropsFunction.vue';
import WorkspaceTabPropsScheduler from '@/components/WorkspaceTabPropsScheduler'; import WorkspaceTabPropsScheduler from '@/components/WorkspaceTabPropsScheduler.vue';
import ModalProcessesList from '@/components/ModalProcessesList'; import ModalProcessesList from '@/components/ModalProcessesList.vue';
import ModalDiscardChanges from '@/components/ModalDiscardChanges'; import ModalDiscardChanges from '@/components/ModalDiscardChanges.vue';
export default { const workspacesStore = useWorkspacesStore();
name: 'Workspace',
components: { const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
Draggable,
WorkspaceEmptyState, const {
WorkspaceExploreBar, getWorkspace,
WorkspaceEditConnectionPanel, addWorkspace,
WorkspaceTabQuery, connectWorkspace,
WorkspaceTabTable, selectTab,
WorkspaceTabNewTable, newTab,
WorkspaceTabPropsTable, removeTab,
WorkspaceTabNewView, updateTabs
WorkspaceTabPropsView, } = workspacesStore;
WorkspaceTabNewTrigger,
WorkspaceTabPropsTrigger, const props = defineProps({
WorkspaceTabNewTriggerFunction, connection: Object as Prop<ConnectionParams>
WorkspaceTabPropsTriggerFunction, });
WorkspaceTabNewRoutine,
WorkspaceTabNewFunction, const hasWheelEvent = ref(false);
WorkspaceTabPropsRoutine, const isProcessesModal = ref(false);
WorkspaceTabPropsFunction, const unsavedTab = ref(null);
WorkspaceTabNewScheduler, const tabWrap = ref(null);
WorkspaceTabPropsScheduler,
ModalProcessesList, const workspace = computed(() => getWorkspace(props.connection.uid));
ModalDiscardChanges
const draggableTabs = computed<WorkspaceTab[]>({
get () {
return workspace.value.tabs;
}, },
props: { set (val) {
connection: Object updateTabs({ uid: props.connection.uid, tabs: val });
}, }
setup () { });
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const isSelected = computed(() => {
return selectedWorkspace.value === props.connection.uid;
});
const { const selectedTab = computed(() => {
getWorkspace, return workspace.value ? workspace.value.selectedTab : null;
addWorkspace, });
connectWorkspace,
removeConnected,
selectTab,
newTab,
removeTab,
updateTabs
} = workspacesStore;
return { const queryTabs = computed(() => {
selectedWorkspace, return workspace.value ? workspace.value.tabs.filter(tab => tab.type === 'query') : [];
getWorkspace, });
addWorkspace,
connectWorkspace,
removeConnected,
selectTab,
newTab,
removeTab,
updateTabs
};
},
data () {
return {
hasWheelEvent: false,
isProcessesModal: false,
unsavedTab: null
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
draggableTabs: {
get () {
return this.workspace.tabs;
},
set (val) {
this.updateTabs({ uid: this.connection.uid, tabs: val });
}
},
isSelected () {
return this.selectedWorkspace === this.connection.uid;
},
isSettingSupported () {
if (this.workspace.breadcrumbs.table && this.workspace.customizations.tableSettings) return true;
if (this.workspace.breadcrumbs.view && this.workspace.customizations.viewSettings) return true;
if (this.workspace.breadcrumbs.trigger && this.workspace.customizations.triggerSettings) return true;
if (this.workspace.breadcrumbs.procedure && this.workspace.customizations.routineSettings) return true;
if (this.workspace.breadcrumbs.function && this.workspace.customizations.functionSettings) return true;
if (this.workspace.breadcrumbs.triggerFunction && this.workspace.customizations.functionSettings) return true;
if (this.workspace.breadcrumbs.scheduler && this.workspace.customizations.schedulerSettings) return true;
return false;
},
selectedTab () {
return this.workspace ? this.workspace.selectedTab : null;
},
queryTabs () {
return this.workspace ? this.workspace.tabs.filter(tab => tab.type === 'query') : [];
},
schemaChild () {
for (const key in this.workspace.breadcrumbs) {
if (key === 'schema') continue;
if (this.workspace.breadcrumbs[key]) return this.workspace.breadcrumbs[key];
}
return false;
},
hasTools () {
if (!this.workspace.customizations) return false;
else {
return this.workspace.customizations.processesList ||
this.workspace.customizations.usersManagement ||
this.workspace.customizations.variables;
}
}
},
watch: {
queryTabs: {
handler (newVal, oldVal) {
if (newVal.length > oldVal.length) {
setTimeout(() => {
const scroller = this.$refs.tabWrap;
if (scroller) scroller.$el.scrollLeft = scroller.$el.scrollWidth;
}, 0);
}
},
deep: true
}
},
async created () {
window.addEventListener('keydown', this.onKey);
await this.addWorkspace(this.connection.uid);
const isInitiated = await Connection.checkConnection(this.connection.uid);
if (isInitiated)
this.connectWorkspace(this.connection);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
addQueryTab () {
this.newTab({ uid: this.connection.uid, type: 'query' });
},
getSelectedTab () {
return this.workspace.tabs.find(tab => tab.uid === this.selectedTab);
},
onKey (e) {
e.stopPropagation();
if (!this.isSelected) const hasTools = computed(() => {
return; if (!workspace.value.customizations) return false;
else {
return workspace.value.customizations.processesList ||
workspace.value.customizations.usersManagement ||
workspace.value.customizations.variables;
}
});
if ((e.ctrlKey || e.metaKey) && e.keyCode === 84 && !e.altKey) { // CTRL|Command + t watch(queryTabs, (newVal, oldVal) => {
this.addQueryTab(); if (newVal.length > oldVal.length) {
} setTimeout(() => {
const scroller = tabWrap.value;
if (scroller) scroller.$el.scrollLeft = scroller.$el.scrollWidth;
}, 0);
}
});
if ((e.ctrlKey || e.metaKey) && e.keyCode === 87 && !e.altKey) { // CTRL|Command + w const addQueryTab = () => {
const currentTab = this.getSelectedTab(); newTab({ uid: props.connection.uid, type: 'query' });
if (currentTab) };
this.closeTab(currentTab);
}
},
openAsPermanentTab (tab) {
const permanentTabs = {
table: 'data',
view: 'data',
trigger: 'trigger-props',
triggerFunction: 'trigger-function-props',
function: 'function-props',
routine: 'routine-props',
scheduler: 'scheduler-props'
};
this.newTab({ const getSelectedTab = () => {
uid: this.connection.uid, return workspace.value.tabs.find(tab => tab.uid === selectedTab.value);
schema: tab.schema, };
elementName: tab.elementName,
type: permanentTabs[tab.elementType],
elementType: tab.elementType
});
},
closeTab (tab, force) {
this.unsavedTab = null;
// if (tab.type === 'query' && this.queryTabs.length === 1) return;
if (!force && tab.isChanged) {
this.unsavedTab = tab;
return;
}
this.removeTab({ uid: this.connection.uid, tab: tab.uid }); const onKey = (e: KeyboardEvent) => {
}, e.stopPropagation();
showProcessesModal () {
this.isProcessesModal = true; if (!isSelected.value)
}, return;
hideProcessesModal () {
this.isProcessesModal = false; if ((e.ctrlKey || e.metaKey) && e.key === 't' && !e.altKey) { // CTRL|Command + t
}, addQueryTab();
addWheelEvent () { }
if (!this.hasWheelEvent) {
this.$refs.tabWrap.$el.addEventListener('wheel', e => { if ((e.ctrlKey || e.metaKey) && e.key === 'w' && !e.altKey) { // CTRL|Command + w
if (e.deltaY > 0) this.$refs.tabWrap.$el.scrollLeft += 50; const currentTab = getSelectedTab();
else this.$refs.tabWrap.$el.scrollLeft -= 50; if (currentTab)
}); closeTab(currentTab);
this.hasWheelEvent = true;
}
},
cutText (string) {
const limit = 20;
const escapedString = string.replace(/\s{2,}/g, ' ');
if (escapedString.length > limit)
return `${escapedString.substr(0, limit)}...`;
return escapedString;
}
} }
}; };
const openAsPermanentTab = (tab: WorkspaceTab) => {
const permanentTabs = {
table: 'data',
view: 'data',
trigger: 'trigger-props',
triggerFunction: 'trigger-function-props',
function: 'function-props',
routine: 'routine-props',
procedure: 'routine-props',
scheduler: 'scheduler-props'
} as {[key: string]: string};
newTab({
uid: props.connection.uid,
schema: tab.schema,
elementName: tab.elementName,
type: permanentTabs[tab.elementType],
elementType: tab.elementType
});
};
const closeTab = (tab: WorkspaceTab, force = false) => {
unsavedTab.value = null;
// if (tab.type === 'query' && this.queryTabs.length === 1) return;
if (!force && tab.isChanged) {
unsavedTab.value = tab;
return;
}
removeTab({ uid: props.connection.uid, tab: tab.uid });
};
const showProcessesModal = () => {
isProcessesModal.value = true;
};
const hideProcessesModal = () => {
isProcessesModal.value = false;
};
const addWheelEvent = () => {
if (!hasWheelEvent.value) {
tabWrap.value.$el.addEventListener('wheel', (e: WheelEvent) => {
if (e.deltaY > 0) tabWrap.value.$el.scrollLeft += 50;
else tabWrap.value.$el.scrollLeft -= 50;
});
hasWheelEvent.value = true;
}
};
const cutText = (string: string) => {
const limit = 20;
const escapedString = string.replace(/\s{2,}/g, ' ');
if (escapedString.length > limit)
return `${escapedString.substr(0, limit)}...`;
return escapedString;
};
(async () => {
window.addEventListener('keydown', onKey);
await addWorkspace(props.connection.uid);
const isInitiated = await Connection.checkConnection(props.connection.uid);
if (isInitiated)
connectWorkspace(props.connection);
})();
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -8,23 +8,23 @@
:class="{'active': selectedTab === 'general'}" :class="{'active': selectedTab === 'general'}"
@click="selectTab('general')" @click="selectTab('general')"
> >
<a class="tab-link">{{ $t('word.general') }}</a> <a class="tab-link">{{ t('word.general') }}</a>
</li> </li>
<li <li
v-if="customizations.sslConnection" v-if="clientCustomizations.sslConnection"
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}" :class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')" @click="selectTab('ssl')"
> >
<a class="tab-link">{{ $t('word.ssl') }}</a> <a class="tab-link">{{ t('word.ssl') }}</a>
</li> </li>
<li <li
v-if="customizations.sshConnection" v-if="clientCustomizations.sshConnection"
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'ssh'}" :class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')" @click="selectTab('ssh')"
> >
<a class="tab-link">{{ $t('word.sshTunnel') }}</a> <a class="tab-link">{{ t('word.sshTunnel') }}</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -34,7 +34,7 @@
<fieldset class="m-0" :disabled="isBusy"> <fieldset class="m-0" :disabled="isBusy">
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.connectionName') }}</label> <label class="form-label cut-text">{{ t('word.connectionName') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -47,7 +47,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.client') }}</label> <label class="form-label cut-text">{{ t('word.client') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseSelect <BaseSelect
@ -61,7 +61,7 @@
</div> </div>
<div v-if="connection.client === 'pg'" class="form-group columns"> <div v-if="connection.client === 'pg'" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.connectionString') }}</label> <label class="form-label cut-text">{{ t('word.connectionString') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -72,9 +72,9 @@
> >
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label> <label class="form-label cut-text">{{ t('word.hostName') }}/IP</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -84,22 +84,22 @@
> >
</div> </div>
</div> </div>
<div v-if="customizations.fileConnection" class="form-group columns"> <div v-if="clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.database') }}</label> <label class="form-label cut-text">{{ t('word.database') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:model-value="connection.databasePath" :model-value="connection.databasePath"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('databasePath')" @clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')" @change="pathSelection($event, 'databasePath')"
/> />
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.port') }}</label> <label class="form-label cut-text">{{ t('word.port') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -111,9 +111,9 @@
> >
</div> </div>
</div> </div>
<div v-if="customizations.database" class="form-group columns"> <div v-if="clientCustomizations.database" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.database') }}</label> <label class="form-label cut-text">{{ t('word.database') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -123,9 +123,9 @@
> >
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.user') }}</label> <label class="form-label cut-text">{{ t('word.user') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -136,9 +136,9 @@
> >
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.password') }}</label> <label class="form-label cut-text">{{ t('word.password') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -149,32 +149,32 @@
> >
</div> </div>
</div> </div>
<div v-if="customizations.connectionSchema" class="form-group columns"> <div v-if="clientCustomizations.connectionSchema" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.schema') }}</label> <label class="form-label cut-text">{{ t('word.schema') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
v-model="connection.schema" v-model="connection.schema"
class="form-input" class="form-input"
type="text" type="text"
:placeholder="$t('word.all')" :placeholder="t('word.all')"
> >
</div> </div>
</div> </div>
<div v-if="customizations.readOnlyMode" class="form-group columns"> <div v-if="clientCustomizations.readOnlyMode" class="form-group columns">
<div class="column col-4 col-sm-12" /> <div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
<input v-model="connection.readonly" type="checkbox"><i class="form-icon" /> {{ $t('message.readOnlyMode') }} <input v-model="connection.readonly" type="checkbox"><i class="form-icon" /> {{ t('message.readOnlyMode') }}
</label> </label>
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12" /> <div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
<input v-model="connection.ask" type="checkbox"><i class="form-icon" /> {{ $t('message.askCredentials') }} <input v-model="connection.ask" type="checkbox"><i class="form-icon" /> {{ t('message.askCredentials') }}
</label> </label>
</div> </div>
</div> </div>
@ -188,7 +188,7 @@
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text"> <label class="form-label cut-text">
{{ $t('message.enableSsl') }} {{ t('message.enableSsl') }}
</label> </label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
@ -201,12 +201,12 @@
<fieldset class="m-0" :disabled="isBusy || !connection.ssl"> <fieldset class="m-0" :disabled="isBusy || !connection.ssl">
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label> <label class="form-label cut-text">{{ t('word.privateKey') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:model-value="connection.key" :model-value="connection.key"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('key')" @clear="pathClear('key')"
@change="pathSelection($event, 'key')" @change="pathSelection($event, 'key')"
/> />
@ -214,12 +214,12 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.certificate') }}</label> <label class="form-label cut-text">{{ t('word.certificate') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:model-value="connection.cert" :model-value="connection.cert"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('cert')" @clear="pathClear('cert')"
@change="pathSelection($event, 'cert')" @change="pathSelection($event, 'cert')"
/> />
@ -227,12 +227,12 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.caCertificate') }}</label> <label class="form-label cut-text">{{ t('word.caCertificate') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:model-value="connection.ca" :model-value="connection.ca"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('ca')" @clear="pathClear('ca')"
@change="pathSelection($event, 'ca')" @change="pathSelection($event, 'ca')"
/> />
@ -240,7 +240,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.ciphers') }}</label> <label class="form-label cut-text">{{ t('word.ciphers') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -255,7 +255,7 @@
<div class="column col-4 col-sm-12" /> <div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
<input v-model="connection.untrustedConnection" type="checkbox"><i class="form-icon" /> {{ $t('message.untrustedConnection') }} <input v-model="connection.untrustedConnection" type="checkbox"><i class="form-icon" /> {{ t('message.untrustedConnection') }}
</label> </label>
</div> </div>
</div> </div>
@ -269,7 +269,7 @@
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text"> <label class="form-label cut-text">
{{ $t('message.enableSsh') }} {{ t('message.enableSsh') }}
</label> </label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
@ -282,7 +282,7 @@
<fieldset class="m-0" :disabled="isBusy || !connection.ssh"> <fieldset class="m-0" :disabled="isBusy || !connection.ssh">
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label> <label class="form-label cut-text">{{ t('word.hostName') }}/IP</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -294,7 +294,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.user') }}</label> <label class="form-label cut-text">{{ t('word.user') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -306,7 +306,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.password') }}</label> <label class="form-label cut-text">{{ t('word.password') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -318,7 +318,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.port') }}</label> <label class="form-label cut-text">{{ t('word.port') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -332,12 +332,12 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label> <label class="form-label cut-text">{{ t('word.privateKey') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:model-value="connection.sshKey" :model-value="connection.sshKey"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('sshKey')" @clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')" @change="pathSelection($event, 'sshKey')"
/> />
@ -345,7 +345,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.passphrase') }}</label> <label class="form-label cut-text">{{ t('word.passphrase') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -368,7 +368,7 @@
@click="startTest" @click="startTest"
> >
<i class="mdi mdi-24px mdi-lightning-bolt mr-1" /> <i class="mdi mdi-24px mdi-lightning-bolt mr-1" />
{{ $t('message.testConnection') }} {{ t('message.testConnection') }}
</button> </button>
<button <button
id="connection-save" id="connection-save"
@ -377,7 +377,7 @@
@click="saveConnection" @click="saveConnection"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
{{ $t('word.save') }} {{ t('word.save') }}
</button> </button>
</div> </div>
</div> </div>
@ -389,188 +389,171 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed, Ref, ref, watch } from 'vue';
import customizations from 'common/customizations'; import customizations from 'common/customizations';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import ModalAskCredentials from '@/components/ModalAskCredentials'; import ModalAskCredentials from '@/components/ModalAskCredentials.vue';
import BaseUploadInput from '@/components/BaseUploadInput'; import BaseUploadInput from '@/components/BaseUploadInput.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { ConnectionParams } from 'common/interfaces/antares';
import { useI18n } from 'vue-i18n';
export default { const { t } = useI18n();
name: 'WorkspaceAddConnectionPanel',
components: {
ModalAskCredentials,
BaseUploadInput,
BaseSelect
},
setup () {
const { addConnection } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { connectWorkspace, selectWorkspace } = workspacesStore; const { addConnection } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
return { const { connectWorkspace, selectWorkspace } = workspacesStore;
addConnection,
addNotification, const clients = ref([
connectWorkspace, { name: 'MySQL', slug: 'mysql' },
selectWorkspace { name: 'MariaDB', slug: 'maria' },
}; { name: 'PostgreSQL', slug: 'pg' },
}, { name: 'SQLite', slug: 'sqlite' }
data () { ]);
return {
clients: [ const connection = ref({
{ name: 'MySQL', slug: 'mysql' }, name: '',
{ name: 'MariaDB', slug: 'maria' }, client: 'mysql',
{ name: 'PostgreSQL', slug: 'pg' }, host: '127.0.0.1',
{ name: 'SQLite', slug: 'sqlite' } database: null,
], databasePath: '',
connection: { port: null,
name: '', user: null,
client: 'mysql', password: '',
host: '127.0.0.1', ask: false,
database: null, readonly: false,
databasePath: '', uid: uidGen('C'),
port: null, ssl: false,
user: null, cert: '',
password: '', key: '',
ask: false, ca: '',
readonly: false, ciphers: '',
uid: uidGen('C'), untrustedConnection: false,
ssl: false, ssh: false,
cert: '', sshHost: '',
key: '', sshUser: '',
ca: '', sshPass: '',
ciphers: '', sshKey: '',
untrustedConnection: false, sshPort: 22,
ssh: false, pgConnString: ''
sshHost: '', }) as Ref<ConnectionParams & { pgConnString: string }>;
sshUser: '',
sshPass: '', const firstInput: Ref<HTMLInputElement> = ref(null);
sshKey: '', const isConnecting = ref(false);
sshPort: 22, const isTesting = ref(false);
pgConnString: '' const isAsking = ref(false);
}, const selectedTab = ref('general');
isConnecting: false,
isTesting: false, const clientCustomizations = computed(() => {
isAsking: false, return customizations[connection.value.client];
selectedTab: 'general' });
};
}, const isBusy = computed(() => {
computed: { return isConnecting.value || isTesting.value;
customizations () { });
return customizations[this.connection.client];
}, watch(() => connection.value.client, () => {
isBusy () { connection.value.user = clientCustomizations.value.defaultUser;
return this.isConnecting || this.isTesting; connection.value.port = clientCustomizations.value.defaultPort;
connection.value.database = clientCustomizations.value.defaultDatabase;
});
const setDefaults = () => {
connection.value.user = clientCustomizations.value.defaultUser;
connection.value.port = clientCustomizations.value.defaultPort;
connection.value.database = clientCustomizations.value.defaultDatabase;
};
const startTest = async () => {
isTesting.value = true;
if (connection.value.ask)
isAsking.value = true;
else {
try {
const res = await Connection.makeTest(connection);
if (res.status === 'error')
addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
addNotification({ status: 'success', message: t('message.connectionSuccessfullyMade') });
} }
}, catch (err) {
watch: { addNotification({ status: 'error', message: err.stack });
'connection.client' () {
this.connection.user = this.customizations.defaultUser;
this.connection.port = this.customizations.defaultPort;
this.connection.database = this.customizations.defaultDatabase;
} }
},
created () {
this.setDefaults();
setTimeout(() => { isTesting.value = false;
if (this.$refs.firstInput) this.$refs.firstInput.focus();
}, 20);
},
methods: {
setDefaults () {
this.connection.user = this.customizations.defaultUser;
this.connection.port = this.customizations.defaultPort;
this.connection.database = this.customizations.defaultDatabase;
},
async startConnection () {
await this.saveConnection();
this.isConnecting = true;
if (this.connection.ask)
this.isAsking = true;
else {
await this.connectWorkspace(this.connection);
this.isConnecting = false;
}
},
async startTest () {
this.isTesting = true;
if (this.connection.ask)
this.isAsking = true;
else {
try {
const res = await Connection.makeTest(this.connection);
if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isTesting = false;
}
},
async continueTest (credentials) { // if "Ask for credentials" is true
this.isAsking = false;
const params = Object.assign({}, this.connection, credentials);
try {
if (this.isConnecting) {
const params = Object.assign({}, this.connection, credentials);
await this.connectWorkspace(params);
this.isConnecting = false;
}
else {
const res = await Connection.makeTest(params);
if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isTesting = false;
},
saveConnection () {
this.selectWorkspace(this.connection.uid);
return this.addConnection(this.connection);
},
closeAsking () {
this.isTesting = false;
this.isAsking = false;
},
selectTab (tab) {
this.selectedTab = tab;
},
toggleSsl () {
this.connection.ssl = !this.connection.ssl;
},
toggleSsh () {
this.connection.ssh = !this.connection.ssh;
},
pathSelection (event, name) {
const { files } = event.target;
if (!files.length) return;
this.connection[name] = files[0].path;
},
pathClear (name) {
this.connection[name] = '';
}
} }
}; };
const continueTest = async (credentials: { user: string; password: string }) => { // if "Ask for credentials" is true
isAsking.value = false;
const params = Object.assign({}, connection.value, credentials);
try {
if (isConnecting.value) {
await connectWorkspace(params);
isConnecting.value = false;
}
else {
const res = await Connection.makeTest(params);
if (res.status === 'error')
addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
addNotification({ status: 'success', message: t('message.connectionSuccessfullyMade') });
}
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isTesting.value = false;
};
const saveConnection = () => {
selectWorkspace(connection.value.uid);
return addConnection(connection.value);
};
const closeAsking = () => {
isTesting.value = false;
isAsking.value = false;
};
const selectTab = (tab: string) => {
selectedTab.value = tab;
};
const toggleSsl = () => {
connection.value.ssl = !connection.value.ssl;
};
const toggleSsh = () => {
connection.value.ssh = !connection.value.ssh;
};
const pathSelection = (event: Event & {target: {files: {path: string}[]}}, name: keyof ConnectionParams) => {
const { files } = event.target;
if (!files.length) return;
(connection.value as unknown as {[key: string]: string})[name] = files[0].path as string;
};
const pathClear = (name: keyof ConnectionParams) => {
(connection.value as unknown as {[key: string]: string})[name] = '';
};
setDefaults();
setTimeout(() => {
if (firstInput.value) firstInput.value.focus();
}, 20);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -8,23 +8,23 @@
:class="{'active': selectedTab === 'general'}" :class="{'active': selectedTab === 'general'}"
@click="selectTab('general')" @click="selectTab('general')"
> >
<a class="tab-link">{{ $t('word.general') }}</a> <a class="tab-link">{{ t('word.general') }}</a>
</li> </li>
<li <li
v-if="customizations.sslConnection" v-if="clientCustomizations.sslConnection"
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}" :class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')" @click="selectTab('ssl')"
> >
<a class="tab-link">{{ $t('word.ssl') }}</a> <a class="tab-link">{{ t('word.ssl') }}</a>
</li> </li>
<li <li
v-if="customizations.sshConnection" v-if="clientCustomizations.sshConnection"
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'ssh'}" :class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')" @click="selectTab('ssh')"
> >
<a class="tab-link">{{ $t('word.sshTunnel') }}</a> <a class="tab-link">{{ t('word.sshTunnel') }}</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -34,7 +34,7 @@
<fieldset class="m-0" :disabled="isBusy"> <fieldset class="m-0" :disabled="isBusy">
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.connectionName') }}</label> <label class="form-label cut-text">{{ t('word.connectionName') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -47,7 +47,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.client') }}</label> <label class="form-label cut-text">{{ t('word.client') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseSelect <BaseSelect
@ -63,7 +63,7 @@
</div> </div>
<div v-if="connection.client === 'pg'" class="form-group columns"> <div v-if="connection.client === 'pg'" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.connectionString') }}</label> <label class="form-label cut-text">{{ t('word.connectionString') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -74,9 +74,9 @@
> >
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label> <label class="form-label cut-text">{{ t('word.hostName') }}/IP</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -86,22 +86,22 @@
> >
</div> </div>
</div> </div>
<div v-if="customizations.fileConnection" class="form-group columns"> <div v-if="clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.database') }}</label> <label class="form-label cut-text">{{ t('word.database') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:model-value="localConnection.databasePath" :model-value="localConnection.databasePath"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('databasePath')" @clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')" @change="pathSelection($event, 'databasePath')"
/> />
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.port') }}</label> <label class="form-label cut-text">{{ t('word.port') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -113,9 +113,9 @@
> >
</div> </div>
</div> </div>
<div v-if="customizations.database" class="form-group columns"> <div v-if="clientCustomizations.database" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.database') }}</label> <label class="form-label cut-text">{{ t('word.database') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -125,9 +125,9 @@
> >
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.user') }}</label> <label class="form-label cut-text">{{ t('word.user') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -138,9 +138,9 @@
> >
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.password') }}</label> <label class="form-label cut-text">{{ t('word.password') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -151,32 +151,32 @@
> >
</div> </div>
</div> </div>
<div v-if="customizations.connectionSchema" class="form-group columns"> <div v-if="clientCustomizations.connectionSchema" class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.schema') }}</label> <label class="form-label cut-text">{{ t('word.schema') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
v-model="localConnection.schema" v-model="localConnection.schema"
class="form-input" class="form-input"
type="text" type="text"
:placeholder="$t('word.all')" :placeholder="t('word.all')"
> >
</div> </div>
</div> </div>
<div v-if="customizations.readOnlyMode" class="form-group columns"> <div v-if="clientCustomizations.readOnlyMode" class="form-group columns">
<div class="column col-4 col-sm-12" /> <div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
<input v-model="localConnection.readonly" type="checkbox"><i class="form-icon" /> {{ $t('message.readOnlyMode') }} <input v-model="localConnection.readonly" type="checkbox"><i class="form-icon" /> {{ t('message.readOnlyMode') }}
</label> </label>
</div> </div>
</div> </div>
<div v-if="!customizations.fileConnection" class="form-group columns"> <div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-4 col-sm-12" /> <div class="column col-4 col-sm-12" />
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
<input v-model="localConnection.ask" type="checkbox"><i class="form-icon" /> {{ $t('message.askCredentials') }} <input v-model="localConnection.ask" type="checkbox"><i class="form-icon" /> {{ t('message.askCredentials') }}
</label> </label>
</div> </div>
</div> </div>
@ -190,7 +190,7 @@
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text"> <label class="form-label cut-text">
{{ $t('message.enableSsl') }} {{ t('message.enableSsl') }}
</label> </label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
@ -203,12 +203,12 @@
<fieldset class="m-0" :disabled="isBusy || !localConnection.ssl"> <fieldset class="m-0" :disabled="isBusy || !localConnection.ssl">
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label> <label class="form-label cut-text">{{ t('word.privateKey') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:model-value="localConnection.key" :model-value="localConnection.key"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('key')" @clear="pathClear('key')"
@change="pathSelection($event, 'key')" @change="pathSelection($event, 'key')"
/> />
@ -216,12 +216,12 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.certificate') }}</label> <label class="form-label cut-text">{{ t('word.certificate') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:model-value="localConnection.cert" :model-value="localConnection.cert"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('cert')" @clear="pathClear('cert')"
@change="pathSelection($event, 'cert')" @change="pathSelection($event, 'cert')"
/> />
@ -229,12 +229,12 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.caCertificate') }}</label> <label class="form-label cut-text">{{ t('word.caCertificate') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:model-value="localConnection.ca" :model-value="localConnection.ca"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('ca')" @clear="pathClear('ca')"
@change="pathSelection($event, 'ca')" @change="pathSelection($event, 'ca')"
/> />
@ -242,7 +242,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.ciphers') }}</label> <label class="form-label cut-text">{{ t('word.ciphers') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -263,7 +263,7 @@
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text"> <label class="form-label cut-text">
{{ $t('message.enableSsh') }} {{ t('message.enableSsh') }}
</label> </label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
@ -276,7 +276,7 @@
<fieldset class="m-0" :disabled="isBusy || !localConnection.ssh"> <fieldset class="m-0" :disabled="isBusy || !localConnection.ssh">
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.hostName') }}/IP</label> <label class="form-label cut-text">{{ t('word.hostName') }}/IP</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -288,7 +288,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.user') }}</label> <label class="form-label cut-text">{{ t('word.user') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -300,7 +300,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.password') }}</label> <label class="form-label cut-text">{{ t('word.password') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -312,7 +312,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.port') }}</label> <label class="form-label cut-text">{{ t('word.port') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -326,12 +326,12 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.privateKey') }}</label> <label class="form-label cut-text">{{ t('word.privateKey') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<BaseUploadInput <BaseUploadInput
:model-value="localConnection.sshKey" :model-value="localConnection.sshKey"
:message="$t('word.browse')" :message="t('word.browse')"
@clear="pathClear('sshKey')" @clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')" @change="pathSelection($event, 'sshKey')"
/> />
@ -339,7 +339,7 @@
</div> </div>
<div class="form-group columns"> <div class="form-group columns">
<div class="column col-4 col-sm-12"> <div class="column col-4 col-sm-12">
<label class="form-label cut-text">{{ $t('word.passphrase') }}</label> <label class="form-label cut-text">{{ t('word.passphrase') }}</label>
</div> </div>
<div class="column col-8 col-sm-12"> <div class="column col-8 col-sm-12">
<input <input
@ -362,7 +362,7 @@
@click="startTest" @click="startTest"
> >
<i class="mdi mdi-24px mdi-lightning-bolt mr-1" /> <i class="mdi mdi-24px mdi-lightning-bolt mr-1" />
{{ $t('message.testConnection') }} {{ t('message.testConnection') }}
</button> </button>
<button <button
id="connection-save" id="connection-save"
@ -371,7 +371,7 @@
@click="saveConnection" @click="saveConnection"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
{{ $t('word.save') }} {{ t('word.save') }}
</button> </button>
<button <button
id="connection-connect" id="connection-connect"
@ -381,7 +381,7 @@
@click="startConnection" @click="startConnection"
> >
<i class="mdi mdi-24px mdi-connection mr-1" /> <i class="mdi mdi-24px mdi-connection mr-1" />
{{ $t('word.connect') }} {{ t('word.connect') }}
</button> </button>
</div> </div>
</div> </div>
@ -393,154 +393,150 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed, Prop, Ref, ref, watch } from 'vue';
import customizations from 'common/customizations'; import customizations from 'common/customizations';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import ModalAskCredentials from '@/components/ModalAskCredentials'; import ModalAskCredentials from '@/components/ModalAskCredentials.vue';
import BaseUploadInput from '@/components/BaseUploadInput'; import BaseUploadInput from '@/components/BaseUploadInput.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { ConnectionParams } from 'common/interfaces/antares';
import { useI18n } from 'vue-i18n';
export default { const { t } = useI18n();
name: 'WorkspaceEditConnectionPanel',
components: {
ModalAskCredentials,
BaseUploadInput,
BaseSelect
},
props: {
connection: Object
},
setup () {
const { editConnection } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const { connectWorkspace } = useWorkspacesStore();
return { const props = defineProps({
editConnection, connection: Object as Prop<ConnectionParams>
addNotification, });
connectWorkspace
};
},
data () {
return {
clients: [
{ name: 'MySQL', slug: 'mysql' },
{ name: 'MariaDB', slug: 'maria' },
{ name: 'PostgreSQL', slug: 'pg' },
{ name: 'SQLite', slug: 'sqlite' }
],
isConnecting: false,
isTesting: false,
isAsking: false,
localConnection: null,
selectedTab: 'general'
};
},
computed: {
customizations () {
return customizations[this.localConnection.client];
},
isBusy () {
return this.isConnecting || this.isTesting;
},
hasChanges () {
return JSON.stringify(this.connection) !== JSON.stringify(this.localConnection);
}
},
watch: {
connection () {
this.localConnection = JSON.parse(JSON.stringify(this.connection));
}
},
created () {
this.localConnection = JSON.parse(JSON.stringify(this.connection));
},
methods: {
async startConnection () {
await this.saveConnection();
this.isConnecting = true;
if (this.localConnection.ask) const { editConnection } = useConnectionsStore();
this.isAsking = true; const { addNotification } = useNotificationsStore();
else { const { connectWorkspace } = useWorkspacesStore();
await this.connectWorkspace(this.localConnection);
this.isConnecting = false;
}
},
async startTest () {
this.isTesting = true;
if (this.localConnection.ask) const clients = ref([
this.isAsking = true; { name: 'MySQL', slug: 'mysql' },
else { { name: 'MariaDB', slug: 'maria' },
try { { name: 'PostgreSQL', slug: 'pg' },
const res = await Connection.makeTest(this.localConnection); { name: 'SQLite', slug: 'sqlite' }
if (res.status === 'error') ]);
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isTesting = false; const firstInput: Ref<HTMLInputElement> = ref(null);
} const localConnection: Ref<ConnectionParams & { pgConnString: string }> = ref(null);
}, const isConnecting = ref(false);
async continueTest (credentials) { // if "Ask for credentials" is true const isTesting = ref(false);
this.isAsking = false; const isAsking = ref(false);
const params = Object.assign({}, this.localConnection, credentials); const selectedTab = ref('general');
try {
if (this.isConnecting) {
const params = Object.assign({}, this.connection, credentials);
await this.connectWorkspace(params);
this.isConnecting = false;
}
else {
const res = await Connection.makeTest(params);
if (res.status === 'error')
this.addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
this.addNotification({ status: 'success', message: this.$t('message.connectionSuccessfullyMade') });
}
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isTesting = false; const clientCustomizations = computed(() => {
}, return customizations[localConnection.value.client];
saveConnection () { });
return this.editConnection(this.localConnection);
},
closeAsking () {
this.isTesting = false;
this.isAsking = false;
this.isConnecting = false;
},
selectTab (tab) {
this.selectedTab = tab;
},
toggleSsl () {
this.localConnection.ssl = !this.localConnection.ssl;
},
toggleSsh () {
this.localConnection.ssh = !this.localConnection.ssh;
},
pathSelection (event, name) {
const { files } = event.target;
if (!files.length) return;
this.localConnection[name] = files[0].path; const isBusy = computed(() => {
}, return isConnecting.value || isTesting.value;
pathClear (name) { });
this.localConnection[name] = '';
} const hasChanges = computed(() => {
return JSON.stringify(props.connection) !== JSON.stringify(localConnection.value);
});
watch(() => props.connection, () => {
localConnection.value = JSON.parse(JSON.stringify(props.connection));
});
const startConnection = async () => {
await saveConnection();
isConnecting.value = true;
if (localConnection.value.ask)
isAsking.value = true;
else {
await connectWorkspace(localConnection.value);
isConnecting.value = false;
} }
}; };
const startTest = async () => {
isTesting.value = true;
if (localConnection.value.ask)
isAsking.value = true;
else {
try {
const res = await Connection.makeTest(localConnection.value);
if (res.status === 'error')
addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
addNotification({ status: 'success', message: t('message.connectionSuccessfullyMade') });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isTesting.value = false;
}
};
const continueTest = async (credentials: {user: string; password: string }) => { // if "Ask for credentials" is true
isAsking.value = false;
const params = Object.assign({}, localConnection.value, credentials);
try {
if (isConnecting.value) {
const params = Object.assign({}, props.connection, credentials);
await connectWorkspace(params);
isConnecting.value = false;
}
else {
const res = await Connection.makeTest(params);
if (res.status === 'error')
addNotification({ status: 'error', message: res.response.message || res.response.toString() });
else
addNotification({ status: 'success', message: t('message.connectionSuccessfullyMade') });
}
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isTesting.value = false;
};
const saveConnection = () => {
return editConnection(localConnection.value);
};
const closeAsking = () => {
isTesting.value = false;
isAsking.value = false;
isConnecting.value = false;
};
const selectTab = (tab: string) => {
selectedTab.value = tab;
};
const toggleSsl = () => {
localConnection.value.ssl = !localConnection.value.ssl;
};
const toggleSsh = () => {
localConnection.value.ssh = !localConnection.value.ssh;
};
const pathSelection = (event: Event & {target: {files: {path: string}[]}}, name: keyof ConnectionParams) => {
const { files } = event.target;
if (!files.length) return;
(localConnection.value as unknown as {[key: string]: string})[name] = files[0].path;
};
const pathClear = (name: keyof ConnectionParams) => {
(localConnection.value as unknown as {[key: string]: string})[name] = '';
};
localConnection.value = JSON.parse(JSON.stringify(props.connection));
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,61 +1,48 @@
<template> <template>
<div class="column col-12 empty"> <div class="column col-12 empty">
<div class="empty-icon"> <div class="empty-icon">
<img <img :src="logos[applicationTheme]" width="200">
v-if="applicationTheme === 'dark'"
src="../images/logo-dark.svg"
width="200"
>
<img
v-if="applicationTheme === 'light'"
src="../images/logo-light.svg"
width="200"
>
</div> </div>
<p class="h6 empty-subtitle"> <p class="h6 empty-subtitle">
{{ $t('message.noOpenTabs') }} {{ t('message.noOpenTabs') }}
</p> </p>
<div class="empty-action"> <div class="empty-action">
<button class="btn btn-gray d-flex" @click="$emit('new-tab')"> <button class="btn btn-gray d-flex" @click="emit('new-tab')">
<i class="mdi mdi-24px mdi-tab-plus mr-2" /> <i class="mdi mdi-24px mdi-tab-plus mr-2" />
{{ $t('message.openNewTab') }} {{ t('message.openNewTab') }}
</button> </button>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts">
<script> import { computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { storeToRefs } from 'pinia'; import { useI18n } from 'vue-i18n';
export default {
name: 'WorkspaceEmptyState',
emits: ['new-tab'],
setup () {
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const { applicationTheme } = storeToRefs(settingsStore); const { t } = useI18n();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, changeBreadcrumbs } = workspacesStore; const emit = defineEmits(['new-tab']);
return { const logos = {
applicationTheme, light: require('../images/logo-light.svg') as string,
selectedWorkspace, dark: require('../images/logo-dark.svg') as string
getWorkspace,
changeBreadcrumbs
};
},
computed: {
workspace () {
return this.getWorkspace(this.selectedWorkspace);
}
},
created () {
this.changeBreadcrumbs({ schema: this.workspace.breadcrumbs.schema });
}
}; };
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const { applicationTheme } = storeToRefs(settingsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, changeBreadcrumbs } = workspacesStore;
const workspace = computed(() => {
return getWorkspace(selectedWorkspace.value);
});
changeBreadcrumbs({ schema: workspace.value.breadcrumbs.schema });
</script> </script>
<style scoped> <style scoped>

View File

@ -48,7 +48,7 @@
/> />
</div> </div>
</div> </div>
<div class="workspace-explorebar-body" @click="$refs.explorebar.focus()"> <div class="workspace-explorebar-body" @click="explorebar.focus()">
<WorkspaceExploreBarSchema <WorkspaceExploreBarSchema
v-for="db of workspace.structure" v-for="db of workspace.structure"
:key="db.name" :key="db.name"
@ -115,7 +115,8 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { Component, computed, onMounted, Ref, ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
@ -125,428 +126,291 @@ import { useWorkspacesStore } from '@/stores/workspaces';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import Views from '@/ipc-api/Views'; import Views from '@/ipc-api/Views';
import Functions from '@/ipc-api/Functions';
import Schedulers from '@/ipc-api/Schedulers';
import WorkspaceExploreBarSchema from '@/components/WorkspaceExploreBarSchema'; import WorkspaceExploreBarSchema from '@/components/WorkspaceExploreBarSchema.vue';
import DatabaseContext from '@/components/WorkspaceExploreBarSchemaContext'; import DatabaseContext from '@/components/WorkspaceExploreBarSchemaContext.vue';
import TableContext from '@/components/WorkspaceExploreBarTableContext'; import TableContext from '@/components/WorkspaceExploreBarTableContext.vue';
import MiscContext from '@/components/WorkspaceExploreBarMiscContext'; import MiscContext from '@/components/WorkspaceExploreBarMiscContext.vue';
import MiscFolderContext from '@/components/WorkspaceExploreBarMiscFolderContext'; import MiscFolderContext from '@/components/WorkspaceExploreBarMiscFolderContext.vue';
import ModalNewSchema from '@/components/ModalNewSchema'; import ModalNewSchema from '@/components/ModalNewSchema.vue';
export default { const props = defineProps({
name: 'WorkspaceExploreBar', connection: Object,
components: { isSelected: Boolean
WorkspaceExploreBarSchema, });
DatabaseContext,
TableContext,
MiscContext,
MiscFolderContext,
ModalNewSchema
},
props: {
connection: Object,
isSelected: Boolean
},
setup () {
const { getConnectionName } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const { explorebarSize } = storeToRefs(settingsStore); const { getConnectionName } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const { changeExplorebarSize } = settingsStore; const { explorebarSize } = storeToRefs(settingsStore);
const {
getWorkspace,
removeConnected: disconnectWorkspace,
refreshStructure,
changeBreadcrumbs,
selectTab,
newTab,
removeTabs,
setSearchTerm,
addLoadingElement,
removeLoadingElement
} = workspacesStore;
return { const { changeExplorebarSize } = settingsStore;
getConnectionName, const {
addNotification, getWorkspace,
explorebarSize, removeConnected: disconnectWorkspace,
changeExplorebarSize, refreshStructure,
getWorkspace, newTab,
disconnectWorkspace, removeTabs,
refreshStructure, setSearchTerm,
changeBreadcrumbs, addLoadingElement,
selectTab, removeLoadingElement
newTab, } = workspacesStore;
removeTabs,
setSearchTerm,
addLoadingElement,
removeLoadingElement
};
},
data () {
return {
isRefreshing: false,
isNewDBModal: false, const searchInput: Ref<HTMLInputElement> = ref(null);
isNewViewModal: false, const explorebar: Ref<HTMLInputElement> = ref(null);
isNewTriggerModal: false, const resizer: Ref<HTMLInputElement> = ref(null);
isNewRoutineModal: false, // eslint-disable-next-line @typescript-eslint/no-explicit-any
isNewFunctionModal: false, const schema: Ref<Component & { selectSchema: (name: string) => void; $refs: any }[]> = ref(null);
isNewTriggerFunctionModal: false, const isRefreshing = ref(false);
isNewSchedulerModal: false, const isNewDBModal = ref(false);
const localWidth = ref(null);
const explorebarWidthInterval = ref(null);
const searchTermInterval = ref(null);
const isDatabaseContext = ref(false);
const isTableContext = ref(false);
const isMiscContext = ref(false);
const isMiscFolderContext = ref(false);
const databaseContextEvent = ref(null);
const tableContextEvent = ref(null);
const miscContextEvent = ref(null);
const selectedSchema = ref('');
const selectedTable = ref(null);
const selectedMisc = ref(null);
const searchTerm = ref('');
localWidth: null, const workspace = computed(() => {
explorebarWidthInterval: null, return getWorkspace(props.connection.uid);
searchTermInterval: null, });
isDatabaseContext: false,
isTableContext: false,
isMiscContext: false,
isMiscFolderContext: false,
databaseContextEvent: null, const connectionName = computed(() => {
tableContextEvent: null, return getConnectionName(props.connection.uid);
miscContextEvent: null, });
selectedSchema: '', const customizations = computed(() => {
selectedTable: null, return workspace.value.customizations;
selectedMisc: null, });
searchTerm: ''
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
connectionName () {
return this.getConnectionName(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
}
},
watch: {
localWidth (val) {
clearTimeout(this.explorebarWidthInterval);
this.explorebarWidthInterval = setTimeout(() => { watch(localWidth, (val: number) => {
this.changeExplorebarSize(val); clearTimeout(explorebarWidthInterval.value);
}, 500);
},
isSelected (val) {
if (val) this.localWidth = this.explorebarSize;
},
searchTerm () {
clearTimeout(this.searchTermInterval);
this.searchTermInterval = setTimeout(() => { explorebarWidthInterval.value = setTimeout(() => {
this.setSearchTerm(this.searchTerm); changeExplorebarSize(val);
}, 200); }, 500);
} });
},
created () {
this.localWidth = this.explorebarSize;
},
mounted () {
const resizer = this.$refs.resizer;
resizer.addEventListener('mousedown', e => { watch(() => props.isSelected, (val: boolean) => {
e.preventDefault(); if (val) localWidth.value = explorebarSize.value;
});
window.addEventListener('mousemove', this.resize); watch(searchTerm, () => {
window.addEventListener('mouseup', this.stopResize); clearTimeout(searchTermInterval.value);
});
if (this.workspace.structure.length === 1) { // Auto-open if juust one schema searchTermInterval.value = setTimeout(() => {
this.$refs.schema[0].selectSchema(this.workspace.structure[0].name); setSearchTerm(searchTerm.value);
this.$refs.schema[0].$refs.schemaAccordion.open = true; }, 200);
} });
},
methods: {
async refresh () {
if (!this.isRefreshing) {
this.isRefreshing = true;
await this.refreshStructure(this.connection.uid);
this.isRefreshing = false;
}
},
explorebarSearch (e) {
if (e.code === 'Backspace') {
e.preventDefault();
if (this.searchTerm.length)
this.searchTerm = this.searchTerm.slice(0, -1);
else
return;
}
else if (e.key.length > 1)// Prevent non-alphanumerics
return;
this.$refs.searchInput.focus(); localWidth.value = explorebarSize.value;
},
resize (e) {
const el = this.$refs.explorebar;
let explorebarWidth = e.pageX - el.getBoundingClientRect().left;
if (explorebarWidth > 500) explorebarWidth = 500;
if (explorebarWidth < 150) explorebarWidth = 150;
this.localWidth = explorebarWidth;
},
stopResize () {
window.removeEventListener('mousemove', this.resize);
},
showNewDBModal () {
this.isNewDBModal = true;
},
hideNewDBModal () {
this.isNewDBModal = false;
},
openCreateElementTab (element) {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.newTab({ onMounted(() => {
uid: this.workspace.uid, resizer.value.addEventListener('mousedown', (e: MouseEvent) => {
schema: this.selectedSchema, e.preventDefault();
elementName: '',
elementType: element,
type: `new-${element}`
});
},
openSchemaContext (payload) {
this.selectedSchema = payload.schema;
this.databaseContextEvent = payload.event;
this.isDatabaseContext = true;
},
closeDatabaseContext () {
this.isDatabaseContext = false;
},
openTableContext (payload) {
this.selectedTable = payload.table;
this.selectedSchema = payload.schema;
this.tableContextEvent = payload.event;
this.isTableContext = true;
},
closeTableContext () {
this.isTableContext = false;
},
openMiscContext (payload) {
this.selectedMisc = payload.misc;
this.selectedSchema = payload.schema;
this.miscContextEvent = payload.event;
this.isMiscContext = true;
},
openMiscFolderContext (payload) {
this.selectedMisc = payload.type;
this.selectedSchema = payload.schema;
this.miscContextEvent = payload.event;
this.isMiscFolderContext = true;
},
closeMiscContext () {
this.isMiscContext = false;
},
closeMiscFolderContext () {
this.isMiscFolderContext = false;
},
showCreateTriggerModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewTriggerModal = true;
},
hideCreateTriggerModal () {
this.isNewTriggerModal = false;
},
showCreateRoutineModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewRoutineModal = true;
},
hideCreateRoutineModal () {
this.isNewRoutineModal = false;
},
showCreateFunctionModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewFunctionModal = true;
},
hideCreateFunctionModal () {
this.isNewFunctionModal = false;
},
showCreateTriggerFunctionModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewTriggerFunctionModal = true;
},
hideCreateTriggerFunctionModal () {
this.isNewTriggerFunctionModal = false;
},
showCreateSchedulerModal () {
this.closeDatabaseContext();
this.closeMiscFolderContext();
this.isNewSchedulerModal = true;
},
hideCreateSchedulerModal () {
this.isNewSchedulerModal = false;
},
async deleteTable (payload) {
this.closeTableContext();
this.addLoadingElement({ window.addEventListener('mousemove', resize);
name: payload.table.name, window.addEventListener('mouseup', stopResize);
schema: payload.schema, });
type: 'table'
});
try { if (workspace.value.structure.length === 1) { // Auto-open if juust one schema
let res; schema.value[0].selectSchema(workspace.value.structure[0].name);
schema.value[0].$refs.schemaAccordion.open = true;
}
});
if (payload.table.type === 'table') { const refresh = async () => {
res = await Tables.dropTable({ if (!isRefreshing.value) {
uid: this.connection.uid, isRefreshing.value = true;
table: payload.table.name, await refreshStructure(props.connection.uid);
schema: payload.schema isRefreshing.value = false;
});
}
else if (payload.table.type === 'view') {
res = await Views.dropView({
uid: this.connection.uid,
view: payload.table.name,
schema: payload.schema
});
}
const { status, response } = res;
if (status === 'success') {
this.refresh();
this.removeTabs({
uid: this.connection.uid,
elementName: payload.table.name,
elementType: payload.table.type,
schema: payload.schema
});
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.removeLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
},
async duplicateTable (payload) {
this.closeTableContext();
this.addLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
try {
const { status, response } = await Tables.duplicateTable({
uid: this.connection.uid,
table: payload.table.name,
schema: payload.schema
});
if (status === 'success')
this.refresh();
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.removeLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
},
async openCreateFunctionEditor (payload) {
const params = {
uid: this.connection.uid,
schema: this.selectedSchema,
...payload
};
const { status, response } = await Functions.createFunction(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedSchema, function: payload.name });
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
elementName: payload.name,
elementType: 'function',
type: 'function-props'
});
}
else
this.addNotification({ status: 'error', message: response });
},
async openCreateTriggerFunctionEditor (payload) {
const params = {
uid: this.connection.uid,
schema: this.selectedSchema,
...payload
};
const { status, response } = await Functions.createTriggerFunction(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedSchema, triggerFunction: payload.name });
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
elementName: payload.name,
elementType: 'triggerFunction',
type: 'trigger-function-props'
});
}
else
this.addNotification({ status: 'error', message: response });
},
async openCreateSchedulerEditor (payload) {
const params = {
uid: this.connection.uid,
schema: this.selectedSchema,
...payload
};
const { status, response } = await Schedulers.createScheduler(params);
if (status === 'success') {
await this.refresh();
this.changeBreadcrumbs({ schema: this.selectedSchema, scheduler: payload.name });
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
elementName: payload.name,
elementType: 'scheduler',
type: 'scheduler-props'
});
}
else
this.addNotification({ status: 'error', message: response });
}
} }
}; };
const explorebarSearch = (e: KeyboardEvent) => {
if (e.code === 'Backspace') {
e.preventDefault();
if (searchTerm.value.length)
searchTerm.value = searchTerm.value.slice(0, -1);
else
return;
}
else if (e.key.length > 1)// Prevent non-alphanumerics
return;
searchInput.value.focus();
};
const resize = (e: MouseEvent) => {
const el = explorebar.value;
let explorebarWidth = e.pageX - el.getBoundingClientRect().left;
if (explorebarWidth > 500) explorebarWidth = 500;
if (explorebarWidth < 150) explorebarWidth = 150;
localWidth.value = explorebarWidth;
};
const stopResize = () => {
window.removeEventListener('mousemove', resize);
};
const showNewDBModal = () => {
isNewDBModal.value = true;
};
const hideNewDBModal = () => {
isNewDBModal.value = false;
};
const openCreateElementTab = (element: string) => {
closeDatabaseContext();
closeMiscFolderContext();
newTab({
uid: workspace.value.uid,
schema: selectedSchema.value,
elementName: '',
elementType: element,
type: `new-${element}`
});
};
const openSchemaContext = (payload: { schema: string; event: PointerEvent }) => {
selectedSchema.value = payload.schema;
databaseContextEvent.value = payload.event;
isDatabaseContext.value = true;
};
const closeDatabaseContext = () => {
isDatabaseContext.value = false;
};
const openTableContext = (payload: { schema: string; table: string; event: PointerEvent }) => {
selectedTable.value = payload.table;
selectedSchema.value = payload.schema;
tableContextEvent.value = payload.event;
isTableContext.value = true;
};
const closeTableContext = () => {
isTableContext.value = false;
};
const openMiscContext = (payload: { schema: string; misc: string; event: PointerEvent }) => {
selectedMisc.value = payload.misc;
selectedSchema.value = payload.schema;
miscContextEvent.value = payload.event;
isMiscContext.value = true;
};
const openMiscFolderContext = (payload: { schema: string; type: string; event: PointerEvent }) => {
selectedMisc.value = payload.type;
selectedSchema.value = payload.schema;
miscContextEvent.value = payload.event;
isMiscFolderContext.value = true;
};
const closeMiscContext = () => {
isMiscContext.value = false;
};
const closeMiscFolderContext = () => {
isMiscFolderContext.value = false;
};
const deleteTable = async (payload: { schema: string; table: { name: string; type: string }; event: PointerEvent }) => {
closeTableContext();
addLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
try {
let res;
if (payload.table.type === 'table') {
res = await Tables.dropTable({
uid: props.connection.uid,
table: payload.table.name,
schema: payload.schema
});
}
else if (payload.table.type === 'view') {
res = await Views.dropView({
uid: props.connection.uid,
view: payload.table.name,
schema: payload.schema
});
}
const { status, response } = res;
if (status === 'success') {
refresh();
removeTabs({
uid: props.connection.uid as string,
elementName: payload.table.name as string,
elementType: payload.table.type,
schema: payload.schema as string
});
}
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
removeLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
};
const duplicateTable = async (payload: { schema: string; table: { name: string }; event: PointerEvent }) => {
closeTableContext();
addLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
try {
const { status, response } = await Tables.duplicateTable({
uid: props.connection.uid,
table: payload.table.name,
schema: payload.schema
});
if (status === 'success')
refresh();
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
removeLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
};
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -248,6 +248,8 @@ import { useWorkspacesStore } from '@/stores/workspaces';
import { formatBytes } from 'common/libs/formatBytes'; import { formatBytes } from 'common/libs/formatBytes';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
// TODO: expose selectSchema & schemaAccordion
export default { export default {
name: 'WorkspaceExploreBarSchema', name: 'WorkspaceExploreBarSchema',
props: { props: {

View File

@ -132,7 +132,7 @@
<input <input
v-model="localRow.unsigned" v-model="localRow.unsigned"
type="checkbox" type="checkbox"
:disabled="!fieldType.unsigned" :disabled="!fieldType?.unsigned"
> >
<i class="form-icon" /> <i class="form-icon" />
</label> </label>

View File

@ -151,7 +151,7 @@
<BaseSelect <BaseSelect
v-model="selectedSchema" v-model="selectedSchema"
:options="[{value: 'null', label: $t('message.noSchema')}, ...databaseSchemas.map(el => ({label: el, value: el}))]" :options="[{value: null, label: $t('message.noSchema')}, ...databaseSchemas.map(el => ({label: el, value: el}))]"
class="form-select select-sm text-bold" class="form-select select-sm text-bold"
/> />
</div> </div>

View File

@ -29,7 +29,7 @@ createApp(App)
.mount('#app'); .mount('#app');
const { locale } = useSettingsStore(); const { locale } = useSettingsStore();
i18n.global.locale = locale as string;// TODO: temp i18n.global.locale = locale;
// IPC exceptions // IPC exceptions
ipcRenderer.on('unhandled-exception', (event, error) => { ipcRenderer.on('unhandled-exception', (event, error) => {

View File

@ -34,7 +34,7 @@ export const useApplicationStore = defineStore('application', {
setLoadingStatus (payload: boolean) { setLoadingStatus (payload: boolean) {
this.isLoading = payload; this.isLoading = payload;
}, },
setBaseCompleters (payload: boolean) { setBaseCompleters (payload: Ace.Completer[]) {
this.baseCompleter = payload; this.baseCompleter = payload;
}, },
// Modals // Modals

View File

@ -60,7 +60,7 @@ export const useConnectionsStore = defineStore('connections', {
this.selected_conection = {}; this.selected_conection = {};
persistentStore.set('connections', this.connections); persistentStore.set('connections', this.connections);
}, },
updateConnections (connections: ConnectionParams) { updateConnections (connections: ConnectionParams[]) {
this.connections = connections; this.connections = connections;
persistentStore.set('connections', this.connections); persistentStore.set('connections', this.connections);
} }

View File

@ -7,6 +7,7 @@ const defaultAppTheme = isDarkTheme.matches ? 'dark' : 'light';
const defaultEditorTheme = isDarkTheme.matches ? 'twilight' : 'sqlserver'; const defaultEditorTheme = isDarkTheme.matches ? 'twilight' : 'sqlserver';
export type EditorFontSize = 'small' | 'medium' | 'large'; export type EditorFontSize = 'small' | 'medium' | 'large';
export type ApplicationTheme = 'light' | 'dark';
export const useSettingsStore = defineStore('settings', { export const useSettingsStore = defineStore('settings', {
state: () => ({ state: () => ({
@ -17,7 +18,7 @@ export const useSettingsStore = defineStore('settings', {
dataTabLimit: persistentStore.get('data_tab_limit', 1000) as number, dataTabLimit: persistentStore.get('data_tab_limit', 1000) as number,
autoComplete: persistentStore.get('auto_complete', true) as boolean, autoComplete: persistentStore.get('auto_complete', true) as boolean,
lineWrap: persistentStore.get('line_wrap', true) as boolean, lineWrap: persistentStore.get('line_wrap', true) as boolean,
applicationTheme: persistentStore.get('application_theme', defaultAppTheme) as string, applicationTheme: persistentStore.get('application_theme', defaultAppTheme) as ApplicationTheme,
editorTheme: persistentStore.get('editor_theme', defaultEditorTheme) as string, editorTheme: persistentStore.get('editor_theme', defaultEditorTheme) as string,
editorFontSize: persistentStore.get('editor_font_size', 'medium') as EditorFontSize, editorFontSize: persistentStore.get('editor_font_size', 'medium') as EditorFontSize,
restoreTabs: persistentStore.get('restore_tabs', true) as boolean, restoreTabs: persistentStore.get('restore_tabs', true) as boolean,

View File

@ -25,16 +25,16 @@ import { Customizations } from 'common/interfaces/customizations';
export interface WorkspaceTab { export interface WorkspaceTab {
uid: string; uid: string;
tab?: string; tab?: string;
index: number; index?: number;
selected: boolean; selected?: boolean;
type: string; type?: string;
schema?: string; schema?: string;
elementName?: string; elementName?: string;
elementNewName?: string; elementNewName?: string;
elementType?: string; elementType?: string;
isChanged?: boolean; isChanged?: boolean;
content?: string; content?: string;
autorun: boolean; autorun?: boolean;
} }
export interface WorkspaceStructure { export interface WorkspaceStructure {
@ -70,7 +70,7 @@ export interface Workspace {
variables: { name: string; value: string }[]; variables: { name: string; value: string }[];
collations: CollationInfos[]; collations: CollationInfos[];
users: { host: string; name: string; password: string }[]; users: { host: string; name: string; password: string }[];
breadcrumbs: Breadcrumb[]; breadcrumbs: Breadcrumb;
loadingElements: { name: string; schema: string; type: string }[]; loadingElements: { name: string; schema: string; type: string }[];
loadedSchemas: Set<string>; loadedSchemas: Set<string>;
dataTypes?: { [key: string]: TypesGroup[] }; dataTypes?: { [key: string]: TypesGroup[] };
@ -171,18 +171,18 @@ export const useWorkspacesStore = defineStore('workspaces', {
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').default;
indexTypes = require('common/index-types/mysql'); indexTypes = require('common/index-types/mysql').default;
clientCustomizations = customizations.mysql; clientCustomizations = customizations.mysql;
break; break;
case 'pg': case 'pg':
dataTypes = require('common/data-types/postgresql'); dataTypes = require('common/data-types/postgresql').default;
indexTypes = require('common/index-types/postgresql'); indexTypes = require('common/index-types/postgresql').default;
clientCustomizations = customizations.pg; clientCustomizations = customizations.pg;
break; break;
case 'sqlite': case 'sqlite':
dataTypes = require('common/data-types/sqlite'); dataTypes = require('common/data-types/sqlite').default;
indexTypes = require('common/index-types/sqlite'); indexTypes = require('common/index-types/sqlite').default;
clientCustomizations = customizations.sqlite; clientCustomizations = customizations.sqlite;
break; break;
} }
@ -392,7 +392,7 @@ export const useWorkspacesStore = defineStore('workspaces', {
variables: [], variables: [],
collations: [], collations: [],
users: [], users: [],
breadcrumbs: [], breadcrumbs: {},
loadingElements: [], loadingElements: [],
loadedSchemas: new Set() loadedSchemas: new Set()
}; };
@ -684,7 +684,7 @@ export const useWorkspacesStore = defineStore('workspaces', {
: workspace : workspace
); );
}, },
updateTabs ({ uid, tabs }: {uid: string; tabs: string[]}) { updateTabs ({ uid, tabs }: {uid: string; tabs: WorkspaceTab[]}) {
this.workspaces = (this.workspaces as Workspace[]).map(workspace => workspace.uid === uid this.workspaces = (this.workspaces as Workspace[]).map(workspace => workspace.uid === uid
? { ...workspace, tabs } ? { ...workspace, tabs }
: workspace : workspace