1
1
mirror of https://github.com/Fabio286/antares.git synced 2025-06-05 21:59:22 +02:00

feat(PostgreSQL): support to materialized views, closes #804

This commit is contained in:
2024-06-14 18:05:29 +02:00
parent a973ec3c60
commit 0b9898f3e7
17 changed files with 942 additions and 8 deletions

View File

@ -63,7 +63,7 @@
>
<BaseIcon
class="mt-1 mr-1"
:icon-name="element.elementType === 'view' ? 'mdiTableEye' : 'mdiTable'"
:icon-name="['view', 'materializedview'].includes(element.elementType) ? 'mdiTableEye' : 'mdiTable'"
:size="18"
/>
<span :title="`${t('general.data').toUpperCase()}: ${t(`database.${element.elementType}`)}`">
@ -80,7 +80,7 @@
<a v-else-if="element.type === 'data'" class="tab-link">
<BaseIcon
class="mt-1 mr-1"
:icon-name="element.elementType === 'view' ? 'mdiTableEye' : 'mdiTable'"
:icon-name="['view', 'materializedview'].includes(element.elementType) ? 'mdiTableEye' : 'mdiTable'"
:size="18"
/>
<span :title="`${t('general.data').toUpperCase()}: ${t(`database.${element.elementType}`)}`">
@ -157,6 +157,27 @@
</span>
</a>
<a
v-else-if="element.type === 'materialized-view-props'"
class="tab-link"
:class="{'badge': element.isChanged}"
>
<BaseIcon
class="mr-1"
icon-name="mdiWrenchCog"
:size="18"
/>
<span :title="`${t('application.settings').toUpperCase()}: ${t(`database.view`)}`">
{{ cutText(element.elementName, 20, true) }}
<span
class="btn btn-clear"
:title="t('general.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
</span>
</a>
<a
v-else-if="element.type === 'new-view'"
class="tab-link"
@ -178,6 +199,27 @@
</span>
</a>
<a
v-else-if="element.type === 'new-materialized-view'"
class="tab-link"
:class="{'badge': element.isChanged}"
>
<BaseIcon
class="mr-1"
icon-name="mdiShapeSquarePlus"
:size="18"
/>
<span :title="`${t('general.new').toUpperCase()}: ${t(`database.${element.elementType}`)}`">
{{ t('database.newMaterializedView') }}
<span
class="btn btn-clear"
:title="t('general.close')"
@mousedown.left.stop
@click.stop="closeTab(element)"
/>
</span>
</a>
<a
v-else-if="element.type === 'new-trigger'"
class="tab-link"
@ -446,6 +488,14 @@
:is-selected="selectedTab === tab.uid && isSelected"
:schema="tab.schema"
/>
<WorkspaceTabNewMaterializedView
v-else-if="tab.type === 'new-materialized-view'"
:tab-uid="tab.uid"
:tab="tab"
:connection="connection"
:is-selected="selectedTab === tab.uid && isSelected"
:schema="tab.schema"
/>
<WorkspaceTabPropsView
v-else-if="tab.type === 'view-props'"
:tab-uid="tab.uid"
@ -454,6 +504,14 @@
:view="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabPropsMaterializedView
v-else-if="tab.type === 'materialized-view-props'"
:tab-uid="tab.uid"
:is-selected="selectedTab === tab.uid && isSelected"
:connection="connection"
:view="tab.elementName"
:schema="tab.schema"
/>
<WorkspaceTabNewTrigger
v-else-if="tab.type === 'new-trigger'"
:tab-uid="tab.uid"
@ -596,6 +654,9 @@ import Connection from '@/ipc-api/Connection';
import { useConsoleStore } from '@/stores/console';
import { useWorkspacesStore, WorkspaceTab } from '@/stores/workspaces';
import WorkspaceTabNewMaterializedView from './WorkspaceTabNewMaterializedView.vue';
import WorkspaceTabPropsMaterializedView from './WorkspaceTabPropsMaterializedView.vue';
const { t } = useI18n();
const { cutText } = useFilters();
@ -638,7 +699,6 @@ const draggableTabs = computed<WorkspaceTab[]>({
get () {
if (workspace.value.customizations.database)
return workspace.value.tabs.filter(tab => tab.type === 'query' || tab.database === workspace.value.database);
else
return workspace.value.tabs;
},
@ -689,6 +749,7 @@ const openAsPermanentTab = (tab: WorkspaceTab) => {
const permanentTabs = {
table: 'data',
view: 'data',
materializedView: 'data',
trigger: 'trigger-props',
triggerFunction: 'trigger-function-props',
function: 'function-props',

View File

@ -111,6 +111,7 @@
@close-context="closeDatabaseContext"
@open-create-table-tab="openCreateElementTab('table')"
@open-create-view-tab="openCreateElementTab('view')"
@open-create-materialized-view-tab="openCreateElementTab('materialized-view')"
@open-create-trigger-tab="openCreateElementTab('trigger')"
@open-create-routine-tab="openCreateElementTab('routine')"
@open-create-function-tab="openCreateElementTab('function')"
@ -142,6 +143,7 @@
:selected-schema="selectedSchema"
:context-event="miscContextEvent"
@open-create-view-tab="openCreateElementTab('view')"
@open-create-materializedview-tab="openCreateElementTab('materialized-view')"
@open-create-trigger-tab="openCreateElementTab('trigger')"
@open-create-trigger-function-tab="openCreateElementTab('trigger-function')"
@open-create-routine-tab="openCreateElementTab('routine')"

View File

@ -15,6 +15,18 @@
:size="18"
/> {{ t('database.createNewView') }}</span>
</div>
<div
v-if="props.selectedMisc === 'materializedview'"
class="context-element"
@click="emit('open-create-materializedview-tab')"
>
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiTableCog"
:size="18"
/> {{ t('database.createNewMaterializedView') }}</span>
</div>
<div
v-if="props.selectedMisc === 'trigger'"
class="context-element"
@ -94,6 +106,7 @@ const props = defineProps({
const emit = defineEmits([
'open-create-view-tab',
'open-create-materializedview-tab',
'open-create-trigger-tab',
'open-create-routine-tab',
'open-create-function-tab',

View File

@ -116,6 +116,55 @@
</details>
</div>
<div v-if="filteredMatViews.length && customizations.materializedViews" class="database-misc">
<details class="accordion">
<summary
class="accordion-header misc-name"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger}"
@contextmenu.prevent="showMiscFolderContext($event, 'materializedview')"
>
<BaseIcon
class="misc-icon mr-1"
icon-name="mdiFolderEye"
:size="18"
/>
<BaseIcon
class="misc-icon open-folder mr-1"
icon-name="mdiFolderOpen"
:size="18"
/>
{{ t('database.materializedview', 2) }}
</summary>
<div class="accordion-body">
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="view of filteredMatViews"
:key="view.name"
class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.view === view.name}"
@mousedown.left="selectTable({schema: database.name, table: view})"
@dblclick="openDataTab({schema: database.name, table: view})"
@contextmenu.prevent="showTableContext($event, view)"
>
<a class="table-name">
<div v-if="checkLoadingStatus(view.name, 'table')" class="icon loading mr-1" />
<BaseIcon
v-else
class="table-icon mr-1"
icon-name="mdiTableEye"
:size="18"
:style="`min-width: 18px`"
/>
<span v-html="highlightWord(view.name)" />
</a>
</li>
</ul>
</div>
</div>
</details>
</div>
<div v-if="filteredTriggers.length && customizations.triggers" class="database-misc">
<details class="accordion">
<summary
@ -438,7 +487,14 @@ const filteredViews = computed(() => {
if (props.searchMethod === 'elements')
return props.database.tables.filter(table => table.name.search(searchTerm.value) >= 0 && table.type === 'view');
else
return props.database.tables;
return props.database.tables.filter(table => table.type === 'view');
});
const filteredMatViews = computed(() => {
if (props.searchMethod === 'elements')
return props.database.tables.filter(table => table.name.search(searchTerm.value) >= 0 && table.type === 'materializedview');
else
return props.database.tables.filter(table => table.type === 'materializedview');
});
const filteredTriggers = computed(() => {

View File

@ -40,6 +40,18 @@
:size="18"
/> {{ t('database.view') }}</span>
</div>
<div
v-if="workspace.customizations.materializedViewAdd"
class="context-element"
@click="openCreateMaterializedViewTab"
>
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiTableEye"
:size="18"
/> {{ t('database.materializedview') }}</span>
</div>
<div
v-if="workspace.customizations.triggerAdd"
class="context-element"
@ -221,6 +233,7 @@ const props = defineProps({
const emit = defineEmits([
'open-create-table-tab',
'open-create-view-tab',
'open-create-materialized-view-tab',
'open-create-trigger-tab',
'open-create-routine-tab',
'open-create-function-tab',
@ -257,6 +270,10 @@ const openCreateViewTab = () => {
emit('open-create-view-tab');
};
const openCreateMaterializedViewTab = () => {
emit('open-create-materialized-view-tab');
};
const openCreateTriggerTab = () => {
emit('open-create-trigger-tab');
};

View File

@ -47,6 +47,18 @@
:size="18"
/> {{ t('application.settings') }}</span>
</div>
<div
v-if="selectedTable && selectedTable.type === 'materializedview' && customizations.materializedViewSettings"
class="context-element"
@click="openMaterializedViewSettingTab"
>
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiWrenchCog"
:size="18"
/> {{ t('application.settings') }}</span>
</div>
<div
v-if="selectedTable && selectedTable.type === 'table' && customizations.tableDuplicate"
class="context-element"
@ -238,6 +250,23 @@ const openViewSettingTab = () => {
closeContext();
};
const openMaterializedViewSettingTab = () => {
newTab({
uid: selectedWorkspace.value,
elementType: 'table',
elementName: props.selectedTable.name,
schema: props.selectedSchema,
type: 'materialized-view-props'
});
changeBreadcrumbs({
schema: props.selectedSchema,
view: props.selectedTable.name
});
closeContext();
};
const duplicateTable = () => {
emit('duplicate-table', { schema: props.selectedSchema, table: props.selectedTable });
};

View File

@ -0,0 +1,293 @@
<template>
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
@click="saveChanges"
>
<BaseIcon
class="mr-1"
icon-name="mdiContentSave"
:size="24"
/>
<span>{{ t('general.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="t('database.clearChanges')"
@click="clearChanges"
>
<BaseIcon
class="mr-1"
icon-name="mdiDeleteSweep"
:size="24"
/>
<span>{{ t('general.clear') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="t('database.schema')">
<BaseIcon
class="mt-1 mr-1"
icon-name="mdiDatabase"
:size="18"
/><b>{{ schema }}</b>
</div>
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ t('general.name') }}</label>
<input
ref="firstInput"
v-model="localView.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-auto">
<div v-if="workspace.customizations.definer" class="form-group">
<label class="form-label">{{ t('database.definer') }}</label>
<BaseSelect
v-model="localView.definer"
:options="users"
:option-label="(user: any) => user.value === '' ? t('database.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select"
/>
</div>
</div>
<div class="column col-auto mr-2">
<div v-if="workspace.customizations.viewSqlSecurity" class="form-group">
<label class="form-label">{{ t('database.sqlSecurity') }}</label>
<BaseSelect
v-model="localView.security"
:options="['DEFINER', 'INVOKER']"
class="form-select"
/>
</div>
</div>
<div class="column col-auto mr-2">
<div v-if="workspace.customizations.viewAlgorithm" class="form-group">
<label class="form-label">{{ t('database.algorithm') }}</label>
<BaseSelect
v-model="localView.algorithm"
:options="['UNDEFINED', 'MERGE', 'TEMPTABLE']"
class="form-select"
/>
</div>
</div>
<div v-if="workspace.customizations.viewUpdateOption" class="column col-auto mr-2">
<div class="form-group">
<label class="form-label">{{ t('database.updateOption') }}</label>
<BaseSelect
v-model="localView.updateOption"
:option-track-by="(user: any) => user.value"
:options="[{label: 'None', value: ''}, {label: 'CASCADED', value: 'CASCADED'}, {label: 'LOCAL', value: 'LOCAL'}]"
class="form-select"
/>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ t('database.selectStatement') }}</label>
<QueryEditor
v-show="isSelected"
ref="queryEditor"
v-model="localView.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { Ace } from 'ace-builds';
import { ipcRenderer } from 'electron';
import { storeToRefs } from 'pinia';
import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import BaseIcon from '@/components/BaseIcon.vue';
import BaseLoader from '@/components/BaseLoader.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import QueryEditor from '@/components/QueryEditor.vue';
import Views from '@/ipc-api/Views';
import { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
const { t } = useI18n();
const props = defineProps({
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
});
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { consoleHeight } = storeToRefs(useConsoleStore());
const {
getWorkspace,
refreshStructure,
setUnsavedChanges,
changeBreadcrumbs,
newTab,
removeTab
} = workspacesStore;
const queryEditor: Ref<Component & {editor: Ace.Editor; $el: HTMLElement}> = ref(null);
const firstInput: Ref<HTMLInputElement> = ref(null);
const isLoading = ref(false);
const isSaving = ref(false);
const originalView = ref(null);
const localView = ref(null);
const editorHeight = ref(300);
const workspace = computed(() => getWorkspace(props.connection.uid));
const isChanged = computed(() => JSON.stringify(originalView.value) !== JSON.stringify(localView.value));
const isDefinerInUsers = computed(() => originalView.value ? workspace.value.users.some(user => originalView.value.definer === `\`${user.name}\`@\`${user.host}\``) : true);
const users = computed(() => {
const users = [{ value: '' }, ...workspace.value.users];
if (!isDefinerInUsers.value) {
const [name, host] = originalView.value.definer.replaceAll('`', '').split('@');
users.unshift({ name, host });
}
return users;
});
const saveChanges = async () => {
if (isSaving.value) return;
isSaving.value = true;
const params = {
uid: props.connection.uid,
schema: props.schema,
...localView.value
};
try {
const { status, response } = await Views.createMaterializedView(params);
if (status === 'success') {
await refreshStructure(props.connection.uid);
newTab({
uid: props.connection.uid,
schema: props.schema,
elementName: localView.value.name,
elementType: 'materializedview',
type: 'materialized-view-props'
});
removeTab({ uid: props.connection.uid, tab: props.tab.uid });
changeBreadcrumbs({ schema: props.schema, view: localView.value.name });
}
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isSaving.value = false;
};
const clearChanges = () => {
localView.value = JSON.parse(JSON.stringify(originalView.value));
queryEditor.value.editor.session.setValue(localView.value.sql);
};
const resizeQueryEditor = () => {
if (queryEditor.value) {
let sizeToSubtract = 0;
const footer = document.getElementById('footer');
if (footer) sizeToSubtract += footer.offsetHeight;
sizeToSubtract += consoleHeight.value;
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - sizeToSubtract;
editorHeight.value = size;
queryEditor.value.editor.resize();
}
};
const saveContentListener = () => {
const hasModalOpen = !!document.querySelectorAll('.modal.active').length;
if (props.isSelected && !hasModalOpen && isChanged.value)
saveChanges();
};
watch(() => props.isSelected, (val) => {
if (val) {
changeBreadcrumbs({ schema: props.schema, view: localView.value.name });
setTimeout(() => {
resizeQueryEditor();
}, 50);
}
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
watch(consoleHeight, () => {
resizeQueryEditor();
});
originalView.value = {
algorithm: 'UNDEFINED',
definer: '',
security: 'DEFINER',
updateOption: '',
sql: '',
name: ''
};
localView.value = JSON.parse(JSON.stringify(originalView.value));
setTimeout(() => {
resizeQueryEditor();
}, 50);
onMounted(() => {
if (props.isSelected)
changeBreadcrumbs({ schema: props.schema });
ipcRenderer.on('save-content', saveContentListener);
setTimeout(() => {
firstInput.value.focus();
}, 100);
window.addEventListener('resize', resizeQueryEditor);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeQueryEditor);
});
onBeforeUnmount(() => {
ipcRenderer.removeListener('save-content', saveContentListener);
});
</script>

View File

@ -0,0 +1,316 @@
<template>
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
@click="saveChanges"
>
<BaseIcon
class="mr-1"
icon-name="mdiContentSave"
:size="24"
/>
<span>{{ t('general.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="t('database.clearChanges')"
@click="clearChanges"
>
<BaseIcon
class="mr-1"
icon-name="mdiDeleteSweep"
:size="24"
/>
<span>{{ t('general.clear') }}</span>
</button>
</div>
<div class="workspace-query-info">
<div class="d-flex" :title="t('database.schema')">
<BaseIcon
class="mt-1 mr-1"
icon-name="mdiDatabase"
:size="18"
/><b>{{ schema }}</b>
</div>
</div>
</div>
</div>
<div class="container">
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ t('general.name') }}</label>
<input
v-model="localView.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="column col-auto">
<div v-if="workspace.customizations.definer" class="form-group">
<label class="form-label">{{ t('database.definer') }}</label>
<BaseSelect
v-model="localView.definer"
:options="users"
:option-label="(user: any) => user.value === '' ? t('database.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select"
/>
</div>
</div>
<div class="column col-auto mr-2">
<div v-if="workspace.customizations.viewSqlSecurity" class="form-group">
<label class="form-label">{{ t('database.sqlSecurity') }}</label>
<BaseSelect
v-model="localView.security"
:options="['DEFINER', 'INVOKER']"
class="form-select"
/>
</div>
</div>
<div class="column col-auto mr-2">
<div v-if="workspace.customizations.viewAlgorithm" class="form-group">
<label class="form-label">{{ t('database.algorithm') }}</label>
<BaseSelect
v-model="localView.algorithm"
:options="['UNDEFINED', 'MERGE', 'TEMPTABLE']"
class="form-select"
/>
</div>
</div>
<div v-if="workspace.customizations.viewUpdateOption" class="column col-auto mr-2">
<div class="form-group">
<label class="form-label">{{ t('database.updateOption') }}</label>
<BaseSelect
v-model="localView.updateOption"
:option-track-by="(user: any) => user.value"
:options="[{label: 'None', value: ''}, {label: 'CASCADED', value: 'CASCADED'}, {label: 'LOCAL', value: 'LOCAL'}]"
class="form-select"
/>
</div>
</div>
</div>
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ t('database.selectStatement') }}</label>
<QueryEditor
v-show="isSelected"
ref="queryEditor"
v-model="localView.sql"
:workspace="workspace"
:schema="schema"
:height="editorHeight"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { Ace } from 'ace-builds';
import { ipcRenderer } from 'electron';
import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import BaseIcon from '@/components/BaseIcon.vue';
import BaseLoader from '@/components/BaseLoader.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import QueryEditor from '@/components/QueryEditor.vue';
import Views from '@/ipc-api/Views';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
const { t } = useI18n();
const props = defineProps({
tabUid: String,
connection: Object,
isSelected: Boolean,
schema: String,
view: String
});
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const {
getWorkspace,
refreshStructure,
renameTabs,
changeBreadcrumbs,
setUnsavedChanges
} = workspacesStore;
const queryEditor: Ref<Component & {editor: Ace.Editor; $el: HTMLElement}> = ref(null);
const isLoading = ref(false);
const isSaving = ref(false);
const originalView = ref(null);
const localView = ref(null);
const editorHeight = ref(300);
const lastView = ref(null);
const sqlProxy = ref('');
const workspace = computed(() => getWorkspace(props.connection.uid));
const isChanged = computed(() => JSON.stringify(originalView.value) !== JSON.stringify(localView.value));
const isDefinerInUsers = computed(() => originalView.value ? workspace.value.users.some(user => originalView.value.definer === `\`${user.name}\`@\`${user.host}\``) : true);
const users = computed(() => {
const users = [{ value: '' }, ...workspace.value.users];
if (!isDefinerInUsers.value) {
const [name, host] = originalView.value.definer.replaceAll('`', '').split('@');
users.unshift({ name, host });
}
return users;
});
const getViewData = async () => {
if (!props.view) return;
isLoading.value = true;
localView.value = { sql: '' };
lastView.value = props.view;
const params = {
uid: props.connection.uid,
schema: props.schema,
view: props.view
};
try {
const { status, response } = await Views.getMaterializedViewInformations(params);
if (status === 'success') {
originalView.value = response;
localView.value = JSON.parse(JSON.stringify(originalView.value));
sqlProxy.value = localView.value.sql;
}
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
resizeQueryEditor();
isLoading.value = false;
};
const saveChanges = async () => {
if (isSaving.value) return;
isSaving.value = true;
const params = {
uid: props.connection.uid,
view: {
...localView.value,
schema: props.schema,
oldName: originalView.value.name
}
};
try {
const { status, response } = await Views.alterMaterializedView(params);
if (status === 'success') {
const oldName = originalView.value.name;
await refreshStructure(props.connection.uid);
if (oldName !== localView.value.name) {
renameTabs({
uid: props.connection.uid,
schema: props.schema,
elementName: oldName,
elementNewName: localView.value.name,
elementType: 'materializedview'
});
changeBreadcrumbs({ schema: props.schema, view: localView.value.name });
}
else
getViewData();
}
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isSaving.value = false;
};
const clearChanges = () => {
localView.value = JSON.parse(JSON.stringify(originalView.value));
queryEditor.value.editor.session.setValue(localView.value.sql);
};
const resizeQueryEditor = () => {
if (queryEditor.value) {
const footer = document.getElementById('footer');
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
editorHeight.value = size;
queryEditor.value.editor.resize();
}
};
const saveContentListener = () => {
const hasModalOpen = !!document.querySelectorAll('.modal.active').length;
if (props.isSelected && !hasModalOpen && isChanged.value)
saveChanges();
};
watch(() => props.schema, async () => {
if (props.isSelected) {
await getViewData();
queryEditor.value.editor.session.setValue(localView.value.sql);
lastView.value = props.view;
}
});
watch(() => props.view, async () => {
if (props.isSelected) {
await getViewData();
queryEditor.value.editor.session.setValue(localView.value.sql);
lastView.value = props.view;
}
});
watch(() => props.isSelected, (val) => {
if (val) {
changeBreadcrumbs({ schema: props.schema, view: localView.value.name });
setTimeout(() => {
resizeQueryEditor();
}, 50);
}
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
(async () => {
await getViewData();
queryEditor.value.editor.session.setValue(localView.value.sql);
})();
onMounted(() => {
window.addEventListener('resize', resizeQueryEditor);
ipcRenderer.on('save-content', saveContentListener);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeQueryEditor);
});
onBeforeUnmount(() => {
ipcRenderer.removeListener('save-content', saveContentListener);
});
</script>

View File

@ -305,7 +305,7 @@ const customizations = computed(() => {
});
const isTable = computed(() => {
return !workspace.value.breadcrumbs.view;
return props.elementType === 'table';
});
const fields = computed(() => {
@ -499,8 +499,8 @@ const openTableSettingTab = () => {
uid: workspace.value.uid,
elementName: props.table,
schema: props.schema,
type: isTable.value ? 'table-props' : 'view-props',
elementType: isTable.value ? 'table' : 'view'
type: isTable.value ? 'table-props' : props.elementType === 'view' ? 'view-props' : 'materialized-view-props',
elementType: props.elementType
});
changeBreadcrumbs({