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

Compare commits

...

19 Commits

Author SHA1 Message Date
5b1cd70e25 chore(release): 0.7.26-beta.0 2024-07-01 19:44:25 +02:00
3e223b475e feat: in-app debug console, closes #824 2024-07-01 18:16:18 +02:00
4a38656b7e fix(PostgreSQL): issue with ssl enabled and connection strings, fixes #786 2024-06-21 08:55:29 +02:00
25b7ae57c6 perf(UI): improved all connections modal, closes #761 2024-06-20 18:12:47 +02:00
4b5718e9b7 ci: update gh actions dependencies 2024-06-20 08:17:09 +02:00
780d83deaa Merge branch 'master' of https://github.com/antares-sql/antares into develop 2024-06-19 14:16:34 +02:00
e952f9f5f8 ci: fix macos build actions 2024-06-19 09:08:25 +02:00
49f1a8ef2e chore(release): 0.7.25 2024-06-19 08:57:50 +02:00
121aa21a6d Merge branch 'beta' of https://github.com/antares-sql/antares 2024-06-19 08:57:07 +02:00
cb25963a67 Merge branch 'master' of https://github.com/antares-sql/antares into develop 2024-06-17 09:26:41 +02:00
3a47607a5f Merge branch 'master' of https://github.com/antares-sql/antares 2024-06-17 09:13:55 +02:00
7494ff6fcf ci: changes on create-artifact-macos.yml 2024-06-17 09:13:31 +02:00
838491bfd4 chore(release): 0.7.25-beta.2 2024-06-16 13:40:02 +02:00
0b9898f3e7 feat(PostgreSQL): support to materialized views, closes #804 2024-06-14 18:05:29 +02:00
a973ec3c60 perf(UI): views grouped in folders 2024-06-13 18:07:05 +02:00
d0c50f17ca ci: temporary disabled auto test e2e 2024-06-10 08:52:00 +02:00
0ec9d3cfc1 Merge pull request #807 from antares-sql/all-contributors/add-mangas
docs: add mangas as a contributor for code
2024-05-26 17:20:52 +02:00
allcontributors[bot]
4f615b26cf docs: update .all-contributorsrc [skip ci] 2024-05-26 15:19:12 +00:00
allcontributors[bot]
86a1e05197 docs: update README.md [skip ci] 2024-05-26 15:19:11 +00:00
48 changed files with 2531 additions and 1226 deletions

View File

