Merge pull request #248 from antares-sql/ts-renderer

TypeScript in renderer process
This commit is contained in:
Fabio Di Stasio 2022-06-21 18:09:42 +02:00 committed by GitHub
commit 174579bf8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
170 changed files with 42143 additions and 12319 deletions

View File

@ -1,4 +1,5 @@
node_modules node_modules
assets assets
out out
dist dist
build

1
.gitignore vendored
View File

@ -7,5 +7,4 @@ node_modules
thumbs.db thumbs.db
NOTES.md NOTES.md
*.txt *.txt
package-lock.json
*.heapsnapshot *.heapsnapshot

30996
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -22,8 +22,8 @@
"postinstall": "electron-builder install-app-deps && npm run devtools:install", "postinstall": "electron-builder install-app-deps && npm run devtools:install",
"test:e2e": "npm run compile && npm run test:e2e-dry", "test:e2e": "npm run compile && npm run test:e2e-dry",
"test:e2e-dry": "xvfb-maybe -- playwright test", "test:e2e-dry": "xvfb-maybe -- playwright test",
"lint": "eslint . --ext .js,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"", "lint": "eslint . --ext .js,.ts,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"",
"lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix", "lint:fix": "eslint . --ext .js,.ts,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix",
"contributors:add": "all-contributors add", "contributors:add": "all-contributors add",
"contributors:generate": "all-contributors generate" "contributors:generate": "all-contributors generate"
}, },
@ -150,6 +150,8 @@
"@babel/preset-typescript": "~7.16.7", "@babel/preset-typescript": "~7.16.7",
"@playwright/test": "~1.21.1", "@playwright/test": "~1.21.1",
"@types/better-sqlite3": "~7.5.0", "@types/better-sqlite3": "~7.5.0",
"@types/leaflet": "~1.7.9",
"@types/marked": "~4.0.3",
"@types/node": "~17.0.23", "@types/node": "~17.0.23",
"@types/pg": "~8.6.5", "@types/pg": "~8.6.5",
"@typescript-eslint/eslint-plugin": "~5.18.0", "@typescript-eslint/eslint-plugin": "~5.18.0",
@ -189,7 +191,7 @@
"unzip-crx-3": "~0.2.0", "unzip-crx-3": "~0.2.0",
"vue-eslint-parser": "~8.3.0", "vue-eslint-parser": "~8.3.0",
"vue-loader": "~16.8.3", "vue-loader": "~16.8.3",
"webpack": "~5.60.0", "webpack": "~5.72.0",
"webpack-cli": "~4.9.1", "webpack-cli": "~4.9.1",
"webpack-dev-server": "~4.4.0", "webpack-dev-server": "~4.4.0",
"xvfb-maybe": "~0.2.1" "xvfb-maybe": "~0.2.1"

View File

@ -59,7 +59,7 @@ async function restartElectron () {
console.error(chalk.red(data.toString())); console.error(chalk.red(data.toString()));
}); });
electronProcess.on('exit', (code, signal) => { electronProcess.on('exit', () => {
if (!manualRestart) process.exit(0); if (!manualRestart) process.exit(0);
}); });
} }

View File

@ -1,4 +1,6 @@
module.exports = { import { Customizations } from '../interfaces/customizations';
export const defaults: Customizations = {
// Defaults // Defaults
defaultPort: null, defaultPort: null,
defaultUser: null, defaultUser: null,
@ -68,24 +70,24 @@ module.exports = {
viewUpdateOption: false, viewUpdateOption: false,
procedureDeterministic: false, procedureDeterministic: false,
procedureDataAccess: false, procedureDataAccess: false,
procedureSql: false, procedureSql: null,
procedureContext: false, procedureContext: false,
procedureLanguage: false, procedureLanguage: false,
functionDeterministic: false, functionDeterministic: false,
functionDataAccess: false, functionDataAccess: false,
functionSql: false, functionSql: null,
functionContext: false, functionContext: false,
functionLanguage: false, functionLanguage: false,
triggerSql: false, triggerSql: null,
triggerStatementInCreation: false, triggerStatementInCreation: false,
triggerMultipleEvents: false, triggerMultipleEvents: false,
triggerTableInName: false, triggerTableInName: false,
triggerUpdateColumns: false, triggerUpdateColumns: false,
triggerOnlyRename: false, triggerOnlyRename: false,
triggerEnableDisable: false, triggerEnableDisable: false,
triggerFunctionSql: false, triggerFunctionSql: null,
triggerFunctionlanguages: false, triggerFunctionlanguages: null,
parametersLength: false, parametersLength: false,
languages: false, languages: null,
readOnlyMode: false readOnlyMode: false
}; };

View File

@ -1,6 +0,0 @@
module.exports = {
maria: require('./mysql'),
mysql: require('./mysql'),
pg: require('./postgresql'),
sqlite: require('./sqlite')
};

View File

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

View File

@ -1,6 +1,7 @@
const defaults = require('./defaults'); import { Customizations } from '../interfaces/customizations';
import { defaults } from './defaults';
module.exports = { export const customizations: Customizations = {
...defaults, ...defaults,
// Defaults // Defaults
defaultPort: 3306, defaultPort: 3306,

View File

@ -1,6 +1,7 @@
const defaults = require('./defaults'); import { Customizations } from '../interfaces/customizations';
import { defaults } from './defaults';
module.exports = { export const customizations: Customizations = {
...defaults, ...defaults,
// Defaults // Defaults
defaultPort: 5432, defaultPort: 5432,

View File

@ -1,4 +1,8 @@
module.exports = { import { Customizations } from '../interfaces/customizations';
import { defaults } from './defaults';
export const customizations: Customizations = {
...defaults,
// Core // Core
fileConnection: true, fileConnection: true,
// Structure // Structure

View File

@ -1,4 +1,6 @@
module.exports = [ import { TypesGroup } from 'common/interfaces/antares';
export default [
{ {
group: 'integer', group: 'integer',
types: [ types: [
@ -306,4 +308,4 @@ module.exports = [
} }
] ]
} }
]; ] as TypesGroup[];

View File

@ -1,4 +1,6 @@
module.exports = [ import { TypesGroup } from 'common/interfaces/antares';
export default [
{ {
group: 'integer', group: 'integer',
types: [ types: [
@ -290,4 +292,4 @@ module.exports = [
} }
] ]
} }
]; ] as TypesGroup[];

View File

@ -1,4 +1,6 @@
module.exports = [ import { TypesGroup } from 'common/interfaces/antares';
export default [
{ {
group: 'integer', group: 'integer',
types: [ types: [
@ -134,4 +136,4 @@ module.exports = [
} }
] ]
} }
]; ] as TypesGroup[];

View File

@ -1,4 +1,4 @@
module.exports = [ export default [
'PRIMARY', 'PRIMARY',
'INDEX', 'INDEX',
'UNIQUE', 'UNIQUE',

View File

@ -1,4 +1,4 @@
module.exports = [ export default [
'PRIMARY', 'PRIMARY',
'INDEX', 'INDEX',
'UNIQUE' 'UNIQUE'

View File

@ -1,4 +1,4 @@
module.exports = [ export default [
'PRIMARY', 'PRIMARY',
'INDEX', 'INDEX',
'UNIQUE' 'UNIQUE'

View File

@ -14,6 +14,12 @@ export type ClientCode = 'mysql' | 'maria' | 'pg' | 'sqlite'
export type Exporter = MysqlExporter | PostgreSQLExporter export type Exporter = MysqlExporter | PostgreSQLExporter
export type Importer = MySQLImporter | PostgreSQLImporter export type Importer = MySQLImporter | PostgreSQLImporter
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface IpcResponse<T = any> {
status: 'success' | 'error';
response?: T;
}
/** /**
* Pasameters needed to create a new Antares connection to a database * Pasameters needed to create a new Antares connection to a database
*/ */
@ -67,12 +73,34 @@ export interface TypeInformations {
zerofill: boolean; zerofill: boolean;
} }
export interface TypesGroup {
group: string;
types: TypeInformations[];
}
// Tables // Tables
export interface TableField { export interface TableInfos {
name: string;
type: string;
rows: number;
created: Date;
updated: Date;
engine: string;
comment: string;
size: number | false;
autoIncrement: number;
collation: string;
}
export type TableOptions = Partial<TableInfos>;
export interface TableField {
// eslint-disable-next-line camelcase
_antares_id?: string;
name: string; name: string;
key: string;
type: string; type: string;
schema: string; schema: string;
table?: string;
numPrecision?: number; numPrecision?: number;
numLength?: number; numLength?: number;
datePrecision?: number; datePrecision?: number;
@ -82,7 +110,8 @@ export interface TableField {
unsigned?: boolean; unsigned?: boolean;
zerofill?: boolean; zerofill?: boolean;
order?: number; order?: number;
default?: number | string; default?: string;
defaultType?: string;
enumValues?: string; enumValues?: string;
charset?: string; charset?: string;
collation?: string; collation?: string;
@ -92,9 +121,16 @@ export interface TableField {
comment?: string; comment?: string;
after?: string; after?: string;
orgName?: string; orgName?: string;
length?: number | false;
alias: string;
tableAlias: string;
orgTable: string;
key?: 'pri' | 'uni';
} }
export interface TableIndex { export interface TableIndex {
// eslint-disable-next-line camelcase
_antares_id?: string;
name: string; name: string;
fields: string[]; fields: string[];
type: string; type: string;
@ -107,6 +143,8 @@ export interface TableIndex {
} }
export interface TableForeign { export interface TableForeign {
// eslint-disable-next-line camelcase
_antares_id?: string;
constraintName: string; constraintName: string;
refSchema: string; refSchema: string;
table: string; table: string;
@ -118,15 +156,6 @@ export interface TableForeign {
oldName?: string; oldName?: string;
} }
export interface TableOptions {
name: string;
type?: 'table' | 'view';
engine?: string;
comment?: string;
collation?: string;
autoIncrement?: number;
}
export interface CreateTableParams { export interface CreateTableParams {
/** Connection UID */ /** Connection UID */
uid?: string; uid?: string;
@ -165,6 +194,7 @@ export interface AlterTableParams {
} }
// Views // Views
export type ViewInfos = TableInfos
export interface CreateViewParams { export interface CreateViewParams {
schema: string; schema: string;
name: string; name: string;
@ -180,6 +210,19 @@ export interface AlterViewParams extends CreateViewParams {
} }
// Triggers // Triggers
export interface TriggerInfos {
name: string;
statement: string;
timing: string;
definer: string;
event: string;
table: string;
sqlMode: string;
created: Date;
charset: string;
enabled?: boolean;
}
export interface CreateTriggerParams { export interface CreateTriggerParams {
definer?: string; definer?: string;
schema: string; schema: string;
@ -195,13 +238,38 @@ export interface AlterTriggerParams extends CreateTriggerParams {
} }
// Routines & Functions // Routines & Functions
export interface FunctionParam { export interface FunctionParam {
// eslint-disable-next-line camelcase
_antares_id: string;
context: string; context: string;
name: string; name: string;
type: string; type: string;
length: number; length: number;
} }
export interface RoutineInfos {
name: string;
type?: string;
definer: string;
created?: string;
sql?: string;
updated?: string;
comment?: string;
charset?: string;
security?: string;
language?: string;
dataAccess?: string;
deterministic?: boolean;
parameters?: FunctionParam[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
returns?: any;
returnsLength?: number;
}
export type FunctionInfos = RoutineInfos
export type TriggerFunctionInfos = FunctionInfos
export interface CreateRoutineParams { export interface CreateRoutineParams {
name: string; name: string;
parameters?: FunctionParam[]; parameters?: FunctionParam[];
@ -239,7 +307,7 @@ export interface AlterFunctionParams extends CreateFunctionParams {
} }
// Events // Events
export interface CreateEventParams { export interface EventInfos {
definer?: string; definer?: string;
schema: string; schema: string;
name: string; name: string;
@ -248,16 +316,39 @@ export interface CreateEventParams {
starts: string; starts: string;
ends: string; ends: string;
at: string; at: string;
preserve: string; preserve: boolean;
state: string; state: string;
comment: string; comment: string;
enabled?: boolean;
sql: string; sql: string;
} }
export type CreateEventParams = EventInfos;
export interface AlterEventParams extends CreateEventParams { export interface AlterEventParams extends CreateEventParams {
oldName?: string; oldName?: string;
} }
// Schema
export interface SchemaInfos {
name: string;
size: number;
tables: TableInfos[];
functions: FunctionInfos[];
procedures: RoutineInfos[];
triggers: TriggerInfos[];
schedulers: EventInfos[];
}
export interface CollationInfos {
charset: string;
collation: string;
compiled: boolean;
default: boolean;
id: string | number;
sortLen: number;
}
// Query // Query
export interface QueryBuilderObject { export interface QueryBuilderObject {
schema: string; schema: string;
@ -285,17 +376,10 @@ export interface QueryParams {
tabUid?: string; tabUid?: string;
} }
export interface QueryField { /**
name: string; * @deprecated Use TableFIeld
alias: string; */
orgName: string; export type QueryField = TableField
schema: string;
table: string;
tableAlias: string;
orgTable: string;
type: string;
length: number;
}
export interface QueryForeign { export interface QueryForeign {
schema: string; schema: string;

View File

@ -0,0 +1,91 @@
export interface Customizations {
// Defaults
defaultPort?: number;
defaultUser?: string;
defaultDatabase?: string;
// Core
database?: boolean;
collations?: boolean;
engines?: boolean;
connectionSchema?: boolean;
sslConnection?: boolean;
sshConnection?: boolean;
fileConnection?: boolean;
cancelQueries?: boolean;
// Tools
processesList?: boolean;
usersManagement?: boolean;
variables?: boolean;
// Structure
schemas?: boolean;
tables?: boolean;
views?: boolean;
triggers?: boolean;
triggerFunctions?: boolean;
routines?: boolean;
functions?: boolean;
schedulers?: boolean;
// Settings
elementsWrapper: string;
stringsWrapper: string;
tableAdd?: boolean;
viewAdd?: boolean;
triggerAdd?: boolean;
triggerFunctionAdd?: boolean;
routineAdd?: boolean;
functionAdd?: boolean;
schedulerAdd?: boolean;
databaseEdit?: boolean;
schemaEdit?: boolean;
schemaDrop?: boolean;
schemaExport?: boolean;
exportByChunks?: boolean;
schemaImport?: boolean;
tableSettings?: boolean;
tableOptions?: boolean;
tableArray?: boolean;
tableRealCount?: boolean;
viewSettings?: boolean;
triggerSettings?: boolean;
triggerFunctionSettings?: boolean;
routineSettings?: boolean;
functionSettings?: boolean;
schedulerSettings?: boolean;
indexes?: boolean;
foreigns?: boolean;
sortableFields?: boolean;
unsigned?: boolean;
nullable?: boolean;
nullablePrimary?: boolean;
zerofill?: boolean;
autoIncrement?: boolean;
comment?: boolean;
collation?: boolean;
definer?: boolean;
onUpdate?: boolean;
viewAlgorithm?: boolean;
viewSqlSecurity?: boolean;
viewUpdateOption?: boolean;
procedureDeterministic?: boolean;
procedureDataAccess?: boolean;
procedureSql?: string;
procedureContext?: boolean;
procedureLanguage?: boolean;
functionDeterministic?: boolean;
functionDataAccess?: boolean;
functionSql?: string;
functionContext?: boolean;
functionLanguage?: boolean;
triggerSql?: string;
triggerStatementInCreation?: boolean;
triggerMultipleEvents?: boolean;
triggerTableInName?: boolean;
triggerUpdateColumns?: boolean;
triggerOnlyRename?: boolean;
triggerEnableDisable?: boolean;
triggerFunctionSql?: string;
triggerFunctionlanguages?: string[];
parametersLength?: boolean;
languages?: string[];
readOnlyMode?: boolean;
}

View File

@ -1,5 +1,34 @@
import { UsableLocale } from '@faker-js/faker'; import { UsableLocale } from '@faker-js/faker';
export interface TableUpdateParams {
uid: string;
schema: string;
table: string;
primary?: string;
id: number | string;
content: number | string | boolean | Date | Blob | null;
type: string;
field: string;
}
export interface TableDeleteParams {
uid: string;
schema: string;
table: string;
primary?: string;
field: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rows: {[key: string]: any};
}
export interface TableFilterClausole {
active: boolean;
field: string;
op: '=' | '!=' | '>' | '<' | '>=' | '<=' | 'IN' | 'NOT IN' | 'LIKE' | 'BETWEEN' | 'IS NULL' | 'IS NOT NULL';
value: '';
value2: '';
}
export interface InsertRowsParams { export interface InsertRowsParams {
uid: string; uid: string;
schema: string; schema: string;

View File

@ -1,7 +1,6 @@
'use strict'; export function bufferToBase64 (buf: Buffer) {
export function bufferToBase64 (buf) {
const binstr = Array.prototype.map.call(buf, ch => { const binstr = Array.prototype.map.call(buf, ch => {
return String.fromCharCode(ch); return String.fromCharCode(ch);
}).join(''); }).join('');
return btoa(binstr); return Buffer.from(binstr, 'binary').toString('base64');
} }

View File

@ -1,5 +1,4 @@
'use strict'; export function formatBytes (bytes: number, decimals = 2) {
export function formatBytes (bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes'; if (bytes === 0) return '0 Bytes';
const k = 1024; const k = 1024;

View File

@ -1,10 +0,0 @@
/**
*
* @param {any[]} array
* @returns {number}
*/
export function getArrayDepth (array) {
return Array.isArray(array)
? 1 + Math.max(0, ...array.map(getArrayDepth))
: 0;
}

View File

@ -0,0 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export function getArrayDepth (array: any[]): number {
return Array.isArray(array)
? 1 + Math.max(0, ...array.map(getArrayDepth))
: 0;
}

View File

@ -1,5 +1,3 @@
'use strict';
const lookup = { const lookup = {
0: '0000', 0: '0000',
1: '0001', 1: '0001',
@ -23,15 +21,11 @@ const lookup = {
D: '1101', D: '1101',
E: '1110', E: '1110',
F: '1111' F: '1111'
}; } as const;
/** export type HexChar = keyof typeof lookup
* Converts hexadecimal string to binary string
* export default function hexToBinary (hex: HexChar[]) {
* @param {string} hex Hexadecimal string
* @returns {string} Binary string
*/
export default function hexToBinary (hex) {
let binary = ''; let binary = '';
for (let i = 0; i < hex.length; i++) for (let i = 0; i < hex.length; i++)
binary += lookup[hex[i]]; binary += lookup[hex[i]];

View File

@ -1,5 +1,4 @@
'use strict'; export function mimeFromHex (hex: string) {
export function mimeFromHex (hex) {
switch (hex.substring(0, 4)) { // 2 bytes switch (hex.substring(0, 4)) { // 2 bytes
case '424D': case '424D':
return { ext: 'bmp', mime: 'image/bmp' }; return { ext: 'bmp', mime: 'image/bmp' };
@ -23,7 +22,7 @@ export function mimeFromHex (hex) {
case '425A68': case '425A68':
return { ext: 'bz2', mime: 'application/x-bzip2' }; return { ext: 'bz2', mime: 'application/x-bzip2' };
default: default:
switch (hex) { // 4 bytes switch (hex) { // 4 bites
case '89504E47': case '89504E47':
return { ext: 'png', mime: 'image/png' }; return { ext: 'png', mime: 'image/png' };
case '47494638': case '47494638':

View File

@ -3,13 +3,7 @@
const pattern = /[\0\x08\x09\x1a\n\r"'\\\%]/gm; const pattern = /[\0\x08\x09\x1a\n\r"'\\\%]/gm;
const regex = new RegExp(pattern); const regex = new RegExp(pattern);
/** function sqlEscaper (string: string) {
* Escapes a string
*
* @param {String} string
* @returns {String}
*/
function sqlEscaper (string) {
return string.replace(regex, char => { return string.replace(regex, char => {
const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '\"', '\\', '\\\\', '%']; const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '\"', '\\', '\\\\', '%'];
const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\\\'', '\\\"', '\\\\', '\\\\\\\\', '\%']; const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\\\'', '\\\"', '\\\\', '\\\\\\\\', '\%'];

View File

@ -1,8 +0,0 @@
/**
* @export
* @param {String} [prefix]
* @returns {String} Unique ID
*/
export function uidGen (prefix) {
return (prefix ? `${prefix}:` : '') + Math.random().toString(36).substr(2, 9).toUpperCase();
}

View File

@ -0,0 +1,3 @@
export function uidGen (prefix?: string) {
return (prefix ? `${prefix}:` : '') + Math.random().toString(36).substr(2, 9).toUpperCase();
}

View File

@ -5,11 +5,6 @@ export default () => {
app.exit(); app.exit();
}); });
ipcMain.on('get-key', async event => {
const key = false;
event.returnValue = key;
});
ipcMain.handle('show-open-dialog', (event, options) => { ipcMain.handle('show-open-dialog', (event, options) => {
return dialog.showOpenDialog(options); return dialog.showOpenDialog(options);
}); });

View File

@ -172,7 +172,10 @@ export default (connections: {[key: string]: antares.Client}) => {
}); });
ipcMain.handle('export', (event, { uid, type, tables, ...rest }) => { ipcMain.handle('export', (event, { uid, type, tables, ...rest }) => {
if (exporter !== null) return; if (exporter !== null) {
exporter.kill();
return;
}
return new Promise((resolve/*, reject */) => { return new Promise((resolve/*, reject */) => {
(async () => { (async () => {
@ -265,7 +268,10 @@ export default (connections: {[key: string]: antares.Client}) => {
}); });
ipcMain.handle('import-sql', async (event, options) => { ipcMain.handle('import-sql', async (event, options) => {
if (importer !== null) return; if (importer !== null) {
importer.kill();
return;
}
return new Promise((resolve/*, reject */) => { return new Promise((resolve/*, reject */) => {
(async () => { (async () => {

View File

@ -1,3 +1,4 @@
import * as fs from 'fs';
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import { InsertRowsParams } from 'common/interfaces/tableApis'; import { InsertRowsParams } from 'common/interfaces/tableApis';
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
@ -5,8 +6,7 @@ import { faker } from '@faker-js/faker';
import * as moment from 'moment'; import * as moment from 'moment';
import { sqlEscaper } from 'common/libs/sqlEscaper'; import { sqlEscaper } from 'common/libs/sqlEscaper';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes';
import * as customizations from 'common/customizations'; import customizations from 'common/customizations';
import fs from 'fs';
export default (connections: {[key: string]: antares.Client}) => { export default (connections: {[key: string]: antares.Client}) => {
ipcMain.handle('get-table-columns', async (event, params) => { ipcMain.handle('get-table-columns', async (event, params) => {
@ -246,84 +246,12 @@ export default (connections: {[key: string]: antares.Client}) => {
} }
}); });
ipcMain.handle('insert-table-rows', async (event, params) => {
try { // TODO: move to client classes
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const insertObj: {[key: string]: any} = {};
for (const key in params.row) {
const type = params.fields[key];
let escapedParam;
if (params.row[key] === null)
escapedParam = 'NULL';
else if ([...NUMBER, ...FLOAT].includes(type))
escapedParam = +params.row[key];
else if ([...TEXT, ...LONG_TEXT].includes(type)) {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = `"${sqlEscaper(params.row[key].value)}"`;
break;
case 'pg':
escapedParam = `'${params.row[key].value.replaceAll('\'', '\'\'')}'`;
break;
}
}
else if (BLOB.includes(type)) {
if (params.row[key].value) {
let fileBlob;
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `0x${fileBlob.toString('hex')}`;
break;
case 'pg':
fileBlob = fs.readFileSync(params.row[key].value);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break;
}
}
else {
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = '""';
break;
case 'pg':
escapedParam = 'decode(\'\', \'hex\')';
break;
}
}
}
insertObj[key] = escapedParam;
}
const rows = new Array(+params.repeat).fill(insertObj);
await connections[params.uid]
.schema(params.schema)
.into(params.table)
.insert(rows)
.run();
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('insert-table-fake-rows', async (event, params: InsertRowsParams) => { ipcMain.handle('insert-table-fake-rows', async (event, params: InsertRowsParams) => {
try { // TODO: move to client classes try { // TODO: move to client classes
// eslint-disable-next-line @typescript-eslint/no-explicit-any const rows: {[key: string]: string | number | boolean | Date | Buffer}[] = [];
const rows: {[key: string]: any}[] = [];
for (let i = 0; i < +params.repeat; i++) { for (let i = 0; i < +params.repeat; i++) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any const insertObj: {[key: string]: string | number | boolean | Date | Buffer} = {};
const insertObj: {[key: string]: any} = {};
for (const key in params.row) { for (const key in params.row) {
const type = params.fields[key]; const type = params.fields[key];
@ -382,8 +310,7 @@ export default (connections: {[key: string]: antares.Client}) => {
insertObj[key] = escapedParam; insertObj[key] = escapedParam;
} }
else { // Faker value else { // Faker value
// eslint-disable-next-line @typescript-eslint/no-explicit-any const parsedParams: {[key: string]: string | number | boolean | Date | Buffer} = {};
const parsedParams: {[key: string]: any} = {};
let fakeValue; let fakeValue;
if (params.locale) if (params.locale)
@ -403,7 +330,7 @@ export default (connections: {[key: string]: antares.Client}) => {
if (typeof fakeValue === 'string') { if (typeof fakeValue === 'string') {
if (params.row[key].length) if (params.row[key].length)
fakeValue = fakeValue.substr(0, params.row[key].length); fakeValue = fakeValue.substring(0, params.row[key].length);
fakeValue = `'${sqlEscaper(fakeValue)}'`; fakeValue = `'${sqlEscaper(fakeValue)}'`;
} }
else if ([...DATE, ...DATETIME].includes(type)) else if ([...DATE, ...DATETIME].includes(type))

View File

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

View File

@ -1,7 +1,7 @@
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import * as mysql from 'mysql2/promise'; import * as mysql from 'mysql2/promise';
import { AntaresCore } from '../AntaresCore'; import { AntaresCore } from '../AntaresCore';
import * as dataTypes from 'common/data-types/mysql'; import dataTypes from 'common/data-types/mysql';
import SSH2Promise = require('ssh2-promise'); import SSH2Promise = require('ssh2-promise');
import SSHConfig from 'ssh2-promise/lib/sshConfig'; import SSHConfig from 'ssh2-promise/lib/sshConfig';
@ -321,7 +321,7 @@ export class MySQLClient extends AntaresCore {
return filteredDatabases.map(db => { return filteredDatabases.map(db => {
if (schemas.has(db.Database)) { if (schemas.has(db.Database)) {
// TABLES // TABLES
const remappedTables = tablesArr.filter(table => table.Db === db.Database).map(table => { const remappedTables: antares.TableInfos[] = tablesArr.filter(table => table.Db === db.Database).map(table => {
let tableType; let tableType;
switch (table.Comment) { switch (table.Comment) {
case 'VIEW': case 'VIEW':
@ -350,7 +350,7 @@ export class MySQLClient extends AntaresCore {
}); });
// PROCEDURES // PROCEDURES
const remappedProcedures = procedures.filter(procedure => procedure.Db === db.Database).map(procedure => { const remappedProcedures: antares.RoutineInfos[] = procedures.filter(procedure => procedure.Db === db.Database).map(procedure => {
return { return {
name: procedure.Name, name: procedure.Name,
type: procedure.Type, type: procedure.Type,
@ -364,7 +364,7 @@ export class MySQLClient extends AntaresCore {
}); });
// FUNCTIONS // FUNCTIONS
const remappedFunctions = functions.filter(func => func.Db === db.Database).map(func => { const remappedFunctions: antares.FunctionInfos[] = functions.filter(func => func.Db === db.Database).map(func => {
return { return {
name: func.Name, name: func.Name,
type: func.Type, type: func.Type,
@ -378,33 +378,26 @@ export class MySQLClient extends AntaresCore {
}); });
// SCHEDULERS // SCHEDULERS
const remappedSchedulers = schedulers.filter(scheduler => scheduler.Db === db.Database).map(scheduler => { const remappedSchedulers: antares.EventInfos[] = schedulers.filter(scheduler => scheduler.Db === db.Database).map(scheduler => {
return { return {
name: scheduler.EVENT_NAME, name: scheduler.EVENT_NAME,
definition: scheduler.EVENT_DEFINITION, schema: scheduler.Db,
type: scheduler.EVENT_TYPE, sql: scheduler.EVENT_DEFINITION,
execution: scheduler.EVENT_TYPE === 'RECURRING' ? 'EVERY' : 'ONCE',
definer: scheduler.DEFINER, definer: scheduler.DEFINER,
body: scheduler.EVENT_BODY,
starts: scheduler.STARTS, starts: scheduler.STARTS,
ends: scheduler.ENDS, ends: scheduler.ENDS,
state: scheduler.STATUS === 'ENABLED' ? 'ENABLE' : scheduler.STATE === 'DISABLED' ? 'DISABLE' : 'DISABLE ON SLAVE',
enabled: scheduler.STATUS === 'ENABLED', enabled: scheduler.STATUS === 'ENABLED',
executeAt: scheduler.EXECUTE_AT, at: scheduler.EXECUTE_AT,
intervalField: scheduler.INTERVAL_FIELD, every: [scheduler.INTERVAL_FIELD, scheduler.INTERVAL_VALUE],
intervalValue: scheduler.INTERVAL_VALUE, preserve: scheduler.ON_COMPLETION.includes('PRESERVE'),
onCompletion: scheduler.ON_COMPLETION, comment: scheduler.EVENT_COMMENT
originator: scheduler.ORIGINATOR,
sqlMode: scheduler.SQL_MODE,
created: scheduler.CREATED,
updated: scheduler.LAST_ALTERED,
lastExecuted: scheduler.LAST_EXECUTED,
comment: scheduler.EVENT_COMMENT,
charset: scheduler.CHARACTER_SET_CLIENT,
timezone: scheduler.TIME_ZONE
}; };
}); });
// TRIGGERS // TRIGGERS
const remappedTriggers = triggersArr.filter(trigger => trigger.Db === db.Database).map(trigger => { const remappedTriggers: antares.TriggerInfos[] = triggersArr.filter(trigger => trigger.Db === db.Database).map(trigger => {
return { return {
name: trigger.Trigger, name: trigger.Trigger,
statement: trigger.Statement, statement: trigger.Statement,
@ -930,19 +923,22 @@ export class MySQLClient extends AntaresCore {
} }
async getViewInformations ({ schema, view }: { schema: string; view: string }) { async getViewInformations ({ schema, view }: { schema: string; view: string }) {
const sql = `SHOW CREATE VIEW \`${schema}\`.\`${view}\``; const { rows: algorithm } = await this.raw(`SHOW CREATE VIEW \`${schema}\`.\`${view}\``);
const results = await this.raw(sql); const { rows: viewInfo } = await this.raw(`
SELECT *
FROM INFORMATION_SCHEMA.VIEWS
WHERE TABLE_SCHEMA = '${schema}'
AND TABLE_NAME = '${view}'
`);
return results.rows.map(row => { return {
return { algorithm: algorithm[0]['Create View'].match(/(?<=CREATE ALGORITHM=).*?(?=\s)/gs)[0],
algorithm: row['Create View'].match(/(?<=CREATE ALGORITHM=).*?(?=\s)/gs)[0], definer: viewInfo[0].DEFINER,
definer: row['Create View'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0], security: viewInfo[0].SECURITY_TYPE,
security: row['Create View'].match(/(?<=SQL SECURITY ).*?(?=\s)/gs)[0], updateOption: viewInfo[0].CHECK_OPTION === 'NONE' ? '' : viewInfo[0].CHECK_OPTION,
updateOption: row['Create View'].match(/(?<=WITH ).*?(?=\s)/gs) ? row['Create View'].match(/(?<=WITH ).*?(?=\s)/gs)[0] : '', sql: viewInfo[0].VIEW_DEFINITION,
sql: row['Create View'].match(/(?<=AS ).*?$/gs)[0], name: viewInfo[0].TABLE_NAME
name: row.View };
};
})[0];
} }
async dropView (params: { schema: string; view: string }) { async dropView (params: { schema: string; view: string }) {
@ -955,7 +951,7 @@ export class MySQLClient extends AntaresCore {
USE \`${view.schema}\`; USE \`${view.schema}\`;
ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''} ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''}
SQL SECURITY ${view.security} SQL SECURITY ${view.security}
params \`${view.schema}\`.\`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''} VIEW \`${view.schema}\`.\`${view.oldName}\` AS ${view.sql} ${view.updateOption ? `WITH ${view.updateOption} CHECK OPTION` : ''}
`; `;
if (view.name !== view.oldName) if (view.name !== view.oldName)
@ -1381,6 +1377,14 @@ export class MySQLClient extends AntaresCore {
xa: row.XA, xa: row.XA,
savepoints: row.Savepoints, savepoints: row.Savepoints,
isDefault: row.Support.includes('DEFAULT') isDefault: row.Support.includes('DEFAULT')
} as {
name: string;
support: string;
comment: string;
transactions: string;
xa: string;
savepoints: string;
isDefault: boolean;
}; };
}); });
} }
@ -1405,7 +1409,12 @@ export class MySQLClient extends AntaresCore {
break; break;
} }
return acc; return acc;
}, {}); }, {}) as {
number: string;
name: string;
arch: string;
os: string;
};
} }
async getProcesses () { async getProcesses () {
@ -1423,6 +1432,15 @@ export class MySQLClient extends AntaresCore {
time: row.TIME, time: row.TIME,
state: row.STATE, state: row.STATE,
info: row.INFO info: row.INFO
} as {
id: number;
user: string;
host: string;
db: string;
command: string;
time: number;
state: string;
info: string;
}; };
}); });
} }

View File

@ -4,7 +4,7 @@ import { builtinsTypes } from 'pg-types';
import * as pg from 'pg'; import * as pg from 'pg';
import * as pgAst from 'pgsql-ast-parser'; import * as pgAst from 'pgsql-ast-parser';
import { AntaresCore } from '../AntaresCore'; import { AntaresCore } from '../AntaresCore';
import * as dataTypes from 'common/data-types/postgresql'; import dataTypes from 'common/data-types/postgresql';
import SSH2Promise = require('ssh2-promise'); import SSH2Promise = require('ssh2-promise');
import SSHConfig from 'ssh2-promise/lib/sshConfig'; import SSHConfig from 'ssh2-promise/lib/sshConfig';

View File

@ -1,7 +1,7 @@
import * as antares from 'common/interfaces/antares'; import * as antares from 'common/interfaces/antares';
import * as sqlite from 'better-sqlite3'; import * as sqlite from 'better-sqlite3';
import { AntaresCore } from '../AntaresCore'; import { AntaresCore } from '../AntaresCore';
import * as dataTypes from 'common/data-types/sqlite'; import dataTypes from 'common/data-types/sqlite';
import { NUMBER, FLOAT, TIME, DATETIME } from 'common/fieldTypes'; import { NUMBER, FLOAT, TIME, DATETIME } from 'common/fieldTypes';
export class SQLiteClient extends AntaresCore { export class SQLiteClient extends AntaresCore {

View File

@ -2,7 +2,7 @@ import * as exporter from 'common/interfaces/exporter';
import * as mysql from 'mysql2/promise'; import * as mysql from 'mysql2/promise';
import { SqlExporter } from './SqlExporter'; import { SqlExporter } from './SqlExporter';
import { BLOB, BIT, DATE, DATETIME, FLOAT, SPATIAL, IS_MULTI_SPATIAL, NUMBER } from 'common/fieldTypes'; import { BLOB, BIT, DATE, DATETIME, FLOAT, SPATIAL, IS_MULTI_SPATIAL, NUMBER } from 'common/fieldTypes';
import hexToBinary from 'common/libs/hexToBinary'; import hexToBinary, { HexChar } from 'common/libs/hexToBinary';
import { getArrayDepth } from 'common/libs/getArrayDepth'; import { getArrayDepth } from 'common/libs/getArrayDepth';
import * as moment from 'moment'; import * as moment from 'moment';
import { lineString, point, polygon } from '@turf/helpers'; import { lineString, point, polygon } from '@turf/helpers';
@ -138,7 +138,7 @@ ${footer}
: this.escapeAndQuote(val); : this.escapeAndQuote(val);
} }
else if (BIT.includes(column.type)) else if (BIT.includes(column.type))
sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex'))}'`; sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex') as undefined as HexChar[])}'`;
else if (BLOB.includes(column.type)) else if (BLOB.includes(column.type))
sqlInsertString += `X'${val.toString('hex').toUpperCase()}'`; sqlInsertString += `X'${val.toString('hex').toUpperCase()}'`;
else if (NUMBER.includes(column.type)) else if (NUMBER.includes(column.type))

View File

@ -2,7 +2,7 @@ import * as antares from 'common/interfaces/antares';
import * as exporter from 'common/interfaces/exporter'; import * as exporter from 'common/interfaces/exporter';
import { SqlExporter } from './SqlExporter'; import { SqlExporter } from './SqlExporter';
import { BLOB, BIT, DATE, DATETIME, FLOAT, NUMBER, TEXT_SEARCH } from 'common/fieldTypes'; import { BLOB, BIT, DATE, DATETIME, FLOAT, NUMBER, TEXT_SEARCH } from 'common/fieldTypes';
import hexToBinary from 'common/libs/hexToBinary'; import hexToBinary, { HexChar } from 'common/libs/hexToBinary';
import * as moment from 'moment'; import * as moment from 'moment';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
@ -249,7 +249,7 @@ SET row_security = off;\n\n\n`;
else if (TEXT_SEARCH.includes(column.type)) else if (TEXT_SEARCH.includes(column.type))
sqlInsertString += `'${val.replaceAll('\'', '\'\'')}'`; sqlInsertString += `'${val.replaceAll('\'', '\'\'')}'`;
else if (BIT.includes(column.type)) else if (BIT.includes(column.type))
sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex'))}'`; sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex') as undefined as HexChar[])}'`;
else if (BLOB.includes(column.type)) else if (BLOB.includes(column.type))
sqlInsertString += `decode('${val.toString('hex').toUpperCase()}', 'hex')`; sqlInsertString += `decode('${val.toString('hex').toUpperCase()}', 'hex')`;
else if (NUMBER.includes(column.type)) else if (NUMBER.includes(column.type))

View File

@ -1,6 +1,6 @@
import * as pg from 'pg'; import * as pg from 'pg';
import * as importer from 'common/interfaces/importer'; import * as importer from 'common/interfaces/importer';
import fs from 'fs/promises'; import * as fs from 'fs/promises';
import PostgreSQLParser from '../../parsers/PostgreSQLParser'; import PostgreSQLParser from '../../parsers/PostgreSQLParser';
import { BaseImporter } from '../BaseImporter'; import { BaseImporter } from '../BaseImporter';

View File

@ -147,7 +147,7 @@ export default {
height: calc(100vh - #{$footer-height}); height: calc(100vh - #{$footer-height});
} }
.connection-panel-wrapper{ .connection-panel-wrapper {
height: calc(100vh - #{$excluding-size}); height: calc(100vh - #{$excluding-size});
width: 100%; width: 100%;
padding-top: 15vh; padding-top: 15vh;
@ -155,6 +155,6 @@ export default {
justify-content: center; justify-content: center;
align-items: flex-start; align-items: flex-start;
overflow: auto; overflow: auto;
} }
} }
</style> </style>

View File

@ -46,65 +46,58 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { computed, onBeforeUnmount, PropType, useSlots } from 'vue';
name: 'BaseConfirmModal',
props: {
size: {
type: String,
validator: prop => ['small', 'medium', '400', 'large'].includes(prop),
default: 'small'
},
hideFooter: {
type: Boolean,
default: false
},
confirmText: String,
cancelText: String
},
emits: ['confirm', 'hide'],
computed: {
hasHeader () {
return !!this.$slots.header;
},
hasBody () {
return !!this.$slots.body;
},
hasDefault () {
return !!this.$slots.default;
},
modalSizeClass () {
if (this.size === 'small')
return 'modal-sm';
if (this.size === '400')
return 'modal-400';
else if (this.size === 'large')
return 'modal-lg';
else return '';
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
confirmModal () {
this.$emit('confirm');
this.hideModal();
},
hideModal () { const props = defineProps({
this.$emit('hide'); size: {
}, type: String as PropType<'small' | 'medium' | '400' | 'large'>,
onKey (e) { validator: (prop: string) => ['small', 'medium', '400', 'large'].includes(prop),
e.stopPropagation(); default: 'small'
if (e.key === 'Escape') },
this.hideModal(); hideFooter: {
} type: Boolean,
} default: false
},
confirmText: String,
cancelText: String
});
const emit = defineEmits(['confirm', 'hide']);
const slots = useSlots();
const hasHeader = computed(() => !!slots.header);
const hasBody = computed(() => !!slots.body);
const hasDefault = computed(() => !!slots.default);
const modalSizeClass = computed(() => {
if (props.size === 'small')
return 'modal-sm';
if (props.size === '400')
return 'modal-400';
else if (props.size === 'large')
return 'modal-lg';
else return '';
});
const confirmModal = () => {
emit('confirm');
hideModal();
}; };
const hideModal = () => {
emit('hide');
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
hideModal();
};
window.addEventListener('keydown', onKey);
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style scoped> <style scoped>

View File

@ -15,67 +15,60 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { computed, onBeforeUnmount, onMounted, Ref, ref } from 'vue';
name: 'BaseContextMenu',
props: {
contextEvent: MouseEvent
},
emits: ['close-context'],
data () {
return {
contextSize: null,
isBottom: false
};
},
computed: {
position () {
let topCord = 0;
let leftCord = 0;
if (this.contextEvent) { const contextContent: Ref<HTMLDivElement> = ref(null);
const { clientY, clientX } = this.contextEvent; const contextSize: Ref<DOMRect> = ref(null);
topCord = `${clientY + 2}px`; const isBottom: Ref<boolean> = ref(false);
leftCord = `${clientX + 5}px`; const props = defineProps<{contextEvent: MouseEvent}>();
const emit = defineEmits(['close-context']);
if (this.contextSize) { const position = computed(() => {
if (clientY + (this.contextSize.height < 200 ? 200 : this.contextSize.height) + 5 >= window.innerHeight) { let topCord = '0px';
topCord = `${clientY + 3 - this.contextSize.height}px`; let leftCord = '0px';
this.isBottom = true;
}
if (clientX + this.contextSize.width + 5 >= window.innerWidth) if (props.contextEvent) {
leftCord = `${clientX - this.contextSize.width}px`; const { clientY, clientX } = props.contextEvent;
} topCord = `${clientY + 2}px`;
leftCord = `${clientX + 5}px`;
if (contextSize.value) {
if (clientY + (contextSize.value.height < 200 ? 200 : contextSize.value.height) + 5 >= window.innerHeight) {
topCord = `${clientY + 3 - contextSize.value.height}px`;
isBottom.value = true;
} }
return { if (clientX + contextSize.value.width + 5 >= window.innerWidth)
top: topCord, leftCord = `${clientX - contextSize.value.width}px`;
left: leftCord
};
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.$refs.contextContent)
this.contextSize = this.$refs.contextContent.getBoundingClientRect();
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
close () {
this.$emit('close-context');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.close();
} }
} }
return {
top: topCord,
left: leftCord
};
});
const close = () => {
emit('close-context');
}; };
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
close();
};
window.addEventListener('keydown', onKey);
onMounted(() => {
if (contextContent.value)
contextSize.value = contextContent.value.getBoundingClientRect();
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -4,11 +4,6 @@
</div> </div>
</template> </template>
<script>
export default {
name: 'BaseLoader'
};
</script>
<style scoped> <style scoped>
.empty { .empty {
position: absolute; position: absolute;

View File

@ -1,95 +1,93 @@
<template> <template>
<div id="map" class="map" /> <div id="map" class="map" />
</template> </template>
<script>
import L from 'leaflet'; <script setup lang="ts">
import { onMounted, PropType, Ref, ref } from 'vue';
import * as L from 'leaflet';
import { import {
point, point,
lineString, lineString,
polygon polygon
} from '@turf/helpers'; } from '@turf/helpers';
import { GeoJsonObject } from 'geojson';
import { getArrayDepth } from 'common/libs/getArrayDepth'; import { getArrayDepth } from 'common/libs/getArrayDepth';
export default { interface Coordinates { x: number; y: number }
name: 'BaseMap',
props: {
points: [Object, Array],
isMultiSpatial: Boolean
},
data () {
return {
map: null,
markers: [],
center: null
};
},
mounted () {
if (this.isMultiSpatial) {
for (const element of this.points)
this.markers.push(this.getMarkers(element));
}
else {
this.markers = this.getMarkers(this.points);
if (!Array.isArray(this.points)) const props = defineProps({
this.center = [this.points.y, this.points.x]; points: [Object, Array] as PropType<Coordinates | Coordinates[]>,
} isMultiSpatial: Boolean
});
const map: Ref<L.Map> = ref(null);
const markers: Ref<GeoJsonObject | GeoJsonObject[]> = ref(null);
const center: Ref<[number, number]> = ref(null);
this.map = L.map('map', { const getMarkers = (points: Coordinates) => {
center: this.center || [0, 0], if (Array.isArray(points)) {
zoom: 15, if (getArrayDepth(points) === 1)
minZoom: 1, return lineString(points.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []));
attributionControl: false else
}); return polygon(points.map(arr => arr.reduce((acc: Coordinates[], curr: Coordinates) => [...acc, [curr.x, curr.y]], [])));
L.control.attribution({ prefix: '<b>Leaflet</b>' }).addTo(this.map);
const geoJsonObj = L.geoJSON(this.markers, {
style: function () {
return {
weight: 2,
fillColor: '#ff7800',
color: '#ff7800',
opacity: 0.8,
fillOpacity: 0.4
};
},
pointToLayer: function (feature, latlng) {
return L.circleMarker(latlng, {
radius: 7,
weight: 2,
fillColor: '#ff7800',
color: '#ff7800',
opacity: 0.8,
fillOpacity: 0.4
});
}
}).addTo(this.map);
const southWest = L.latLng(-90, -180);
const northEast = L.latLng(90, 180);
const bounds = L.latLngBounds(southWest, northEast);
this.map.setMaxBounds(bounds);
if (!this.center) this.map.fitBounds(geoJsonObj.getBounds());
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <b>OpenStreetMap</b>'
}).addTo(this.map);
},
methods: {
getMarkers (points) {
if (Array.isArray(points)) {
if (getArrayDepth(points) === 1)
return lineString(points.reduce((acc, curr) => [...acc, [curr.x, curr.y]], []));
else
return polygon(points.map(arr => arr.reduce((acc, curr) => [...acc, [curr.x, curr.y]], [])));
}
else
return point([points.x, points.y]);
}
} }
else
return point([points.x, points.y]);
}; };
onMounted(() => {
if (props.isMultiSpatial) {
for (const element of props.points as Coordinates[])
(markers.value as GeoJsonObject[]).push(getMarkers(element));
}
else {
markers.value = getMarkers(props.points as Coordinates);
if (!Array.isArray(props.points))
center.value = [props.points.y, props.points.x];
}
map.value = L.map('map', {
center: center.value || [0, 0],
zoom: 15,
minZoom: 1,
attributionControl: false
});
L.control.attribution({ prefix: '<b>Leaflet</b>' }).addTo(map.value);
const geoJsonObj = L.geoJSON((markers.value as GeoJsonObject), {
style: function () {
return {
weight: 2,
fillColor: '#ff7800',
color: '#ff7800',
opacity: 0.8,
fillOpacity: 0.4
};
},
pointToLayer: function (feature, latlng) {
return L.circleMarker(latlng, {
radius: 7,
weight: 2,
fillColor: '#ff7800',
color: '#ff7800',
opacity: 0.8,
fillOpacity: 0.4
});
}
}).addTo(map.value);
const southWest = L.latLng(-90, -180);
const northEast = L.latLng(90, 180);
const bounds = L.latLngBounds(southWest, northEast);
map.value.setMaxBounds(bounds);
if (!center.value) map.value.fitBounds(geoJsonObj.getBounds());
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <b>OpenStreetMap</b>'
}).addTo(map.value);
});
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -14,64 +14,58 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { computed, ref } from 'vue';
name: 'BaseNotification',
props: {
message: {
type: String,
default: ''
},
status: {
type: String,
default: ''
}
},
emits: ['close'],
data () {
return {
isExpanded: false
};
},
computed: {
notificationStatus () {
let className = '';
let iconName = '';
switch (this.status) {
case 'success':
className = 'toast-success';
iconName = 'mdi-check';
break;
case 'error':
className = 'toast-error';
iconName = 'mdi-alert-rhombus';
break;
case 'warning':
className = 'toast-warning';
iconName = 'mdi-alert';
break;
case 'primary':
className = 'toast-primary';
iconName = 'mdi-information-outline';
break;
}
return { className, iconName }; const props = defineProps({
}, message: {
isExpandable () { type: String,
return this.message.length > 80; default: ''
}
}, },
methods: { status: {
hideToast () { type: String,
this.$emit('close'); default: ''
},
toggleExpand () {
this.isExpanded = !this.isExpanded;
}
} }
});
const isExpanded = ref(false);
const emit = defineEmits(['close']);
const notificationStatus = computed(() => {
let className = '';
let iconName = '';
switch (props.status) {
case 'success':
className = 'toast-success';
iconName = 'mdi-check';
break;
case 'error':
className = 'toast-error';
iconName = 'mdi-alert-rhombus';
break;
case 'warning':
className = 'toast-warning';
iconName = 'mdi-alert';
break;
case 'primary':
className = 'toast-primary';
iconName = 'mdi-information-outline';
break;
}
return { className, iconName };
});
const isExpandable = computed(() => props.message.length > 80);
const hideToast = () => {
emit('close');
};
const toggleExpand = () => {
isExpanded.value = !isExpanded.value;
}; };
</script> </script>
<style scoped> <style scoped>
.toast { .toast {
display: flex; display: flex;

View File

@ -143,7 +143,7 @@ export default defineComponent({
const hightlightedIndex = ref(0); const hightlightedIndex = ref(0);
const isOpen = ref(false); const isOpen = ref(false);
const isMouseDown = ref(false); const isMouseDown = ref(false);
const internalValue = ref(props.modelValue || props.value); const internalValue = ref(props.modelValue !== false ? props.modelValue : props.value);
const el = ref(null); const el = ref(null);
const searchInput = ref(null); const searchInput = ref(null);
const optionList = ref(null); const optionList = ref(null);
@ -403,7 +403,8 @@ export default defineComponent({
optionList, optionList,
optionRefs, optionRefs,
handleBlurEvent, handleBlurEvent,
handleMouseUpEvent handleMouseUpEvent,
internalValue
}; };
} }
}); });

View File

@ -9,121 +9,111 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { onMounted, watch } from 'vue';
import * as ace from 'ace-builds'; import * as ace from 'ace-builds';
import { storeToRefs } from 'pinia';
import 'ace-builds/webpack-resolver'; import 'ace-builds/webpack-resolver';
import { storeToRefs } from 'pinia';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
export default { const props = defineProps({
name: 'BaseTextEditor', modelValue: String,
props: { mode: { type: String, default: 'text' },
modelValue: String, editorClass: { type: String, default: '' },
mode: { type: String, default: 'text' }, autoFocus: { type: Boolean, default: false },
editorClass: { type: String, default: '' }, readOnly: { type: Boolean, default: false },
autoFocus: { type: Boolean, default: false }, showLineNumbers: { type: Boolean, default: true },
readOnly: { type: Boolean, default: false }, height: { type: Number, default: 200 }
showLineNumbers: { type: Boolean, default: true }, });
height: { type: Number, default: 200 } const emit = defineEmits(['update:modelValue']);
}, const settingsStore = useSettingsStore();
emits: ['update:modelValue'],
setup () {
const settingsStore = useSettingsStore();
const { const {
editorTheme, editorTheme,
editorFontSize, editorFontSize,
autoComplete, autoComplete,
lineWrap lineWrap
} = storeToRefs(settingsStore); } = storeToRefs(settingsStore);
return { let editor: ace.Ace.Editor;
editorTheme, const id = uidGen();
editorFontSize,
autoComplete,
lineWrap
};
},
data () {
return {
editor: null,
id: uidGen()
};
},
watch: {
mode () {
if (this.editor)
this.editor.session.setMode(`ace/mode/${this.mode}`);
},
editorTheme () {
if (this.editor)
this.editor.setTheme(`ace/theme/${this.editorTheme}`);
},
editorFontSize () {
const sizes = {
small: '12px',
medium: '14px',
large: '16px'
};
if (this.editor) { watch(() => props.mode, () => {
this.editor.setOptions({ if (editor)
fontSize: sizes[this.editorFontSize] editor.session.setMode(`ace/mode/${props.mode}`);
}); });
}
}, watch(editorTheme, () => {
autoComplete () { if (editor)
if (this.editor) { editor.setTheme(`ace/theme/${editorTheme.value}`);
this.editor.setOptions({ });
enableLiveAutocompletion: this.autoComplete
}); watch(editorFontSize, () => {
} const sizes = {
}, small: 12,
lineWrap () { medium: 14,
if (this.editor) { large: 16
this.editor.setOptions({ };
wrap: this.lineWrap
}); if (editor) {
} editor.setOptions({
} fontSize: sizes[editorFontSize.value as undefined as 'small' | 'medium' | 'large']
},
mounted () {
this.editor = ace.edit(`editor-${this.id}`, {
mode: `ace/mode/${this.mode}`,
theme: `ace/theme/${this.editorTheme}`,
value: this.modelValue || '',
fontSize: '14px',
printMargin: false,
readOnly: this.readOnly,
showLineNumbers: this.showLineNumbers,
showGutter: this.showLineNumbers
}); });
}
});
this.editor.setOptions({ watch(autoComplete, () => {
enableBasicAutocompletion: false, if (editor) {
wrap: this.lineWrap, editor.setOptions({
enableSnippets: false, enableLiveAutocompletion: autoComplete.value
enableLiveAutocompletion: false
}); });
}
});
this.editor.session.on('change', () => { watch(lineWrap, () => {
const content = this.editor.getValue(); if (editor) {
this.$emit('update:modelValue', content); editor.setOptions({
wrap: lineWrap.value
}); });
}
});
if (this.autoFocus) { onMounted(() => {
setTimeout(() => { editor = ace.edit(`editor-${id}`, {
this.editor.focus(); mode: `ace/mode/${props.mode}`,
this.editor.resize(); theme: `ace/theme/${editorTheme.value}`,
}, 20); value: props.modelValue || '',
} fontSize: 14,
printMargin: false,
readOnly: props.readOnly,
showLineNumbers: props.showLineNumbers,
showGutter: props.showLineNumbers
});
editor.setOptions({
enableBasicAutocompletion: false,
wrap: lineWrap,
enableSnippets: false,
enableLiveAutocompletion: false
});
(editor.session as unknown as ace.Ace.Editor).on('change', () => {
const content = editor.getValue();
emit('update:modelValue', content);
});
if (props.autoFocus) {
setTimeout(() => { setTimeout(() => {
this.editor.resize(); editor.focus();
editor.resize();
}, 20); }, 20);
} }
};
setTimeout(() => {
editor.resize();
}, 20);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -9,67 +9,63 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { computed, ref, watch } from 'vue';
name: 'BaseToast',
props: {
message: {
type: String,
default: ''
},
status: {
type: String,
default: ''
}
},
emits: ['close'],
data () {
return {
isVisible: false
};
},
computed: {
toastStatus () {
let className = '';
let iconName = '';
switch (this.status) {
case 'success':
className = 'toast-success';
iconName = 'mdi-check';
break;
case 'error':
className = 'toast-error';
iconName = 'mdi-alert-rhombus';
break;
case 'warning':
className = 'toast-warning';
iconName = 'mdi-alert';
break;
case 'primary':
className = 'toast-primary';
iconName = 'mdi-information-outline';
break;
}
return { className, iconName }; const props = defineProps({
} message: {
type: String,
default: ''
}, },
watch: { status: {
message: function () { type: String,
if (this.message) default: ''
this.isVisible = true;
else
this.isVisible = false;
}
},
methods: {
hideToast () {
this.isVisible = false;
this.$emit('close');
}
} }
});
const isVisible = ref(false);
const message = ref(props.message);
const emit = defineEmits(['close']);
const toastStatus = computed(() => {
let className = '';
let iconName = '';
switch (props.status) {
case 'success':
className = 'toast-success';
iconName = 'mdi-check';
break;
case 'error':
className = 'toast-error';
iconName = 'mdi-alert-rhombus';
break;
case 'warning':
className = 'toast-warning';
iconName = 'mdi-alert';
break;
case 'primary':
className = 'toast-primary';
iconName = 'mdi-information-outline';
break;
}
return { className, iconName };
});
watch(message, () => {
if (message.value)
isVisible.value = true;
else
isVisible.value = false;
});
const hideToast = () => {
isVisible.value = false;
emit('close');
}; };
</script> </script>
<style scoped> <style scoped>
.toast { .toast {
display: flex; display: flex;

View File

@ -7,7 +7,7 @@
{{ lastPart(modelValue) }} {{ lastPart(modelValue) }}
</span> </span>
<i <i
v-if="modelValue.length" v-if="modelValue"
class="file-uploader-reset mdi mdi-close" class="file-uploader-reset mdi mdi-close"
@click.prevent="clear" @click.prevent="clear"
/> />
@ -22,40 +22,35 @@
</label> </label>
</template> </template>
<script> <script setup lang="ts">
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
export default { defineProps({
name: 'BaseUploadInput', message: {
props: { default: 'Browse',
message: { type: String
default: 'Browse',
type: String
},
modelValue: {
default: '',
type: String
}
}, },
emits: ['change', 'clear'], modelValue: {
data () { default: '',
return { type: String
id: uidGen()
};
},
methods: {
clear () {
this.$emit('clear');
},
lastPart (string) {
if (!string) return '';
string = string.split(/[/\\]+/).pop();
if (string.length >= 19)
string = `...${string.slice(-19)}`;
return string;
}
} }
});
const emit = defineEmits(['change', 'clear']);
const id = uidGen();
const clear = () => {
emit('clear');
};
const lastPart = (string: string) => {
if (!string) return '';
string = string.split(/[/\\]+/).pop();
if (string.length >= 19)
string = `...${string.slice(-19)}`;
return string;
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="vscroll-holder"> <div ref="root" class="vscroll-holder">
<div <div
class="vscroll-spacer" class="vscroll-spacer"
:style="{ :style="{
@ -20,71 +20,76 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { onBeforeUnmount, onMounted, Ref, ref, watch } from 'vue';
name: 'BaseVirtualScroll',
props: {
items: Array,
itemHeight: Number,
visibleHeight: Number,
scrollElement: {
type: HTMLDivElement,
default: null
}
},
data () {
return {
topHeight: 0,
bottomHeight: 0,
visibleItems: [],
renderTimeout: null,
localScrollElement: null
};
},
watch: {
scrollElement () {
this.setScrollElement();
}
},
mounted () {
this.setScrollElement();
},
beforeUnmount () {
this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition);
},
methods: {
checkScrollPosition (e) {
clearTimeout(this.renderTimeout);
this.renderTimeout = setTimeout(() => { const props = defineProps({
this.updateWindow(e); items: Array,
}, 200); itemHeight: Number,
}, visibleHeight: Number,
updateWindow () { scrollElement: {
const visibleItemsCount = Math.ceil(this.visibleHeight / this.itemHeight); type: HTMLDivElement,
const totalScrollHeight = this.items.length * this.itemHeight; default: null
const offset = 50;
const scrollTop = this.localScrollElement.scrollTop;
const firstVisibleIndex = Math.floor(scrollTop / this.itemHeight);
const lastVisibleIndex = firstVisibleIndex + visibleItemsCount;
const firstCutIndex = Math.max(firstVisibleIndex - offset, 0);
const lastCutIndex = lastVisibleIndex + offset;
this.visibleItems = this.items.slice(firstCutIndex, lastCutIndex);
this.topHeight = firstCutIndex * this.itemHeight;
this.bottomHeight = totalScrollHeight - this.visibleItems.length * this.itemHeight - this.topHeight;
},
setScrollElement () {
if (this.localScrollElement)
this.localScrollElement.removeEventListener('scroll', this.checkScrollPosition);
this.localScrollElement = this.scrollElement ? this.scrollElement : this.$el;
this.updateWindow();
this.localScrollElement.addEventListener('scroll', this.checkScrollPosition);
}
} }
});
const root = ref(null);
const topHeight: Ref<number> = ref(0);
const bottomHeight: Ref<number> = ref(0);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const visibleItems: Ref<any[]> = ref([]);
const renderTimeout: Ref<NodeJS.Timeout> = ref(null);
const localScrollElement: Ref<HTMLDivElement> = ref(null);
const checkScrollPosition = () => {
clearTimeout(renderTimeout.value);
renderTimeout.value = setTimeout(() => {
updateWindow();
}, 200);
}; };
const updateWindow = () => {
const visibleItemsCount = Math.ceil(props.visibleHeight / props.itemHeight);
const totalScrollHeight = props.items.length * props.itemHeight;
const offset = 50;
const scrollTop = localScrollElement.value.scrollTop;
const firstVisibleIndex = Math.floor(scrollTop / props.itemHeight);
const lastVisibleIndex = firstVisibleIndex + visibleItemsCount;
const firstCutIndex = Math.max(firstVisibleIndex - offset, 0);
const lastCutIndex = lastVisibleIndex + offset;
visibleItems.value = props.items.slice(firstCutIndex, lastCutIndex);
topHeight.value = firstCutIndex * props.itemHeight;
bottomHeight.value = totalScrollHeight - visibleItems.value.length * props.itemHeight - topHeight.value;
};
const setScrollElement = () => {
if (localScrollElement.value)
localScrollElement.value.removeEventListener('scroll', checkScrollPosition);
localScrollElement.value = props.scrollElement ? props.scrollElement : root.value;
updateWindow();
localScrollElement.value.addEventListener('scroll', checkScrollPosition);
};
watch(() => props.scrollElement, () => {
setScrollElement();
});
onMounted(() => {
setScrollElement();
});
onBeforeUnmount(() => {
localScrollElement.value.removeEventListener('scroll', checkScrollPosition);
});
defineExpose({
updateWindow
});
</script> </script>

View File

@ -4,7 +4,7 @@
v-model="selectedGroup" v-model="selectedGroup"
class="form-select" class="form-select"
:options="[{name: 'manual'}, ...fakerGroups]" :options="[{name: 'manual'}, ...fakerGroups]"
:option-label="(opt) => opt.name === 'manual' ? $t('message.manualValue') : $t(`faker.${opt.name}`)" :option-label="(opt: any) => opt.name === 'manual' ? $t('message.manualValue') : $t(`faker.${opt.name}`)"
option-track-by="name" option-track-by="name"
:disabled="!isChecked" :disabled="!isChecked"
style="flex-grow: 0;" style="flex-grow: 0;"
@ -15,7 +15,7 @@
v-if="selectedGroup !== 'manual'" v-if="selectedGroup !== 'manual'"
v-model="selectedMethod" v-model="selectedMethod"
:options="fakerMethods" :options="fakerMethods"
:option-label="(opt) => $t(`faker.${opt.name}`)" :option-label="(opt: any) => $t(`faker.${opt.name}`)"
option-track-by="name" option-track-by="name"
class="form-select" class="form-select"
:disabled="!isChecked" :disabled="!isChecked"
@ -85,153 +85,149 @@
</fieldset> </fieldset>
</template> </template>
<script> <script setup lang="ts">
import { computed, PropType, Ref, ref, watch } from 'vue';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import BaseUploadInput from '@/components/BaseUploadInput'; import BaseUploadInput from '@/components/BaseUploadInput.vue';
import ForeignKeySelect from '@/components/ForeignKeySelect'; import ForeignKeySelect from '@/components/ForeignKeySelect.vue';
import FakerMethods from 'common/FakerMethods'; import FakerMethods from 'common/FakerMethods';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
export default { const props = defineProps({
name: 'FakerSelect', type: String,
components: { field: Object,
ForeignKeySelect, isChecked: Boolean,
BaseUploadInput, foreignKeys: Array,
BaseSelect keyUsage: Array as PropType<{field: string}[]>,
}, fieldLength: Number,
props: { fieldObj: Object
type: String, });
field: Object, const emit = defineEmits(['update:modelValue']);
isChecked: Boolean,
foreignKeys: Array,
keyUsage: Array,
fieldLength: Number,
fieldObj: Object
},
emits: ['update:modelValue'],
data () {
return {
localType: null,
selectedGroup: 'manual',
selectedMethod: '',
selectedValue: '',
debounceTimeout: null,
methodParams: {},
enumArray: null
};
},
computed: {
fakerGroups () {
if ([...TEXT, ...LONG_TEXT].includes(this.type))
this.localType = 'string';
else if (NUMBER.includes(this.type))
this.localType = 'number';
else if (FLOAT.includes(this.type))
this.localType = 'float';
else if ([...DATE, ...DATETIME].includes(this.type))
this.localType = 'datetime';
else if (TIME.includes(this.type))
this.localType = 'time';
else
this.localType = 'none';
return FakerMethods.getGroupsByType(this.localType); const localType: Ref<string> = ref(null);
}, const selectedGroup: Ref<string> = ref('manual');
fakerMethods () { const selectedMethod: Ref<string> = ref('');
return FakerMethods.getMethods({ type: this.localType, group: this.selectedGroup }); const selectedValue: Ref<string> = ref('');
}, const debounceTimeout: Ref<NodeJS.Timeout> = ref(null);
methodData () { const methodParams: Ref<{[key: string]: string}> = ref({});
return this.fakerMethods.find(method => method.name === this.selectedMethod); const enumArray: Ref<string[]> = ref(null);
}
},
watch: {
fieldObj () {
if (this.fieldObj) {
if (Array.isArray(this.fieldObj.value)) {
this.enumArray = this.fieldObj.value;
this.selectedValue = this.fieldObj.value[0];
}
else
this.selectedValue = this.fieldObj.value;
}
},
selectedGroup () {
if (this.fakerMethods.length)
this.selectedMethod = this.fakerMethods[0].name;
else
this.selectedMethod = '';
},
selectedMethod () {
this.onChange();
},
selectedValue () {
clearTimeout(this.debounceTimeout);
this.debounceTimeout = null;
this.debounceTimeout = setTimeout(() => {
this.onChange();
}, 200);
}
},
methods: {
inputProps () {
if ([...TEXT, ...LONG_TEXT].includes(this.type))
return { type: 'text', mask: false };
if ([...NUMBER, ...FLOAT].includes(this.type)) const fakerGroups = computed(() => {
return { type: 'number', mask: false }; if ([...TEXT, ...LONG_TEXT].includes(props.type))
localType.value = 'string';
else if (NUMBER.includes(props.type))
localType.value = 'number';
else if (FLOAT.includes(props.type))
localType.value = 'float';
else if ([...DATE, ...DATETIME].includes(props.type))
localType.value = 'datetime';
else if (TIME.includes(props.type))
localType.value = 'time';
else
localType.value = 'none';
if (TIME.includes(this.type)) { return FakerMethods.getGroupsByType(localType.value);
let timeMask = '##:##:##'; });
const precision = this.fieldLength;
for (let i = 0; i < precision; i++) const fakerMethods = computed(() => {
timeMask += i === 0 ? '.#' : '#'; return FakerMethods.getMethods({ type: localType.value, group: selectedGroup.value });
});
return { type: 'text', mask: timeMask }; const methodData = computed(() => {
} return fakerMethods.value.find(method => method.name === selectedMethod.value);
});
if (DATE.includes(this.type)) const inputProps = () => {
return { type: 'text', mask: '####-##-##' }; if ([...TEXT, ...LONG_TEXT].includes(props.type))
return { type: 'text', mask: false };
if (DATETIME.includes(this.type)) { if ([...NUMBER, ...FLOAT].includes(props.type))
let datetimeMask = '####-##-## ##:##:##'; return { type: 'number', mask: false };
const precision = this.fieldLength;
for (let i = 0; i < precision; i++) if (TIME.includes(props.type)) {
datetimeMask += i === 0 ? '.#' : '#'; let timeMask = '##:##:##';
const precision = props.fieldLength;
return { type: 'text', mask: datetimeMask }; for (let i = 0; i < precision; i++)
} timeMask += i === 0 ? '.#' : '#';
if (BLOB.includes(this.type)) return { type: 'text', mask: timeMask };
return { type: 'file', mask: false };
if (BIT.includes(this.type))
return { type: 'text', mask: false };
return { type: 'text', mask: false };
},
getKeyUsage (keyName) {
return this.keyUsage.find(key => key.field === keyName);
},
filesChange (event) {
const { files } = event.target;
if (!files.length) return;
this.selectedValue = files[0].path;
},
clearValue () {
this.selectedValue = '';
},
onChange () {
this.$emit('update:modelValue', {
group: this.selectedGroup,
method: this.selectedMethod,
params: this.methodParams,
value: this.selectedValue,
length: this.fieldLength
});
}
} }
if (DATE.includes(props.type))
return { type: 'text', mask: '####-##-##' };
if (DATETIME.includes(props.type)) {
let datetimeMask = '####-##-## ##:##:##';
const precision = props.fieldLength;
for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#';
return { type: 'text', mask: datetimeMask };
}
if (BLOB.includes(props.type))
return { type: 'file', mask: false };
if (BIT.includes(props.type))
return { type: 'text', mask: false };
return { type: 'text', mask: false };
}; };
const getKeyUsage = (keyName: string) => {
return props.keyUsage.find(key => key.field === keyName);
};
const filesChange = ({ target } : {target: HTMLInputElement }) => {
const { files } = target;
if (!files.length) return;
selectedValue.value = files[0].path;
};
const clearValue = () => {
selectedValue.value = '';
};
const onChange = () => {
emit('update:modelValue', {
group: selectedGroup.value,
method: selectedMethod.value,
params: methodParams.value,
value: selectedValue.value,
length: props.fieldLength
});
};
watch(() => props.fieldObj, () => {
if (props.fieldObj) {
if (Array.isArray(props.fieldObj.value)) {
enumArray.value = props.fieldObj.value;
selectedValue.value = props.fieldObj.value[0];
}
else
selectedValue.value = props.fieldObj.value;
}
});
watch(selectedGroup, () => {
if (fakerMethods.value.length)
selectedMethod.value = fakerMethods.value[0].name;
else
selectedMethod.value = '';
});
watch(selectedMethod, () => {
onChange();
});
watch(selectedValue, () => {
clearTimeout(debounceTimeout.value);
debounceTimeout.value = null;
debounceTimeout.value = setTimeout(() => {
onChange();
}, 200);
});
</script> </script>

View File

@ -8,107 +8,100 @@
dropdown-class="select-sm" dropdown-class="select-sm"
dropdown-container=".workspace-query-results > .vscroll" dropdown-container=".workspace-query-results > .vscroll"
@change="onChange" @change="onChange"
@blur="$emit('blur')" @blur="emit('blur')"
/> />
</template> </template>
<script> <script setup lang="ts">
import { computed, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { TEXT, LONG_TEXT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT } from 'common/fieldTypes';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { TableField } from 'common/interfaces/antares';
export default { const props = defineProps({
name: 'ForeignKeySelect', modelValue: [String, Number],
components: { BaseSelect }, keyUsage: Object,
props: { size: {
modelValue: [String, Number], type: String,
keyUsage: Object, default: ''
size: {
type: String,
default: ''
}
},
emits: ['update:modelValue', 'blur'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
return { addNotification, selectedWorkspace };
},
data () {
return {
foreignList: [],
currentValue: this.modelValue || null
};
},
computed: {
isValidDefault () {
if (!this.foreignList.length) return true;
if (this.modelValue === null) return false;
return this.foreignList.some(foreign => foreign.foreign_column.toString() === this.modelValue.toString());
},
foreigns () {
const list = [];
if (!this.isValidDefault)
list.push({ value: this.modelValue, label: this.modelValue === null ? 'NULL' : this.modelValue });
for (const row of this.foreignList)
list.push({ value: row.foreign_column, label: `${row.foreign_column} ${this.cutText('foreign_description' in row ? ` - ${row.foreign_description}` : '')}` });
return list;
}
},
async created () {
let foreignDesc;
const params = {
uid: this.selectedWorkspace,
schema: this.keyUsage.refSchema,
table: this.keyUsage.refTable
};
try { // Field data
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') {
const textField = response.find(field => [...TEXT, ...LONG_TEXT].includes(field.type) && field.name !== this.keyUsage.refField);
foreignDesc = textField ? textField.name : false;
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
try { // Foregn list
const { status, response } = await Tables.getForeignList({
...params,
column: this.keyUsage.refField,
description: foreignDesc
});
if (status === 'success')
this.foreignList = response.rows;
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
methods: {
onChange (opt) {
this.$emit('update:modelValue', opt.value);
},
cutText (val) {
if (typeof val !== 'string') return val;
return val.length > 15 ? `${val.substring(0, 15)}...` : val;
}
} }
});
const emit = defineEmits(['update:modelValue', 'blur']);
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const editField: Ref<HTMLSelectElement> = ref(null);
const foreignList = ref([]);
const currentValue = ref(props.modelValue);
const isValidDefault = computed(() => {
if (!foreignList.value.length) return true;
if (props.modelValue === null) return false;
return foreignList.value.some(foreign => foreign.foreign_column.toString() === props.modelValue.toString());
});
const foreigns = computed(() => {
const list = [];
if (!isValidDefault.value)
list.push({ value: props.modelValue, label: props.modelValue === null ? 'NULL' : props.modelValue });
for (const row of foreignList.value)
list.push({ value: row.foreign_column, label: `${row.foreign_column} ${cutText('foreign_description' in row ? ` - ${row.foreign_description}` : '')}` });
return list;
});
const onChange = (opt: HTMLSelectElement) => {
emit('update:modelValue', opt.value);
}; };
const cutText = (val: string) => {
if (typeof val !== 'string') return val;
return val.length > 15 ? `${val.substring(0, 15)}...` : val;
};
let foreignDesc: string | false;
const params = {
uid: selectedWorkspace.value,
schema: props.keyUsage.refSchema,
table: props.keyUsage.refTable
};
(async () => {
try { // Field data
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') {
const textField = (response as TableField[]).find((field: {type: string; name: string}) => [...TEXT, ...LONG_TEXT].includes(field.type) && field.name !== props.keyUsage.refField);
foreignDesc = textField ? textField.name : false;
}
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
try { // Foregn list
const { status, response } = await Tables.getForeignList({
...params,
column: props.keyUsage.refField,
description: foreignDesc
});
if (status === 'success')
foreignList.value = response.rows;
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
})();
</script> </script>

View File

@ -55,30 +55,25 @@
</Teleport> </Teleport>
</template> </template>
<script> <script setup lang="ts">
export default { import { Ref, ref } from 'vue';
name: 'ModalAskCredentials',
emits: ['close-asking', 'credentials'], const credentials = ref({
data () { user: '',
return { password: ''
credentials: { });
user: '', const firstInput: Ref<HTMLInputElement> = ref(null);
password: '' const emit = defineEmits(['close-asking', 'credentials']);
}
}; const closeModal = () => {
}, emit('close-asking');
created () {
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
methods: {
closeModal () {
this.$emit('close-asking');
},
sendCredentials () {
this.$emit('credentials', this.credentials);
}
}
}; };
const sendCredentials = () => {
emit('credentials', credentials.value);
};
setTimeout(() => {
firstInput.value.focus();
}, 20);
</script> </script>

View File

@ -47,83 +47,75 @@
</ConfirmModal> </ConfirmModal>
</template> </template>
<script> <script setup lang="ts">
import { computed, PropType, Ref, ref } from 'vue';
import { NUMBER, FLOAT } from 'common/fieldTypes'; import { NUMBER, FLOAT } from 'common/fieldTypes';
import ConfirmModal from '@/components/BaseConfirmModal'; import { FunctionInfos, RoutineInfos } from 'common/interfaces/antares';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
export default { const props = defineProps({
name: 'ModalAskParameters', localRoutine: Object as PropType<RoutineInfos | FunctionInfos>,
components: { client: String
ConfirmModal });
},
props: {
localRoutine: Object,
client: String
},
emits: ['confirm', 'close'],
data () {
return {
values: {}
};
},
computed: {
inParameters () {
return this.localRoutine.parameters.filter(param => param.context === 'IN');
}
},
created () {
window.addEventListener('keydown', this.onKey);
setTimeout(() => { const emit = defineEmits(['confirm', 'close']);
this.$refs.firstInput[0].focus();
}, 20);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
runRoutine () {
const valArr = Object.keys(this.values).reduce((acc, curr, i) => {
let qc;
switch (this.client) {
case 'maria':
case 'mysql':
qc = '"';
break;
case 'pg':
qc = '\'';
break;
default:
qc = '"';
}
const param = this.localRoutine.parameters.find(param => `${i}-${param.name}` === curr); const firstInput: Ref<HTMLInputElement[]> = ref(null);
const values: Ref<{[key: string]: string}> = ref({});
const value = [...NUMBER, ...FLOAT].includes(param.type) ? this.values[curr] : `${qc}${this.values[curr]}${qc}`; const inParameters = computed(() => {
acc.push(value); return props.localRoutine.parameters.filter(param => param.context === 'IN');
return acc; });
}, []);
this.$emit('confirm', valArr); const typeClass = (type: string) => {
}, if (type)
closeModal () { return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
this.$emit('close'); return '';
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
}
}; };
const runRoutine = () => {
const valArr = Object.keys(values.value).reduce((acc, curr, i) => {
let qc;
switch (props.client) {
case 'maria':
case 'mysql':
qc = '"';
break;
case 'pg':
qc = '\'';
break;
default:
qc = '"';
}
const param = props.localRoutine.parameters.find(param => `${i}-${param.name}` === curr);
const value = [...NUMBER, ...FLOAT].includes(param.type) ? values.value[curr] : `${qc}${values.value[curr]}${qc}`;
acc.push(value);
return acc;
}, []);
emit('confirm', valArr);
};
const closeModal = () => emit('close');
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
const wrapNumber = (num: number) => {
if (!num) return '';
return `(${num})`;
};
window.addEventListener('keydown', onKey);
setTimeout(() => {
firstInput.value[0].focus();
}, 20);
</script> </script>
<style scoped> <style scoped>

View File

@ -2,8 +2,8 @@
<ConfirmModal <ConfirmModal
:confirm-text="$t('word.discard')" :confirm-text="$t('word.discard')"
:cancel-text="$t('word.stay')" :cancel-text="$t('word.stay')"
@confirm="$emit('confirm')" @confirm="emit('confirm')"
@hide="$emit('close')" @hide="emit('close')"
> >
<template #header> <template #header>
<div class="d-flex"> <div class="d-flex">
@ -18,29 +18,23 @@
</ConfirmModal> </ConfirmModal>
</template> </template>
<script> <script setup lang="ts">
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import { onBeforeUnmount } from 'vue';
export default { const emit = defineEmits(['confirm', 'close']);
name: 'ModalDiscardChanges',
components: { const onKey = (e: KeyboardEvent) => {
ConfirmModal e.stopPropagation();
}, if (e.key === 'Escape')
emits: ['confirm', 'close'], emit('close');
created () {
window.addEventListener('keydown', this.onKey);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
}
}; };
window.addEventListener('keydown', onKey);
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style scoped> <style scoped>

View File

@ -62,116 +62,99 @@
</Teleport> </Teleport>
</template> </template>
<script> <script setup lang="ts">
import { computed, onBeforeUnmount, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
export default { const props = defineProps({
name: 'ModalEditSchema', selectedSchema: String
components: { });
BaseSelect
},
props: {
selectedSchema: String
},
emits: ['close'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const emit = defineEmits(['close']);
const { getWorkspace, getDatabaseVariable } = workspacesStore; const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
return { const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
addNotification,
selectedWorkspace, const { getWorkspace, getDatabaseVariable } = workspacesStore;
getWorkspace,
getDatabaseVariable const firstInput: Ref<HTMLInputElement> = ref(null);
}; const database = ref({
}, name: '',
data () { prevName: '',
return { collation: '',
database: { prevCollation: null
name: '', });
prevName: '',
collation: '' const collations = computed(() => getWorkspace(selectedWorkspace.value).collations);
} const defaultCollation = computed(() => (getDatabaseVariable(selectedWorkspace.value, 'collation_server').value || ''));
};
}, const updateSchema = async () => {
computed: { if (database.value.collation !== database.value.prevCollation) {
collations () {
return this.getWorkspace(this.selectedWorkspace).collations;
},
defaultCollation () {
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
}
},
async created () {
let actualCollation;
try { try {
const { status, response } = await Schema.getDatabaseCollation({ uid: this.selectedWorkspace, database: this.selectedSchema }); const { status, response } = await Schema.updateSchema({
uid: selectedWorkspace.value,
...database.value
});
if (status === 'success') if (status === 'success')
actualCollation = response; closeModal();
else else
this.addNotification({ status: 'error', message: response }); addNotification({ status: 'error', message: response });
} }
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); addNotification({ status: 'error', message: err.stack });
}
this.database = {
name: this.selectedSchema,
prevName: this.selectedSchema,
collation: actualCollation || this.defaultCollation,
prevCollation: actualCollation || this.defaultCollation
};
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async updateSchema () {
if (this.database.collation !== this.database.prevCollation) {
try {
const { status, response } = await Schema.updateSchema({
uid: this.selectedWorkspace,
...this.database
});
if (status === 'success')
this.closeModal();
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
}
else
this.closeModal();
},
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
} }
} }
else closeModal();
}; };
const closeModal = () => emit('close');
const onKey =(e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
(async () => {
let actualCollation;
try {
const { status, response } = await Schema.getDatabaseCollation({ uid: selectedWorkspace.value, database: props.selectedSchema });
if (status === 'success')
actualCollation = response;
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
database.value = {
name: props.selectedSchema,
prevName: props.selectedSchema,
collation: actualCollation || defaultCollation.value,
prevCollation: actualCollation || defaultCollation.value
};
window.addEventListener('keydown', onKey);
setTimeout(() => {
firstInput.value.focus();
}, 20);
})();
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style scoped> <style scoped>

View File

@ -146,7 +146,7 @@
<div class="tbody"> <div class="tbody">
<div <div
v-for="item in tables" v-for="item in tables"
:key="item.name" :key="item.table"
class="tr" class="tr"
> >
<div class="td"> <div class="td">
@ -193,7 +193,7 @@
> >
<input v-model="options.includes[key]" type="checkbox"><i class="form-icon" /> {{ $tc(`word.${key}`, 2) }} <input v-model="options.includes[key]" type="checkbox"><i class="form-icon" /> {{ $tc(`word.${key}`, 2) }}
</label> </label>
<div v-if="customizations.exportByChunks"> <div v-if="clientCustoms.exportByChunks">
<div class="h6 mt-4 mb-2"> <div class="h6 mt-4 mb-2">
{{ $t('message.newInserStmtEvery') }}: {{ $t('message.newInserStmtEvery') }}:
</div> </div>
@ -263,211 +263,204 @@
</Teleport> </Teleport>
</template> </template>
<script> <script setup lang="ts">
import moment from 'moment'; import { computed, onBeforeUnmount, Ref, ref } from 'vue';
import * as moment from 'moment';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { SchemaInfos } from 'common/interfaces/antares';
import { ExportState, TableParams } from 'common/interfaces/exporter';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import customizations from 'common/customizations';
import Application from '@/ipc-api/Application'; import Application from '@/ipc-api/Application';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { Customizations } from 'common/interfaces/customizations';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
export default { const props = defineProps({
name: 'ModalExportSchema', selectedSchema: String
components: { });
BaseSelect
},
props: {
selectedSchema: String
},
emits: ['close'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const emit = defineEmits(['close']);
const { t } = useI18n();
const { const { addNotification } = useNotificationsStore();
getWorkspace, const workspacesStore = useWorkspacesStore();
getDatabaseVariable,
refreshSchema
} = workspacesStore;
return { const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
addNotification,
selectedWorkspace,
getWorkspace,
getDatabaseVariable,
refreshSchema
};
},
data () {
return {
isExporting: false,
isRefreshing: false,
progressPercentage: 0,
progressStatus: '',
tables: [],
options: {
includes: {},
outputFormat: 'sql',
sqlInsertAfter: 250,
sqlInsertDivider: 'bytes'
},
basePath: ''
};
},
computed: {
currentWorkspace () {
return this.getWorkspace(this.selectedWorkspace);
},
customizations () {
return this.currentWorkspace.customizations;
},
schemaItems () {
const db = this.currentWorkspace.structure.find(db => db.name === this.selectedSchema);
if (db)
return db.tables.filter(table => table.type === 'table');
return []; const {
}, getWorkspace,
filename () { refreshSchema
const date = moment().format('YYYY-MM-DD'); } = workspacesStore;
return `${this.selectedSchema}_${date}.${this.options.outputFormat}`;
},
dumpFilePath () {
return `${this.basePath}/${this.filename}`;
},
includeStructureStatus () {
if (this.tables.every(item => item.includeStructure)) return 1;
else if (this.tables.some(item => item.includeStructure)) return 2;
else return 0;
},
includeContentStatus () {
if (this.tables.every(item => item.includeContent)) return 1;
else if (this.tables.some(item => item.includeContent)) return 2;
else return 0;
},
includeDropStatementStatus () {
if (this.tables.every(item => item.includeDropStatement)) return 1;
else if (this.tables.some(item => item.includeDropStatement)) return 2;
else return 0;
}
},
async created () {
if (!this.schemaItems.length) await this.refresh();
window.addEventListener('keydown', this.onKey); const isExporting = ref(false);
const isRefreshing = ref(false);
const progressPercentage = ref(0);
const progressStatus = ref('');
const tables: Ref<TableParams[]> = ref([]);
const options = ref({
includes: {} as {[key: string]: boolean},
outputFormat: 'sql',
sqlInsertAfter: 250,
sqlInsertDivider: 'bytes'
});
const basePath = ref('');
this.basePath = await Application.getDownloadPathDirectory(); const currentWorkspace = computed(() => getWorkspace(selectedWorkspace.value));
this.tables = this.schemaItems.map(item => ({ const clientCustoms: Ref<Customizations> = computed(() => currentWorkspace.value.customizations);
table: item.name, const schemaItems = computed(() => {
includeStructure: true, const db: SchemaInfos = currentWorkspace.value.structure.find((db: SchemaInfos) => db.name === props.selectedSchema);
includeContent: true, if (db)
includeDropStatement: true return db.tables.filter(table => table.type === 'table');
}));
const structure = ['functions', 'views', 'triggers', 'routines', 'schedulers']; return [];
});
const filename = computed(() => {
const date = moment().format('YYYY-MM-DD');
return `${props.selectedSchema}_${date}.${options.value.outputFormat}`;
});
const dumpFilePath = computed(() => `${basePath.value}/${filename.value}`);
const includeStructureStatus = computed(() => {
if (tables.value.every(item => item.includeStructure)) return 1;
else if (tables.value.some(item => item.includeStructure)) return 2;
else return 0;
});
const includeContentStatus = computed(() => {
if (tables.value.every(item => item.includeContent)) return 1;
else if (tables.value.some(item => item.includeContent)) return 2;
else return 0;
});
const includeDropStatementStatus = computed(() => {
if (tables.value.every(item => item.includeDropStatement)) return 1;
else if (tables.value.some(item => item.includeDropStatement)) return 2;
else return 0;
});
structure.forEach(feat => { const startExport = async () => {
const val = customizations[this.currentWorkspace.client][feat]; isExporting.value = true;
if (val) const { uid, client } = currentWorkspace.value;
this.options.includes[feat] = true; const params = {
}); uid,
type: client,
schema: props.selectedSchema,
outputFile: dumpFilePath.value,
tables: [...tables.value],
...options.value
};
ipcRenderer.on('export-progress', this.updateProgress); try {
}, const { status, response } = await Schema.export(params);
beforeUnmount () { if (status === 'success')
window.removeEventListener('keydown', this.onKey); progressStatus.value = response.cancelled ? t('word.aborted') : t('word.completed');
ipcRenderer.off('export-progress', this.updateProgress); else {
}, progressStatus.value = response;
methods: { addNotification({ status: 'error', message: response });
async startExport () {
this.isExporting = true;
const { uid, client } = this.currentWorkspace;
const params = {
uid,
type: client,
schema: this.selectedSchema,
outputFile: this.dumpFilePath,
tables: [...this.tables],
...this.options
};
try {
const { status, response } = await Schema.export(params);
if (status === 'success')
this.progressStatus = response.cancelled ? this.$t('word.aborted') : this.$t('word.completed');
else {
this.progressStatus = response;
this.addNotification({ status: 'error', message: response });
}
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isExporting = false;
},
updateProgress (event, state) {
this.progressPercentage = Number((state.currentItemIndex / state.totalItems * 100).toFixed(1));
switch (state.op) {
case 'PROCESSING':
this.progressStatus = this.$t('message.processingTableExport', { table: state.currentItem });
break;
case 'FETCH':
this.progressStatus = this.$t('message.fechingTableExport', { table: state.currentItem });
break;
case 'WRITE':
this.progressStatus = this.$t('message.writingTableExport', { table: state.currentItem });
break;
}
},
async closeModal () {
let willClose = true;
if (this.isExporting) {
willClose = false;
const { response } = await Schema.abortExport();
willClose = response.willAbort;
}
if (willClose)
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
checkAllTables () {
this.tables = this.tables.map(item => ({ ...item, includeStructure: true, includeContent: true, includeDropStatement: true }));
},
uncheckAllTables () {
this.tables = this.tables.map(item => ({ ...item, includeStructure: false, includeContent: false, includeDropStatement: false }));
},
toggleAllTablesOption (option) {
const options = ['includeStructure', 'includeContent', 'includeDropStatement'];
if (!options.includes(option)) return;
if (this[`${option}Status`] !== 1)
this.tables = this.tables.map(item => ({ ...item, [option]: true }));
else
this.tables = this.tables.map(item => ({ ...item, [option]: false }));
},
async refresh () {
this.isRefreshing = true;
await this.refreshSchema({ uid: this.currentWorkspace.uid, schema: this.selectedSchema });
this.isRefreshing = false;
},
async openPathDialog () {
const result = await Application.showOpenDialog({ properties: ['openDirectory'] });
if (result && !result.canceled)
this.basePath = result.filePaths[0];
} }
} }
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isExporting.value = false;
}; };
const updateProgress = (event: Event, state: ExportState) => {
progressPercentage.value = Number((state.currentItemIndex / state.totalItems * 100).toFixed(1));
switch (state.op) {
case 'PROCESSING':
progressStatus.value = t('message.processingTableExport', { table: state.currentItem });
break;
case 'FETCH':
progressStatus.value = t('message.fechingTableExport', { table: state.currentItem });
break;
case 'WRITE':
progressStatus.value = t('message.writingTableExport', { table: state.currentItem });
break;
}
};
const closeModal = async () => {
let willClose = true;
if (isExporting.value) {
willClose = false;
const { response } = await Schema.abortExport();
willClose = response.willAbort;
}
if (willClose)
emit('close');
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
const checkAllTables = () => {
tables.value = tables.value.map(item => ({ ...item, includeStructure: true, includeContent: true, includeDropStatement: true }));
};
const uncheckAllTables = () => {
tables.value = tables.value.map(item => ({ ...item, includeStructure: false, includeContent: false, includeDropStatement: false }));
};
const toggleAllTablesOption = (option: 'includeStructure' | 'includeContent' |'includeDropStatement') => {
const options = {
includeStructure: includeStructureStatus.value,
includeContent: includeContentStatus.value,
includeDropStatement: includeDropStatementStatus.value
};
if (options[option] !== 1)
tables.value = tables.value.map(item => ({ ...item, [option]: true }));
else
tables.value = tables.value.map(item => ({ ...item, [option]: false }));
};
const refresh = async () => {
isRefreshing.value = true;
await refreshSchema({ uid: currentWorkspace.value.uid, schema: props.selectedSchema });
isRefreshing.value = false;
};
const openPathDialog = async () => {
const result = await Application.showOpenDialog({ properties: ['openDirectory'] });
if (result && !result.canceled)
basePath.value = result.filePaths[0];
};
(async () => {
if (!schemaItems.value.length) await refresh();
window.addEventListener('keydown', onKey);
basePath.value = await Application.getDownloadPathDirectory();
tables.value = schemaItems.value.map(item => ({
table: item.name,
includeStructure: true,
includeContent: true,
includeDropStatement: true
}));
const structure = ['functions', 'views', 'triggers', 'routines', 'schedulers'];
structure.forEach((feat: keyof Customizations) => {
const val = clientCustoms.value[feat];
if (val)
options.value.includes[feat] = true;
});
ipcRenderer.on('export-progress', updateProgress);
})();
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
ipcRenderer.off('export-progress', updateProgress);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -476,14 +469,15 @@ export default {
overflow: hidden; overflow: hidden;
.left { .left {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
} }
} }
.workspace-query-results { .workspace-query-results {
flex: 1 0 1px; flex: 1 0 1px;
.table { .table {
width: 100% !important; width: 100% !important;
} }
@ -499,25 +493,24 @@ export default {
} }
.modal { .modal {
.modal-container { .modal-container {
max-width: 800px; max-width: 800px;
} }
.modal-body { .modal-body {
max-height: 60vh; max-height: 60vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.modal-footer { .modal-footer {
display: flex; display: flex;
} }
} }
.progress-status { .progress-status {
font-style: italic; font-style: italic;
font-size: 80%; font-size: 80%;
} }
</style> </style>

View File

@ -97,251 +97,228 @@
</Teleport> </Teleport>
</template> </template>
<script> <script setup lang="ts">
import moment from 'moment'; import { computed, onBeforeMount, onMounted, Prop, Ref, ref, watch } from 'vue';
import * as moment from 'moment';
import { TableField, TableForeign } from 'common/interfaces/antares';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import FakerSelect from '@/components/FakerSelect'; import FakerSelect from '@/components/FakerSelect.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
export default { const props = defineProps({
name: 'ModalFakerRows', tabUid: [String, Number],
components: { fields: Array as Prop<TableField[]>,
FakerSelect, keyUsage: Array as Prop<TableForeign[]>
BaseSelect });
},
props: {
tabUid: [String, Number],
fields: Array,
keyUsage: Array
},
emits: ['reload', 'hide'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const emit = defineEmits(['reload', 'hide']);
const { getWorkspace, getWorkspaceTab } = workspacesStore;
const locales = [
{ value: 'ar', label: 'Arabic' },
{ value: 'az', label: 'Azerbaijani' },
{ value: 'zh_CN', label: 'Chinese' },
{ value: 'zh_TW', label: 'Chinese (Taiwan)' },
{ value: 'cz', label: 'Czech' },
{ value: 'nl', label: 'Dutch' },
{ value: 'nl_BE', label: 'Dutch (Belgium)' },
{ value: 'en', label: 'English' },
{ value: 'en_AU_ocker', label: 'English (Australia Ocker)' },
{ value: 'en_AU', label: 'English (Australia)' },
{ value: 'en_BORK', label: 'English (Bork)' },
{ value: 'en_CA', label: 'English (Canada)' },
{ value: 'en_GB', label: 'English (Great Britain)' },
{ value: 'en_IND', label: 'English (India)' },
{ value: 'en_IE', label: 'English (Ireland)' },
{ value: 'en_ZA', label: 'English (South Africa)' },
{ value: 'en_US', label: 'English (United States)' },
{ value: 'fa', label: 'Farsi' },
{ value: 'fi', label: 'Finnish' },
{ value: 'fr', label: 'French' },
{ value: 'fr_CA', label: 'French (Canada)' },
{ value: 'fr_CH', label: 'French (Switzerland)' },
{ value: 'ge', label: 'Georgian' },
{ value: 'de', label: 'German' },
{ value: 'de_AT', label: 'German (Austria)' },
{ value: 'de_CH', label: 'German (Switzerland)' },
{ value: 'hr', label: 'Hrvatski' },
{ value: 'id_ID', label: 'Indonesia' },
{ value: 'it', label: 'Italian' },
{ value: 'ja', label: 'Japanese' },
{ value: 'ko', label: 'Korean' },
{ value: 'nep', label: 'Nepalese' },
{ value: 'nb_NO', label: 'Norwegian' },
{ value: 'pl', label: 'Polish' },
{ value: 'pt_BR', label: 'Portuguese (Brazil)' },
{ value: 'pt_PT', label: 'Portuguese (Portugal)' },
{ value: 'ro', label: 'Romanian' },
{ value: 'ru', label: 'Russian' },
{ value: 'sk', label: 'Slovakian' },
{ value: 'es', label: 'Spanish' },
{ value: 'es_MX', label: 'Spanish (Mexico)' },
{ value: 'sv', label: 'Swedish' },
{ value: 'tr', label: 'Turkish' },
{ value: 'uk', label: 'Ukrainian' },
{ value: 'vi', label: 'Vietnamese' }
]; const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
return { const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
addNotification,
selectedWorkspace,
getWorkspace,
getWorkspaceTab,
locales
};
},
data () {
return {
localRow: {},
fieldsToExclude: [],
nInserts: 1,
isInserting: false,
fakerLocale: 'en'
};
},
computed: {
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
foreignKeys () {
return this.keyUsage.map(key => key.field);
},
hasFakes () {
return Object.keys(this.localRow).some(field => 'group' in this.localRow[field] && this.localRow[field].group !== 'manual');
}
},
watch: {
nInserts (val) {
if (!val || val < 1)
this.nInserts = 1;
else if (val > 1000)
this.nInserts = 1000;
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
mounted () {
const rowObj = {};
for (const field of this.fields) { const { getWorkspace } = workspacesStore;
let fieldDefault;
if (field.default === 'NULL') fieldDefault = null; // eslint-disable-next-line @typescript-eslint/no-explicit-any
else { const localRow: Ref<{[key: string]: any}> = ref({});
if ([...NUMBER, ...FLOAT].includes(field.type)) const fieldsToExclude = ref([]);
fieldDefault = !field.default || Number.isNaN(+field.default.replaceAll('\'', '')) ? null : +field.default.replaceAll('\'', ''); const nInserts = ref(1);
else if ([...TEXT, ...LONG_TEXT].includes(field.type)) { const isInserting = ref(false);
fieldDefault = field.default const fakerLocale = ref('en');
? field.default.includes('\'')
? field.default.split('\'')[1]
: field.default
: '';
}
else if ([...TIME, ...DATE].includes(field.type))
fieldDefault = field.default;
else if (BIT.includes(field.type))
fieldDefault = field.default?.replaceAll('\'', '').replaceAll('b', '');
else if (DATETIME.includes(field.type)) {
if (field.default && ['current_timestamp', 'now()'].some(term => field.default.toLowerCase().includes(term))) {
let datePrecision = '';
for (let i = 0; i < field.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
fieldDefault = moment().format(`YYYY-MM-DD HH:mm:ss${datePrecision}`);
}
else
fieldDefault = field.default;
}
else if (field.enumValues)
fieldDefault = field.enumValues.replaceAll('\'', '').split(',');
else
fieldDefault = field.default;
}
rowObj[field.name] = { value: fieldDefault }; const workspace = computed(() => getWorkspace(selectedWorkspace.value));
const foreignKeys = computed(() => props.keyUsage.map(key => key.field));
const hasFakes = computed(() => Object.keys(localRow.value).some(field => 'group' in localRow.value[field] && localRow.value[field].group !== 'manual'));
if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields const locales = [
this.fieldsToExclude = [...this.fieldsToExclude, field.name]; { value: 'ar', label: 'Arabic' },
} { value: 'az', label: 'Azerbaijani' },
{ value: 'zh_CN', label: 'Chinese' },
{ value: 'zh_TW', label: 'Chinese (Taiwan)' },
{ value: 'cz', label: 'Czech' },
{ value: 'nl', label: 'Dutch' },
{ value: 'nl_BE', label: 'Dutch (Belgium)' },
{ value: 'en', label: 'English' },
{ value: 'en_AU_ocker', label: 'English (Australia Ocker)' },
{ value: 'en_AU', label: 'English (Australia)' },
{ value: 'en_BORK', label: 'English (Bork)' },
{ value: 'en_CA', label: 'English (Canada)' },
{ value: 'en_GB', label: 'English (Great Britain)' },
{ value: 'en_IND', label: 'English (India)' },
{ value: 'en_IE', label: 'English (Ireland)' },
{ value: 'en_ZA', label: 'English (South Africa)' },
{ value: 'en_US', label: 'English (United States)' },
{ value: 'fa', label: 'Farsi' },
{ value: 'fi', label: 'Finnish' },
{ value: 'fr', label: 'French' },
{ value: 'fr_CA', label: 'French (Canada)' },
{ value: 'fr_CH', label: 'French (Switzerland)' },
{ value: 'ge', label: 'Georgian' },
{ value: 'de', label: 'German' },
{ value: 'de_AT', label: 'German (Austria)' },
{ value: 'de_CH', label: 'German (Switzerland)' },
{ value: 'hr', label: 'Hrvatski' },
{ value: 'id_ID', label: 'Indonesia' },
{ value: 'it', label: 'Italian' },
{ value: 'ja', label: 'Japanese' },
{ value: 'ko', label: 'Korean' },
{ value: 'nep', label: 'Nepalese' },
{ value: 'nb_NO', label: 'Norwegian' },
{ value: 'pl', label: 'Polish' },
{ value: 'pt_BR', label: 'Portuguese (Brazil)' },
{ value: 'pt_PT', label: 'Portuguese (Portugal)' },
{ value: 'ro', label: 'Romanian' },
{ value: 'ru', label: 'Russian' },
{ value: 'sk', label: 'Slovakian' },
{ value: 'es', label: 'Spanish' },
{ value: 'es_MX', label: 'Spanish (Mexico)' },
{ value: 'sv', label: 'Swedish' },
{ value: 'tr', label: 'Turkish' },
{ value: 'uk', label: 'Ukrainian' },
{ value: 'vi', label: 'Vietnamese' }
this.localRow = { ...rowObj }; ];
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
async insertRows () {
this.isInserting = true;
const rowToInsert = this.localRow;
Object.keys(rowToInsert).forEach(key => { watch(nInserts, (val) => {
if (this.fieldsToExclude.includes(key)) if (!val || val < 1)
delete rowToInsert[key]; nInserts.value = 1;
else if (val > 1000)
nInserts.value = 1000;
});
if (typeof rowToInsert[key] === 'undefined') const typeClass = (type: string) => {
delete rowToInsert[key]; if (type)
}); return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
const fieldTypes = {};
this.fields.forEach(field => {
fieldTypes[field.name] = field.type;
});
try {
const { status, response } = await Tables.insertTableFakeRows({
uid: this.selectedWorkspace,
schema: this.workspace.breadcrumbs.schema,
table: this.workspace.breadcrumbs.table,
row: rowToInsert,
repeat: this.nInserts,
fields: fieldTypes,
locale: this.fakerLocale
});
if (status === 'success') {
this.closeModal();
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isInserting = false;
},
closeModal () {
this.$emit('hide');
},
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
else if (TEXT.includes(field.type)) return Number(field.charLength);
return Number(field.length);
},
toggleFields (event, field) {
if (event.target.checked)
this.fieldsToExclude = this.fieldsToExclude.filter(f => f !== field.name);
else
this.fieldsToExclude = [...this.fieldsToExclude, field.name];
},
filesChange (event, field) {
const { files } = event.target;
if (!files.length) return;
this.localRow[field] = files[0].path;
},
getKeyUsage (keyName) {
return this.keyUsage.find(key => key.field === keyName);
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
}
}; };
const insertRows = async () => {
isInserting.value = true;
const rowToInsert = localRow.value;
Object.keys(rowToInsert).forEach(key => {
if (fieldsToExclude.value.includes(key))
delete rowToInsert[key];
if (typeof rowToInsert[key] === 'undefined')
delete rowToInsert[key];
});
const fieldTypes: {[key: string]: string} = {};
props.fields.forEach(field => {
fieldTypes[field.name] = field.type;
});
try {
const { status, response } = await Tables.insertTableFakeRows({
uid: selectedWorkspace.value,
schema: workspace.value.breadcrumbs.schema,
table: workspace.value.breadcrumbs.table,
row: rowToInsert,
repeat: nInserts.value,
fields: fieldTypes,
locale: fakerLocale.value
});
if (status === 'success') {
closeModal();
emit('reload');
}
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isInserting.value = false;
};
const closeModal = () => {
emit('hide');
};
const fieldLength = (field: TableField) => {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
else if (TEXT.includes(field.type)) return Number(field.charLength);
return Number(field.length);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const toggleFields = (event: any, field: TableField) => {
if (event.target.checked)
fieldsToExclude.value = fieldsToExclude.value.filter(f => f !== field.name);
else
fieldsToExclude.value = [...fieldsToExclude.value, field.name];
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
const wrapNumber = (num: number) => {
if (!num) return '';
return `(${num})`;
};
window.addEventListener('keydown', onKey);
onMounted(() => {
const rowObj: {[key: string]: unknown} = {};
for (const field of props.fields) {
let fieldDefault;
if (field.default === 'NULL') fieldDefault = null;
else {
if ([...NUMBER, ...FLOAT].includes(field.type))
fieldDefault = !field.default || Number.isNaN(+field.default.replaceAll('\'', '')) ? null : +field.default.replaceAll('\'', '');
else if ([...TEXT, ...LONG_TEXT].includes(field.type)) {
fieldDefault = field.default
? field.default.includes('\'')
? field.default.split('\'')[1]
: field.default
: '';
}
else if ([...TIME, ...DATE].includes(field.type))
fieldDefault = field.default;
else if (BIT.includes(field.type))
fieldDefault = field.default?.replaceAll('\'', '').replaceAll('b', '');
else if (DATETIME.includes(field.type)) {
if (field.default && ['current_timestamp', 'now()'].some(term => field.default.toLowerCase().includes(term))) {
let datePrecision = '';
for (let i = 0; i < field.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
fieldDefault = moment().format(`YYYY-MM-DD HH:mm:ss${datePrecision}`);
}
else
fieldDefault = field.default;
}
else if (field.enumValues)
fieldDefault = field.enumValues.replaceAll('\'', '').split(',');
else
fieldDefault = field.default;
}
rowObj[field.name] = { value: fieldDefault };
if (field.autoIncrement || !!field.onUpdate)// Disable by default auto increment or "on update" fields
fieldsToExclude.value = [...fieldsToExclude.value, field.name];
}
localRow.value = { ...rowObj };
});
onBeforeMount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style scoped> <style scoped>

View File

@ -7,7 +7,7 @@
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-history mr-1" /> <i class="mdi mdi-24px mdi-history mr-1" />
<span class="cut-text">{{ $t('word.history') }}: {{ connectionName }}</span> <span class="cut-text">{{ t('word.history') }}: {{ connectionName }}</span>
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <a class="btn btn-clear c-hand" @click.stop="closeModal" />
@ -22,7 +22,7 @@
v-model="searchTerm" v-model="searchTerm"
class="form-input" class="form-input"
type="text" type="text"
:placeholder="$t('message.searchForQueries')" :placeholder="t('message.searchForQueries')"
> >
<i v-if="!searchTerm" class="form-icon mdi mdi-magnify mdi-18px pr-4" /> <i v-if="!searchTerm" class="form-icon mdi mdi-magnify mdi-18px pr-4" />
<i <i
@ -67,13 +67,13 @@
<small class="tile-subtitle">{{ query.schema }} · {{ formatDate(query.date) }}</small> <small class="tile-subtitle">{{ query.schema }} · {{ formatDate(query.date) }}</small>
<div class="tile-history-buttons"> <div class="tile-history-buttons">
<button class="btn btn-link pl-1" @click.stop="$emit('select-query', query.sql)"> <button class="btn btn-link pl-1" @click.stop="$emit('select-query', query.sql)">
<i class="mdi mdi-open-in-app pr-1" /> {{ $t('word.select') }} <i class="mdi mdi-open-in-app pr-1" /> {{ t('word.select') }}
</button> </button>
<button class="btn btn-link pl-1" @click="copyQuery(query.sql)"> <button class="btn btn-link pl-1" @click="copyQuery(query.sql)">
<i class="mdi mdi-content-copy pr-1" /> {{ $t('word.copy') }} <i class="mdi mdi-content-copy pr-1" /> {{ t('word.copy') }}
</button> </button>
<button class="btn btn-link pl-1" @click="deleteQuery(query)"> <button class="btn btn-link pl-1" @click="deleteQuery(query)">
<i class="mdi mdi-delete-forever pr-1" /> {{ $t('word.delete') }} <i class="mdi mdi-delete-forever pr-1" /> {{ t('word.delete') }}
</button> </button>
</div> </div>
</div> </div>
@ -88,7 +88,7 @@
<i class="mdi mdi-history mdi-48px" /> <i class="mdi mdi-history mdi-48px" />
</div> </div>
<p class="empty-title h5"> <p class="empty-title h5">
{{ $t('message.thereIsNoQueriesYet') }} {{ t('message.thereIsNoQueriesYet') }}
</p> </p>
</div> </div>
</div> </div>
@ -97,129 +97,111 @@
</Teleport> </Teleport>
</template> </template>
<script> <script setup lang="ts">
import moment from 'moment'; import { Component, computed, ComputedRef, onBeforeUnmount, onMounted, onUpdated, Prop, Ref, ref, watch } from 'vue';
import { useHistoryStore } from '@/stores/history'; import { useI18n } from 'vue-i18n';
import * as moment from 'moment';
import { ConnectionParams } from 'common/interfaces/antares';
import { HistoryRecord, useHistoryStore } from '@/stores/history';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useNotificationsStore } from '@/stores/notifications'; import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
export default { const { t } = useI18n();
name: 'ModalHistory',
components: {
BaseVirtualScroll
},
props: {
connection: Object
},
emits: ['select-query', 'close'],
setup () {
const { getHistoryByWorkspace, deleteQueryFromHistory } = useHistoryStore();
const { getConnectionName } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
return { const { getHistoryByWorkspace, deleteQueryFromHistory } = useHistoryStore();
getHistoryByWorkspace, const { getConnectionName } = useConnectionsStore();
deleteQueryFromHistory,
getConnectionName,
addNotification
};
},
data () {
return {
resultsSize: 1000,
isQuering: false,
scrollElement: null,
searchTermInterval: null,
searchTerm: '',
localSearchTerm: ''
};
},
computed: {
connectionName () {
return this.getConnectionName(this.connection.uid);
},
history () {
return this.getHistoryByWorkspace(this.connection.uid) || [];
},
filteredHistory () {
return this.history.filter(q => q.sql.toLowerCase().search(this.searchTerm.toLowerCase()) >= 0);
}
},
watch: {
searchTerm () {
clearTimeout(this.searchTermInterval);
this.searchTermInterval = setTimeout(() => { const props = defineProps({
this.localSearchTerm = this.searchTerm; connection: Object as Prop<ConnectionParams>
}, 200); });
}
},
created () {
window.addEventListener('keydown', this.onKey, { capture: true });
},
updated () {
if (this.$refs.table)
this.refreshScroller();
if (this.$refs.tableWrapper) const emit = defineEmits(['select-query', 'close']);
this.scrollElement = this.$refs.tableWrapper;
},
mounted () {
this.resizeResults();
window.addEventListener('resize', this.resizeResults);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey, { capture: true });
window.removeEventListener('resize', this.resizeResults);
clearInterval(this.refreshInterval);
},
methods: {
copyQuery (sql) {
navigator.clipboard.writeText(sql);
},
deleteQuery (query) {
this.deleteQueryFromHistory({
workspace: this.connection.uid,
...query
});
},
resizeResults () {
if (this.$refs.resultTable) {
const el = this.$refs.tableWrapper.parentElement;
if (el) const table: Ref<HTMLDivElement> = ref(null);
this.resultsSize = el.offsetHeight - this.$refs.searchForm.offsetHeight; const resultTable: Ref<Component & { updateWindow: () => void }> = ref(null);
const tableWrapper: Ref<HTMLDivElement> = ref(null);
const searchForm: Ref<HTMLInputElement> = ref(null);
const resultsSize = ref(1000);
const scrollElement: Ref<HTMLDivElement> = ref(null);
const searchTermInterval: Ref<NodeJS.Timeout> = ref(null);
const searchTerm = ref('');
const localSearchTerm = ref('');
this.$refs.resultTable.updateWindow(); const connectionName = computed(() => getConnectionName(props.connection.uid));
} const history: ComputedRef<HistoryRecord[]> = computed(() => (getHistoryByWorkspace(props.connection.uid) || []));
}, const filteredHistory = computed(() => history.value.filter(q => q.sql.toLowerCase().search(searchTerm.value.toLowerCase()) >= 0));
formatDate (date) {
return moment(date).isValid() ? moment(date).format('HH:mm:ss - YYYY/MM/DD') : date;
},
refreshScroller () {
this.resizeResults();
},
closeModal () {
this.$emit('close');
},
highlightWord (string) {
string = string.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
if (this.searchTerm) { watch(searchTerm, () => {
const regexp = new RegExp(`(${this.searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'); clearTimeout(searchTermInterval.value);
return string.replace(regexp, '<span class="text-primary text-bold">$1</span>');
} searchTermInterval.value = setTimeout(() => {
else localSearchTerm.value = searchTerm.value;
return string; }, 200);
}, });
onKey (e) {
e.stopPropagation(); const copyQuery = (sql: string) => {
if (e.key === 'Escape') navigator.clipboard.writeText(sql);
this.closeModal(); };
}
const deleteQuery = (query: HistoryRecord[]) => {
deleteQueryFromHistory({
workspace: props.connection.uid,
...query
});
};
const resizeResults = () => {
if (resultTable.value) {
const el = tableWrapper.value.parentElement;
if (el)
resultsSize.value = el.offsetHeight - searchForm.value.offsetHeight;
resultTable.value.updateWindow();
} }
}; };
const formatDate = (date: Date) => moment(date).isValid() ? moment(date).format('HH:mm:ss - YYYY/MM/DD') : date;
const refreshScroller = () => resizeResults();
const closeModal = () => emit('close');
const highlightWord = (string: string) => {
string = string.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
if (searchTerm.value) {
const regexp = new RegExp(`(${searchTerm.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return string.replace(regexp, '<span class="text-primary text-bold">$1</span>');
}
else
return string;
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
window.addEventListener('keydown', onKey, { capture: true });
onUpdated(() => {
if (table.value)
refreshScroller();
if (tableWrapper.value)
scrollElement.value = tableWrapper.value;
});
onMounted(() => {
resizeResults();
window.addEventListener('resize', resizeResults);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey, { capture: true });
window.removeEventListener('resize', resizeResults);
clearInterval(searchTermInterval.value);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -49,146 +49,140 @@
</teleport> </teleport>
</template> </template>
<script> <script setup lang="ts">
import { computed, onBeforeUnmount, Ref, ref } from 'vue';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import * as moment from 'moment';
import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import moment from 'moment';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { storeToRefs } from 'pinia'; import { useI18n } from 'vue-i18n';
import { ImportState } from 'common/interfaces/importer';
export default { const { t } = useI18n();
name: 'ModalImportSchema',
props: { const { addNotification } = useNotificationsStore();
selectedSchema: String const workspacesStore = useWorkspacesStore();
},
emits: ['close'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, refreshSchema } = workspacesStore; const { getWorkspace, refreshSchema } = workspacesStore;
return { const props = defineProps({
addNotification, selectedSchema: String
selectedWorkspace, });
getWorkspace,
refreshSchema const emit = defineEmits(['close']);
};
}, const sqlFile = ref('');
data () { const isImporting = ref(false);
return { const progressPercentage = ref(0);
sqlFile: '', const queryCount = ref(0);
isImporting: false, const completed = ref(false);
progressPercentage: 0, const progressStatus = ref('Reading');
queryCount: 0, const queryErrors: Ref<{time: string; message: string}[]> = ref([]);
completed: false,
progressStatus: 'Reading', const currentWorkspace = computed(() => getWorkspace(selectedWorkspace.value));
queryErrors: []
}; const formattedQueryErrors = computed(() => {
}, return queryErrors.value.map(err =>
computed: { `Time: ${moment(err.time).format('HH:mm:ss.S')} (${err.time})\nError: ${err.message}`
currentWorkspace () { ).join('\n\n');
return this.getWorkspace(this.selectedWorkspace); });
},
formattedQueryErrors () { const startImport = async (file: string) => {
return this.queryErrors.map(err => isImporting.value = true;
`Time: ${moment(err.time).format('HH:mm:ss.S')} (${err.time})\nError: ${err.message}` sqlFile.value = file;
).join('\n\n');
} const { uid, client } = currentWorkspace.value;
}, const params = {
async created () { uid,
window.addEventListener('keydown', this.onKey); type: client,
schema: props.selectedSchema,
ipcRenderer.on('import-progress', this.updateProgress); file: sqlFile.value
ipcRenderer.on('query-error', this.handleQueryError); };
},
beforeUnmount () { try {
window.removeEventListener('keydown', this.onKey); completed.value = false;
ipcRenderer.off('import-progress', this.updateProgress); const { status, response } = await Schema.import(params);
ipcRenderer.off('query-error', this.handleQueryError);
}, if (status === 'success')
methods: { progressStatus.value = response.cancelled ? t('word.aborted') : t('word.completed');
async startImport (sqlFile) { else {
this.isImporting = true; progressStatus.value = response;
this.sqlFile = sqlFile; addNotification({ status: 'error', message: response });
const { uid, client } = this.currentWorkspace;
const params = {
uid,
type: client,
schema: this.selectedSchema,
file: sqlFile
};
try {
this.completed = false;
const { status, response } = await Schema.import(params);
if (status === 'success')
this.progressStatus = response.cancelled ? this.$t('word.aborted') : this.$t('word.completed');
else {
this.progressStatus = response;
this.addNotification({ status: 'error', message: response });
}
this.refreshSchema({ uid, schema: this.selectedSchema });
this.completed = true;
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isImporting = false;
},
updateProgress (event, state) {
this.progressPercentage = Number(state.percentage).toFixed(1);
this.queryCount = Number(state.queryCount);
},
handleQueryError (event, err) {
this.queryErrors.push(err);
},
async closeModal () {
let willClose = true;
if (this.isImporting) {
willClose = false;
const { response } = await Schema.abortImport();
willClose = response.willAbort;
}
if (willClose)
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
} }
refreshSchema({ uid, schema: props.selectedSchema });
completed.value = true;
} }
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isImporting.value = false;
}; };
const updateProgress = (event: Event, state: ImportState) => {
progressPercentage.value = parseFloat(Number(state.percentage).toFixed(1));
queryCount.value = Number(state.queryCount);
};
const handleQueryError = (event: Event, err: { time: string; message: string }) => {
queryErrors.value.push(err);
};
const closeModal = async () => {
let willClose = true;
if (isImporting.value) {
willClose = false;
const { response } = await Schema.abortImport();
willClose = response.willAbort;
}
if (willClose)
emit('close');
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
window.addEventListener('keydown', onKey);
ipcRenderer.on('import-progress', updateProgress);
ipcRenderer.on('query-error', handleQueryError);
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
ipcRenderer.off('import-progress', updateProgress);
ipcRenderer.off('query-error', handleQueryError);
});
defineExpose({ startImport });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.modal { .modal {
.modal-container {
max-width: 800px;
}
.modal-container { .modal-body {
max-width: 800px; max-height: 60vh;
} display: flex;
flex-direction: column;
}
.modal-body { .modal-footer {
max-height: 60vh; display: flex;
display: flex; }
flex-direction: column;
}
.modal-footer {
display: flex;
}
} }
.progress-status { .progress-status {
font-style: italic; font-style: italic;
font-size: 80%; font-size: 80%;
} }
</style> </style>

View File

@ -65,93 +65,75 @@
</Teleport> </Teleport>
</template> </template>
<script> <script setup lang="ts">
import { computed, onBeforeUnmount, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { storeToRefs } from 'pinia';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
export default { const { addNotification } = useNotificationsStore();
name: 'ModalNewSchema', const workspacesStore = useWorkspacesStore();
components: { BaseSelect },
emits: ['reload', 'close'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, getDatabaseVariable } = workspacesStore; const { getWorkspace, getDatabaseVariable } = workspacesStore;
return { const emit = defineEmits(['reload', 'close']);
addNotification,
selectedWorkspace, const firstInput: Ref<HTMLInputElement> = ref(null);
getWorkspace, const isLoading = ref(false);
getDatabaseVariable const database = ref({
}; name: '',
}, collation: ''
data () { });
return {
isLoading: false, const collations = computed(() => getWorkspace(selectedWorkspace.value).collations);
database: { const customizations = computed(() => getWorkspace(selectedWorkspace.value).customizations);
name: '', const defaultCollation = computed(() => getDatabaseVariable(selectedWorkspace.value, 'collation_server') ? getDatabaseVariable(selectedWorkspace.value, 'collation_server').value : '');
collation: ''
} database.value = { ...database.value, collation: defaultCollation.value };
};
}, const createSchema = async () => {
computed: { isLoading.value = true;
collations () { try {
return this.getWorkspace(this.selectedWorkspace).collations; const { status, response } = await Schema.createSchema({
}, uid: selectedWorkspace.value,
customizations () { ...database.value
return this.getWorkspace(this.selectedWorkspace).customizations; });
},
defaultCollation () { if (status === 'success') {
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server') ? this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value : ''; closeModal();
} emit('reload');
},
created () {
this.database = { ...this.database, collation: this.defaultCollation };
window.addEventListener('keydown', this.onKey);
setTimeout(() => {
this.$refs.firstInput.focus();
}, 20);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async createSchema () {
this.isLoading = true;
try {
const { status, response } = await Schema.createSchema({
uid: this.selectedWorkspace,
...this.database
});
if (status === 'success') {
this.closeModal();
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isLoading = false;
},
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
} }
else
addNotification({ status: 'error', message: response });
} }
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isLoading.value = false;
}; };
const closeModal = () => {
emit('close');
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
window.addEventListener('keydown', onKey);
setTimeout(() => {
firstInput.value.focus();
}, 20);
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style scoped> <style scoped>

View File

@ -1,366 +0,0 @@
<template>
<Teleport to="#window-content">
<div class="modal active">
<a class="modal-overlay" @click.stop="closeModal" />
<div class="modal-container p-0">
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span class="cut-text">{{ $t('message.addNewRow') }}</span>
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body pb-0">
<div class="content">
<form class="form-horizontal">
<fieldset :disabled="isInserting">
<div
v-for="(field, key) in fields"
:key="field.name"
class="form-group"
>
<div class="col-4 col-sm-12">
<label class="form-label" :title="field.name">{{ field.name }}</label>
</div>
<div class="input-group col-8 col-sm-12">
<ForeignKeySelect
v-if="foreignKeys.includes(field.name)"
ref="formInput"
v-model="localRow[field.name]"
class="form-select"
:key-usage="getKeyUsage(field.name)"
:disabled="fieldsToExclude.includes(field.name)"
/>
<input
v-else-if="inputProps(field).mask"
ref="formInput"
v-model="localRow[field.name]"
v-mask="inputProps(field).mask"
class="form-input"
:type="inputProps(field).type"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
>
<input
v-else-if="inputProps(field).type === 'file'"
ref="formInput"
class="form-input"
type="file"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
@change="filesChange($event,field.name)"
>
<input
v-else-if="inputProps(field).type === 'number'"
ref="formInput"
v-model="localRow[field.name]"
class="form-input"
step="any"
:type="inputProps(field).type"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
>
<input
v-else
ref="formInput"
v-model="localRow[field.name]"
class="form-input"
:type="inputProps(field).type"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
>
<span class="input-group-addon" :class="typeCLass(field.type)">
{{ field.type }} {{ wrapNumber(fieldLength(field)) }}
</span>
<label class="form-checkbox ml-3" :title="$t('word.insert')">
<input
type="checkbox"
:checked="!field.autoIncrement"
@change.prevent="toggleFields($event, field)"
><i class="form-icon" />
</label>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div class="modal-footer">
<div class="input-group col-3 tooltip tooltip-right" :data-tooltip="$t('message.numberOfInserts')">
<input
v-model="nInserts"
type="number"
class="form-input"
min="1"
:disabled="isInserting"
>
<span class="input-group-addon">
<i class="mdi mdi-24px mdi-repeat" />
</span>
</div>
<div>
<button
class="btn btn-primary mr-2"
:class="{'loading': isInserting}"
@click.stop="insertRows"
>
{{ $t('word.insert') }}
</button>
<button class="btn btn-link" @click.stop="closeModal">
{{ $t('word.close') }}
</button>
</div>
</div>
</div>
</div>
</Teleport>
</template>
<script>
import moment from 'moment';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import Tables from '@/ipc-api/Tables';
import ForeignKeySelect from '@/components/ForeignKeySelect';
import { storeToRefs } from 'pinia';
export default {
name: 'ModalNewTableRow',
components: {
ForeignKeySelect
},
props: {
tabUid: [String, Number],
fields: Array,
keyUsage: Array
},
emits: ['reload', 'hide'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, getWorkspaceTab } = workspacesStore;
return {
addNotification,
selectedWorkspace,
getWorkspace,
getWorkspaceTab
};
},
data () {
return {
localRow: {},
fieldsToExclude: [],
nInserts: 1,
isInserting: false
};
},
computed: {
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
foreignKeys () {
return this.keyUsage.map(key => key.field);
}
},
watch: {
nInserts (val) {
if (!val || val < 1)
this.nInserts = 1;
else if (val > 1000)
this.nInserts = 1000;
}
},
created () {
window.addEventListener('keydown', this.onKey);
},
mounted () {
const rowObj = {};
for (const field of this.fields) {
let fieldDefault;
if (field.default === 'NULL') fieldDefault = null;
else {
if ([...NUMBER, ...FLOAT].includes(field.type))
fieldDefault = +field.default;
if ([...TEXT, ...LONG_TEXT].includes(field.type))
fieldDefault = field.default ? field.default.substring(1, field.default.length - 1) : '';
if ([...TIME, ...DATE].includes(field.type))
fieldDefault = field.default;
if (DATETIME.includes(field.type)) {
if (field.default && field.default.toLowerCase().includes('current_timestamp')) {
let datePrecision = '';
for (let i = 0; i < field.datePrecision; i++)
datePrecision += i === 0 ? '.S' : 'S';
fieldDefault = moment().format(`YYYY-MM-DD HH:mm:ss${datePrecision}`);
}
}
}
rowObj[field.name] = fieldDefault;
if (field.autoIncrement)// Disable by default auto increment fields
this.fieldsToExclude = [...this.fieldsToExclude, field.name];
}
this.localRow = { ...rowObj };
// Auto focus
setTimeout(() => {
const firstSelectableInput = this.$refs.formInput.find(input => !input.disabled);
firstSelectableInput.focus();
}, 20);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
async insertRows () {
this.isInserting = true;
const rowToInsert = this.localRow;
Object.keys(rowToInsert).forEach(key => {
if (this.fieldsToExclude.includes(key))
delete rowToInsert[key];
if (typeof rowToInsert[key] === 'undefined')
delete rowToInsert[key];
});
const fieldTypes = {};
this.fields.forEach(field => {
fieldTypes[field.name] = field.type;
});
try {
const { status, response } = await Tables.insertTableRows({
uid: this.selectedWorkspace,
schema: this.workspace.breadcrumbs.schema,
table: this.workspace.breadcrumbs.table,
row: rowToInsert,
repeat: this.nInserts,
fields: fieldTypes
});
if (status === 'success') {
this.closeModal();
this.$emit('reload');
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isInserting = false;
},
closeModal () {
this.$emit('hide');
},
fieldLength (field) {
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
else if (TEXT.includes(field.type)) return field.charLength;
return field.length;
},
inputProps (field) {
if ([...TEXT, ...LONG_TEXT].includes(field.type))
return { type: 'text', mask: false };
if ([...NUMBER, ...FLOAT].includes(field.type))
return { type: 'number', mask: false };
if (TIME.includes(field.type)) {
let timeMask = '##:##:##';
const precision = this.fieldLength(field);
for (let i = 0; i < precision; i++)
timeMask += i === 0 ? '.#' : '#';
return { type: 'text', mask: timeMask };
}
if (DATE.includes(field.type))
return { type: 'text', mask: '####-##-##' };
if (DATETIME.includes(field.type)) {
let datetimeMask = '####-##-## ##:##:##';
const precision = this.fieldLength(field);
for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#';
return { type: 'text', mask: datetimeMask };
}
if (BLOB.includes(field.type))
return { type: 'file', mask: false };
if (BIT.includes(field.type))
return { type: 'text', mask: false };
return { type: 'text', mask: false };
},
toggleFields (event, field) {
if (event.target.checked)
this.fieldsToExclude = this.fieldsToExclude.filter(f => f !== field.name);
else
this.fieldsToExclude = [...this.fieldsToExclude, field.name];
},
filesChange (event, field) {
const { files } = event.target;
if (!files.length) return;
this.localRow[field] = files[0].path;
},
getKeyUsage (keyName) {
return this.keyUsage.find(key => key.field === keyName);
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
}
};
</script>
<style scoped>
.modal-container {
max-width: 500px;
}
.form-label {
overflow: hidden;
white-space: normal;
text-overflow: ellipsis;
}
.input-group-addon {
display: flex;
align-items: center;
}
.modal-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@ -133,218 +133,218 @@
</Teleport> </Teleport>
</template> </template>
<script> <script setup lang="ts">
import arrayToFile from '../libs/arrayToFile'; import { Component, computed, onBeforeUnmount, onMounted, onUpdated, Prop, Ref, ref } from 'vue';
import { ConnectionParams } from 'common/interfaces/antares';
import { arrayToFile } from '../libs/arrayToFile';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import BaseVirtualScroll from '@/components/BaseVirtualScroll'; import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
import ModalProcessesListRow from '@/components/ModalProcessesListRow'; import ModalProcessesListRow from '@/components/ModalProcessesListRow.vue';
import ModalProcessesListContext from '@/components/ModalProcessesListContext'; import ModalProcessesListContext from '@/components/ModalProcessesListContext.vue';
export default { const { addNotification } = useNotificationsStore();
name: 'ModalProcessesList', const { getConnectionName } = useConnectionsStore();
components: {
BaseVirtualScroll,
ModalProcessesListRow,
ModalProcessesListContext
},
props: {
connection: Object
},
emits: ['close'],
setup () {
const { addNotification } = useNotificationsStore();
const { getConnectionName } = useConnectionsStore();
return { addNotification, getConnectionName }; const props = defineProps({
}, connection: Object as Prop<ConnectionParams>
data () { });
return {
resultsSize: 1000, const emit = defineEmits(['close']);
isQuering: false,
isContext: false, const tableWrapper: Ref<HTMLDivElement> = ref(null);
autorefreshTimer: 0, const table: Ref<HTMLDivElement> = ref(null);
refreshInterval: null, const resultTable: Ref<Component & {updateWindow: () => void}> = ref(null);
contextEvent: null, const resultsSize = ref(1000);
selectedCell: null, const isQuering = ref(false);
selectedRow: null, const isContext = ref(false);
results: [], const autorefreshTimer = ref(0);
fields: [], const refreshInterval: Ref<NodeJS.Timeout> = ref(null);
currentSort: '', const contextEvent = ref(null);
currentSortDir: 'asc', const selectedCell = ref(null);
scrollElement: null const selectedRow: Ref<number> = ref(null);
}; const results = ref([]);
}, const fields = ref([]);
computed: { const currentSort = ref('');
connectionName () { const currentSortDir = ref('asc');
return this.getConnectionName(this.connection.uid); const scrollElement = ref(null);
},
sortedResults () { const connectionName = computed(() => getConnectionName(props.connection.uid));
if (this.currentSort) {
return [...this.results].sort((a, b) => { const sortedResults = computed(() => {
let modifier = 1; if (currentSort.value) {
const valA = typeof a[this.currentSort] === 'string' ? a[this.currentSort].toLowerCase() : a[this.currentSort]; return [...results.value].sort((a, b) => {
const valB = typeof b[this.currentSort] === 'string' ? b[this.currentSort].toLowerCase() : b[this.currentSort]; let modifier = 1;
if (this.currentSortDir === 'desc') modifier = -1; const valA = typeof a[currentSort.value] === 'string' ? a[currentSort.value].toLowerCase() : a[currentSort.value];
if (valA < valB) return -1 * modifier; const valB = typeof b[currentSort.value] === 'string' ? b[currentSort.value].toLowerCase() : b[currentSort.value];
if (valA > valB) return 1 * modifier; if (currentSortDir.value === 'desc') modifier = -1;
return 0; if (valA < valB) return -1 * modifier;
}); if (valA > valB) return 1 * modifier;
} return 0;
else });
return this.results; }
else
return results.value;
});
const getProcessesList = async () => {
isQuering.value = true;
try { // Table data
const { status, response } = await Schema.getProcesses(props.connection.uid);
if (status === 'success') {
results.value = response;
fields.value = response.length ? Object.keys(response[0]) : [];
} }
}, else
created () { addNotification({ status: 'error', message: response });
window.addEventListener('keydown', this.onKey, { capture: true }); }
}, catch (err) {
updated () { addNotification({ status: 'error', message: err.stack });
if (this.$refs.table) }
this.refreshScroller();
if (this.$refs.tableWrapper) isQuering.value = false;
this.scrollElement = this.$refs.tableWrapper; };
},
mounted () {
this.getProcessesList();
window.addEventListener('resize', this.resizeResults);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey, { capture: true });
window.removeEventListener('resize', this.resizeResults);
clearInterval(this.refreshInterval);
},
methods: {
async getProcessesList () {
this.isQuering = true;
// if table changes clear cached values const setRefreshInterval = () => {
if (this.lastTable !== this.table) clearRefresh();
this.results = [];
try { // Table data if (+autorefreshTimer.value) {
const { status, response } = await Schema.getProcesses(this.connection.uid); refreshInterval.value = setInterval(() => {
if (!isQuering.value)
if (status === 'success') { getProcessesList();
this.results = response; }, autorefreshTimer.value * 1000);
this.fields = response.length ? Object.keys(response[0]) : [];
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
},
setRefreshInterval () {
this.clearRefresh();
if (+this.autorefreshTimer) {
this.refreshInterval = setInterval(() => {
if (!this.isQuering)
this.getProcessesList();
}, this.autorefreshTimer * 1000);
}
},
clearRefresh () {
if (this.refreshInterval)
clearInterval(this.refreshInterval);
},
resizeResults () {
if (this.$refs.resultTable) {
const el = this.$refs.tableWrapper.parentElement;
if (el) {
const size = el.offsetHeight;
this.resultsSize = size;
}
this.$refs.resultTable.updateWindow();
}
},
refreshScroller () {
this.resizeResults();
},
sort (field) {
if (field === this.currentSort) {
if (this.currentSortDir === 'asc')
this.currentSortDir = 'desc';
else
this.resetSort();
}
else {
this.currentSortDir = 'asc';
this.currentSort = field;
}
},
resetSort () {
this.currentSort = '';
this.currentSortDir = 'asc';
},
stopRefresh () {
this.autorefreshTimer = 0;
this.clearRefresh();
},
selectRow (row) {
this.selectedRow = Number(row);
},
contextMenu (event, cell) {
if (event.target.localName === 'input') return;
this.stopRefresh();
this.selectedCell = cell;
this.selectedRow = Number(cell.id);
this.contextEvent = event;
this.isContext = true;
},
async killProcess () {
try { // Table data
const { status, response } = await Schema.killProcess({ uid: this.connection.uid, pid: this.selectedRow });
if (status === 'success')
this.getProcessesList();
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
closeContext () {
this.isContext = false;
},
copyCell () {
const row = this.results.find(row => row.id === this.selectedRow);
const valueToCopy = row[this.selectedCell.field];
navigator.clipboard.writeText(valueToCopy);
},
copyRow () {
const row = this.results.find(row => row.id === this.selectedRow);
const rowToCopy = JSON.parse(JSON.stringify(row));
navigator.clipboard.writeText(JSON.stringify(rowToCopy));
},
closeModal () {
this.$emit('close');
},
downloadTable (format) {
if (!this.sortedResults) return;
arrayToFile({
type: format,
content: this.sortedResults,
filename: 'processes'
});
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
if (e.key === 'F5')
this.getProcessesList();
}
} }
}; };
const clearRefresh = () => {
if (refreshInterval.value)
clearInterval(refreshInterval.value);
};
const resizeResults = () => {
if (resultTable.value) {
const el = tableWrapper.value.parentElement;
if (el) {
const size = el.offsetHeight;
resultsSize.value = size;
}
resultTable.value.updateWindow();
}
};
const refreshScroller = () => resizeResults();
const sort = (field: string) => {
if (field === currentSort.value) {
if (currentSortDir.value === 'asc')
currentSortDir.value = 'desc';
else
resetSort();
}
else {
currentSortDir.value = 'asc';
currentSort.value = field;
}
};
const resetSort = () => {
currentSort.value = '';
currentSortDir.value = 'asc';
};
const stopRefresh = () => {
autorefreshTimer.value = 0;
clearRefresh();
};
const selectRow = (row: number) => {
selectedRow.value = Number(row);
};
const contextMenu = (event: MouseEvent, cell: { id: number; field: string }) => {
if ((event.target as HTMLElement).localName === 'input') return;
stopRefresh();
selectedCell.value = cell;
selectedRow.value = Number(cell.id);
contextEvent.value = event;
isContext.value = true;
};
const killProcess = async () => {
try { // Table data
const { status, response } = await Schema.killProcess({ uid: props.connection.uid, pid: selectedRow.value });
if (status === 'success')
getProcessesList();
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
};
const closeContext = () => {
isContext.value = false;
};
const copyCell = () => {
const row = results.value.find(row => Number(row.id) === selectedRow.value);
const valueToCopy = row[selectedCell.value.field];
navigator.clipboard.writeText(valueToCopy);
};
const copyRow = () => {
const row = results.value.find(row => Number(row.id) === selectedRow.value);
const rowToCopy = JSON.parse(JSON.stringify(row));
navigator.clipboard.writeText(JSON.stringify(rowToCopy));
};
const closeModal = () => emit('close');
const downloadTable = (format: 'csv' | 'json') => {
if (!sortedResults.value) return;
arrayToFile({
type: format,
content: sortedResults.value,
filename: 'processes'
});
};
const onKey = (e:KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
if (e.key === 'F5')
getProcessesList();
};
window.addEventListener('keydown', onKey, { capture: true });
onMounted(() => {
getProcessesList();
window.addEventListener('resize', resizeResults);
});
onUpdated(() => {
if (table.value)
refreshScroller();
if (tableWrapper.value)
scrollElement.value = tableWrapper.value;
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey, { capture: true });
window.removeEventListener('resize', resizeResults);
clearInterval(refreshInterval.value);
});
defineExpose({ refreshScroller });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -1,14 +1,14 @@
<template> <template>
<BaseContextMenu <BaseContextMenu
:context-event="contextEvent" :context-event="props.contextEvent"
@close-context="closeContext" @close-context="closeContext"
> >
<div v-if="selectedRow" class="context-element"> <div v-if="props.selectedRow" class="context-element">
<span class="d-flex"><i class="mdi mdi-18px mdi-content-copy text-light pr-1" /> {{ $t('word.copy') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-content-copy text-light pr-1" /> {{ $t('word.copy') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" /> <i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu"> <div class="context-submenu">
<div <div
v-if="selectedRow" v-if="props.selectedRow"
class="context-element" class="context-element"
@click="copyCell" @click="copyCell"
> >
@ -17,7 +17,7 @@
</span> </span>
</div> </div>
<div <div
v-if="selectedRow" v-if="props.selectedRow"
class="context-element" class="context-element"
@click="copyRow" @click="copyRow"
> >
@ -28,7 +28,7 @@
</div> </div>
</div> </div>
<div <div
v-if="selectedRow" v-if="props.selectedRow"
class="context-element" class="context-element"
@click="killProcess" @click="killProcess"
> >
@ -39,38 +39,33 @@
</BaseContextMenu> </BaseContextMenu>
</template> </template>
<script> <script setup lang="ts">
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu.vue';
export default { const props = defineProps({
name: 'ModalProcessesListContext', contextEvent: MouseEvent,
components: { selectedRow: Number,
BaseContextMenu selectedCell: Object
}, });
props: {
contextEvent: MouseEvent, const emit = defineEmits(['close-context', 'copy-cell', 'copy-row', 'kill-process']);
selectedRow: Number,
selectedCell: Object const closeContext = () => {
}, emit('close-context');
emits: ['close-context', 'copy-cell', 'copy-row', 'kill-process'], };
computed: {
}, const copyCell = () => {
methods: { emit('copy-cell');
closeContext () { closeContext();
this.$emit('close-context'); };
},
copyCell () { const copyRow = () => {
this.$emit('copy-cell'); emit('copy-row');
this.closeContext(); closeContext();
}, };
copyRow () {
this.$emit('copy-row'); const killProcess = () => {
this.closeContext(); emit('kill-process');
}, closeContext();
killProcess () {
this.$emit('kill-process');
this.closeContext();
}
}
}; };
</script> </script>

View File

@ -31,11 +31,12 @@
<div> <div>
<div> <div>
<TextEditor <TextEditor
:value="row.info || ''" :model-value="props.row.info || ''"
editor-class="textarea-editor" editor-class="textarea-editor"
:mode="editorMode" :mode="editorMode"
:read-only="true" :read-only="true"
/> />
<div class="mb-4" />
</div> </div>
</div> </div>
</template> </template>
@ -43,60 +44,46 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import ConfirmModal from '@/components/BaseConfirmModal'; import { Ref, ref } from 'vue';
import TextEditor from '@/components/BaseTextEditor'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import TextEditor from '@/components/BaseTextEditor.vue';
export default { const props = defineProps({
name: 'ModalProcessesListRow', row: Object
components: { });
ConfirmModal,
TextEditor const emit = defineEmits(['select-row', 'contextmenu', 'stop-refresh']);
},
props: { const isInlineEditor: Ref<{[key: string]: boolean}> = ref({});
row: Object const isInfoModal = ref(false);
}, const editorMode = ref('sql');
emits: ['select-row', 'contextmenu', 'stop-refresh'],
data () { const isNull = (value: string | number) => value === null ? ' is-null' : '';
return {
isInlineEditor: {}, const selectRow = () => {
isInfoModal: false, emit('select-row');
editorMode: 'sql'
};
},
computed: {},
methods: {
isNull (value) {
return value === null ? ' is-null' : '';
},
selectRow () {
this.$emit('select-row');
},
openContext (event, payload) {
this.$emit('contextmenu', event, payload);
},
hideInfoModal () {
this.isInfoModal = false;
},
dblClick (col) {
if (col !== 'info') return;
this.$emit('stop-refresh');
this.isInfoModal = true;
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape') {
this.isInlineEditor[this.editingField] = false;
this.editingField = null;
window.removeEventListener('keydown', this.onKey);
}
},
cutText (val) {
if (typeof val !== 'string') return val;
return val.length > 250 ? `${val.substring(0, 250)}[...]` : val;
}
}
}; };
const openContext = (event: MouseEvent, payload: { id: number; field: string }) => {
emit('contextmenu', event, payload);
};
const hideInfoModal = () => {
isInfoModal.value = false;
};
const dblClick = (col: string) => {
if (col !== 'info') return;
emit('stop-refresh');
isInfoModal.value = true;
};
const cutText = (val: string | number) => {
if (typeof val !== 'string') return val;
return val.length > 250 ? `${val.substring(0, 250)}[...]` : val;
};
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -7,7 +7,7 @@
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-cog mr-1" /> <i class="mdi mdi-24px mdi-cog mr-1" />
<span class="cut-text">{{ $t('word.settings') }}</span> <span class="cut-text">{{ t('word.settings') }}</span>
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click="closeModal" /> <a class="btn btn-clear c-hand" @click="closeModal" />
@ -21,14 +21,14 @@
:class="{'active': selectedTab === 'general'}" :class="{'active': selectedTab === 'general'}"
@click="selectTab('general')" @click="selectTab('general')"
> >
<a class="tab-link">{{ $t('word.general') }}</a> <a class="tab-link">{{ t('word.general') }}</a>
</li> </li>
<li <li
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'themes'}" :class="{'active': selectedTab === 'themes'}"
@click="selectTab('themes')" @click="selectTab('themes')"
> >
<a class="tab-link">{{ $t('word.themes') }}</a> <a class="tab-link">{{ t('word.themes') }}</a>
</li> </li>
<li <li
v-if="updateStatus !== 'disabled'" v-if="updateStatus !== 'disabled'"
@ -36,21 +36,21 @@
:class="{'active': selectedTab === 'update'}" :class="{'active': selectedTab === 'update'}"
@click="selectTab('update')" @click="selectTab('update')"
> >
<a class="tab-link" :class="{'badge badge-update': hasUpdates}">{{ $t('word.update') }}</a> <a class="tab-link" :class="{'badge badge-update': hasUpdates}">{{ t('word.update') }}</a>
</li> </li>
<li <li
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'changelog'}" :class="{'active': selectedTab === 'changelog'}"
@click="selectTab('changelog')" @click="selectTab('changelog')"
> >
<a class="tab-link">{{ $t('word.changelog') }}</a> <a class="tab-link">{{ t('word.changelog') }}</a>
</li> </li>
<li <li
class="tab-item c-hand" class="tab-item c-hand"
:class="{'active': selectedTab === 'about'}" :class="{'active': selectedTab === 'about'}"
@click="selectTab('about')" @click="selectTab('about')"
> >
<a class="tab-link">{{ $t('word.about') }}</a> <a class="tab-link">{{ t('word.about') }}</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -58,14 +58,14 @@
<div class="container"> <div class="container">
<form class="form-horizontal columns"> <form class="form-horizontal columns">
<div class="column col-12 h6 text-uppercase mb-1"> <div class="column col-12 h6 text-uppercase mb-1">
{{ $t('word.application') }} {{ t('word.application') }}
</div> </div>
<div class="column col-12 col-sm-12 mb-2 columns"> <div class="column col-12 col-sm-12 mb-2 columns">
<div class="form-group column col-12"> <div class="form-group column col-12">
<div class="col-5 col-sm-12"> <div class="col-5 col-sm-12">
<label class="form-label"> <label class="form-label">
<i class="mdi mdi-18px mdi-translate mr-1" /> <i class="mdi mdi-18px mdi-translate mr-1" />
{{ $t('word.language') }} {{ t('word.language') }}
</label> </label>
</div> </div>
<div class="col-3 col-sm-12"> <div class="col-3 col-sm-12">
@ -79,16 +79,16 @@
/> />
</div> </div>
<div class="col-4 col-sm-12 px-2 p-vcentered"> <div class="col-4 col-sm-12 px-2 p-vcentered">
<small class="d-block" style="line-height:1.1; font-size:70%;"> <small class="d-block" style="line-height: 1.1; font-size: 70%;">
{{ $t('message.missingOrIncompleteTranslation') }}<br> {{ t('message.missingOrIncompleteTranslation') }}<br>
<a class="text-bold c-hand" @click="openOutside('https://github.com/antares-sql/antares/wiki/Translate-Antares')">{{ $t('message.findOutHowToContribute') }}</a> <a class="text-bold c-hand" @click="openOutside('https://github.com/antares-sql/antares/wiki/Translate-Antares')">{{ t('message.findOutHowToContribute') }}</a>
</small> </small>
</div> </div>
</div> </div>
<div class="form-group column col-12"> <div class="form-group column col-12">
<div class="col-5 col-sm-12"> <div class="col-5 col-sm-12">
<label class="form-label"> <label class="form-label">
{{ $t('message.dataTabPageSize') }} {{ t('message.dataTabPageSize') }}
</label> </label>
</div> </div>
<div class="col-3 col-sm-12"> <div class="col-3 col-sm-12">
@ -103,7 +103,7 @@
<div class="form-group column col-12 mb-0"> <div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12"> <div class="col-5 col-sm-12">
<label class="form-label"> <label class="form-label">
{{ $t('message.restorePreviourSession') }} {{ t('message.restorePreviourSession') }}
</label> </label>
</div> </div>
<div class="col-3 col-sm-12"> <div class="col-3 col-sm-12">
@ -116,7 +116,7 @@
<div class="form-group column col-12 mb-0"> <div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12"> <div class="col-5 col-sm-12">
<label class="form-label"> <label class="form-label">
{{ $t('message.disableBlur') }} {{ t('message.disableBlur') }}
</label> </label>
</div> </div>
<div class="col-3 col-sm-12"> <div class="col-3 col-sm-12">
@ -129,7 +129,7 @@
<div class="form-group column col-12"> <div class="form-group column col-12">
<div class="col-5 col-sm-12"> <div class="col-5 col-sm-12">
<label class="form-label"> <label class="form-label">
{{ $t('message.notificationsTimeout') }} {{ t('message.notificationsTimeout') }}
</label> </label>
</div> </div>
<div class="col-3 col-sm-12"> <div class="col-3 col-sm-12">
@ -141,19 +141,19 @@
min="1" min="1"
@focusout="checkNotificationsTimeout" @focusout="checkNotificationsTimeout"
> >
<span class="input-group-addon">{{ $t('word.seconds') }}</span> <span class="input-group-addon">{{ t('word.seconds') }}</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="column col-12 h6 mt-4 text-uppercase mb-1"> <div class="column col-12 h6 mt-4 text-uppercase mb-1">
{{ $t('word.editor') }} {{ t('word.editor') }}
</div> </div>
<div class="column col-12 col-sm-12 columns"> <div class="column col-12 col-sm-12 columns">
<div class="form-group column col-12 mb-0"> <div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12"> <div class="col-5 col-sm-12">
<label class="form-label"> <label class="form-label">
{{ $t('word.autoCompletion') }} {{ t('word.autoCompletion') }}
</label> </label>
</div> </div>
<div class="col-3 col-sm-12"> <div class="col-3 col-sm-12">
@ -166,7 +166,7 @@
<div class="form-group column col-12 mb-0"> <div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12"> <div class="col-5 col-sm-12">
<label class="form-label"> <label class="form-label">
{{ $t('message.wrapLongLines') }} {{ t('message.wrapLongLines') }}
</label> </label>
</div> </div>
<div class="col-3 col-sm-12"> <div class="col-3 col-sm-12">
@ -185,18 +185,18 @@
<div class="container"> <div class="container">
<div class="columns"> <div class="columns">
<div class="column col-12 h6 text-uppercase mb-2"> <div class="column col-12 h6 text-uppercase mb-2">
{{ $t('message.applicationTheme') }} {{ t('message.applicationTheme') }}
</div> </div>
<div <div
class="column col-6 c-hand theme-block" class="column col-6 c-hand theme-block"
:class="{'selected': applicationTheme === 'dark'}" :class="{'selected': applicationTheme === 'dark'}"
@click="changeApplicationTheme('dark')" @click="changeApplicationTheme('dark')"
> >
<img src="../images/dark.png" class="img-responsive img-fit-cover s-rounded"> <img :src="darkPreview" class="img-responsive img-fit-cover s-rounded">
<div class="theme-name text-light"> <div class="theme-name text-light">
<i class="mdi mdi-moon-waning-crescent mdi-48px" /> <i class="mdi mdi-moon-waning-crescent mdi-48px" />
<div class="h6 mt-4"> <div class="h6 mt-4">
{{ $t('word.dark') }} {{ t('word.dark') }}
</div> </div>
</div> </div>
</div> </div>
@ -205,11 +205,11 @@
:class="{'selected': applicationTheme === 'light'}" :class="{'selected': applicationTheme === 'light'}"
@click="changeApplicationTheme('light')" @click="changeApplicationTheme('light')"
> >
<img src="../images/light.png" class="img-responsive img-fit-cover s-rounded"> <img :src="lightPreview" class="img-responsive img-fit-cover s-rounded">
<div class="theme-name text-dark"> <div class="theme-name text-dark">
<i class="mdi mdi-white-balance-sunny mdi-48px" /> <i class="mdi mdi-white-balance-sunny mdi-48px" />
<div class="h6 mt-4"> <div class="h6 mt-4">
{{ $t('word.light') }} {{ t('word.light') }}
</div> </div>
</div> </div>
</div> </div>
@ -217,7 +217,7 @@
<div class="columns mt-4"> <div class="columns mt-4">
<div class="column col-12 h6 text-uppercase mb-2 mt-4"> <div class="column col-12 h6 text-uppercase mb-2 mt-4">
{{ $t('message.editorTheme') }} {{ t('message.editorTheme') }}
</div> </div>
<div class="column col-6 h5 mb-4"> <div class="column col-6 h5 mb-4">
<BaseSelect <BaseSelect
@ -238,21 +238,21 @@
:class="{'active': editorFontSize === 'small'}" :class="{'active': editorFontSize === 'small'}"
@click="changeEditorFontSize('small')" @click="changeEditorFontSize('small')"
> >
{{ $t('word.small') }} {{ t('word.small') }}
</button> </button>
<button <button
class="btn btn-dark cut-text" class="btn btn-dark cut-text"
:class="{'active': editorFontSize === 'medium'}" :class="{'active': editorFontSize === 'medium'}"
@click="changeEditorFontSize('medium')" @click="changeEditorFontSize('medium')"
> >
{{ $t('word.medium') }} {{ t('word.medium') }}
</button> </button>
<button <button
class="btn btn-dark cut-text" class="btn btn-dark cut-text"
:class="{'active': editorFontSize === 'large'}" :class="{'active': editorFontSize === 'large'}"
@click="changeEditorFontSize('large')" @click="changeEditorFontSize('large')"
> >
{{ $t('word.large') }} {{ t('word.large') }}
</button> </button>
</div> </div>
</div> </div>
@ -278,19 +278,19 @@
<div v-show="selectedTab === 'about'" class="panel-body py-4"> <div v-show="selectedTab === 'about'" class="panel-body py-4">
<div class="text-center"> <div class="text-center">
<img src="../images/logo.svg" width="128"> <img :src="appLogo" width="128">
<h4>{{ appName }}</h4> <h4>{{ appName }}</h4>
<p class="mb-2"> <p class="mb-2">
{{ $t('word.version') }} {{ appVersion }}<br> {{ t('word.version') }} {{ appVersion }}<br>
<a class="c-hand" @click="openOutside('https://github.com/antares-sql/antares')"><i class="mdi mdi-github d-inline" /> GitHub</a> <a class="c-hand" @click="openOutside('https://twitter.com/AntaresSQL')"><i class="mdi mdi-twitter d-inline" /> Twitter</a> <a class="c-hand" @click="openOutside('https://antares-sql.app/')"><i class="mdi mdi-web d-inline" /> Website</a><br> <a class="c-hand" @click="openOutside('https://github.com/antares-sql/antares')"><i class="mdi mdi-github d-inline" /> GitHub</a> <a class="c-hand" @click="openOutside('https://twitter.com/AntaresSQL')"><i class="mdi mdi-twitter d-inline" /> Twitter</a> <a class="c-hand" @click="openOutside('https://antares-sql.app/')"><i class="mdi mdi-web d-inline" /> Website</a><br>
<small>{{ $t('word.author') }} <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">{{ appAuthor }}</a></small><br> <small>{{ t('word.author') }} <a class="c-hand" @click="openOutside('https://github.com/Fabio286')">{{ appAuthor }}</a></small><br>
</p> </p>
<div class="mb-2"> <div class="mb-2">
<small class="d-block text-uppercase">{{ $t('word.contributors') }}:</small> <small class="d-block text-uppercase">{{ t('word.contributors') }}:</small>
<div class="d-block py-1"> <div class="d-block py-1">
<small v-for="(contributor, i) in otherContributors" :key="i">{{ i !== 0 ? ', ' : '' }}{{ contributor }}</small> <small v-for="(contributor, i) in otherContributors" :key="i">{{ i !== 0 ? ', ' : '' }}{{ contributor }}</small>
</div> </div>
<small>{{ $t('message.madeWithJS') }}</small> <small>{{ t('message.madeWithJS') }}</small>
</div> </div>
</div> </div>
</div> </div>
@ -301,176 +301,121 @@
</Teleport> </Teleport>
</template> </template>
<script> <script setup lang="ts">
import { onBeforeUnmount, Ref, ref } from 'vue';
import { shell } from 'electron'; import { shell } from 'electron';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import localesNames from '@/i18n/supported-locales'; import { localesNames } from '@/i18n/supported-locales';
import ModalSettingsUpdate from '@/components/ModalSettingsUpdate'; import ModalSettingsUpdate from '@/components/ModalSettingsUpdate.vue';
import ModalSettingsChangelog from '@/components/ModalSettingsChangelog'; import ModalSettingsChangelog from '@/components/ModalSettingsChangelog.vue';
import BaseTextEditor from '@/components/BaseTextEditor'; import BaseTextEditor from '@/components/BaseTextEditor.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { computed } from '@vue/reactivity';
export default { const { t, availableLocales } = useI18n();
name: 'ModalSettings',
components: { const applicationStore = useApplicationStore();
ModalSettingsUpdate, const settingsStore = useSettingsStore();
ModalSettingsChangelog, const workspacesStore = useWorkspacesStore();
BaseTextEditor,
BaseSelect const {
selectedSettingTab,
updateStatus
} = storeToRefs(applicationStore);
const {
locale: selectedLocale,
dataTabLimit: pageSize,
autoComplete: selectedAutoComplete,
lineWrap: selectedLineWrap,
notificationsTimeout,
restoreTabs,
disableBlur,
applicationTheme,
editorTheme,
editorFontSize
} = storeToRefs(settingsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const {
changeLocale,
changePageSize,
changeRestoreTabs,
changeDisableBlur,
changeAutoComplete,
changeLineWrap,
changeApplicationTheme,
changeEditorTheme,
changeEditorFontSize,
updateNotificationsTimeout
} = settingsStore;
const {
hideSettingModal: closeModal,
appName,
appVersion
} = applicationStore;
const { getWorkspace } = workspacesStore;
const appAuthor = 'Fabio Di Stasio';
const pageSizes = [30, 40, 100, 250, 500, 1000];
const contributors = process.env.APP_CONTRIBUTORS;
const appLogo = require('../images/logo.svg');
const darkPreview = require('../images/dark.png');
const lightPreview = require('../images/light.png');
const editorThemes= [
{
group: t('word.light'),
themes: [
{ code: 'chrome', name: 'Chrome' },
{ code: 'clouds', name: 'Clouds' },
{ code: 'crimson_editor', name: 'Crimson Editor' },
{ code: 'dawn', name: 'Dawn' },
{ code: 'dreamweaver', name: 'Dreamweaver' },
{ code: 'eclupse', name: 'Eclipse' },
{ code: 'github', name: 'GitHub' },
{ code: 'iplastic', name: 'IPlastic' },
{ code: 'solarized_light', name: 'Solarized Light' },
{ code: 'textmate', name: 'TextMate' },
{ code: 'tomorrow', name: 'Tomorrow' },
{ code: 'xcode', name: 'Xcode' },
{ code: 'kuroir', name: 'Kuroir' },
{ code: 'katzenmilch', name: 'KatzenMilch' },
{ code: 'sqlserver', name: 'SQL Server' }
]
}, },
setup () { {
const applicationStore = useApplicationStore(); group: t('word.dark'),
const settingsStore = useSettingsStore(); themes: [
const workspacesStore = useWorkspacesStore(); { code: 'ambiance', name: 'Ambiance' },
{ code: 'chaos', name: 'Chaos' },
const { { code: 'clouds_midnight', name: 'Clouds Midnight' },
selectedSettingTab, { code: 'dracula', name: 'Dracula' },
updateStatus { code: 'cobalt', name: 'Cobalt' },
} = storeToRefs(applicationStore); { code: 'gruvbox', name: 'Gruvbox' },
const { { code: 'gob', name: 'Green on Black' },
locale: selectedLocale, { code: 'idle_fingers', name: 'Idle Fingers' },
dataTabLimit: pageSize, { code: 'kr_theme', name: 'krTheme' },
autoComplete: selectedAutoComplete, { code: 'merbivore', name: 'Merbivore' },
lineWrap: selectedLineWrap, { code: 'mono_industrial', name: 'Mono Industrial' },
notificationsTimeout, { code: 'monokai', name: 'Monokai' },
restoreTabs, { code: 'nord_dark', name: 'Nord Dark' },
disableBlur, { code: 'pastel_on_dark', name: 'Pastel on Dark' },
applicationTheme, { code: 'solarized_dark', name: 'Solarized Dark' },
editorTheme, { code: 'terminal', name: 'Terminal' },
editorFontSize { code: 'tomorrow_night', name: 'Tomorrow Night' },
} = storeToRefs(settingsStore); { code: 'tomorrow_night_blue', name: 'Tomorrow Night Blue' },
{ code: 'tomorrow_night_bright', name: 'Tomorrow Night Bright' },
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); { code: 'tomorrow_night_eighties', name: 'Tomorrow Night 80s' },
{ code: 'twilight', name: 'Twilight' },
const { { code: 'vibrant_ink', name: 'Vibrant Ink' }
changeLocale, ]
changePageSize, }
changeRestoreTabs, ];
changeDisableBlur, const exampleQuery = `-- This is an example
changeAutoComplete,
changeLineWrap,
changeApplicationTheme,
changeEditorTheme,
changeEditorFontSize,
updateNotificationsTimeout
} = settingsStore;
const {
hideSettingModal,
appName,
appVersion
} = applicationStore;
const { getWorkspace } = workspacesStore;
return {
appName,
appVersion,
selectedSettingTab,
updateStatus,
closeModal: hideSettingModal,
selectedLocale,
pageSize,
selectedAutoComplete,
selectedLineWrap,
notificationsTimeout,
restoreTabs,
disableBlur,
applicationTheme,
editorTheme,
editorFontSize,
changeLocale,
changePageSize,
changeRestoreTabs,
changeDisableBlur,
changeAutoComplete,
changeLineWrap,
changeApplicationTheme,
changeEditorTheme,
changeEditorFontSize,
updateNotificationsTimeout,
selectedWorkspace,
getWorkspace
};
},
data () {
return {
appAuthor: 'Fabio Di Stasio',
localLocale: null,
localPageSize: null,
localTimeout: null,
localEditorTheme: null,
selectedTab: 'general',
pageSizes: [30, 40, 100, 250, 500, 1000],
editorThemes: [
{
group: this.$t('word.light'),
themes: [
{ code: 'chrome', name: 'Chrome' },
{ code: 'clouds', name: 'Clouds' },
{ code: 'crimson_editor', name: 'Crimson Editor' },
{ code: 'dawn', name: 'Dawn' },
{ code: 'dreamweaver', name: 'Dreamweaver' },
{ code: 'eclupse', name: 'Eclipse' },
{ code: 'github', name: 'GitHub' },
{ code: 'iplastic', name: 'IPlastic' },
{ code: 'solarized_light', name: 'Solarized Light' },
{ code: 'textmate', name: 'TextMate' },
{ code: 'tomorrow', name: 'Tomorrow' },
{ code: 'xcode', name: 'Xcode' },
{ code: 'kuroir', name: 'Kuroir' },
{ code: 'katzenmilch', name: 'KatzenMilch' },
{ code: 'sqlserver', name: 'SQL Server' }
]
},
{
group: this.$t('word.dark'),
themes: [
{ code: 'ambiance', name: 'Ambiance' },
{ code: 'chaos', name: 'Chaos' },
{ code: 'clouds_midnight', name: 'Clouds Midnight' },
{ code: 'dracula', name: 'Dracula' },
{ code: 'cobalt', name: 'Cobalt' },
{ code: 'gruvbox', name: 'Gruvbox' },
{ code: 'gob', name: 'Green on Black' },
{ code: 'idle_fingers', name: 'Idle Fingers' },
{ code: 'kr_theme', name: 'krTheme' },
{ code: 'merbivore', name: 'Merbivore' },
{ code: 'mono_industrial', name: 'Mono Industrial' },
{ code: 'monokai', name: 'Monokai' },
{ code: 'nord_dark', name: 'Nord Dark' },
{ code: 'pastel_on_dark', name: 'Pastel on Dark' },
{ code: 'solarized_dark', name: 'Solarized Dark' },
{ code: 'terminal', name: 'Terminal' },
{ code: 'tomorrow_night', name: 'Tomorrow Night' },
{ code: 'tomorrow_night_blue', name: 'Tomorrow Night Blue' },
{ code: 'tomorrow_night_bright', name: 'Tomorrow Night Bright' },
{ code: 'tomorrow_night_eighties', name: 'Tomorrow Night 80s' },
{ code: 'twilight', name: 'Twilight' },
{ code: 'vibrant_ink', name: 'Vibrant Ink' }
]
}
],
contributors: process.env.APP_CONTRIBUTORS
};
},
computed: {
locales () {
const locales = [];
for (const locale of this.$i18n.availableLocales)
locales.push({ code: locale, name: localesNames[locale] });
return locales;
},
hasUpdates () {
return ['available', 'downloading', 'downloaded', 'link'].includes(this.updateStatus);
},
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
exampleQuery () {
return `-- This is an example
SELECT SELECT
employee.id, employee.id,
employee.first_name, employee.first_name,
@ -485,57 +430,81 @@ GROUP BY
ORDER BY ORDER BY
employee.id ASC; employee.id ASC;
`; `;
},
otherContributors () {
return this.contributors
.split(',')
.filter(c => !c.includes(this.appAuthor))
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
}
},
created () {
this.localLocale = this.selectedLocale;
this.localPageSize = this.pageSize;
this.localTimeout = this.notificationsTimeout;
this.localEditorTheme = this.editorTheme;
this.selectedTab = this.selectedSettingTab;
window.addEventListener('keydown', this.onKey);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
selectTab (tab) {
this.selectedTab = tab;
},
openOutside (link) {
shell.openExternal(link);
},
checkNotificationsTimeout () {
if (!this.localTimeout)
this.localTimeout = 10;
this.updateNotificationsTimeout(+this.localTimeout); const localLocale: Ref<string> = ref(null);
}, const localPageSize: Ref<number> = ref(null);
onKey (e) { const localTimeout: Ref<number> = ref(null);
e.stopPropagation(); const localEditorTheme: Ref<string> = ref(null);
if (e.key === 'Escape') const selectedTab: Ref<string> = ref('general');
this.closeModal();
}, const locales = computed(() => {
toggleRestoreSession () { const locales = [];
this.changeRestoreTabs(!this.restoreTabs); for (const locale of availableLocales)
}, locales.push({ code: locale, name: localesNames[locale] });
toggleDisableBlur () {
this.changeDisableBlur(!this.disableBlur); return locales;
}, });
toggleAutoComplete () {
this.changeAutoComplete(!this.selectedAutoComplete); const hasUpdates = computed(() => ['available', 'downloading', 'downloaded', 'link'].includes(updateStatus.value));
},
toggleLineWrap () { const workspace = computed(() => {
this.changeLineWrap(!this.selectedLineWrap); return getWorkspace(selectedWorkspace.value);
} });
}
const otherContributors = computed(() => {
return contributors
.split(',')
.filter(c => !c.includes(appAuthor))
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
});
const selectTab = (tab: string) => {
selectedTab.value = tab;
}; };
const openOutside = (link: string) => {
shell.openExternal(link);
};
const checkNotificationsTimeout = () => {
if (!localTimeout.value)
localTimeout.value = 10;
updateNotificationsTimeout(+localTimeout.value);
};
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape')
closeModal();
};
const toggleRestoreSession = () => {
changeRestoreTabs(!restoreTabs.value);
};
const toggleDisableBlur = () => {
changeDisableBlur(!disableBlur.value);
};
const toggleAutoComplete = () => {
changeAutoComplete(!selectedAutoComplete.value);
};
const toggleLineWrap = () => {
changeLineWrap(!selectedLineWrap.value);
};
localLocale.value = selectedLocale.value as string;
localPageSize.value = pageSize.value as number;
localTimeout.value = notificationsTimeout.value as number;
localEditorTheme.value = editorTheme.value as string;
selectedTab.value = selectedSettingTab.value;
window.addEventListener('keydown', onKey);
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -13,66 +13,53 @@
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts">
<script>
import { marked } from 'marked'; import { marked } from 'marked';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader.vue';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { ref } from 'vue';
export default { const { appVersion } = useApplicationStore();
name: 'ModalSettingsChangelog',
components: { const changelog = ref('');
BaseLoader const isLoading = ref(true);
}, const error = ref('');
setup () { const isError = ref(false);
const { appVersion } = useApplicationStore();
return { appVersion }; const getChangelog = async () => {
}, try {
data () { const apiRes = await fetch(`https://api.github.com/repos/antares-sql/antares/releases/tags/v${appVersion}`, {
return { method: 'GET'
changelog: '', });
isLoading: true,
error: '', const { body } = await apiRes.json();
isError: false const cutOffset = body.indexOf('### Download');
const markdown = cutOffset >= 0
? body.substr(0, cutOffset)
: body;
const renderer = {
link (href: string, title: string, text: string) {
return text;
},
listitem (text: string) {
return `<li>${text.replace(/ *\([^)]*\) */g, '')}</li>`;
}
}; };
},
created () {
this.getChangelog();
},
methods: {
async getChangelog () {
try {
const apiRes = await fetch(`https://api.github.com/repos/antares-sql/antares/releases/tags/v${this.appVersion}`, {
method: 'GET'
});
const { body } = await apiRes.json(); marked.use({ renderer });
const cutOffset = body.indexOf('### Download');
const markdown = cutOffset >= 0
? body.substr(0, cutOffset)
: body;
const renderer = { changelog.value = marked(markdown);
link (href, title, text) {
return text;
},
listitem (text) {
return `<li>${text.replace(/ *\([^)]*\) */g, '')}</li>`;
}
};
marked.use({ renderer });
this.changelog = marked(markdown);
}
catch (err) {
this.error = err.message;
this.isError = true;
}
this.isLoading = false;
}
} }
catch (err) {
error.value = err.message;
isError.value = true;
}
isLoading.value = false;
}; };
getChangelog();
</script> </script>
<style lang="scss"> <style lang="scss">
#changelog { #changelog {

View File

@ -52,68 +52,61 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { ipcRenderer, shell } from 'electron'; import { ipcRenderer, shell } from 'electron';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
export default { const { t } = useI18n();
name: 'ModalSettingsUpdate',
setup () {
const applicationStore = useApplicationStore();
const settingsStore = useSettingsStore();
const { const applicationStore = useApplicationStore();
updateStatus, const settingsStore = useSettingsStore();
getDownloadProgress
} = storeToRefs(applicationStore);
const { allowPrerelease } = storeToRefs(settingsStore);
const { changeAllowPrerelease } = settingsStore; const {
updateStatus,
getDownloadProgress: downloadPercentage
} = storeToRefs(applicationStore);
const { allowPrerelease } = storeToRefs(settingsStore);
return { const { changeAllowPrerelease } = settingsStore;
updateStatus,
downloadPercentage: getDownloadProgress, const updateMessage = computed(() => {
allowPrerelease, switch (updateStatus.value) {
changeAllowPrerelease case 'noupdate':
}; return t('message.noUpdatesAvailable');
}, case 'checking':
computed: { return t('message.checkingForUpdate');
updateMessage () { case 'nocheck':
switch (this.updateStatus) { return t('message.checkFailure');
case 'noupdate': case 'available':
return this.$t('message.noUpdatesAvailable'); return t('message.updateAvailable');
case 'checking': case 'downloading':
return this.$t('message.checkingForUpdate'); return t('message.downloadingUpdate');
case 'nocheck': case 'downloaded':
return this.$t('message.checkFailure'); return t('message.updateDownloaded');
case 'available': case 'link':
return this.$t('message.updateAvailable'); return t('message.updateAvailable');
case 'downloading': default:
return this.$t('message.downloadingUpdate'); return updateStatus.value;
case 'downloaded':
return this.$t('message.updateDownloaded');
case 'link':
return this.$t('message.updateAvailable');
default:
return this.updateStatus;
}
}
},
methods: {
openOutside (link) {
shell.openExternal(link);
},
checkForUpdates () {
ipcRenderer.send('check-for-updates');
},
restartToUpdate () {
ipcRenderer.send('restart-to-update');
},
toggleAllowPrerelease () {
this.changeAllowPrerelease(!this.allowPrerelease);
}
} }
});
const openOutside = (link: string) => {
shell.openExternal(link);
};
const checkForUpdates = () => {
ipcRenderer.send('check-for-updates');
};
const restartToUpdate = () => {
ipcRenderer.send('restart-to-update');
};
const toggleAllowPrerelease = () => {
changeAllowPrerelease(!allowPrerelease.value);
}; };
</script> </script>

View File

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

View File

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

View File

@ -26,46 +26,39 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { shell } from 'electron';
import { storeToRefs } from 'pinia';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { storeToRefs } from 'pinia'; import { computed, ComputedRef } from 'vue';
const { shell } = require('electron');
export default { interface DatabaseInfos {// TODO: temp
name: 'TheFooter', name: string;
setup () { number: string;
const applicationStore = useApplicationStore(); arch: string;
const workspacesStore = useWorkspacesStore(); os: string;
}
const { getSelected: workspace } = storeToRefs(workspacesStore); const applicationStore = useApplicationStore();
const workspacesStore = useWorkspacesStore();
const { appVersion, showSettingModal } = applicationStore; const { getSelected: workspace } = storeToRefs(workspacesStore);
const { getWorkspace } = workspacesStore;
return { const { showSettingModal } = applicationStore;
appVersion, const { getWorkspace } = workspacesStore;
showSettingModal,
workspace, const version: ComputedRef<DatabaseInfos> = computed(() => {
getWorkspace return getWorkspace(workspace.value) ? getWorkspace(workspace.value).version : null;
}; });
},
computed: { const versionString = computed(() => {
version () { if (version.value)
return this.getWorkspace(this.workspace) ? this.getWorkspace(this.workspace).version : null; return `${version.value.name} ${version.value.number} (${version.value.arch} ${version.value.os})`;
}, return '';
versionString () { });
if (this.version)
return `${this.version.name} ${this.version.number} (${this.version.arch} ${this.version.os})`; const openOutside = (link: string) => shell.openExternal(link);
return '';
}
},
methods: {
openOutside (link) {
shell.openExternal(link);
}
}
};
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -16,71 +16,51 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed, Ref, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import BaseNotification from '@/components/BaseNotification'; import BaseNotification from '@/components/BaseNotification.vue';
import { storeToRefs } from 'pinia';
export default { const notificationsStore = useNotificationsStore();
name: 'TheNotificationsBoard', const settingsStore = useSettingsStore();
components: {
BaseNotification
},
setup () {
const notificationsStore = useNotificationsStore();
const settingsStore = useSettingsStore();
const { removeNotification } = notificationsStore; const { removeNotification } = notificationsStore;
const { notifications } = storeToRefs(notificationsStore); const { notifications } = storeToRefs(notificationsStore);
const { notificationsTimeout } = storeToRefs(settingsStore); const { notificationsTimeout } = storeToRefs(settingsStore);
return { const timeouts: Ref<{[key: string]: NodeJS.Timeout}> = ref({});
removeNotification,
notifications, const latestNotifications = computed(() => notifications.value.slice(0, 10));
notificationsTimeout
}; watch(() => notifications.value.length, (val) => {
}, if (val > 0) {
data () { const nUid: string = notifications.value[0].uid;
return { timeouts.value[nUid] = setTimeout(() => {
timeouts: {} removeNotification(nUid);
}; delete timeouts.value[nUid];
}, }, notificationsTimeout.value * 1000);
computed: { }
latestNotifications () { });
return this.notifications.slice(0, 10);
} const clearTimeouts = () => {
}, for (const uid in timeouts.value) {
watch: { clearTimeout(timeouts.value[uid]);
'notifications.length': function (val) { delete timeouts.value[uid];
if (val > 0) { }
const nUid = this.notifications[0].uid; };
this.timeouts[nUid] = setTimeout(() => {
this.removeNotification(nUid); const rearmTimeouts = () => {
delete this.timeouts[nUid]; const delay = 50;
}, this.notificationsTimeout * 1000); let i = notifications.value.length * delay;
} for (const notification of notifications.value) {
} timeouts.value[notification.uid] = setTimeout(() => {
}, removeNotification(notification.uid);
methods: { delete timeouts.value[notification.uid];
clearTimeouts () { }, (notificationsTimeout.value * 1000) + i);
for (const uid in this.timeouts) { i = i > delay ? i - delay : 0;
clearTimeout(this.timeouts[uid]);
delete this.timeouts[uid];
}
},
rearmTimeouts () {
const delay = 50;
let i = this.notifications.length * delay;
for (const notification of this.notifications) {
this.timeouts[notification.uid] = setTimeout(() => {
this.removeNotification(notification.uid);
delete this.timeouts[notification.uid];
}, (this.notificationsTimeout * 1000) + i);
i = i > delay ? i - delay : 0;
}
}
} }
}; };
</script> </script>

View File

@ -28,55 +28,30 @@
</ConfirmModal> </ConfirmModal>
</template> </template>
<script> <script setup lang="ts">
import { ref, Ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
import { useScratchpadStore } from '@/stores/scratchpad'; import { useScratchpadStore } from '@/stores/scratchpad';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import TextEditor from '@/components/BaseTextEditor.vue';
export default { const applicationStore = useApplicationStore();
name: 'TheScratchpad', const scratchpadStore = useScratchpadStore();
components: {
ConfirmModal,
TextEditor
},
emits: ['hide'],
setup () {
const applicationStore = useApplicationStore();
const scratchpadStore = useScratchpadStore();
const { notes } = storeToRefs(scratchpadStore); const { notes } = storeToRefs(scratchpadStore);
const { changeNotes } = scratchpadStore; const { changeNotes } = scratchpadStore;
const { hideScratchpad } = applicationStore;
return { const localNotes = ref(notes.value);
notes, const debounceTimeout: Ref<NodeJS.Timeout> = ref(null);
hideScratchpad: applicationStore.hideScratchpad,
changeNotes watch(localNotes, () => {
}; clearTimeout(debounceTimeout.value);
},
data () { debounceTimeout.value = setTimeout(() => {
return { changeNotes(localNotes.value);
localNotes: '', }, 200);
debounceTimeout: null });
};
},
watch: {
localNotes () {
clearTimeout(this.debounceTimeout);
this.debounceTimeout = setTimeout(() => {
this.changeNotes(this.localNotes);
}, 200);
}
},
created () {
this.localNotes = this.notes;
},
methods: {
hideModal () {
this.$emit('hide');
}
}
};
</script> </script>

View File

@ -55,111 +55,84 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { ref, Ref, computed } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import Draggable from 'vuedraggable'; import * as Draggable from 'vuedraggable';
import SettingBarContext from '@/components/SettingBarContext'; import SettingBarContext from '@/components/SettingBarContext.vue';
import { ConnectionParams } from 'common/interfaces/antares';
export default { const applicationStore = useApplicationStore();
name: 'TheSettingBar', const connectionsStore = useConnectionsStore();
components: { const workspacesStore = useWorkspacesStore();
Draggable,
SettingBarContext const { updateStatus } = storeToRefs(applicationStore);
const { connections: getConnections } = storeToRefs(connectionsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { showSettingModal, showScratchpad } = applicationStore;
const { getConnectionName, updateConnections } = connectionsStore;
const { getWorkspace, selectWorkspace } = workspacesStore;
const isLinux = process.platform === 'linux';
const isContext: Ref<boolean> = ref(false);
const isDragging: Ref<boolean> = ref(false);
const contextEvent: Ref<MouseEvent> = ref(null);
const contextConnection: Ref<ConnectionParams> = ref(null);
const connections = computed({
get () {
return getConnections.value;
}, },
setup () { set (value: ConnectionParams[]) {
const applicationStore = useApplicationStore(); updateConnections(value);
const connectionsStore = useConnectionsStore(); }
const workspacesStore = useWorkspacesStore(); });
const { updateStatus } = storeToRefs(applicationStore); const hasUpdates = computed(() => ['available', 'downloading', 'downloaded', 'link'].includes(updateStatus.value));
const { connections: getConnections } = storeToRefs(connectionsStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { showSettingModal, showScratchpad } = applicationStore; const contextMenu = (event: MouseEvent, connection: ConnectionParams) => {
const { getConnectionName, updateConnections } = connectionsStore; contextEvent.value = event;
const { getWorkspace, selectWorkspace } = workspacesStore; contextConnection.value = connection;
isContext.value = true;
};
return { const tooltipPosition = (e: Event) => {
applicationStore, const el = e.target ? e.target : e;
updateStatus, const fromTop = isLinux
showSettingModal, ? window.scrollY + (el as HTMLElement).getBoundingClientRect().top + ((el as HTMLElement).offsetHeight / 4)
showScratchpad, : window.scrollY + (el as HTMLElement).getBoundingClientRect().top - ((el as HTMLElement).offsetHeight / 4);
getConnections, (el as HTMLElement).querySelector<HTMLElement>('.ex-tooltip-content').style.top = `${fromTop}px`;
getConnectionName, };
updateConnections,
selectedWorkspace,
getWorkspace,
selectWorkspace
};
},
data () {
return {
dragElement: null,
isLinux: process.platform === 'linux',
isContext: false,
isDragging: false,
contextEvent: null,
contextConnection: {},
scale: 0
};
},
computed: {
connections: {
get () {
return this.getConnections;
},
set (value) {
this.updateConnections(value);
}
},
hasUpdates () {
return ['available', 'downloading', 'downloaded', 'link'].includes(this.updateStatus);
}
},
methods: {
contextMenu (event, connection) {
this.contextEvent = event;
this.contextConnection = connection;
this.isContext = true;
},
workspaceName (connection) {
return connection.ask ? '' : `${connection.user + '@'}${connection.host}:${connection.port}`;
},
tooltipPosition (e) {
const el = e.target ? e.target : e;
const fromTop = this.isLinux
? window.scrollY + el.getBoundingClientRect().top + (el.offsetHeight / 4)
: window.scrollY + el.getBoundingClientRect().top - (el.offsetHeight / 4)
el.querySelector('.ex-tooltip-content').style.top = `${fromTop}px`;
},
getStatusBadge (uid) {
if (this.getWorkspace(uid)) {
const status = this.getWorkspace(uid).connectionStatus;
switch (status) { const getStatusBadge = (uid: string) => {
case 'connected': if (getWorkspace(uid)) {
return 'badge badge-connected'; const status = getWorkspace(uid).connectionStatus;
case 'connecting':
return 'badge badge-connecting';
case 'failed':
return 'badge badge-failed';
default:
return '';
}
}
},
dragStop (e) {
this.isDragging = false;
setTimeout(() => { switch (status) {
this.tooltipPosition(e.originalEvent.target.parentNode); case 'connected':
}, 200); return 'badge badge-connected';
case 'connecting':
return 'badge badge-connecting';
case 'failed':
return 'badge badge-failed';
default:
return '';
} }
} }
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dragStop = (e: any) => { // TODO: temp
isDragging.value = false;
setTimeout(() => {
tooltipPosition(e.originalEvent.target.parentNode);
}, 200);
};
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -9,7 +9,7 @@
<img <img
v-if="!isMacOS" v-if="!isMacOS"
class="titlebar-logo" class="titlebar-logo"
src="@/images/logo.svg" :src="appIcon"
> >
</div> </div>
<div class="titlebar-elements titlebar-title"> <div class="titlebar-elements titlebar-title">
@ -31,112 +31,70 @@
<i class="mdi mdi-24px mdi-refresh" /> <i class="mdi mdi-24px mdi-refresh" />
</div> </div>
<div v-if="isWindows" style="width: 140px;" /> <div v-if="isWindows" style="width: 140px;" />
<!-- <div
v-if="isLinux"
class="titlebar-element"
@click="minimizeApp"
>
<i class="mdi mdi-24px mdi-minus" />
</div>
<div
v-if="isLinux"
class="titlebar-element"
@click="toggleFullScreen"
>
<i v-if="isMaximized" class="mdi mdi-24px mdi-fullscreen-exit" />
<i v-else class="mdi mdi-24px mdi-fullscreen" />
</div>
<div
v-if="isLinux"
class="titlebar-element close-button"
@click="closeApp"
>
<i class="mdi mdi-24px mdi-close" />
</div> -->
</div> </div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { ipcRenderer } from 'electron'; import { computed, onUnmounted, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { getCurrentWindow } from '@electron/remote'; import { getCurrentWindow } from '@electron/remote';
import { useConnectionsStore } from '@/stores/connections'; import { useConnectionsStore } from '@/stores/connections';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { storeToRefs } from 'pinia'; import { useI18n } from 'vue-i18n';
export default { const { t } = useI18n();
name: 'TheTitleBar',
setup () {
const { getConnectionName } = useConnectionsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { getConnectionName } = useConnectionsStore();
const workspacesStore = useWorkspacesStore();
const { getWorkspace } = workspacesStore; const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
return { const { getWorkspace } = workspacesStore;
getConnectionName,
selectedWorkspace,
getWorkspace
};
},
data () {
return {
w: getCurrentWindow(),
isMaximized: getCurrentWindow().isMaximized(),
isDevelopment: process.env.NODE_ENV === 'development',
isMacOS: process.platform === 'darwin',
isWindows: process.platform === 'win32',
isLinux: process.platform === 'linux'
};
},
computed: {
windowTitle () {
if (!this.selectedWorkspace) return '';
if (this.selectedWorkspace === 'NEW') return this.$t('message.createNewConnection');
const connectionName = this.getConnectionName(this.selectedWorkspace); const appIcon = require('@/images/logo.svg');
const workspace = this.getWorkspace(this.selectedWorkspace); const w = ref(getCurrentWindow());
const breadcrumbs = Object.values(workspace.breadcrumbs).filter(breadcrumb => breadcrumb) || [workspace.client]; const isMaximized = ref(getCurrentWindow().isMaximized());
const isDevelopment = ref(process.env.NODE_ENV === 'development');
const isMacOS = process.platform === 'darwin';
const isWindows = process.platform === 'win32';
const isLinux = process.platform === 'linux';
return [connectionName, ...breadcrumbs].join(' • '); const windowTitle = computed(() => {
} if (!selectedWorkspace.value) return '';
}, if (selectedWorkspace.value === 'NEW') return t('message.createNewConnection');
watch: {
windowTitle: function (val) { const connectionName = getConnectionName(selectedWorkspace.value);
ipcRenderer.send('change-window-title', val); const workspace = getWorkspace(selectedWorkspace.value);
} const breadcrumbs = Object.values(workspace.breadcrumbs).filter(breadcrumb => breadcrumb) || [workspace.client];
},
created () { return [connectionName, ...breadcrumbs].join(' • ');
window.addEventListener('resize', this.onResize); });
},
unmounted () { const toggleFullScreen = () => {
window.removeEventListener('resize', this.onResize); if (isMaximized.value)
}, w.value.unmaximize();
methods: { else
closeApp () { w.value.maximize();
ipcRenderer.send('close-app');
},
minimizeApp () {
this.w.minimize();
},
toggleFullScreen () {
if (this.isMaximized)
this.w.unmaximize();
else
this.w.maximize();
},
openDevTools () {
this.w.openDevTools();
},
reload () {
this.w.reload();
},
onResize () {
this.isMaximized = this.w.isMaximized();
}
}
}; };
const openDevTools = () => {
w.value.webContents.openDevTools();
};
const reload = () => {
w.value.reload();
};
const onResize = () => {
isMaximized.value = w.value.isMaximized();
};
window.addEventListener('resize', onResize);
onUnmounted(() => {
window.removeEventListener('resize', onResize);
});
</script> </script>
<style lang="scss"> <style lang="scss">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
@close-context="closeContext" @close-context="closeContext"
> >
<div <div
v-if="['procedure', 'function'].includes(selectedMisc.type)" v-if="['procedure', 'routine', 'function'].includes(selectedMisc.type)"
class="context-element" class="context-element"
@click="runElementCheck" @click="runElementCheck"
> >
@ -56,7 +56,7 @@
</ConfirmModal> </ConfirmModal>
<ModalAskParameters <ModalAskParameters
v-if="isAskingParameters" v-if="isAskingParameters"
:local-routine="localElement" :local-routine="(localElement as any)"
:client="workspace.client" :client="workspace.client"
@confirm="runElement" @confirm="runElement"
@close="hideAskParamsModal" @close="hideAskParamsModal"
@ -64,324 +64,333 @@
</BaseContextMenu> </BaseContextMenu>
</template> </template>
<script> <script setup lang="ts">
import { computed, Prop, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu.vue';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import ModalAskParameters from '@/components/ModalAskParameters'; import ModalAskParameters from '@/components/ModalAskParameters.vue';
import Triggers from '@/ipc-api/Triggers'; import Triggers from '@/ipc-api/Triggers';
import Routines from '@/ipc-api/Routines'; import Routines from '@/ipc-api/Routines';
import Functions from '@/ipc-api/Functions'; import Functions from '@/ipc-api/Functions';
import Schedulers from '@/ipc-api/Schedulers'; import Schedulers from '@/ipc-api/Schedulers';
import { storeToRefs } from 'pinia'; import { EventInfos, FunctionInfos, IpcResponse, RoutineInfos, TriggerInfos } from 'common/interfaces/antares';
export default { const { t } = useI18n();
name: 'WorkspaceExploreBarMiscContext',
components: {
BaseContextMenu,
ConfirmModal,
ModalAskParameters
},
props: {
contextEvent: MouseEvent,
selectedMisc: Object,
selectedSchema: String
},
emits: ['close-context', 'reload'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const props = defineProps({
contextEvent: MouseEvent,
selectedMisc: Object as Prop<{ name:string; type:string; enabled?: boolean }>,
selectedSchema: String
});
const { const emit = defineEmits(['close-context', 'reload']);
getWorkspace,
changeBreadcrumbs,
addLoadingElement,
removeLoadingElement,
removeTabs,
newTab
} = workspacesStore;
return { const { addNotification } = useNotificationsStore();
addNotification, const workspacesStore = useWorkspacesStore();
selectedWorkspace,
getWorkspace,
changeBreadcrumbs,
addLoadingElement,
removeLoadingElement,
removeTabs,
newTab
};
},
data () {
return {
isDeleteModal: false,
isRunModal: false,
isAskingParameters: false,
localElement: {}
};
},
computed: {
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
customizations () {
return this.getWorkspace(this.selectedWorkspace).customizations;
},
deleteMessage () {
switch (this.selectedMisc.type) {
case 'trigger':
return this.$t('message.deleteTrigger');
case 'procedure':
return this.$t('message.deleteRoutine');
case 'function':
case 'triggerFunction':
return this.$t('message.deleteFunction');
case 'scheduler':
return this.$t('message.deleteScheduler');
default:
return '';
}
}
},
methods: {
showDeleteModal () {
this.isDeleteModal = true;
},
hideDeleteModal () {
this.isDeleteModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
this.closeContext();
},
closeContext () {
this.$emit('close-context');
},
async deleteMisc () {
try {
let res;
switch (this.selectedMisc.type) { const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
case 'trigger':
res = await Triggers.dropTrigger({
uid: this.selectedWorkspace,
schema: this.selectedSchema,
trigger: this.selectedMisc.name
});
break;
case 'procedure':
res = await Routines.dropRoutine({
uid: this.selectedWorkspace,
schema: this.selectedSchema,
routine: this.selectedMisc.name
});
break;
case 'function':
case 'triggerFunction':
res = await Functions.dropFunction({
uid: this.selectedWorkspace,
schema: this.selectedSchema,
func: this.selectedMisc.name
});
break;
case 'scheduler':
res = await Schedulers.dropScheduler({
uid: this.selectedWorkspace,
schema: this.selectedSchema,
scheduler: this.selectedMisc.name
});
break;
}
const { status, response } = res; const {
getWorkspace,
addLoadingElement,
removeLoadingElement,
removeTabs,
newTab
} = workspacesStore;
if (status === 'success') { const isDeleteModal = ref(false);
this.removeTabs({ const isAskingParameters = ref(false);
uid: this.selectedWorkspace, const localElement: Ref<TriggerInfos | RoutineInfos | FunctionInfos | EventInfos> = ref(null);
elementName: this.selectedMisc.name,
elementType: this.selectedMisc.type,
schema: this.selectedSchema
});
this.closeContext(); const workspace = computed(() => {
this.$emit('reload'); return getWorkspace(selectedWorkspace.value);
} });
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
runElementCheck () {
if (this.selectedMisc.type === 'procedure')
this.runRoutineCheck();
else if (this.selectedMisc.type === 'function')
this.runFunctionCheck();
},
runElement (params) {
if (this.selectedMisc.type === 'procedure')
this.runRoutine(params);
else if (this.selectedMisc.type === 'function')
this.runFunction(params);
},
async runRoutineCheck () {
const params = {
uid: this.selectedWorkspace,
schema: this.selectedSchema,
routine: this.selectedMisc.name
};
try { const customizations = computed(() => {
const { status, response } = await Routines.getRoutineInformations(params); return getWorkspace(selectedWorkspace.value).customizations;
if (status === 'success') });
this.localElement = response;
else const deleteMessage = computed(() => {
this.addNotification({ status: 'error', message: response }); switch (props.selectedMisc.type) {
} case 'trigger':
catch (err) { return t('message.deleteTrigger');
this.addNotification({ status: 'error', message: err.stack }); case 'procedure':
} return t('message.deleteRoutine');
case 'function':
case 'triggerFunction':
return t('message.deleteFunction');
case 'scheduler':
return t('message.deleteScheduler');
default:
return '';
}
});
if (this.localElement.parameters.length) const showDeleteModal = () => {
this.showAskParamsModal(); isDeleteModal.value = true;
else };
this.runRoutine();
},
runRoutine (params) {
if (!params) params = [];
let sql; const hideDeleteModal = () => {
switch (this.workspace.client) { // TODO: move in a better place isDeleteModal.value = false;
case 'maria': };
case 'mysql':
case 'pg':
sql = `CALL ${this.localElement.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `EXEC ${this.localElement.name} ${params.join(',')}`;
break;
default:
sql = `CALL \`${this.localElement.name}\`(${params.join(',')})`;
}
this.newTab({ uid: this.workspace.uid, content: sql, type: 'query', autorun: true }); const showAskParamsModal = () => {
this.closeContext(); isAskingParameters.value = true;
}, };
async runFunctionCheck () {
const params = {
uid: this.selectedWorkspace,
schema: this.selectedSchema,
func: this.selectedMisc.name
};
try { const hideAskParamsModal = () => {
const { status, response } = await Functions.getFunctionInformations(params); isAskingParameters.value = false;
if (status === 'success') closeContext();
this.localElement = response; };
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
if (this.localElement.parameters.length) const closeContext = () => {
this.showAskParamsModal(); emit('close-context');
else };
this.runFunction();
},
runFunction (params) {
if (!params) params = [];
let sql; const deleteMisc = async () => {
switch (this.workspace.client) { // TODO: move in a better place try {
case 'maria': let res: IpcResponse;
case 'mysql':
sql = `SELECT \`${this.localElement.name}\` (${params.join(',')})`;
break;
case 'pg':
sql = `SELECT ${this.localElement.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `SELECT ${this.localElement.name} ${params.join(',')}`;
break;
default:
sql = `SELECT \`${this.localElement.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.workspace.uid, content: sql, type: 'query', autorun: true }); switch (props.selectedMisc.type) {
this.closeContext(); case 'trigger':
}, res = await Triggers.dropTrigger({
async toggleTrigger () { uid: selectedWorkspace.value,
this.addLoadingElement({ schema: props.selectedSchema,
name: this.selectedMisc.name, trigger: props.selectedMisc.name
schema: this.selectedSchema,
type: 'trigger'
});
try {
const { status, response } = await Triggers.toggleTrigger({
uid: this.selectedWorkspace,
schema: this.selectedSchema,
trigger: this.selectedMisc.name,
enabled: this.selectedMisc.enabled
}); });
break;
if (status !== 'success') case 'routine':
this.addNotification({ status: 'error', message: response }); case 'procedure':
} res = await Routines.dropRoutine({
catch (err) { uid: selectedWorkspace.value,
this.addNotification({ status: 'error', message: err.stack }); schema: props.selectedSchema,
} routine: props.selectedMisc.name
this.removeLoadingElement({
name: this.selectedMisc.name,
schema: this.selectedSchema,
type: 'trigger'
});
this.closeContext();
this.$emit('reload');
},
async toggleScheduler () {
this.addLoadingElement({
name: this.selectedMisc.name,
schema: this.selectedSchema,
type: 'scheduler'
});
try {
const { status, response } = await Schedulers.toggleScheduler({
uid: this.selectedWorkspace,
schema: this.selectedSchema,
scheduler: this.selectedMisc.name,
enabled: this.selectedMisc.enabled
}); });
break;
case 'function':
case 'triggerFunction':
res = await Functions.dropFunction({
uid: selectedWorkspace.value,
schema: props.selectedSchema,
func: props.selectedMisc.name
});
break;
case 'scheduler':
res = await Schedulers.dropScheduler({
uid: selectedWorkspace.value,
schema: props.selectedSchema,
scheduler: props.selectedMisc.name
});
break;
}
if (status !== 'success') console.log(res);
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.removeLoadingElement({ const { status, response } = res;
name: this.selectedMisc.name,
schema: this.selectedSchema, if (status === 'success') {
type: 'scheduler' removeTabs({
uid: selectedWorkspace.value,
elementName: props.selectedMisc.name,
elementType: props.selectedMisc.type,
schema: props.selectedSchema
}); });
this.closeContext(); closeContext();
this.$emit('reload'); emit('reload');
} }
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
} }
}; };
const runElementCheck = () => {
if (['procedure', 'routine'].includes(props.selectedMisc.type))
runRoutineCheck();
else if (props.selectedMisc.type === 'function')
runFunctionCheck();
};
const runElement = (params: string[]) => {
if (props.selectedMisc.type === 'procedure')
runRoutine(params);
else if (props.selectedMisc.type === 'function')
runFunction(params);
};
const runRoutineCheck = async () => {
const params = {
uid: selectedWorkspace.value,
schema: props.selectedSchema,
routine: props.selectedMisc.name
};
try {
const { status, response } = await Routines.getRoutineInformations(params);
if (status === 'success')
localElement.value = response;
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
if ((localElement.value as RoutineInfos).parameters.length)
showAskParamsModal();
else
runRoutine();
};
const runRoutine = (params?: string[]) => {
if (!params) params = [];
let sql;
switch (workspace.value.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `CALL ${localElement.value.name}(${params.join(',')})`;
break;
// case 'mssql':
// sql = `EXEC ${localElement.value.name} ${params.join(',')}`;
// break;
default:
sql = `CALL \`${localElement.value.name}\`(${params.join(',')})`;
}
newTab({
uid: workspace.value.uid,
content: sql,
type: 'query',
schema: props.selectedSchema,
autorun: true
});
closeContext();
};
const runFunctionCheck = async () => {
const params = {
uid: selectedWorkspace.value,
schema: props.selectedSchema,
func: props.selectedMisc.name
};
try {
const { status, response } = await Functions.getFunctionInformations(params);
if (status === 'success')
localElement.value = response;
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
if ((localElement.value as FunctionInfos).parameters.length)
showAskParamsModal();
else
runFunction();
};
const runFunction = (params?: string[]) => {
if (!params) params = [];
let sql;
switch (workspace.value.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
sql = `SELECT \`${localElement.value.name}\` (${params.join(',')})`;
break;
case 'pg':
sql = `SELECT ${localElement.value.name}(${params.join(',')})`;
break;
// case 'mssql':
// sql = `SELECT ${localElement.value.name} ${params.join(',')}`;
// break;
default:
sql = `SELECT \`${localElement.value.name}\` (${params.join(',')})`;
}
newTab({
uid: workspace.value.uid,
content: sql,
type: 'query',
schema: props.selectedSchema,
autorun: true
});
closeContext();
};
const toggleTrigger = async () => {
addLoadingElement({
name: props.selectedMisc.name,
schema: props.selectedSchema,
type: 'trigger'
});
try {
const { status, response } = await Triggers.toggleTrigger({
uid: selectedWorkspace.value,
schema: props.selectedSchema,
trigger: props.selectedMisc.name,
enabled: props.selectedMisc.enabled
});
if (status !== 'success')
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
removeLoadingElement({
name: props.selectedMisc.name,
schema: props.selectedSchema,
type: 'trigger'
});
closeContext();
emit('reload');
};
const toggleScheduler = async () => {
addLoadingElement({
name: props.selectedMisc.name,
schema: props.selectedSchema,
type: 'scheduler'
});
try {
const { status, response } = await Schedulers.toggleScheduler({
uid: selectedWorkspace.value,
schema: props.selectedSchema,
scheduler: props.selectedMisc.name,
enabled: props.selectedMisc.enabled
});
if (status !== 'success')
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
removeLoadingElement({
name: props.selectedMisc.name,
schema: props.selectedSchema,
type: 'scheduler'
});
closeContext();
emit('reload');
};
</script> </script>

View File

@ -1,112 +1,68 @@
<template> <template>
<BaseContextMenu <BaseContextMenu
:context-event="contextEvent" :context-event="props.contextEvent"
@close-context="closeContext" @close-context="closeContext"
> >
<div <div
v-if="selectedMisc === 'trigger'" v-if="props.selectedMisc === 'trigger'"
class="context-element" class="context-element"
@click="$emit('open-create-trigger-tab')" @click="emit('open-create-trigger-tab')"
> >
<span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ $t('message.createNewTrigger') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ t('message.createNewTrigger') }}</span>
</div> </div>
<div <div
v-if="selectedMisc === 'procedure'" v-if="['procedure', 'routine'].includes(props.selectedMisc)"
class="context-element" class="context-element"
@click="$emit('open-create-routine-tab')" @click="emit('open-create-routine-tab')"
> >
<span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle text-light pr-1" /> {{ $t('message.createNewRoutine') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle text-light pr-1" /> {{ t('message.createNewRoutine') }}</span>
</div> </div>
<div <div
v-if="selectedMisc === 'function'" v-if="props.selectedMisc === 'function'"
class="context-element" class="context-element"
@click="$emit('open-create-function-tab')" @click="emit('open-create-function-tab')"
> >
<span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box text-light pr-1" /> {{ $t('message.createNewFunction') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box text-light pr-1" /> {{ t('message.createNewFunction') }}</span>
</div> </div>
<div <div
v-if="selectedMisc === 'triggerFunction'" v-if="props.selectedMisc === 'triggerFunction'"
class="context-element" class="context-element"
@click="$emit('open-create-trigger-function-tab')" @click="emit('open-create-trigger-function-tab')"
> >
<span class="d-flex"><i class="mdi mdi-18px mdi-cog-clockwise text-light pr-1" /> {{ $t('message.createNewFunction') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-cog-clockwise text-light pr-1" /> {{ t('message.createNewFunction') }}</span>
</div> </div>
<div <div
v-if="selectedMisc === 'scheduler'" v-if="props.selectedMisc === 'scheduler'"
class="context-element" class="context-element"
@click="$emit('open-create-scheduler-tab')" @click="emit('open-create-scheduler-tab')"
> >
<span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ $t('message.createNewScheduler') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ t('message.createNewScheduler') }}</span>
</div> </div>
</BaseContextMenu> </BaseContextMenu>
</template> </template>
<script> <script setup lang="ts">
import { useNotificationsStore } from '@/stores/notifications'; import { useI18n } from 'vue-i18n';
import { useWorkspacesStore } from '@/stores/workspaces'; import BaseContextMenu from '@/components/BaseContextMenu.vue';
import BaseContextMenu from '@/components/BaseContextMenu';
import { storeToRefs } from 'pinia';
export default { const { t } = useI18n();
name: 'WorkspaceExploreBarMiscContext',
components: {
BaseContextMenu
},
props: {
contextEvent: MouseEvent,
selectedMisc: String,
selectedSchema: String
},
emits: [
'open-create-trigger-tab',
'open-create-routine-tab',
'open-create-function-tab',
'open-create-trigger-function-tab',
'open-create-scheduler-tab',
'close-context'
],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const props = defineProps({
contextEvent: MouseEvent,
selectedMisc: String,
selectedSchema: String
});
const { getWorkspace, changeBreadcrumbs } = workspacesStore; const emit = defineEmits([
'open-create-trigger-tab',
'open-create-routine-tab',
'open-create-function-tab',
'open-create-trigger-function-tab',
'open-create-scheduler-tab',
'close-context'
]);
return { const closeContext = () => {
addNotification, emit('close-context');
selectedWorkspace,
getWorkspace,
changeBreadcrumbs
};
},
data () {
return {
localElement: {}
};
},
computed: {
workspace () {
return this.getWorkspace(this.selectedWorkspace);
}
},
methods: {
showDeleteModal () {
this.isDeleteModal = true;
},
hideDeleteModal () {
this.isDeleteModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
this.closeContext();
},
closeContext () {
this.$emit('close-context');
}
}
}; };
</script> </script>

View File

@ -25,7 +25,7 @@
<ul class="menu menu-nav pt-0"> <ul class="menu menu-nav pt-0">
<li <li
v-for="table of filteredTables" v-for="table of filteredTables"
:ref="breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name) ? 'explorebar-selected' : ''" :ref="breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name) ? 'explorebarSelected' : ''"
:key="table.name" :key="table.name"
class="menu-item" class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}" :class="{'selected': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}"
@ -61,7 +61,7 @@
@contextmenu.prevent="showMiscFolderContext($event, 'trigger')" @contextmenu.prevent="showMiscFolderContext($event, 'trigger')"
> >
<i class="misc-icon mdi mdi-18px mdi-folder-cog mr-1" /> <i class="misc-icon mdi mdi-18px mdi-folder-cog mr-1" />
{{ $tc('word.trigger', 2) }} {{ t('word.trigger', 2) }}
</summary> </summary>
<div class="accordion-body"> <div class="accordion-body">
<div> <div>
@ -69,7 +69,7 @@
<li <li
v-for="trigger of filteredTriggers" v-for="trigger of filteredTriggers"
:key="trigger.name" :key="trigger.name"
:ref="breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name ? 'explorebar-selected' : ''" :ref="breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name ? 'explorebarSelected' : ''"
class="menu-item" class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}" :class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}"
@mousedown.left="selectMisc({schema: database.name, misc: trigger, type: 'trigger'})" @mousedown.left="selectMisc({schema: database.name, misc: trigger, type: 'trigger'})"
@ -84,7 +84,7 @@
<div <div
v-if="trigger.enabled === false" v-if="trigger.enabled === false"
class="tooltip tooltip-left disabled-indicator" class="tooltip tooltip-left disabled-indicator"
:data-tooltip="$t('word.disabled')" :data-tooltip="t('word.disabled')"
> >
<i class="table-icon mdi mdi-pause mdi-18px mr-1" /> <i class="table-icon mdi mdi-pause mdi-18px mr-1" />
</div> </div>
@ -100,27 +100,27 @@
<summary <summary
class="accordion-header misc-name" class="accordion-header misc-name"
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.routine}" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.routine}"
@contextmenu.prevent="showMiscFolderContext($event, 'procedure')" @contextmenu.prevent="showMiscFolderContext($event, 'routine')"
> >
<i class="misc-icon mdi mdi-18px mdi-folder-sync mr-1" /> <i class="misc-icon mdi mdi-18px mdi-folder-sync mr-1" />
{{ $tc('word.storedRoutine', 2) }} {{ t('word.storedRoutine', 2) }}
</summary> </summary>
<div class="accordion-body"> <div class="accordion-body">
<div> <div>
<ul class="menu menu-nav pt-0"> <ul class="menu menu-nav pt-0">
<li <li
v-for="(procedure, i) of filteredProcedures" v-for="(routine, i) of filteredProcedures"
:key="`${procedure.name}-${i}`" :key="`${routine.name}-${i}`"
:ref="breadcrumbs.schema === database.name && breadcrumbs.routine === procedure.name ? 'explorebar-selected' : ''" :ref="breadcrumbs.schema === database.name && breadcrumbs.routine === routine.name ? 'explorebarSelected' : ''"
class="menu-item" class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.routine === procedure.name}" :class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.routine === routine.name}"
@mousedown.left="selectMisc({schema: database.name, misc: procedure, type: 'routine'})" @mousedown.left="selectMisc({schema: database.name, misc: routine, type: 'routine'})"
@dblclick="openMiscPermanentTab({schema: database.name, misc: procedure, type: 'routine'})" @dblclick="openMiscPermanentTab({schema: database.name, misc: routine, type: 'routine'})"
@contextmenu.prevent="showMiscContext($event, {...procedure, type: 'procedure'})" @contextmenu.prevent="showMiscContext($event, {...routine, type: 'routine'})"
> >
<a class="table-name"> <a class="table-name">
<i class="table-icon mdi mdi-sync-circle mdi-18px mr-1" /> <i class="table-icon mdi mdi-sync-circle mdi-18px mr-1" />
<span v-html="highlightWord(procedure.name)" /> <span v-html="highlightWord(routine.name)" />
</a> </a>
</li> </li>
</ul> </ul>
@ -137,7 +137,7 @@
@contextmenu.prevent="showMiscFolderContext($event, 'triggerFunction')" @contextmenu.prevent="showMiscFolderContext($event, 'triggerFunction')"
> >
<i class="misc-icon mdi mdi-18px mdi-folder-refresh mr-1" /> <i class="misc-icon mdi mdi-18px mdi-folder-refresh mr-1" />
{{ $tc('word.triggerFunction', 2) }} {{ t('word.triggerFunction', 2) }}
</summary> </summary>
<div class="accordion-body"> <div class="accordion-body">
<div> <div>
@ -145,7 +145,7 @@
<li <li
v-for="(func, i) of filteredTriggerFunctions" v-for="(func, i) of filteredTriggerFunctions"
:key="`${func.name}-${i}`" :key="`${func.name}-${i}`"
:ref="breadcrumbs.schema === database.name && breadcrumbs.triggerFunction === func.name ? 'explorebar-selected' : ''" :ref="breadcrumbs.schema === database.name && breadcrumbs.triggerFunction === func.name ? 'explorebarSelected' : ''"
class="menu-item" class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.triggerFunction === func.name}" :class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.triggerFunction === func.name}"
@mousedown.left="selectMisc({schema: database.name, misc: func, type: 'triggerFunction'})" @mousedown.left="selectMisc({schema: database.name, misc: func, type: 'triggerFunction'})"
@ -171,7 +171,7 @@
@contextmenu.prevent="showMiscFolderContext($event, 'function')" @contextmenu.prevent="showMiscFolderContext($event, 'function')"
> >
<i class="misc-icon mdi mdi-18px mdi-folder-move mr-1" /> <i class="misc-icon mdi mdi-18px mdi-folder-move mr-1" />
{{ $tc('word.function', 2) }} {{ t('word.function', 2) }}
</summary> </summary>
<div class="accordion-body"> <div class="accordion-body">
<div> <div>
@ -179,7 +179,7 @@
<li <li
v-for="(func, i) of filteredFunctions" v-for="(func, i) of filteredFunctions"
:key="`${func.name}-${i}`" :key="`${func.name}-${i}`"
:ref="breadcrumbs.schema === database.name && breadcrumbs.function === func.name ? 'explorebar-selected' : ''" :ref="breadcrumbs.schema === database.name && breadcrumbs.function === func.name ? 'explorebarSelected' : ''"
class="menu-item" class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}" :class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
@mousedown.left="selectMisc({schema: database.name, misc: func, type: 'function'})" @mousedown.left="selectMisc({schema: database.name, misc: func, type: 'function'})"
@ -205,7 +205,7 @@
@contextmenu.prevent="showMiscFolderContext($event, 'scheduler')" @contextmenu.prevent="showMiscFolderContext($event, 'scheduler')"
> >
<i class="misc-icon mdi mdi-18px mdi-folder-clock mr-1" /> <i class="misc-icon mdi mdi-18px mdi-folder-clock mr-1" />
{{ $tc('word.scheduler', 2) }} {{ t('word.scheduler', 2) }}
</summary> </summary>
<div class="accordion-body"> <div class="accordion-body">
<div> <div>
@ -213,7 +213,7 @@
<li <li
v-for="scheduler of filteredSchedulers" v-for="scheduler of filteredSchedulers"
:key="scheduler.name" :key="scheduler.name"
:ref="breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name ? 'explorebar-selected' : ''" :ref="breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name ? 'explorebarSelected' : ''"
class="menu-item" class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}" :class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}"
@mousedown.left="selectMisc({schema: database.name, misc: scheduler, type: 'scheduler'})" @mousedown.left="selectMisc({schema: database.name, misc: scheduler, type: 'scheduler'})"
@ -228,7 +228,7 @@
<div <div
v-if="scheduler.enabled === false" v-if="scheduler.enabled === false"
class="tooltip tooltip-left disabled-indicator" class="tooltip tooltip-left disabled-indicator"
:data-tooltip="$t('word.disabled')" :data-tooltip="t('word.disabled')"
> >
<i class="table-icon mdi mdi-pause mdi-18px mr-1" /> <i class="table-icon mdi mdi-pause mdi-18px mr-1" />
</div> </div>
@ -242,226 +242,235 @@
</details> </details>
</template> </template>
<script> <script setup lang="ts">
import { useSettingsStore } from '@/stores/settings'; import { computed, Prop, Ref, ref, watch } from 'vue';
import { useWorkspacesStore } from '@/stores/workspaces';
import { formatBytes } from 'common/libs/formatBytes';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useSettingsStore } from '@/stores/settings';
import { Breadcrumb, useWorkspacesStore, WorkspaceStructure } from '@/stores/workspaces';
import { formatBytes } from 'common/libs/formatBytes';
import { EventInfos, FunctionInfos, RoutineInfos, TableInfos, TriggerFunctionInfos, TriggerInfos } from 'common/interfaces/antares';
export default { const { t } = useI18n();
name: 'WorkspaceExploreBarSchema',
props: {
database: Object,
connection: Object
},
emits: [
'show-schema-context',
'show-table-context',
'show-misc-context',
'show-misc-folder-context'
],
setup () {
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const { applicationTheme } = storeToRefs(settingsStore); const props = defineProps({
database: Object as Prop<WorkspaceStructure>,
connection: Object
});
const { const emit = defineEmits([
getLoadedSchemas, 'show-schema-context',
getWorkspace, 'show-table-context',
getSearchTerm, 'show-misc-context',
changeBreadcrumbs, 'show-misc-folder-context'
addLoadedSchema, ]);
newTab,
refreshSchema
} = workspacesStore;
return { const settingsStore = useSettingsStore();
applicationTheme, const workspacesStore = useWorkspacesStore();
getLoadedSchemas,
getWorkspace,
getSearchTerm,
changeBreadcrumbs,
addLoadedSchema,
newTab,
refreshSchema
};
},
data () {
return {
isLoading: false
};
},
computed: {
searchTerm () {
return this.getSearchTerm(this.connection.uid);
},
filteredTables () {
return this.database.tables.filter(table => table.name.search(this.searchTerm) >= 0);
},
filteredTriggers () {
return this.database.triggers.filter(trigger => trigger.name.search(this.searchTerm) >= 0);
},
filteredProcedures () {
return this.database.procedures.filter(procedure => procedure.name.search(this.searchTerm) >= 0);
},
filteredFunctions () {
return this.database.functions.filter(func => func.name.search(this.searchTerm) >= 0);
},
filteredTriggerFunctions () {
return this.database.triggerFunctions
? this.database.triggerFunctions.filter(func => func.name.search(this.searchTerm) >= 0)
: [];
},
filteredSchedulers () {
return this.database.schedulers.filter(scheduler => scheduler.name.search(this.searchTerm) >= 0);
},
workspace () {
return this.getWorkspace(this.connection.uid);
},
breadcrumbs () {
return this.workspace.breadcrumbs;
},
customizations () {
return this.workspace.customizations;
},
loadedSchemas () {
return this.getLoadedSchemas(this.connection.uid);
},
maxSize () {
return this.database.tables.reduce((acc, curr) => {
if (curr.size > acc) acc = curr.size;
return acc;
}, 0);
},
totalSize () {
return this.database.tables.reduce((acc, curr) => acc + curr.size, 0);
}
},
watch: {
breadcrumbs (newVal, oldVal) {
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
setTimeout(() => {
const element = this.$refs['explorebar-selected'] ? this.$refs['explorebar-selected'][0] : null;
if (element) {
const rect = element.getBoundingClientRect();
const elemTop = rect.top;
const elemBottom = rect.bottom;
const isVisible = (elemTop >= 0) && (elemBottom <= window.innerHeight);
if (!isVisible) { const { applicationTheme } = storeToRefs(settingsStore);
element.setAttribute('tabindex', '-1');
element.focus(); const {
element.removeAttribute('tabindex'); getLoadedSchemas,
} getWorkspace,
} getSearchTerm,
}, 50); changeBreadcrumbs,
addLoadedSchema,
newTab,
refreshSchema
} = workspacesStore;
const schemaAccordion: Ref<HTMLDetailsElement> = ref(null);
const explorebarSelected: Ref<HTMLElement[]> = ref(null);
const isLoading = ref(false);
const searchTerm = computed(() => {
return getSearchTerm(props.connection.uid);
});
const filteredTables = computed(() => {
return props.database.tables.filter(table => table.name.search(searchTerm.value) >= 0);
});
const filteredTriggers = computed(() => {
return props.database.triggers.filter(trigger => trigger.name.search(searchTerm.value) >= 0);
});
const filteredProcedures = computed(() => {
return props.database.procedures.filter(procedure => procedure.name.search(searchTerm.value) >= 0);
});
const filteredFunctions = computed(() => {
return props.database.functions.filter(func => func.name.search(searchTerm.value) >= 0);
});
const filteredTriggerFunctions = computed(() => {
return props.database.triggerFunctions
? props.database.triggerFunctions.filter(func => func.name.search(searchTerm.value) >= 0)
: [];
});
const filteredSchedulers = computed(() => {
return props.database.schedulers.filter(scheduler => scheduler.name.search(searchTerm.value) >= 0);
});
const workspace = computed(() => {
return getWorkspace(props.connection.uid);
});
const breadcrumbs = computed(() => {
return workspace.value.breadcrumbs;
});
const customizations = computed(() => {
return workspace.value.customizations;
});
const loadedSchemas = computed(() => {
return getLoadedSchemas(props.connection.uid);
});
const maxSize = computed(() => {
return props.database.tables.reduce((acc: number, curr) => {
if (curr.size && curr.size > acc) acc = curr.size;
return acc;
}, 0);
});
watch(breadcrumbs, (newVal, oldVal) => {
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
setTimeout(() => {
const element = explorebarSelected.value ? explorebarSelected.value[0] : null;
if (element) {
const rect = element.getBoundingClientRect();
const elemTop = rect.top;
const elemBottom = rect.bottom;
const isVisible = (elemTop >= 0) && (elemBottom <= window.innerHeight);
if (!isVisible) {
element.setAttribute('tabindex', '-1');
element.focus();
element.removeAttribute('tabindex');
}
} }
} }, 50);
}, }
methods: { });
formatBytes,
async selectSchema (schema) {
if (!this.loadedSchemas.has(schema) && !this.isLoading) {
this.isLoading = true;
await this.refreshSchema({ uid: this.connection.uid, schema });
this.addLoadedSchema(schema);
this.isLoading = false;
}
},
selectTable ({ schema, table }) {
this.newTab({
uid: this.connection.uid,
elementName: table.name,
schema: this.database.name,
type: 'temp-data',
elementType: table.type
});
this.setBreadcrumbs({ schema, [table.type]: table.name }); const selectSchema = async (schema: string) => {
}, if (!loadedSchemas.value.has(schema) && !isLoading.value) {
selectMisc ({ schema, misc, type }) { isLoading.value = true;
const miscTempTabs = { setBreadcrumbs({ schema });
trigger: 'temp-trigger-props', await refreshSchema({ uid: props.connection.uid, schema });
triggerFunction: 'temp-trigger-function-props', addLoadedSchema(schema);
function: 'temp-function-props', isLoading.value = false;
routine: 'temp-routine-props',
scheduler: 'temp-scheduler-props'
};
this.newTab({
uid: this.connection.uid,
elementName: misc.name,
schema: this.database.name,
type: miscTempTabs[type],
elementType: type
});
this.setBreadcrumbs({ schema, [type]: misc.name });
},
openDataTab ({ schema, table }) {
this.newTab({ uid: this.connection.uid, elementName: table.name, schema: this.database.name, type: 'data', elementType: table.type });
this.setBreadcrumbs({ schema, [table.type]: table.name });
},
openMiscPermanentTab ({ schema, misc, type }) {
const miscTabs = {
trigger: 'trigger-props',
triggerFunction: 'trigger-function-props',
function: 'function-props',
routine: 'routine-props',
scheduler: 'scheduler-props'
};
this.newTab({
uid: this.connection.uid,
elementName: misc.name,
schema: this.database.name,
type: miscTabs[type],
elementType: type
});
this.setBreadcrumbs({ schema, [type]: misc.name });
},
showSchemaContext (event, schema) {
this.$emit('show-schema-context', { event, schema });
},
showTableContext (event, table) {
this.$emit('show-table-context', { event, schema: this.database.name, table });
},
showMiscContext (event, misc) {
this.$emit('show-misc-context', { event, schema: this.database.name, misc });
},
showMiscFolderContext (event, type) {
this.$emit('show-misc-folder-context', { event, schema: this.database.name, type });
},
piePercentage (val) {
const perc = val / this.maxSize * 100;
if (this.applicationTheme === 'dark')
return { background: `conic-gradient(lime ${perc}%, white 0)` };
else
return { background: `conic-gradient(teal ${perc}%, silver 0)` };
},
setBreadcrumbs (payload) {
if (this.breadcrumbs.schema === payload.schema && this.breadcrumbs.table === payload.table) return;
this.changeBreadcrumbs(payload);
},
highlightWord (string) {
string = string.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
if (this.searchTerm) {
const regexp = new RegExp(`(${this.searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return string.replace(regexp, '<span class="text-primary">$1</span>');
}
else
return string;
},
checkLoadingStatus (name, type) {
return this.workspace.loadingElements.some(el =>
el.name === name &&
el.type === type &&
el.schema === this.database.name);
}
} }
}; };
const selectTable = ({ schema, table }: { schema: string; table: TableInfos }) => {
newTab({
uid: props.connection.uid,
elementName: table.name,
schema: props.database.name,
type: 'temp-data',
elementType: table.type
});
setBreadcrumbs({ schema, [table.type]: table.name });
};
const selectMisc = ({ schema, misc, type }: { schema: string; misc: { name: string }; type: 'trigger' | 'triggerFunction' | 'function' | 'routine' | 'scheduler' }) => {
const miscTempTabs = {
trigger: 'temp-trigger-props',
triggerFunction: 'temp-trigger-function-props',
function: 'temp-function-props',
routine: 'temp-routine-props',
scheduler: 'temp-scheduler-props'
};
newTab({
uid: props.connection.uid,
elementName: misc.name,
schema: props.database.name,
type: miscTempTabs[type],
elementType: type
});
setBreadcrumbs({ schema, [type]: misc.name });
};
const openDataTab = ({ schema, table }: { schema: string; table: TableInfos }) => {
newTab({ uid: props.connection.uid, elementName: table.name, schema: props.database.name, type: 'data', elementType: table.type });
setBreadcrumbs({ schema, [table.type]: table.name });
};
const openMiscPermanentTab = ({ schema, misc, type }: { schema: string; misc: { name: string }; type: 'trigger' | 'triggerFunction' | 'function' | 'routine' | 'scheduler' }) => {
const miscTabs = {
trigger: 'trigger-props',
triggerFunction: 'trigger-function-props',
function: 'function-props',
routine: 'routine-props',
scheduler: 'scheduler-props'
};
newTab({
uid: props.connection.uid,
elementName: misc.name,
schema: props.database.name,
type: miscTabs[type],
elementType: type
});
setBreadcrumbs({ schema, [type]: misc.name });
};
const showSchemaContext = (event: MouseEvent, schema: string) => {
emit('show-schema-context', { event, schema });
};
const showTableContext = (event: MouseEvent, table: TableInfos) => {
emit('show-table-context', { event, schema: props.database.name, table });
};
const showMiscContext = (event: MouseEvent, misc: TriggerInfos | TriggerFunctionInfos | RoutineInfos | FunctionInfos | EventInfos) => {
emit('show-misc-context', { event, schema: props.database.name, misc });
};
const showMiscFolderContext = (event: MouseEvent, type: string) => {
emit('show-misc-folder-context', { event, schema: props.database.name, type });
};
const piePercentage = (val: number) => {
const perc = val / maxSize.value * 100;
if (applicationTheme.value === 'dark')
return { background: `conic-gradient(lime ${perc}%, white 0)` };
else
return { background: `conic-gradient(teal ${perc}%, silver 0)` };
};
const setBreadcrumbs = (payload: Breadcrumb) => {
if (breadcrumbs.value.schema === payload.schema && breadcrumbs.value.table === payload.table) return;
changeBreadcrumbs(payload);
};
const highlightWord = (string: string) => {
string = string.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
if (searchTerm.value) {
const regexp = new RegExp(`(${searchTerm.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return string.replace(regexp, '<span class="text-primary">$1</span>');
}
else
return string;
};
const checkLoadingStatus = (name: string, type: string) => {
return workspace.value.loadingElements.some(el =>
el.name === name &&
el.type === type &&
el.schema === props.database.name);
};
defineExpose({ selectSchema, schemaAccordion });
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -123,159 +123,154 @@
</BaseContextMenu> </BaseContextMenu>
</template> </template>
<script> <script setup lang="ts">
import { Component, computed, nextTick, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu.vue';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import ModalEditSchema from '@/components/ModalEditSchema'; import ModalEditSchema from '@/components/ModalEditSchema.vue';
import ModalExportSchema from '@/components/ModalExportSchema'; import ModalExportSchema from '@/components/ModalExportSchema.vue';
import ModalImportSchema from '@/components/ModalImportSchema'; import ModalImportSchema from '@/components/ModalImportSchema.vue';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import Application from '@/ipc-api/Application'; import Application from '@/ipc-api/Application';
import { storeToRefs } from 'pinia';
export default { const props = defineProps({
name: 'WorkspaceExploreBarSchemaContext', contextEvent: MouseEvent,
components: { selectedSchema: String
BaseContextMenu, });
ConfirmModal,
ModalEditSchema,
ModalExportSchema,
ModalImportSchema
},
props: {
contextEvent: MouseEvent,
selectedSchema: String
},
emits: [
'open-create-table-tab',
'open-create-view-tab',
'open-create-trigger-tab',
'open-create-routine-tab',
'open-create-function-tab',
'open-create-trigger-function-tab',
'open-create-scheduler-tab',
'close-context',
'reload'
],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const emit = defineEmits([
'open-create-table-tab',
'open-create-view-tab',
'open-create-trigger-tab',
'open-create-routine-tab',
'open-create-function-tab',
'open-create-trigger-function-tab',
'open-create-scheduler-tab',
'close-context',
'reload'
]);
const { const { addNotification } = useNotificationsStore();
getWorkspace, const workspacesStore = useWorkspacesStore();
changeBreadcrumbs
} = workspacesStore;
return { const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
addNotification,
selectedWorkspace, const {
getWorkspace, getWorkspace,
changeBreadcrumbs changeBreadcrumbs
}; } = workspacesStore;
},
data () { const importModalRef: Ref<Component & {startImport: (file: string) => void}> = ref(null);
return { const isDeleteModal = ref(false);
isDeleteModal: false, const isEditModal = ref(false);
isEditModal: false, const isExportSchemaModal = ref(false);
isExportSchemaModal: false, const isImportSchemaModal = ref(false);
isImportSchemaModal: false
}; const workspace = computed(() => getWorkspace(selectedWorkspace.value));
},
computed: { const openCreateTableTab = () => {
workspace () { emit('open-create-table-tab');
return this.getWorkspace(this.selectedWorkspace); };
}
}, const openCreateViewTab = () => {
methods: { emit('open-create-view-tab');
openCreateTableTab () { };
this.$emit('open-create-table-tab');
}, const openCreateTriggerTab = () => {
openCreateViewTab () { emit('open-create-trigger-tab');
this.$emit('open-create-view-tab'); };
},
openCreateTriggerTab () { const openCreateRoutineTab = () => {
this.$emit('open-create-trigger-tab'); emit('open-create-routine-tab');
}, };
openCreateRoutineTab () {
this.$emit('open-create-routine-tab'); const openCreateFunctionTab = () => {
}, emit('open-create-function-tab');
openCreateFunctionTab () { };
this.$emit('open-create-function-tab');
}, const openCreateTriggerFunctionTab = () => {
openCreateTriggerFunctionTab () { emit('open-create-trigger-function-tab');
this.$emit('open-create-trigger-function-tab'); };
},
openCreateSchedulerTab () { const openCreateSchedulerTab = () => {
this.$emit('open-create-scheduler-tab'); emit('open-create-scheduler-tab');
}, };
showDeleteModal () {
this.isDeleteModal = true; const showDeleteModal = () => {
}, isDeleteModal.value = true;
hideDeleteModal () { };
this.isDeleteModal = false;
}, const hideDeleteModal = () => {
showEditModal () { isDeleteModal.value = false;
this.isEditModal = true; };
},
hideEditModal () { const showEditModal = () => {
this.isEditModal = false; isEditModal.value = true;
this.closeContext(); };
},
showExportSchemaModal () { const hideEditModal = () => {
this.isExportSchemaModal = true; isEditModal.value = false;
}, closeContext();
hideExportSchemaModal () { };
this.isExportSchemaModal = false;
this.closeContext(); const showExportSchemaModal = () => {
}, isExportSchemaModal.value = true;
showImportSchemaModal () { };
this.isImportSchemaModal = true;
}, const hideExportSchemaModal = () => {
hideImportSchemaModal () { isExportSchemaModal.value = false;
this.isImportSchemaModal = false; closeContext();
this.closeContext(); };
},
async initImport () { const showImportSchemaModal = () => {
const result = await Application.showOpenDialog({ properties: ['openFile'], filters: [{ name: 'SQL', extensions: ['sql'] }] }); isImportSchemaModal.value = true;
if (result && !result.canceled) { };
const file = result.filePaths[0];
this.showImportSchemaModal(); const hideImportSchemaModal = () => {
this.$nextTick(() => { isImportSchemaModal.value = false;
this.$refs.importModalRef.startImport(file); closeContext();
}); };
}
}, const initImport = async () => {
closeContext () { const result = await Application.showOpenDialog({ properties: ['openFile'], filters: [{ name: 'SQL', extensions: ['sql'] }] });
this.$emit('close-context'); if (result && !result.canceled) {
}, const file = result.filePaths[0];
async deleteSchema () { showImportSchemaModal();
try { await nextTick();
const { status, response } = await Schema.deleteSchema({ importModalRef.value.startImport(file);
uid: this.selectedWorkspace, }
database: this.selectedSchema };
});
const closeContext = () => {
if (status === 'success') { emit('close-context');
if (this.selectedSchema === this.workspace.breadcrumbs.schema) };
this.changeBreadcrumbs({ schema: null });
const deleteSchema = async () => {
this.closeContext(); try {
this.$emit('reload'); const { status, response } = await Schema.deleteSchema({
} uid: selectedWorkspace.value,
else database: props.selectedSchema
this.addNotification({ status: 'error', message: response }); });
}
catch (err) { if (status === 'success') {
this.addNotification({ status: 'error', message: err.stack }); if (props.selectedSchema === workspace.value.breadcrumbs.schema)
} changeBreadcrumbs({ schema: null });
closeContext();
emit('reload');
} }
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
} }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.context-submenu { .context-submenu {
min-width: 150px !important; min-width: 150px !important;

View File

@ -71,151 +71,133 @@
</BaseContextMenu> </BaseContextMenu>
</template> </template>
<script> <script setup lang="ts">
import { computed, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu.vue';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import { storeToRefs } from 'pinia';
export default { const props = defineProps({
name: 'WorkspaceExploreBarTableContext', contextEvent: MouseEvent,
components: { selectedTable: Object,
BaseContextMenu, selectedSchema: String
ConfirmModal });
},
props: {
contextEvent: MouseEvent,
selectedTable: Object,
selectedSchema: String
},
emits: ['close-context', 'duplicate-table', 'reload', 'delete-table'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const emit = defineEmits(['close-context', 'duplicate-table', 'reload', 'delete-table']);
const { const { addNotification } = useNotificationsStore();
getWorkspace, const workspacesStore = useWorkspacesStore();
newTab,
removeTabs,
addLoadingElement,
removeLoadingElement,
changeBreadcrumbs
} = workspacesStore;
return { const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
addNotification,
getWorkspace,
newTab,
removeTabs,
addLoadingElement,
removeLoadingElement,
changeBreadcrumbs,
selectedWorkspace
};
},
data () {
return {
isDeleteModal: false,
isEmptyModal: false
};
},
computed: {
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
customizations () {
return this.workspace && this.workspace.customizations ? this.workspace.customizations : {};
}
},
methods: {
showDeleteModal () {
this.isDeleteModal = true;
},
hideDeleteModal () {
this.isDeleteModal = false;
},
showEmptyModal () {
this.isEmptyModal = true;
},
hideEmptyModal () {
this.isEmptyModal = false;
},
closeContext () {
this.$emit('close-context');
},
openTableSettingTab () {
this.newTab({
uid: this.selectedWorkspace,
elementName: this.selectedTable.name,
schema: this.selectedSchema,
type: 'table-props',
elementType: 'table'
});
this.changeBreadcrumbs({ const {
schema: this.selectedSchema, getWorkspace,
table: this.selectedTable.name newTab,
}); addLoadingElement,
removeLoadingElement,
changeBreadcrumbs
} = workspacesStore;
this.closeContext(); const isDeleteModal = ref(false);
}, const isEmptyModal = ref(false);
openViewSettingTab () {
this.newTab({
uid: this.selectedWorkspace,
elementType: 'table',
elementName: this.selectedTable.name,
schema: this.selectedSchema,
type: 'view-props'
});
this.changeBreadcrumbs({ const workspace = computed(() => getWorkspace(selectedWorkspace.value));
schema: this.selectedSchema, const customizations = computed(() => workspace.value && workspace.value.customizations ? workspace.value.customizations : null);
view: this.selectedTable.name
});
this.closeContext(); const showDeleteModal = () => {
}, isDeleteModal.value = true;
duplicateTable () { };
this.$emit('duplicate-table', { schema: this.selectedSchema, table: this.selectedTable });
},
async emptyTable () {
this.closeContext();
this.addLoadingElement({ const hideDeleteModal = () => {
name: this.selectedTable.name, isDeleteModal.value = false;
schema: this.selectedSchema, };
type: 'table'
});
try { const showEmptyModal = () => {
const { status, response } = await Tables.truncateTable({ isEmptyModal.value = true;
uid: this.selectedWorkspace, };
table: this.selectedTable.name,
schema: this.selectedSchema
});
if (status === 'success') const hideEmptyModal = () => {
this.$emit('reload'); isEmptyModal.value = false;
else };
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.removeLoadingElement({ const closeContext = () => {
name: this.selectedTable.name, emit('close-context');
schema: this.selectedSchema, };
type: 'table'
}); const openTableSettingTab = () => {
}, newTab({
deleteTable () { uid: selectedWorkspace.value,
this.$emit('delete-table', { schema: this.selectedSchema, table: this.selectedTable }); elementName: props.selectedTable.name,
} schema: props.selectedSchema,
type: 'table-props',
elementType: 'table'
});
changeBreadcrumbs({
schema: props.selectedSchema,
table: props.selectedTable.name
});
closeContext();
};
const openViewSettingTab = () => {
newTab({
uid: selectedWorkspace.value,
elementType: 'table',
elementName: props.selectedTable.name,
schema: props.selectedSchema,
type: 'view-props'
});
changeBreadcrumbs({
schema: props.selectedSchema,
view: props.selectedTable.name
});
closeContext();
};
const duplicateTable = () => {
emit('duplicate-table', { schema: props.selectedSchema, table: props.selectedTable });
};
const emptyTable = async () => {
closeContext();
addLoadingElement({
name: props.selectedTable.name,
schema: props.selectedSchema,
type: 'table'
});
try {
const { status, response } = await Tables.truncateTable({
uid: selectedWorkspace.value,
table: props.selectedTable.name,
schema: props.selectedSchema
});
if (status === 'success')
emit('reload');
else
addNotification({ status: 'error', message: response });
} }
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
removeLoadingElement({
name: props.selectedTable.name,
schema: props.selectedSchema,
type: 'table'
});
};
const deleteTable = () => {
emit('delete-table', { schema: props.selectedSchema, table: props.selectedTable });
}; };
</script> </script>

View File

@ -11,27 +11,27 @@
@click="saveChanges" @click="saveChanges"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span> <span>{{ t('word.save') }}</span>
</button> </button>
<button <button
:disabled="!isChanged" :disabled="!isChanged"
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
@click="clearChanges" @click="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
<div class="divider-vert py-3" /> <div class="divider-vert py-3" />
<button class="btn btn-dark btn-sm" @click="showParamsModal"> <button class="btn btn-dark btn-sm" @click="showParamsModal">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" /> <i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span> <span>{{ t('word.parameters') }}</span>
</button> </button>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div> </div>
</div> </div>
@ -42,7 +42,7 @@
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.name') }} {{ t('word.name') }}
</label> </label>
<input <input
ref="firstInput" ref="firstInput"
@ -55,7 +55,7 @@
<div v-if="customizations.languages" class="column col-auto"> <div v-if="customizations.languages" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.language') }} {{ t('word.language') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.language" v-model="localFunction.language"
@ -67,13 +67,13 @@
<div v-if="customizations.definer" class="column col-auto"> <div v-if="customizations.definer" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.definer') }} {{ t('word.definer') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.definer" v-model="localFunction.definer"
:options="[{value: '', name:$t('message.currentUser')}, ...workspace.users]" :options="[{value: '', name:t('message.currentUser')}, ...workspace.users]"
:option-label="(user) => user.value === '' ? user.name : `${user.name}@${user.host}`" :option-label="(user: any) => user.value === '' ? user.name : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``" :option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select" class="form-select"
/> />
</div> </div>
@ -81,13 +81,13 @@
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.returns') }} {{ t('word.returns') }}
</label> </label>
<div class="input-group"> <div class="input-group">
<BaseSelect <BaseSelect
v-model="localFunction.returns" v-model="localFunction.returns"
class="form-select text-uppercase" class="form-select text-uppercase"
:options="[{ name: 'VOID' }, ...workspace.dataTypes]" :options="[{ name: 'VOID' }, ...(workspace.dataTypes as any)]"
group-label="group" group-label="group"
group-values="types" group-values="types"
option-label="name" option-label="name"
@ -101,7 +101,7 @@
class="form-input" class="form-input"
type="number" type="number"
min="0" min="0"
:placeholder="$t('word.length')" :placeholder="t('word.length')"
> >
</div> </div>
</div> </div>
@ -109,7 +109,7 @@
<div v-if="customizations.comment" class="column"> <div v-if="customizations.comment" class="column">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.comment') }} {{ t('word.comment') }}
</label> </label>
<input <input
v-model="localFunction.comment" v-model="localFunction.comment"
@ -121,7 +121,7 @@
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('message.sqlSecurity') }} {{ t('message.sqlSecurity') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.security" v-model="localFunction.security"
@ -133,7 +133,7 @@
<div v-if="customizations.functionDataAccess" class="column col-auto"> <div v-if="customizations.functionDataAccess" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('message.dataAccess') }} {{ t('message.dataAccess') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.dataAccess" v-model="localFunction.dataAccess"
@ -146,7 +146,7 @@
<div class="form-group"> <div class="form-group">
<label class="form-label d-invisible">.</label> <label class="form-label d-invisible">.</label>
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
<input v-model="localFunction.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }} <input v-model="localFunction.deterministic" type="checkbox"><i class="form-icon" /> {{ t('word.deterministic') }}
</label> </label>
</div> </div>
</div> </div>
@ -154,7 +154,7 @@
</div> </div>
<div class="workspace-query-results column col-12 mt-2 p-relative"> <div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.functionBody') }}</label> <label class="form-label ml-2">{{ t('message.functionBody') }}</label>
<QueryEditor <QueryEditor
v-show="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
@ -175,213 +175,177 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import Functions from '@/ipc-api/Functions';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader.vue';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor.vue';
import WorkspaceTabPropsFunctionParamsModal from '@/components/WorkspaceTabPropsFunctionParamsModal'; import WorkspaceTabPropsFunctionParamsModal from '@/components/WorkspaceTabPropsFunctionParamsModal.vue';
import Functions from '@/ipc-api/Functions';
import { storeToRefs } from 'pinia';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { FunctionInfos, FunctionParam } from 'common/interfaces/antares';
import { useI18n } from 'vue-i18n';
export default { const { t } = useI18n();
name: 'WorkspaceTabNewFunction',
components: {
BaseLoader,
QueryEditor,
WorkspaceTabPropsFunctionParamsModal,
BaseSelect
},
props: {
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const props = defineProps({
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
});
const { const { addNotification } = useNotificationsStore();
getWorkspace, const workspacesStore = useWorkspacesStore();
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
removeTab,
renameTabs
} = workspacesStore;
return { const {
addNotification, getWorkspace,
selectedWorkspace, refreshStructure,
getWorkspace, changeBreadcrumbs,
refreshStructure, setUnsavedChanges,
changeBreadcrumbs, newTab,
setUnsavedChanges, removeTab
newTab, } = workspacesStore;
removeTab,
renameTabs
};
},
data () {
return {
isLoading: false,
isSaving: false,
isParamsModal: false,
originalFunction: {},
localFunction: {},
lastFunction: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
isChanged () {
return JSON.stringify(this.originalFunction) !== JSON.stringify(this.localFunction);
},
isDefinerInUsers () {
return this.originalFunction
? this.workspace.users.some(user => this.originalFunction.definer === `\`${user.name}\`@\`${user.host}\``)
: true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : []; 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 isParamsModal = ref(false);
const originalFunction: Ref<FunctionInfos> = ref(null);
const localFunction = ref(null);
const editorHeight = ref(300);
const workspace = computed(() => {
return getWorkspace(props.connection.uid);
});
const customizations = computed(() => {
return workspace.value.customizations;
});
const isChanged = computed(() => {
return JSON.stringify(originalFunction.value) !== JSON.stringify(localFunction.value);
});
const saveChanges = async () => {
if (isSaving.value) return;
isSaving.value = true;
const params = {
uid: props.connection.uid,
schema: props.schema,
...localFunction.value
};
try {
const { status, response } = await Functions.createFunction(params);
if (status === 'success') {
await refreshStructure(props.connection.uid);
newTab({
uid: props.connection.uid,
schema: props.schema,
elementName: localFunction.value.name,
elementType: 'function',
type: 'function-props'
});
removeTab({ uid: props.connection.uid, tab: props.tab.uid });
changeBreadcrumbs({ schema: props.schema, function: localFunction.value.name });
} }
}, else
watch: { addNotification({ status: 'error', message: response });
isSelected (val) { }
if (val) catch (err) {
this.changeBreadcrumbs({ schema: this.schema }); addNotification({ status: 'error', message: err.stack });
}, }
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
created () {
this.originalFunction = {
sql: this.customizations.functionSql,
language: this.customizations.languages ? this.customizations.languages[0] : null,
name: '',
definer: '',
comment: '',
security: 'DEFINER',
dataAccess: 'CONTAINS SQL',
deterministic: false,
returns: this.workspace.dataTypes.length ? this.workspace.dataTypes[0].types[0].name : null
};
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction)); isSaving.value = false;
};
setTimeout(() => { const clearChanges = () => {
this.resizeQueryEditor(); localFunction.value = JSON.parse(JSON.stringify(originalFunction.value));
}, 50); queryEditor.value.editor.session.setValue(localFunction.value.sql);
};
window.addEventListener('keydown', this.onKey); const resizeQueryEditor = () => {
}, if (queryEditor.value) {
mounted () { const footer = document.getElementById('footer');
if (this.isSelected) const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
this.changeBreadcrumbs({ schema: this.schema }); editorHeight.value = size;
queryEditor.value.editor.resize();
}
};
setTimeout(() => { const parametersUpdate = (parameters: FunctionParam[]) => {
this.$refs.firstInput.focus(); localFunction.value = { ...localFunction.value, parameters };
}, 100); };
},
unmounted () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
...this.localFunction
};
try { const showParamsModal = () => {
const { status, response } = await Functions.createFunction(params); isParamsModal.value = true;
};
if (status === 'success') { const hideParamsModal = () => {
await this.refreshStructure(this.connection.uid); isParamsModal.value = false;
};
this.newTab({ const onKey = (e: KeyboardEvent) => {
uid: this.connection.uid, if (props.isSelected) {
schema: this.schema, e.stopPropagation();
elementName: this.localFunction.name, if (e.ctrlKey && e.key === 's') { // CTRL + S
elementType: 'function', if (isChanged.value)
type: 'function-props' saveChanges();
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, function: this.localFunction.name });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
optionsUpdate (options) {
this.localFunction = options;
},
parametersUpdate (parameters) {
this.localFunction = { ...this.localFunction, parameters };
},
showParamsModal () {
this.isParamsModal = true;
},
hideParamsModal () {
this.isParamsModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
} }
} }
}; };
watch(() => props.isSelected, (val) => {
if (val) changeBreadcrumbs({ schema: props.schema });
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
originalFunction.value = {
sql: customizations.value.functionSql,
language: customizations.value.languages ? customizations.value.languages[0] : null,
name: '',
definer: '',
comment: '',
security: 'DEFINER',
dataAccess: 'CONTAINS SQL',
deterministic: false,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
returns: workspace.value.dataTypes.length ? (workspace.value.dataTypes[0] as any).types[0].name : null
};
localFunction.value = JSON.parse(JSON.stringify(originalFunction.value));
setTimeout(() => {
resizeQueryEditor();
}, 50);
window.addEventListener('keydown', onKey);
onMounted(() => {
if (props.isSelected)
changeBreadcrumbs({ schema: props.schema });
setTimeout(() => {
firstInput.value.focus();
}, 100);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeQueryEditor);
});
</script> </script>

View File

@ -72,8 +72,8 @@
<BaseSelect <BaseSelect
v-model="localRoutine.definer" v-model="localRoutine.definer"
:options="[{value: '', name:$t('message.currentUser')}, ...workspace.users]" :options="[{value: '', name:$t('message.currentUser')}, ...workspace.users]"
:option-label="(user) => user.value === '' ? user.name : `${user.name}@${user.host}`" :option-label="(user: any) => user.value === '' ? user.name : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``" :option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select" class="form-select"
/> />
</div> </div>
@ -129,7 +129,6 @@
<label class="form-label ml-2">{{ $t('message.routineBody') }}</label> <label class="form-label ml-2">{{ $t('message.routineBody') }}</label>
<QueryEditor <QueryEditor
v-show="isSelected" v-show="isSelected"
:key="`new-${_uid}`"
ref="queryEditor" ref="queryEditor"
v-model="localRoutine.sql" v-model="localRoutine.sql"
:workspace="workspace" :workspace="workspace"
@ -148,209 +147,174 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import Routines from '@/ipc-api/Routines';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor.vue';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader.vue';
import WorkspaceTabPropsRoutineParamsModal from '@/components/WorkspaceTabPropsRoutineParamsModal'; import WorkspaceTabPropsRoutineParamsModal from '@/components/WorkspaceTabPropsRoutineParamsModal.vue';
import Routines from '@/ipc-api/Routines';
import { storeToRefs } from 'pinia';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { FunctionParam } from 'common/interfaces/antares';
export default { const props = defineProps({
name: 'WorkspaceTabNewRoutine', tabUid: String,
components: { connection: Object,
QueryEditor, tab: Object,
BaseLoader, isSelected: Boolean,
WorkspaceTabPropsRoutineParamsModal, schema: String
BaseSelect });
},
props: {
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { const {
getWorkspace, getWorkspace,
refreshStructure, refreshStructure,
changeBreadcrumbs, changeBreadcrumbs,
setUnsavedChanges, setUnsavedChanges,
newTab, newTab,
removeTab, removeTab
renameTabs } = workspacesStore;
} = workspacesStore;
return { const queryEditor: Ref<Component & {editor: Ace.Editor; $el: HTMLElement}> = ref(null);
addNotification, const firstInput: Ref<HTMLInputElement> = ref(null);
selectedWorkspace, const isLoading = ref(false);
getWorkspace, const isSaving = ref(false);
refreshStructure, const isParamsModal = ref(false);
changeBreadcrumbs, const originalRoutine = ref(null);
setUnsavedChanges, const localRoutine = ref(null);
newTab, const editorHeight = ref(300);
removeTab,
renameTabs
};
},
data () {
return {
isLoading: false,
isSaving: false,
isParamsModal: false,
originalRoutine: {},
localRoutine: {},
lastRoutine: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
isChanged () {
return JSON.stringify(this.originalRoutine) !== JSON.stringify(this.localRoutine);
},
isDefinerInUsers () {
return this.originalRoutine ? this.workspace.users.some(user => this.originalRoutine.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : []; const workspace = computed(() => {
return getWorkspace(props.connection.uid);
});
const customizations = computed(() => {
return workspace.value.customizations;
});
const isChanged = computed(() => {
return JSON.stringify(originalRoutine.value) !== JSON.stringify(localRoutine.value);
});
const saveChanges = async () => {
if (isSaving.value) return;
isSaving.value = true;
const params = {
uid: props.connection.uid,
schema: props.schema,
...localRoutine.value
};
try {
const { status, response } = await Routines.createRoutine(params);
if (status === 'success') {
await refreshStructure(props.connection.uid);
newTab({
uid: props.connection.uid,
schema: props.schema,
elementName: localRoutine.value.name,
elementType: 'routine',
type: 'routine-props'
});
removeTab({ uid: props.connection.uid, tab: props.tab.uid });
changeBreadcrumbs({ schema: props.schema, routine: localRoutine.value.name });
} }
}, else
watch: { addNotification({ status: 'error', message: response });
isSelected (val) { }
if (val) catch (err) {
this.changeBreadcrumbs({ schema: this.schema }); addNotification({ status: 'error', message: err.stack });
}, }
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
created () {
this.originalRoutine = {
sql: this.customizations.procedureSql,
language: this.customizations.languages ? this.customizations.languages[0] : null,
name: '',
definer: '',
comment: '',
security: 'DEFINER',
dataAccess: 'CONTAINS SQL',
deterministic: false
};
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine)); isSaving.value = false;
};
setTimeout(() => { const clearChanges = () => {
this.resizeQueryEditor(); localRoutine.value = JSON.parse(JSON.stringify(originalRoutine.value));
}, 50); queryEditor.value.editor.session.setValue(localRoutine.value.sql);
};
window.addEventListener('keydown', this.onKey); const resizeQueryEditor = () => {
}, if (queryEditor.value) {
mounted () { const footer = document.getElementById('footer');
if (this.isSelected) const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
this.changeBreadcrumbs({ schema: this.schema }); editorHeight.value = size;
queryEditor.value.editor.resize();
}
};
setTimeout(() => { const parametersUpdate = (parameters: FunctionParam[]) => {
this.$refs.firstInput.focus(); localRoutine.value = { ...localRoutine.value, parameters };
}, 100); };
window.addEventListener('resize', this.resizeQueryEditor); const showParamsModal = () => {
}, isParamsModal.value = true;
unmounted () { };
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
...this.localRoutine
};
try { const hideParamsModal = () => {
const { status, response } = await Routines.createRoutine(params); isParamsModal.value = false;
};
if (status === 'success') { const onKey = (e: KeyboardEvent) => {
await this.refreshStructure(this.connection.uid); if (props.isSelected) {
e.stopPropagation();
this.newTab({ if (e.ctrlKey && e.key === 's') { // CTRL + S
uid: this.connection.uid, if (isChanged.value)
schema: this.schema, saveChanges();
elementName: this.localRoutine.name,
elementType: 'routine',
type: 'routine-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, routine: this.localRoutine.name });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
parametersUpdate (parameters) {
this.localRoutine = { ...this.localRoutine, parameters };
},
showParamsModal () {
this.isParamsModal = true;
},
hideParamsModal () {
this.isParamsModal = false;
},
showAskParamsModal () {
this.isAskingParameters = true;
},
hideAskParamsModal () {
this.isAskingParameters = false;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
} }
} }
}; };
watch(() => props.isSelected, (val) => {
if (val) changeBreadcrumbs({ schema: props.schema });
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
originalRoutine.value = {
sql: customizations.value.functionSql,
language: customizations.value.languages ? customizations.value.languages[0] : null,
name: '',
definer: '',
comment: '',
security: 'DEFINER',
dataAccess: 'CONTAINS SQL',
deterministic: false,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
returns: workspace.value.dataTypes.length ? (workspace.value.dataTypes[0] as any).types[0].name : null
};
localRoutine.value = JSON.parse(JSON.stringify(originalRoutine.value));
setTimeout(() => {
resizeQueryEditor();
}, 50);
window.addEventListener('keydown', onKey);
onMounted(() => {
if (props.isSelected)
changeBreadcrumbs({ schema: props.schema });
setTimeout(() => {
firstInput.value.focus();
}, 100);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeQueryEditor);
});
</script> </script>

View File

@ -11,26 +11,26 @@
@click="saveChanges" @click="saveChanges"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span> <span>{{ t('word.save') }}</span>
</button> </button>
<button <button
:disabled="!isChanged" :disabled="!isChanged"
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
@click="clearChanges" @click="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
<div class="divider-vert py-3" /> <div class="divider-vert py-3" />
<button class="btn btn-dark btn-sm" @click="showTimingModal"> <button class="btn btn-dark btn-sm" @click="showTimingModal">
<i class="mdi mdi-24px mdi-timer mr-1" /> <i class="mdi mdi-24px mdi-timer mr-1" />
<span>{{ $t('word.timing') }}</span> <span>{{ t('word.timing') }}</span>
</button> </button>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div> </div>
</div> </div>
@ -40,7 +40,7 @@
<div class="columns"> <div class="columns">
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label> <label class="form-label">{{ t('word.name') }}</label>
<input <input
ref="firstInput" ref="firstInput"
v-model="localScheduler.name" v-model="localScheduler.name"
@ -51,19 +51,19 @@
</div> </div>
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label> <label class="form-label">{{ t('word.definer') }}</label>
<BaseSelect <BaseSelect
v-model="localScheduler.definer" v-model="localScheduler.definer"
:options="users" :options="users"
:option-label="(user) => user.value === '' ? $t('message.currentUser') : `${user.name}@${user.host}`" :option-label="(user: any) => user.value === '' ? t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``" :option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select" class="form-select"
/> />
</div> </div>
</div> </div>
<div class="column"> <div class="column">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.comment') }}</label> <label class="form-label">{{ t('word.comment') }}</label>
<input <input
v-model="localScheduler.comment" v-model="localScheduler.comment"
class="form-input" class="form-input"
@ -73,7 +73,7 @@
</div> </div>
<div class="column"> <div class="column">
<div class="form-group"> <div class="form-group">
<label class="form-label mr-2">{{ $t('word.state') }}</label> <label class="form-label mr-2">{{ t('word.state') }}</label>
<label class="form-radio form-inline"> <label class="form-radio form-inline">
<input <input
v-model="localScheduler.state" v-model="localScheduler.state"
@ -104,7 +104,7 @@
</div> </div>
<div class="workspace-query-results column col-12 mt-2 p-relative"> <div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.schedulerBody') }}</label> <label class="form-label ml-2">{{ t('message.schedulerBody') }}</label>
<QueryEditor <QueryEditor
v-show="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
@ -124,209 +124,186 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { storeToRefs } from 'pinia'; import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { EventInfos } from 'common/interfaces/antares';
import { useI18n } from 'vue-i18n';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader.vue';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor.vue';
import WorkspaceTabPropsSchedulerTimingModal from '@/components/WorkspaceTabPropsSchedulerTimingModal'; import WorkspaceTabPropsSchedulerTimingModal from '@/components/WorkspaceTabPropsSchedulerTimingModal.vue';
import Schedulers from '@/ipc-api/Schedulers'; import Schedulers from '@/ipc-api/Schedulers';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
export default { const { t } = useI18n();
name: 'WorkspaceTabNewScheduler',
components: {
BaseLoader,
QueryEditor,
WorkspaceTabPropsSchedulerTimingModal,
BaseSelect
},
props: {
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const props = defineProps({
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
});
const { const { addNotification } = useNotificationsStore();
getWorkspace, const workspacesStore = useWorkspacesStore();
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
removeTab,
renameTabs
} = workspacesStore;
return { const {
addNotification, getWorkspace,
selectedWorkspace, refreshStructure,
getWorkspace, changeBreadcrumbs,
refreshStructure, setUnsavedChanges,
changeBreadcrumbs, newTab,
setUnsavedChanges, removeTab
newTab, } = workspacesStore;
removeTab,
renameTabs
};
},
data () {
return {
isLoading: false,
isSaving: false,
isTimingModal: false,
originalScheduler: {},
localScheduler: {},
lastScheduler: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
isChanged () {
return JSON.stringify(this.originalScheduler) !== JSON.stringify(this.localScheduler);
},
isDefinerInUsers () {
return this.originalScheduler ? this.workspace.users.some(user => this.originalScheduler.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : []; const queryEditor: Ref<Component & {editor: Ace.Editor; $el: HTMLElement}> = ref(null);
}, const firstInput: Ref<HTMLInputElement> = ref(null);
users () { const isLoading = ref(false);
const users = [{ value: '' }, ...this.workspace.users]; const isSaving = ref(false);
if (!this.isDefinerInUsers) { const isTimingModal = ref(false);
const [name, host] = this.originalScheduler.definer.replaceAll('`', '').split('@'); const originalScheduler = ref(null);
users.unshift({ name, host }); const localScheduler = ref(null);
} const editorHeight = ref(300);
return users; const workspace = computed(() => {
return getWorkspace(props.connection.uid);
});
const isChanged = computed(() => {
return JSON.stringify(originalScheduler.value) !== JSON.stringify(localScheduler.value);
});
const isDefinerInUsers = computed(() => {
return originalScheduler.value ? workspace.value.users.some(user => originalScheduler.value.definer === `\`${user.name}\`@\`${user.host}\``) : true;
});
const users = computed(() => {
const users = [{ value: '' }, ...workspace.value.users];
if (!isDefinerInUsers.value) {
const [name, host] = originalScheduler.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,
...localScheduler.value
};
try {
const { status, response } = await Schedulers.createScheduler(params);
if (status === 'success') {
await refreshStructure(props.connection.uid);
newTab({
uid: props.connection.uid,
schema: props.schema,
elementName: localScheduler.value.name,
elementType: 'scheduler',
type: 'scheduler-props'
});
removeTab({ uid: props.connection.uid, tab: props.tab.uid });
changeBreadcrumbs({ schema: props.schema, scheduler: localScheduler.value.name });
} }
}, else
watch: { addNotification({ status: 'error', message: response });
isSelected (val) { }
if (val) catch (err) {
this.changeBreadcrumbs({ schema: this.schema }); addNotification({ status: 'error', message: err.stack });
}, }
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
async created () {
this.originalScheduler = {
definer: '',
sql: 'BEGIN\r\n\r\nEND',
name: '',
comment: '',
execution: 'EVERY',
every: ['1', 'DAY'],
preserve: true,
state: 'DISABLE'
};
this.localScheduler = JSON.parse(JSON.stringify(this.originalScheduler)); isSaving.value = false;
};
setTimeout(() => { const clearChanges = () => {
this.resizeQueryEditor(); localScheduler.value = JSON.parse(JSON.stringify(originalScheduler.value));
}, 50); queryEditor.value.editor.session.setValue(localScheduler.value.sql);
};
window.addEventListener('keydown', this.onKey); const resizeQueryEditor = () => {
}, if (queryEditor.value) {
mounted () { const footer = document.getElementById('footer');
if (this.isSelected) const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
this.changeBreadcrumbs({ schema: this.schema }); editorHeight.value = size;
queryEditor.value.editor.resize();
}
};
setTimeout(() => { const showTimingModal = () => {
this.$refs.firstInput.focus(); isTimingModal.value = true;
}, 100); };
window.addEventListener('resize', this.resizeQueryEditor); const hideTimingModal = () => {
}, isTimingModal.value = false;
unmounted () { };
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
...this.localScheduler
};
try { const timingUpdate = (options: EventInfos) => {
const { status, response } = await Schedulers.createScheduler(params); localScheduler.value = options;
};
if (status === 'success') { const onKey = (e: KeyboardEvent) => {
await this.refreshStructure(this.connection.uid); if (props.isSelected) {
e.stopPropagation();
this.newTab({ if (e.ctrlKey && e.key === 's') { // CTRL + S
uid: this.connection.uid, if (isChanged.value)
schema: this.schema, saveChanges();
elementName: this.localScheduler.name,
elementType: 'scheduler',
type: 'scheduler-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, scheduler: this.localScheduler.name });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localScheduler = JSON.parse(JSON.stringify(this.originalScheduler));
this.$refs.queryEditor.editor.session.setValue(this.localScheduler.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
showTimingModal () {
this.isTimingModal = true;
},
hideTimingModal () {
this.isTimingModal = false;
},
timingUpdate (options) {
this.localScheduler = options;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
} }
} }
}; };
watch(() => props.isSelected, (val) => {
if (val) changeBreadcrumbs({ schema: props.schema });
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
originalScheduler.value = {
definer: '',
sql: 'BEGIN\r\n\r\nEND',
name: '',
comment: '',
execution: 'EVERY',
every: ['1', 'DAY'],
preserve: true,
state: 'DISABLE'
};
originalScheduler.value = JSON.parse(JSON.stringify(originalScheduler.value));
setTimeout(() => {
resizeQueryEditor();
}, 50);
window.addEventListener('keydown', onKey);
onMounted(() => {
if (props.isSelected)
changeBreadcrumbs({ schema: props.schema });
setTimeout(() => {
firstInput.value.focus();
}, 100);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeQueryEditor);
});
</script> </script>

View File

@ -11,16 +11,16 @@
@click="saveChanges" @click="saveChanges"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span> <span>{{ t('word.save') }}</span>
</button> </button>
<button <button
:disabled="!isChanged || isSaving" :disabled="!isChanged || isSaving"
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
@click="clearChanges" @click="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
<div class="divider-vert py-3" /> <div class="divider-vert py-3" />
@ -28,20 +28,20 @@
<button <button
:disabled="isSaving" :disabled="isSaving"
class="btn btn-dark btn-sm" class="btn btn-dark btn-sm"
:title="$t('message.addNewField')" :title="t('message.addNewField')"
@click="addField" @click="addField"
> >
<i class="mdi mdi-24px mdi-playlist-plus mr-1" /> <i class="mdi mdi-24px mdi-playlist-plus mr-1" />
<span>{{ $t('word.add') }}</span> <span>{{ t('word.add') }}</span>
</button> </button>
<button <button
:disabled="isSaving || !localFields.length" :disabled="isSaving || !localFields.length"
class="btn btn-dark btn-sm" class="btn btn-dark btn-sm"
:title="$t('message.manageIndexes')" :title="t('message.manageIndexes')"
@click="showIntdexesModal" @click="showIntdexesModal"
> >
<i class="mdi mdi-24px mdi-key mdi-rotate-45 mr-1" /> <i class="mdi mdi-24px mdi-key mdi-rotate-45 mr-1" />
<span>{{ $t('word.indexes') }}</span> <span>{{ t('word.indexes') }}</span>
</button> </button>
<button <button
class="btn btn-dark btn-sm" class="btn btn-dark btn-sm"
@ -49,11 +49,11 @@
@click="showForeignModal" @click="showForeignModal"
> >
<i class="mdi mdi-24px mdi-key-link mr-1" /> <i class="mdi mdi-24px mdi-key-link mr-1" />
<span>{{ $t('word.foreignKeys') }}</span> <span>{{ t('word.foreignKeys') }}</span>
</button> </button>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div> </div>
</div> </div>
@ -63,7 +63,7 @@
<div class="columns mb-4"> <div class="columns mb-4">
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label> <label class="form-label">{{ t('word.name') }}</label>
<input <input
ref="firstInput" ref="firstInput"
v-model="localOptions.name" v-model="localOptions.name"
@ -74,7 +74,7 @@
</div> </div>
<div v-if="workspace.customizations.comment" class="column"> <div v-if="workspace.customizations.comment" class="column">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.comment') }}</label> <label class="form-label">{{ t('word.comment') }}</label>
<input <input
v-model="localOptions.comment" v-model="localOptions.comment"
class="form-input" class="form-input"
@ -86,7 +86,7 @@
<div v-if="workspace.customizations.collations" class="column col-auto"> <div v-if="workspace.customizations.collations" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.collation') }} {{ t('word.collation') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localOptions.collation" v-model="localOptions.collation"
@ -100,7 +100,7 @@
<div v-if="workspace.customizations.engines" class="column col-auto"> <div v-if="workspace.customizations.engines" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.engine') }} {{ t('word.engine') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localOptions.engine" v-model="localOptions.engine"
@ -160,310 +160,302 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { Component, computed, onBeforeUnmount, onMounted, Prop, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import Tables from '@/ipc-api/Tables'; import Tables from '@/ipc-api/Tables';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader.vue';
import WorkspaceTabPropsTableFields from '@/components/WorkspaceTabPropsTableFields'; import WorkspaceTabPropsTableFields from '@/components/WorkspaceTabPropsTableFields.vue';
import WorkspaceTabPropsTableIndexesModal from '@/components/WorkspaceTabPropsTableIndexesModal'; import WorkspaceTabPropsTableIndexesModal from '@/components/WorkspaceTabPropsTableIndexesModal.vue';
import WorkspaceTabPropsTableForeignModal from '@/components/WorkspaceTabPropsTableForeignModal'; import WorkspaceTabPropsTableForeignModal from '@/components/WorkspaceTabPropsTableForeignModal.vue';
import WorkspaceTabNewTableEmptyState from '@/components/WorkspaceTabNewTableEmptyState'; import WorkspaceTabNewTableEmptyState from '@/components/WorkspaceTabNewTableEmptyState.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { ConnectionParams, TableField, TableForeign, TableIndex, TableOptions } from 'common/interfaces/antares';
export default { const { t } = useI18n();
name: 'WorkspaceTabNewTable',
components: {
BaseLoader,
WorkspaceTabPropsTableFields,
WorkspaceTabPropsTableIndexesModal,
WorkspaceTabPropsTableForeignModal,
WorkspaceTabNewTableEmptyState,
BaseSelect
},
props: {
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const props = defineProps({
tabUid: String,
connection: Object as Prop<ConnectionParams>,
tab: Object,
isSelected: Boolean,
schema: String
});
const { const { addNotification } = useNotificationsStore();
getWorkspace, const workspacesStore = useWorkspacesStore();
getDatabaseVariable,
refreshStructure,
setUnsavedChanges,
newTab,
renameTabs,
removeTab,
changeBreadcrumbs
} = workspacesStore;
return { const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
addNotification,
getWorkspace,
getDatabaseVariable,
refreshStructure,
setUnsavedChanges,
newTab,
renameTabs,
removeTab,
changeBreadcrumbs,
selectedWorkspace
};
},
data () {
return {
isLoading: false,
isSaving: false,
isIndexesModal: false,
isForeignModal: false,
isOptionsChanging: false,
originalFields: [],
localFields: [],
originalKeyUsage: [],
localKeyUsage: [],
originalIndexes: [],
localIndexes: [],
tableOptions: {},
localOptions: {},
lastTable: null,
newFieldsCounter: 0
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
defaultCollation () {
if (this.workspace.customizations.collations)
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server')?.value || '';
return '';
},
defaultEngine () {
if (this.workspace.customizations.engines)
return this.workspace.engines?.find(engine => engine.isDefault)?.name || '';
return '';
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : []; const {
}, getWorkspace,
isChanged () { getDatabaseVariable,
return JSON.stringify(this.originalFields) !== JSON.stringify(this.localFields) || refreshStructure,
JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage) || setUnsavedChanges,
JSON.stringify(this.originalIndexes) !== JSON.stringify(this.localIndexes) || newTab,
JSON.stringify(this.tableOptions) !== JSON.stringify(this.localOptions); removeTab,
}, changeBreadcrumbs
isValid () { } = workspacesStore;
return !!this.localFields.length && !!this.localOptions.name.trim().length;
const indexTable: Ref<Component & { tableWrapper: HTMLDivElement }> = ref(null);
const firstInput: Ref<HTMLInputElement> = ref(null);
const isLoading = ref(false);
const isSaving = ref(false);
const isIndexesModal = ref(false);
const isForeignModal = ref(false);
const originalFields: Ref<TableField[]> = ref([]);
const localFields: Ref<TableField[]> = ref([]);
const originalKeyUsage: Ref<TableForeign[]> = ref([]);
const localKeyUsage: Ref<TableForeign[]> = ref([]);
const originalIndexes: Ref<TableIndex[]> = ref([]);
const localIndexes: Ref<TableIndex[]> = ref([]);
const tableOptions: Ref<TableOptions> = ref(null);
const localOptions: Ref<TableOptions> = ref(null);
const newFieldsCounter = ref(0);
const workspace = computed(() => {
return getWorkspace(props.connection.uid);
});
const defaultCollation = computed(() => {
if (workspace.value.customizations.collations)
return getDatabaseVariable(selectedWorkspace.value, 'collation_server')?.value || '';
return '';
});
const defaultEngine = computed(() => {
if (workspace.value.customizations.engines)
return workspace.value.engines?.find(engine => engine.isDefault)?.name as string || '';
return '';
});
const schemaTables = computed(() => {
const schemaTables = workspace.value.structure
.filter(schema => schema.name === props.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
});
const isChanged = computed(() => {
return JSON.stringify(originalFields.value) !== JSON.stringify(localFields.value) ||
JSON.stringify(originalKeyUsage.value) !== JSON.stringify(localKeyUsage.value) ||
JSON.stringify(originalIndexes.value) !== JSON.stringify(localIndexes.value) ||
JSON.stringify(tableOptions.value) !== JSON.stringify(localOptions.value);
});
const isValid = computed(() => {
return !!localFields.value.length && !!localOptions.value.name.trim().length;
});
const saveChanges = async () => {
if (isSaving.value || !isValid.value) return;
isSaving.value = true;
const params = {
uid: props.connection.uid,
schema: props.schema,
fields: localFields.value,
foreigns: localKeyUsage.value,
indexes: localIndexes.value,
options: localOptions.value
};
try {
const { status, response } = await Tables.createTable(params);
if (status === 'success') {
await refreshStructure(props.connection.uid);
newTab({
uid: props.connection.uid,
schema: props.schema,
elementName: localOptions.value.name,
elementType: 'table',
type: 'table-props'
});
removeTab({ uid: props.connection.uid, tab: props.tab.uid });
changeBreadcrumbs({ schema: props.schema, table: localOptions.value.name });
} }
}, else
watch: { addNotification({ status: 'error', message: response });
isSelected (val) { }
if (val) catch (err) {
this.changeBreadcrumbs({ schema: this.schema }); addNotification({ status: 'error', message: err.stack });
}, }
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
created () {
this.tableOptions = {
name: '',
type: 'table',
engine: this.defaultEngine,
comment: '',
collation: this.defaultCollation
};
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions)); isSaving.value = false;
window.addEventListener('keydown', this.onKey); newFieldsCounter.value = 0;
}, };
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
setTimeout(() => { const clearChanges = () => {
this.$refs.firstInput.focus(); localFields.value = JSON.parse(JSON.stringify(originalFields.value));
}, 100); localIndexes.value = JSON.parse(JSON.stringify(originalIndexes.value));
}, localKeyUsage.value = JSON.parse(JSON.stringify(originalKeyUsage.value));
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async saveChanges () {
if (this.isSaving || !this.isValid) return;
this.isSaving = true;
const params = { tableOptions.value = {
uid: this.connection.uid, name: '',
schema: this.schema, type: 'table',
fields: this.localFields, engine: defaultEngine.value,
foreigns: this.localKeyUsage, comment: '',
indexes: this.localIndexes, collation: defaultCollation.value
options: this.localOptions };
};
try { localOptions.value = JSON.parse(JSON.stringify(tableOptions.value));
const { status, response } = await Tables.createTable(params); newFieldsCounter.value = 0;
};
if (status === 'success') { const addField = () => {
await this.refreshStructure(this.connection.uid); localFields.value.push({
_antares_id: uidGen(),
name: `${t('word.field', 1)}_${++newFieldsCounter.value}`,
key: '',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: (workspace.value.dataTypes[0] as any).types[0].name,
schema: props.schema,
numPrecision: null,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
numLength: (workspace.value.dataTypes[0] as any).types[0].length,
datePrecision: null,
charLength: null,
nullable: false,
unsigned: false,
zerofill: false,
order: localFields.value.length + 1,
default: null,
charset: null,
collation: defaultCollation.value,
autoIncrement: false,
onUpdate: '',
comment: ''
});
this.newTab({ setTimeout(() => {
uid: this.connection.uid, const scrollable = indexTable.value.tableWrapper;
schema: this.schema, scrollable.scrollTop = scrollable.scrollHeight + 30;
elementName: this.localOptions.name, }, 20);
elementType: 'table', };
type: 'table-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid }); const renameField = (payload: {index: string; new: string; old: string}) => {
this.changeBreadcrumbs({ schema: this.schema, table: this.localOptions.name }); localIndexes.value = localIndexes.value.map(index => {
} const fi = index.fields.findIndex(field => field === payload.old);
else if (fi !== -1)
this.addNotification({ status: 'error', message: response }); index.fields[fi] = payload.new;
} return index;
catch (err) { });
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false; localKeyUsage.value = localKeyUsage.value.map(key => {
this.newFieldsCounter = 0; if (key.field === payload.old)
}, key.field = payload.new;
clearChanges () { return key;
this.localFields = JSON.parse(JSON.stringify(this.originalFields)); });
this.localIndexes = JSON.parse(JSON.stringify(this.originalIndexes)); };
this.localKeyUsage = JSON.parse(JSON.stringify(this.originalKeyUsage));
this.tableOptions = { const duplicateField = (uid: string) => {
name: '', const fieldToClone = Object.assign({}, localFields.value.find(field => field._antares_id === uid));
type: 'table', fieldToClone._antares_id = uidGen();
engine: this.defaultEngine, fieldToClone.name = `${fieldToClone.name}_copy`;
comment: '', fieldToClone.order = localFields.value.length + 1;
collation: this.defaultCollation localFields.value = [...localFields.value, fieldToClone];
};
this.localOptions = JSON.parse(JSON.stringify(this.tableOptions)); setTimeout(() => {
this.newFieldsCounter = 0; const scrollable = indexTable.value.tableWrapper;
}, scrollable.scrollTop = scrollable.scrollHeight + 30;
addField () { }, 20);
this.localFields.push({ };
_antares_id: uidGen(),
name: `${this.$tc('word.field', 1)}_${++this.newFieldsCounter}`,
key: '',
type: this.workspace.dataTypes[0].types[0].name,
schema: this.schema,
numPrecision: null,
numLength: this.workspace.dataTypes[0].types[0].length,
datePrecision: null,
charLength: null,
nullable: false,
unsigned: false,
zerofill: false,
order: this.localFields.length + 1,
default: null,
charset: null,
collation: null,
autoIncrement: false,
onUpdate: '',
comment: ''
});
setTimeout(() => { const removeField = (uid: string) => {
const scrollable = this.$refs.indexTable.$refs.tableWrapper; localFields.value = localFields.value.filter(field => field._antares_id !== uid);
scrollable.scrollTop = scrollable.scrollHeight + 30; };
}, 20);
},
renameField (payload) {
this.localIndexes = this.localIndexes.map(index => {
const fi = index.fields.findIndex(field => field === payload.old);
if (fi !== -1)
index.fields[fi] = payload.new;
return index;
});
this.localKeyUsage = this.localKeyUsage.map(key => { const addNewIndex = (payload: { index: string; field: string }) => {
if (key.field === payload.old) localIndexes.value = [...localIndexes.value, {
key.field = payload.new; _antares_id: uidGen(),
return key; name: payload.index === 'PRIMARY' ? 'PRIMARY' : payload.field,
}); fields: [payload.field],
}, type: payload.index,
duplicateField (uid) { comment: '',
const fieldToClone = Object.assign({}, this.localFields.find(field => field._antares_id === uid)); indexType: 'BTREE',
fieldToClone._antares_id = uidGen(); indexComment: '',
fieldToClone.name = `${fieldToClone.name}_copy`; cardinality: 0
fieldToClone.order = this.localFields.length + 1; }];
this.localFields = [...this.localFields, fieldToClone]; };
setTimeout(() => { const addToIndex = (payload: { index: string; field: string }) => {
const scrollable = this.$refs.indexTable.$refs.tableWrapper; localIndexes.value = localIndexes.value.map(index => {
scrollable.scrollTop = scrollable.scrollHeight + 30; if (index._antares_id === payload.index) index.fields.push(payload.field);
}, 20); return index;
}, });
removeField (uid) { };
this.localFields = this.localFields.filter(field => field._antares_id !== uid);
}, const showIntdexesModal = () => {
addNewIndex (payload) { isIndexesModal.value = true;
this.localIndexes = [...this.localIndexes, { };
_antares_id: uidGen(),
name: payload.index === 'PRIMARY' ? 'PRIMARY' : payload.field, const hideIndexesModal = () => {
fields: [payload.field], isIndexesModal.value = false;
type: payload.index, };
comment: '',
indexType: 'BTREE', const indexesUpdate = (indexes: TableIndex[]) => {
indexComment: '', localIndexes.value = indexes;
cardinality: 0 };
}];
}, const showForeignModal = () => {
addToIndex (payload) { isForeignModal.value = true;
this.localIndexes = this.localIndexes.map(index => { };
if (index._antares_id === payload.index) index.fields.push(payload.field);
return index; const hideForeignModal = () => {
}); isForeignModal.value = false;
}, };
optionsUpdate (options) {
this.localOptions = options; const foreignsUpdate = (foreigns: TableForeign[]) => {
}, localKeyUsage.value = foreigns;
showIntdexesModal () { };
this.isIndexesModal = true;
}, const onKey = (e: KeyboardEvent) => {
hideIndexesModal () { if (props.isSelected) {
this.isIndexesModal = false; e.stopPropagation();
}, if (e.ctrlKey && e.key === 's') { // CTRL + S
indexesUpdate (indexes) { if (isChanged.value)
this.localIndexes = indexes; saveChanges();
},
showForeignModal () {
this.isForeignModal = true;
},
hideForeignModal () {
this.isForeignModal = false;
},
foreignsUpdate (foreigns) {
this.localKeyUsage = foreigns;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
} }
} }
}; };
watch(() => props.isSelected, (val) => {
if (val) changeBreadcrumbs({ schema: props.schema });
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
tableOptions.value = {
name: '',
type: 'table',
engine: defaultEngine.value,
comment: '',
collation: defaultCollation.value
};
localOptions.value = JSON.parse(JSON.stringify(tableOptions.value));
window.addEventListener('keydown', onKey);
onMounted(() => {
if (props.isSelected)
changeBreadcrumbs({ schema: props.schema });
setTimeout(() => {
firstInput.value.focus();
}, 100);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>

View File

@ -1,22 +1,22 @@
<template> <template>
<div class="column col-12 empty"> <div class="column col-12 empty">
<p class="h6 empty-subtitle"> <p class="h6 empty-subtitle">
{{ $t('message.thereAreNoTableFields') }} {{ t('message.thereAreNoTableFields') }}
</p> </p>
<div class="empty-action"> <div class="empty-action">
<button class="btn btn-gray d-flex" @click="$emit('new-field')"> <button class="btn btn-gray d-flex" @click="emit('new-field')">
<i class="mdi mdi-24px mdi-playlist-plus mr-2" /> <i class="mdi mdi-24px mdi-playlist-plus mr-2" />
{{ $t('message.addNewField') }} {{ t('message.addNewField') }}
</button> </button>
</div> </div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { useI18n } from 'vue-i18n';
name: 'WorkspaceTabNewTableEmptyState',
emits: ['new-field'] const { t } = useI18n();
}; const emit = defineEmits(['new-field']);
</script> </script>
<style scoped> <style scoped>

View File

@ -11,20 +11,20 @@
@click="saveChanges" @click="saveChanges"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span> <span>{{ t('word.save') }}</span>
</button> </button>
<button <button
:disabled="!isChanged" :disabled="!isChanged"
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
@click="clearChanges" @click="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div> </div>
</div> </div>
@ -34,7 +34,7 @@
<div class="columns"> <div class="columns">
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label> <label class="form-label">{{ t('word.name') }}</label>
<input <input
ref="firstInput" ref="firstInput"
v-model="localTrigger.name" v-model="localTrigger.name"
@ -45,12 +45,12 @@
</div> </div>
<div v-if="customizations.definer" class="column col-auto"> <div v-if="customizations.definer" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label> <label class="form-label">{{ t('word.definer') }}</label>
<BaseSelect <BaseSelect
v-model="localTrigger.definer" v-model="localTrigger.definer"
:options="users" :options="users"
:option-label="(user) => user.value === '' ? $t('message.currentUser') : `${user.name}@${user.host}`" :option-label="(user: any) => user.value === '' ? t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``" :option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select" class="form-select"
/> />
</div> </div>
@ -58,7 +58,7 @@
<fieldset class="column columns mb-0" :disabled="customizations.triggerOnlyRename"> <fieldset class="column columns mb-0" :disabled="customizations.triggerOnlyRename">
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.table') }}</label> <label class="form-label">{{ t('word.table') }}</label>
<BaseSelect <BaseSelect
v-model="localTrigger.table" v-model="localTrigger.table"
:options="schemaTables" :options="schemaTables"
@ -70,7 +70,7 @@
</div> </div>
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.event') }}</label> <label class="form-label">{{ t('word.event') }}</label>
<div class="input-group"> <div class="input-group">
<BaseSelect <BaseSelect
v-model="localTrigger.activation" v-model="localTrigger.activation"
@ -85,7 +85,7 @@
/> />
<div v-if="customizations.triggerMultipleEvents" class="px-4"> <div v-if="customizations.triggerMultipleEvents" class="px-4">
<label <label
v-for="event in Object.keys(localEvents)" v-for="event in Object.keys(localEvents) as ('INSERT' | 'UPDATE' | 'DELETE')[]"
:key="event" :key="event"
class="form-checkbox form-inline" class="form-checkbox form-inline"
@change.prevent="changeEvents(event)" @change.prevent="changeEvents(event)"
@ -101,7 +101,7 @@
</div> </div>
<div class="workspace-query-results column col-12 mt-2 p-relative"> <div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.triggerStatement') }}</label> <label class="form-label ml-2">{{ t('message.triggerStatement') }}</label>
<QueryEditor <QueryEditor
v-show="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
@ -114,219 +114,204 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { storeToRefs } from 'pinia'; import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor.vue';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader.vue';
import Triggers from '@/ipc-api/Triggers'; import Triggers from '@/ipc-api/Triggers';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
export default { const { t } = useI18n();
name: 'WorkspaceTabNewTrigger',
components: {
BaseLoader,
QueryEditor,
BaseSelect
},
props: {
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const props = defineProps({
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
});
const { const { addNotification } = useNotificationsStore();
getWorkspace, const workspacesStore = useWorkspacesStore();
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
removeTab,
renameTabs
} = workspacesStore;
return { const {
addNotification, getWorkspace,
selectedWorkspace, refreshStructure,
getWorkspace, changeBreadcrumbs,
refreshStructure, setUnsavedChanges,
changeBreadcrumbs, newTab,
setUnsavedChanges, removeTab
newTab, } = workspacesStore;
removeTab,
renameTabs
};
},
data () {
return {
isLoading: false,
isSaving: false,
originalTrigger: {},
localTrigger: {},
lastTrigger: null,
sqlProxy: '',
editorHeight: 300,
localEvents: { INSERT: false, UPDATE: false, DELETE: false }
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
isChanged () {
return JSON.stringify(this.originalTrigger) !== JSON.stringify(this.localTrigger);
},
isDefinerInUsers () {
return this.originalTrigger ? this.workspace.users.some(user => this.originalTrigger.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : []; const queryEditor: Ref<Component & {editor: Ace.Editor; $el: HTMLElement}> = ref(null);
}, const firstInput: Ref<HTMLInputElement> = ref(null);
users () { const isLoading = ref(false);
const users = [{ value: '' }, ...this.workspace.users]; const isSaving = ref(false);
if (!this.isDefinerInUsers) { const originalTrigger = ref(null);
const [name, host] = this.originalTrigger.definer.replaceAll('`', '').split('@'); const localTrigger = ref(null);
users.unshift({ name, host }); const editorHeight = ref(300);
} const localEvents = ref({ INSERT: false, UPDATE: false, DELETE: false });
return users; const workspace = computed(() => {
} return getWorkspace(props.connection.uid);
}, });
watch: {
isSelected (val) {
if (val)
this.changeBreadcrumbs({ schema: this.schema });
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
created () {
this.originalTrigger = {
sql: this.customizations.triggerSql,
definer: '',
table: this.schemaTables.length ? this.schemaTables[0].name : null,
activation: 'BEFORE',
event: this.customizations.triggerMultipleEvents ? ['INSERT'] : 'INSERT',
name: ''
};
this.localTrigger = JSON.parse(JSON.stringify(this.originalTrigger)); const customizations = computed(() => {
return workspace.value.customizations;
});
setTimeout(() => { const isChanged = computed(() => {
this.resizeQueryEditor(); return JSON.stringify(originalTrigger.value) !== JSON.stringify(localTrigger.value);
}, 50); });
window.addEventListener('keydown', this.onKey); const isDefinerInUsers = computed(() => {
}, return originalTrigger.value ? workspace.value.users.some(user => originalTrigger.value.definer === `\`${user.name}\`@\`${user.host}\``) : true;
mounted () { });
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
setTimeout(() => { const schemaTables = computed(() => {
this.$refs.firstInput.focus(); const schemaTables = workspace.value.structure
}, 100); .filter(schema => schema.name === props.schema)
.map(schema => schema.tables);
window.addEventListener('resize', this.resizeQueryEditor); return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
}, });
unmounted () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
changeEvents (event) {
if (this.customizations.triggerMultipleEvents) {
this.localEvents[event] = !this.localEvents[event];
this.localTrigger.event = [];
for (const key in this.localEvents) {
if (this.localEvents[key])
this.localTrigger.event.push(key);
}
}
},
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
...this.localTrigger
};
try { const users = computed(() => {
const { status, response } = await Triggers.createTrigger(params); const users = [{ value: '' }, ...workspace.value.users];
if (!isDefinerInUsers.value) {
const [name, host] = originalTrigger.value.definer.replaceAll('`', '').split('@');
users.unshift({ name, host });
}
if (status === 'success') { return users;
await this.refreshStructure(this.connection.uid); });
this.newTab({ const changeEvents = (event: 'INSERT' | 'UPDATE' | 'DELETE') => {
uid: this.connection.uid, if (customizations.value.triggerMultipleEvents) {
schema: this.schema, localEvents.value[event] = !localEvents.value[event];
elementName: this.localTrigger.name, localTrigger.value.event = [];
elementType: 'trigger', for (const key in localEvents.value) {
type: 'trigger-props' if (localEvents.value[key as 'INSERT' | 'UPDATE' | 'DELETE'])
}); localTrigger.value.event.push(key);
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, trigger: this.localTrigger.name });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localTrigger = JSON.parse(JSON.stringify(this.originalTrigger));
this.$refs.queryEditor.editor.session.setValue(this.localTrigger.sql);
Object.keys(this.localEvents).forEach(event => {
this.localEvents[event] = false;
});
if (this.customizations.triggerMultipleEvents) {
this.originalTrigger.event.forEach(e => {
this.localEvents[e] = true;
});
}
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
} }
} }
}; };
const saveChanges = async () => {
if (isSaving.value) return;
isSaving.value = true;
const params = {
uid: props.connection.uid,
schema: props.schema,
...localTrigger.value
};
try {
const { status, response } = await Triggers.createTrigger(params);
if (status === 'success') {
await refreshStructure(props.connection.uid);
newTab({
uid: props.connection.uid,
schema: props.schema,
elementName: localTrigger.value.name,
elementType: 'trigger',
type: 'trigger-props'
});
removeTab({ uid: props.connection.uid, tab: props.tab.uid });
changeBreadcrumbs({ schema: props.schema, trigger: localTrigger.value.name });
}
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
isSaving.value = false;
};
const clearChanges = () => {
localTrigger.value = JSON.parse(JSON.stringify(originalTrigger.value));
queryEditor.value.editor.session.setValue(localTrigger.value.sql);
Object.keys(localEvents.value).forEach((event: 'INSERT' | 'UPDATE' | 'DELETE') => {
localEvents.value[event] = false;
});
if (customizations.value.triggerMultipleEvents) {
originalTrigger.value.event.forEach((e: 'INSERT' | 'UPDATE' | 'DELETE') => {
localEvents.value[e] = true;
});
}
};
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 onKey = (e: KeyboardEvent) => {
if (props.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (isChanged.value)
saveChanges();
}
}
};
watch(() => props.isSelected, (val) => {
if (val) changeBreadcrumbs({ schema: props.schema });
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
originalTrigger.value = {
sql: customizations.value.triggerSql,
definer: '',
table: schemaTables.value.length ? schemaTables.value[0].name : null,
activation: 'BEFORE',
event: customizations.value.triggerMultipleEvents ? ['INSERT'] : 'INSERT',
name: ''
};
localTrigger.value = JSON.parse(JSON.stringify(originalTrigger.value));
setTimeout(() => {
resizeQueryEditor();
}, 50);
window.addEventListener('keydown', onKey);
onMounted(() => {
if (props.isSelected)
changeBreadcrumbs({ schema: props.schema });
setTimeout(() => {
firstInput.value.focus();
}, 100);
window.addEventListener('resize', resizeQueryEditor);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeQueryEditor);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>

View File

@ -11,16 +11,16 @@
@click="saveChanges" @click="saveChanges"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span> <span>{{ t('word.save') }}</span>
</button> </button>
<button <button
:disabled="!isChanged" :disabled="!isChanged"
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
@click="clearChanges" @click="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
</div> </div>
</div> </div>
@ -30,7 +30,7 @@
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.name') }} {{ t('word.name') }}
</label> </label>
<input <input
ref="firstInput" ref="firstInput"
@ -43,7 +43,7 @@
<div v-if="customizations.triggerFunctionlanguages" class="column col-auto"> <div v-if="customizations.triggerFunctionlanguages" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.language') }} {{ t('word.language') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.language" v-model="localFunction.language"
@ -55,20 +55,20 @@
<div v-if="customizations.definer" class="column col-auto"> <div v-if="customizations.definer" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.definer') }} {{ t('word.definer') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.definer" v-model="localFunction.definer"
:options="workspace.users" :options="workspace.users"
:option-label="(user) => user.value === '' ? $t('message.currentUser') : `${user.name}@${user.host}`" :option-label="(user: any) => user.value === '' ? t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``" :option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select" class="form-select"
/> />
</div> </div>
</div> </div>
<div v-if="customizations.comment" class="form-group"> <div v-if="customizations.comment" class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.comment') }} {{ t('word.comment') }}
</label> </label>
<input <input
v-model="localFunction.comment" v-model="localFunction.comment"
@ -80,7 +80,7 @@
</div> </div>
<div class="workspace-query-results column col-12 mt-2 p-relative"> <div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.functionBody') }}</label> <label class="form-label ml-2">{{ t('message.functionBody') }}</label>
<QueryEditor <QueryEditor
v-show="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
@ -93,190 +93,157 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { storeToRefs } from 'pinia'; import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader.vue';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor.vue';
import Functions from '@/ipc-api/Functions'; import Functions from '@/ipc-api/Functions';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
export default { const { t } = useI18n();
name: 'WorkspaceTabNewTriggerFunction',
components: {
BaseLoader,
QueryEditor,
BaseSelect
},
props: {
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const props = defineProps({
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
});
const { const { addNotification } = useNotificationsStore();
getWorkspace, const workspacesStore = useWorkspacesStore();
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
removeTab,
renameTabs
} = workspacesStore;
return { const {
addNotification, getWorkspace,
selectedWorkspace, refreshStructure,
getWorkspace, changeBreadcrumbs,
refreshStructure, setUnsavedChanges,
changeBreadcrumbs, newTab,
setUnsavedChanges, removeTab
newTab, } = workspacesStore;
removeTab,
renameTabs
};
},
data () {
return {
isLoading: false,
isSaving: false,
isParamsModal: false,
isAskingParameters: false,
originalFunction: {},
localFunction: {},
lastFunction: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
isChanged () {
return JSON.stringify(this.originalFunction) !== JSON.stringify(this.localFunction);
},
isDefinerInUsers () {
return this.originalFunction
? this.workspace.users.some(user => this.originalFunction.definer === `\`${user.name}\`@\`${user.host}\``)
: true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : []; 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 originalFunction = ref(null);
const localFunction = ref(null);
const editorHeight = ref(300);
const workspace = computed(() => {
return getWorkspace(props.connection.uid);
});
const customizations = computed(() => {
return workspace.value.customizations;
});
const isChanged = computed(() => {
return JSON.stringify(originalFunction.value) !== JSON.stringify(localFunction.value);
});
const saveChanges = async () => {
if (isSaving.value) return;
isSaving.value = true;
const params = {
uid: props.connection.uid,
schema: props.schema,
...localFunction.value
};
try {
const { status, response } = await Functions.createTriggerFunction(params);
if (status === 'success') {
await refreshStructure(props.connection.uid);
newTab({
uid: props.connection.uid,
schema: props.schema,
elementName: localFunction.value.name,
elementType: 'triggerFunction',
type: 'trigger-function-props'
});
removeTab({ uid: props.connection.uid, tab: props.tab.uid });
changeBreadcrumbs({ schema: props.schema, triggerFunction: localFunction.value.name });
} }
}, else
watch: { addNotification({ status: 'error', message: response });
isSelected (val) { }
if (val) catch (err) {
this.changeBreadcrumbs({ schema: this.schema }); addNotification({ status: 'error', message: err.stack });
}, }
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
created () {
this.originalFunction = {
sql: this.customizations.triggerFunctionSql,
language: this.customizations.triggerFunctionlanguages.length ? this.customizations.triggerFunctionlanguages[0] : null,
name: ''
};
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction)); isSaving.value = false;
};
setTimeout(() => { const clearChanges = () => {
this.resizeQueryEditor(); localFunction.value = JSON.parse(JSON.stringify(originalFunction.value));
}, 50); queryEditor.value.editor.session.setValue(localFunction.value.sql);
};
window.addEventListener('keydown', this.onKey); const resizeQueryEditor = () => {
}, if (queryEditor.value) {
mounted () { const footer = document.getElementById('footer');
if (this.isSelected) const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
this.changeBreadcrumbs({ schema: this.schema }); editorHeight.value = size;
queryEditor.value.editor.resize();
}
};
setTimeout(() => { const onKey = (e: KeyboardEvent) => {
this.$refs.firstInput.focus(); if (props.isSelected) {
}, 100); e.stopPropagation();
if (e.ctrlKey && e.key === 's') { // CTRL + S
window.addEventListener('resize', this.resizeQueryEditor); if (isChanged.value)
}, saveChanges();
unmounted () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
...this.localFunction
};
try {
const { status, response } = await Functions.createTriggerFunction(params);
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
this.newTab({
uid: this.connection.uid,
schema: this.schema,
elementName: this.localFunction.name,
elementType: 'triggerFunction',
type: 'trigger-function-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, triggerFunction: this.localFunction.name });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
} }
} }
}; };
originalFunction.value = {
sql: customizations.value.triggerFunctionSql,
language: customizations.value.triggerFunctionlanguages.length ? customizations.value.triggerFunctionlanguages[0] : null,
name: ''
};
localFunction.value = JSON.parse(JSON.stringify(originalFunction.value));
setTimeout(() => {
resizeQueryEditor();
}, 50);
window.addEventListener('keydown', onKey);
onMounted(() => {
if (props.isSelected)
changeBreadcrumbs({ schema: props.schema });
setTimeout(() => {
firstInput.value.focus();
}, 100);
window.addEventListener('resize', resizeQueryEditor);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeQueryEditor);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
watch(() => props.isSelected, (val) => {
if (val) changeBreadcrumbs({ schema: props.schema });
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
</script> </script>

View File

@ -11,20 +11,20 @@
@click="saveChanges" @click="saveChanges"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span> <span>{{ t('word.save') }}</span>
</button> </button>
<button <button
:disabled="!isChanged" :disabled="!isChanged"
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
@click="clearChanges" @click="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div> </div>
</div> </div>
@ -34,7 +34,7 @@
<div class="columns"> <div class="columns">
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label> <label class="form-label">{{ t('word.name') }}</label>
<input <input
ref="firstInput" ref="firstInput"
v-model="localView.name" v-model="localView.name"
@ -45,19 +45,19 @@
</div> </div>
<div class="column col-auto"> <div class="column col-auto">
<div v-if="workspace.customizations.definer" class="form-group"> <div v-if="workspace.customizations.definer" class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label> <label class="form-label">{{ t('word.definer') }}</label>
<BaseSelect <BaseSelect
v-model="localView.definer" v-model="localView.definer"
:options="users" :options="users"
:option-label="(user) => user.value === '' ? $t('message.currentUser') : `${user.name}@${user.host}`" :option-label="(user: any) => user.value === '' ? t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``" :option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select" class="form-select"
/> />
</div> </div>
</div> </div>
<div class="column col-auto mr-2"> <div class="column col-auto mr-2">
<div v-if="workspace.customizations.viewSqlSecurity" class="form-group"> <div v-if="workspace.customizations.viewSqlSecurity" class="form-group">
<label class="form-label">{{ $t('message.sqlSecurity') }}</label> <label class="form-label">{{ t('message.sqlSecurity') }}</label>
<BaseSelect <BaseSelect
v-model="localView.security" v-model="localView.security"
:options="['DEFINER', 'INVOKER']" :options="['DEFINER', 'INVOKER']"
@ -67,7 +67,7 @@
</div> </div>
<div class="column col-auto mr-2"> <div class="column col-auto mr-2">
<div v-if="workspace.customizations.viewAlgorithm" class="form-group"> <div v-if="workspace.customizations.viewAlgorithm" class="form-group">
<label class="form-label">{{ $t('word.algorithm') }}</label> <label class="form-label">{{ t('word.algorithm') }}</label>
<BaseSelect <BaseSelect
v-model="localView.algorithm" v-model="localView.algorithm"
:options="['UNDEFINED', 'MERGE', 'TEMPTABLE']" :options="['UNDEFINED', 'MERGE', 'TEMPTABLE']"
@ -77,10 +77,10 @@
</div> </div>
<div v-if="workspace.customizations.viewUpdateOption" class="column col-auto mr-2"> <div v-if="workspace.customizations.viewUpdateOption" class="column col-auto mr-2">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('message.updateOption') }}</label> <label class="form-label">{{ t('message.updateOption') }}</label>
<BaseSelect <BaseSelect
v-model="localView.updateOption" v-model="localView.updateOption"
:option-track-by="(user) => user.value" :option-track-by="(user: any) => user.value"
:options="[{label: 'None', value: ''}, {label: 'CASCADED', value: 'CASCADED'}, {label: 'LOCAL', value: 'LOCAL'}]" :options="[{label: 'None', value: ''}, {label: 'CASCADED', value: 'CASCADED'}, {label: 'LOCAL', value: 'LOCAL'}]"
class="form-select" class="form-select"
/> />
@ -90,7 +90,7 @@
</div> </div>
<div class="workspace-query-results column col-12 mt-2 p-relative"> <div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.selectStatement') }}</label> <label class="form-label ml-2">{{ t('message.selectStatement') }}</label>
<QueryEditor <QueryEditor
v-show="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
@ -103,194 +103,170 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { storeToRefs } from 'pinia'; import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader.vue';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor.vue';
import Views from '@/ipc-api/Views'; import Views from '@/ipc-api/Views';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
export default { const { t } = useI18n();
name: 'WorkspaceTabNewView',
components: {
BaseLoader,
QueryEditor,
BaseSelect
},
props: {
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const props = defineProps({
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
});
const { const { addNotification } = useNotificationsStore();
getWorkspace, const workspacesStore = useWorkspacesStore();
refreshStructure,
setUnsavedChanges,
changeBreadcrumbs,
newTab,
removeTab,
renameTabs
} = workspacesStore;
return { const {
addNotification, getWorkspace,
selectedWorkspace, refreshStructure,
getWorkspace, setUnsavedChanges,
refreshStructure, changeBreadcrumbs,
setUnsavedChanges, newTab,
changeBreadcrumbs, removeTab
newTab, } = workspacesStore;
removeTab,
renameTabs
};
},
data () {
return {
isLoading: false,
isSaving: false,
originalView: {},
localView: {},
lastView: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
isChanged () {
return JSON.stringify(this.originalView) !== JSON.stringify(this.localView);
},
isDefinerInUsers () {
return this.originalView ? this.workspace.users.some(user => this.originalView.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
users () {
const users = [{ value: '' }, ...this.workspace.users];
if (!this.isDefinerInUsers) {
const [name, host] = this.originalView.definer.replaceAll('`', '').split('@');
users.unshift({ name, host });
}
return users; 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.createView(params);
if (status === 'success') {
await refreshStructure(props.connection.uid);
newTab({
uid: props.connection.uid,
schema: props.schema,
elementName: localView.value.name,
elementType: 'view',
type: 'view-props'
});
removeTab({ uid: props.connection.uid, tab: props.tab.uid });
changeBreadcrumbs({ schema: props.schema, view: localView.value.name });
} }
}, else
watch: { addNotification({ status: 'error', message: response });
isSelected (val) { }
if (val) { catch (err) {
this.changeBreadcrumbs({ schema: this.schema, view: this.view }); addNotification({ status: 'error', message: err.stack });
}
setTimeout(() => { isSaving.value = false;
this.resizeQueryEditor(); };
}, 50);
}
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
async created () {
this.originalView = {
algorithm: 'UNDEFINED',
definer: '',
security: 'DEFINER',
updateOption: '',
sql: '',
name: ''
};
this.localView = JSON.parse(JSON.stringify(this.originalView)); const clearChanges = () => {
localView.value = JSON.parse(JSON.stringify(originalView.value));
queryEditor.value.editor.session.setValue(localView.value.sql);
};
setTimeout(() => { const resizeQueryEditor = () => {
this.resizeQueryEditor(); if (queryEditor.value) {
}, 50); const footer = document.getElementById('footer');
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
editorHeight.value = size;
queryEditor.value.editor.resize();
}
};
window.addEventListener('keydown', this.onKey); const onKey = (e: KeyboardEvent) => {
}, if (props.isSelected) {
mounted () { e.stopPropagation();
if (this.isSelected) if (e.ctrlKey && e.key === 's') { // CTRL + S
this.changeBreadcrumbs({ schema: this.schema }); if (isChanged.value)
saveChanges();
setTimeout(() => {
this.$refs.firstInput.focus();
}, 100);
window.addEventListener('resize', this.resizeQueryEditor);
},
unmounted () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
schema: this.schema,
...this.localView
};
try {
const { status, response } = await Views.createView(params);
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
this.newTab({
uid: this.connection.uid,
schema: this.schema,
elementName: this.localView.name,
elementType: 'view',
type: 'view-props'
});
this.removeTab({ uid: this.connection.uid, tab: this.tab.uid });
this.changeBreadcrumbs({ schema: this.schema, view: this.localView.name });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localView = JSON.parse(JSON.stringify(this.originalView));
this.$refs.queryEditor.editor.session.setValue(this.localView.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.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 });
});
originalView.value = {
algorithm: 'UNDEFINED',
definer: '',
security: 'DEFINER',
updateOption: '',
sql: '',
name: ''
};
localView.value = JSON.parse(JSON.stringify(originalView.value));
setTimeout(() => {
resizeQueryEditor();
}, 50);
window.addEventListener('keydown', onKey);
onMounted(() => {
if (props.isSelected)
changeBreadcrumbs({ schema: props.schema });
setTimeout(() => {
firstInput.value.focus();
}, 100);
window.addEventListener('resize', resizeQueryEditor);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeQueryEditor);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>

View File

@ -11,16 +11,16 @@
@click="saveChanges" @click="saveChanges"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span> <span>{{ t('word.save') }}</span>
</button> </button>
<button <button
:disabled="!isChanged" :disabled="!isChanged"
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
@click="clearChanges" @click="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
<div class="divider-vert py-3" /> <div class="divider-vert py-3" />
@ -31,15 +31,15 @@
@click="runFunctionCheck" @click="runFunctionCheck"
> >
<i class="mdi mdi-24px mdi-play mr-1" /> <i class="mdi mdi-24px mdi-play mr-1" />
<span>{{ $t('word.run') }}</span> <span>{{ t('word.run') }}</span>
</button> </button>
<button class="btn btn-dark btn-sm" @click="showParamsModal"> <button class="btn btn-dark btn-sm" @click="showParamsModal">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" /> <i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span> <span>{{ t('word.parameters') }}</span>
</button> </button>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div> </div>
</div> </div>
@ -50,7 +50,7 @@
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.name') }} {{ t('word.name') }}
</label> </label>
<input <input
ref="firstInput" ref="firstInput"
@ -64,7 +64,7 @@
<div v-if="customizations.languages" class="column col-auto"> <div v-if="customizations.languages" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.language') }} {{ t('word.language') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.language" v-model="localFunction.language"
@ -76,13 +76,13 @@
<div v-if="customizations.definer" class="column col-auto"> <div v-if="customizations.definer" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.definer') }} {{ t('word.definer') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.definer" v-model="localFunction.definer"
:options="[{value: '', name:$t('message.currentUser')}, ...workspace.users]" :options="[{value: '', name:t('message.currentUser')}, ...workspace.users]"
:option-label="(user) => user.value === '' ? user.name : `${user.name}@${user.host}`" :option-label="(user: any) => user.value === '' ? user.name : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``" :option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select" class="form-select"
/> />
</div> </div>
@ -90,13 +90,13 @@
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.returns') }} {{ t('word.returns') }}
</label> </label>
<div class="input-group"> <div class="input-group">
<BaseSelect <BaseSelect
v-model="localFunction.returns" v-model="localFunction.returns"
class="form-select text-uppercase" class="form-select text-uppercase"
:options="[{ name: 'VOID' }, ...workspace.dataTypes]" :options="[{ name: 'VOID' }, ...(workspace.dataTypes as any)]"
group-label="group" group-label="group"
group-values="types" group-values="types"
option-label="name" option-label="name"
@ -110,7 +110,7 @@
class="form-input" class="form-input"
type="number" type="number"
min="0" min="0"
:placeholder="$t('word.length')" :placeholder="t('word.length')"
> >
</div> </div>
</div> </div>
@ -118,7 +118,7 @@
<div v-if="customizations.comment" class="column"> <div v-if="customizations.comment" class="column">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.comment') }} {{ t('word.comment') }}
</label> </label>
<input <input
v-model="localFunction.comment" v-model="localFunction.comment"
@ -130,7 +130,7 @@
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('message.sqlSecurity') }} {{ t('message.sqlSecurity') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.security" v-model="localFunction.security"
@ -142,7 +142,7 @@
<div v-if="customizations.functionDataAccess" class="column col-auto"> <div v-if="customizations.functionDataAccess" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('message.dataAccess') }} {{ t('message.dataAccess') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localFunction.dataAccess" v-model="localFunction.dataAccess"
@ -155,7 +155,7 @@
<div class="form-group"> <div class="form-group">
<label class="form-label d-invisible">.</label> <label class="form-label d-invisible">.</label>
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
<input v-model="localFunction.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }} <input v-model="localFunction.deterministic" type="checkbox"><i class="form-icon" /> {{ t('word.deterministic') }}
</label> </label>
</div> </div>
</div> </div>
@ -163,7 +163,7 @@
</div> </div>
<div class="workspace-query-results column col-12 mt-2 p-relative"> <div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.functionBody') }}</label> <label class="form-label ml-2">{{ t('message.functionBody') }}</label>
<QueryEditor <QueryEditor
v-show="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
@ -191,300 +191,273 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { storeToRefs } from 'pinia'; import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader.vue';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor.vue';
import WorkspaceTabPropsFunctionParamsModal from '@/components/WorkspaceTabPropsFunctionParamsModal'; import WorkspaceTabPropsFunctionParamsModal from '@/components/WorkspaceTabPropsFunctionParamsModal.vue';
import ModalAskParameters from '@/components/ModalAskParameters'; import ModalAskParameters from '@/components/ModalAskParameters.vue';
import Functions from '@/ipc-api/Functions'; import Functions from '@/ipc-api/Functions';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import { useI18n } from 'vue-i18n';
import { AlterFunctionParams, FunctionInfos, FunctionParam } from 'common/interfaces/antares';
export default { const { t } = useI18n();
name: 'WorkspaceTabPropsFunction',
components: {
BaseLoader,
QueryEditor,
WorkspaceTabPropsFunctionParamsModal,
ModalAskParameters,
BaseSelect
},
props: {
tabUid: String,
connection: Object,
function: String,
isSelected: Boolean,
schema: String
},
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const props = defineProps({
tabUid: String,
connection: Object,
function: String,
isSelected: Boolean,
schema: String
});
const { const { addNotification } = useNotificationsStore();
getWorkspace, const workspacesStore = useWorkspacesStore();
refreshStructure,
renameTabs,
newTab,
changeBreadcrumbs,
setUnsavedChanges
} = workspacesStore;
return { const {
addNotification, getWorkspace,
selectedWorkspace, refreshStructure,
getWorkspace, renameTabs,
refreshStructure, newTab,
renameTabs, changeBreadcrumbs,
newTab, setUnsavedChanges
changeBreadcrumbs, } = workspacesStore;
setUnsavedChanges
};
},
data () {
return {
isLoading: false,
isSaving: false,
isParamsModal: false,
isAskingParameters: false,
originalFunction: null,
localFunction: { sql: '' },
lastFunction: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
isChanged () {
return JSON.stringify(this.originalFunction) !== JSON.stringify(this.localFunction);
},
isDefinerInUsers () {
return this.originalFunction
? this.workspace.users.some(user => this.originalFunction.definer === `\`${user.name}\`@\`${user.host}\``)
: true;
},
isTableNameValid () {
return this.localFunction.name !== '';
},
isInDataTypes () {
let typeNames = [];
for (const group of this.workspace.dataTypes) {
typeNames = group.types.reduce((acc, curr) => {
acc.push(curr.name);
return acc;
}, []);
}
return typeNames.includes(this.localFunction.returns);
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : []; 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 isParamsModal = ref(false);
const isAskingParameters = ref(false);
const originalFunction: Ref<FunctionInfos> = ref(null);
const localFunction: Ref<FunctionInfos> = ref({ name: '', sql: '', definer: null });
const lastFunction = ref(null);
const sqlProxy = ref('');
const editorHeight = ref(300);
const workspace = computed(() => {
return getWorkspace(props.connection.uid);
});
const customizations = computed(() => {
return workspace.value.customizations;
});
const isChanged = computed(() => {
return JSON.stringify(originalFunction.value) !== JSON.stringify(localFunction.value);
});
const isTableNameValid = computed(() => {
return localFunction.value.name !== '';
});
const getFunctionData = async () => {
if (!props.function) return;
isLoading.value = true;
localFunction.value = { name: '', sql: '', definer: null };
lastFunction.value = props.function;
const params = {
uid: props.connection.uid,
schema: props.schema,
func: props.function
};
try {
const { status, response } = await Functions.getFunctionInformations(params);
if (status === 'success') {
originalFunction.value = response;
originalFunction.value.parameters = [...originalFunction.value.parameters.map(param => {
param._antares_id = uidGen();
return param;
})];
localFunction.value = JSON.parse(JSON.stringify(originalFunction.value));
sqlProxy.value = localFunction.value.sql;
} }
}, else
watch: { addNotification({ status: 'error', message: response });
async schema () { }
if (this.isSelected) { catch (err) {
await this.getFunctionData(); addNotification({ status: 'error', message: err.stack });
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql); }
this.lastFunction = this.function;
resizeQueryEditor();
isLoading.value = false;
};
const saveChanges = async () => {
if (isSaving.value) return;
isSaving.value = true;
const params = {
uid: props.connection.uid,
func: {
...localFunction.value,
schema: props.schema,
oldName: originalFunction.value.name
} as AlterFunctionParams
};
try {
const { status, response } = await Functions.alterFunction(params);
if (status === 'success') {
const oldName = originalFunction.value.name;
await refreshStructure(props.connection.uid);
if (oldName !== localFunction.value.name) {
renameTabs({
uid: props.connection.uid,
schema: props.schema,
elementName: oldName,
elementNewName: localFunction.value.name,
elementType: 'function'
});
changeBreadcrumbs({ schema: props.schema, function: localFunction.value.name });
} }
},
async function () {
if (this.isSelected) {
await this.getFunctionData();
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
this.lastFunction = this.function;
}
},
async isSelected (val) {
if (val) {
this.changeBreadcrumbs({ schema: this.schema, function: this.function });
setTimeout(() => {
this.resizeQueryEditor();
}, 200);
if (this.lastFunction !== this.function)
this.getRoutineData();
}
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
async created () {
await this.getFunctionData();
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
window.addEventListener('keydown', this.onKey);
},
mounted () {
window.addEventListener('resize', this.resizeQueryEditor);
},
unmounted () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async getFunctionData () {
if (!this.function) return;
this.isLoading = true;
this.localFunction = { sql: '' };
this.lastFunction = this.function;
const params = {
uid: this.connection.uid,
schema: this.schema,
func: this.function
};
try {
const { status, response } = await Functions.getFunctionInformations(params);
if (status === 'success') {
this.originalFunction = response;
this.originalFunction.parameters = [...this.originalFunction.parameters.map(param => {
param._antares_id = uidGen();
return param;
})];
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
this.sqlProxy = this.localFunction.sql;
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.resizeQueryEditor();
this.isLoading = false;
},
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
func: {
...this.localFunction,
schema: this.schema,
oldName: this.originalFunction.name
}
};
try {
const { status, response } = await Functions.alterFunction(params);
if (status === 'success') {
const oldName = this.originalFunction.name;
await this.refreshStructure(this.connection.uid);
if (oldName !== this.localFunction.name) {
this.renameTabs({
uid: this.connection.uid,
schema: this.schema,
elementName: oldName,
elementNewName: this.localFunction.name,
elementType: 'function'
});
this.changeBreadcrumbs({ schema: this.schema, function: this.localFunction.name });
}
else
this.getFunctionData();
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
optionsUpdate (options) {
this.localFunction = options;
},
parametersUpdate (parameters) {
this.localFunction = { ...this.localFunction, parameters };
},
runFunctionCheck () {
if (this.localFunction.parameters.length)
this.showAskParamsModal();
else else
this.runFunction(); getFunctionData();
}, }
runFunction (params) { else
if (!params) params = []; addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
let sql; isSaving.value = false;
switch (this.connection.client) { // TODO: move in a better place };
case 'maria':
case 'mysql':
sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`;
break;
case 'pg':
sql = `SELECT ${this.originalFunction.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `SELECT ${this.originalFunction.name} ${params.join(',')}`;
break;
default:
sql = `SELECT \`${this.originalFunction.name}\` (${params.join(',')})`;
}
this.newTab({ uid: this.connection.uid, content: sql, type: 'query', autorun: true }); const clearChanges = () => {
}, localFunction.value = JSON.parse(JSON.stringify(originalFunction.value));
showParamsModal () { queryEditor.value.editor.session.setValue(localFunction.value.sql);
this.isParamsModal = true; };
},
hideParamsModal () { const resizeQueryEditor = () => {
this.isParamsModal = false; if (queryEditor.value) {
}, const footer = document.getElementById('footer');
showAskParamsModal () { const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
this.isAskingParameters = true; editorHeight.value = size;
}, queryEditor.value.editor.resize();
hideAskParamsModal () { }
this.isAskingParameters = false; };
},
onKey (e) { const parametersUpdate = (parameters: FunctionParam[]) => {
if (this.isSelected) { localFunction.value = { ...localFunction.value, parameters };
e.stopPropagation(); };
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged) const runFunctionCheck = () => {
this.saveChanges(); if (localFunction.value.parameters.length)
} showAskParamsModal();
} else
runFunction();
};
const runFunction = (params?: string[]) => {
if (!params) params = [];
let sql;
switch (props.connection.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
sql = `SELECT \`${originalFunction.value.name}\` (${params.join(',')})`;
break;
case 'pg':
sql = `SELECT ${originalFunction.value.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `SELECT ${originalFunction.value.name} ${params.join(',')}`;
break;
default:
sql = `SELECT \`${originalFunction.value.name}\` (${params.join(',')})`;
}
newTab({ uid: props.connection.uid, content: sql, type: 'query', autorun: true });
};
const showParamsModal = () => {
isParamsModal.value = true;
};
const hideParamsModal = () => {
isParamsModal.value = false;
};
const showAskParamsModal = () => {
isAskingParameters.value = true;
};
const hideAskParamsModal = () => {
isAskingParameters.value = false;
};
const onKey = (e: KeyboardEvent) => {
if (props.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (isChanged.value)
saveChanges();
} }
} }
}; };
watch(() => props.schema, async () => {
if (props.isSelected) {
await getFunctionData();
queryEditor.value.editor.session.setValue(localFunction.value.sql);
lastFunction.value = props.function;
}
});
watch(() => props.function, async () => {
if (props.isSelected) {
await getFunctionData();
queryEditor.value.editor.session.setValue(localFunction.value.sql);
lastFunction.value = props.function;
}
});
watch(() => props.isSelected, async (val) => {
if (val) {
changeBreadcrumbs({ schema: props.schema, function: props.function });
setTimeout(() => {
resizeQueryEditor();
}, 200);
if (lastFunction.value !== props.function)
getFunctionData();
}
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
(async () => {
await getFunctionData();
queryEditor.value.editor.session.setValue(localFunction.value.sql);
window.addEventListener('keydown', onKey);
})();
onMounted(() => {
window.addEventListener('resize', resizeQueryEditor);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeQueryEditor);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<ConfirmModal <ConfirmModal
:confirm-text="$t('word.confirm')" :confirm-text="t('word.confirm')"
size="medium" size="medium"
class="options-modal" class="options-modal"
@confirm="confirmParametersChange" @confirm="confirmParametersChange"
@ -9,7 +9,7 @@
<template #header> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" /> <i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span class="cut-text">{{ $t('word.parameters') }} "{{ func }}"</span> <span class="cut-text">{{ t('word.parameters') }} "{{ func }}"</span>
</div> </div>
</template> </template>
<template #body> <template #body>
@ -20,16 +20,16 @@
<div class="d-flex"> <div class="d-flex">
<button class="btn btn-dark btn-sm d-flex" @click="addParameter"> <button class="btn btn-dark btn-sm d-flex" @click="addParameter">
<i class="mdi mdi-24px mdi-plus mr-1" /> <i class="mdi mdi-24px mdi-plus mr-1" />
<span>{{ $t('word.add') }}</span> <span>{{ t('word.add') }}</span>
</button> </button>
<button <button
class="btn btn-dark btn-sm d-flex ml-2 mr-0" class="btn btn-dark btn-sm d-flex ml-2 mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
:disabled="!isChanged" :disabled="!isChanged"
@click.prevent="clearChanges" @click.prevent="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
</div> </div>
</div> </div>
@ -55,7 +55,7 @@
<div class="tile-action"> <div class="tile-action">
<button <button
class="btn btn-link remove-field p-0 mr-2" class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')" :title="t('word.delete')"
@click.prevent="removeParameter(param._antares_id)" @click.prevent="removeParameter(param._antares_id)"
> >
<i class="mdi mdi-close" /> <i class="mdi mdi-close" />
@ -74,7 +74,7 @@
> >
<div class="form-group"> <div class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.name') }} {{ t('word.name') }}
</label> </label>
<div class="column"> <div class="column">
<input <input
@ -86,7 +86,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.type') }} {{ t('word.type') }}
</label> </label>
<div class="column"> <div class="column">
<BaseSelect <BaseSelect
@ -102,7 +102,7 @@
</div> </div>
<div v-if="customizations.parametersLength" class="form-group"> <div v-if="customizations.parametersLength" class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.length') }} {{ t('word.length') }}
</label> </label>
<div class="column"> <div class="column">
<input <input
@ -115,7 +115,7 @@
</div> </div>
<div v-if="customizations.functionContext" class="form-group"> <div v-if="customizations.functionContext" class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.context') }} {{ t('word.context') }}
</label> </label>
<div class="column"> <div class="column">
<label class="form-radio"> <label class="form-radio">
@ -150,11 +150,11 @@
<i class="mdi mdi-dots-horizontal mdi-48px" /> <i class="mdi mdi-dots-horizontal mdi-48px" />
</div> </div>
<p class="empty-title h5"> <p class="empty-title h5">
{{ $t('message.thereAreNoParameters') }} {{ t('message.thereAreNoParameters') }}
</p> </p>
<div class="empty-action"> <div class="empty-action">
<button class="btn btn-primary" @click="addParameter"> <button class="btn btn-primary" @click="addParameter">
{{ $t('message.createNewParameter') }} {{ t('message.createNewParameter') }}
</button> </button>
</div> </div>
</div> </div>
@ -164,113 +164,118 @@
</ConfirmModal> </ConfirmModal>
</template> </template>
<script> <script setup lang="ts">
import { computed, onMounted, onUnmounted, Ref, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
export default { const { t } = useI18n();
name: 'WorkspaceTabPropsFunctionParamsModal',
components: {
ConfirmModal,
BaseSelect
},
props: {
localParameters: {
type: Array,
default: () => []
},
func: String,
workspace: Object
},
emits: ['hide', 'parameters-update'],
data () {
return {
parametersProxy: [],
isOptionsChanging: false,
selectedParam: '',
modalInnerHeight: 400,
i: 1
};
},
computed: {
selectedParamObj () {
return this.parametersProxy.find(param => param._antares_id === this.selectedParam);
},
isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
},
customizations () {
return this.workspace.customizations;
}
},
mounted () {
this.parametersProxy = JSON.parse(JSON.stringify(this.localParameters));
this.i = this.parametersProxy.length + 1;
if (this.parametersProxy.length) const props = defineProps({
this.resetSelectedID(); localParameters: {
type: Array,
this.getModalInnerHeight(); default: () => []
window.addEventListener('resize', this.getModalInnerHeight);
}, },
unmounted () { func: String,
window.removeEventListener('resize', this.getModalInnerHeight); workspace: Object
}, });
methods: {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
confirmParametersChange () {
this.$emit('parameters-update', this.parametersProxy);
},
selectParameter (event, uid) {
if (this.selectedParam !== uid && !event.target.classList.contains('remove-field'))
this.selectedParam = uid;
},
getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body');
if (modalBody)
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
},
addParameter () {
const newUid = uidGen();
this.parametersProxy = [...this.parametersProxy, {
_antares_id: newUid,
name: `param${this.i++}`,
type: this.workspace.dataTypes[0].types[0].name,
context: 'IN',
length: ''
}];
if (this.parametersProxy.length === 1) const emit = defineEmits(['hide', 'parameters-update']);
this.resetSelectedID();
setTimeout(() => { const parametersPanel: Ref<HTMLDivElement> = ref(null);
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60; const parametersProxy = ref([]);
this.selectedParam = newUid; const selectedParam = ref('');
}, 20); const modalInnerHeight = ref(400);
}, const i = ref(1);
removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param._antares_id !== uid);
if (this.parametersProxy.length && this.selectedParam === uid) const selectedParamObj = computed(() => {
this.resetSelectedID(); return parametersProxy.value.find(param => param._antares_id === selectedParam.value);
}, });
clearChanges () {
this.parametersProxy = JSON.parse(JSON.stringify(this.localParameters));
this.i = this.parametersProxy.length + 1;
if (!this.parametersProxy.some(param => param.name === this.selectedParam)) const isChanged = computed(() => {
this.resetSelectedID(); return JSON.stringify(props.localParameters) !== JSON.stringify(parametersProxy.value);
}, });
resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._antares_id : ''; const customizations = computed(() => {
} return props.workspace.customizations;
} });
const typeClass = (type: string) => {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
}; };
const confirmParametersChange = () => {
emit('parameters-update', parametersProxy.value);
};
const selectParameter = (event: MouseEvent, uid: string) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (selectedParam.value !== uid && !(event.target as any).classList.contains('remove-field'))
selectedParam.value = uid;
};
const getModalInnerHeight = () => {
const modalBody = document.querySelector('.modal-body');
if (modalBody)
modalInnerHeight.value = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
};
const addParameter = () => {
const newUid = uidGen();
parametersProxy.value = [...parametersProxy.value, {
_antares_id: newUid,
name: `param${i.value++}`,
type: props.workspace.dataTypes[0].types[0].name,
context: 'IN',
length: ''
}];
if (parametersProxy.value.length === 1)
resetSelectedID();
setTimeout(() => {
parametersPanel.value.scrollTop = parametersPanel.value.scrollHeight + 60;
selectedParam.value = newUid;
}, 20);
};
const removeParameter = (uid: string) => {
parametersProxy.value = parametersProxy.value.filter(param => param._antares_id !== uid);
if (parametersProxy.value.length && selectedParam.value === uid)
resetSelectedID();
};
const clearChanges = () => {
parametersProxy.value = JSON.parse(JSON.stringify(props.localParameters));
i.value = parametersProxy.value.length + 1;
if (!parametersProxy.value.some(param => param.name === selectedParam.value))
resetSelectedID();
};
const resetSelectedID = () => {
selectedParam.value = parametersProxy.value.length ? parametersProxy.value[0]._antares_id : '';
};
onMounted(() => {
parametersProxy.value = JSON.parse(JSON.stringify(props.localParameters));
i.value = parametersProxy.value.length + 1;
if (parametersProxy.value.length)
resetSelectedID();
getModalInnerHeight();
window.addEventListener('resize', getModalInnerHeight);
});
onUnmounted(() => {
window.removeEventListener('resize', getModalInnerHeight);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -11,16 +11,16 @@
@click="saveChanges" @click="saveChanges"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span> <span>{{ t('word.save') }}</span>
</button> </button>
<button <button
:disabled="!isChanged" :disabled="!isChanged"
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
@click="clearChanges" @click="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
<div class="divider-vert py-3" /> <div class="divider-vert py-3" />
@ -31,15 +31,15 @@
@click="runRoutineCheck" @click="runRoutineCheck"
> >
<i class="mdi mdi-24px mdi-play mr-1" /> <i class="mdi mdi-24px mdi-play mr-1" />
<span>{{ $t('word.run') }}</span> <span>{{ t('word.run') }}</span>
</button> </button>
<button class="btn btn-dark btn-sm" @click="showParamsModal"> <button class="btn btn-dark btn-sm" @click="showParamsModal">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" /> <i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span> <span>{{ t('word.parameters') }}</span>
</button> </button>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div> </div>
</div> </div>
@ -50,7 +50,7 @@
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.name') }} {{ t('word.name') }}
</label> </label>
<input <input
ref="firstInput" ref="firstInput"
@ -64,7 +64,7 @@
<div v-if="customizations.languages" class="column col-auto"> <div v-if="customizations.languages" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.language') }} {{ t('word.language') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localRoutine.language" v-model="localRoutine.language"
@ -76,13 +76,13 @@
<div v-if="customizations.definer" class="column col-auto"> <div v-if="customizations.definer" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.definer') }} {{ t('word.definer') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localRoutine.definer" v-model="localRoutine.definer"
:options="[{value: '', name:$t('message.currentUser')}, ...workspace.users]" :options="[{value: '', name: t('message.currentUser')}, ...workspace.users]"
:option-label="(user) => user.value === '' ? user.name : `${user.name}@${user.host}`" :option-label="(user: any) => user.value === '' ? user.name : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``" :option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select" class="form-select"
/> />
</div> </div>
@ -90,7 +90,7 @@
<div v-if="customizations.comment" class="column"> <div v-if="customizations.comment" class="column">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('word.comment') }} {{ t('word.comment') }}
</label> </label>
<input <input
v-model="localRoutine.comment" v-model="localRoutine.comment"
@ -102,7 +102,7 @@
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('message.sqlSecurity') }} {{ t('message.sqlSecurity') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localRoutine.security" v-model="localRoutine.security"
@ -114,7 +114,7 @@
<div v-if="customizations.procedureDataAccess" class="column col-auto"> <div v-if="customizations.procedureDataAccess" class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
{{ $t('message.dataAccess') }} {{ t('message.dataAccess') }}
</label> </label>
<BaseSelect <BaseSelect
v-model="localRoutine.dataAccess" v-model="localRoutine.dataAccess"
@ -127,7 +127,7 @@
<div class="form-group"> <div class="form-group">
<label class="form-label d-invisible">.</label> <label class="form-label d-invisible">.</label>
<label class="form-checkbox form-inline"> <label class="form-checkbox form-inline">
<input v-model="localRoutine.deterministic" type="checkbox"><i class="form-icon" /> {{ $t('word.deterministic') }} <input v-model="localRoutine.deterministic" type="checkbox"><i class="form-icon" /> {{ t('word.deterministic') }}
</label> </label>
</div> </div>
</div> </div>
@ -135,7 +135,7 @@
</div> </div>
<div class="workspace-query-results column col-12 mt-2 p-relative"> <div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.routineBody') }}</label> <label class="form-label ml-2">{{ t('message.routineBody') }}</label>
<QueryEditor <QueryEditor
v-show="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
@ -163,286 +163,272 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { storeToRefs } from 'pinia'; import { Component, computed, onUnmounted, onBeforeUnmount, onMounted, Ref, ref, watch } from 'vue';
import { AlterRoutineParams, FunctionParam, RoutineInfos } from 'common/interfaces/antares';
import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
import WorkspaceTabPropsRoutineParamsModal from '@/components/WorkspaceTabPropsRoutineParamsModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import Routines from '@/ipc-api/Routines'; import Routines from '@/ipc-api/Routines';
import QueryEditor from '@/components/QueryEditor.vue';
import BaseLoader from '@/components/BaseLoader.vue';
import WorkspaceTabPropsRoutineParamsModal from '@/components/WorkspaceTabPropsRoutineParamsModal.vue';
import ModalAskParameters from '@/components/ModalAskParameters.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
export default { const { t } = useI18n();
name: 'WorkspaceTabPropsRoutine',
components: {
QueryEditor,
BaseLoader,
WorkspaceTabPropsRoutineParamsModal,
ModalAskParameters,
BaseSelect
},
props: {
tabUid: String,
connection: Object,
routine: String,
isSelected: Boolean,
schema: String
},
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const props = defineProps({
tabUid: String,
connection: Object,
routine: String,
isSelected: Boolean,
schema: String
});
const { const { addNotification } = useNotificationsStore();
getWorkspace, const workspacesStore = useWorkspacesStore();
refreshStructure,
renameTabs,
newTab,
changeBreadcrumbs,
setUnsavedChanges
} = workspacesStore;
return { const {
addNotification, getWorkspace,
selectedWorkspace, refreshStructure,
getWorkspace, renameTabs,
refreshStructure, newTab,
renameTabs, changeBreadcrumbs,
newTab, setUnsavedChanges
changeBreadcrumbs, } = workspacesStore;
setUnsavedChanges
};
},
data () {
return {
isLoading: false,
isSaving: false,
isParamsModal: false,
isAskingParameters: false,
originalRoutine: null,
localRoutine: { sql: '' },
lastRoutine: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
customizations () {
return this.workspace.customizations;
},
isChanged () {
return JSON.stringify(this.originalRoutine) !== JSON.stringify(this.localRoutine);
},
isDefinerInUsers () {
return this.originalRoutine ? this.workspace.users.some(user => this.originalRoutine.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
isTableNameValid () {
return this.localRoutine.name !== '';
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : []; 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 isParamsModal = ref(false);
const isAskingParameters = ref(false);
const originalRoutine: Ref<RoutineInfos> = ref(null);
const localRoutine: Ref<RoutineInfos> = ref(null);
const lastRoutine = ref(null);
const sqlProxy = ref('');
const editorHeight = ref(300);
const workspace = computed(() => {
return getWorkspace(props.connection.uid);
});
const customizations = computed(() => {
return workspace.value.customizations;
});
const isChanged = computed(() => {
return JSON.stringify(originalRoutine.value) !== JSON.stringify(localRoutine.value);
});
const isTableNameValid = computed(() => {
return localRoutine.value.name !== '';
});
const getRoutineData = async () => {
if (!props.routine) return;
localRoutine.value = { name: '', sql: '', definer: null };
isLoading.value = true;
lastRoutine.value = props.routine;
const params = {
uid: props.connection.uid,
schema: props.schema,
routine: props.routine
};
try {
const { status, response } = await Routines.getRoutineInformations(params);
if (status === 'success') {
originalRoutine.value = response;
originalRoutine.value.parameters = [...originalRoutine.value.parameters.map(param => {
param._antares_id = uidGen();
return param;
})];
localRoutine.value = JSON.parse(JSON.stringify(originalRoutine.value));
sqlProxy.value = localRoutine.value.sql;
} }
}, else
watch: { addNotification({ status: 'error', message: response });
async schema () { }
if (this.isSelected) { catch (err) {
await this.getRoutineData(); addNotification({ status: 'error', message: err.stack });
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql); }
this.lastRoutine = this.routine;
resizeQueryEditor();
isLoading.value = false;
};
const saveChanges = async () => {
if (isSaving.value) return;
isSaving.value = true;
const params = {
uid: props.connection.uid as string,
routine: {
...localRoutine.value,
schema: props.schema,
oldName: originalRoutine.value.name
} as AlterRoutineParams
};
try {
const { status, response } = await Routines.alterRoutine(params);
if (status === 'success') {
const oldName = originalRoutine.value.name;
await refreshStructure(props.connection.uid);
if (oldName !== localRoutine.value.name) {
renameTabs({
uid: props.connection.uid,
schema: props.schema,
elementName: oldName,
elementNewName: localRoutine.value.name,
elementType: 'procedure'
});
changeBreadcrumbs({ schema: props.schema, routine: localRoutine.value.name });
} }
},
async routine () {
if (this.isSelected) {
await this.getRoutineData();
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql);
this.lastRoutine = this.routine;
}
},
async isSelected (val) {
if (val) {
this.changeBreadcrumbs({ schema: this.schema, routine: this.routine });
setTimeout(() => {
this.resizeQueryEditor();
}, 200);
if (this.lastRoutine !== this.routine)
this.getRoutineData();
}
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
}
},
async created () {
await this.getRoutineData();
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql);
window.addEventListener('keydown', this.onKey);
},
mounted () {
window.addEventListener('resize', this.resizeQueryEditor);
},
unmounted () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async getRoutineData () {
if (!this.routine) return;
this.localRoutine = { sql: '' };
this.isLoading = true;
this.lastRoutine = this.routine;
const params = {
uid: this.connection.uid,
schema: this.schema,
routine: this.routine
};
try {
const { status, response } = await Routines.getRoutineInformations(params);
if (status === 'success') {
this.originalRoutine = response;
this.originalRoutine.parameters = [...this.originalRoutine.parameters.map(param => {
param._antares_id = uidGen();
return param;
})];
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
this.sqlProxy = this.localRoutine.sql;
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.resizeQueryEditor();
this.isLoading = false;
},
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
routine: {
...this.localRoutine,
schema: this.schema,
oldName: this.originalRoutine.name
}
};
try {
const { status, response } = await Routines.alterRoutine(params);
if (status === 'success') {
const oldName = this.originalRoutine.name;
await this.refreshStructure(this.connection.uid);
if (oldName !== this.localRoutine.name) {
this.renameTabs({
uid: this.connection.uid,
schema: this.schema,
elementName: oldName,
elementNewName: this.localRoutine.name,
elementType: 'procedure'
});
this.changeBreadcrumbs({ schema: this.schema, procedure: this.localRoutine.name });
}
else
this.getRoutineData();
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
optionsUpdate (options) {
this.localRoutine = options;
},
parametersUpdate (parameters) {
this.localRoutine = { ...this.localRoutine, parameters };
},
runRoutineCheck () {
if (this.localRoutine.parameters.length)
this.showAskParamsModal();
else else
this.runRoutine(); getRoutineData();
}, }
runRoutine (params) { else
if (!params) params = []; addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
let sql; isSaving.value = false;
switch (this.connection.client) { // TODO: move in a better place };
case 'maria':
case 'mysql':
case 'pg':
sql = `CALL ${this.originalRoutine.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `EXEC ${this.originalRoutine.name} ${params.join(',')}`;
break;
default:
sql = `CALL \`${this.originalRoutine.name}\`(${params.join(',')})`;
}
this.newTab({ uid: this.connection.uid, content: sql, type: 'query', autorun: true }); const clearChanges = () => {
}, localRoutine.value = JSON.parse(JSON.stringify(originalRoutine.value));
showParamsModal () { queryEditor.value.editor.session.setValue(localRoutine.value.sql);
this.isParamsModal = true; };
},
hideParamsModal () { const resizeQueryEditor = () => {
this.isParamsModal = false; if (queryEditor.value) {
}, const footer = document.getElementById('footer');
showAskParamsModal () { const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
this.isAskingParameters = true; editorHeight.value = size;
}, queryEditor.value.editor.resize();
hideAskParamsModal () { }
this.isAskingParameters = false; };
},
onKey (e) { const parametersUpdate = (parameters: FunctionParam[]) => {
if (this.isSelected) { localRoutine.value = { ...localRoutine.value, parameters };
e.stopPropagation(); };
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged) const runRoutineCheck = () => {
this.saveChanges(); if (localRoutine.value.parameters.length)
} showAskParamsModal();
} else
runRoutine();
};
const runRoutine = (params?: string[]) => {
if (!params) params = [];
let sql;
switch (props.connection.client) { // TODO: move in a better place
case 'maria':
case 'mysql':
case 'pg':
sql = `CALL ${originalRoutine.value.name}(${params.join(',')})`;
break;
case 'mssql':
sql = `EXEC ${originalRoutine.value.name} ${params.join(',')}`;
break;
default:
sql = `CALL \`${originalRoutine.value.name}\`(${params.join(',')})`;
}
newTab({ uid: props.connection.uid, content: sql, type: 'query', autorun: true });
};
const showParamsModal = () => {
isParamsModal.value = true;
};
const hideParamsModal = () => {
isParamsModal.value = false;
};
const showAskParamsModal = () => {
isAskingParameters.value = true;
};
const hideAskParamsModal = () => {
isAskingParameters.value = false;
};
const onKey = (e: KeyboardEvent) => {
if (props.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.key === 's') { // CTRL + S
if (isChanged.value)
saveChanges();
} }
} }
}; };
watch(() => props.schema, async () => {
if (props.isSelected) {
await getRoutineData();
queryEditor.value.editor.session.setValue(localRoutine.value.sql);
lastRoutine.value = props.routine;
}
});
watch(() => props.routine, async () => {
if (props.isSelected) {
await getRoutineData();
queryEditor.value.editor.session.setValue(localRoutine.value.sql);
lastRoutine.value = props.routine;
}
});
watch(() => props.isSelected, async (val) => {
if (val) {
changeBreadcrumbs({ schema: props.schema, routine: props.routine });
setTimeout(() => {
resizeQueryEditor();
}, 200);
if (lastRoutine.value !== props.routine)
getRoutineData();
}
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
(async () => {
await getRoutineData();
queryEditor.value.editor.session.setValue(localRoutine.value.sql);
window.addEventListener('keydown', onKey);
})();
onMounted(() => {
window.addEventListener('resize', resizeQueryEditor);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeQueryEditor);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<ConfirmModal <ConfirmModal
:confirm-text="$t('word.confirm')" :confirm-text="t('word.confirm')"
size="medium" size="medium"
class="options-modal" class="options-modal"
@confirm="confirmParametersChange" @confirm="confirmParametersChange"
@ -9,7 +9,7 @@
<template #header> <template #header>
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" /> <i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span class="cut-text">{{ $t('word.parameters') }} "{{ routine }}"</span> <span class="cut-text">{{ t('word.parameters') }} "{{ routine }}"</span>
</div> </div>
</template> </template>
<template #body> <template #body>
@ -20,16 +20,16 @@
<div class="d-flex"> <div class="d-flex">
<button class="btn btn-dark btn-sm d-flex" @click="addParameter"> <button class="btn btn-dark btn-sm d-flex" @click="addParameter">
<i class="mdi mdi-24px mdi-plus mr-1" /> <i class="mdi mdi-24px mdi-plus mr-1" />
<span>{{ $t('word.add') }}</span> <span>{{ t('word.add') }}</span>
</button> </button>
<button <button
class="btn btn-dark btn-sm d-flex ml-2 mr-0" class="btn btn-dark btn-sm d-flex ml-2 mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
:disabled="!isChanged" :disabled="!isChanged"
@click.prevent="clearChanges" @click.prevent="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
</div> </div>
</div> </div>
@ -55,7 +55,7 @@
<div class="tile-action"> <div class="tile-action">
<button <button
class="btn btn-link remove-field p-0 mr-2" class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')" :title="t('word.delete')"
@click.prevent="removeParameter(param._antares_id)" @click.prevent="removeParameter(param._antares_id)"
> >
<i class="mdi mdi-close" /> <i class="mdi mdi-close" />
@ -74,7 +74,7 @@
> >
<div class="form-group"> <div class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.name') }} {{ t('word.name') }}
</label> </label>
<div class="column"> <div class="column">
<input <input
@ -86,7 +86,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.type') }} {{ t('word.type') }}
</label> </label>
<div class="column"> <div class="column">
<BaseSelect <BaseSelect
@ -102,7 +102,7 @@
</div> </div>
<div v-if="customizations.parametersLength" class="form-group"> <div v-if="customizations.parametersLength" class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.length') }} {{ t('word.length') }}
</label> </label>
<div class="column"> <div class="column">
<input <input
@ -115,7 +115,7 @@
</div> </div>
<div v-if="customizations.procedureContext" class="form-group"> <div v-if="customizations.procedureContext" class="form-group">
<label class="form-label col-3"> <label class="form-label col-3">
{{ $t('word.context') }} {{ t('word.context') }}
</label> </label>
<div class="column"> <div class="column">
<label class="form-radio"> <label class="form-radio">
@ -150,11 +150,11 @@
<i class="mdi mdi-dots-horizontal mdi-48px" /> <i class="mdi mdi-dots-horizontal mdi-48px" />
</div> </div>
<p class="empty-title h5"> <p class="empty-title h5">
{{ $t('message.thereAreNoParameters') }} {{ t('message.thereAreNoParameters') }}
</p> </p>
<div class="empty-action"> <div class="empty-action">
<button class="btn btn-primary" @click="addParameter"> <button class="btn btn-primary" @click="addParameter">
{{ $t('message.createNewParameter') }} {{ t('message.createNewParameter') }}
</button> </button>
</div> </div>
</div> </div>
@ -164,113 +164,118 @@
</ConfirmModal> </ConfirmModal>
</template> </template>
<script> <script setup lang="ts">
import { computed, onMounted, onUnmounted, Ref, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
export default { const { t } = useI18n();
name: 'WorkspaceTabPropsRoutineParamsModal',
components: {
ConfirmModal,
BaseSelect
},
props: {
localParameters: {
type: Array,
default: () => []
},
routine: String,
workspace: Object
},
emits: ['parameters-update', 'hide'],
data () {
return {
parametersProxy: [],
isOptionsChanging: false,
selectedParam: '',
modalInnerHeight: 400,
i: 1
};
},
computed: {
selectedParamObj () {
return this.parametersProxy.find(param => param._antares_id === this.selectedParam);
},
isChanged () {
return JSON.stringify(this.localParameters) !== JSON.stringify(this.parametersProxy);
},
customizations () {
return this.workspace.customizations;
}
},
mounted () {
this.parametersProxy = JSON.parse(JSON.stringify(this.localParameters));
this.i = this.parametersProxy.length + 1;
if (this.parametersProxy.length) const props = defineProps({
this.resetSelectedID(); localParameters: {
type: Array,
this.getModalInnerHeight(); default: () => []
window.addEventListener('resize', this.getModalInnerHeight);
}, },
unmounted () { routine: String,
window.removeEventListener('resize', this.getModalInnerHeight); workspace: Object
}, });
methods: {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
confirmParametersChange () {
this.$emit('parameters-update', this.parametersProxy);
},
selectParameter (event, uid) {
if (this.selectedParam !== uid && !event.target.classList.contains('remove-field'))
this.selectedParam = uid;
},
getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body');
if (modalBody)
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
},
addParameter () {
const newUid = uidGen();
this.parametersProxy = [...this.parametersProxy, {
_antares_id: newUid,
name: `param${this.i++}`,
type: this.workspace.dataTypes[0].types[0].name,
context: 'IN',
length: ''
}];
if (this.parametersProxy.length === 1) const emit = defineEmits(['hide', 'parameters-update']);
this.resetSelectedID();
setTimeout(() => { const parametersPanel: Ref<HTMLDivElement> = ref(null);
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60; const parametersProxy = ref([]);
this.selectedParam = newUid; const selectedParam = ref('');
}, 20); const modalInnerHeight = ref(400);
}, const i = ref(1);
removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param._antares_id !== uid);
if (this.parametersProxy.length && this.selectedParam === uid) const selectedParamObj = computed(() => {
this.resetSelectedID(); return parametersProxy.value.find(param => param._antares_id === selectedParam.value);
}, });
clearChanges () {
this.parametersProxy = JSON.parse(JSON.stringify(this.localParameters));
this.i = this.parametersProxy.length + 1;
if (!this.parametersProxy.some(param => param.name === this.selectedParam)) const isChanged = computed(() => {
this.resetSelectedID(); return JSON.stringify(props.localParameters) !== JSON.stringify(parametersProxy.value);
}, });
resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._antares_id : ''; const customizations = computed(() => {
} return props.workspace.customizations;
} });
const typeClass = (type: string) => {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
}; };
const confirmParametersChange = () => {
emit('parameters-update', parametersProxy.value);
};
const selectParameter = (event: MouseEvent, uid: string) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (selectedParam.value !== uid && !(event.target as any).classList.contains('remove-field'))
selectedParam.value = uid;
};
const getModalInnerHeight = () => {
const modalBody = document.querySelector('.modal-body');
if (modalBody)
modalInnerHeight.value = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
};
const addParameter = () => {
const newUid = uidGen();
parametersProxy.value = [...parametersProxy.value, {
_antares_id: newUid,
name: `param${i.value++}`,
type: props.workspace.dataTypes[0].types[0].name,
context: 'IN',
length: ''
}];
if (parametersProxy.value.length === 1)
resetSelectedID();
setTimeout(() => {
parametersPanel.value.scrollTop = parametersPanel.value.scrollHeight + 60;
selectedParam.value = newUid;
}, 20);
};
const removeParameter = (uid: string) => {
parametersProxy.value = parametersProxy.value.filter(param => param._antares_id !== uid);
if (parametersProxy.value.length && selectedParam.value === uid)
resetSelectedID();
};
const clearChanges = () => {
parametersProxy.value = JSON.parse(JSON.stringify(props.localParameters));
i.value = parametersProxy.value.length + 1;
if (!parametersProxy.value.some(param => param.name === selectedParam.value))
resetSelectedID();
};
const resetSelectedID = () => {
selectedParam.value = parametersProxy.value.length ? parametersProxy.value[0]._antares_id : '';
};
onMounted(() => {
parametersProxy.value = JSON.parse(JSON.stringify(props.localParameters));
i.value = parametersProxy.value.length + 1;
if (parametersProxy.value.length)
resetSelectedID();
getModalInnerHeight();
window.addEventListener('resize', getModalInnerHeight);
});
onUnmounted(() => {
window.removeEventListener('resize', getModalInnerHeight);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -11,26 +11,26 @@
@click="saveChanges" @click="saveChanges"
> >
<i class="mdi mdi-24px mdi-content-save mr-1" /> <i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span> <span>{{ t('word.save') }}</span>
</button> </button>
<button <button
:disabled="!isChanged" :disabled="!isChanged"
class="btn btn-link btn-sm mr-0" class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')" :title="t('message.clearChanges')"
@click="clearChanges" @click="clearChanges"
> >
<i class="mdi mdi-24px mdi-delete-sweep mr-1" /> <i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span> <span>{{ t('word.clear') }}</span>
</button> </button>
<div class="divider-vert py-3" /> <div class="divider-vert py-3" />
<button class="btn btn-dark btn-sm" @click="showTimingModal"> <button class="btn btn-dark btn-sm" @click="showTimingModal">
<i class="mdi mdi-24px mdi-timer mr-1" /> <i class="mdi mdi-24px mdi-timer mr-1" />
<span>{{ $t('word.timing') }}</span> <span>{{ t('word.timing') }}</span>
</button> </button>
</div> </div>
<div class="workspace-query-info"> <div class="workspace-query-info">
<div class="d-flex" :title="$t('word.schema')"> <div class="d-flex" :title="t('word.schema')">
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b> <i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
</div> </div>
</div> </div>
@ -40,7 +40,7 @@
<div class="columns"> <div class="columns">
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label> <label class="form-label">{{ t('word.name') }}</label>
<input <input
v-model="localScheduler.name" v-model="localScheduler.name"
class="form-input" class="form-input"
@ -50,19 +50,19 @@
</div> </div>
<div class="column col-auto"> <div class="column col-auto">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label> <label class="form-label">{{ t('word.definer') }}</label>
<BaseSelect <BaseSelect
v-model="localScheduler.definer" v-model="localScheduler.definer"
:options="users" :options="users"
:option-label="(user) => user.value === '' ? $t('message.currentUser') : `${user.name}@${user.host}`" :option-label="(user: any) => user.value === '' ? t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``" :option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select" class="form-select"
/> />
</div> </div>
</div> </div>
<div class="column"> <div class="column">
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ $t('word.comment') }}</label> <label class="form-label">{{ t('word.comment') }}</label>
<input <input
v-model="localScheduler.comment" v-model="localScheduler.comment"
class="form-input" class="form-input"
@ -72,7 +72,7 @@
</div> </div>
<div class="column"> <div class="column">
<div class="form-group"> <div class="form-group">
<label class="form-label mr-2">{{ $t('word.state') }}</label> <label class="form-label mr-2">{{ t('word.state') }}</label>
<label class="form-radio form-inline"> <label class="form-radio form-inline">
<input <input
v-model="localScheduler.state" v-model="localScheduler.state"
@ -103,7 +103,7 @@
</div> </div>
<div class="workspace-query-results column col-12 mt-2 p-relative"> <div class="workspace-query-results column col-12 mt-2 p-relative">
<BaseLoader v-if="isLoading" /> <BaseLoader v-if="isLoading" />
<label class="form-label ml-2">{{ $t('message.schedulerBody') }}</label> <label class="form-label ml-2">{{ t('message.schedulerBody') }}</label>
<QueryEditor <QueryEditor
v-show="isSelected" v-show="isSelected"
ref="queryEditor" ref="queryEditor"
@ -122,245 +122,231 @@
/> />
</div> </div>
</template> </template>
<script setup lang="ts">
<script> import { AlterEventParams, EventInfos } from 'common/interfaces/antares';
import { storeToRefs } from 'pinia'; import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { useI18n } from 'vue-i18n';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader.vue';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor.vue';
import WorkspaceTabPropsSchedulerTimingModal from '@/components/WorkspaceTabPropsSchedulerTimingModal'; import WorkspaceTabPropsSchedulerTimingModal from '@/components/WorkspaceTabPropsSchedulerTimingModal.vue';
import Schedulers from '@/ipc-api/Schedulers';
import BaseSelect from '@/components/BaseSelect.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import Schedulers from '@/ipc-api/Schedulers';
export default { const { t } = useI18n();
name: 'WorkspaceTabPropsScheduler',
components: {
BaseLoader,
QueryEditor,
WorkspaceTabPropsSchedulerTimingModal,
BaseSelect
},
props: {
tabUid: String,
connection: Object,
scheduler: String,
isSelected: Boolean,
schema: String
},
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const props = defineProps({
tabUid: String,
connection: Object,
scheduler: String,
isSelected: Boolean,
schema: String
});
const { const { addNotification } = useNotificationsStore();
getWorkspace, const workspacesStore = useWorkspacesStore();
refreshStructure,
renameTabs,
newTab,
changeBreadcrumbs,
setUnsavedChanges
} = workspacesStore;
return { const {
addNotification, getWorkspace,
selectedWorkspace, refreshStructure,
getWorkspace, renameTabs,
refreshStructure, changeBreadcrumbs,
renameTabs, setUnsavedChanges
newTab, } = workspacesStore;
changeBreadcrumbs,
setUnsavedChanges
};
},
data () {
return {
isLoading: false,
isSaving: false,
isTimingModal: false,
originalScheduler: null,
localScheduler: { sql: '' },
lastScheduler: null,
sqlProxy: '',
editorHeight: 300
};
},
computed: {
workspace () {
return this.getWorkspace(this.connection.uid);
},
isChanged () {
return JSON.stringify(this.originalScheduler) !== JSON.stringify(this.localScheduler);
},
isDefinerInUsers () {
return this.originalScheduler ? this.workspace.users.some(user => this.originalScheduler.definer === `\`${user.name}\`@\`${user.host}\``) : true;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : []; const queryEditor: Ref<Component & {editor: Ace.Editor; $el: HTMLElement}> = ref(null);
}, const isLoading = ref(false);
users () { const isSaving = ref(false);
const users = [{ value: '' }, ...this.workspace.users]; const isTimingModal = ref(false);
if (!this.isDefinerInUsers) { const originalScheduler: Ref<EventInfos> = ref(null);
const [name, host] = this.originalScheduler.definer.replaceAll('`', '').split('@'); const localScheduler: Ref<EventInfos> = ref({} as EventInfos);
users.unshift({ name, host }); const lastScheduler = ref(null);
} const sqlProxy = ref('');
const editorHeight = ref(300);
return users; const workspace = computed(() => {
return getWorkspace(props.connection.uid);
});
const isChanged = computed(() => {
return JSON.stringify(originalScheduler.value) !== JSON.stringify(localScheduler.value);
});
const isDefinerInUsers = computed(() => {
return originalScheduler.value ? workspace.value.users.some(user => originalScheduler.value.definer === `\`${user.name}\`@\`${user.host}\``) : true;
});
const users = computed(() => {
const users = [{ value: '' }, ...workspace.value.users];
if (!isDefinerInUsers.value) {
const [name, host] = originalScheduler.value.definer.replaceAll('`', '').split('@');
users.unshift({ name, host });
}
return users;
});
const getSchedulerData = async () => {
if (!props.scheduler) return;
isLoading.value = true;
lastScheduler.value = props.scheduler;
const params = {
uid: props.connection.uid,
schema: props.schema,
scheduler: props.scheduler
};
try {
const { status, response } = await Schedulers.getSchedulerInformations(params);
if (status === 'success') {
originalScheduler.value = response;
localScheduler.value = JSON.parse(JSON.stringify(originalScheduler.value));
sqlProxy.value = localScheduler.value.sql;
} }
}, else
watch: { addNotification({ status: 'error', message: response });
async schema () { }
if (this.isSelected) { catch (err) {
await this.getSchedulerData(); addNotification({ status: 'error', message: err.stack });
this.$refs.queryEditor.editor.session.setValue(this.localScheduler.sql); }
this.lastScheduler = this.scheduler;
}
},
async scheduler () {
if (this.isSelected) {
await this.getSchedulerData();
this.$refs.queryEditor.editor.session.setValue(this.localScheduler.sql);
this.lastScheduler = this.scheduler;
}
},
async isSelected (val) {
if (val) {
this.changeBreadcrumbs({ schema: this.schema, scheduler: this.scheduler });
setTimeout(() => { resizeQueryEditor();
this.resizeQueryEditor(); isLoading.value = false;
}, 200); };
if (this.lastScheduler !== this.scheduler) const saveChanges = async () => {
this.getSchedulerData(); if (isSaving.value) return;
isSaving.value = true;
const params = {
uid: props.connection.uid,
scheduler: {
...localScheduler.value,
schema: props.schema,
oldName: originalScheduler.value.name
} as AlterEventParams
};
try {
const { status, response } = await Schedulers.alterScheduler(params);
if (status === 'success') {
const oldName = originalScheduler.value.name;
await refreshStructure(props.connection.uid);
if (oldName !== localScheduler.value.name) {
renameTabs({
uid: props.connection.uid,
schema: props.schema,
elementName: oldName,
elementNewName: localScheduler.value.name,
elementType: 'scheduler'
});
changeBreadcrumbs({ schema: props.schema, scheduler: localScheduler.value.name });
} }
}, else
isChanged (val) { getSchedulerData();
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
} }
}, else
async created () { addNotification({ status: 'error', message: response });
await this.getSchedulerData(); }
this.$refs.queryEditor.editor.session.setValue(this.localScheduler.sql); catch (err) {
window.addEventListener('keydown', this.onKey); addNotification({ status: 'error', message: err.stack });
}, }
mounted () {
window.addEventListener('resize', this.resizeQueryEditor);
},
unmounted () {
window.removeEventListener('resize', this.resizeQueryEditor);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
},
methods: {
async getSchedulerData () {
if (!this.scheduler) return;
this.isLoading = true; isSaving.value = false;
this.lastScheduler = this.scheduler; };
const params = { const clearChanges = () => {
uid: this.connection.uid, localScheduler.value = JSON.parse(JSON.stringify(originalScheduler.value));
schema: this.schema, queryEditor.value.editor.session.setValue(localScheduler.value.sql);
scheduler: this.scheduler };
};
try { const resizeQueryEditor = () => {
const { status, response } = await Schedulers.getSchedulerInformations(params); if (queryEditor.value) {
if (status === 'success') { const footer = document.getElementById('footer');
this.originalScheduler = response; const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
this.localScheduler = JSON.parse(JSON.stringify(this.originalScheduler)); editorHeight.value = size;
this.sqlProxy = this.localScheduler.sql; queryEditor.value.editor.resize();
} }
else };
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.resizeQueryEditor(); const showTimingModal = () => {
this.isLoading = false; isTimingModal.value = true;
}, };
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const params = {
uid: this.connection.uid,
scheduler: {
...this.localScheduler,
schema: this.schema,
oldName: this.originalScheduler.name
}
};
try { const hideTimingModal = () => {
const { status, response } = await Schedulers.alterScheduler(params); isTimingModal.value = false;
};
if (status === 'success') { const timingUpdate = (options: EventInfos) => {
const oldName = this.originalScheduler.name; localScheduler.value = options;
};
await this.refreshStructure(this.connection.uid); const onKey = (e: KeyboardEvent) => {
if (props.isSelected) {
if (oldName !== this.localScheduler.name) { e.stopPropagation();
this.renameTabs({ if (e.ctrlKey && e.key === 's') { // CTRL + S
uid: this.connection.uid, if (isChanged.value)
schema: this.schema, saveChanges();
elementName: oldName,
elementNewName: this.localScheduler.name,
elementType: 'scheduler'
});
this.changeBreadcrumbs({ schema: this.schema, scheduler: this.localScheduler.name });
}
else
this.getSchedulerData();
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localScheduler = JSON.parse(JSON.stringify(this.originalScheduler));
this.$refs.queryEditor.editor.session.setValue(this.localScheduler.sql);
},
resizeQueryEditor () {
if (this.$refs.queryEditor) {
const footer = document.getElementById('footer');
const size = window.innerHeight - this.$refs.queryEditor.$el.getBoundingClientRect().top - footer.offsetHeight;
this.editorHeight = size;
this.$refs.queryEditor.editor.resize();
}
},
showTimingModal () {
this.isTimingModal = true;
},
hideTimingModal () {
this.isTimingModal = false;
},
timingUpdate (options) {
this.localScheduler = options;
},
onKey (e) {
if (this.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.keyCode === 83) { // CTRL + S
if (this.isChanged)
this.saveChanges();
}
}
} }
} }
}; };
watch(() => props.schema, async () => {
if (props.isSelected) {
await getSchedulerData();
queryEditor.value.editor.session.setValue(localScheduler.value.sql);
lastScheduler.value = props.scheduler;
}
});
watch(() => props.scheduler, async () => {
if (props.isSelected) {
await getSchedulerData();
queryEditor.value.editor.session.setValue(localScheduler.value.sql);
lastScheduler.value = props.scheduler;
}
});
watch(() => props.isSelected, async (val) => {
if (val) {
changeBreadcrumbs({ schema: props.schema, scheduler: props.scheduler });
setTimeout(() => {
resizeQueryEditor();
}, 200);
if (lastScheduler.value !== props.scheduler)
getSchedulerData();
}
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
(async () => {
await getSchedulerData();
queryEditor.value.editor.session.setValue(localScheduler.value.sql);
window.addEventListener('keydown', onKey);
})();
onMounted(() => {
window.addEventListener('resize', resizeQueryEditor);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeQueryEditor);
});
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKey);
});
</script> </script>

Some files were not shown because too many files have changed in this diff Show More