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
assets
out
dist
dist
build

1
.gitignore vendored
View File

@ -7,5 +7,4 @@ node_modules
thumbs.db
NOTES.md
*.txt
package-lock.json
*.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",
"test:e2e": "npm run compile && npm run test:e2e-dry",
"test:e2e-dry": "xvfb-maybe -- playwright test",
"lint": "eslint . --ext .js,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"",
"lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix",
"lint": "eslint . --ext .js,.ts,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"",
"lint:fix": "eslint . --ext .js,.ts,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix",
"contributors:add": "all-contributors add",
"contributors:generate": "all-contributors generate"
},
@ -150,6 +150,8 @@
"@babel/preset-typescript": "~7.16.7",
"@playwright/test": "~1.21.1",
"@types/better-sqlite3": "~7.5.0",
"@types/leaflet": "~1.7.9",
"@types/marked": "~4.0.3",
"@types/node": "~17.0.23",
"@types/pg": "~8.6.5",
"@typescript-eslint/eslint-plugin": "~5.18.0",
@ -189,7 +191,7 @@
"unzip-crx-3": "~0.2.0",
"vue-eslint-parser": "~8.3.0",
"vue-loader": "~16.8.3",
"webpack": "~5.60.0",
"webpack": "~5.72.0",
"webpack-cli": "~4.9.1",
"webpack-dev-server": "~4.4.0",
"xvfb-maybe": "~0.2.1"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,12 @@ export type ClientCode = 'mysql' | 'maria' | 'pg' | 'sqlite'
export type Exporter = MysqlExporter | PostgreSQLExporter
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
*/
@ -67,12 +73,34 @@ export interface TypeInformations {
zerofill: boolean;
}
export interface TypesGroup {
group: string;
types: TypeInformations[];
}
// 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;
key: string;
type: string;
schema: string;
table?: string;
numPrecision?: number;
numLength?: number;
datePrecision?: number;
@ -82,7 +110,8 @@ export interface TableField {
unsigned?: boolean;
zerofill?: boolean;
order?: number;
default?: number | string;
default?: string;
defaultType?: string;
enumValues?: string;
charset?: string;
collation?: string;
@ -92,9 +121,16 @@ export interface TableField {
comment?: string;
after?: string;
orgName?: string;
length?: number | false;
alias: string;
tableAlias: string;
orgTable: string;
key?: 'pri' | 'uni';
}
export interface TableIndex {
// eslint-disable-next-line camelcase
_antares_id?: string;
name: string;
fields: string[];
type: string;
@ -107,6 +143,8 @@ export interface TableIndex {
}
export interface TableForeign {
// eslint-disable-next-line camelcase
_antares_id?: string;
constraintName: string;
refSchema: string;
table: string;
@ -118,15 +156,6 @@ export interface TableForeign {
oldName?: string;
}
export interface TableOptions {
name: string;
type?: 'table' | 'view';
engine?: string;
comment?: string;
collation?: string;
autoIncrement?: number;
}
export interface CreateTableParams {
/** Connection UID */
uid?: string;
@ -165,6 +194,7 @@ export interface AlterTableParams {
}
// Views
export type ViewInfos = TableInfos
export interface CreateViewParams {
schema: string;
name: string;
@ -180,6 +210,19 @@ export interface AlterViewParams extends CreateViewParams {
}
// 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 {
definer?: string;
schema: string;
@ -195,13 +238,38 @@ export interface AlterTriggerParams extends CreateTriggerParams {
}
// Routines & Functions
export interface FunctionParam {
// eslint-disable-next-line camelcase
_antares_id: string;
context: string;
name: string;
type: string;
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 {
name: string;
parameters?: FunctionParam[];
@ -239,7 +307,7 @@ export interface AlterFunctionParams extends CreateFunctionParams {
}
// Events
export interface CreateEventParams {
export interface EventInfos {
definer?: string;
schema: string;
name: string;
@ -248,16 +316,39 @@ export interface CreateEventParams {
starts: string;
ends: string;
at: string;
preserve: string;
preserve: boolean;
state: string;
comment: string;
enabled?: boolean;
sql: string;
}
export type CreateEventParams = EventInfos;
export interface AlterEventParams extends CreateEventParams {
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
export interface QueryBuilderObject {
schema: string;
@ -285,17 +376,10 @@ export interface QueryParams {
tabUid?: string;
}
export interface QueryField {
name: string;
alias: string;
orgName: string;
schema: string;
table: string;
tableAlias: string;
orgTable: string;
type: string;
length: number;
}
/**
* @deprecated Use TableFIeld
*/
export type QueryField = TableField
export interface QueryForeign {
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';
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 {
uid: string;
schema: string;

View File

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

View File

@ -1,5 +1,4 @@
'use strict';
export function formatBytes (bytes, decimals = 2) {
export function formatBytes (bytes: number, decimals = 2) {
if (bytes === 0) return '0 Bytes';
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 = {
0: '0000',
1: '0001',
@ -23,15 +21,11 @@ const lookup = {
D: '1101',
E: '1110',
F: '1111'
};
} as const;
/**
* Converts hexadecimal string to binary string
*
* @param {string} hex Hexadecimal string
* @returns {string} Binary string
*/
export default function hexToBinary (hex) {
export type HexChar = keyof typeof lookup
export default function hexToBinary (hex: HexChar[]) {
let binary = '';
for (let i = 0; i < hex.length; i++)
binary += lookup[hex[i]];

View File

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

View File

@ -3,13 +3,7 @@
const pattern = /[\0\x08\x09\x1a\n\r"'\\\%]/gm;
const regex = new RegExp(pattern);
/**
* Escapes a string
*
* @param {String} string
* @returns {String}
*/
function sqlEscaper (string) {
function sqlEscaper (string: string) {
return string.replace(regex, char => {
const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\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();
});
ipcMain.on('get-key', async event => {
const key = false;
event.returnValue = key;
});
ipcMain.handle('show-open-dialog', (event, 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 }) => {
if (exporter !== null) return;
if (exporter !== null) {
exporter.kill();
return;
}
return new Promise((resolve/*, reject */) => {
(async () => {
@ -265,7 +268,10 @@ export default (connections: {[key: string]: antares.Client}) => {
});
ipcMain.handle('import-sql', async (event, options) => {
if (importer !== null) return;
if (importer !== null) {
importer.kill();
return;
}
return new Promise((resolve/*, reject */) => {
(async () => {

View File

@ -1,3 +1,4 @@
import * as fs from 'fs';
import * as antares from 'common/interfaces/antares';
import { InsertRowsParams } from 'common/interfaces/tableApis';
import { ipcMain } from 'electron';
@ -5,8 +6,7 @@ import { faker } from '@faker-js/faker';
import * as moment from 'moment';
import { sqlEscaper } from 'common/libs/sqlEscaper';
import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes';
import * as customizations from 'common/customizations';
import fs from 'fs';
import customizations from 'common/customizations';
export default (connections: {[key: string]: antares.Client}) => {
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) => {
try { // TODO: move to client classes
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rows: {[key: string]: any}[] = [];
const rows: {[key: string]: string | number | boolean | Date | Buffer}[] = [];
for (let i = 0; i < +params.repeat; i++) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const insertObj: {[key: string]: any} = {};
const insertObj: {[key: string]: string | number | boolean | Date | Buffer} = {};
for (const key in params.row) {
const type = params.fields[key];
@ -382,8 +310,7 @@ export default (connections: {[key: string]: antares.Client}) => {
insertObj[key] = escapedParam;
}
else { // Faker value
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parsedParams: {[key: string]: any} = {};
const parsedParams: {[key: string]: string | number | boolean | Date | Buffer} = {};
let fakeValue;
if (params.locale)
@ -403,7 +330,7 @@ export default (connections: {[key: string]: antares.Client}) => {
if (typeof fakeValue === 'string') {
if (params.row[key].length)
fakeValue = fakeValue.substr(0, params.row[key].length);
fakeValue = fakeValue.substring(0, params.row[key].length);
fakeValue = `'${sqlEscaper(fakeValue)}'`;
}
else if ([...DATE, ...DATETIME].includes(type))

View File

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

View File

@ -1,7 +1,7 @@
import * as antares from 'common/interfaces/antares';
import * as mysql from 'mysql2/promise';
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 SSHConfig from 'ssh2-promise/lib/sshConfig';
@ -321,7 +321,7 @@ export class MySQLClient extends AntaresCore {
return filteredDatabases.map(db => {
if (schemas.has(db.Database)) {
// 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;
switch (table.Comment) {
case 'VIEW':
@ -350,7 +350,7 @@ export class MySQLClient extends AntaresCore {
});
// 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 {
name: procedure.Name,
type: procedure.Type,
@ -364,7 +364,7 @@ export class MySQLClient extends AntaresCore {
});
// 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 {
name: func.Name,
type: func.Type,
@ -378,33 +378,26 @@ export class MySQLClient extends AntaresCore {
});
// 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 {
name: scheduler.EVENT_NAME,
definition: scheduler.EVENT_DEFINITION,
type: scheduler.EVENT_TYPE,
schema: scheduler.Db,
sql: scheduler.EVENT_DEFINITION,
execution: scheduler.EVENT_TYPE === 'RECURRING' ? 'EVERY' : 'ONCE',
definer: scheduler.DEFINER,
body: scheduler.EVENT_BODY,
starts: scheduler.STARTS,
ends: scheduler.ENDS,
state: scheduler.STATUS === 'ENABLED' ? 'ENABLE' : scheduler.STATE === 'DISABLED' ? 'DISABLE' : 'DISABLE ON SLAVE',
enabled: scheduler.STATUS === 'ENABLED',
executeAt: scheduler.EXECUTE_AT,
intervalField: scheduler.INTERVAL_FIELD,
intervalValue: scheduler.INTERVAL_VALUE,
onCompletion: scheduler.ON_COMPLETION,
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
at: scheduler.EXECUTE_AT,
every: [scheduler.INTERVAL_FIELD, scheduler.INTERVAL_VALUE],
preserve: scheduler.ON_COMPLETION.includes('PRESERVE'),
comment: scheduler.EVENT_COMMENT
};
});
// 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 {
name: trigger.Trigger,
statement: trigger.Statement,
@ -930,19 +923,22 @@ export class MySQLClient extends AntaresCore {
}
async getViewInformations ({ schema, view }: { schema: string; view: string }) {
const sql = `SHOW CREATE VIEW \`${schema}\`.\`${view}\``;
const results = await this.raw(sql);
const { rows: algorithm } = await this.raw(`SHOW CREATE VIEW \`${schema}\`.\`${view}\``);
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 {
algorithm: row['Create View'].match(/(?<=CREATE ALGORITHM=).*?(?=\s)/gs)[0],
definer: row['Create View'].match(/(?<=DEFINER=).*?(?=\s)/gs)[0],
security: row['Create View'].match(/(?<=SQL SECURITY ).*?(?=\s)/gs)[0],
updateOption: row['Create View'].match(/(?<=WITH ).*?(?=\s)/gs) ? row['Create View'].match(/(?<=WITH ).*?(?=\s)/gs)[0] : '',
sql: row['Create View'].match(/(?<=AS ).*?$/gs)[0],
name: row.View
};
})[0];
return {
algorithm: algorithm[0]['Create View'].match(/(?<=CREATE ALGORITHM=).*?(?=\s)/gs)[0],
definer: viewInfo[0].DEFINER,
security: viewInfo[0].SECURITY_TYPE,
updateOption: viewInfo[0].CHECK_OPTION === 'NONE' ? '' : viewInfo[0].CHECK_OPTION,
sql: viewInfo[0].VIEW_DEFINITION,
name: viewInfo[0].TABLE_NAME
};
}
async dropView (params: { schema: string; view: string }) {
@ -955,7 +951,7 @@ export class MySQLClient extends AntaresCore {
USE \`${view.schema}\`;
ALTER ALGORITHM = ${view.algorithm}${view.definer ? ` DEFINER=${view.definer}` : ''}
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)
@ -1381,6 +1377,14 @@ export class MySQLClient extends AntaresCore {
xa: row.XA,
savepoints: row.Savepoints,
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;
}
return acc;
}, {});
}, {}) as {
number: string;
name: string;
arch: string;
os: string;
};
}
async getProcesses () {
@ -1423,6 +1432,15 @@ export class MySQLClient extends AntaresCore {
time: row.TIME,
state: row.STATE,
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 pgAst from 'pgsql-ast-parser';
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 SSHConfig from 'ssh2-promise/lib/sshConfig';

View File

@ -1,7 +1,7 @@
import * as antares from 'common/interfaces/antares';
import * as sqlite from 'better-sqlite3';
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';
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 { SqlExporter } from './SqlExporter';
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 * as moment from 'moment';
import { lineString, point, polygon } from '@turf/helpers';
@ -138,7 +138,7 @@ ${footer}
: this.escapeAndQuote(val);
}
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))
sqlInsertString += `X'${val.toString('hex').toUpperCase()}'`;
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 { SqlExporter } from './SqlExporter';
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';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
@ -249,7 +249,7 @@ SET row_security = off;\n\n\n`;
else if (TEXT_SEARCH.includes(column.type))
sqlInsertString += `'${val.replaceAll('\'', '\'\'')}'`;
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))
sqlInsertString += `decode('${val.toString('hex').toUpperCase()}', 'hex')`;
else if (NUMBER.includes(column.type))

View File

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

View File

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

View File

@ -46,65 +46,58 @@
</div>
</template>
<script>
export default {
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();
},
<script setup lang="ts">
import { computed, onBeforeUnmount, PropType, useSlots } from 'vue';
hideModal () {
this.$emit('hide');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.hideModal();
}
}
const props = defineProps({
size: {
type: String as PropType<'small' | 'medium' | '400' | 'large'>,
validator: (prop: string) => ['small', 'medium', '400', 'large'].includes(prop),
default: 'small'
},
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>
<style scoped>

View File

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

View File

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

View File

@ -1,95 +1,93 @@
<template>
<div id="map" class="map" />
</template>
<script>
import L from 'leaflet';
<script setup lang="ts">
import { onMounted, PropType, Ref, ref } from 'vue';
import * as L from 'leaflet';
import {
point,
lineString,
polygon
} from '@turf/helpers';
import { GeoJsonObject } from 'geojson';
import { getArrayDepth } from 'common/libs/getArrayDepth';
export default {
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);
interface Coordinates { x: number; y: number }
if (!Array.isArray(this.points))
this.center = [this.points.y, this.points.x];
}
const props = defineProps({
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', {
center: this.center || [0, 0],
zoom: 15,
minZoom: 1,
attributionControl: false
});
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]);
}
const getMarkers = (points: Coordinates) => {
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: Coordinates[], curr: Coordinates) => [...acc, [curr.x, curr.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>
<style lang="scss">

View File

@ -14,64 +14,58 @@
</div>
</template>
<script>
export default {
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;
}
<script setup lang="ts">
import { computed, ref } from 'vue';
return { className, iconName };
},
isExpandable () {
return this.message.length > 80;
}
const props = defineProps({
message: {
type: String,
default: ''
},
methods: {
hideToast () {
this.$emit('close');
},
toggleExpand () {
this.isExpanded = !this.isExpanded;
}
status: {
type: String,
default: ''
}
});
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>
<style scoped>
.toast {
display: flex;

View File

@ -143,7 +143,7 @@ export default defineComponent({
const hightlightedIndex = ref(0);
const isOpen = 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 searchInput = ref(null);
const optionList = ref(null);
@ -403,7 +403,8 @@ export default defineComponent({
optionList,
optionRefs,
handleBlurEvent,
handleMouseUpEvent
handleMouseUpEvent,
internalValue
};
}
});

View File

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

View File

@ -9,67 +9,63 @@
</div>
</template>
<script>
export default {
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;
}
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
return { className, iconName };
}
const props = defineProps({
message: {
type: String,
default: ''
},
watch: {
message: function () {
if (this.message)
this.isVisible = true;
else
this.isVisible = false;
}
},
methods: {
hideToast () {
this.isVisible = false;
this.$emit('close');
}
status: {
type: String,
default: ''
}
});
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>
<style scoped>
.toast {
display: flex;

View File

@ -7,7 +7,7 @@
{{ lastPart(modelValue) }}
</span>
<i
v-if="modelValue.length"
v-if="modelValue"
class="file-uploader-reset mdi mdi-close"
@click.prevent="clear"
/>
@ -22,40 +22,35 @@
</label>
</template>
<script>
<script setup lang="ts">
import { uidGen } from 'common/libs/uidGen';
export default {
name: 'BaseUploadInput',
props: {
message: {
default: 'Browse',
type: String
},
modelValue: {
default: '',
type: String
}
defineProps({
message: {
default: 'Browse',
type: String
},
emits: ['change', 'clear'],
data () {
return {
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;
}
modelValue: {
default: '',
type: 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>

View File

@ -1,5 +1,5 @@
<template>
<div class="vscroll-holder">
<div ref="root" class="vscroll-holder">
<div
class="vscroll-spacer"
:style="{
@ -20,71 +20,76 @@
</div>
</template>
<script>
export default {
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);
<script setup lang="ts">
import { onBeforeUnmount, onMounted, Ref, ref, watch } from 'vue';
this.renderTimeout = setTimeout(() => {
this.updateWindow(e);
}, 200);
},
updateWindow () {
const visibleItemsCount = Math.ceil(this.visibleHeight / this.itemHeight);
const totalScrollHeight = this.items.length * this.itemHeight;
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 props = defineProps({
items: Array,
itemHeight: Number,
visibleHeight: Number,
scrollElement: {
type: HTMLDivElement,
default: null
}
});
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>

View File

@ -4,7 +4,7 @@
v-model="selectedGroup"
class="form-select"
: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"
:disabled="!isChecked"
style="flex-grow: 0;"
@ -15,7 +15,7 @@
v-if="selectedGroup !== 'manual'"
v-model="selectedMethod"
:options="fakerMethods"
:option-label="(opt) => $t(`faker.${opt.name}`)"
:option-label="(opt: any) => $t(`faker.${opt.name}`)"
option-track-by="name"
class="form-select"
:disabled="!isChecked"
@ -85,153 +85,149 @@
</fieldset>
</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 BaseUploadInput from '@/components/BaseUploadInput';
import ForeignKeySelect from '@/components/ForeignKeySelect';
import BaseUploadInput from '@/components/BaseUploadInput.vue';
import ForeignKeySelect from '@/components/ForeignKeySelect.vue';
import FakerMethods from 'common/FakerMethods';
import BaseSelect from '@/components/BaseSelect.vue';
export default {
name: 'FakerSelect',
components: {
ForeignKeySelect,
BaseUploadInput,
BaseSelect
},
props: {
type: String,
field: Object,
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';
const props = defineProps({
type: String,
field: Object,
isChecked: Boolean,
foreignKeys: Array,
keyUsage: Array as PropType<{field: string}[]>,
fieldLength: Number,
fieldObj: Object
});
const emit = defineEmits(['update:modelValue']);
return FakerMethods.getGroupsByType(this.localType);
},
fakerMethods () {
return FakerMethods.getMethods({ type: this.localType, group: this.selectedGroup });
},
methodData () {
return this.fakerMethods.find(method => method.name === this.selectedMethod);
}
},
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 };
const localType: Ref<string> = ref(null);
const selectedGroup: Ref<string> = ref('manual');
const selectedMethod: Ref<string> = ref('');
const selectedValue: Ref<string> = ref('');
const debounceTimeout: Ref<NodeJS.Timeout> = ref(null);
const methodParams: Ref<{[key: string]: string}> = ref({});
const enumArray: Ref<string[]> = ref(null);
if ([...NUMBER, ...FLOAT].includes(this.type))
return { type: 'number', mask: false };
const fakerGroups = computed(() => {
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)) {
let timeMask = '##:##:##';
const precision = this.fieldLength;
return FakerMethods.getGroupsByType(localType.value);
});
for (let i = 0; i < precision; i++)
timeMask += i === 0 ? '.#' : '#';
const fakerMethods = computed(() => {
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))
return { type: 'text', mask: '####-##-##' };
const inputProps = () => {
if ([...TEXT, ...LONG_TEXT].includes(props.type))
return { type: 'text', mask: false };
if (DATETIME.includes(this.type)) {
let datetimeMask = '####-##-## ##:##:##';
const precision = this.fieldLength;
if ([...NUMBER, ...FLOAT].includes(props.type))
return { type: 'number', mask: false };
for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#';
if (TIME.includes(props.type)) {
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: '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
});
}
return { type: 'text', mask: timeMask };
}
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>

View File

@ -8,107 +8,100 @@
dropdown-class="select-sm"
dropdown-container=".workspace-query-results > .vscroll"
@change="onChange"
@blur="$emit('blur')"
@blur="emit('blur')"
/>
</template>
<script>
<script setup lang="ts">
import { computed, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia';
import Tables from '@/ipc-api/Tables';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import { TEXT, LONG_TEXT } from 'common/fieldTypes';
import BaseSelect from '@/components/BaseSelect.vue';
import { TableField } from 'common/interfaces/antares';
export default {
name: 'ForeignKeySelect',
components: { BaseSelect },
props: {
modelValue: [String, Number],
keyUsage: Object,
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 props = defineProps({
modelValue: [String, Number],
keyUsage: Object,
size: {
type: String,
default: ''
}
});
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>

View File

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

View File

@ -47,83 +47,75 @@
</ConfirmModal>
</template>
<script>
<script setup lang="ts">
import { computed, PropType, Ref, ref } from 'vue';
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 {
name: 'ModalAskParameters',
components: {
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);
const props = defineProps({
localRoutine: Object as PropType<RoutineInfos | FunctionInfos>,
client: String
});
setTimeout(() => {
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 emit = defineEmits(['confirm', 'close']);
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}`;
acc.push(value);
return acc;
}, []);
this.$emit('confirm', valArr);
},
closeModal () {
this.$emit('close');
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
}
const inParameters = computed(() => {
return props.localRoutine.parameters.filter(param => param.context === 'IN');
});
const typeClass = (type: string) => {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
};
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>
<style scoped>

View File

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

View File

@ -62,116 +62,99 @@
</Teleport>
</template>
<script>
<script setup lang="ts">
import { computed, onBeforeUnmount, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import Schema from '@/ipc-api/Schema';
import BaseSelect from '@/components/BaseSelect.vue';
export default {
name: 'ModalEditSchema',
components: {
BaseSelect
},
props: {
selectedSchema: String
},
emits: ['close'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const props = defineProps({
selectedSchema: String
});
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const emit = defineEmits(['close']);
const { getWorkspace, getDatabaseVariable } = workspacesStore;
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
return {
addNotification,
selectedWorkspace,
getWorkspace,
getDatabaseVariable
};
},
data () {
return {
database: {
name: '',
prevName: '',
collation: ''
}
};
},
computed: {
collations () {
return this.getWorkspace(this.selectedWorkspace).collations;
},
defaultCollation () {
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
}
},
async created () {
let actualCollation;
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspace, getDatabaseVariable } = workspacesStore;
const firstInput: Ref<HTMLInputElement> = ref(null);
const database = ref({
name: '',
prevName: '',
collation: '',
prevCollation: null
});
const collations = computed(() => getWorkspace(selectedWorkspace.value).collations);
const defaultCollation = computed(() => (getDatabaseVariable(selectedWorkspace.value, 'collation_server').value || ''));
const updateSchema = async () => {
if (database.value.collation !== database.value.prevCollation) {
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')
actualCollation = response;
closeModal();
else
this.addNotification({ status: 'error', message: response });
addNotification({ status: 'error', message: response });
}
catch (err) {
this.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();
addNotification({ status: 'error', message: err.stack });
}
}
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>
<style scoped>

View File

@ -146,7 +146,7 @@
<div class="tbody">
<div
v-for="item in tables"
:key="item.name"
:key="item.table"
class="tr"
>
<div class="td">
@ -193,7 +193,7 @@
>
<input v-model="options.includes[key]" type="checkbox"><i class="form-icon" /> {{ $tc(`word.${key}`, 2) }}
</label>
<div v-if="customizations.exportByChunks">
<div v-if="clientCustoms.exportByChunks">
<div class="h6 mt-4 mb-2">
{{ $t('message.newInserStmtEvery') }}:
</div>
@ -263,211 +263,204 @@
</Teleport>
</template>
<script>
import moment from 'moment';
<script setup lang="ts">
import { computed, onBeforeUnmount, Ref, ref } from 'vue';
import * as moment from 'moment';
import { ipcRenderer } from 'electron';
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 { useWorkspacesStore } from '@/stores/workspaces';
import customizations from 'common/customizations';
import Application from '@/ipc-api/Application';
import Schema from '@/ipc-api/Schema';
import { Customizations } from 'common/interfaces/customizations';
import BaseSelect from '@/components/BaseSelect.vue';
export default {
name: 'ModalExportSchema',
components: {
BaseSelect
},
props: {
selectedSchema: String
},
emits: ['close'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const props = defineProps({
selectedSchema: String
});
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const emit = defineEmits(['close']);
const { t } = useI18n();
const {
getWorkspace,
getDatabaseVariable,
refreshSchema
} = workspacesStore;
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
return {
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');
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
return [];
},
filename () {
const date = moment().format('YYYY-MM-DD');
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();
const {
getWorkspace,
refreshSchema
} = workspacesStore;
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();
this.tables = this.schemaItems.map(item => ({
table: item.name,
includeStructure: true,
includeContent: true,
includeDropStatement: true
}));
const currentWorkspace = computed(() => getWorkspace(selectedWorkspace.value));
const clientCustoms: Ref<Customizations> = computed(() => currentWorkspace.value.customizations);
const schemaItems = computed(() => {
const db: SchemaInfos = currentWorkspace.value.structure.find((db: SchemaInfos) => db.name === props.selectedSchema);
if (db)
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 val = customizations[this.currentWorkspace.client][feat];
if (val)
this.options.includes[feat] = true;
});
const startExport = async () => {
isExporting.value = true;
const { uid, client } = currentWorkspace.value;
const params = {
uid,
type: client,
schema: props.selectedSchema,
outputFile: dumpFilePath.value,
tables: [...tables.value],
...options.value
};
ipcRenderer.on('export-progress', this.updateProgress);
},
beforeUnmount () {
window.removeEventListener('keydown', this.onKey);
ipcRenderer.off('export-progress', this.updateProgress);
},
methods: {
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];
try {
const { status, response } = await Schema.export(params);
if (status === 'success')
progressStatus.value = response.cancelled ? t('word.aborted') : t('word.completed');
else {
progressStatus.value = response;
addNotification({ status: 'error', message: response });
}
}
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>
<style lang="scss" scoped>
@ -476,14 +469,15 @@ export default {
overflow: hidden;
.left {
display: flex;
flex-direction: column;
flex: 1;
display: flex;
flex-direction: column;
flex: 1;
}
}
.workspace-query-results {
flex: 1 0 1px;
flex: 1 0 1px;
.table {
width: 100% !important;
}
@ -499,25 +493,24 @@ export default {
}
.modal {
.modal-container {
max-width: 800px;
}
.modal-body {
max-height: 60vh;
display: flex;
flex-direction: column;
}
.modal-body {
max-height: 60vh;
display: flex;
flex-direction: column;
}
.modal-footer {
display: flex;
}
.modal-footer {
display: flex;
}
}
.progress-status {
font-style: italic;
font-size: 80%;
font-style: italic;
font-size: 80%;
}
</style>

View File

@ -97,251 +97,228 @@
</Teleport>
</template>
<script>
import moment from 'moment';
<script setup lang="ts">
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 { 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 FakerSelect from '@/components/FakerSelect';
import FakerSelect from '@/components/FakerSelect.vue';
import BaseSelect from '@/components/BaseSelect.vue';
export default {
name: 'ModalFakerRows',
components: {
FakerSelect,
BaseSelect
},
props: {
tabUid: [String, Number],
fields: Array,
keyUsage: Array
},
emits: ['reload', 'hide'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const props = defineProps({
tabUid: [String, Number],
fields: Array as Prop<TableField[]>,
keyUsage: Array as Prop<TableForeign[]>
});
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
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 emit = defineEmits(['reload', 'hide']);
];
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
return {
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 = {};
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
for (const field of this.fields) {
let fieldDefault;
const { getWorkspace } = workspacesStore;
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;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const localRow: Ref<{[key: string]: any}> = ref({});
const fieldsToExclude = ref([]);
const nInserts = ref(1);
const isInserting = ref(false);
const fakerLocale = ref('en');
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
this.fieldsToExclude = [...this.fieldsToExclude, field.name];
}
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' }
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 => {
if (this.fieldsToExclude.includes(key))
delete rowToInsert[key];
watch(nInserts, (val) => {
if (!val || val < 1)
nInserts.value = 1;
else if (val > 1000)
nInserts.value = 1000;
});
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.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 typeClass = (type: string) => {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
};
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>
<style scoped>

View File

@ -7,7 +7,7 @@
<div class="modal-title h6">
<div class="d-flex">
<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>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
@ -22,7 +22,7 @@
v-model="searchTerm"
class="form-input"
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
@ -67,13 +67,13 @@
<small class="tile-subtitle">{{ query.schema }} · {{ formatDate(query.date) }}</small>
<div class="tile-history-buttons">
<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 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 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>
</div>
</div>
@ -88,7 +88,7 @@
<i class="mdi mdi-history mdi-48px" />
</div>
<p class="empty-title h5">
{{ $t('message.thereIsNoQueriesYet') }}
{{ t('message.thereIsNoQueriesYet') }}
</p>
</div>
</div>
@ -97,129 +97,111 @@
</Teleport>
</template>
<script>
import moment from 'moment';
import { useHistoryStore } from '@/stores/history';
<script setup lang="ts">
import { Component, computed, ComputedRef, onBeforeUnmount, onMounted, onUpdated, Prop, Ref, ref, watch } from 'vue';
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 { useNotificationsStore } from '@/stores/notifications';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
export default {
name: 'ModalHistory',
components: {
BaseVirtualScroll
},
props: {
connection: Object
},
emits: ['select-query', 'close'],
setup () {
const { getHistoryByWorkspace, deleteQueryFromHistory } = useHistoryStore();
const { getConnectionName } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const { t } = useI18n();
return {
getHistoryByWorkspace,
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);
const { getHistoryByWorkspace, deleteQueryFromHistory } = useHistoryStore();
const { getConnectionName } = useConnectionsStore();
this.searchTermInterval = setTimeout(() => {
this.localSearchTerm = this.searchTerm;
}, 200);
}
},
created () {
window.addEventListener('keydown', this.onKey, { capture: true });
},
updated () {
if (this.$refs.table)
this.refreshScroller();
const props = defineProps({
connection: Object as Prop<ConnectionParams>
});
if (this.$refs.tableWrapper)
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;
const emit = defineEmits(['select-query', 'close']);
if (el)
this.resultsSize = el.offsetHeight - this.$refs.searchForm.offsetHeight;
const table: Ref<HTMLDivElement> = ref(null);
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();
}
},
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;');
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));
if (this.searchTerm) {
const regexp = new RegExp(`(${this.searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return string.replace(regexp, '<span class="text-primary text-bold">$1</span>');
}
else
return string;
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
}
watch(searchTerm, () => {
clearTimeout(searchTermInterval.value);
searchTermInterval.value = setTimeout(() => {
localSearchTerm.value = searchTerm.value;
}, 200);
});
const copyQuery = (sql: string) => {
navigator.clipboard.writeText(sql);
};
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>
<style lang="scss" scoped>

View File

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

View File

@ -65,93 +65,75 @@
</Teleport>
</template>
<script>
<script setup lang="ts">
import { computed, onBeforeUnmount, Ref, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import Schema from '@/ipc-api/Schema';
import { storeToRefs } from 'pinia';
import BaseSelect from '@/components/BaseSelect.vue';
export default {
name: 'ModalNewSchema',
components: { BaseSelect },
emits: ['reload', 'close'],
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
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 {
addNotification,
selectedWorkspace,
getWorkspace,
getDatabaseVariable
};
},
data () {
return {
isLoading: false,
database: {
name: '',
collation: ''
}
};
},
computed: {
collations () {
return this.getWorkspace(this.selectedWorkspace).collations;
},
customizations () {
return this.getWorkspace(this.selectedWorkspace).customizations;
},
defaultCollation () {
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server') ? this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value : '';
}
},
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();
const emit = defineEmits(['reload', 'close']);
const firstInput: Ref<HTMLInputElement> = ref(null);
const isLoading = ref(false);
const database = ref({
name: '',
collation: ''
});
const collations = computed(() => getWorkspace(selectedWorkspace.value).collations);
const customizations = computed(() => getWorkspace(selectedWorkspace.value).customizations);
const defaultCollation = computed(() => getDatabaseVariable(selectedWorkspace.value, 'collation_server') ? getDatabaseVariable(selectedWorkspace.value, 'collation_server').value : '');
database.value = { ...database.value, collation: defaultCollation.value };
const createSchema = async () => {
isLoading.value = true;
try {
const { status, response } = await Schema.createSchema({
uid: selectedWorkspace.value,
...database.value
});
if (status === 'success') {
closeModal();
emit('reload');
}
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>
<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>
</template>
<script>
import arrayToFile from '../libs/arrayToFile';
<script setup lang="ts">
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 Schema from '@/ipc-api/Schema';
import { useConnectionsStore } from '@/stores/connections';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import ModalProcessesListRow from '@/components/ModalProcessesListRow';
import ModalProcessesListContext from '@/components/ModalProcessesListContext';
import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
import ModalProcessesListRow from '@/components/ModalProcessesListRow.vue';
import ModalProcessesListContext from '@/components/ModalProcessesListContext.vue';
export default {
name: 'ModalProcessesList',
components: {
BaseVirtualScroll,
ModalProcessesListRow,
ModalProcessesListContext
},
props: {
connection: Object
},
emits: ['close'],
setup () {
const { addNotification } = useNotificationsStore();
const { getConnectionName } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const { getConnectionName } = useConnectionsStore();
return { addNotification, getConnectionName };
},
data () {
return {
resultsSize: 1000,
isQuering: false,
isContext: false,
autorefreshTimer: 0,
refreshInterval: null,
contextEvent: null,
selectedCell: null,
selectedRow: null,
results: [],
fields: [],
currentSort: '',
currentSortDir: 'asc',
scrollElement: null
};
},
computed: {
connectionName () {
return this.getConnectionName(this.connection.uid);
},
sortedResults () {
if (this.currentSort) {
return [...this.results].sort((a, b) => {
let modifier = 1;
const valA = typeof a[this.currentSort] === 'string' ? a[this.currentSort].toLowerCase() : a[this.currentSort];
const valB = typeof b[this.currentSort] === 'string' ? b[this.currentSort].toLowerCase() : b[this.currentSort];
if (this.currentSortDir === 'desc') modifier = -1;
if (valA < valB) return -1 * modifier;
if (valA > valB) return 1 * modifier;
return 0;
});
}
else
return this.results;
const props = defineProps({
connection: Object as Prop<ConnectionParams>
});
const emit = defineEmits(['close']);
const tableWrapper: Ref<HTMLDivElement> = ref(null);
const table: Ref<HTMLDivElement> = ref(null);
const resultTable: Ref<Component & {updateWindow: () => void}> = ref(null);
const resultsSize = ref(1000);
const isQuering = ref(false);
const isContext = ref(false);
const autorefreshTimer = ref(0);
const refreshInterval: Ref<NodeJS.Timeout> = ref(null);
const contextEvent = ref(null);
const selectedCell = ref(null);
const selectedRow: Ref<number> = ref(null);
const results = ref([]);
const fields = ref([]);
const currentSort = ref('');
const currentSortDir = ref('asc');
const scrollElement = ref(null);
const connectionName = computed(() => getConnectionName(props.connection.uid));
const sortedResults = computed(() => {
if (currentSort.value) {
return [...results.value].sort((a, b) => {
let modifier = 1;
const valA = typeof a[currentSort.value] === 'string' ? a[currentSort.value].toLowerCase() : a[currentSort.value];
const valB = typeof b[currentSort.value] === 'string' ? b[currentSort.value].toLowerCase() : b[currentSort.value];
if (currentSortDir.value === 'desc') modifier = -1;
if (valA < valB) return -1 * modifier;
if (valA > valB) return 1 * modifier;
return 0;
});
}
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]) : [];
}
},
created () {
window.addEventListener('keydown', this.onKey, { capture: true });
},
updated () {
if (this.$refs.table)
this.refreshScroller();
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
if (this.$refs.tableWrapper)
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;
isQuering.value = false;
};
// if table changes clear cached values
if (this.lastTable !== this.table)
this.results = [];
const setRefreshInterval = () => {
clearRefresh();
try { // Table data
const { status, response } = await Schema.getProcesses(this.connection.uid);
if (status === 'success') {
this.results = response;
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();
}
if (+autorefreshTimer.value) {
refreshInterval.value = setInterval(() => {
if (!isQuering.value)
getProcessesList();
}, autorefreshTimer.value * 1000);
}
};
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>
<style lang="scss" scoped>

View File

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

View File

@ -31,11 +31,12 @@
<div>
<div>
<TextEditor
:value="row.info || ''"
:model-value="props.row.info || ''"
editor-class="textarea-editor"
:mode="editorMode"
:read-only="true"
/>
<div class="mb-4" />
</div>
</div>
</template>
@ -43,60 +44,46 @@
</div>
</template>
<script>
import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor';
<script setup lang="ts">
import { Ref, ref } from 'vue';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import TextEditor from '@/components/BaseTextEditor.vue';
export default {
name: 'ModalProcessesListRow',
components: {
ConfirmModal,
TextEditor
},
props: {
row: Object
},
emits: ['select-row', 'contextmenu', 'stop-refresh'],
data () {
return {
isInlineEditor: {},
isInfoModal: false,
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 props = defineProps({
row: Object
});
const emit = defineEmits(['select-row', 'contextmenu', 'stop-refresh']);
const isInlineEditor: Ref<{[key: string]: boolean}> = ref({});
const isInfoModal = ref(false);
const editorMode = ref('sql');
const isNull = (value: string | number) => value === null ? ' is-null' : '';
const selectRow = () => {
emit('select-row');
};
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>
<style lang="scss">

View File

@ -7,7 +7,7 @@
<div class="modal-title h6">
<div class="d-flex">
<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>
<a class="btn btn-clear c-hand" @click="closeModal" />
@ -21,14 +21,14 @@
:class="{'active': selectedTab === 'general'}"
@click="selectTab('general')"
>
<a class="tab-link">{{ $t('word.general') }}</a>
<a class="tab-link">{{ t('word.general') }}</a>
</li>
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'themes'}"
@click="selectTab('themes')"
>
<a class="tab-link">{{ $t('word.themes') }}</a>
<a class="tab-link">{{ t('word.themes') }}</a>
</li>
<li
v-if="updateStatus !== 'disabled'"
@ -36,21 +36,21 @@
:class="{'active': selectedTab === '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
class="tab-item c-hand"
:class="{'active': selectedTab === 'changelog'}"
@click="selectTab('changelog')"
>
<a class="tab-link">{{ $t('word.changelog') }}</a>
<a class="tab-link">{{ t('word.changelog') }}</a>
</li>
<li
class="tab-item c-hand"
:class="{'active': selectedTab === 'about'}"
@click="selectTab('about')"
>
<a class="tab-link">{{ $t('word.about') }}</a>
<a class="tab-link">{{ t('word.about') }}</a>
</li>
</ul>
</div>
@ -58,14 +58,14 @@
<div class="container">
<form class="form-horizontal columns">
<div class="column col-12 h6 text-uppercase mb-1">
{{ $t('word.application') }}
{{ t('word.application') }}
</div>
<div class="column col-12 col-sm-12 mb-2 columns">
<div class="form-group column col-12">
<div class="col-5 col-sm-12">
<label class="form-label">
<i class="mdi mdi-18px mdi-translate mr-1" />
{{ $t('word.language') }}
{{ t('word.language') }}
</label>
</div>
<div class="col-3 col-sm-12">
@ -79,16 +79,16 @@
/>
</div>
<div class="col-4 col-sm-12 px-2 p-vcentered">
<small class="d-block" style="line-height:1.1; font-size:70%;">
{{ $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>
<small class="d-block" style="line-height: 1.1; font-size: 70%;">
{{ 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>
</small>
</div>
</div>
<div class="form-group column col-12">
<div class="col-5 col-sm-12">
<label class="form-label">
{{ $t('message.dataTabPageSize') }}
{{ t('message.dataTabPageSize') }}
</label>
</div>
<div class="col-3 col-sm-12">
@ -103,7 +103,7 @@
<div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12">
<label class="form-label">
{{ $t('message.restorePreviourSession') }}
{{ t('message.restorePreviourSession') }}
</label>
</div>
<div class="col-3 col-sm-12">
@ -116,7 +116,7 @@
<div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12">
<label class="form-label">
{{ $t('message.disableBlur') }}
{{ t('message.disableBlur') }}
</label>
</div>
<div class="col-3 col-sm-12">
@ -129,7 +129,7 @@
<div class="form-group column col-12">
<div class="col-5 col-sm-12">
<label class="form-label">
{{ $t('message.notificationsTimeout') }}
{{ t('message.notificationsTimeout') }}
</label>
</div>
<div class="col-3 col-sm-12">
@ -141,19 +141,19 @@
min="1"
@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 class="column col-12 h6 mt-4 text-uppercase mb-1">
{{ $t('word.editor') }}
{{ t('word.editor') }}
</div>
<div class="column col-12 col-sm-12 columns">
<div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12">
<label class="form-label">
{{ $t('word.autoCompletion') }}
{{ t('word.autoCompletion') }}
</label>
</div>
<div class="col-3 col-sm-12">
@ -166,7 +166,7 @@
<div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12">
<label class="form-label">
{{ $t('message.wrapLongLines') }}
{{ t('message.wrapLongLines') }}
</label>
</div>
<div class="col-3 col-sm-12">
@ -185,18 +185,18 @@
<div class="container">
<div class="columns">
<div class="column col-12 h6 text-uppercase mb-2">
{{ $t('message.applicationTheme') }}
{{ t('message.applicationTheme') }}
</div>
<div
class="column col-6 c-hand theme-block"
:class="{'selected': applicationTheme === '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">
<i class="mdi mdi-moon-waning-crescent mdi-48px" />
<div class="h6 mt-4">
{{ $t('word.dark') }}
{{ t('word.dark') }}
</div>
</div>
</div>
@ -205,11 +205,11 @@
:class="{'selected': applicationTheme === '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">
<i class="mdi mdi-white-balance-sunny mdi-48px" />
<div class="h6 mt-4">
{{ $t('word.light') }}
{{ t('word.light') }}
</div>
</div>
</div>
@ -217,7 +217,7 @@
<div class="columns mt-4">
<div class="column col-12 h6 text-uppercase mb-2 mt-4">
{{ $t('message.editorTheme') }}
{{ t('message.editorTheme') }}
</div>
<div class="column col-6 h5 mb-4">
<BaseSelect
@ -238,21 +238,21 @@
:class="{'active': editorFontSize === 'small'}"
@click="changeEditorFontSize('small')"
>
{{ $t('word.small') }}
{{ t('word.small') }}
</button>
<button
class="btn btn-dark cut-text"
:class="{'active': editorFontSize === 'medium'}"
@click="changeEditorFontSize('medium')"
>
{{ $t('word.medium') }}
{{ t('word.medium') }}
</button>
<button
class="btn btn-dark cut-text"
:class="{'active': editorFontSize === 'large'}"
@click="changeEditorFontSize('large')"
>
{{ $t('word.large') }}
{{ t('word.large') }}
</button>
</div>
</div>
@ -278,19 +278,19 @@
<div v-show="selectedTab === 'about'" class="panel-body py-4">
<div class="text-center">
<img src="../images/logo.svg" width="128">
<img :src="appLogo" width="128">
<h4>{{ appName }}</h4>
<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>
<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>
<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">
<small v-for="(contributor, i) in otherContributors" :key="i">{{ i !== 0 ? ', ' : '' }}{{ contributor }}</small>
</div>
<small>{{ $t('message.madeWithJS') }}</small>
<small>{{ t('message.madeWithJS') }}</small>
</div>
</div>
</div>
@ -301,176 +301,121 @@
</Teleport>
</template>
<script>
<script setup lang="ts">
import { onBeforeUnmount, Ref, ref } from 'vue';
import { shell } from 'electron';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useApplicationStore } from '@/stores/application';
import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces';
import localesNames from '@/i18n/supported-locales';
import ModalSettingsUpdate from '@/components/ModalSettingsUpdate';
import ModalSettingsChangelog from '@/components/ModalSettingsChangelog';
import BaseTextEditor from '@/components/BaseTextEditor';
import { localesNames } from '@/i18n/supported-locales';
import ModalSettingsUpdate from '@/components/ModalSettingsUpdate.vue';
import ModalSettingsChangelog from '@/components/ModalSettingsChangelog.vue';
import BaseTextEditor from '@/components/BaseTextEditor.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import { computed } from '@vue/reactivity';
export default {
name: 'ModalSettings',
components: {
ModalSettingsUpdate,
ModalSettingsChangelog,
BaseTextEditor,
BaseSelect
const { t, availableLocales } = useI18n();
const applicationStore = useApplicationStore();
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
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();
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
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,
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
{
group: 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' }
]
}
];
const exampleQuery = `-- This is an example
SELECT
employee.id,
employee.first_name,
@ -485,57 +430,81 @@ GROUP BY
ORDER BY
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);
},
onKey (e) {
e.stopPropagation();
if (e.key === 'Escape')
this.closeModal();
},
toggleRestoreSession () {
this.changeRestoreTabs(!this.restoreTabs);
},
toggleDisableBlur () {
this.changeDisableBlur(!this.disableBlur);
},
toggleAutoComplete () {
this.changeAutoComplete(!this.selectedAutoComplete);
},
toggleLineWrap () {
this.changeLineWrap(!this.selectedLineWrap);
}
}
const localLocale: Ref<string> = ref(null);
const localPageSize: Ref<number> = ref(null);
const localTimeout: Ref<number> = ref(null);
const localEditorTheme: Ref<string> = ref(null);
const selectedTab: Ref<string> = ref('general');
const locales = computed(() => {
const locales = [];
for (const locale of availableLocales)
locales.push({ code: locale, name: localesNames[locale] });
return locales;
});
const hasUpdates = computed(() => ['available', 'downloading', 'downloaded', 'link'].includes(updateStatus.value));
const workspace = computed(() => {
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>
<style lang="scss">

View File

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

View File

@ -52,68 +52,61 @@
</div>
</template>
<script>
<script setup lang="ts">
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { ipcRenderer, shell } from 'electron';
import { storeToRefs } from 'pinia';
import { useApplicationStore } from '@/stores/application';
import { useSettingsStore } from '@/stores/settings';
export default {
name: 'ModalSettingsUpdate',
setup () {
const applicationStore = useApplicationStore();
const settingsStore = useSettingsStore();
const { t } = useI18n();
const {
updateStatus,
getDownloadProgress
} = storeToRefs(applicationStore);
const { allowPrerelease } = storeToRefs(settingsStore);
const applicationStore = useApplicationStore();
const settingsStore = useSettingsStore();
const { changeAllowPrerelease } = settingsStore;
const {
updateStatus,
getDownloadProgress: downloadPercentage
} = storeToRefs(applicationStore);
const { allowPrerelease } = storeToRefs(settingsStore);
return {
updateStatus,
downloadPercentage: getDownloadProgress,
allowPrerelease,
changeAllowPrerelease
};
},
computed: {
updateMessage () {
switch (this.updateStatus) {
case 'noupdate':
return this.$t('message.noUpdatesAvailable');
case 'checking':
return this.$t('message.checkingForUpdate');
case 'nocheck':
return this.$t('message.checkFailure');
case 'available':
return this.$t('message.updateAvailable');
case 'downloading':
return this.$t('message.downloadingUpdate');
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 { changeAllowPrerelease } = settingsStore;
const updateMessage = computed(() => {
switch (updateStatus.value) {
case 'noupdate':
return t('message.noUpdatesAvailable');
case 'checking':
return t('message.checkingForUpdate');
case 'nocheck':
return t('message.checkFailure');
case 'available':
return t('message.updateAvailable');
case 'downloading':
return t('message.downloadingUpdate');
case 'downloaded':
return t('message.updateDownloaded');
case 'link':
return t('message.updateAvailable');
default:
return updateStatus.value;
}
});
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@
<img
v-if="!isMacOS"
class="titlebar-logo"
src="@/images/logo.svg"
:src="appIcon"
>
</div>
<div class="titlebar-elements titlebar-title">
@ -31,112 +31,70 @@
<i class="mdi mdi-24px mdi-refresh" />
</div>
<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>
</template>
<script>
import { ipcRenderer } from 'electron';
<script setup lang="ts">
import { computed, onUnmounted, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { getCurrentWindow } from '@electron/remote';
import { useConnectionsStore } from '@/stores/connections';
import { useWorkspacesStore } from '@/stores/workspaces';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
export default {
name: 'TheTitleBar',
setup () {
const { getConnectionName } = useConnectionsStore();
const workspacesStore = useWorkspacesStore();
const { t } = useI18n();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getConnectionName } = useConnectionsStore();
const workspacesStore = useWorkspacesStore();
const { getWorkspace } = workspacesStore;
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
return {
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 { getWorkspace } = workspacesStore;
const connectionName = this.getConnectionName(this.selectedWorkspace);
const workspace = this.getWorkspace(this.selectedWorkspace);
const breadcrumbs = Object.values(workspace.breadcrumbs).filter(breadcrumb => breadcrumb) || [workspace.client];
const appIcon = require('@/images/logo.svg');
const w = ref(getCurrentWindow());
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(' • ');
}
},
watch: {
windowTitle: function (val) {
ipcRenderer.send('change-window-title', val);
}
},
created () {
window.addEventListener('resize', this.onResize);
},
unmounted () {
window.removeEventListener('resize', this.onResize);
},
methods: {
closeApp () {
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 windowTitle = computed(() => {
if (!selectedWorkspace.value) return '';
if (selectedWorkspace.value === 'NEW') return t('message.createNewConnection');
const connectionName = getConnectionName(selectedWorkspace.value);
const workspace = getWorkspace(selectedWorkspace.value);
const breadcrumbs = Object.values(workspace.breadcrumbs).filter(breadcrumb => breadcrumb) || [workspace.client];
return [connectionName, ...breadcrumbs].join(' • ');
});
const toggleFullScreen = () => {
if (isMaximized.value)
w.value.unmaximize();
else
w.value.maximize();
};
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>
<style lang="scss">

View File

@ -469,266 +469,196 @@
</div>
</template>
<script>
<script setup lang="ts">
import { computed, onBeforeUnmount, Prop, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import Draggable from 'vuedraggable';
import * as Draggable from 'vuedraggable';
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 WorkspaceExploreBar from '@/components/WorkspaceExploreBar';
import WorkspaceEditConnectionPanel from '@/components/WorkspaceEditConnectionPanel';
import WorkspaceTabQuery from '@/components/WorkspaceTabQuery';
import WorkspaceTabTable from '@/components/WorkspaceTabTable';
import WorkspaceEmptyState from '@/components/WorkspaceEmptyState.vue';
import WorkspaceExploreBar from '@/components/WorkspaceExploreBar.vue';
import WorkspaceEditConnectionPanel from '@/components/WorkspaceEditConnectionPanel.vue';
import WorkspaceTabQuery from '@/components/WorkspaceTabQuery.vue';
import WorkspaceTabTable from '@/components/WorkspaceTabTable.vue';
import WorkspaceTabNewTable from '@/components/WorkspaceTabNewTable';
import WorkspaceTabNewView from '@/components/WorkspaceTabNewView';
import WorkspaceTabNewTrigger from '@/components/WorkspaceTabNewTrigger';
import WorkspaceTabNewRoutine from '@/components/WorkspaceTabNewRoutine';
import WorkspaceTabNewFunction from '@/components/WorkspaceTabNewFunction';
import WorkspaceTabNewScheduler from '@/components/WorkspaceTabNewScheduler';
import WorkspaceTabNewTriggerFunction from '@/components/WorkspaceTabNewTriggerFunction';
import WorkspaceTabNewTable from '@/components/WorkspaceTabNewTable.vue';
import WorkspaceTabNewView from '@/components/WorkspaceTabNewView.vue';
import WorkspaceTabNewTrigger from '@/components/WorkspaceTabNewTrigger.vue';
import WorkspaceTabNewRoutine from '@/components/WorkspaceTabNewRoutine.vue';
import WorkspaceTabNewFunction from '@/components/WorkspaceTabNewFunction.vue';
import WorkspaceTabNewScheduler from '@/components/WorkspaceTabNewScheduler.vue';
import WorkspaceTabNewTriggerFunction from '@/components/WorkspaceTabNewTriggerFunction.vue';
import WorkspaceTabPropsTable from '@/components/WorkspaceTabPropsTable';
import WorkspaceTabPropsView from '@/components/WorkspaceTabPropsView';
import WorkspaceTabPropsTrigger from '@/components/WorkspaceTabPropsTrigger';
import WorkspaceTabPropsTriggerFunction from '@/components/WorkspaceTabPropsTriggerFunction';
import WorkspaceTabPropsRoutine from '@/components/WorkspaceTabPropsRoutine';
import WorkspaceTabPropsFunction from '@/components/WorkspaceTabPropsFunction';
import WorkspaceTabPropsScheduler from '@/components/WorkspaceTabPropsScheduler';
import ModalProcessesList from '@/components/ModalProcessesList';
import ModalDiscardChanges from '@/components/ModalDiscardChanges';
import WorkspaceTabPropsTable from '@/components/WorkspaceTabPropsTable.vue';
import WorkspaceTabPropsView from '@/components/WorkspaceTabPropsView.vue';
import WorkspaceTabPropsTrigger from '@/components/WorkspaceTabPropsTrigger.vue';
import WorkspaceTabPropsTriggerFunction from '@/components/WorkspaceTabPropsTriggerFunction.vue';
import WorkspaceTabPropsRoutine from '@/components/WorkspaceTabPropsRoutine.vue';
import WorkspaceTabPropsFunction from '@/components/WorkspaceTabPropsFunction.vue';
import WorkspaceTabPropsScheduler from '@/components/WorkspaceTabPropsScheduler.vue';
import ModalProcessesList from '@/components/ModalProcessesList.vue';
import ModalDiscardChanges from '@/components/ModalDiscardChanges.vue';
export default {
name: 'Workspace',
components: {
Draggable,
WorkspaceEmptyState,
WorkspaceExploreBar,
WorkspaceEditConnectionPanel,
WorkspaceTabQuery,
WorkspaceTabTable,
WorkspaceTabNewTable,
WorkspaceTabPropsTable,
WorkspaceTabNewView,
WorkspaceTabPropsView,
WorkspaceTabNewTrigger,
WorkspaceTabPropsTrigger,
WorkspaceTabNewTriggerFunction,
WorkspaceTabPropsTriggerFunction,
WorkspaceTabNewRoutine,
WorkspaceTabNewFunction,
WorkspaceTabPropsRoutine,
WorkspaceTabPropsFunction,
WorkspaceTabNewScheduler,
WorkspaceTabPropsScheduler,
ModalProcessesList,
ModalDiscardChanges
const workspacesStore = useWorkspacesStore();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const {
getWorkspace,
addWorkspace,
connectWorkspace,
selectTab,
newTab,
removeTab,
updateTabs
} = workspacesStore;
const props = defineProps({
connection: Object as Prop<ConnectionParams>
});
const hasWheelEvent = ref(false);
const isProcessesModal = ref(false);
const unsavedTab = ref(null);
const tabWrap = ref(null);
const workspace = computed(() => getWorkspace(props.connection.uid));
const draggableTabs = computed<WorkspaceTab[]>({
get () {
return workspace.value.tabs;
},
props: {
connection: Object
},
setup () {
const workspacesStore = useWorkspacesStore();
set (val) {
updateTabs({ uid: props.connection.uid, tabs: val });
}
});
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const isSelected = computed(() => {
return selectedWorkspace.value === props.connection.uid;
});
const {
getWorkspace,
addWorkspace,
connectWorkspace,
removeConnected,
selectTab,
newTab,
removeTab,
updateTabs,
selectNextTab,
selectPrevTab
} = workspacesStore;
const selectedTab = computed(() => {
return workspace.value ? workspace.value.selectedTab : null;
});
return {
selectedWorkspace,
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();
const queryTabs = computed(() => {
return workspace.value ? workspace.value.tabs.filter(tab => tab.type === 'query') : [];
});
if (!this.isSelected)
return;
const hasTools = computed(() => {
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
this.addQueryTab();
}
watch(queryTabs, (newVal, oldVal) => {
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 currentTab = this.getSelectedTab();
if (currentTab)
this.closeTab(currentTab);
}
const addQueryTab = () => {
newTab({ uid: props.connection.uid, type: 'query' });
};
// select next tab
if (e.altKey && (e.ctrlKey || e.metaKey) && e.key === 'ArrowRight')
this.selectNextTab({ uid: this.connection.uid });
const getSelectedTab = () => {
return workspace.value.tabs.find(tab => tab.uid === selectedTab.value);
};
// select prev tab
if (e.altKey && (e.ctrlKey || e.metaKey) && e.key === 'ArrowLeft')
this.selectPrevTab({ uid: this.connection.uid });
const onKey = (e: KeyboardEvent) => {
e.stopPropagation();
// select tab by index (range 1-9). CTRL|CMD number
if ((e.ctrlKey || e.metaKey) && !e.altKey && e.keyCode >= 49 && e.keyCode <= 57) {
const newIndex = parseInt(e.key) - 1;
if (!isSelected.value)
return;
if (this.workspace.tabs[newIndex])
this.selectTab({ uid: this.connection.uid, tab: this.workspace.tabs[newIndex].uid });
}
},
openAsPermanentTab (tab) {
const permanentTabs = {
table: 'data',
view: 'data',
trigger: 'trigger-props',
triggerFunction: 'trigger-function-props',
function: 'function-props',
routine: 'routine-props',
scheduler: 'scheduler-props'
};
if ((e.ctrlKey || e.metaKey) && e.key === 't' && !e.altKey) { // CTRL|Command + t
addQueryTab();
}
this.newTab({
uid: this.connection.uid,
schema: tab.schema,
elementName: tab.elementName,
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;
}
if ((e.ctrlKey || e.metaKey) && e.key === 'w' && !e.altKey) { // CTRL|Command + w
const currentTab = getSelectedTab();
if (currentTab)
closeTab(currentTab);
}
};
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>
<style lang="scss">

View File

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

View File

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

View File

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

View File

@ -48,7 +48,7 @@
/>
</div>
</div>
<div class="workspace-explorebar-body" @click="$refs.explorebar.focus()">
<div class="workspace-explorebar-body" @click="explorebar.focus()">
<WorkspaceExploreBarSchema
v-for="db of workspace.structure"
:key="db.name"
@ -115,7 +115,8 @@
</div>
</template>
<script>
<script setup lang="ts">
import { Component, computed, onMounted, Ref, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useConnectionsStore } from '@/stores/connections';
@ -125,428 +126,290 @@ import { useWorkspacesStore } from '@/stores/workspaces';
import Tables from '@/ipc-api/Tables';
import Views from '@/ipc-api/Views';
import Functions from '@/ipc-api/Functions';
import Schedulers from '@/ipc-api/Schedulers';
import WorkspaceExploreBarSchema from '@/components/WorkspaceExploreBarSchema';
import DatabaseContext from '@/components/WorkspaceExploreBarSchemaContext';
import TableContext from '@/components/WorkspaceExploreBarTableContext';
import MiscContext from '@/components/WorkspaceExploreBarMiscContext';
import MiscFolderContext from '@/components/WorkspaceExploreBarMiscFolderContext';
import ModalNewSchema from '@/components/ModalNewSchema';
import WorkspaceExploreBarSchema from '@/components/WorkspaceExploreBarSchema.vue';
import DatabaseContext from '@/components/WorkspaceExploreBarSchemaContext.vue';
import TableContext from '@/components/WorkspaceExploreBarTableContext.vue';
import MiscContext from '@/components/WorkspaceExploreBarMiscContext.vue';
import MiscFolderContext from '@/components/WorkspaceExploreBarMiscFolderContext.vue';
import ModalNewSchema from '@/components/ModalNewSchema.vue';
export default {
name: 'WorkspaceExploreBar',
components: {
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 props = defineProps({
connection: Object,
isSelected: Boolean
});
const { explorebarSize } = storeToRefs(settingsStore);
const { getConnectionName } = useConnectionsStore();
const { addNotification } = useNotificationsStore();
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
const { changeExplorebarSize } = settingsStore;
const {
getWorkspace,
removeConnected: disconnectWorkspace,
refreshStructure,
changeBreadcrumbs,
selectTab,
newTab,
removeTabs,
setSearchTerm,
addLoadingElement,
removeLoadingElement
} = workspacesStore;
const { explorebarSize } = storeToRefs(settingsStore);
return {
getConnectionName,
addNotification,
explorebarSize,
changeExplorebarSize,
getWorkspace,
disconnectWorkspace,
refreshStructure,
changeBreadcrumbs,
selectTab,
newTab,
removeTabs,
setSearchTerm,
addLoadingElement,
removeLoadingElement
};
},
data () {
return {
isRefreshing: false,
const { changeExplorebarSize } = settingsStore;
const {
getWorkspace,
removeConnected: disconnectWorkspace,
refreshStructure,
newTab,
removeTabs,
setSearchTerm,
addLoadingElement,
removeLoadingElement
} = workspacesStore;
isNewDBModal: false,
isNewViewModal: false,
isNewTriggerModal: false,
isNewRoutineModal: false,
isNewFunctionModal: false,
isNewTriggerFunctionModal: false,
isNewSchedulerModal: false,
const searchInput: Ref<HTMLInputElement> = ref(null);
const explorebar: Ref<HTMLInputElement> = ref(null);
const resizer: Ref<HTMLInputElement> = ref(null);
const schema: Ref<Component & { selectSchema: (name: string) => void; $refs: {schemaAccordion: HTMLDetailsElement} }[]> = ref(null);
const isRefreshing = ref(false);
const isNewDBModal = ref(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,
explorebarWidthInterval: null,
searchTermInterval: null,
isDatabaseContext: false,
isTableContext: false,
isMiscContext: false,
isMiscFolderContext: false,
const workspace = computed(() => {
return getWorkspace(props.connection.uid);
});
databaseContextEvent: null,
tableContextEvent: null,
miscContextEvent: null,
const connectionName = computed(() => {
return getConnectionName(props.connection.uid);
});
selectedSchema: '',
selectedTable: null,
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);
const customizations = computed(() => {
return workspace.value.customizations;
});
this.explorebarWidthInterval = setTimeout(() => {
this.changeExplorebarSize(val);
}, 500);
},
isSelected (val) {
if (val) this.localWidth = this.explorebarSize;
},
searchTerm () {
clearTimeout(this.searchTermInterval);
watch(localWidth, (val: number) => {
clearTimeout(explorebarWidthInterval.value);
this.searchTermInterval = setTimeout(() => {
this.setSearchTerm(this.searchTerm);
}, 200);
}
},
created () {
this.localWidth = this.explorebarSize;
},
mounted () {
const resizer = this.$refs.resizer;
explorebarWidthInterval.value = setTimeout(() => {
changeExplorebarSize(val);
}, 500);
});
resizer.addEventListener('mousedown', e => {
e.preventDefault();
watch(() => props.isSelected, (val: boolean) => {
if (val) localWidth.value = explorebarSize.value;
});
window.addEventListener('mousemove', this.resize);
window.addEventListener('mouseup', this.stopResize);
});
watch(searchTerm, () => {
clearTimeout(searchTermInterval.value);
if (this.workspace.structure.length === 1) { // Auto-open if juust one schema
this.$refs.schema[0].selectSchema(this.workspace.structure[0].name);
this.$refs.schema[0].$refs.schemaAccordion.open = true;
}
},
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;
searchTermInterval.value = setTimeout(() => {
setSearchTerm(searchTerm.value);
}, 200);
});
this.$refs.searchInput.focus();
},
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();
localWidth.value = explorebarSize.value;
this.newTab({
uid: this.workspace.uid,
schema: this.selectedSchema,
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();
onMounted(() => {
resizer.value.addEventListener('mousedown', (e: MouseEvent) => {
e.preventDefault();
this.addLoadingElement({
name: payload.table.name,
schema: payload.schema,
type: 'table'
});
window.addEventListener('mousemove', resize);
window.addEventListener('mouseup', stopResize);
});
try {
let res;
if (workspace.value.structure.length === 1) { // Auto-open if juust one schema
schema.value[0].selectSchema(workspace.value.structure[0].name);
schema.value[0].$refs.schemaAccordion.open = true;
}
});
if (payload.table.type === 'table') {
res = await Tables.dropTable({
uid: this.connection.uid,
table: payload.table.name,
schema: payload.schema
});
}
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 refresh = async () => {
if (!isRefreshing.value) {
isRefreshing.value = true;
await refreshStructure(props.connection.uid);
isRefreshing.value = false;
}
};
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>
<style lang="scss">

View File

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

View File

@ -1,112 +1,68 @@
<template>
<BaseContextMenu
:context-event="contextEvent"
:context-event="props.contextEvent"
@close-context="closeContext"
>
<div
v-if="selectedMisc === 'trigger'"
v-if="props.selectedMisc === 'trigger'"
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
v-if="selectedMisc === 'procedure'"
v-if="['procedure', 'routine'].includes(props.selectedMisc)"
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
v-if="selectedMisc === 'function'"
v-if="props.selectedMisc === 'function'"
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
v-if="selectedMisc === 'triggerFunction'"
v-if="props.selectedMisc === 'triggerFunction'"
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
v-if="selectedMisc === 'scheduler'"
v-if="props.selectedMisc === 'scheduler'"
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>
</BaseContextMenu>
</template>
<script>
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import BaseContextMenu from '@/components/BaseContextMenu';
import { storeToRefs } from 'pinia';
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import BaseContextMenu from '@/components/BaseContextMenu.vue';
export default {
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 { t } = useI18n();
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 {
addNotification,
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');
}
}
const closeContext = () => {
emit('close-context');
};
</script>

View File

@ -25,7 +25,7 @@
<ul class="menu menu-nav pt-0">
<li
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"
class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}"
@ -61,7 +61,7 @@
@contextmenu.prevent="showMiscFolderContext($event, 'trigger')"
>
<i class="misc-icon mdi mdi-18px mdi-folder-cog mr-1" />
{{ $tc('word.trigger', 2) }}
{{ t('word.trigger', 2) }}
</summary>
<div class="accordion-body">
<div>
@ -69,7 +69,7 @@
<li
v-for="trigger of filteredTriggers"
: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="{'selected': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}"
@mousedown.left="selectMisc({schema: database.name, misc: trigger, type: 'trigger'})"
@ -84,7 +84,7 @@
<div
v-if="trigger.enabled === false"
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" />
</div>
@ -100,27 +100,27 @@
<summary
class="accordion-header misc-name"
: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" />
{{ $tc('word.storedRoutine', 2) }}
{{ t('word.storedRoutine', 2) }}
</summary>
<div class="accordion-body">
<div>
<ul class="menu menu-nav pt-0">
<li
v-for="(procedure, i) of filteredProcedures"
:key="`${procedure.name}-${i}`"
:ref="breadcrumbs.schema === database.name && breadcrumbs.routine === procedure.name ? 'explorebar-selected' : ''"
v-for="(routine, i) of filteredProcedures"
:key="`${routine.name}-${i}`"
:ref="breadcrumbs.schema === database.name && breadcrumbs.routine === routine.name ? 'explorebarSelected' : ''"
class="menu-item"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.routine === procedure.name}"
@mousedown.left="selectMisc({schema: database.name, misc: procedure, type: 'routine'})"
@dblclick="openMiscPermanentTab({schema: database.name, misc: procedure, type: 'routine'})"
@contextmenu.prevent="showMiscContext($event, {...procedure, type: 'procedure'})"
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.routine === routine.name}"
@mousedown.left="selectMisc({schema: database.name, misc: routine, type: 'routine'})"
@dblclick="openMiscPermanentTab({schema: database.name, misc: routine, type: 'routine'})"
@contextmenu.prevent="showMiscContext($event, {...routine, type: 'routine'})"
>
<a class="table-name">
<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>
</li>
</ul>
@ -137,7 +137,7 @@
@contextmenu.prevent="showMiscFolderContext($event, 'triggerFunction')"
>
<i class="misc-icon mdi mdi-18px mdi-folder-refresh mr-1" />
{{ $tc('word.triggerFunction', 2) }}
{{ t('word.triggerFunction', 2) }}
</summary>
<div class="accordion-body">
<div>
@ -145,7 +145,7 @@
<li
v-for="(func, i) of filteredTriggerFunctions"
: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="{'selected': breadcrumbs.schema === database.name && breadcrumbs.triggerFunction === func.name}"
@mousedown.left="selectMisc({schema: database.name, misc: func, type: 'triggerFunction'})"
@ -171,7 +171,7 @@
@contextmenu.prevent="showMiscFolderContext($event, 'function')"
>
<i class="misc-icon mdi mdi-18px mdi-folder-move mr-1" />
{{ $tc('word.function', 2) }}
{{ t('word.function', 2) }}
</summary>
<div class="accordion-body">
<div>
@ -179,7 +179,7 @@
<li
v-for="(func, i) of filteredFunctions"
: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="{'selected': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
@mousedown.left="selectMisc({schema: database.name, misc: func, type: 'function'})"
@ -205,7 +205,7 @@
@contextmenu.prevent="showMiscFolderContext($event, 'scheduler')"
>
<i class="misc-icon mdi mdi-18px mdi-folder-clock mr-1" />
{{ $tc('word.scheduler', 2) }}
{{ t('word.scheduler', 2) }}
</summary>
<div class="accordion-body">
<div>
@ -213,7 +213,7 @@
<li
v-for="scheduler of filteredSchedulers"
: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="{'selected': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}"
@mousedown.left="selectMisc({schema: database.name, misc: scheduler, type: 'scheduler'})"
@ -228,7 +228,7 @@
<div
v-if="scheduler.enabled === false"
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" />
</div>
@ -242,226 +242,235 @@
</details>
</template>
<script>
import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces';
import { formatBytes } from 'common/libs/formatBytes';
<script setup lang="ts">
import { computed, Prop, Ref, ref, watch } from 'vue';
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 {
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 { t } = useI18n();
const { applicationTheme } = storeToRefs(settingsStore);
const props = defineProps({
database: Object as Prop<WorkspaceStructure>,
connection: Object
});
const {
getLoadedSchemas,
getWorkspace,
getSearchTerm,
changeBreadcrumbs,
addLoadedSchema,
newTab,
refreshSchema
} = workspacesStore;
const emit = defineEmits([
'show-schema-context',
'show-table-context',
'show-misc-context',
'show-misc-folder-context'
]);
return {
applicationTheme,
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);
const settingsStore = useSettingsStore();
const workspacesStore = useWorkspacesStore();
if (!isVisible) {
element.setAttribute('tabindex', '-1');
element.focus();
element.removeAttribute('tabindex');
}
}
}, 50);
const { applicationTheme } = storeToRefs(settingsStore);
const {
getLoadedSchemas,
getWorkspace,
getSearchTerm,
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');
}
}
}
},
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
});
}, 50);
}
});
this.setBreadcrumbs({ schema, [table.type]: table.name });
},
selectMisc ({ schema, misc, type }) {
const miscTempTabs = {
trigger: 'temp-trigger-props',
triggerFunction: 'temp-trigger-function-props',
function: 'temp-function-props',
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 selectSchema = async (schema: string) => {
if (!loadedSchemas.value.has(schema) && !isLoading.value) {
isLoading.value = true;
setBreadcrumbs({ schema });
await refreshSchema({ uid: props.connection.uid, schema });
addLoadedSchema(schema);
isLoading.value = false;
}
};
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>
<style lang="scss">

View File

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

View File

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

View File

@ -11,27 +11,27 @@
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
<span>{{ t('word.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
:title="t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<span>{{ t('word.clear') }}</span>
</button>
<div class="divider-vert py-3" />
<button class="btn btn-dark btn-sm" @click="showParamsModal">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span>
<span>{{ t('word.parameters') }}</span>
</button>
</div>
<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>
</div>
</div>
@ -42,7 +42,7 @@
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.name') }}
{{ t('word.name') }}
</label>
<input
ref="firstInput"
@ -55,7 +55,7 @@
<div v-if="customizations.languages" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.language') }}
{{ t('word.language') }}
</label>
<BaseSelect
v-model="localFunction.language"
@ -67,13 +67,13 @@
<div v-if="customizations.definer" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.definer') }}
{{ t('word.definer') }}
</label>
<BaseSelect
v-model="localFunction.definer"
:options="[{value: '', name:$t('message.currentUser')}, ...workspace.users]"
:option-label="(user) => user.value === '' ? user.name : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
:options="[{value: '', name:t('message.currentUser')}, ...workspace.users]"
:option-label="(user: any) => user.value === '' ? user.name : `${user.name}@${user.host}`"
:option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select"
/>
</div>
@ -81,13 +81,13 @@
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.returns') }}
{{ t('word.returns') }}
</label>
<div class="input-group">
<BaseSelect
v-model="localFunction.returns"
class="form-select text-uppercase"
:options="[{ name: 'VOID' }, ...workspace.dataTypes]"
:options="[{ name: 'VOID' }, ...(workspace.dataTypes as any)]"
group-label="group"
group-values="types"
option-label="name"
@ -101,7 +101,7 @@
class="form-input"
type="number"
min="0"
:placeholder="$t('word.length')"
:placeholder="t('word.length')"
>
</div>
</div>
@ -109,7 +109,7 @@
<div v-if="customizations.comment" class="column">
<div class="form-group">
<label class="form-label">
{{ $t('word.comment') }}
{{ t('word.comment') }}
</label>
<input
v-model="localFunction.comment"
@ -121,7 +121,7 @@
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.sqlSecurity') }}
{{ t('message.sqlSecurity') }}
</label>
<BaseSelect
v-model="localFunction.security"
@ -133,7 +133,7 @@
<div v-if="customizations.functionDataAccess" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.dataAccess') }}
{{ t('message.dataAccess') }}
</label>
<BaseSelect
v-model="localFunction.dataAccess"
@ -146,7 +146,7 @@
<div class="form-group">
<label class="form-label d-invisible">.</label>
<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>
</div>
</div>
@ -154,7 +154,7 @@
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<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
v-show="isSelected"
ref="queryEditor"
@ -175,213 +175,177 @@
</div>
</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 { useWorkspacesStore } from '@/stores/workspaces';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspaceTabPropsFunctionParamsModal from '@/components/WorkspaceTabPropsFunctionParamsModal';
import Functions from '@/ipc-api/Functions';
import { storeToRefs } from 'pinia';
import BaseLoader from '@/components/BaseLoader.vue';
import QueryEditor from '@/components/QueryEditor.vue';
import WorkspaceTabPropsFunctionParamsModal from '@/components/WorkspaceTabPropsFunctionParamsModal.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import { FunctionInfos, FunctionParam } from 'common/interfaces/antares';
import { useI18n } from 'vue-i18n';
export default {
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 { t } = useI18n();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const props = defineProps({
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
});
const {
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
removeTab,
renameTabs
} = workspacesStore;
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
return {
addNotification,
selectedWorkspace,
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
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);
const {
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
removeTab
} = workspacesStore;
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 });
}
},
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.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
};
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
isSaving.value = false;
};
setTimeout(() => {
this.resizeQueryEditor();
}, 50);
const clearChanges = () => {
localFunction.value = JSON.parse(JSON.stringify(originalFunction.value));
queryEditor.value.editor.session.setValue(localFunction.value.sql);
};
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
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();
}
};
setTimeout(() => {
this.$refs.firstInput.focus();
}, 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
};
const parametersUpdate = (parameters: FunctionParam[]) => {
localFunction.value = { ...localFunction.value, parameters };
};
try {
const { status, response } = await Functions.createFunction(params);
const showParamsModal = () => {
isParamsModal.value = true;
};
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
const hideParamsModal = () => {
isParamsModal.value = false;
};
this.newTab({
uid: this.connection.uid,
schema: this.schema,
elementName: this.localFunction.name,
elementType: 'function',
type: 'function-props'
});
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();
}
}
const onKey = (e: KeyboardEvent) => {
if (props.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.key === 's') { // 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 });
});
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>

View File

@ -72,8 +72,8 @@
<BaseSelect
v-model="localRoutine.definer"
:options="[{value: '', name:$t('message.currentUser')}, ...workspace.users]"
:option-label="(user) => user.value === '' ? user.name : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
:option-label="(user: any) => user.value === '' ? user.name : `${user.name}@${user.host}`"
:option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select"
/>
</div>
@ -129,7 +129,6 @@
<label class="form-label ml-2">{{ $t('message.routineBody') }}</label>
<QueryEditor
v-show="isSelected"
:key="`new-${_uid}`"
ref="queryEditor"
v-model="localRoutine.sql"
:workspace="workspace"
@ -148,209 +147,174 @@
</div>
</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 { useWorkspacesStore } from '@/stores/workspaces';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
import WorkspaceTabPropsRoutineParamsModal from '@/components/WorkspaceTabPropsRoutineParamsModal';
import Routines from '@/ipc-api/Routines';
import { storeToRefs } from 'pinia';
import QueryEditor from '@/components/QueryEditor.vue';
import BaseLoader from '@/components/BaseLoader.vue';
import WorkspaceTabPropsRoutineParamsModal from '@/components/WorkspaceTabPropsRoutineParamsModal.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import { FunctionParam } from 'common/interfaces/antares';
export default {
name: 'WorkspaceTabNewRoutine',
components: {
QueryEditor,
BaseLoader,
WorkspaceTabPropsRoutineParamsModal,
BaseSelect
},
props: {
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
},
setup () {
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const props = defineProps({
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
});
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
const {
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
removeTab,
renameTabs
} = workspacesStore;
const {
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
removeTab
} = workspacesStore;
return {
addNotification,
selectedWorkspace,
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
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);
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 originalRoutine = ref(null);
const localRoutine = ref(null);
const editorHeight = ref(300);
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 });
}
},
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.originalRoutine = {
sql: this.customizations.procedureSql,
language: this.customizations.languages ? this.customizations.languages[0] : null,
name: '',
definer: '',
comment: '',
security: 'DEFINER',
dataAccess: 'CONTAINS SQL',
deterministic: false
};
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
this.localRoutine = JSON.parse(JSON.stringify(this.originalRoutine));
isSaving.value = false;
};
setTimeout(() => {
this.resizeQueryEditor();
}, 50);
const clearChanges = () => {
localRoutine.value = JSON.parse(JSON.stringify(originalRoutine.value));
queryEditor.value.editor.session.setValue(localRoutine.value.sql);
};
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
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();
}
};
setTimeout(() => {
this.$refs.firstInput.focus();
}, 100);
const parametersUpdate = (parameters: FunctionParam[]) => {
localRoutine.value = { ...localRoutine.value, parameters };
};
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.localRoutine
};
const showParamsModal = () => {
isParamsModal.value = true;
};
try {
const { status, response } = await Routines.createRoutine(params);
const hideParamsModal = () => {
isParamsModal.value = false;
};
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
this.newTab({
uid: this.connection.uid,
schema: this.schema,
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();
}
}
const onKey = (e: KeyboardEvent) => {
if (props.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.key === 's') { // 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 });
});
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>

View File

@ -11,26 +11,26 @@
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
<span>{{ t('word.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
:title="t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<span>{{ t('word.clear') }}</span>
</button>
<div class="divider-vert py-3" />
<button class="btn btn-dark btn-sm" @click="showTimingModal">
<i class="mdi mdi-24px mdi-timer mr-1" />
<span>{{ $t('word.timing') }}</span>
<span>{{ t('word.timing') }}</span>
</button>
</div>
<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>
</div>
</div>
@ -40,7 +40,7 @@
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<label class="form-label">{{ t('word.name') }}</label>
<input
ref="firstInput"
v-model="localScheduler.name"
@ -51,19 +51,19 @@
</div>
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<label class="form-label">{{ t('word.definer') }}</label>
<BaseSelect
v-model="localScheduler.definer"
:options="users"
:option-label="(user) => user.value === '' ? $t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
:option-label="(user: any) => user.value === '' ? t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select"
/>
</div>
</div>
<div class="column">
<div class="form-group">
<label class="form-label">{{ $t('word.comment') }}</label>
<label class="form-label">{{ t('word.comment') }}</label>
<input
v-model="localScheduler.comment"
class="form-input"
@ -73,7 +73,7 @@
</div>
<div class="column">
<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">
<input
v-model="localScheduler.state"
@ -104,7 +104,7 @@
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<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
v-show="isSelected"
ref="queryEditor"
@ -124,209 +124,186 @@
</div>
</template>
<script>
import { storeToRefs } from 'pinia';
<script setup lang="ts">
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 { useWorkspacesStore } from '@/stores/workspaces';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspaceTabPropsSchedulerTimingModal from '@/components/WorkspaceTabPropsSchedulerTimingModal';
import BaseLoader from '@/components/BaseLoader.vue';
import QueryEditor from '@/components/QueryEditor.vue';
import WorkspaceTabPropsSchedulerTimingModal from '@/components/WorkspaceTabPropsSchedulerTimingModal.vue';
import Schedulers from '@/ipc-api/Schedulers';
import BaseSelect from '@/components/BaseSelect.vue';
export default {
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 { t } = useI18n();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const props = defineProps({
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
});
const {
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
removeTab,
renameTabs
} = workspacesStore;
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
return {
addNotification,
selectedWorkspace,
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
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);
const {
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
removeTab
} = workspacesStore;
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
},
users () {
const users = [{ value: '' }, ...this.workspace.users];
if (!this.isDefinerInUsers) {
const [name, host] = this.originalScheduler.definer.replaceAll('`', '').split('@');
users.unshift({ name, host });
}
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 isTimingModal = ref(false);
const originalScheduler = ref(null);
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 });
}
},
watch: {
isSelected (val) {
if (val)
this.changeBreadcrumbs({ schema: this.schema });
},
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'
};
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
this.localScheduler = JSON.parse(JSON.stringify(this.originalScheduler));
isSaving.value = false;
};
setTimeout(() => {
this.resizeQueryEditor();
}, 50);
const clearChanges = () => {
localScheduler.value = JSON.parse(JSON.stringify(originalScheduler.value));
queryEditor.value.editor.session.setValue(localScheduler.value.sql);
};
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
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();
}
};
setTimeout(() => {
this.$refs.firstInput.focus();
}, 100);
const showTimingModal = () => {
isTimingModal.value = true;
};
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.localScheduler
};
const hideTimingModal = () => {
isTimingModal.value = false;
};
try {
const { status, response } = await Schedulers.createScheduler(params);
const timingUpdate = (options: EventInfos) => {
localScheduler.value = options;
};
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
this.newTab({
uid: this.connection.uid,
schema: this.schema,
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();
}
}
const onKey = (e: KeyboardEvent) => {
if (props.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.key === 's') { // 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 });
});
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>

View File

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

View File

@ -1,22 +1,22 @@
<template>
<div class="column col-12 empty">
<p class="h6 empty-subtitle">
{{ $t('message.thereAreNoTableFields') }}
{{ t('message.thereAreNoTableFields') }}
</p>
<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" />
{{ $t('message.addNewField') }}
{{ t('message.addNewField') }}
</button>
</div>
</div>
</template>
<script>
export default {
name: 'WorkspaceTabNewTableEmptyState',
emits: ['new-field']
};
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const emit = defineEmits(['new-field']);
</script>
<style scoped>

View File

@ -11,20 +11,20 @@
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
<span>{{ t('word.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
:title="t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<span>{{ t('word.clear') }}</span>
</button>
</div>
<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>
</div>
</div>
@ -34,7 +34,7 @@
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<label class="form-label">{{ t('word.name') }}</label>
<input
ref="firstInput"
v-model="localTrigger.name"
@ -45,12 +45,12 @@
</div>
<div v-if="customizations.definer" class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<label class="form-label">{{ t('word.definer') }}</label>
<BaseSelect
v-model="localTrigger.definer"
:options="users"
:option-label="(user) => user.value === '' ? $t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
:option-label="(user: any) => user.value === '' ? t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select"
/>
</div>
@ -58,7 +58,7 @@
<fieldset class="column columns mb-0" :disabled="customizations.triggerOnlyRename">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.table') }}</label>
<label class="form-label">{{ t('word.table') }}</label>
<BaseSelect
v-model="localTrigger.table"
:options="schemaTables"
@ -70,7 +70,7 @@
</div>
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.event') }}</label>
<label class="form-label">{{ t('word.event') }}</label>
<div class="input-group">
<BaseSelect
v-model="localTrigger.activation"
@ -85,7 +85,7 @@
/>
<div v-if="customizations.triggerMultipleEvents" class="px-4">
<label
v-for="event in Object.keys(localEvents)"
v-for="event in Object.keys(localEvents) as ('INSERT' | 'UPDATE' | 'DELETE')[]"
:key="event"
class="form-checkbox form-inline"
@change.prevent="changeEvents(event)"
@ -101,7 +101,7 @@
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<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
v-show="isSelected"
ref="queryEditor"
@ -114,219 +114,204 @@
</div>
</template>
<script>
import { storeToRefs } from 'pinia';
<script setup lang="ts">
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 { useWorkspacesStore } from '@/stores/workspaces';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor.vue';
import BaseLoader from '@/components/BaseLoader.vue';
import Triggers from '@/ipc-api/Triggers';
import BaseSelect from '@/components/BaseSelect.vue';
export default {
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 { t } = useI18n();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const props = defineProps({
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
});
const {
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
removeTab,
renameTabs
} = workspacesStore;
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
return {
addNotification,
selectedWorkspace,
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
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);
const {
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
removeTab
} = workspacesStore;
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
},
users () {
const users = [{ value: '' }, ...this.workspace.users];
if (!this.isDefinerInUsers) {
const [name, host] = this.originalTrigger.definer.replaceAll('`', '').split('@');
users.unshift({ name, host });
}
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 originalTrigger = ref(null);
const localTrigger = ref(null);
const editorHeight = ref(300);
const localEvents = ref({ INSERT: false, UPDATE: false, DELETE: false });
return users;
}
},
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: ''
};
const workspace = computed(() => {
return getWorkspace(props.connection.uid);
});
this.localTrigger = JSON.parse(JSON.stringify(this.originalTrigger));
const customizations = computed(() => {
return workspace.value.customizations;
});
setTimeout(() => {
this.resizeQueryEditor();
}, 50);
const isChanged = computed(() => {
return JSON.stringify(originalTrigger.value) !== JSON.stringify(localTrigger.value);
});
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
const isDefinerInUsers = computed(() => {
return originalTrigger.value ? workspace.value.users.some(user => originalTrigger.value.definer === `\`${user.name}\`@\`${user.host}\``) : true;
});
setTimeout(() => {
this.$refs.firstInput.focus();
}, 100);
const schemaTables = computed(() => {
const schemaTables = workspace.value.structure
.filter(schema => schema.name === props.schema)
.map(schema => schema.tables);
window.addEventListener('resize', this.resizeQueryEditor);
},
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
};
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
});
try {
const { status, response } = await Triggers.createTrigger(params);
const users = computed(() => {
const users = [{ value: '' }, ...workspace.value.users];
if (!isDefinerInUsers.value) {
const [name, host] = originalTrigger.value.definer.replaceAll('`', '').split('@');
users.unshift({ name, host });
}
if (status === 'success') {
await this.refreshStructure(this.connection.uid);
return users;
});
this.newTab({
uid: this.connection.uid,
schema: this.schema,
elementName: this.localTrigger.name,
elementType: 'trigger',
type: 'trigger-props'
});
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 changeEvents = (event: 'INSERT' | 'UPDATE' | 'DELETE') => {
if (customizations.value.triggerMultipleEvents) {
localEvents.value[event] = !localEvents.value[event];
localTrigger.value.event = [];
for (const key in localEvents.value) {
if (localEvents.value[key as 'INSERT' | 'UPDATE' | 'DELETE'])
localTrigger.value.event.push(key);
}
}
};
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>

View File

@ -11,16 +11,16 @@
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
<span>{{ t('word.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
:title="t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<span>{{ t('word.clear') }}</span>
</button>
</div>
</div>
@ -30,7 +30,7 @@
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.name') }}
{{ t('word.name') }}
</label>
<input
ref="firstInput"
@ -43,7 +43,7 @@
<div v-if="customizations.triggerFunctionlanguages" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.language') }}
{{ t('word.language') }}
</label>
<BaseSelect
v-model="localFunction.language"
@ -55,20 +55,20 @@
<div v-if="customizations.definer" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.definer') }}
{{ t('word.definer') }}
</label>
<BaseSelect
v-model="localFunction.definer"
:options="workspace.users"
:option-label="(user) => user.value === '' ? $t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
:option-label="(user: any) => user.value === '' ? t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select"
/>
</div>
</div>
<div v-if="customizations.comment" class="form-group">
<label class="form-label">
{{ $t('word.comment') }}
{{ t('word.comment') }}
</label>
<input
v-model="localFunction.comment"
@ -80,7 +80,7 @@
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<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
v-show="isSelected"
ref="queryEditor"
@ -93,190 +93,157 @@
</div>
</template>
<script>
import { storeToRefs } from 'pinia';
<script setup lang="ts">
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 { useWorkspacesStore } from '@/stores/workspaces';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader.vue';
import QueryEditor from '@/components/QueryEditor.vue';
import Functions from '@/ipc-api/Functions';
import BaseSelect from '@/components/BaseSelect.vue';
export default {
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 { t } = useI18n();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const props = defineProps({
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
});
const {
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
removeTab,
renameTabs
} = workspacesStore;
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
return {
addNotification,
selectedWorkspace,
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
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);
const {
getWorkspace,
refreshStructure,
changeBreadcrumbs,
setUnsavedChanges,
newTab,
removeTab
} = workspacesStore;
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 });
}
},
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.originalFunction = {
sql: this.customizations.triggerFunctionSql,
language: this.customizations.triggerFunctionlanguages.length ? this.customizations.triggerFunctionlanguages[0] : null,
name: ''
};
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
this.localFunction = JSON.parse(JSON.stringify(this.originalFunction));
isSaving.value = false;
};
setTimeout(() => {
this.resizeQueryEditor();
}, 50);
const clearChanges = () => {
localFunction.value = JSON.parse(JSON.stringify(originalFunction.value));
queryEditor.value.editor.session.setValue(localFunction.value.sql);
};
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
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();
}
};
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.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();
}
}
const onKey = (e: KeyboardEvent) => {
if (props.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.key === 's') { // CTRL + S
if (isChanged.value)
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>

View File

@ -11,20 +11,20 @@
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
<span>{{ t('word.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
:title="t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<span>{{ t('word.clear') }}</span>
</button>
</div>
<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>
</div>
</div>
@ -34,7 +34,7 @@
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<label class="form-label">{{ t('word.name') }}</label>
<input
ref="firstInput"
v-model="localView.name"
@ -45,19 +45,19 @@
</div>
<div class="column col-auto">
<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
v-model="localView.definer"
:options="users"
:option-label="(user) => user.value === '' ? $t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
:option-label="(user: any) => user.value === '' ? t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select"
/>
</div>
</div>
<div class="column col-auto mr-2">
<div v-if="workspace.customizations.viewSqlSecurity" class="form-group">
<label class="form-label">{{ $t('message.sqlSecurity') }}</label>
<label class="form-label">{{ t('message.sqlSecurity') }}</label>
<BaseSelect
v-model="localView.security"
:options="['DEFINER', 'INVOKER']"
@ -67,7 +67,7 @@
</div>
<div class="column col-auto mr-2">
<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
v-model="localView.algorithm"
:options="['UNDEFINED', 'MERGE', 'TEMPTABLE']"
@ -77,10 +77,10 @@
</div>
<div v-if="workspace.customizations.viewUpdateOption" class="column col-auto mr-2">
<div class="form-group">
<label class="form-label">{{ $t('message.updateOption') }}</label>
<label class="form-label">{{ t('message.updateOption') }}</label>
<BaseSelect
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'}]"
class="form-select"
/>
@ -90,7 +90,7 @@
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<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
v-show="isSelected"
ref="queryEditor"
@ -103,194 +103,170 @@
</div>
</template>
<script>
import { storeToRefs } from 'pinia';
<script setup lang="ts">
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 { useWorkspacesStore } from '@/stores/workspaces';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader.vue';
import QueryEditor from '@/components/QueryEditor.vue';
import Views from '@/ipc-api/Views';
import BaseSelect from '@/components/BaseSelect.vue';
export default {
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 { t } = useI18n();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const props = defineProps({
tabUid: String,
connection: Object,
tab: Object,
isSelected: Boolean,
schema: String
});
const {
getWorkspace,
refreshStructure,
setUnsavedChanges,
changeBreadcrumbs,
newTab,
removeTab,
renameTabs
} = workspacesStore;
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
return {
addNotification,
selectedWorkspace,
getWorkspace,
refreshStructure,
setUnsavedChanges,
changeBreadcrumbs,
newTab,
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 });
}
const {
getWorkspace,
refreshStructure,
setUnsavedChanges,
changeBreadcrumbs,
newTab,
removeTab
} = workspacesStore;
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 });
}
},
watch: {
isSelected (val) {
if (val) {
this.changeBreadcrumbs({ schema: this.schema, view: this.view });
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
setTimeout(() => {
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: ''
};
isSaving.value = false;
};
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(() => {
this.resizeQueryEditor();
}, 50);
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();
}
};
window.addEventListener('keydown', this.onKey);
},
mounted () {
if (this.isSelected)
this.changeBreadcrumbs({ schema: this.schema });
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();
}
}
const onKey = (e: KeyboardEvent) => {
if (props.isSelected) {
e.stopPropagation();
if (e.ctrlKey && e.key === 's') { // CTRL + S
if (isChanged.value)
saveChanges();
}
}
};
watch(() => props.isSelected, (val) => {
if (val) {
changeBreadcrumbs({ schema: props.schema, view: localView.value.name });
setTimeout(() => {
resizeQueryEditor();
}, 50);
}
});
watch(isChanged, (val) => {
setUnsavedChanges({ uid: props.connection.uid, tUid: props.tabUid, isChanged: val });
});
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>

View File

@ -11,16 +11,16 @@
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
<span>{{ t('word.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
:title="t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<span>{{ t('word.clear') }}</span>
</button>
<div class="divider-vert py-3" />
@ -31,15 +31,15 @@
@click="runFunctionCheck"
>
<i class="mdi mdi-24px mdi-play mr-1" />
<span>{{ $t('word.run') }}</span>
<span>{{ t('word.run') }}</span>
</button>
<button class="btn btn-dark btn-sm" @click="showParamsModal">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span>
<span>{{ t('word.parameters') }}</span>
</button>
</div>
<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>
</div>
</div>
@ -50,7 +50,7 @@
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.name') }}
{{ t('word.name') }}
</label>
<input
ref="firstInput"
@ -64,7 +64,7 @@
<div v-if="customizations.languages" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.language') }}
{{ t('word.language') }}
</label>
<BaseSelect
v-model="localFunction.language"
@ -76,13 +76,13 @@
<div v-if="customizations.definer" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.definer') }}
{{ t('word.definer') }}
</label>
<BaseSelect
v-model="localFunction.definer"
:options="[{value: '', name:$t('message.currentUser')}, ...workspace.users]"
:option-label="(user) => user.value === '' ? user.name : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
:options="[{value: '', name:t('message.currentUser')}, ...workspace.users]"
:option-label="(user: any) => user.value === '' ? user.name : `${user.name}@${user.host}`"
:option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select"
/>
</div>
@ -90,13 +90,13 @@
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.returns') }}
{{ t('word.returns') }}
</label>
<div class="input-group">
<BaseSelect
v-model="localFunction.returns"
class="form-select text-uppercase"
:options="[{ name: 'VOID' }, ...workspace.dataTypes]"
:options="[{ name: 'VOID' }, ...(workspace.dataTypes as any)]"
group-label="group"
group-values="types"
option-label="name"
@ -110,7 +110,7 @@
class="form-input"
type="number"
min="0"
:placeholder="$t('word.length')"
:placeholder="t('word.length')"
>
</div>
</div>
@ -118,7 +118,7 @@
<div v-if="customizations.comment" class="column">
<div class="form-group">
<label class="form-label">
{{ $t('word.comment') }}
{{ t('word.comment') }}
</label>
<input
v-model="localFunction.comment"
@ -130,7 +130,7 @@
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.sqlSecurity') }}
{{ t('message.sqlSecurity') }}
</label>
<BaseSelect
v-model="localFunction.security"
@ -142,7 +142,7 @@
<div v-if="customizations.functionDataAccess" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.dataAccess') }}
{{ t('message.dataAccess') }}
</label>
<BaseSelect
v-model="localFunction.dataAccess"
@ -155,7 +155,7 @@
<div class="form-group">
<label class="form-label d-invisible">.</label>
<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>
</div>
</div>
@ -163,7 +163,7 @@
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<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
v-show="isSelected"
ref="queryEditor"
@ -191,300 +191,273 @@
</div>
</template>
<script>
import { storeToRefs } from 'pinia';
<script setup lang="ts">
import { Component, computed, onBeforeUnmount, onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { Ace } from 'ace-builds';
import { useNotificationsStore } from '@/stores/notifications';
import { useWorkspacesStore } from '@/stores/workspaces';
import { uidGen } from 'common/libs/uidGen';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspaceTabPropsFunctionParamsModal from '@/components/WorkspaceTabPropsFunctionParamsModal';
import ModalAskParameters from '@/components/ModalAskParameters';
import BaseLoader from '@/components/BaseLoader.vue';
import QueryEditor from '@/components/QueryEditor.vue';
import WorkspaceTabPropsFunctionParamsModal from '@/components/WorkspaceTabPropsFunctionParamsModal.vue';
import ModalAskParameters from '@/components/ModalAskParameters.vue';
import Functions from '@/ipc-api/Functions';
import BaseSelect from '@/components/BaseSelect.vue';
import { useI18n } from 'vue-i18n';
import { AlterFunctionParams, FunctionInfos, FunctionParam } from 'common/interfaces/antares';
export default {
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 { t } = useI18n();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const props = defineProps({
tabUid: String,
connection: Object,
function: String,
isSelected: Boolean,
schema: String
});
const {
getWorkspace,
refreshStructure,
renameTabs,
newTab,
changeBreadcrumbs,
setUnsavedChanges
} = workspacesStore;
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
return {
addNotification,
selectedWorkspace,
getWorkspace,
refreshStructure,
renameTabs,
newTab,
changeBreadcrumbs,
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);
const {
getWorkspace,
refreshStructure,
renameTabs,
newTab,
changeBreadcrumbs,
setUnsavedChanges
} = workspacesStore;
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;
}
},
watch: {
async schema () {
if (this.isSelected) {
await this.getFunctionData();
this.$refs.queryEditor.editor.session.setValue(this.localFunction.sql);
this.lastFunction = this.function;
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
resizeQueryEditor();
isLoading.value = false;
};
const saveChanges = async () => {
if (isSaving.value) return;
isSaving.value = true;
const params = {
uid: props.connection.uid,
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
this.runFunction();
},
runFunction (params) {
if (!params) params = [];
getFunctionData();
}
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
let sql;
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(',')})`;
}
isSaving.value = false;
};
this.newTab({ uid: this.connection.uid, content: sql, type: 'query', autorun: true });
},
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();
}
}
const clearChanges = () => {
localFunction.value = JSON.parse(JSON.stringify(originalFunction.value));
queryEditor.value.editor.session.setValue(localFunction.value.sql);
};
const resizeQueryEditor = () => {
if (queryEditor.value) {
const footer = document.getElementById('footer');
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
editorHeight.value = size;
queryEditor.value.editor.resize();
}
};
const parametersUpdate = (parameters: FunctionParam[]) => {
localFunction.value = { ...localFunction.value, parameters };
};
const runFunctionCheck = () => {
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>

View File

@ -1,6 +1,6 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
:confirm-text="t('word.confirm')"
size="medium"
class="options-modal"
@confirm="confirmParametersChange"
@ -9,7 +9,7 @@
<template #header>
<div class="d-flex">
<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>
</template>
<template #body>
@ -20,16 +20,16 @@
<div class="d-flex">
<button class="btn btn-dark btn-sm d-flex" @click="addParameter">
<i class="mdi mdi-24px mdi-plus mr-1" />
<span>{{ $t('word.add') }}</span>
<span>{{ t('word.add') }}</span>
</button>
<button
class="btn btn-dark btn-sm d-flex ml-2 mr-0"
:title="$t('message.clearChanges')"
:title="t('message.clearChanges')"
:disabled="!isChanged"
@click.prevent="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<span>{{ t('word.clear') }}</span>
</button>
</div>
</div>
@ -55,7 +55,7 @@
<div class="tile-action">
<button
class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')"
:title="t('word.delete')"
@click.prevent="removeParameter(param._antares_id)"
>
<i class="mdi mdi-close" />
@ -74,7 +74,7 @@
>
<div class="form-group">
<label class="form-label col-3">
{{ $t('word.name') }}
{{ t('word.name') }}
</label>
<div class="column">
<input
@ -86,7 +86,7 @@
</div>
<div class="form-group">
<label class="form-label col-3">
{{ $t('word.type') }}
{{ t('word.type') }}
</label>
<div class="column">
<BaseSelect
@ -102,7 +102,7 @@
</div>
<div v-if="customizations.parametersLength" class="form-group">
<label class="form-label col-3">
{{ $t('word.length') }}
{{ t('word.length') }}
</label>
<div class="column">
<input
@ -115,7 +115,7 @@
</div>
<div v-if="customizations.functionContext" class="form-group">
<label class="form-label col-3">
{{ $t('word.context') }}
{{ t('word.context') }}
</label>
<div class="column">
<label class="form-radio">
@ -150,11 +150,11 @@
<i class="mdi mdi-dots-horizontal mdi-48px" />
</div>
<p class="empty-title h5">
{{ $t('message.thereAreNoParameters') }}
{{ t('message.thereAreNoParameters') }}
</p>
<div class="empty-action">
<button class="btn btn-primary" @click="addParameter">
{{ $t('message.createNewParameter') }}
{{ t('message.createNewParameter') }}
</button>
</div>
</div>
@ -164,113 +164,118 @@
</ConfirmModal>
</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 ConfirmModal from '@/components/BaseConfirmModal';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseSelect from '@/components/BaseSelect.vue';
export default {
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;
const { t } = useI18n();
if (this.parametersProxy.length)
this.resetSelectedID();
this.getModalInnerHeight();
window.addEventListener('resize', this.getModalInnerHeight);
const props = defineProps({
localParameters: {
type: Array,
default: () => []
},
unmounted () {
window.removeEventListener('resize', this.getModalInnerHeight);
},
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: ''
}];
func: String,
workspace: Object
});
if (this.parametersProxy.length === 1)
this.resetSelectedID();
const emit = defineEmits(['hide', 'parameters-update']);
setTimeout(() => {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60;
this.selectedParam = newUid;
}, 20);
},
removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param._antares_id !== uid);
const parametersPanel: Ref<HTMLDivElement> = ref(null);
const parametersProxy = ref([]);
const selectedParam = ref('');
const modalInnerHeight = ref(400);
const i = ref(1);
if (this.parametersProxy.length && this.selectedParam === uid)
this.resetSelectedID();
},
clearChanges () {
this.parametersProxy = JSON.parse(JSON.stringify(this.localParameters));
this.i = this.parametersProxy.length + 1;
const selectedParamObj = computed(() => {
return parametersProxy.value.find(param => param._antares_id === selectedParam.value);
});
if (!this.parametersProxy.some(param => param.name === this.selectedParam))
this.resetSelectedID();
},
resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._antares_id : '';
}
}
const isChanged = computed(() => {
return JSON.stringify(props.localParameters) !== JSON.stringify(parametersProxy.value);
});
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>
<style lang="scss" scoped>

View File

@ -11,16 +11,16 @@
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
<span>{{ t('word.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
:title="t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<span>{{ t('word.clear') }}</span>
</button>
<div class="divider-vert py-3" />
@ -31,15 +31,15 @@
@click="runRoutineCheck"
>
<i class="mdi mdi-24px mdi-play mr-1" />
<span>{{ $t('word.run') }}</span>
<span>{{ t('word.run') }}</span>
</button>
<button class="btn btn-dark btn-sm" @click="showParamsModal">
<i class="mdi mdi-24px mdi-dots-horizontal mr-1" />
<span>{{ $t('word.parameters') }}</span>
<span>{{ t('word.parameters') }}</span>
</button>
</div>
<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>
</div>
</div>
@ -50,7 +50,7 @@
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.name') }}
{{ t('word.name') }}
</label>
<input
ref="firstInput"
@ -64,7 +64,7 @@
<div v-if="customizations.languages" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.language') }}
{{ t('word.language') }}
</label>
<BaseSelect
v-model="localRoutine.language"
@ -76,13 +76,13 @@
<div v-if="customizations.definer" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('word.definer') }}
{{ t('word.definer') }}
</label>
<BaseSelect
v-model="localRoutine.definer"
:options="[{value: '', name:$t('message.currentUser')}, ...workspace.users]"
:option-label="(user) => user.value === '' ? user.name : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
:options="[{value: '', name: t('message.currentUser')}, ...workspace.users]"
:option-label="(user: any) => user.value === '' ? user.name : `${user.name}@${user.host}`"
:option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select"
/>
</div>
@ -90,7 +90,7 @@
<div v-if="customizations.comment" class="column">
<div class="form-group">
<label class="form-label">
{{ $t('word.comment') }}
{{ t('word.comment') }}
</label>
<input
v-model="localRoutine.comment"
@ -102,7 +102,7 @@
<div class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.sqlSecurity') }}
{{ t('message.sqlSecurity') }}
</label>
<BaseSelect
v-model="localRoutine.security"
@ -114,7 +114,7 @@
<div v-if="customizations.procedureDataAccess" class="column col-auto">
<div class="form-group">
<label class="form-label">
{{ $t('message.dataAccess') }}
{{ t('message.dataAccess') }}
</label>
<BaseSelect
v-model="localRoutine.dataAccess"
@ -127,7 +127,7 @@
<div class="form-group">
<label class="form-label d-invisible">.</label>
<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>
</div>
</div>
@ -135,7 +135,7 @@
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<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
v-show="isSelected"
ref="queryEditor"
@ -163,286 +163,272 @@
</div>
</template>
<script>
import { storeToRefs } from 'pinia';
<script setup lang="ts">
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 { useWorkspacesStore } from '@/stores/workspaces';
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 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';
export default {
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 { t } = useI18n();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const props = defineProps({
tabUid: String,
connection: Object,
routine: String,
isSelected: Boolean,
schema: String
});
const {
getWorkspace,
refreshStructure,
renameTabs,
newTab,
changeBreadcrumbs,
setUnsavedChanges
} = workspacesStore;
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
return {
addNotification,
selectedWorkspace,
getWorkspace,
refreshStructure,
renameTabs,
newTab,
changeBreadcrumbs,
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);
const {
getWorkspace,
refreshStructure,
renameTabs,
newTab,
changeBreadcrumbs,
setUnsavedChanges
} = workspacesStore;
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;
}
},
watch: {
async schema () {
if (this.isSelected) {
await this.getRoutineData();
this.$refs.queryEditor.editor.session.setValue(this.localRoutine.sql);
this.lastRoutine = this.routine;
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
resizeQueryEditor();
isLoading.value = false;
};
const saveChanges = async () => {
if (isSaving.value) return;
isSaving.value = true;
const params = {
uid: props.connection.uid 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
this.runRoutine();
},
runRoutine (params) {
if (!params) params = [];
getRoutineData();
}
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
let sql;
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(',')})`;
}
isSaving.value = false;
};
this.newTab({ uid: this.connection.uid, content: sql, type: 'query', autorun: true });
},
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();
}
}
const clearChanges = () => {
localRoutine.value = JSON.parse(JSON.stringify(originalRoutine.value));
queryEditor.value.editor.session.setValue(localRoutine.value.sql);
};
const resizeQueryEditor = () => {
if (queryEditor.value) {
const footer = document.getElementById('footer');
const size = window.innerHeight - queryEditor.value.$el.getBoundingClientRect().top - footer.offsetHeight;
editorHeight.value = size;
queryEditor.value.editor.resize();
}
};
const parametersUpdate = (parameters: FunctionParam[]) => {
localRoutine.value = { ...localRoutine.value, parameters };
};
const runRoutineCheck = () => {
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>

View File

@ -1,6 +1,6 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
:confirm-text="t('word.confirm')"
size="medium"
class="options-modal"
@confirm="confirmParametersChange"
@ -9,7 +9,7 @@
<template #header>
<div class="d-flex">
<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>
</template>
<template #body>
@ -20,16 +20,16 @@
<div class="d-flex">
<button class="btn btn-dark btn-sm d-flex" @click="addParameter">
<i class="mdi mdi-24px mdi-plus mr-1" />
<span>{{ $t('word.add') }}</span>
<span>{{ t('word.add') }}</span>
</button>
<button
class="btn btn-dark btn-sm d-flex ml-2 mr-0"
:title="$t('message.clearChanges')"
:title="t('message.clearChanges')"
:disabled="!isChanged"
@click.prevent="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<span>{{ t('word.clear') }}</span>
</button>
</div>
</div>
@ -55,7 +55,7 @@
<div class="tile-action">
<button
class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')"
:title="t('word.delete')"
@click.prevent="removeParameter(param._antares_id)"
>
<i class="mdi mdi-close" />
@ -74,7 +74,7 @@
>
<div class="form-group">
<label class="form-label col-3">
{{ $t('word.name') }}
{{ t('word.name') }}
</label>
<div class="column">
<input
@ -86,7 +86,7 @@
</div>
<div class="form-group">
<label class="form-label col-3">
{{ $t('word.type') }}
{{ t('word.type') }}
</label>
<div class="column">
<BaseSelect
@ -102,7 +102,7 @@
</div>
<div v-if="customizations.parametersLength" class="form-group">
<label class="form-label col-3">
{{ $t('word.length') }}
{{ t('word.length') }}
</label>
<div class="column">
<input
@ -115,7 +115,7 @@
</div>
<div v-if="customizations.procedureContext" class="form-group">
<label class="form-label col-3">
{{ $t('word.context') }}
{{ t('word.context') }}
</label>
<div class="column">
<label class="form-radio">
@ -150,11 +150,11 @@
<i class="mdi mdi-dots-horizontal mdi-48px" />
</div>
<p class="empty-title h5">
{{ $t('message.thereAreNoParameters') }}
{{ t('message.thereAreNoParameters') }}
</p>
<div class="empty-action">
<button class="btn btn-primary" @click="addParameter">
{{ $t('message.createNewParameter') }}
{{ t('message.createNewParameter') }}
</button>
</div>
</div>
@ -164,113 +164,118 @@
</ConfirmModal>
</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 ConfirmModal from '@/components/BaseConfirmModal';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseSelect from '@/components/BaseSelect.vue';
export default {
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;
const { t } = useI18n();
if (this.parametersProxy.length)
this.resetSelectedID();
this.getModalInnerHeight();
window.addEventListener('resize', this.getModalInnerHeight);
const props = defineProps({
localParameters: {
type: Array,
default: () => []
},
unmounted () {
window.removeEventListener('resize', this.getModalInnerHeight);
},
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: ''
}];
routine: String,
workspace: Object
});
if (this.parametersProxy.length === 1)
this.resetSelectedID();
const emit = defineEmits(['hide', 'parameters-update']);
setTimeout(() => {
this.$refs.parametersPanel.scrollTop = this.$refs.parametersPanel.scrollHeight + 60;
this.selectedParam = newUid;
}, 20);
},
removeParameter (uid) {
this.parametersProxy = this.parametersProxy.filter(param => param._antares_id !== uid);
const parametersPanel: Ref<HTMLDivElement> = ref(null);
const parametersProxy = ref([]);
const selectedParam = ref('');
const modalInnerHeight = ref(400);
const i = ref(1);
if (this.parametersProxy.length && this.selectedParam === uid)
this.resetSelectedID();
},
clearChanges () {
this.parametersProxy = JSON.parse(JSON.stringify(this.localParameters));
this.i = this.parametersProxy.length + 1;
const selectedParamObj = computed(() => {
return parametersProxy.value.find(param => param._antares_id === selectedParam.value);
});
if (!this.parametersProxy.some(param => param.name === this.selectedParam))
this.resetSelectedID();
},
resetSelectedID () {
this.selectedParam = this.parametersProxy.length ? this.parametersProxy[0]._antares_id : '';
}
}
const isChanged = computed(() => {
return JSON.stringify(props.localParameters) !== JSON.stringify(parametersProxy.value);
});
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>
<style lang="scss" scoped>

View File

@ -11,26 +11,26 @@
@click="saveChanges"
>
<i class="mdi mdi-24px mdi-content-save mr-1" />
<span>{{ $t('word.save') }}</span>
<span>{{ t('word.save') }}</span>
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
:title="t('message.clearChanges')"
@click="clearChanges"
>
<i class="mdi mdi-24px mdi-delete-sweep mr-1" />
<span>{{ $t('word.clear') }}</span>
<span>{{ t('word.clear') }}</span>
</button>
<div class="divider-vert py-3" />
<button class="btn btn-dark btn-sm" @click="showTimingModal">
<i class="mdi mdi-24px mdi-timer mr-1" />
<span>{{ $t('word.timing') }}</span>
<span>{{ t('word.timing') }}</span>
</button>
</div>
<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>
</div>
</div>
@ -40,7 +40,7 @@
<div class="columns">
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.name') }}</label>
<label class="form-label">{{ t('word.name') }}</label>
<input
v-model="localScheduler.name"
class="form-input"
@ -50,19 +50,19 @@
</div>
<div class="column col-auto">
<div class="form-group">
<label class="form-label">{{ $t('word.definer') }}</label>
<label class="form-label">{{ t('word.definer') }}</label>
<BaseSelect
v-model="localScheduler.definer"
:options="users"
:option-label="(user) => user.value === '' ? $t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
:option-label="(user: any) => user.value === '' ? t('message.currentUser') : `${user.name}@${user.host}`"
:option-track-by="(user: any) => user.value === '' ? '' : `\`${user.name}\`@\`${user.host}\``"
class="form-select"
/>
</div>
</div>
<div class="column">
<div class="form-group">
<label class="form-label">{{ $t('word.comment') }}</label>
<label class="form-label">{{ t('word.comment') }}</label>
<input
v-model="localScheduler.comment"
class="form-input"
@ -72,7 +72,7 @@
</div>
<div class="column">
<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">
<input
v-model="localScheduler.state"
@ -103,7 +103,7 @@
</div>
<div class="workspace-query-results column col-12 mt-2 p-relative">
<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
v-show="isSelected"
ref="queryEditor"
@ -122,245 +122,231 @@
/>
</div>
</template>
<script>
import { storeToRefs } from 'pinia';
<script setup lang="ts">
import { AlterEventParams, EventInfos } from 'common/interfaces/antares';
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 { useWorkspacesStore } from '@/stores/workspaces';
import BaseLoader from '@/components/BaseLoader';
import QueryEditor from '@/components/QueryEditor';
import WorkspaceTabPropsSchedulerTimingModal from '@/components/WorkspaceTabPropsSchedulerTimingModal';
import Schedulers from '@/ipc-api/Schedulers';
import BaseLoader from '@/components/BaseLoader.vue';
import QueryEditor from '@/components/QueryEditor.vue';
import WorkspaceTabPropsSchedulerTimingModal from '@/components/WorkspaceTabPropsSchedulerTimingModal.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import Schedulers from '@/ipc-api/Schedulers';
export default {
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 { t } = useI18n();
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const props = defineProps({
tabUid: String,
connection: Object,
scheduler: String,
isSelected: Boolean,
schema: String
});
const {
getWorkspace,
refreshStructure,
renameTabs,
newTab,
changeBreadcrumbs,
setUnsavedChanges
} = workspacesStore;
const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore();
return {
addNotification,
selectedWorkspace,
getWorkspace,
refreshStructure,
renameTabs,
newTab,
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);
const {
getWorkspace,
refreshStructure,
renameTabs,
changeBreadcrumbs,
setUnsavedChanges
} = workspacesStore;
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
},
users () {
const users = [{ value: '' }, ...this.workspace.users];
if (!this.isDefinerInUsers) {
const [name, host] = this.originalScheduler.definer.replaceAll('`', '').split('@');
users.unshift({ name, host });
}
const queryEditor: Ref<Component & {editor: Ace.Editor; $el: HTMLElement}> = ref(null);
const isLoading = ref(false);
const isSaving = ref(false);
const isTimingModal = ref(false);
const originalScheduler: Ref<EventInfos> = ref(null);
const localScheduler: Ref<EventInfos> = ref({} as EventInfos);
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;
}
},
watch: {
async schema () {
if (this.isSelected) {
await this.getSchedulerData();
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 });
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
setTimeout(() => {
this.resizeQueryEditor();
}, 200);
resizeQueryEditor();
isLoading.value = false;
};
if (this.lastScheduler !== this.scheduler)
this.getSchedulerData();
const saveChanges = async () => {
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 });
}
},
isChanged (val) {
this.setUnsavedChanges({ uid: this.connection.uid, tUid: this.tabUid, isChanged: val });
else
getSchedulerData();
}
},
async created () {
await this.getSchedulerData();
this.$refs.queryEditor.editor.session.setValue(this.localScheduler.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 getSchedulerData () {
if (!this.scheduler) return;
else
addNotification({ status: 'error', message: response });
}
catch (err) {
addNotification({ status: 'error', message: err.stack });
}
this.isLoading = true;
this.lastScheduler = this.scheduler;
isSaving.value = false;
};
const params = {
uid: this.connection.uid,
schema: this.schema,
scheduler: this.scheduler
};
const clearChanges = () => {
localScheduler.value = JSON.parse(JSON.stringify(originalScheduler.value));
queryEditor.value.editor.session.setValue(localScheduler.value.sql);
};
try {
const { status, response } = await Schedulers.getSchedulerInformations(params);
if (status === 'success') {
this.originalScheduler = response;
this.localScheduler = JSON.parse(JSON.stringify(this.originalScheduler));
this.sqlProxy = this.localScheduler.sql;
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
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();
}
};
this.resizeQueryEditor();
this.isLoading = false;
},
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
}
};
const showTimingModal = () => {
isTimingModal.value = true;
};
try {
const { status, response } = await Schedulers.alterScheduler(params);
const hideTimingModal = () => {
isTimingModal.value = false;
};
if (status === 'success') {
const oldName = this.originalScheduler.name;
const timingUpdate = (options: EventInfos) => {
localScheduler.value = options;
};
await this.refreshStructure(this.connection.uid);
if (oldName !== this.localScheduler.name) {
this.renameTabs({
uid: this.connection.uid,
schema: this.schema,
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();
}
}
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 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>

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