@@ -284,6 +284,15 @@
"contributions": [
"translation"
]
},
{
"login": "mangas",
"name": "Filipe Azevedo",
"avatar_url": "https://avatars.githubusercontent.com/u/1640325?v=4",
"profile": "https://fazevedo.dev",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@@ -19,17 +19,19 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: beta
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm i
run: |
npm i
npm install "dmg-license" --save-optional
- name: "Build"
run: npm run build

View File

@@ -25,17 +25,19 @@ jobs:
exit 0
- name: Check out Git repository
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: master
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm i
run: |
npm i
npm install "dmg-license" --save-optional
- name: "Build"
run: npm run build

View File

@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.

View File

@@ -8,12 +8,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: master
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 20
@@ -24,7 +24,7 @@ jobs:
run: npm run build -- --arm64 --linux deb AppImage
- name: Upload Artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: linux-build
retention-days: 3

View File

@@ -8,10 +8,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 20
@@ -22,7 +22,7 @@ jobs:
run: npm run build
- name: Upload Artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: linux-build
retention-days: 3

View File

@@ -8,20 +8,21 @@ jobs:
runs-on: macos-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 20
- name: npm install & build
run: |
npm install
npm install "dmg-license" --save-optional
npm run build
- name: Upload Artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: macos-build
retention-days: 3
@@ -38,17 +39,18 @@ jobs:
ref: beta
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 20
- name: npm install & build
run: |
npm install
npm install "dmg-license" --save-optional
npm run build
- name: Upload Artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: macos-build-beta
retention-days: 3

View File

@@ -11,7 +11,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: '3.8'
python-version: '3.11'
- name: Install pipx
uses: CfirTsabari/actions-pipx@v1
@@ -30,7 +30,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 20
# - name: Delete old package-lock.json
# run: rm package-lock.json

View File

@@ -17,12 +17,12 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
ref: develop
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 20

View File

@@ -1,9 +1,10 @@
name: Test end-to-end
on:
push:
branches:
- develop
workflow_dispatch: {}
# push:
# branches:
# - develop
jobs:
release:
@@ -15,10 +16,10 @@ jobs:
steps:
- name: Check out Git repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 20

View File

@@ -2,6 +2,37 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.7.26-beta.0](https://github.com/antares-sql/antares/compare/v0.7.25...v0.7.26-beta.0) (2024-07-01)
### Features
* in-app debug console, closes [#824](https://github.com/antares-sql/antares/issues/824) ([3e223b4](https://github.com/antares-sql/antares/commit/3e223b475ea57b24a6782feeabecad9c5596e271))
### Bug Fixes
* **PostgreSQL:** issue with ssl enabled and connection strings, fixes [#786](https://github.com/antares-sql/antares/issues/786) ([4a38656](https://github.com/antares-sql/antares/commit/4a38656b7e1094d3a9df28ce263c272f2014adb7))
### Improvements
* **UI:** improved all connections modal, closes [#761](https://github.com/antares-sql/antares/issues/761) ([25b7ae5](https://github.com/antares-sql/antares/commit/25b7ae57c6a4be9e825dddc7a52a49b67e03771b))
### [0.7.25](https://github.com/antares-sql/antares/compare/v0.7.25-beta.2...v0.7.25) (2024-06-19)
### [0.7.25-beta.2](https://github.com/antares-sql/antares/compare/v0.7.25-beta.1...v0.7.25-beta.2) (2024-06-16)
### Features
* **PostgreSQL:** support to materialized views, closes [#804](https://github.com/antares-sql/antares/issues/804) ([0b9898f](https://github.com/antares-sql/antares/commit/0b9898f3e714d2cb24d100f55dd3858a644de162))
### Improvements
* **UI:** views grouped in folders ([a973ec3](https://github.com/antares-sql/antares/commit/a973ec3c60398cb16685a4f991c43ec4ee74c986))
### [0.7.25-beta.1](https://github.com/antares-sql/antares/compare/v0.7.25-beta.0...v0.7.25-beta.1) (2024-06-08)

View File

@@ -151,6 +151,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/bagusindrayana"><img src="https://avatars.githubusercontent.com/u/36830534?v=4?s=100" width="100px;" alt="Bagus Indrayana"/><br /><sub><b>Bagus Indrayana</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=bagusindrayana" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/penguinlab"><img src="https://avatars.githubusercontent.com/u/10959317?v=4?s=100" width="100px;" alt="Naoki Ishikawa"/><br /><sub><b>Naoki Ishikawa</b></sub></a><br /><a href="#translation-penguinlab" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://fazevedo.dev"><img src="https://avatars.githubusercontent.com/u/1640325?v=4?s=100" width="100px;" alt="Filipe Azevedo"/><br /><sub><b>Filipe Azevedo</b></sub></a><br /><a href="https://github.com/antares-sql/antares/commits?author=mangas" title="Code">💻</a></td>
</tr>
</tbody>
</table>

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "antares",
"version": "0.7.25-beta.1",
"version": "0.7.26-beta.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "antares",
"version": "0.7.25-beta.1",
"version": "0.7.26-beta.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.7.25-beta.1",
"version": "0.7.26-beta.0",
"description": "A modern, fast and productivity driven SQL client with a focus in UX.",
"license": "MIT",
"repository": "https://github.com/antares-sql/antares.git",

View File

@@ -31,6 +31,7 @@ export const customizations: Customizations = {
schemas: true,
tables: true,
views: true,
materializedViews: true,
triggers: true,
triggerFunctions: true,
routines: true,
@@ -42,6 +43,7 @@ export const customizations: Customizations = {
tableDuplicate: true,
tableDdl: true,
viewAdd: true,
materializedViewAdd: true,
triggerAdd: true,
triggerFunctionAdd: true,
routineAdd: true,
@@ -52,6 +54,7 @@ export const customizations: Customizations = {
databaseEdit: false,
tableSettings: true,
viewSettings: true,
materializedViewSettings: true,
triggerSettings: true,
triggerFunctionSettings: true,
routineSettings: true,

View File

@@ -28,6 +28,7 @@ export interface Customizations {
schemas?: boolean;
tables?: boolean;
views?: boolean;
materializedViews?: boolean;
triggers?: boolean;
triggerFunctions?: boolean;
routines?: boolean;
@@ -45,6 +46,8 @@ export interface Customizations {
tableDdl?: boolean;
viewAdd?: boolean;
viewSettings?: boolean;
materializedViewAdd?: boolean;
materializedViewSettings?: boolean;
triggerAdd?: boolean;
triggerFunctionAdd?: boolean;
routineAdd?: boolean;

View File

@@ -51,4 +51,52 @@ export default (connections: Record<string, antares.Client>) => {
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() };
}
});
};

View File

@@ -234,6 +234,18 @@ export abstract class BaseClient {
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) {
throw new Error('Method "getEventInformations" not implemented');
}

View File

@@ -335,6 +335,19 @@ export class PostgreSQLClient extends BaseClient {
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) {
tables = tables.map(table => {
table.Db = db.database;
@@ -343,6 +356,14 @@ export class PostgreSQLClient extends BaseClient {
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>>(`
SELECT
pg_class.relname AS table_name,
@@ -378,7 +399,11 @@ export class PostgreSQLClient extends BaseClient {
return {
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,
size: tableSize,
collation: table.Collation,
@@ -1056,11 +1081,32 @@ export class PostgreSQLClient extends BaseClient {
})[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 }) {
const sql = `DROP VIEW "${params.schema}"."${params.view}"`;
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 }) {
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);
}
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) {
const sql = `CREATE VIEW "${params.schema}"."${params.name}" AS ${params.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 }) {
const [table, triggerName] = trigger.split('.');

View File

@@ -10,9 +10,7 @@
:key="connection.uid"
:connection="connection"
/>
<div class="connection-panel-wrapper p-relative">
<WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" />
</div>
<WorkspaceAddConnectionPanel v-if="selectedWorkspace === 'NEW'" />
</div>
<TheFooter />
<TheNotificationsBoard />
@@ -48,6 +46,8 @@ import { useSchemaExportStore } from '@/stores/schemaExport';
import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces';
import { useConsoleStore } from './stores/console';
const { t } = useI18n();
const TheTitleBar = defineAsyncComponent(() => import(/* webpackChunkName: "TheTitleBar" */'@/components/TheTitleBar.vue'));
@@ -80,6 +80,8 @@ const schemaExportStore = useSchemaExportStore();
const { hideExportModal } = schemaExportStore;
const { isExportModal: isExportSchemaModal } = storeToRefs(schemaExportStore);
const consoleStore = useConsoleStore();
const isAllConnectionsModal: Ref<boolean> = ref(false);
document.addEventListener('DOMContentLoaded', () => {
@@ -152,6 +154,60 @@ onMounted(() => {
}
});
});
// Console messages
const oldLog = console.log;
const oldWarn = console.warn;
const oldInfo = console.info;
const oldError = console.error;
console.log = function (...args) {
consoleStore.putLog('debug', {
level: 'log',
process: 'renderer',
message: args.join(' '),
date: new Date()
});
oldLog.apply(this, args);
};
console.info = function (...args) {
consoleStore.putLog('debug', {
level: 'info',
process: 'renderer',
message: args.join(' '),
date: new Date()
});
oldInfo.apply(this, args);
};
console.warn = function (...args) {
consoleStore.putLog('debug', {
level: 'warn',
process: 'renderer',
message: args.join(' '),
date: new Date()
});
oldWarn.apply(this, args);
};
console.error = function (...args) {
consoleStore.putLog('debug', {
level: 'error',
process: 'renderer',
message: args.join(' '),
date: new Date()
});
oldError.apply(this, args);
};
window.addEventListener('unhandledrejection', (event) => {
console.error(event.reason);
});
window.addEventListener('error', (event) => {
console.error(event.error, '| File name:', event.filename.split('/').pop().split('?')[0]);
});
</script>
<style lang="scss">

View File

@@ -365,7 +365,11 @@ export default defineComponent({
};
const handleWheelEvent = (e) => {
if (!e.target.className.includes('select__')) deactivate();
try {
if (!e.target.className.includes('select__')) deactivate();
}
catch (_) {
}
};
onMounted(() => {

View File

@@ -0,0 +1,285 @@
<template>
<div
ref="wrapper"
class="console-wrapper"
@mouseenter="isHover = true"
@mouseleave="isHover = false"
>
<div ref="resizer" class="console-resizer" />
<div
id="console"
ref="queryConsole"
class="console column col-12"
:style="{height: localHeight ? localHeight+'px' : ''}"
>
<div class="console-header">
<ul class="tab tab-block">
<li class="tab-item" :class="{'active': selectedTab === 'query'}">
<a class="tab-link" @click="selectedTab = 'query'">{{ t('application.executedQueries') }}</a>
</li>
<li class="tab-item" :class="{'active': selectedTab === 'debug'}">
<a class="tab-link" @click="selectedTab = 'debug'">{{ t('application.debugConsole') }}</a>
</li>
</ul>
<button class="btn btn-clear mr-1" @click="resizeConsole(0)" />
</div>
<div
v-show="selectedTab === 'query'"
ref="queryConsoleBody"
class="console-body"
>
<div
v-for="(wLog, i) in workspaceQueryLogs"
:key="i"
class="console-log"
tabindex="0"
@contextmenu.prevent="contextMenu($event, wLog)"
>
<span class="console-log-datetime">{{ moment(wLog.date).format('HH:mm:ss') }}</span>: <code class="console-log-sql" v-html="highlight(wLog.sql, {html: true})" />
</div>
</div>
<div
v-show="selectedTab === 'debug'"
ref="logConsoleBody"
class="console-body"
>
<div
v-for="(log, i) in debugLogs"
:key="i"
class="console-log"
tabindex="0"
@contextmenu.prevent="contextMenu($event, log)"
>
<span class="console-log-datetime">{{ moment(log.date).format('HH:mm:ss') }}</span> <small>[{{ log.process.substring(0, 1).toUpperCase() }}]</small>: <span class="console-log-message" :class="`console-log-level-${log.level}`">{{ log.message }}</span>
</div>
</div>
</div>
</div>
<BaseContextMenu
v-if="isContext"
:context-event="contextEvent"
@close-context="isContext = false"
>
<div class="context-element" @click="copyLog">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiContentCopy"
:size="18"
/> {{ t('general.copy') }}</span>
</div>
</BaseContextMenu>
</template>
<script setup lang="ts">
import * as moment from 'moment';
import { storeToRefs } from 'pinia';
import { highlight } from 'sql-highlight';
import { computed, nextTick, onMounted, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import BaseContextMenu from '@/components/BaseContextMenu.vue';
import BaseIcon from '@/components/BaseIcon.vue';
import { copyText } from '@/libs/copyText';
import { useConsoleStore } from '@/stores/console';
const { t } = useI18n();
const consoleStore = useConsoleStore();
const { resizeConsole, getLogsByWorkspace } = consoleStore;
const {
isConsoleOpen,
consoleHeight,
selectedTab,
debugLogs
} = storeToRefs(consoleStore);
const props = defineProps({
uid: {
type: String,
default: null,
required: false
}
});
const wrapper: Ref<HTMLInputElement> = ref(null);
const queryConsole: Ref<HTMLInputElement> = ref(null);
const queryConsoleBody: Ref<HTMLInputElement> = ref(null);
const logConsoleBody: Ref<HTMLInputElement> = ref(null);
const resizer: Ref<HTMLInputElement> = ref(null);
const localHeight = ref(consoleHeight.value);
const isHover = ref(false);
const isContext = ref(false);
const contextContent: Ref<string> = ref(null);
const contextEvent: Ref<MouseEvent> = ref(null);
const resize = (e: MouseEvent) => {
const el = queryConsole.value;
let elementHeight = el.getBoundingClientRect().bottom - e.pageY;
if (elementHeight > 400) elementHeight = 400;
localHeight.value = elementHeight;
};
const workspaceQueryLogs = computed(() => {
return getLogsByWorkspace(props.uid);
});
const stopResize = () => {
if (localHeight.value < 0) localHeight.value = 0;
resizeConsole(localHeight.value);
window.removeEventListener('mousemove', resize);
window.removeEventListener('mouseup', stopResize);
};
const contextMenu = (event: MouseEvent, wLog: {date: Date; sql?: string; message?: string}) => {
contextEvent.value = event;
contextContent.value = wLog.sql || wLog.message;
isContext.value = true;
};
const copyLog = () => {
copyText(contextContent.value);
isContext.value = false;
};
watch(workspaceQueryLogs, async () => {
if (!isHover.value) {
await nextTick();
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
}
});
watch(() => debugLogs.value.length, async () => {
if (!isHover.value) {
await nextTick();
logConsoleBody.value.scrollTop = logConsoleBody.value.scrollHeight;
}
});
watch(isConsoleOpen, async () => {
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
logConsoleBody.value.scrollTop = logConsoleBody.value.scrollHeight;
});
watch(selectedTab, async () => {
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
logConsoleBody.value.scrollTop = logConsoleBody.value.scrollHeight;
});
watch(consoleHeight, async (val) => {
await nextTick();
localHeight.value = val;
});
onMounted(() => {
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
logConsoleBody.value.scrollTop = logConsoleBody.value.scrollHeight;
resizer.value.addEventListener('mousedown', (e: MouseEvent) => {
e.preventDefault();
window.addEventListener('mousemove', resize);
window.addEventListener('mouseup', stopResize);
});
});
</script>
<style lang="scss" scoped>
.console-wrapper {
width: -webkit-fill-available;
z-index: 9;
margin-top: auto;
position: absolute;
bottom: 0;
.console-resizer {
height: 4px;
top: -1px;
width: 100%;
cursor: ns-resize;
position: absolute;
z-index: 99;
transition: background 0.2s;
&:hover {
background: var(--primary-color-dark);
}
}
.console {
padding: 0;
padding-bottom: $footer-height;
.console-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 4px;
.tab-block {
margin-top: 0;
margin-bottom: 0;
}
.tab-block,
.tab-item {
background-color: transparent;
}
.tab-link {
padding: 0.2rem 0.6rem;
cursor: pointer;
white-space: nowrap;
}
}
.console-body {
overflow: auto;
display: flex;
flex-direction: column;
max-height: 100%;
padding: 0 6px 3px;
.console-log {
padding: 1px 3px;
margin: 1px 0;
border-radius: $border-radius;
user-select: text;
&-datetime {
opacity: .6;
font-size: 90%;
}
&-sql {
font-size: 95%;
opacity: 0.8;
font-weight: 700;
&:hover {
user-select: text;
}
}
&-message {
font-size: 95%;
}
&-level {
// &-log,
// &-info {}
&-warn {
color: orange;
}
&-error {
color: red;
}
}
small {
opacity: .6;
}
}
}
}
}
</style>

View File

@@ -57,8 +57,21 @@
>
<div class="panel">
<div class="panel-header p-2 text-center p-relative">
<figure class="avatar avatar-lg pt-1 mb-1">
<i class="settingbar-element-icon dbi" :class="[`dbi-${connection.client}`]" />
<figure class="avatar avatar-lg pt-1 mb-1 bg-dark">
<div
v-if="connection.icon"
class="settingbar-connection-icon"
>
<BaseIcon
:icon-name="camelize(connection.icon)"
:size="42"
/>
</div>
<div
v-else
class="settingbar-element-icon dbi ml-1"
:class="[`dbi-${connection.client}`]"
/>
</figure>
<div class="panel-title h6 text-ellipsis">
{{ getConnectionName(connection.uid) }}
@@ -136,7 +149,19 @@
</div>
</div>
<div class="panel-footer text-center py-0">
<div v-if="connection.ssl" class="chip bg-success mt-2">
<div
v-if="connection.folderName"
class="chip mt-2 bg-dark"
>
<BaseIcon
icon-name="mdiFolder"
class="mr-1"
:style="[connection.color ? `color: ${connection.color};`: '']"
:size="18"
/>
{{ connection.folderName }}
</div>
<div v-if="connection.ssl" class="chip bg-dark mt-2">
<BaseIcon
icon-name="mdiShieldKey"
class="mr-1"
@@ -144,7 +169,7 @@
/>
SSL
</div>
<div v-if="connection.ssh" class="chip bg-success mt-2">
<div v-if="connection.ssh" class="chip bg-dark mt-2">
<BaseIcon
icon-name="mdiConsoleNetwork"
class="mr-1"
@@ -152,7 +177,7 @@
/>
SSH
</div>
<div v-if="connection.readonly" class="chip bg-success mt-2">
<div v-if="connection.readonly" class="chip bg-dark mt-2">
<BaseIcon
icon-name="mdiLock"
class="mr-1"
@@ -209,6 +234,7 @@ import { useI18n } from 'vue-i18n';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseIcon from '@/components/BaseIcon.vue';
import { useFocusTrap } from '@/composables/useFocusTrap';
import { camelize } from '@/libs/camelize';
import { useConnectionsStore } from '@/stores/connections';
import { useWorkspacesStore } from '@/stores/workspaces';
@@ -218,7 +244,9 @@ const connectionsStore = useConnectionsStore();
const workspacesStore = useWorkspacesStore();
const { connections,
lastConnections
connectionsOrder,
lastConnections,
getFolders: folders
} = storeToRefs(connectionsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
@@ -236,7 +264,8 @@ const clients = new Map([
['mysql', 'MySQL'],
['maria', 'MariaDB'],
['pg', 'PostgreSQL'],
['sqlite', 'SQLite']
['sqlite', 'SQLite'],
['firebird', 'Firebird SQL']
]);
const searchTerm = ref('');
@@ -244,12 +273,17 @@ const isConfirmModal = ref(false);
const connectionHover: Ref<string> = ref(null);
const selectedConnection: Ref<ConnectionParams> = ref(null);
const sortedConnections = computed(() => {
const remappedConnections = computed(() => {
return connections.value
.map(c => {
const connTime = lastConnections.value.find((lc) => lc.uid === c.uid)?.time || 0;
const connIcon = connectionsOrder.value.find((co) => co.uid === c.uid).icon;
const folder = folders.value.find(f => f.connections.includes(c.uid));
return {
...c,
icon: connIcon,
color: folder?.color,
folderName: folder?.name,
time: connTime
};
})
@@ -261,7 +295,7 @@ const sortedConnections = computed(() => {
});
const filteredConnections = computed(() => {
return sortedConnections.value.filter(connection => {
return remappedConnections.value.filter(connection => {
return connection.name?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) ||
connection.host?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) ||
connection.database?.toLocaleLowerCase().includes(searchTerm.value.toLocaleLowerCase()) ||

View File

@@ -282,7 +282,7 @@
import { ClientCode, SchemaInfos } from 'common/interfaces/antares';
import { Customizations } from 'common/interfaces/customizations';
import { ExportOptions, ExportState } from 'common/interfaces/exporter';
import { ipcRenderer } from 'electron';
import { ipcRenderer, IpcRendererEvent } from 'electron';
import * as moment from 'moment';
import { storeToRefs } from 'pinia';
import { computed, onBeforeUnmount, Ref, ref } from 'vue';
@@ -293,6 +293,7 @@ import BaseSelect from '@/components/BaseSelect.vue';
import { useFocusTrap } from '@/composables/useFocusTrap';
import Application from '@/ipc-api/Application';
import Schema from '@/ipc-api/Schema';
import { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications';
import { useSchemaExportStore } from '@/stores/schemaExport';
import { useWorkspacesStore } from '@/stores/workspaces';
@@ -384,16 +385,28 @@ const startExport = async () => {
else {
progressStatus.value = response;
addNotification({ status: 'error', message: response });
useConsoleStore().putLog('debug', {
level: 'error',
process: 'worker',
message: response,
date: new Date()
});
}
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
useConsoleStore().putLog('debug', {
level: 'error',
process: 'worker',
message: err.stack,
date: new Date()
});
}
isExporting.value = false;
};
const updateProgress = (event: Event, state: ExportState) => {
const updateProgress = (event: IpcRendererEvent, state: ExportState) => {
progressPercentage.value = Number((state.currentItemIndex / state.totalItems * 100).toFixed(1));
switch (state.op) {
case 'PROCESSING':

View File

@@ -55,7 +55,7 @@
<script setup lang="ts">
import { ImportState } from 'common/interfaces/importer';
import { ipcRenderer } from 'electron';
import { ipcRenderer, IpcRendererEvent } from 'electron';
import * as moment from 'moment';
import { storeToRefs } from 'pinia';
import { computed, onBeforeUnmount, Ref, ref } from 'vue';
@@ -63,6 +63,7 @@ import { useI18n } from 'vue-i18n';
import BaseIcon from '@/components/BaseIcon.vue';
import Schema from '@/ipc-api/Schema';
import { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
@@ -118,23 +119,35 @@ const startImport = async (file: string) => {
else {
progressStatus.value = response;
addNotification({ status: 'error', message: response });
useConsoleStore().putLog('debug', {
level: 'error',
process: 'worker',
message: response,
date: new Date()
});
}
refreshSchema({ uid, schema: props.selectedSchema });
completed.value = true;
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
useConsoleStore().putLog('debug', {
level: 'error',
process: 'worker',
message: err.stack,
date: new Date()
});
}
isImporting.value = false;
};
const updateProgress = (event: Event, state: ImportState) => {
const updateProgress = (event: IpcRendererEvent, state: ImportState) => {
progressPercentage.value = parseFloat(Number(state.percentage).toFixed(1));
queryCount.value = Number(state.queryCount);
};
const handleQueryError = (event: Event, err: { time: string; message: string }) => {
const handleQueryError = (event: IpcRendererEvent, err: { time: string; message: string }) => {
queryErrors.value.push(err);
};

View File

@@ -93,6 +93,7 @@ import * as Draggable from 'vuedraggable';
import BaseIcon from '@/components/BaseIcon.vue';
import SettingBarConnectionsFolder from '@/components/SettingBarConnectionsFolder.vue';
import { camelize } from '@/libs/camelize';
import { SidebarElement, useConnectionsStore } from '@/stores/connections';
import { useWorkspacesStore } from '@/stores/workspaces';
@@ -165,16 +166,6 @@ const getStatusBadge = (uid: string) => {
}
};
const camelize = (text: string) => {
const textArr = text.split('-');
for (let i = 0; i < textArr.length; i++) {
if (i === 0) continue;
textArr[i] = textArr[i].charAt(0).toUpperCase() + textArr[i].slice(1);
}
return textArr.join('');
};
watch(() => dummyNested.value.length, () => {
dummyNested.value = [];
});

View File

@@ -43,11 +43,7 @@
<div class="footer-right-elements">
<ul class="footer-elements">
<li
v-if="workspace?.connectionStatus === 'connected'"
class="footer-element footer-link"
@click="toggleConsole()"
>
<li class="footer-element footer-link" @click="toggleConsole()">
<BaseIcon
icon-name="mdiConsoleLine"
class="mr-1"

File diff suppressed because it is too large Load Diff

View File

@@ -1,440 +1,446 @@
<template>
<div class="connection-panel">
<div class="panel">
<div class="panel-nav">
<ul class="tab tab-block">
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'general'}"
@click="selectTab('general')"
>
<a class="tab-link">{{ t('application.general') }}</a>
</li>
<li
v-if="clientCustomizations.sslConnection"
class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')"
>
<a class="tab-link">{{ t('connection.ssl') }}</a>
</li>
<li
v-if="clientCustomizations.sshConnection"
class="tab-item c-hand"
:class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')"
>
<a class="tab-link">{{ t('connection.sshTunnel') }}</a>
</li>
</ul>
</div>
<div v-if="selectedTab === 'general'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<fieldset class="m-0" :disabled="isBusy">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.connectionName') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
ref="firstInput"
v-model="connection.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.client') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseSelect
v-model="connection.client"
:options="clients"
option-track-by="slug"
option-label="name"
class="form-select"
/>
</div>
</div>
<div v-if="connection.client === 'pg'" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.connectionString') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
ref="pgString"
v-model="connection.pgConnString"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.hostName') }}/IP</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.host"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('database.database') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.databasePath"
:message="t('general.browse')"
@clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')"
/>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.port') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.port"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div v-if="clientCustomizations.database" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('database.database') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.database"
class="form-input"
type="text"
:placeholder="clientCustomizations.defaultDatabase"
>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.user') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.user"
class="form-input"
type="text"
:disabled="connection.ask"
>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.password') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.password"
class="form-input"
type="password"
:disabled="connection.ask"
>
</div>
</div>
<div v-if="clientCustomizations.connectionSchema" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('database.schema') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.schema"
class="form-input"
type="text"
:placeholder="t('general.all')"
>
</div>
</div>
<div v-if="clientCustomizations.readOnlyMode" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline my-0">
<input v-model="connection.readonly" type="checkbox"><i class="form-icon" /> {{ t('connection.readOnlyMode') }}
</label>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline my-0">
<input v-model="connection.ask" type="checkbox"><i class="form-icon" /> {{ t('connection.askCredentials') }}
</label>
</div>
</div>
<div v-if="clientCustomizations.singleConnectionMode" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline my-0">
<input v-model="connection.singleConnectionMode" type="checkbox"><i class="form-icon" /> {{ t('connection.singleConnection') }}
</label>
</div>
</div>
</fieldset>
</form>
<div class="connection-panel-wrapper p-relative">
<div class="connection-panel">
<div class="panel">
<div class="panel-nav">
<ul class="tab tab-block">
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'general'}"
@click="selectTab('general')"
>
<a class="tab-link">{{ t('application.general') }}</a>
</li>
<li
v-if="clientCustomizations.sslConnection"
class="tab-item c-hand"
:class="{'active': selectedTab === 'ssl'}"
@click="selectTab('ssl')"
>
<a class="tab-link">{{ t('connection.ssl') }}</a>
</li>
<li
v-if="clientCustomizations.sshConnection"
class="tab-item c-hand"
:class="{'active': selectedTab === 'ssh'}"
@click="selectTab('ssh')"
>
<a class="tab-link">{{ t('connection.sshTunnel') }}</a>
</li>
</ul>
</div>
</div>
<div v-if="selectedTab === 'ssl'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">
{{ t('connection.enableSsl') }}
</label>
</div>
<div class="column col-7 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsl">
<input type="checkbox" :checked="connection.ssl">
<i class="form-icon" />
</label>
</div>
</div>
<fieldset class="m-0" :disabled="isBusy || !connection.ssl">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.privateKey') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.key"
:message="t('general.browse')"
@clear="pathClear('key')"
@change="pathSelection($event, 'key')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.certificate') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.cert"
:message="t('general.browse')"
@clear="pathClear('cert')"
@change="pathSelection($event, 'cert')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.caCertificate') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.ca"
:message="t('general.browse')"
@clear="pathClear('ca')"
@change="pathSelection($event, 'ca')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.ciphers') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
ref="firstInput"
v-model="connection.ciphers"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="connection.untrustedConnection" type="checkbox"><i class="form-icon" /> {{ t('connection.untrustedConnection') }}
</label>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div v-if="selectedTab === 'ssh'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">
{{ t('connection.enableSsh') }}
</label>
</div>
<div class="column col-7 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsh">
<input type="checkbox" :checked="connection.ssh">
<i class="form-icon" />
</label>
</div>
</div>
<fieldset class="m-0" :disabled="isBusy || !connection.ssh">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.hostName') }}/IP</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshHost"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.user') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshUser"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.password') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshPass"
class="form-input"
type="password"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.port') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshPort"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.privateKey') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.sshKey"
:message="t('general.browse')"
@clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.passphrase') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshPassphrase"
class="form-input"
type="password"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.keepAliveInterval') }}</label>
</div>
<div class="column col-7 col-sm-12">
<div class="input-group">
<div v-if="selectedTab === 'general'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<fieldset class="m-0" :disabled="isBusy">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.connectionName') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshKeepAliveInterval"
ref="firstInput"
v-model="connection.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.client') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseSelect
v-model="connection.client"
:options="clients"
option-track-by="slug"
option-label="name"
class="form-select"
/>
</div>
</div>
<div v-if="connection.client === 'pg'" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.connectionString') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
ref="pgString"
v-model="connection.pgConnString"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.hostName') }}/IP</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.host"
class="form-input"
type="text"
>
</div>
</div>
<div v-if="clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('database.database') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.databasePath"
:message="t('general.browse')"
@clear="pathClear('databasePath')"
@change="pathSelection($event, 'databasePath')"
/>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.port') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.port"
class="form-input"
type="number"
min="1"
max="65535"
>
<span class="input-group-addon">{{ t('general.seconds') }}</span>
</div>
</div>
<div v-if="clientCustomizations.database" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('database.database') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.database"
class="form-input"
type="text"
:placeholder="clientCustomizations.defaultDatabase"
>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.user') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.user"
class="form-input"
type="text"
:disabled="connection.ask"
>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.password') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.password"
class="form-input"
type="password"
:disabled="connection.ask"
>
</div>
</div>
<div v-if="clientCustomizations.connectionSchema" class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('database.schema') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.schema"
class="form-input"
type="text"
:placeholder="t('general.all')"
>
</div>
</div>
<div v-if="clientCustomizations.readOnlyMode" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline my-0">
<input v-model="connection.readonly" type="checkbox"><i class="form-icon" /> {{ t('connection.readOnlyMode') }}
</label>
</div>
</div>
<div v-if="!clientCustomizations.fileConnection" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline my-0">
<input v-model="connection.ask" type="checkbox"><i class="form-icon" /> {{ t('connection.askCredentials') }}
</label>
</div>
</div>
<div v-if="clientCustomizations.singleConnectionMode" class="form-group columns mb-0">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline my-0">
<input v-model="connection.singleConnectionMode" type="checkbox"><i class="form-icon" /> {{ t('connection.singleConnection') }}
</label>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div v-if="selectedTab === 'ssl'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">
{{ t('connection.enableSsl') }}
</label>
</div>
<div class="column col-7 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsl">
<input type="checkbox" :checked="connection.ssl">
<i class="form-icon" />
</label>
</div>
</div>
</fieldset>
</form>
<fieldset class="m-0" :disabled="isBusy || !connection.ssl">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.privateKey') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.key"
:message="t('general.browse')"
@clear="pathClear('key')"
@change="pathSelection($event, 'key')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.certificate') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.cert"
:message="t('general.browse')"
@clear="pathClear('cert')"
@change="pathSelection($event, 'cert')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.caCertificate') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.ca"
:message="t('general.browse')"
@clear="pathClear('ca')"
@change="pathSelection($event, 'ca')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.ciphers') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
ref="firstInput"
v-model="connection.ciphers"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12" />
<div class="column col-7 col-sm-12">
<label class="form-checkbox form-inline">
<input v-model="connection.untrustedConnection" type="checkbox"><i class="form-icon" /> {{ t('connection.untrustedConnection') }}
</label>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div v-if="selectedTab === 'ssh'" class="panel-body py-0">
<div>
<form class="form-horizontal">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">
{{ t('connection.enableSsh') }}
</label>
</div>
<div class="column col-7 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleSsh">
<input type="checkbox" :checked="connection.ssh">
<i class="form-icon" />
</label>
</div>
</div>
<fieldset class="m-0" :disabled="isBusy || !connection.ssh">
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.hostName') }}/IP</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshHost"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.user') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshUser"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.password') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshPass"
class="form-input"
type="password"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.port') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshPort"
class="form-input"
type="number"
min="1"
max="65535"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.privateKey') }}</label>
</div>
<div class="column col-7 col-sm-12">
<BaseUploadInput
:model-value="connection.sshKey"
:message="t('general.browse')"
@clear="pathClear('sshKey')"
@change="pathSelection($event, 'sshKey')"
/>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.passphrase') }}</label>
</div>
<div class="column col-7 col-sm-12">
<input
v-model="connection.sshPassphrase"
class="form-input"
type="password"
>
</div>
</div>
<div class="form-group columns">
<div class="column col-5 col-sm-12">
<label class="form-label cut-text">{{ t('connection.keepAliveInterval') }}</label>
</div>
<div class="column col-7 col-sm-12">
<div class="input-group">
<input
v-model="connection.sshKeepAliveInterval"
class="form-input"
type="number"
min="1"
>
<span class="input-group-addon">{{ t('general.seconds') }}</span>
</div>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div class="panel-footer">
<button
id="connection-test"
class="btn btn-gray mr-2 d-flex"
:class="{'loading': isTesting}"
:disabled="isBusy"
@click="startTest"
>
<BaseIcon
icon-name="mdiLightningBolt"
:size="24"
class="mr-1"
/>
{{ t('connection.testConnection') }}
</button>
<button
id="connection-save"
class="btn btn-primary mr-2 d-flex"
:disabled="isBusy"
@click="saveConnection"
>
<BaseIcon
icon-name="mdiContentSave"
:size="24"
class="mr-1"
/>
{{ t('general.save') }}
</button>
</div>
</div>
<div class="panel-footer">
<button
id="connection-test"
class="btn btn-gray mr-2 d-flex"
:class="{'loading': isTesting}"
:disabled="isBusy"
@click="startTest"
>
<BaseIcon
icon-name="mdiLightningBolt"
:size="24"
class="mr-1"
/>
{{ t('connection.testConnection') }}
</button>
<button
id="connection-save"
class="btn btn-primary mr-2 d-flex"
:disabled="isBusy"
@click="saveConnection"
>
<BaseIcon
icon-name="mdiContentSave"
:size="24"
class="mr-1"
/>
{{ t('general.save') }}
</button>
</div>
<ModalAskCredentials
v-if="isAsking"
@close-asking="closeAsking"
@credentials="continueTest"
/>
</div>
<ModalAskCredentials
v-if="isAsking"
@close-asking="closeAsking"
@credentials="continueTest"
/>
</div>
<DebugConsole v-if="isConsoleOpen" />
</template>
<script setup lang="ts">
import customizations from 'common/customizations';
import { ConnectionParams } from 'common/interfaces/antares';
import { uidGen } from 'common/libs/uidGen';
import { storeToRefs } from 'pinia';
import { computed, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import BaseIcon from '@/components/BaseIcon.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import BaseUploadInput from '@/components/BaseUploadInput.vue';
import DebugConsole from '@/components/DebugConsole.vue';
import ModalAskCredentials from '@/components/ModalAskCredentials.vue';
import Connection from '@/ipc-api/Connection';
import { useConnectionsStore } from '@/stores/connections';
import { useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
@@ -443,6 +449,7 @@ const { t } = useI18n();
const { addConnection } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { isConsoleOpen } = storeToRefs(useConsoleStore());
const { connectWorkspace, selectWorkspace } = workspacesStore;

View File

@@ -598,6 +598,22 @@ localConnection.value = JSON.parse(JSON.stringify(props.connection));
min-width: 450px;
border-radius: $border-radius;
.panel-nav {
.tab-block {
background: transparent;
margin: 0.2rem 0 0.15rem 0;
.tab-item {
background: transparent;
flex: 1 0 0;
> a {
padding: 8px 4px 6px 4px
}
}
}
}
.panel-body {
flex: initial;
}

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')"
@@ -141,10 +142,12 @@
:selected-misc="selectedMisc"
: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')"
@open-create-function-tab="openCreateElementTab('function')"
@open-create-trigger-function-tab="openCreateElementTab('trigger-function')"
@open-create-scheduler-tab="openCreateElementTab('scheduler')"
@close-context="closeMiscFolderContext"
@reload="refresh"

View File

@@ -3,6 +3,30 @@
:context-event="props.contextEvent"
@close-context="closeContext"
>
<div
v-if="props.selectedMisc === 'view'"
class="context-element"
@click="emit('open-create-view-tab')"
>
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiTableCog"
: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"
@@ -81,6 +105,8 @@ 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

@@ -67,6 +67,104 @@
</ul>
</div>
<div v-if="filteredViews.length" 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, 'view')"
>
<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.view', 2) }}
</summary>
<div class="accordion-body">
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="view of filteredViews"
: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="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
@@ -380,11 +478,25 @@ const searchTerm = computed(() => {
const filteredTables = computed(() => {
if (props.searchMethod === 'elements')
return props.database.tables.filter(table => table.name.search(searchTerm.value) >= 0);
return props.database.tables.filter(table => table.name.search(searchTerm.value) >= 0 && table.type === 'table');
else
return props.database.tables;
});
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.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(() => {
if (props.searchMethod === 'elements')
return props.database.triggers.filter(trigger => trigger.name.search(searchTerm.value) >= 0);

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

@@ -1,189 +0,0 @@
<template>
<div
ref="wrapper"
class="query-console-wrapper"
@mouseenter="isHover = true"
@mouseleave="isHover = false"
>
<div ref="resizer" class="query-console-resizer" />
<div
id="query-console"
ref="queryConsole"
class="query-console column col-12"
:style="{height: localHeight ? localHeight+'px' : ''}"
>
<div class="query-console-header">
<div>{{ t('application.console') }}</div>
<button class="btn btn-clear mr-1" @click="resizeConsole(0)" />
</div>
<div ref="queryConsoleBody" class="query-console-body">
<div
v-for="(wLog, i) in workspaceLogs"
:key="i"
class="query-console-log"
tabindex="0"
@contextmenu.prevent="contextMenu($event, wLog)"
>
<span class="type-datetime">{{ moment(wLog.date).format('HH:mm:ss') }}</span>: <code class="query-console-log-sql" v-html="highlight(wLog.sql, {html: true})" />
</div>
</div>
</div>
</div>
<BaseContextMenu
v-if="isContext"
:context-event="contextEvent"
@close-context="isContext = false"
>
<div class="context-element" @click="copyQuery">
<span class="d-flex">
<BaseIcon
class="text-light mt-1 mr-1"
icon-name="mdiContentCopy"
:size="18"
/> {{ t('general.copy') }}</span>
</div>
</BaseContextMenu>
</template>
<script setup lang="ts">
import * as moment from 'moment';
import { storeToRefs } from 'pinia';
import { highlight } from 'sql-highlight';
import { computed, nextTick, onMounted, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import BaseContextMenu from '@/components/BaseContextMenu.vue';
import BaseIcon from '@/components/BaseIcon.vue';
import { copyText } from '@/libs/copyText';
import { useConsoleStore } from '@/stores/console';
const { t } = useI18n();
const consoleStore = useConsoleStore();
const { resizeConsole, getLogsByWorkspace } = consoleStore;
const { consoleHeight } = storeToRefs(consoleStore);
const props = defineProps({
uid: String
});
const wrapper: Ref<HTMLInputElement> = ref(null);
const queryConsole: Ref<HTMLInputElement> = ref(null);
const queryConsoleBody: Ref<HTMLInputElement> = ref(null);
const resizer: Ref<HTMLInputElement> = ref(null);
const localHeight = ref(250);
const isHover = ref(false);
const isContext = ref(false);
const contextQuery: Ref<string> = ref(null);
const contextEvent: Ref<MouseEvent> = ref(null);
const resize = (e: MouseEvent) => {
const el = queryConsole.value;
let consoleHeight = el.getBoundingClientRect().bottom - e.pageY;
if (consoleHeight > 400) consoleHeight = 400;
localHeight.value = consoleHeight;
};
const workspaceLogs = computed(() => {
return getLogsByWorkspace(props.uid);
});
const stopResize = () => {
if (localHeight.value < 0) localHeight.value = 0;
resizeConsole(localHeight.value);
window.removeEventListener('mousemove', resize);
window.removeEventListener('mouseup', stopResize);
};
const contextMenu = (event: MouseEvent, wLog: {date: Date; sql: string}) => {
contextEvent.value = event;
contextQuery.value = wLog.sql;
isContext.value = true;
};
const copyQuery = () => {
copyText(contextQuery.value);
isContext.value = false;
};
watch(workspaceLogs, async () => {
if (!isHover.value) {
await nextTick();
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
}
});
onMounted(() => {
localHeight.value = consoleHeight.value;
queryConsoleBody.value.scrollTop = queryConsoleBody.value.scrollHeight;
});
onMounted(() => {
resizer.value.addEventListener('mousedown', (e: MouseEvent) => {
e.preventDefault();
window.addEventListener('mousemove', resize);
window.addEventListener('mouseup', stopResize);
});
});
</script>
<style lang="scss" scoped>
.query-console-wrapper {
width: 100%;
z-index: 9;
margin-top: auto;
position: absolute;
bottom: 0;
.query-console-resizer {
height: 4px;
top: -1px;
width: 100%;
cursor: ns-resize;
position: absolute;
z-index: 99;
transition: background 0.2s;
&:hover {
background: var(--primary-color-dark);
}
}
.query-console {
padding: 0;
padding-bottom: $footer-height;
.query-console-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px;
font-weight: 700;
}
.query-console-body {
overflow: auto;
display: flex;
flex-direction: column;
max-height: 100%;
padding: 0 6px 3px;
.query-console-log {
padding: 1px 3px;
margin: 1px 0;
border-radius: $border-radius;
.query-console-log-sql {
font-size: 95%;
opacity: 0.8;
font-weight: 700;
&:hover {
user-select: text;
}
}
}
}
}
}
</style>

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({

View File

@@ -140,6 +140,7 @@ export const enUS = {
total: 'Total',
table: 'Table | Tables',
view: 'View | Views',
materializedview: 'Materialized view | Materialized views',
definer: 'Definer',
algorithm: 'Algorithm',
trigger: 'Trigger | Triggers',
@@ -216,6 +217,7 @@ export const enUS = {
updateOption: 'Update option',
deleteView: 'Delete view',
createNewView: 'Create new view',
createNewMaterializedView: 'Create new materialized view',
deleteTrigger: 'Delete trigger',
createNewTrigger: 'Create new trigger',
currentUser: 'Current user',
@@ -248,6 +250,7 @@ export const enUS = {
thereAreNoTableFields: 'There are no table fields',
newTable: 'New table',
newView: 'New view',
newMaterializedView: 'New materialized view',
newTrigger: 'New trigger',
newRoutine: 'New routine',
newFunction: 'New function',
@@ -404,8 +407,9 @@ export const enUS = {
saveFile: 'Save file',
saveFileAs: 'Save file as',
openFile: 'Open file',
openNotes: 'Open notes'
openNotes: 'Open notes',
debugConsole: 'Debug console', // <- console tab name
executedQueries: 'Executed queries' // <- console tab name
},
faker: { // Faker.js methods, used in random generated content
address: 'Address',

View File

@@ -12,7 +12,7 @@ import { createApp } from 'vue';
import App from '@/App.vue';
import { i18n } from '@/i18n';
import { useApplicationStore } from '@/stores/application';
import { useConsoleStore } from '@/stores/console';
import { QueryLog, useConsoleStore } from '@/stores/console';
import { useNotificationsStore } from '@/stores/notifications';
import { useSettingsStore } from '@/stores/settings';
@@ -37,11 +37,17 @@ i18n.global.locale = locale;
// IPC exceptions
ipcRenderer.on('unhandled-exception', (event, error) => {
useNotificationsStore().addNotification({ status: 'error', message: error.message });
useConsoleStore().putLog('debug', {
level: 'error',
process: 'main',
message: error.message,
date: new Date()
});
});
// IPC query logs
ipcRenderer.on('query-log', (event, logRecord) => {
useConsoleStore().putLog(logRecord);
ipcRenderer.on('query-log', (event, logRecord: QueryLog) => {
useConsoleStore().putLog('query', logRecord);
});
ipcRenderer.on('toggle-console', () => {

View File

@@ -19,4 +19,20 @@ export default class {
static createView (params: CreateViewParams & { uid: string }): Promise<IpcResponse> {
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));
}
}

View File

@@ -0,0 +1,9 @@
export const camelize = (text: string) => {
const textArr = text.split('-');
for (let i = 0; i < textArr.length; i++) {
if (i === 0) continue;
textArr[i] = textArr[i].charAt(0).toUpperCase() + textArr[i].slice(1);
}
return textArr.join('');
};

View File

@@ -38,7 +38,7 @@ const connStringConstruct = (args: ConnectionParams & { pgConnString?: string })
args.sshPort = stringArgs.sshPort;
// ssl mode
args.ssl = checkForSSl(args.pgConnString);
args.ssl = checkForSSl(args.pgConnString) || args.ssl;
args.cert = stringArgs.sslcert;
args.key = stringArgs.sslkey;
args.ca = stringArgs.sslrootcert;

View File

@@ -349,11 +349,11 @@
}
}
.query-console {
.console {
border-top: 1px solid #444;
background-color: $bg-color-dark;
.query-console-log {
.console-log {
&:hover,
&:focus {
background: $bg-color-gray;

View File

@@ -173,11 +173,11 @@
}
}
.query-console {
.console {
border-top: 1px solid darken($bg-color-light-gray, 15%);
background-color: $bg-color-light;
.query-console-log {
.console-log {
&:hover,
&:focus {
background: $bg-color-light-gray;

View File

@@ -1,56 +1,63 @@
import { defineStore } from 'pinia';
import { useWorkspacesStore } from './workspaces';
const logsSize = 1000;
export interface ConsoleRecord {
export type LogType = 'query' | 'debug'
export interface QueryLog {
cUid: string;
sql: string;
date: Date;
}
export interface DebugLog {
level: 'log' | 'info' | 'warn' | 'error';
process: 'renderer' | 'main' | 'worker';
message: string;
date: Date;
}
export const useConsoleStore = defineStore('console', {
state: () => ({
records: [] as ConsoleRecord[],
consolesHeight: new Map<string, number>(),
consolesOpened: new Set([])
isConsoleOpen: false,
queryLogs: [] as QueryLog[],
debugLogs: [] as DebugLog[],
selectedTab: 'query' as LogType,
consoleHeight: 0
}),
getters: {
getLogsByWorkspace: state => (uid: string) => state.records.filter(r => r.cUid === uid),
isConsoleOpen: state => (uid: string) => state.consolesOpened.has(uid),
consoleHeight: state => {
const uid = useWorkspacesStore().getSelected;
return state.consolesHeight.get(uid) || 0;
}
getLogsByWorkspace: state => (uid: string) => state.queryLogs.filter(r => r.cUid === uid)
},
actions: {
putLog (record: ConsoleRecord) {
this.records.push(record);
putLog (type: LogType, record: QueryLog | DebugLog) {
if (type === 'query') {
this.queryLogs.push(record);
if (this.records.length > logsSize)
this.records = this.records.slice(0, logsSize);
if (this.queryLogs.length > logsSize)
this.queryLogs = this.queryLogs.slice(0, logsSize);
}
else if (type === 'debug') {
this.debugLogs.push(record);
if (this.debugLogs.length > logsSize)
this.debugLogs = this.debugLogs.slice(0, logsSize);
}
},
openConsole () {
const uid = useWorkspacesStore().getSelected;
this.consolesOpened.add(uid);
this.consolesHeight.set(uid, 250);
this.isConsoleOpen = true;
this.consoleHeight = 250;
},
closeConsole () {
const uid = useWorkspacesStore().getSelected;
this.consolesOpened.delete(uid);
this.consolesHeight.set(uid, 0);
this.isConsoleOpen = false;
this.consoleHeight = 0;
},
resizeConsole (height: number) {
const uid = useWorkspacesStore().getSelected;
if (height < 30)
this.closeConsole();
else
this.consolesHeight.set(uid, height);
this.consoleHeight = height;
},
toggleConsole () {
const uid = useWorkspacesStore().getSelected;
if (this.consolesOpened.has(uid))
if (this.isConsoleOpen)
this.closeConsole();
else
this.openConsole();

View File

@@ -558,6 +558,8 @@ export const useWorkspacesStore = defineStore('workspaces', {
switch (type) {
case 'new-table':
case 'new-view':
case 'new-materialized-view':
case 'new-trigger':
case 'new-trigger-function':
case 'new-function':
@@ -659,6 +661,8 @@ export const useWorkspacesStore = defineStore('workspaces', {
break;
case 'data':
case 'table-props':
case 'view-props':
case 'materialized-view-props':
case 'trigger-props':
case 'trigger-function-props':
case 'function-props':