mirror of
https://github.com/Fabio286/antares.git
synced 2025-04-24 06:57:19 +02:00
feat(PostgreSQL): support to materialized views, closes #804
This commit is contained in:
parent
a973ec3c60
commit
0b9898f3e7
@ -43,6 +43,7 @@ export const customizations: Customizations = {
|
|||||||
tableDuplicate: true,
|
tableDuplicate: true,
|
||||||
tableDdl: true,
|
tableDdl: true,
|
||||||
viewAdd: true,
|
viewAdd: true,
|
||||||
|
materializedViewAdd: true,
|
||||||
triggerAdd: true,
|
triggerAdd: true,
|
||||||
triggerFunctionAdd: true,
|
triggerFunctionAdd: true,
|
||||||
routineAdd: true,
|
routineAdd: true,
|
||||||
@ -53,6 +54,7 @@ export const customizations: Customizations = {
|
|||||||
databaseEdit: false,
|
databaseEdit: false,
|
||||||
tableSettings: true,
|
tableSettings: true,
|
||||||
viewSettings: true,
|
viewSettings: true,
|
||||||
|
materializedViewSettings: true,
|
||||||
triggerSettings: true,
|
triggerSettings: true,
|
||||||
triggerFunctionSettings: true,
|
triggerFunctionSettings: true,
|
||||||
routineSettings: true,
|
routineSettings: true,
|
||||||
|
@ -46,6 +46,8 @@ export interface Customizations {
|
|||||||
tableDdl?: boolean;
|
tableDdl?: boolean;
|
||||||
viewAdd?: boolean;
|
viewAdd?: boolean;
|
||||||
viewSettings?: boolean;
|
viewSettings?: boolean;
|
||||||
|
materializedViewAdd?: boolean;
|
||||||
|
materializedViewSettings?: boolean;
|
||||||
triggerAdd?: boolean;
|
triggerAdd?: boolean;
|
||||||
triggerFunctionAdd?: boolean;
|
triggerFunctionAdd?: boolean;
|
||||||
routineAdd?: boolean;
|
routineAdd?: boolean;
|
||||||
|
@ -51,4 +51,52 @@ export default (connections: Record<string, antares.Client>) => {
|
|||||||
return { status: 'error', response: err.toString() };
|
return { status: 'error', response: err.toString() };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-materialized-view-informations', async (event, params) => {
|
||||||
|
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await connections[params.uid].getMaterializedViewInformations(params);
|
||||||
|
return { status: 'success', response: result };
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
return { status: 'error', response: err.toString() };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('drop-materialized-view', async (event, params) => {
|
||||||
|
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connections[params.uid].dropMaterializedView(params);
|
||||||
|
return { status: 'success' };
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
return { status: 'error', response: err.toString() };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('alter-materialized-view', async (event, params) => {
|
||||||
|
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connections[params.uid].alterView(params);
|
||||||
|
return { status: 'success' };
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
return { status: 'error', response: err.toString() };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('create-materialized-view', async (event, params) => {
|
||||||
|
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
await connections[params.uid].createMaterializedView(params);
|
||||||
|
return { status: 'success' };
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
return { status: 'error', response: err.toString() };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -234,6 +234,18 @@ export abstract class BaseClient {
|
|||||||
throw new Error('Method "getVariables" not implemented');
|
throw new Error('Method "getVariables" not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMaterializedViewInformations (...args: any) {
|
||||||
|
throw new Error('Method "getMaterializedViewInformations" not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
dropMaterializedView (...args: any) {
|
||||||
|
throw new Error('Method "dropMaterializedView" not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
createMaterializedView (...args: any) {
|
||||||
|
throw new Error('Method "createMaterializedView" not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
getEventInformations (...args: any) {
|
getEventInformations (...args: any) {
|
||||||
throw new Error('Method "getEventInformations" not implemented');
|
throw new Error('Method "getEventInformations" not implemented');
|
||||||
}
|
}
|
||||||
|
@ -335,6 +335,19 @@ export class PostgreSQLClient extends BaseClient {
|
|||||||
ORDER BY table_name
|
ORDER BY table_name
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
let { rows: matViews } = await this.raw<antares.QueryResult<ShowTableResult>>(`
|
||||||
|
SELECT schemaname AS schema_name,
|
||||||
|
matviewname AS table_name,
|
||||||
|
matviewowner AS owner,
|
||||||
|
ispopulated AS is_populated,
|
||||||
|
definition,
|
||||||
|
'materializedview' AS table_type
|
||||||
|
FROM pg_matviews
|
||||||
|
WHERE schemaname = '${db.database}'
|
||||||
|
ORDER BY schema_name,
|
||||||
|
table_name;
|
||||||
|
`);
|
||||||
|
|
||||||
if (tables.length) {
|
if (tables.length) {
|
||||||
tables = tables.map(table => {
|
tables = tables.map(table => {
|
||||||
table.Db = db.database;
|
table.Db = db.database;
|
||||||
@ -343,6 +356,14 @@ export class PostgreSQLClient extends BaseClient {
|
|||||||
tablesArr.push(...tables);
|
tablesArr.push(...tables);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (matViews.length) {
|
||||||
|
matViews = matViews.map(view => {
|
||||||
|
view.Db = db.database;
|
||||||
|
return view;
|
||||||
|
});
|
||||||
|
tablesArr.push(...matViews);
|
||||||
|
}
|
||||||
|
|
||||||
let { rows: triggers } = await this.raw<antares.QueryResult<ShowTriggersResult>>(`
|
let { rows: triggers } = await this.raw<antares.QueryResult<ShowTriggersResult>>(`
|
||||||
SELECT
|
SELECT
|
||||||
pg_class.relname AS table_name,
|
pg_class.relname AS table_name,
|
||||||
@ -378,7 +399,11 @@ export class PostgreSQLClient extends BaseClient {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
name: table.table_name,
|
name: table.table_name,
|
||||||
type: table.table_type === 'VIEW' ? 'view' : 'table',
|
type: table.table_type === 'VIEW'
|
||||||
|
? 'view'
|
||||||
|
: table.table_type === 'materializedview'
|
||||||
|
? 'materializedview'
|
||||||
|
: 'table',
|
||||||
rows: table.reltuples,
|
rows: table.reltuples,
|
||||||
size: tableSize,
|
size: tableSize,
|
||||||
collation: table.Collation,
|
collation: table.Collation,
|
||||||
@ -1056,11 +1081,32 @@ export class PostgreSQLClient extends BaseClient {
|
|||||||
})[0];
|
})[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMaterializedViewInformations ({ schema, view }: { schema: string; view: string }) {
|
||||||
|
const sql = `SELECT "definition" FROM "pg_matviews" WHERE "matviewname"='${view}' AND "schemaname"='${schema}'`;
|
||||||
|
const results = await this.raw(sql);
|
||||||
|
|
||||||
|
return results.rows.map(row => {
|
||||||
|
return {
|
||||||
|
algorithm: '',
|
||||||
|
definer: '',
|
||||||
|
security: '',
|
||||||
|
updateOption: '',
|
||||||
|
sql: row.definition,
|
||||||
|
name: view
|
||||||
|
};
|
||||||
|
})[0];
|
||||||
|
}
|
||||||
|
|
||||||
async dropView (params: { schema: string; view: string }) {
|
async dropView (params: { schema: string; view: string }) {
|
||||||
const sql = `DROP VIEW "${params.schema}"."${params.view}"`;
|
const sql = `DROP VIEW "${params.schema}"."${params.view}"`;
|
||||||
return await this.raw(sql);
|
return await this.raw(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async dropMaterializedView (params: { schema: string; view: string }) {
|
||||||
|
const sql = `DROP MATERIALIZED VIEW "${params.schema}"."${params.view}"`;
|
||||||
|
return await this.raw(sql);
|
||||||
|
}
|
||||||
|
|
||||||
async alterView ({ view }: { view: antares.AlterViewParams }) {
|
async alterView ({ view }: { view: antares.AlterViewParams }) {
|
||||||
let sql = `CREATE OR REPLACE VIEW "${view.schema}"."${view.oldName}" AS ${view.sql}`;
|
let sql = `CREATE OR REPLACE VIEW "${view.schema}"."${view.oldName}" AS ${view.sql}`;
|
||||||
|
|
||||||
@ -1070,11 +1116,25 @@ export class PostgreSQLClient extends BaseClient {
|
|||||||
return await this.raw(sql);
|
return await this.raw(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async alterMaterializedView ({ view }: { view: antares.AlterViewParams }) {
|
||||||
|
let sql = `CREATE OR REPLACE MATERIALIZED VIEW "${view.schema}"."${view.oldName}" AS ${view.sql}`;
|
||||||
|
|
||||||
|
if (view.name !== view.oldName)
|
||||||
|
sql += `; ALTER VIEW "${view.schema}"."${view.oldName}" RENAME TO "${view.name}"`;
|
||||||
|
|
||||||
|
return await this.raw(sql);
|
||||||
|
}
|
||||||
|
|
||||||
async createView (params: antares.CreateViewParams) {
|
async createView (params: antares.CreateViewParams) {
|
||||||
const sql = `CREATE VIEW "${params.schema}"."${params.name}" AS ${params.sql}`;
|
const sql = `CREATE VIEW "${params.schema}"."${params.name}" AS ${params.sql}`;
|
||||||
return await this.raw(sql);
|
return await this.raw(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createMaterializedView (params: antares.CreateViewParams) {
|
||||||
|
const sql = `CREATE MATERIALIZED VIEW "${params.schema}"."${params.name}" AS ${params.sql}`;
|
||||||
|
return await this.raw(sql);
|
||||||
|
}
|
||||||
|
|
||||||
async getTriggerInformations ({ schema, trigger }: { schema: string; trigger: string }) {
|
async getTriggerInformations ({ schema, trigger }: { schema: string; trigger: string }) {
|
||||||
const [table, triggerName] = trigger.split('.');
|
const [table, triggerName] = trigger.split('.');
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
>
|
>
|
||||||
<BaseIcon
|
<BaseIcon
|
||||||
class="mt-1 mr-1"
|
class="mt-1 mr-1"
|
||||||
:icon-name="element.elementType === 'view' ? 'mdiTableEye' : 'mdiTable'"
|
:icon-name="['view', 'materializedview'].includes(element.elementType) ? 'mdiTableEye' : 'mdiTable'"
|
||||||
:size="18"
|
:size="18"
|
||||||
/>
|
/>
|
||||||
<span :title="`${t('general.data').toUpperCase()}: ${t(`database.${element.elementType}`)}`">
|
<span :title="`${t('general.data').toUpperCase()}: ${t(`database.${element.elementType}`)}`">
|
||||||
@ -80,7 +80,7 @@
|
|||||||
<a v-else-if="element.type === 'data'" class="tab-link">
|
<a v-else-if="element.type === 'data'" class="tab-link">
|
||||||
<BaseIcon
|
<BaseIcon
|
||||||
class="mt-1 mr-1"
|
class="mt-1 mr-1"
|
||||||
:icon-name="element.elementType === 'view' ? 'mdiTableEye' : 'mdiTable'"
|
:icon-name="['view', 'materializedview'].includes(element.elementType) ? 'mdiTableEye' : 'mdiTable'"
|
||||||
:size="18"
|
:size="18"
|
||||||
/>
|
/>
|
||||||
<span :title="`${t('general.data').toUpperCase()}: ${t(`database.${element.elementType}`)}`">
|
<span :title="`${t('general.data').toUpperCase()}: ${t(`database.${element.elementType}`)}`">
|
||||||
@ -157,6 +157,27 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</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
|
<a
|
||||||
v-else-if="element.type === 'new-view'"
|
v-else-if="element.type === 'new-view'"
|
||||||
class="tab-link"
|
class="tab-link"
|
||||||
@ -178,6 +199,27 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</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
|
<a
|
||||||
v-else-if="element.type === 'new-trigger'"
|
v-else-if="element.type === 'new-trigger'"
|
||||||
class="tab-link"
|
class="tab-link"
|
||||||
@ -446,6 +488,14 @@
|
|||||||
:is-selected="selectedTab === tab.uid && isSelected"
|
:is-selected="selectedTab === tab.uid && isSelected"
|
||||||
:schema="tab.schema"
|
: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
|
<WorkspaceTabPropsView
|
||||||
v-else-if="tab.type === 'view-props'"
|
v-else-if="tab.type === 'view-props'"
|
||||||
:tab-uid="tab.uid"
|
:tab-uid="tab.uid"
|
||||||
@ -454,6 +504,14 @@
|
|||||||
:view="tab.elementName"
|
:view="tab.elementName"
|
||||||
:schema="tab.schema"
|
: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
|
<WorkspaceTabNewTrigger
|
||||||
v-else-if="tab.type === 'new-trigger'"
|
v-else-if="tab.type === 'new-trigger'"
|
||||||
:tab-uid="tab.uid"
|
:tab-uid="tab.uid"
|
||||||
@ -596,6 +654,9 @@ import Connection from '@/ipc-api/Connection';
|
|||||||
import { useConsoleStore } from '@/stores/console';
|
import { useConsoleStore } from '@/stores/console';
|
||||||
import { useWorkspacesStore, WorkspaceTab } from '@/stores/workspaces';
|
import { useWorkspacesStore, WorkspaceTab } from '@/stores/workspaces';
|
||||||
|
|
||||||
|
import WorkspaceTabNewMaterializedView from './WorkspaceTabNewMaterializedView.vue';
|
||||||
|
import WorkspaceTabPropsMaterializedView from './WorkspaceTabPropsMaterializedView.vue';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const { cutText } = useFilters();
|
const { cutText } = useFilters();
|
||||||
@ -638,7 +699,6 @@ const draggableTabs = computed<WorkspaceTab[]>({
|
|||||||
get () {
|
get () {
|
||||||
if (workspace.value.customizations.database)
|
if (workspace.value.customizations.database)
|
||||||
return workspace.value.tabs.filter(tab => tab.type === 'query' || tab.database === workspace.value.database);
|
return workspace.value.tabs.filter(tab => tab.type === 'query' || tab.database === workspace.value.database);
|
||||||
|
|
||||||
else
|
else
|
||||||
return workspace.value.tabs;
|
return workspace.value.tabs;
|
||||||
},
|
},
|
||||||
@ -689,6 +749,7 @@ const openAsPermanentTab = (tab: WorkspaceTab) => {
|
|||||||
const permanentTabs = {
|
const permanentTabs = {
|
||||||
table: 'data',
|
table: 'data',
|
||||||
view: 'data',
|
view: 'data',
|
||||||
|
materializedView: 'data',
|
||||||
trigger: 'trigger-props',
|
trigger: 'trigger-props',
|
||||||
triggerFunction: 'trigger-function-props',
|
triggerFunction: 'trigger-function-props',
|
||||||
function: 'function-props',
|
function: 'function-props',
|
||||||
|
@ -111,6 +111,7 @@
|
|||||||
@close-context="closeDatabaseContext"
|
@close-context="closeDatabaseContext"
|
||||||
@open-create-table-tab="openCreateElementTab('table')"
|
@open-create-table-tab="openCreateElementTab('table')"
|
||||||
@open-create-view-tab="openCreateElementTab('view')"
|
@open-create-view-tab="openCreateElementTab('view')"
|
||||||
|
@open-create-materialized-view-tab="openCreateElementTab('materialized-view')"
|
||||||
@open-create-trigger-tab="openCreateElementTab('trigger')"
|
@open-create-trigger-tab="openCreateElementTab('trigger')"
|
||||||
@open-create-routine-tab="openCreateElementTab('routine')"
|
@open-create-routine-tab="openCreateElementTab('routine')"
|
||||||
@open-create-function-tab="openCreateElementTab('function')"
|
@open-create-function-tab="openCreateElementTab('function')"
|
||||||
@ -142,6 +143,7 @@
|
|||||||
:selected-schema="selectedSchema"
|
:selected-schema="selectedSchema"
|
||||||
:context-event="miscContextEvent"
|
:context-event="miscContextEvent"
|
||||||
@open-create-view-tab="openCreateElementTab('view')"
|
@open-create-view-tab="openCreateElementTab('view')"
|
||||||
|
@open-create-materializedview-tab="openCreateElementTab('materialized-view')"
|
||||||
@open-create-trigger-tab="openCreateElementTab('trigger')"
|
@open-create-trigger-tab="openCreateElementTab('trigger')"
|
||||||
@open-create-trigger-function-tab="openCreateElementTab('trigger-function')"
|
@open-create-trigger-function-tab="openCreateElementTab('trigger-function')"
|
||||||
@open-create-routine-tab="openCreateElementTab('routine')"
|
@open-create-routine-tab="openCreateElementTab('routine')"
|
||||||
|
@ -15,6 +15,18 @@
|
|||||||
:size="18"
|
:size="18"
|
||||||
/> {{ t('database.createNewView') }}</span>
|
/> {{ t('database.createNewView') }}</span>
|
||||||
</div>
|
</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
|
<div
|
||||||
v-if="props.selectedMisc === 'trigger'"
|
v-if="props.selectedMisc === 'trigger'"
|
||||||
class="context-element"
|
class="context-element"
|
||||||
@ -94,6 +106,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
'open-create-view-tab',
|
'open-create-view-tab',
|
||||||
|
'open-create-materializedview-tab',
|
||||||
'open-create-trigger-tab',
|
'open-create-trigger-tab',
|
||||||
'open-create-routine-tab',
|
'open-create-routine-tab',
|
||||||
'open-create-function-tab',
|
'open-create-function-tab',
|
||||||
|
@ -116,6 +116,55 @@
|
|||||||
</details>
|
</details>
|
||||||
</div>
|
</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">
|
<div v-if="filteredTriggers.length && customizations.triggers" class="database-misc">
|
||||||
<details class="accordion">
|
<details class="accordion">
|
||||||
<summary
|
<summary
|
||||||
@ -438,7 +487,14 @@ const filteredViews = computed(() => {
|
|||||||
if (props.searchMethod === 'elements')
|
if (props.searchMethod === 'elements')
|
||||||
return props.database.tables.filter(table => table.name.search(searchTerm.value) >= 0 && table.type === 'view');
|
return props.database.tables.filter(table => table.name.search(searchTerm.value) >= 0 && table.type === 'view');
|
||||||
else
|
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(() => {
|
const filteredTriggers = computed(() => {
|
||||||
|
@ -40,6 +40,18 @@
|
|||||||
:size="18"
|
:size="18"
|
||||||
/> {{ t('database.view') }}</span>
|
/> {{ t('database.view') }}</span>
|
||||||
</div>
|
</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
|
<div
|
||||||
v-if="workspace.customizations.triggerAdd"
|
v-if="workspace.customizations.triggerAdd"
|
||||||
class="context-element"
|
class="context-element"
|
||||||
@ -221,6 +233,7 @@ const props = defineProps({
|
|||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
'open-create-table-tab',
|
'open-create-table-tab',
|
||||||
'open-create-view-tab',
|
'open-create-view-tab',
|
||||||
|
'open-create-materialized-view-tab',
|
||||||
'open-create-trigger-tab',
|
'open-create-trigger-tab',
|
||||||
'open-create-routine-tab',
|
'open-create-routine-tab',
|
||||||
'open-create-function-tab',
|
'open-create-function-tab',
|
||||||
@ -257,6 +270,10 @@ const openCreateViewTab = () => {
|
|||||||
emit('open-create-view-tab');
|
emit('open-create-view-tab');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openCreateMaterializedViewTab = () => {
|
||||||
|
emit('open-create-materialized-view-tab');
|
||||||
|
};
|
||||||
|
|
||||||
const openCreateTriggerTab = () => {
|
const openCreateTriggerTab = () => {
|
||||||
emit('open-create-trigger-tab');
|
emit('open-create-trigger-tab');
|
||||||
};
|
};
|
||||||
|
@ -47,6 +47,18 @@
|
|||||||
:size="18"
|
:size="18"
|
||||||
/> {{ t('application.settings') }}</span>
|
/> {{ t('application.settings') }}</span>
|
||||||
</div>
|
</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
|
<div
|
||||||
v-if="selectedTable && selectedTable.type === 'table' && customizations.tableDuplicate"
|
v-if="selectedTable && selectedTable.type === 'table' && customizations.tableDuplicate"
|
||||||
class="context-element"
|
class="context-element"
|
||||||
@ -238,6 +250,23 @@ const openViewSettingTab = () => {
|
|||||||
closeContext();
|
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 = () => {
|
const duplicateTable = () => {
|
||||||
emit('duplicate-table', { schema: props.selectedSchema, table: props.selectedTable });
|
emit('duplicate-table', { schema: props.selectedSchema, table: props.selectedTable });
|
||||||
};
|
};
|
||||||
|
293
src/renderer/components/WorkspaceTabNewMaterializedView.vue
Normal file
293
src/renderer/components/WorkspaceTabNewMaterializedView.vue
Normal 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>
|
316
src/renderer/components/WorkspaceTabPropsMaterializedView.vue
Normal file
316
src/renderer/components/WorkspaceTabPropsMaterializedView.vue
Normal 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>
|
@ -305,7 +305,7 @@ const customizations = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isTable = computed(() => {
|
const isTable = computed(() => {
|
||||||
return !workspace.value.breadcrumbs.view;
|
return props.elementType === 'table';
|
||||||
});
|
});
|
||||||
|
|
||||||
const fields = computed(() => {
|
const fields = computed(() => {
|
||||||
@ -499,8 +499,8 @@ const openTableSettingTab = () => {
|
|||||||
uid: workspace.value.uid,
|
uid: workspace.value.uid,
|
||||||
elementName: props.table,
|
elementName: props.table,
|
||||||
schema: props.schema,
|
schema: props.schema,
|
||||||
type: isTable.value ? 'table-props' : 'view-props',
|
type: isTable.value ? 'table-props' : props.elementType === 'view' ? 'view-props' : 'materialized-view-props',
|
||||||
elementType: isTable.value ? 'table' : 'view'
|
elementType: props.elementType
|
||||||
});
|
});
|
||||||
|
|
||||||
changeBreadcrumbs({
|
changeBreadcrumbs({
|
||||||
|
@ -140,6 +140,7 @@ export const enUS = {
|
|||||||
total: 'Total',
|
total: 'Total',
|
||||||
table: 'Table | Tables',
|
table: 'Table | Tables',
|
||||||
view: 'View | Views',
|
view: 'View | Views',
|
||||||
|
materializedview: 'Materialized view | Materialized views',
|
||||||
definer: 'Definer',
|
definer: 'Definer',
|
||||||
algorithm: 'Algorithm',
|
algorithm: 'Algorithm',
|
||||||
trigger: 'Trigger | Triggers',
|
trigger: 'Trigger | Triggers',
|
||||||
@ -216,6 +217,7 @@ export const enUS = {
|
|||||||
updateOption: 'Update option',
|
updateOption: 'Update option',
|
||||||
deleteView: 'Delete view',
|
deleteView: 'Delete view',
|
||||||
createNewView: 'Create new view',
|
createNewView: 'Create new view',
|
||||||
|
createNewMaterializedView: 'Create new materialized view',
|
||||||
deleteTrigger: 'Delete trigger',
|
deleteTrigger: 'Delete trigger',
|
||||||
createNewTrigger: 'Create new trigger',
|
createNewTrigger: 'Create new trigger',
|
||||||
currentUser: 'Current user',
|
currentUser: 'Current user',
|
||||||
@ -248,6 +250,7 @@ export const enUS = {
|
|||||||
thereAreNoTableFields: 'There are no table fields',
|
thereAreNoTableFields: 'There are no table fields',
|
||||||
newTable: 'New table',
|
newTable: 'New table',
|
||||||
newView: 'New view',
|
newView: 'New view',
|
||||||
|
newMaterializedView: 'New materialized view',
|
||||||
newTrigger: 'New trigger',
|
newTrigger: 'New trigger',
|
||||||
newRoutine: 'New routine',
|
newRoutine: 'New routine',
|
||||||
newFunction: 'New function',
|
newFunction: 'New function',
|
||||||
|
@ -19,4 +19,20 @@ export default class {
|
|||||||
static createView (params: CreateViewParams & { uid: string }): Promise<IpcResponse> {
|
static createView (params: CreateViewParams & { uid: string }): Promise<IpcResponse> {
|
||||||
return ipcRenderer.invoke('create-view', unproxify(params));
|
return ipcRenderer.invoke('create-view', unproxify(params));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static createMaterializedView (params: CreateViewParams & { uid: string }): Promise<IpcResponse> {
|
||||||
|
return ipcRenderer.invoke('create-materialized-view', unproxify(params));
|
||||||
|
}
|
||||||
|
|
||||||
|
static getMaterializedViewInformations (params: { uid: string; schema: string; view: string }): Promise<IpcResponse> {
|
||||||
|
return ipcRenderer.invoke('get-materialized-view-informations', unproxify(params));
|
||||||
|
}
|
||||||
|
|
||||||
|
static dropMaterializedView (params: { uid: string; schema: string; view: string }): Promise<IpcResponse> {
|
||||||
|
return ipcRenderer.invoke('drop-materialized-view', unproxify(params));
|
||||||
|
}
|
||||||
|
|
||||||
|
static alterMaterializedView (params: { view: AlterViewParams & { uid: string }}): Promise<IpcResponse> {
|
||||||
|
return ipcRenderer.invoke('alter-materialized-view', unproxify(params));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -558,6 +558,8 @@ export const useWorkspacesStore = defineStore('workspaces', {
|
|||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'new-table':
|
case 'new-table':
|
||||||
|
case 'new-view':
|
||||||
|
case 'new-materialized-view':
|
||||||
case 'new-trigger':
|
case 'new-trigger':
|
||||||
case 'new-trigger-function':
|
case 'new-trigger-function':
|
||||||
case 'new-function':
|
case 'new-function':
|
||||||
@ -659,6 +661,8 @@ export const useWorkspacesStore = defineStore('workspaces', {
|
|||||||
break;
|
break;
|
||||||
case 'data':
|
case 'data':
|
||||||
case 'table-props':
|
case 'table-props':
|
||||||
|
case 'view-props':
|
||||||
|
case 'materialized-view-props':
|
||||||
case 'trigger-props':
|
case 'trigger-props':
|
||||||
case 'trigger-function-props':
|
case 'trigger-function-props':
|
||||||
case 'function-props':
|
case 'function-props':
|
||||||
|
Loading…
x
Reference in New Issue
Block a user