1
1
mirror of https://github.com/Fabio286/antares.git synced 2025-02-16 19:50:37 +01:00

feat(PostgreSQL): ability to switch the database, closes #432

This commit is contained in:
Fabio Di Stasio 2023-06-13 18:10:52 +02:00
parent 9d00f58998
commit 89815bf5e7
15 changed files with 15113 additions and 33 deletions

14939
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
import * as antares from 'common/interfaces/antares';
import { ipcMain } from 'electron';
export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('get-databases', async (event, uid) => {
try {
const result = await connections[uid].getDatabases();
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
};

View File

@ -9,6 +9,7 @@ import functions from './functions';
import schedulers from './schedulers'; import schedulers from './schedulers';
import updates from './updates'; import updates from './updates';
import application from './application'; import application from './application';
import database from './database';
import schema from './schema'; import schema from './schema';
import users from './users'; import users from './users';
@ -22,6 +23,7 @@ export default () => {
routines(connections); routines(connections);
functions(connections); functions(connections);
schedulers(connections); schedulers(connections);
database(connections);
schema(connections); schema(connections);
users(connections); users(connections);
updates(); updates();

View File

@ -162,6 +162,10 @@ export abstract class AntaresCore {
throw new Error('Method "getDbConfig" not implemented'); throw new Error('Method "getDbConfig" not implemented');
} }
getDatabases () {
throw new Error('Method "getDatabases" not implemented');
}
createSchema (...args: any) { createSchema (...args: any) {
throw new Error('Method "createSchema" not implemented'); throw new Error('Method "createSchema" not implemented');
} }

View File

@ -154,7 +154,7 @@ export class PostgreSQLClient extends AntaresCore {
host: this._params.host, host: this._params.host,
port: this._params.port, port: this._params.port,
user: this._params.user, user: this._params.user,
database: undefined as string | undefined, database: 'postgres' as string,
password: this._params.password, password: this._params.password,
ssl: null as mysql.SslOptions ssl: null as mysql.SslOptions
}; };
@ -262,6 +262,18 @@ export class PostgreSQLClient extends AntaresCore {
return []; return [];
} }
async getDatabases () {
const { rows } = await this.raw('SELECT datname FROM pg_database WHERE datistemplate = false');
if (rows) {
return rows.reduce((acc, cur) => {
acc.push(cur.datname);
return acc;
}, [] as string[]);
}
else
return [];
}
async getStructure (schemas: Set<string>) { async getStructure (schemas: Set<string>) {
/* eslint-disable camelcase */ /* eslint-disable camelcase */
interface ShowTableResult { interface ShowTableResult {

View File

@ -124,8 +124,8 @@ else {
if (isWindows) if (isWindows)
mainWindow.show(); mainWindow.show();
if (isDevelopment) // if (isDevelopment)
mainWindow.webContents.openDevTools(); // mainWindow.webContents.openDevTools();
process.on('uncaughtException', error => { process.on('uncaughtException', error => {
mainWindow.webContents.send('unhandled-exception', error); mainWindow.webContents.send('unhandled-exception', error);

View File

@ -380,7 +380,7 @@ emit('folder-sort');// To apply changes on component key change
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
line-height: 1; line-height: 1.02;
transition: bottom .2s; transition: bottom .2s;
} }
} }
@ -444,7 +444,7 @@ emit('folder-sort');// To apply changes on component key change
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
line-height: 1; line-height: 1.02;
} }
} }
} }

View File

@ -565,7 +565,11 @@ const workspace = computed(() => getWorkspace(props.connection.uid));
const draggableTabs = computed<WorkspaceTab[]>({ const draggableTabs = computed<WorkspaceTab[]>({
get () { get () {
return workspace.value.tabs; if (workspace.value.customizations.database)
return workspace.value.tabs.filter(tab => tab.type === 'query' || tab.database === workspace.value.database);
else
return workspace.value.tabs;
}, },
set (val) { set (val) {
updateTabs({ uid: props.connection.uid, tabs: val }); updateTabs({ uid: props.connection.uid, tabs: val });

View File

@ -120,6 +120,7 @@
v-model="connection.database" v-model="connection.database"
class="form-input" class="form-input"
type="text" type="text"
:placeholder="clientCustomizations.defaultDatabase"
> >
</div> </div>
</div> </div>

View File

@ -122,6 +122,7 @@
v-model="localConnection.database" v-model="localConnection.database"
class="form-input" class="form-input"
type="text" type="text"
:placeholder="clientCustomizations.defaultDatabase"
> >
</div> </div>
</div> </div>

View File

@ -10,7 +10,18 @@
@keydown="explorebarSearch" @keydown="explorebarSearch"
> >
<div class="workspace-explorebar-header"> <div class="workspace-explorebar-header">
<span class="workspace-explorebar-title">{{ connectionName }}</span> <div
v-if="customizations.database"
class="workspace-explorebar-database-switch"
:title="t('message.switchDatabase')"
>
<BaseSelect
v-model="selectedDatabase"
:options="databases"
class="form-select select-sm text-bold my-0"
/>
</div>
<span v-else class="workspace-explorebar-title">{{ connectionName }}</span>
<span v-if="workspace.connectionStatus === 'connected'" class="workspace-explorebar-tools"> <span v-if="workspace.connectionStatus === 'connected'" class="workspace-explorebar-tools">
<i <i
v-if="customizations.schemas" v-if="customizations.schemas"
@ -124,10 +135,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Component, computed, onMounted, Ref, ref, watch } from 'vue'; import { Component, computed, onMounted, Prop, Ref, ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { ConnectionParams } from 'common/interfaces/antares';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
@ -141,12 +153,14 @@ import TableContext from '@/components/WorkspaceExploreBarTableContext.vue';
import MiscContext from '@/components/WorkspaceExploreBarMiscContext.vue'; import MiscContext from '@/components/WorkspaceExploreBarMiscContext.vue';
import MiscFolderContext from '@/components/WorkspaceExploreBarMiscFolderContext.vue'; import MiscFolderContext from '@/components/WorkspaceExploreBarMiscFolderContext.vue';
import ModalNewSchema from '@/components/ModalNewSchema.vue'; import ModalNewSchema from '@/components/ModalNewSchema.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import Databases from '@/ipc-api/Databases';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
connection: Object, connection: Object as Prop<ConnectionParams>,
isSelected: Boolean isSelected: Boolean
}); });
@ -160,11 +174,13 @@ const { explorebarSize } = storeToRefs(settingsStore);
const { changeExplorebarSize } = settingsStore; const { changeExplorebarSize } = settingsStore;
const { const {
getWorkspace, getWorkspace,
switchConnection,
removeConnected: disconnectWorkspace, removeConnected: disconnectWorkspace,
refreshStructure, refreshStructure,
newTab, newTab,
removeTabs, removeTabs,
setSearchTerm, setSearchTerm,
setDatabase,
addLoadingElement, addLoadingElement,
removeLoadingElement removeLoadingElement
} = workspacesStore; } = workspacesStore;
@ -172,6 +188,7 @@ const {
const searchInput: Ref<HTMLInputElement> = ref(null); const searchInput: Ref<HTMLInputElement> = ref(null);
const explorebar: Ref<HTMLInputElement> = ref(null); const explorebar: Ref<HTMLInputElement> = ref(null);
const resizer: Ref<HTMLInputElement> = ref(null); const resizer: Ref<HTMLInputElement> = ref(null);
const databases: Ref<string[]> = ref([]);
const schema: Ref<Component & { selectSchema: (name: string) => void; $refs: {schemaAccordion: HTMLDetailsElement} }[]> = ref(null); const schema: Ref<Component & { selectSchema: (name: string) => void; $refs: {schemaAccordion: HTMLDetailsElement} }[]> = ref(null);
const isRefreshing = ref(false); const isRefreshing = ref(false);
const isNewDBModal = ref(false); const isNewDBModal = ref(false);
@ -185,6 +202,7 @@ const isMiscFolderContext = ref(false);
const databaseContextEvent = ref(null); const databaseContextEvent = ref(null);
const tableContextEvent = ref(null); const tableContextEvent = ref(null);
const miscContextEvent = ref(null); const miscContextEvent = ref(null);
const selectedDatabase = ref(props.connection.database);
const selectedSchema = ref(''); const selectedSchema = ref('');
const selectedTable = ref(null); const selectedTable = ref(null);
const selectedMisc = ref(null); const selectedMisc = ref(null);
@ -230,9 +248,14 @@ watch(searchTerm, () => {
}, 200); }, 200);
}); });
watch(selectedDatabase, (val, oldVal) => {
if (oldVal)
switchConnection({ ...props.connection, database: selectedDatabase.value });
});
localWidth.value = explorebarSize.value; localWidth.value = explorebarSize.value;
onMounted(() => { onMounted(async () => {
resizer.value.addEventListener('mousedown', (e: MouseEvent) => { resizer.value.addEventListener('mousedown', (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
@ -240,10 +263,28 @@ onMounted(() => {
window.addEventListener('mouseup', stopResize); window.addEventListener('mouseup', stopResize);
}); });
if (workspace.value.structure.length === 1) { // Auto-open if juust one schema if (workspace.value.structure.length === 1) { // Auto-open if just one schema
schema.value[0].selectSchema(workspace.value.structure[0].name); schema.value[0].selectSchema(workspace.value.structure[0].name);
schema.value[0].$refs.schemaAccordion.open = true; schema.value[0].$refs.schemaAccordion.open = true;
} }
if (customizations.value.database) {
try {
const { status, response } = await Databases.getDatabases(props.connection.uid);
if (status === 'success') {
databases.value = response;
if (selectedDatabase.value === '') {
selectedDatabase.value = response[0];
setDatabase(selectedDatabase.value);
}
}
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
}
}); });
const refresh = async () => { const refresh = async () => {
@ -254,8 +295,11 @@ const refresh = async () => {
} }
}; };
const explorebarSearch = () => { const explorebarSearch = (event: KeyboardEvent) => {
searchInput.value.focus(); const isLetter = (event.key >= 'a' && event.key <= 'z');
const isNumber = (event.key >= '0' && event.key <= '9');
if (isLetter || isNumber)
searchInput.value.focus();
}; };
const resize = (e: MouseEvent) => { const resize = (e: MouseEvent) => {
@ -497,13 +541,31 @@ const toggleSearchMethod = () => {
} }
} }
.workspace-explorebar-database-switch {
width: 100%;
display: flex;
justify-content: space-between;
z-index: 20;
margin-right: 5px;
margin-left: -4px;
margin-top: -3px;
margin-bottom: -0.5rem;
height: 24px;
.form-select.select-sm {
font-size: 0.6rem;
height: 1.2rem;
line-height: 1rem;
}
}
.workspace-explorebar-search { .workspace-explorebar-search {
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
font-size: 0.6rem; font-size: 0.6rem;
height: 28px; height: 28px;
margin: 5px 0; margin: 0 0 5px 0;
z-index: 10; z-index: 10;
.has-icon-right { .has-icon-right {
@ -533,7 +595,7 @@ const toggleSearchMethod = () => {
.workspace-explorebar-body { .workspace-explorebar-body {
width: 100%; width: 100%;
height: calc((100vh - 68px) - #{$excluding-size}); height: calc((100vh - 63px) - #{$excluding-size});
overflow: overlay; overflow: overlay;
padding: 0 0.1rem; padding: 0 0.1rem;
} }

View File

@ -291,6 +291,11 @@ watch(selectedSchema, () => {
changeBreadcrumbs({ schema: selectedSchema.value, query: `Query #${props.tab.index}` }); changeBreadcrumbs({ schema: selectedSchema.value, query: `Query #${props.tab.index}` });
}); });
watch(databaseSchemas, () => {
if (!databaseSchemas.value.includes(selectedSchema.value))
selectedSchema.value = null;
}, { deep: true });
const runQuery = async (query: string) => { const runQuery = async (query: string) => {
if (!query || isQuering.value) return; if (!query || isQuering.value) return;
isQuering.value = true; isQuering.value = true;
@ -496,9 +501,6 @@ defineExpose({ resizeResults });
query.value = props.tab.content as string; query.value = props.tab.content as string;
selectedSchema.value = props.tab.schema || breadcrumbsSchema.value; selectedSchema.value = props.tab.schema || breadcrumbsSchema.value;
if (!databaseSchemas.value.includes(selectedSchema.value))
selectedSchema.value = null;
window.addEventListener('resize', onWindowResize); window.addEventListener('resize', onWindowResize);
const reloadListener = () => { const reloadListener = () => {

View File

@ -348,7 +348,8 @@ export const enUS = {
closeAllTabs: 'Close all tabs', closeAllTabs: 'Close all tabs',
closeOtherTabs: 'Close other tabs', closeOtherTabs: 'Close other tabs',
closeTabsToLeft: 'Close tabs to the left', closeTabsToLeft: 'Close tabs to the left',
closeTabsToRight: 'Close tabs to the right' closeTabsToRight: 'Close tabs to the right',
switchDatabase: 'Switch the database'
}, },
faker: { faker: {
address: 'Address', address: 'Address',

View File

@ -0,0 +1,9 @@
import { ipcRenderer } from 'electron';
import { unproxify } from '../libs/unproxify';
import { IpcResponse } from 'common/interfaces/antares';
export default class {
static getDatabases (params: string): Promise<IpcResponse> {
return ipcRenderer.invoke('get-databases', unproxify(params));
}
}

View File

@ -30,6 +30,7 @@ export interface WorkspaceTab {
index?: number; index?: number;
selected?: boolean; selected?: boolean;
type?: string; type?: string;
database?: string;
schema?: string; schema?: string;
elementName?: string; elementName?: string;
elementNewName?: string; elementNewName?: string;
@ -65,6 +66,7 @@ export interface Breadcrumb {
export interface Workspace { export interface Workspace {
uid: string; uid: string;
client?: ClientCode; client?: ClientCode;
database?: string;
connectionStatus: string; connectionStatus: string;
selectedTab: string | number; selectedTab: string | number;
searchTerm: string; searchTerm: string;
@ -145,14 +147,15 @@ export const useWorkspacesStore = defineStore('workspaces', {
else else
this.selectedWorkspace = uid; this.selectedWorkspace = uid;
}, },
async connectWorkspace (connection: ConnectionParams & { pgConnString?: string }) { async connectWorkspace (connection: ConnectionParams & { pgConnString?: string }, mode?: string) {
this.workspaces = (this.workspaces as Workspace[]).map(workspace => workspace.uid === connection.uid this.workspaces = (this.workspaces as Workspace[]).map(workspace => workspace.uid === connection.uid
? { ? {
...workspace, ...workspace,
structure: {}, structure: [],
breadcrumbs: {}, breadcrumbs: {},
loadedSchemas: new Set(), loadedSchemas: new Set(),
connectionStatus: 'connecting' database: connection.database,
connectionStatus: mode === 'switch' ? 'connected' : 'connecting'
} }
: workspace); : workspace);
@ -168,7 +171,7 @@ export const useWorkspacesStore = defineStore('workspaces', {
this.workspaces = (this.workspaces as Workspace[]).map(workspace => workspace.uid === connection.uid this.workspaces = (this.workspaces as Workspace[]).map(workspace => workspace.uid === connection.uid
? { ? {
...workspace, ...workspace,
structure: {}, structure: [],
breadcrumbs: {}, breadcrumbs: {},
loadedSchemas: new Set(), loadedSchemas: new Set(),
connectionStatus: 'failed' connectionStatus: 'failed'
@ -228,6 +231,12 @@ export const useWorkspacesStore = defineStore('workspaces', {
}, null); }, null);
} }
const selectedTab = cachedTabs.length
? connection.database
? cachedTabs.filter(tab => tab.type === 'query' || tab.database === connection.database)[0].uid
: cachedTabs[0].uid
: null;
this.workspaces = (this.workspaces as Workspace[]).map(workspace => workspace.uid === connection.uid this.workspaces = (this.workspaces as Workspace[]).map(workspace => workspace.uid === connection.uid
? { ? {
...workspace, ...workspace,
@ -238,7 +247,7 @@ export const useWorkspacesStore = defineStore('workspaces', {
structure: response, structure: response,
connectionStatus: 'connected', connectionStatus: 'connected',
tabs: cachedTabs, tabs: cachedTabs,
selectedTab: cachedTabs.length ? cachedTabs[0].uid : null, selectedTab,
version version
} }
: workspace); : workspace);
@ -385,7 +394,7 @@ export const useWorkspacesStore = defineStore('workspaces', {
this.workspaces = (this.workspaces as Workspace[]).map(workspace => workspace.uid === uid this.workspaces = (this.workspaces as Workspace[]).map(workspace => workspace.uid === uid
? { ? {
...workspace, ...workspace,
structure: {}, structure: [],
breadcrumbs: {}, breadcrumbs: {},
loadedSchemas: new Set(), loadedSchemas: new Set(),
connectionStatus: 'disconnected' connectionStatus: 'disconnected'
@ -394,6 +403,10 @@ export const useWorkspacesStore = defineStore('workspaces', {
this.selectTab({ uid, tab: 0 }); this.selectTab({ uid, tab: 0 });
}, },
async switchConnection (connection: ConnectionParams & { pgConnString?: string }) {
await Connection.disconnect(connection.uid);
return this.connectWorkspace(connection, 'switch');
},
addWorkspace (uid: string) { addWorkspace (uid: string) {
const workspace: Workspace = { const workspace: Workspace = {
uid, uid,
@ -468,7 +481,15 @@ export const useWorkspacesStore = defineStore('workspaces', {
} }
: workspace); : workspace);
}, },
_addTab ({ uid, tab, content, type, autorun, schema, elementName, elementType }: WorkspaceTab) { setDatabase (databaseName: string) {
this.workspaces = (this.workspaces as Workspace[]).map(workspace => workspace.uid === this.getSelected
? {
...workspace,
database: databaseName
}
: workspace);
},
_addTab ({ uid, tab, content, type, autorun, schema, database, elementName, elementType }: WorkspaceTab) {
if (type === 'query') if (type === 'query')
tabIndex[uid] = tabIndex[uid] ? ++tabIndex[uid] : 1; tabIndex[uid] = tabIndex[uid] ? ++tabIndex[uid] : 1;
@ -477,6 +498,7 @@ export const useWorkspacesStore = defineStore('workspaces', {
index: type === 'query' ? tabIndex[uid] : null, index: type === 'query' ? tabIndex[uid] : null,
selected: false, selected: false,
type, type,
database,
schema, schema,
elementName, elementName,
elementType, elementType,
@ -534,6 +556,7 @@ export const useWorkspacesStore = defineStore('workspaces', {
content, content,
type, type,
autorun, autorun,
database: workspaceTabs.database,
schema, schema,
elementName, elementName,
elementType elementType
@ -572,7 +595,7 @@ export const useWorkspacesStore = defineStore('workspaces', {
}); });
tabUid = uidGen('T'); tabUid = uidGen('T');
this._addTab({ uid, tab: tabUid, content, type, autorun, schema, elementName, elementType }); this._addTab({ uid, tab: tabUid, content, type, autorun, database: workspaceTabs.database, schema, elementName, elementType });
} }
else { else {
this._replaceTab({ uid, tab: tab.uid, type, schema, elementName, elementType }); this._replaceTab({ uid, tab: tab.uid, type, schema, elementName, elementType });
@ -582,7 +605,7 @@ export const useWorkspacesStore = defineStore('workspaces', {
} }
else { else {
tabUid = uidGen('T'); tabUid = uidGen('T');
this._addTab({ uid, tab: tabUid, content, type, autorun, schema, elementName, elementType }); this._addTab({ uid, tab: tabUid, content, type, autorun, database: workspaceTabs.database, schema, elementName, elementType });
} }
} }
} }
@ -603,18 +626,18 @@ export const useWorkspacesStore = defineStore('workspaces', {
: false; : false;
if (existentTab) { if (existentTab) {
this._replaceTab({ uid, tab: existentTab.uid, type, schema, elementName, elementType }); this._replaceTab({ uid, tab: existentTab.uid, type, database: workspaceTabs.database, schema, elementName, elementType });
tabUid = existentTab.uid; tabUid = existentTab.uid;
} }
else { else {
tabUid = uidGen('T'); tabUid = uidGen('T');
this._addTab({ uid, tab: tabUid, content, type, autorun, schema, elementName, elementType }); this._addTab({ uid, tab: tabUid, content, type, autorun, database: workspaceTabs.database, schema, elementName, elementType });
} }
} }
break; break;
default: default:
tabUid = uidGen('T'); tabUid = uidGen('T');
this._addTab({ uid, tab: tabUid, content, type, autorun, schema, elementName, elementType }); this._addTab({ uid, tab: tabUid, content, type, autorun, database: workspaceTabs.database, schema, elementName, elementType });
break; break;
} }
@ -626,8 +649,14 @@ export const useWorkspacesStore = defineStore('workspaces', {
? workspace.tabs.some(tab => tab.uid === workspace.selectedTab) ? workspace.tabs.some(tab => tab.uid === workspace.selectedTab)
: false; : false;
if (!isSelectedExistent && workspace.tabs.length) if (!isSelectedExistent && workspace.tabs.length) {
this.selectTab({ uid, tab: workspace.tabs[workspace.tabs.length - 1].uid }); if (workspace.customizations.database) {
const databaseTabs = workspace.tabs.filter(tab => tab.type === 'query' || tab.database === workspace.database);
this.selectTab({ uid, tab: databaseTabs[databaseTabs.length - 1].uid });
}
else
this.selectTab({ uid, tab: workspace.tabs[workspace.tabs.length - 1].uid });
}
}, },
updateTabContent ({ uid, tab, type, schema, content }: WorkspaceTab) { updateTabContent ({ uid, tab, type, schema, content }: WorkspaceTab) {
this._replaceTab({ uid, tab, type, schema, content }); this._replaceTab({ uid, tab, type, schema, content });