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

Compare commits

..

18 Commits

Author SHA1 Message Date
d20414b692 chore(release): 0.1.0 2021-03-21 13:01:35 +01:00
22a8c25717 fix: update or delete rows with more than one primary key 2021-03-21 13:00:27 +01:00
db47b4040a fix(PostgreSQL): issue getting foreign keys informations 2021-03-21 11:51:22 +01:00
e89911b185 fix: remove last char from datetime and time if is a dot 2021-03-20 16:29:56 +01:00
fccfe92453 fix(PostgreSQL): various issues in query results 2021-03-19 18:49:26 +01:00
d465e18dba feat(PostgreSQL): support to microseconds 2021-03-18 15:56:52 +01:00
ffb1712a59 feat(UI): support to boolean fields 2021-03-18 12:59:46 +01:00
9f6a183d9b fix(PostgreSQL): single quote escape 2021-03-18 12:30:06 +01:00
1f80a64fe1 feat(PostgreSQL): insert and edit blob fields 2021-03-18 11:09:50 +01:00
fc651149b9 feat(PostgreSQL): edit array and text search fields 2021-03-17 18:06:17 +01:00
964570247f feat(PostgreSQL): database in connection parameters 2021-03-17 16:51:26 +01:00
8a6c59f7ce fix: schema content not loaded if selected with right click 2021-03-17 11:57:47 +01:00
4d844fe2c9 refactor: rename database to schema 2021-03-17 11:15:14 +01:00
d892fa6fb3 feat(PostgreSQL): partial postgre implementation 2021-03-16 18:42:03 +01:00
8c9e4f6e96 chore: update issue templates 2021-03-16 15:55:11 +01:00
966ca60c89 chore: update README.md 2021-03-16 15:51:21 +01:00
9bbe218f90 chore: update README.md 2021-03-14 15:38:55 +01:00
a1c6be372b fix(MySQL): handle NEWDECIMAL data type 2021-03-14 15:04:20 +01:00
46 changed files with 2420 additions and 221 deletions

View File

@@ -3,7 +3,7 @@ name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ''
labels: '' labels: ''
assignees: '' assignees: Fabio286
--- ---
@@ -25,13 +25,6 @@ If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):** **Desktop (please complete the following information):**
- OS: [e.g. iOS] - OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22] - Version [e.g. 22]
**Additional context** **Additional context**

View File

@@ -3,7 +3,7 @@ name: Feature request
about: Suggest an idea for this project about: Suggest an idea for this project
title: '' title: ''
labels: '' labels: ''
assignees: '' assignees: Fabio286
--- ---

View File

@@ -2,6 +2,29 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [0.1.0](https://github.com/Fabio286/antares/compare/v0.0.20...v0.1.0) (2021-03-21)
### Features
* **PostgreSQL:** database in connection parameters ([9645702](https://github.com/Fabio286/antares/commit/964570247ff5b7b8317419730eec5bed4f0f0580))
* **PostgreSQL:** edit array and text search fields ([fc65114](https://github.com/Fabio286/antares/commit/fc651149b95399c52d2d63e946731e9c1b0303a9))
* **PostgreSQL:** insert and edit blob fields ([1f80a64](https://github.com/Fabio286/antares/commit/1f80a64fe1400baacca26f1a762c5aeb4ef6350d))
* **PostgreSQL:** partial postgre implementation ([d892fa6](https://github.com/Fabio286/antares/commit/d892fa6fb3e86fbb96887d8eb67319ae855260a1))
* **PostgreSQL:** support to microseconds ([d465e18](https://github.com/Fabio286/antares/commit/d465e18dba8ea3aa00726e33f9b1f70ca4c0683c))
* **UI:** support to boolean fields ([ffb1712](https://github.com/Fabio286/antares/commit/ffb1712a593d1421793011e48a17369b884ea3c0))
### Bug Fixes
* update or delete rows with more than one primary key ([22a8c25](https://github.com/Fabio286/antares/commit/22a8c25717a4d4b285855426098a3a2846ce7448))
* **MySQL:** handle NEWDECIMAL data type ([a1c6be3](https://github.com/Fabio286/antares/commit/a1c6be372b570cf13e89ef7ecf9b7a7c033a9293))
* **PostgreSQL:** issue getting foreign keys informations ([db47b40](https://github.com/Fabio286/antares/commit/db47b4040a5282a6ac0711b1926c4c2ac867999e))
* remove last char from datetime and time if is a dot ([e89911b](https://github.com/Fabio286/antares/commit/e89911b1851c19813d4acf2c79adfbc2ac7c1112))
* **PostgreSQL:** single quote escape ([9f6a183](https://github.com/Fabio286/antares/commit/9f6a183d9b293dfe9ad9f3759f2375f05f37db8e))
* **PostgreSQL:** various issues in query results ([fccfe92](https://github.com/Fabio286/antares/commit/fccfe92453325cd54c0331cc5670af0a56822c5b))
* schema content not loaded if selected with right click ([8a6c59f](https://github.com/Fabio286/antares/commit/8a6c59f7ce7d051315b04cea38a96e4739b5b9d3))
### [0.0.20](https://github.com/Fabio286/antares/compare/v0.0.19...v0.0.20) (2021-03-13) ### [0.0.20](https://github.com/Fabio286/antares/compare/v0.0.19...v0.0.20) (2021-03-13)

View File

@@ -1,10 +1,10 @@
<p align="center"> <p align="center">
<img width="800" src="https://raw.githubusercontent.com/Fabio286/antares/master/docs/screen-alpha.png"> <img width="800" src="https://raw.githubusercontent.com/Fabio286/antares/master/docs/gh-logo.png">
</p> </p>
# Antares SQL Client # Antares SQL Client
![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) [![Build Status](https://travis-ci.com/Fabio286/antares.svg?branch=master)](https://travis-ci.com/Fabio286/antares) ![GitHub All Releases](https://img.shields.io/github/downloads/fabio286/antares/total) ![GitHub](https://img.shields.io/github/license/fabio286/antares) ![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) [![Build Status](https://travis-ci.com/Fabio286/antares.svg?branch=master)](https://travis-ci.com/Fabio286/antares) ![GitHub All Releases](https://img.shields.io/github/downloads/fabio286/antares/total) ![GitHub](https://img.shields.io/github/license/fabio286/antares) [![antares](https://snapcraft.io/antares/badge.svg)](https://snapcraft.io/antares) [![antares](https://snapcraft.io/antares/trending.svg?name=0)](https://snapcraft.io/antares)
Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers. Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers.
My target is to support as many databases as possible, and all major operating systems, including the ARM versions. My target is to support as many databases as possible, and all major operating systems, including the ARM versions.
@@ -85,7 +85,7 @@ Depending on your distribution, you will need to run the following command:
- [x] Windows - [x] Windows
- [x] Linux - [x] Linux
- [x] MacOS (i need feedbacks) - [x] MacOS (not tested due lack of hardware)
#### • ARM #### • ARM

BIN
docs/gh-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -1,7 +1,7 @@
{ {
"name": "antares", "name": "antares",
"productName": "Antares", "productName": "Antares",
"version": "0.0.20", "version": "0.1.0",
"description": "A cross-platform easy to use SQL client.", "description": "A cross-platform easy to use SQL client.",
"license": "MIT", "license": "MIT",
"repository": "https://github.com/Fabio286/antares.git", "repository": "https://github.com/Fabio286/antares.git",
@@ -71,7 +71,9 @@
"moment": "^2.29.1", "moment": "^2.29.1",
"mssql": "^6.2.3", "mssql": "^6.2.3",
"mysql2": "^2.2.5", "mysql2": "^2.2.5",
"node-sql-parser": "^3.1.0",
"pg": "^8.5.1", "pg": "^8.5.1",
"pgsql-ast-parser": "^7.0.2",
"source-map-support": "^0.5.16", "source-map-support": "^0.5.16",
"spectre.css": "^0.5.9", "spectre.css": "^0.5.9",
"v-mask": "^2.2.4", "v-mask": "^2.2.4",

View File

@@ -0,0 +1,41 @@
module.exports = {
// Defaults
defaultPort: null,
defaultUser: null,
defaultDatabase: null,
// Core
database: false,
collations: false,
engines: false,
// Tools
processesList: false,
usersManagement: false,
variables: false,
// Structure
schemas: false,
tables: false,
views: false,
triggers: false,
routines: false,
functions: false,
schedulers: false,
// Settings
tableAdd: false,
viewAdd: false,
triggerAdd: false,
routineAdd: false,
functionAdd: false,
schedulerAdd: false,
databaseEdit: false,
schemaEdit: false,
tableSettings: false,
viewSettings: false,
triggerSettings: false,
routineSettings: false,
functionSettings: false,
schedulerSettings: false,
indexes: false,
foreigns: false,
sortableFields: false,
zerofill: false
};

View File

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

View File

@@ -0,0 +1,40 @@
const defaults = require('./defaults');
module.exports = {
...defaults,
// Defaults
defaultPort: 3306,
defaultUser: 'root',
defaultDatabase: null,
// Core
collations: true,
engines: true,
// Tools
processesList: true,
// Structure
schemas: true,
tables: true,
views: true,
triggers: true,
routines: true,
functions: true,
schedulers: true,
// Settings
tableAdd: true,
viewAdd: true,
triggerAdd: true,
routineAdd: true,
functionAdd: true,
schedulerAdd: true,
schemaEdit: true,
tableSettings: true,
viewSettings: true,
triggerSettings: true,
routineSettings: true,
functionSettings: true,
schedulerSettings: true,
indexes: true,
foreigns: true,
sortableFields: true,
zerofill: true
};

View File

@@ -0,0 +1,31 @@
const defaults = require('./defaults');
module.exports = {
...defaults,
// Defaults
defaultPort: 5432,
defaultUser: 'postgres',
defaultDatabase: 'postgres',
// Core
database: true,
// Tools
processesList: true,
// Structure
tables: true,
views: false,
triggers: false,
routines: false,
functions: false,
schedulers: false,
// Settings
databaseEdit: false,
tableSettings: false,
viewSettings: false,
triggerSettings: false,
routineSettings: false,
functionSettings: false,
schedulerSettings: false,
indexes: true,
foreigns: true,
sortableFields: false
};

View File

@@ -0,0 +1,297 @@
module.exports = [
{
group: 'integer',
types: [
{
name: 'SMALLINT',
length: true,
unsigned: true
},
{
name: 'INTEGER',
length: true,
unsigned: true
},
{
name: 'BIGINT',
length: true,
unsigned: true
},
{
name: 'DECIMAL',
length: true,
unsigned: true
},
{
name: 'NUMERIC',
length: true,
unsigned: true
},
{
name: 'SMALLSERIAL',
length: true,
unsigned: true
},
{
name: 'SERIAL',
length: true,
unsigned: true
},
{
name: 'BIGSERIAL',
length: true,
unsigned: true
}
]
},
{
group: 'float',
types: [
{
name: 'REAL',
length: true,
unsigned: true
},
{
name: 'DOUBLE PRECISION',
length: true,
unsigned: true
}
]
},
{
group: 'monetary',
types: [
{
name: 'money',
length: true,
unsigned: true
}
]
},
{
group: 'string',
types: [
{
name: 'CHARACTER VARYING',
length: true,
unsigned: false
},
{
name: 'CHAR',
length: false,
unsigned: false
},
{
name: 'CHARACTER',
length: false,
unsigned: false
},
{
name: 'TEXT',
length: false,
unsigned: false
},
{
name: '"CHAR"',
length: false,
unsigned: false
},
{
name: 'NAME',
length: false,
unsigned: false
}
]
},
{
group: 'binary',
types: [
{
name: 'BYTEA',
length: true,
unsigned: false
}
]
},
{
group: 'time',
types: [
{
name: 'TIMESTAMP WITHOUT TIME ZONE',
length: false,
unsigned: false
},
{
name: 'TIMESTAMP WITH TIME ZONE',
length: false,
unsigned: false
},
{
name: 'DATE',
length: true,
unsigned: false
},
{
name: 'TIME',
length: true,
unsigned: false
},
{
name: 'TIME WITH TIME ZONE',
length: true,
unsigned: false
},
{
name: 'INTERVAL',
length: false,
unsigned: false
}
]
},
{
group: 'boolean',
types: [
{
name: 'BOOLEAN',
length: false,
unsigned: false
}
]
},
{
group: 'geometric',
types: [
{
name: 'POINT',
length: false,
unsigned: false
},
{
name: 'LINE',
length: false,
unsigned: false
},
{
name: 'LSEG',
length: false,
unsigned: false
},
{
name: 'BOX',
length: false,
unsigned: false
},
{
name: 'PATH',
length: false,
unsigned: false
},
{
name: 'POLYGON',
length: false,
unsigned: false
},
{
name: 'CIRCLE',
length: false,
unsigned: false
}
]
},
{
group: 'network',
types: [
{
name: 'CIDR',
length: false,
unsigned: false
},
{
name: 'INET',
length: false,
unsigned: false
},
{
name: 'MACADDR',
length: false,
unsigned: false
},
{
name: 'MACADDR8',
length: false,
unsigned: false
}
]
},
{
group: 'bit',
types: [
{
name: 'BIT',
length: false,
unsigned: false
},
{
name: 'BIT VARYING',
length: false,
unsigned: false
}
]
},
{
group: 'text search',
types: [
{
name: 'TSVECTOR',
length: false,
unsigned: false
},
{
name: 'TSQUERY',
length: false,
unsigned: false
}
]
},
{
group: 'uuid',
types: [
{
name: 'UUID',
length: false,
unsigned: false
}
]
},
{
group: 'xml',
types: [
{
name: 'XML',
length: false,
unsigned: false
}
]
},
{
group: 'json',
types: [
{
name: 'JSON',
length: false,
unsigned: false
},
{
name: 'JSONB',
length: false,
unsigned: false
},
{
name: 'JSONPATH',
length: false,
unsigned: false
}
]
}
];

View File

@@ -1 +0,0 @@
module.exports = [];

View File

@@ -1,13 +1,75 @@
export const TEXT = ['CHAR', 'VARCHAR']; export const TEXT = [
export const LONG_TEXT = ['TEXT', 'MEDIUMTEXT', 'LONGTEXT']; 'CHAR',
'VARCHAR',
'CHARACTER',
'CHARACTER VARYING'
];
export const LONG_TEXT = [
'TEXT',
'MEDIUMTEXT',
'LONGTEXT'
];
export const NUMBER = ['INT', 'TINYINT', 'SMALLINT', 'MEDIUMINT', 'BIGINT', 'DECIMAL', 'BOOL']; export const ARRAY = [
export const FLOAT = ['FLOAT', 'DOUBLE']; 'ARRAY',
'ANYARRAY'
];
export const TEXT_SEARCH = [
'TSVECTOR',
'TSQUERY'
];
export const NUMBER = [
'INT',
'TINYINT',
'SMALLINT',
'MEDIUMINT',
'BIGINT',
'DECIMAL',
'NUMERIC',
'INTEGER',
'SMALLSERIAL',
'SERIAL',
'BIGSERIAL',
'OID',
'XID'
];
export const FLOAT = [
'FLOAT',
'DOUBLE',
'REAL',
'DOUBLE PRECISION',
'MONEY'
];
export const BOOLEAN = [
'BOOL',
'BOOLEAN'
];
export const DATE = ['DATE']; export const DATE = ['DATE'];
export const TIME = ['TIME']; export const TIME = [
export const DATETIME = ['DATETIME', 'TIMESTAMP']; 'TIME',
'TIME WITH TIME ZONE'
];
export const DATETIME = [
'DATETIME',
'TIMESTAMP',
'TIMESTAMP WITHOUT TIME ZONE',
'TIMESTAMP WITH TIME ZONE'
];
export const BLOB = ['BLOB', 'TINYBLOB', 'MEDIUMBLOB', 'LONGBLOB']; export const BLOB = [
'BLOB',
'TINYBLOB',
'MEDIUMBLOB',
'LONGBLOB',
'BYTEA'
];
export const BIT = ['BIT']; export const BIT = [
'BIT',
'BIT VARYING'
];

View File

@@ -0,0 +1,5 @@
module.exports = [
'PRIMARY',
'KEY',
'UNIQUE'
];

View File

@@ -11,6 +11,9 @@ export default connections => {
password: conn.password password: conn.password
}; };
if (conn.database)
params.database = conn.database;
if (conn.ssl) { if (conn.ssl) {
params.ssl = { params.ssl = {
key: conn.key ? fs.readFileSync(conn.key) : null, key: conn.key ? fs.readFileSync(conn.key) : null,
@@ -50,6 +53,9 @@ export default connections => {
password: conn.password password: conn.password
}; };
if (conn.database)
params.database = conn.database;
if (conn.ssl) { if (conn.ssl) {
params.ssl = { params.ssl = {
key: conn.key ? fs.readFileSync(conn.key) : null, key: conn.key ? fs.readFileSync(conn.key) : null,
@@ -59,13 +65,13 @@ export default connections => {
}; };
} }
const connection = ClientsFactory.getConnection({
client: conn.client,
params,
poolSize: 1
});
try { try {
const connection = ClientsFactory.getConnection({
client: conn.client,
params,
poolSize: 1
});
await connection.connect(); await connection.connect();
const structure = await connection.getStructure(new Set()); const structure = await connection.getStructure(new Set());

View File

@@ -7,7 +7,7 @@ import functions from './functions';
import schedulers from './schedulers'; import schedulers from './schedulers';
import updates from './updates'; import updates from './updates';
import application from './application'; import application from './application';
import database from './database'; import schema from './schema';
import users from './users'; import users from './users';
const connections = {}; const connections = {};
@@ -20,7 +20,7 @@ export default () => {
routines(connections); routines(connections);
functions(connections); functions(connections);
schedulers(connections); schedulers(connections);
database(connections); schema(connections);
users(connections); users(connections);
updates(); updates();
application(); application();

View File

@@ -2,10 +2,9 @@
import { ipcMain } from 'electron'; import { ipcMain } from 'electron';
export default connections => { export default connections => {
ipcMain.handle('create-database', async (event, params) => { ipcMain.handle('create-schema', async (event, params) => {
try { try {
const query = `CREATE DATABASE \`${params.name}\` COLLATE ${params.collation}`; await connections[params.uid].createSchema(params);
await connections[params.uid].raw(query);
return { status: 'success' }; return { status: 'success' };
} }
@@ -14,10 +13,9 @@ export default connections => {
} }
}); });
ipcMain.handle('update-database', async (event, params) => { ipcMain.handle('update-schema', async (event, params) => {
try { try {
const query = `ALTER DATABASE \`${params.name}\` COLLATE ${params.collation}`; await connections[params.uid].alterSchema(params);
await connections[params.uid].raw(query);
return { status: 'success' }; return { status: 'success' };
} }
@@ -26,10 +24,9 @@ export default connections => {
} }
}); });
ipcMain.handle('delete-database', async (event, params) => { ipcMain.handle('delete-schema', async (event, params) => {
try { try {
const query = `DROP DATABASE \`${params.database}\``; await connections[params.uid].dropSchema(params);
await connections[params.uid].raw(query);
return { status: 'success' }; return { status: 'success' };
} }
@@ -38,10 +35,9 @@ export default connections => {
} }
}); });
ipcMain.handle('get-database-collation', async (event, params) => { // TODO: move to mysql class ipcMain.handle('get-schema-collation', async (event, params) => {
try { try {
const query = `SELECT \`DEFAULT_COLLATION_NAME\` FROM \`information_schema\`.\`SCHEMATA\` WHERE \`SCHEMA_NAME\`='${params.database}'`; const collation = await connections[params.uid].getDatabaseCollation(params);
const collation = await connections[params.uid].raw(query);
return { status: 'success', response: collation.rows.length ? collation.rows[0].DEFAULT_COLLATION_NAME : '' }; return { status: 'success', response: collation.rows.length ? collation.rows[0].DEFAULT_COLLATION_NAME : '' };
} }

View File

@@ -2,7 +2,7 @@ import { ipcMain } from 'electron';
import faker from 'faker'; import faker from 'faker';
import moment from 'moment'; import moment from 'moment';
import { sqlEscaper } from 'common/libs/sqlEscaper'; import { sqlEscaper } from 'common/libs/sqlEscaper';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes';
import fs from 'fs'; import fs from 'fs';
export default (connections) => { export default (connections) => {
@@ -59,23 +59,56 @@ export default (connections) => {
}); });
ipcMain.handle('update-table-cell', async (event, params) => { ipcMain.handle('update-table-cell', async (event, params) => {
try { try { // TODO: move to client classes
let escapedParam; let escapedParam;
let reload = false; let reload = false;
const id = typeof params.id === 'number' ? params.id : `"${params.id}"`; const id = typeof params.id === 'number' ? params.id : `"${params.id}"`;
if ([...NUMBER, ...FLOAT].includes(params.type)) if ([...NUMBER, ...FLOAT].includes(params.type))
escapedParam = params.content; escapedParam = params.content;
else if ([...TEXT, ...LONG_TEXT].includes(params.type)) else if ([...TEXT, ...LONG_TEXT].includes(params.type)) {
escapedParam = `"${sqlEscaper(params.content)}"`; switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = `"${sqlEscaper(params.content)}"`;
break;
case 'pg':
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
break;
}
}
else if (ARRAY.includes(params.type))
escapedParam = `'${params.content}'`;
else if (TEXT_SEARCH.includes(params.type))
escapedParam = `'${params.content.replaceAll('\'', '\'\'')}'`;
else if (BLOB.includes(params.type)) { else if (BLOB.includes(params.type)) {
if (params.content) { if (params.content) {
const fileBlob = fs.readFileSync(params.content); let fileBlob;
escapedParam = `0x${fileBlob.toString('hex')}`;
switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
fileBlob = fs.readFileSync(params.content);
escapedParam = `0x${fileBlob.toString('hex')}`;
break;
case 'pg':
fileBlob = fs.readFileSync(params.content);
escapedParam = `decode('${fileBlob.toString('hex')}', 'hex')`;
break;
}
reload = true; reload = true;
} }
else else {
escapedParam = '""'; switch (connections[params.uid]._client) {
case 'mysql':
case 'maria':
escapedParam = '\'\'';
break;
case 'pg':
escapedParam = 'decode(\'\', \'hex\')';
break;
}
}
} }
else if ([...BIT].includes(params.type)) { else if ([...BIT].includes(params.type)) {
escapedParam = `b'${sqlEscaper(params.content)}'`; escapedParam = `b'${sqlEscaper(params.content)}'`;
@@ -84,32 +117,33 @@ export default (connections) => {
else if (params.content === null) else if (params.content === null)
escapedParam = 'NULL'; escapedParam = 'NULL';
else else
escapedParam = `"${sqlEscaper(params.content)}"`; escapedParam = `'${sqlEscaper(params.content)}'`;
if (params.primary) { if (params.primary) { // TODO: handle multiple primary
await connections[params.uid] await connections[params.uid]
.update({ [params.field]: `= ${escapedParam}` }) .update({ [params.field]: `= ${escapedParam}` })
.schema(params.schema) .schema(params.schema)
.from(params.table) .from(params.table)
.where({ [params.primary]: `= ${id}` }) .where({ [params.primary]: `= ${id}` })
.limit(1)
.run(); .run();
} }
else { else {
const { row } = params; const { orgRow } = params;
reload = true; reload = true;
for (const key in row) { for (const key in orgRow) {
if (typeof row[key] === 'string') if (typeof orgRow[key] === 'string')
row[key] = `'${row[key]}'`; orgRow[key] = `'${orgRow[key]}'`;
row[key] = `= ${row[key]}`; orgRow[key] = `= ${orgRow[key]}`;
} }
await connections[params.uid] await connections[params.uid]
.schema(params.schema) .schema(params.schema)
.update({ [params.field]: `= ${escapedParam}` }) .update({ [params.field]: `= ${escapedParam}` })
.from(params.table) .from(params.table)
.where(row) .where(orgRow)
.limit(1) .limit(1)
.run(); .run();
} }
@@ -167,7 +201,7 @@ export default (connections) => {
}); });
ipcMain.handle('insert-table-rows', async (event, params) => { ipcMain.handle('insert-table-rows', async (event, params) => {
try { try { // TODO: move to client classes
const insertObj = {}; const insertObj = {};
for (const key in params.row) { for (const key in params.row) {
const type = params.fields[key]; const type = params.fields[key];
@@ -176,19 +210,46 @@ export default (connections) => {
if (params.row[key] === null) if (params.row[key] === null)
escapedParam = 'NULL'; escapedParam = 'NULL';
else if ([...NUMBER, ...FLOAT].includes(type)) else if ([...NUMBER, ...FLOAT].includes(type))
escapedParam = params.row[key]; escapedParam = +params.row[key];
else if ([...TEXT, ...LONG_TEXT].includes(type)) else if ([...TEXT, ...LONG_TEXT].includes(type)) {
escapedParam = `"${sqlEscaper(params.row[key])}"`; switch (connections[params.uid]._client) {
else if (BLOB.includes(type)) { case 'mysql':
if (params.row[key]) { case 'maria':
const fileBlob = fs.readFileSync(params.row[key]); escapedParam = `"${sqlEscaper(params.row[key].value)}"`;
escapedParam = `0x${fileBlob.toString('hex')}`; 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;
}
} }
else
escapedParam = '""';
} }
else
escapedParam = `"${sqlEscaper(params.row[key])}"`;
insertObj[key] = escapedParam; insertObj[key] = escapedParam;
} }
@@ -209,7 +270,7 @@ export default (connections) => {
}); });
ipcMain.handle('insert-table-fake-rows', async (event, params) => { ipcMain.handle('insert-table-fake-rows', async (event, params) => {
try { try { // TODO: move to client classes
const rows = []; const rows = [];
for (let i = 0; i < +params.repeat; i++) { for (let i = 0; i < +params.repeat; i++) {
@@ -224,20 +285,49 @@ export default (connections) => {
escapedParam = 'NULL'; escapedParam = 'NULL';
else if ([...NUMBER, ...FLOAT].includes(type)) else if ([...NUMBER, ...FLOAT].includes(type))
escapedParam = params.row[key].value; escapedParam = params.row[key].value;
else if ([...TEXT, ...LONG_TEXT].includes(type)) else if ([...TEXT, ...LONG_TEXT].includes(type)) {
escapedParam = `"${sqlEscaper(params.row[key].value)}"`; 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)) { else if (BLOB.includes(type)) {
if (params.row[key].value) { if (params.row[key].value) {
const fileBlob = fs.readFileSync(params.row[key].value); let fileBlob;
escapedParam = `0x${fileBlob.toString('hex')}`;
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;
}
} }
else
escapedParam = '""';
} }
else if (BIT.includes(type)) else if (BIT.includes(type))
escapedParam = `b'${sqlEscaper(params.row[key].value)}'`; escapedParam = `b'${sqlEscaper(params.row[key].value)}'`;
else else
escapedParam = `"${sqlEscaper(params.row[key].value)}"`; escapedParam = `'${sqlEscaper(params.row[key].value)}'`;
insertObj[key] = escapedParam; insertObj[key] = escapedParam;
} }
@@ -261,10 +351,10 @@ export default (connections) => {
if (typeof fakeValue === 'string') { if (typeof fakeValue === 'string') {
if (params.row[key].length) if (params.row[key].length)
fakeValue = fakeValue.substr(0, params.row[key].length); fakeValue = fakeValue.substr(0, params.row[key].length);
fakeValue = `"${sqlEscaper(fakeValue)}"`; fakeValue = `'${sqlEscaper(fakeValue)}'`;
} }
else if ([...DATE, ...DATETIME].includes(type)) else if ([...DATE, ...DATETIME].includes(type))
fakeValue = `"${moment(fakeValue).format('YYYY-MM-DD HH:mm:ss.SSSSSS')}"`; fakeValue = `'${moment(fakeValue).format('YYYY-MM-DD HH:mm:ss.SSSSSS')}'`;
insertObj[key] = fakeValue; insertObj[key] = fakeValue;
} }
@@ -289,13 +379,13 @@ export default (connections) => {
ipcMain.handle('get-foreign-list', async (event, { uid, schema, table, column, description }) => { ipcMain.handle('get-foreign-list', async (event, { uid, schema, table, column, description }) => {
try { try {
const query = connections[uid] const query = connections[uid]
.select(`${column} AS foreignColumn`) .select(`${column} AS foreign_column`)
.schema(schema) .schema(schema)
.from(table) .from(table)
.orderBy('foreignColumn ASC'); .orderBy('foreign_column ASC');
if (description) if (description)
query.select(`LEFT(${description}, 20) AS foreignDescription`); query.select(`LEFT(${description}, 20) AS foreign_description`);
const results = await query.run(); const results = await query.run();

View File

@@ -1,5 +1,6 @@
'use strict'; 'use strict';
import { MySQLClient } from './clients/MySQLClient'; import { MySQLClient } from './clients/MySQLClient';
import { PostgreSQLClient } from './clients/PostgreSQLClient';
export class ClientsFactory { export class ClientsFactory {
/** /**
@@ -20,8 +21,10 @@ export class ClientsFactory {
case 'mysql': case 'mysql':
case 'maria': case 'maria':
return new MySQLClient(args); return new MySQLClient(args);
case 'pg':
return new PostgreSQLClient(args);
default: default:
return new Error(`Unknown database client: ${args.client}`); throw new Error(`Unknown database client: ${args.client}`);
} }
} }
} }

View File

@@ -404,6 +404,44 @@ export class MySQLClient extends AntaresCore {
}); });
} }
/**
* CREATE DATABASE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async createSchema (params) {
return await this.raw(`CREATE DATABASE \`${params.name}\` COLLATE ${params.collation}`);
}
/**
* ALTER DATABASE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async alterSchema (params) {
return await this.raw(`ALTER DATABASE \`${params.name}\` COLLATE ${params.collation}`);
}
/**
* DROP DATABASE
*
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async dropSchema (params) {
return await this.raw(`DROP DATABASE \`${params.database}\``);
}
/**
* @returns {Array.<Object>} parameters
* @memberof MySQLClient
*/
async getDatabaseCollation (params) {
return await this.raw(`SELECT \`DEFAULT_COLLATION_NAME\` FROM \`information_schema\`.\`SCHEMATA\` WHERE \`SCHEMA_NAME\`='${params.database}'`);
}
/** /**
* SHOW CREATE VIEW * SHOW CREATE VIEW
* *
@@ -1281,7 +1319,7 @@ export class MySQLClient extends AntaresCore {
const response = await this.getTableColumns(paramObj); const response = await this.getTableColumns(paramObj);
remappedFields = remappedFields.map(field => { remappedFields = remappedFields.map(field => {
const detailedField = response.find(f => f.name === field.name); const detailedField = response.find(f => f.name === field.name);
if (detailedField && field.orgTable === paramObj.table && field.schema === paramObj.schema && detailedField.name === field.orgName) if (detailedField && field.orgTable === paramObj.table && field.schema === paramObj.schema)
field = { ...detailedField, ...field }; field = { ...detailedField, ...field };
return field; return field;
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -11,11 +11,11 @@
</option> </option>
<option <option
v-for="row in foreignList" v-for="row in foreignList"
:key="row.foreignColumn" :key="row.foreign_column"
:value="row.foreignColumn" :value="row.foreign_column"
:selected="row.foreignColumn === value" :selected="row.foreign_column === value"
> >
{{ row.foreignColumn }} {{ 'foreignDescription' in row ? ` - ${row.foreignDescription}` : '' | cutText }} {{ row.foreign_column }} {{ 'foreign_description' in row ? ` - ${row.foreign_description}` : '' | cutText }}
</option> </option>
</select> </select>
</template> </template>
@@ -51,11 +51,11 @@ export default {
}), }),
isValidDefault () { isValidDefault () {
if (!this.foreignList.length) return true; if (!this.foreignList.length) return true;
return this.foreignList.some(foreign => foreign.foreignColumn.toString() === this.value.toString()); return this.foreignList.some(foreign => foreign.foreign_column.toString() === this.value.toString());
} }
}, },
async created () { async created () {
let firstTextField; let foreignDesc;
const params = { const params = {
uid: this.selectedWorkspace, uid: this.selectedWorkspace,
schema: this.keyUsage.refSchema, schema: this.keyUsage.refSchema,
@@ -64,8 +64,10 @@ export default {
try { // Field data try { // Field data
const { status, response } = await Tables.getTableColumns(params); const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') if (status === 'success') {
firstTextField = response.find(field => [...TEXT, ...LONG_TEXT].includes(field.type)).name || false; const textField = response.find(field => [...TEXT, ...LONG_TEXT].includes(field.type));
foreignDesc = textField ? textField.name : false;
}
else else
this.addNotification({ status: 'error', message: response }); this.addNotification({ status: 'error', message: response });
} }
@@ -77,7 +79,7 @@ export default {
const { status, response } = await Tables.getForeignList({ const { status, response } = await Tables.getForeignList({
...params, ...params,
column: this.keyUsage.refField, column: this.keyUsage.refField,
description: firstTextField description: foreignDesc
}); });
if (status === 'success') if (status === 'success')

View File

@@ -30,7 +30,7 @@
class="form-input" class="form-input"
type="text" type="text"
> >
<span class="input-group-addon field-type" :class="`type-${parameter.type.toLowerCase()}`"> <span class="input-group-addon field-type" :class="typeClass(parameter.type)">
{{ parameter.type }} {{ parameter.length | wrapNumber }} {{ parameter.type }} {{ parameter.length | wrapNumber }}
</span> </span>
</div> </div>
@@ -75,6 +75,11 @@ export default {
window.removeEventListener('keydown', this.onKey); window.removeEventListener('keydown', this.onKey);
}, },
methods: { methods: {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
runRoutine () { runRoutine () {
const valArr = Object.keys(this.values).reduce((acc, curr) => { const valArr = Object.keys(this.values).reduce((acc, curr) => {
const value = isNaN(this.values[curr]) ? `"${this.values[curr]}"` : this.values[curr]; const value = isNaN(this.values[curr]) ? `"${this.values[curr]}"` : this.values[curr];

View File

@@ -59,15 +59,15 @@
<option value="maria"> <option value="maria">
MariaDB MariaDB
</option> </option>
<option value="pg">
PostgreSQL
</option>
<!-- <option value="mssql"> <!-- <option value="mssql">
Microsoft SQL Microsoft SQL
</option> </option>
<option value="pg"> <option value="oracledb">
PostgreSQL Oracle DB
</option> </option> -->
<option value="oracledb">
Oracle DB
</option> -->
</select> </select>
</div> </div>
</div> </div>
@@ -97,6 +97,18 @@
> >
</div> </div>
</div> </div>
<div v-if="customizations.database" class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="localConnection.database"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group"> <div class="form-group">
<div class="col-4 col-sm-12"> <div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label> <label class="form-label">{{ $t('word.user') }}</label>
@@ -247,6 +259,7 @@
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import customizations from 'common/customizations';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import ModalAskCredentials from '@/components/ModalAskCredentials'; import ModalAskCredentials from '@/components/ModalAskCredentials';
import BaseToast from '@/components/BaseToast'; import BaseToast from '@/components/BaseToast';
@@ -274,6 +287,11 @@ export default {
selectedTab: 'general' selectedTab: 'general'
}; };
}, },
computed: {
customizations () {
return customizations[this.connection.client];
}
},
created () { created () {
this.localConnection = Object.assign({}, this.connection); this.localConnection = Object.assign({}, this.connection);
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);

View File

@@ -5,7 +5,7 @@
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-database-edit mr-1" /> {{ $t('message.editDatabase') }} <i class="mdi mdi-24px mdi-database-edit mr-1" /> {{ $t('message.editSchema') }}
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -23,7 +23,7 @@
class="form-input" class="form-input"
type="text" type="text"
required required
:placeholder="$t('message.databaseName')" :placeholder="$t('message.schemaName')"
readonly readonly
> >
</div> </div>
@@ -53,7 +53,7 @@
</div> </div>
</div> </div>
<div class="modal-footer text-light"> <div class="modal-footer text-light">
<button class="btn btn-primary mr-2" @click.stop="updateDatabase"> <button class="btn btn-primary mr-2" @click.stop="updateSchema">
{{ $t('word.update') }} {{ $t('word.update') }}
</button> </button>
<button class="btn btn-link" @click.stop="closeModal"> <button class="btn btn-link" @click.stop="closeModal">
@@ -66,10 +66,10 @@
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import Database from '@/ipc-api/Database'; import Schema from '@/ipc-api/Schema';
export default { export default {
name: 'ModalEditDatabase', name: 'ModalEditSchema',
props: { props: {
selectedDatabase: String selectedDatabase: String
}, },
@@ -98,7 +98,7 @@ export default {
async created () { async created () {
let actualCollation; let actualCollation;
try { try {
const { status, response } = await Database.getDatabaseCollation({ uid: this.selectedWorkspace, database: this.selectedDatabase }); const { status, response } = await Schema.getDatabaseCollation({ uid: this.selectedWorkspace, database: this.selectedDatabase });
if (status === 'success') if (status === 'success')
actualCollation = response; actualCollation = response;
@@ -130,10 +130,10 @@ export default {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification' addNotification: 'notifications/addNotification'
}), }),
async updateDatabase () { async updateSchema () {
if (this.database.collation !== this.database.prevCollation) { if (this.database.collation !== this.database.prevCollation) {
try { try {
const { status, response } = await Database.updateDatabase({ const { status, response } = await Schema.updateSchema({
uid: this.selectedWorkspace, uid: this.selectedWorkspace,
...this.database ...this.database
}); });

View File

@@ -34,7 +34,7 @@
:field-obj="localRow[field.name]" :field-obj="localRow[field.name]"
:value.sync="localRow[field.name]" :value.sync="localRow[field.name]"
> >
<span class="input-group-addon field-type" :class="`type-${field.type.toLowerCase()}`"> <span class="input-group-addon field-type" :class="typeClass(field.type)">
{{ field.type }} {{ fieldLength(field) | wrapNumber }} {{ field.type }} {{ fieldLength(field) | wrapNumber }}
</span> </span>
<label class="form-checkbox ml-3" :title="$t('word.insert')"> <label class="form-checkbox ml-3" :title="$t('word.insert')">
@@ -286,6 +286,11 @@ export default {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification' addNotification: 'notifications/addNotification'
}), }),
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
async insertRows () { async insertRows () {
this.isInserting = true; this.isInserting = true;
const rowToInsert = this.localRow; const rowToInsert = this.localRow;

View File

@@ -63,12 +63,12 @@
<option value="maria"> <option value="maria">
MariaDB MariaDB
</option> </option>
<option value="pg">
PostgreSQL
</option>
<!-- <option value="mssql"> <!-- <option value="mssql">
Microsoft SQL Microsoft SQL
</option> </option>
<option value="pg">
PostgreSQL
</option>
<option value="oracledb"> <option value="oracledb">
Oracle DB Oracle DB
</option> --> </option> -->
@@ -101,6 +101,18 @@
> >
</div> </div>
</div> </div>
<div v-if="customizations.database" class="form-group">
<div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.database') }}</label>
</div>
<div class="col-8 col-sm-12">
<input
v-model="connection.database"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group"> <div class="form-group">
<div class="col-4 col-sm-12"> <div class="col-4 col-sm-12">
<label class="form-label">{{ $t('word.user') }}</label> <label class="form-label">{{ $t('word.user') }}</label>
@@ -251,6 +263,7 @@
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import customizations from 'common/customizations';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
import ModalAskCredentials from '@/components/ModalAskCredentials'; import ModalAskCredentials from '@/components/ModalAskCredentials';
@@ -270,8 +283,9 @@ export default {
name: '', name: '',
client: 'mysql', client: 'mysql',
host: '127.0.0.1', host: '127.0.0.1',
port: '3306', database: null,
user: 'root', port: null,
user: null,
password: '', password: '',
ask: false, ask: false,
uid: uidGen('C'), uid: uidGen('C'),
@@ -291,7 +305,13 @@ export default {
selectedTab: 'general' selectedTab: 'general'
}; };
}, },
computed: {
customizations () {
return customizations[this.connection.client];
}
},
created () { created () {
this.setDefaults();
window.addEventListener('keydown', this.onKey); window.addEventListener('keydown', this.onKey);
setTimeout(() => { setTimeout(() => {
@@ -307,20 +327,9 @@ export default {
addConnection: 'connections/addConnection' addConnection: 'connections/addConnection'
}), }),
setDefaults () { setDefaults () {
switch (this.connection.client) { this.connection.user = this.customizations.defaultUser;
case 'mysql': this.connection.port = this.customizations.defaultPort;
this.connection.port = '3306'; this.connection.database = this.customizations.defaultDatabase;
break;
case 'mssql':
this.connection.port = '1433';
break;
case 'pg':
this.connection.port = '5432';
break;
case 'oracledb':
this.connection.port = '1521';
break;
}
}, },
async startTest () { async startTest () {
this.isTesting = true; this.isTesting = true;

View File

@@ -5,7 +5,7 @@
<div class="modal-header pl-2"> <div class="modal-header pl-2">
<div class="modal-title h6"> <div class="modal-title h6">
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-database-plus mr-1" /> {{ $t('message.createNewDatabase') }} <i class="mdi mdi-24px mdi-database-plus mr-1" /> {{ $t('message.createNewSchema') }}
</div> </div>
</div> </div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" /> <a class="btn btn-clear c-hand" @click.stop="closeModal" />
@@ -24,11 +24,11 @@
class="form-input" class="form-input"
type="text" type="text"
required required
:placeholder="$t('message.databaseName')" :placeholder="$t('message.schemaName')"
> >
</div> </div>
</div> </div>
<div class="form-group"> <div v-if="customizations.collations" class="form-group">
<div class="col-3"> <div class="col-3">
<label class="form-label">{{ $t('word.collation') }}</label> <label class="form-label">{{ $t('word.collation') }}</label>
</div> </div>
@@ -49,7 +49,11 @@
</div> </div>
</div> </div>
<div class="modal-footer text-light"> <div class="modal-footer text-light">
<button class="btn btn-primary mr-2" @click.stop="createDatabase"> <button
class="btn btn-primary mr-2"
:class="{'loading': isLoading}"
@click.stop="createSchema"
>
{{ $t('word.add') }} {{ $t('word.add') }}
</button> </button>
<button class="btn btn-link" @click.stop="closeModal"> <button class="btn btn-link" @click.stop="closeModal">
@@ -62,12 +66,13 @@
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import Database from '@/ipc-api/Database'; import Schema from '@/ipc-api/Schema';
export default { export default {
name: 'ModalNewDatabase', name: 'ModalNewSchema',
data () { data () {
return { return {
isLoading: false,
database: { database: {
name: '', name: '',
collation: '' collation: ''
@@ -83,8 +88,11 @@ export default {
collations () { collations () {
return this.getWorkspace(this.selectedWorkspace).collations; return this.getWorkspace(this.selectedWorkspace).collations;
}, },
customizations () {
return this.getWorkspace(this.selectedWorkspace).customizations;
},
defaultCollation () { defaultCollation () {
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || ''; return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server') ? this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value : '';
} }
}, },
created () { created () {
@@ -101,9 +109,10 @@ export default {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification' addNotification: 'notifications/addNotification'
}), }),
async createDatabase () { async createSchema () {
this.isLoading = true;
try { try {
const { status, response } = await Database.createDatabase({ const { status, response } = await Schema.createSchema({
uid: this.selectedWorkspace, uid: this.selectedWorkspace,
...this.database ...this.database
}); });
@@ -118,6 +127,7 @@ export default {
catch (err) { catch (err) {
this.addNotification({ status: 'error', message: err.stack }); this.addNotification({ status: 'error', message: err.stack });
} }
this.isLoading = false;
}, },
closeModal () { closeModal () {
this.$emit('close'); this.$emit('close');

View File

@@ -69,7 +69,7 @@
:disabled="fieldsToExclude.includes(field.name)" :disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1" :tabindex="key+1"
> >
<span class="input-group-addon" :class="`type-${field.type.toLowerCase()}`"> <span class="input-group-addon" :class="typeCLass(field.type)">
{{ field.type }} {{ fieldLength(field) | wrapNumber }} {{ field.type }} {{ fieldLength(field) | wrapNumber }}
</span> </span>
<label class="form-checkbox ml-3" :title="$t('word.insert')"> <label class="form-checkbox ml-3" :title="$t('word.insert')">
@@ -222,6 +222,11 @@ export default {
...mapActions({ ...mapActions({
addNotification: 'notifications/addNotification' addNotification: 'notifications/addNotification'
}), }),
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
async insertRows () { async insertRows () {
this.isInserting = true; this.isInserting = true;
const rowToInsert = this.localRow; const rowToInsert = this.localRow;

View File

@@ -101,7 +101,7 @@
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import Database from '@/ipc-api/Database'; import Schema from '@/ipc-api/Schema';
import BaseVirtualScroll from '@/components/BaseVirtualScroll'; import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import ProcessesListRow from '@/components/ProcessesListRow'; import ProcessesListRow from '@/components/ProcessesListRow';
@@ -181,7 +181,7 @@ export default {
this.results = []; this.results = [];
try { // Table data try { // Table data
const { status, response } = await Database.getProcesses(this.connection.uid); const { status, response } = await Schema.getProcesses(this.connection.uid);
if (status === 'success') { if (status === 'success') {
this.results = response; this.results = response;

View File

@@ -16,19 +16,27 @@
<i class="mdi mdi-24px mdi-tools" /> <i class="mdi mdi-24px mdi-tools" />
</a> </a>
<ul class="menu text-left text-uppercase"> <ul class="menu text-left text-uppercase">
<li class="menu-item"> <li v-if="workspace.customizations.processesList" class="menu-item">
<a class="c-hand p-vcentered" @click="showProcessesModal"> <a class="c-hand p-vcentered" @click="showProcessesModal">
<i class="mdi mdi-memory mr-1 tool-icon" /> <i class="mdi mdi-memory mr-1 tool-icon" />
<span>{{ $t('message.processesList') }}</span> <span>{{ $t('message.processesList') }}</span>
</a> </a>
</li> </li>
<li class="menu-item" title="Coming..."> <li
v-if="workspace.customizations.variables"
class="menu-item"
title="Coming..."
>
<a class="c-hand p-vcentered disabled"> <a class="c-hand p-vcentered disabled">
<i class="mdi mdi-shape mr-1 tool-icon" /> <i class="mdi mdi-shape mr-1 tool-icon" />
<span>{{ $t('word.variables') }}</span> <span>{{ $t('word.variables') }}</span>
</a> </a>
</li> </li>
<li class="menu-item" title="Coming..."> <li
v-if="workspace.customizations.usersManagement"
class="menu-item"
title="Coming..."
>
<a class="c-hand p-vcentered disabled"> <a class="c-hand p-vcentered disabled">
<i class="mdi mdi-account-group mr-1 tool-icon" /> <i class="mdi mdi-account-group mr-1 tool-icon" />
<span>{{ $t('message.manageUsers') }}</span> <span>{{ $t('message.manageUsers') }}</span>
@@ -37,7 +45,7 @@
</ul> </ul>
</li> </li>
<li <li
v-if="schemaChild" v-if="schemaChild && isSettingSupported"
class="tab-item" class="tab-item"
:class="{'active': selectedTab === 'prop'}" :class="{'active': selectedTab === 'prop'}"
@click="selectTab({uid: workspace.uid, tab: 'prop'})" @click="selectTab({uid: workspace.uid, tab: 'prop'})"
@@ -194,6 +202,15 @@ export default {
isSelected () { isSelected () {
return this.selectedWorkspace === this.connection.uid; 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.scheduler && this.workspace.customizations.schedulerSettings) return true;
return false;
},
selectedTab () { selectedTab () {
if ( if (
( (

View File

@@ -11,7 +11,7 @@
<span v-if="workspace.connected" class="workspace-explorebar-tools"> <span v-if="workspace.connected" class="workspace-explorebar-tools">
<i <i
class="mdi mdi-18px mdi-database-plus c-hand mr-2" class="mdi mdi-18px mdi-database-plus c-hand mr-2"
:title="$t('message.createNewDatabase')" :title="$t('message.createNewSchema')"
@click="showNewDBModal" @click="showNewDBModal"
/> />
<i <i
@@ -44,18 +44,18 @@
:connection="connection" :connection="connection"
/> />
<div v-else class="workspace-explorebar-body"> <div v-else class="workspace-explorebar-body">
<WorkspaceExploreBarDatabase <WorkspaceExploreBarSchema
v-for="db of workspace.structure" v-for="db of workspace.structure"
:key="db.name" :key="db.name"
:database="db" :database="db"
:connection="connection" :connection="connection"
@show-database-context="openDatabaseContext" @show-schema-context="openSchemaContext"
@show-table-context="openTableContext" @show-table-context="openTableContext"
@show-misc-context="openMiscContext" @show-misc-context="openMiscContext"
/> />
</div> </div>
</div> </div>
<ModalNewDatabase <ModalNewSchema
v-if="isNewDBModal" v-if="isNewDBModal"
@close="hideNewDBModal" @close="hideNewDBModal"
@reload="refresh" @reload="refresh"
@@ -137,11 +137,11 @@ import Functions from '@/ipc-api/Functions';
import Schedulers from '@/ipc-api/Schedulers'; import Schedulers from '@/ipc-api/Schedulers';
import WorkspaceConnectPanel from '@/components/WorkspaceConnectPanel'; import WorkspaceConnectPanel from '@/components/WorkspaceConnectPanel';
import WorkspaceExploreBarDatabase from '@/components/WorkspaceExploreBarDatabase'; import WorkspaceExploreBarSchema from '@/components/WorkspaceExploreBarSchema';
import DatabaseContext from '@/components/WorkspaceExploreBarDatabaseContext'; import DatabaseContext from '@/components/WorkspaceExploreBarSchemaContext';
import TableContext from '@/components/WorkspaceExploreBarTableContext'; import TableContext from '@/components/WorkspaceExploreBarTableContext';
import MiscContext from '@/components/WorkspaceExploreBarMiscContext'; import MiscContext from '@/components/WorkspaceExploreBarMiscContext';
import ModalNewDatabase from '@/components/ModalNewDatabase'; import ModalNewSchema from '@/components/ModalNewSchema';
import ModalNewTable from '@/components/ModalNewTable'; import ModalNewTable from '@/components/ModalNewTable';
import ModalNewView from '@/components/ModalNewView'; import ModalNewView from '@/components/ModalNewView';
import ModalNewTrigger from '@/components/ModalNewTrigger'; import ModalNewTrigger from '@/components/ModalNewTrigger';
@@ -153,11 +153,11 @@ export default {
name: 'WorkspaceExploreBar', name: 'WorkspaceExploreBar',
components: { components: {
WorkspaceConnectPanel, WorkspaceConnectPanel,
WorkspaceExploreBarDatabase, WorkspaceExploreBarSchema,
DatabaseContext, DatabaseContext,
TableContext, TableContext,
MiscContext, MiscContext,
ModalNewDatabase, ModalNewSchema,
ModalNewTable, ModalNewTable,
ModalNewView, ModalNewView,
ModalNewTrigger, ModalNewTrigger,
@@ -299,8 +299,8 @@ export default {
else else
this.addNotification({ status: 'error', message: response }); this.addNotification({ status: 'error', message: response });
}, },
openDatabaseContext (payload) { openSchemaContext (payload) {
this.selectedDatabase = payload.database; this.selectedDatabase = payload.schema;
this.databaseContextEvent = payload.event; this.databaseContextEvent = payload.event;
this.isDatabaseContext = true; this.isDatabaseContext = true;
}, },

View File

@@ -4,7 +4,7 @@
class="accordion-header database-name" class="accordion-header database-name"
:class="{'text-bold': breadcrumbs.schema === database.name}" :class="{'text-bold': breadcrumbs.schema === database.name}"
@click="selectSchema(database.name)" @click="selectSchema(database.name)"
@contextmenu.prevent="showDatabaseContext($event, database.name)" @contextmenu.prevent="showSchemaContext($event, database.name)"
> >
<div v-if="isLoading" class="icon loading" /> <div v-if="isLoading" class="icon loading" />
<i v-else class="icon mdi mdi-18px mdi-chevron-right" /> <i v-else class="icon mdi mdi-18px mdi-chevron-right" />
@@ -37,7 +37,7 @@
</ul> </ul>
</div> </div>
<div v-if="filteredTriggers.length" class="database-misc"> <div v-if="filteredTriggers.length && customizations.triggers" class="database-misc">
<details class="accordion"> <details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger}"> <summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger}">
<i class="misc-icon mdi mdi-18px mdi-folder-cog mr-1" /> <i class="misc-icon mdi mdi-18px mdi-folder-cog mr-1" />
@@ -65,7 +65,7 @@
</details> </details>
</div> </div>
<div v-if="filteredProcedures.length" class="database-misc"> <div v-if="filteredProcedures.length && customizations.routines" class="database-misc">
<details class="accordion"> <details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure}"> <summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.procedure}">
<i class="misc-icon mdi mdi-18px mdi-folder-sync mr-1" /> <i class="misc-icon mdi mdi-18px mdi-folder-sync mr-1" />
@@ -93,7 +93,7 @@
</details> </details>
</div> </div>
<div v-if="filteredFunctions.length" class="database-misc"> <div v-if="filteredFunctions.length && customizations.functions" class="database-misc">
<details class="accordion"> <details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function}"> <summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function}">
<i class="misc-icon mdi mdi-18px mdi-folder-move mr-1" /> <i class="misc-icon mdi mdi-18px mdi-folder-move mr-1" />
@@ -121,7 +121,7 @@
</details> </details>
</div> </div>
<div v-if="filteredSchedulers.length" class="database-misc"> <div v-if="filteredSchedulers.length && customizations.schedulers" class="database-misc">
<details class="accordion"> <details class="accordion">
<summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler}"> <summary class="accordion-header misc-name" :class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler}">
<i class="misc-icon mdi mdi-18px mdi-folder-clock mr-1" /> <i class="misc-icon mdi mdi-18px mdi-folder-clock mr-1" />
@@ -157,7 +157,7 @@ import { mapActions, mapGetters } from 'vuex';
import { formatBytes } from 'common/libs/formatBytes'; import { formatBytes } from 'common/libs/formatBytes';
export default { export default {
name: 'WorkspaceExploreBarDatabase', name: 'WorkspaceExploreBarSchema',
props: { props: {
database: Object, database: Object,
connection: Object connection: Object
@@ -194,6 +194,9 @@ export default {
breadcrumbs () { breadcrumbs () {
return this.getWorkspace(this.connection.uid).breadcrumbs; return this.getWorkspace(this.connection.uid).breadcrumbs;
}, },
customizations () {
return this.getWorkspace(this.connection.uid).customizations;
},
loadedSchemas () { loadedSchemas () {
return this.getLoadedSchemas(this.connection.uid); return this.getLoadedSchemas(this.connection.uid);
}, },
@@ -222,9 +225,9 @@ export default {
this.changeBreadcrumbs({ schema, table: null }); this.changeBreadcrumbs({ schema, table: null });
}, },
showDatabaseContext (event, database) { showSchemaContext (event, schema) {
this.changeBreadcrumbs({ schema: database, table: null }); this.selectSchema(schema);
this.$emit('show-database-context', { event, database }); this.$emit('show-schema-context', { event, schema });
}, },
showTableContext (event, table) { showTableContext (event, table) {
this.setBreadcrumbs({ schema: this.database.name, [table.type]: table.name }); this.setBreadcrumbs({ schema: this.database.name, [table.type]: table.name });

View File

@@ -7,27 +7,55 @@
<span class="d-flex"><i class="mdi mdi-18px mdi-plus text-light pr-1" /> {{ $t('word.add') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-plus text-light pr-1" /> {{ $t('word.add') }}</span>
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" /> <i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
<div class="context-submenu"> <div class="context-submenu">
<div class="context-element" @click="showCreateTableModal"> <div
v-if="workspace.customizations.tableAdd"
class="context-element"
@click="showCreateTableModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table text-light pr-1" /> {{ $t('word.table') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-table text-light pr-1" /> {{ $t('word.table') }}</span>
</div> </div>
<div class="context-element" @click="showCreateViewModal"> <div
v-if="workspace.customizations.viewAdd"
class="context-element"
@click="showCreateViewModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-eye text-light pr-1" /> {{ $t('word.view') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-table-eye text-light pr-1" /> {{ $t('word.view') }}</span>
</div> </div>
<div class="context-element" @click="showCreateTriggerModal"> <div
v-if="workspace.customizations.triggerAdd"
class="context-element"
@click="showCreateTriggerModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ $tc('word.trigger', 1) }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-table-cog text-light pr-1" /> {{ $tc('word.trigger', 1) }}</span>
</div> </div>
<div class="context-element" @click="showCreateRoutineModal"> <div
v-if="workspace.customizations.routineAdd"
class="context-element"
@click="showCreateRoutineModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle pr-1" /> {{ $tc('word.storedRoutine', 1) }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-sync-circle pr-1" /> {{ $tc('word.storedRoutine', 1) }}</span>
</div> </div>
<div class="context-element" @click="showCreateFunctionModal"> <div
v-if="workspace.customizations.functionAdd"
class="context-element"
@click="showCreateFunctionModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box pr-1" /> {{ $tc('word.function', 1) }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-arrow-right-bold-box pr-1" /> {{ $tc('word.function', 1) }}</span>
</div> </div>
<div class="context-element" @click="showCreateSchedulerModal"> <div
v-if="workspace.customizations.schedulerAdd"
class="context-element"
@click="showCreateSchedulerModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ $tc('word.scheduler', 1) }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-calendar-clock text-light pr-1" /> {{ $tc('word.scheduler', 1) }}</span>
</div> </div>
</div> </div>
</div> </div>
<div class="context-element" @click="showEditModal"> <div
v-if="workspace.customizations.schemaEdit"
class="context-element"
@click="showEditModal"
>
<span class="d-flex"><i class="mdi mdi-18px mdi-database-edit text-light pr-1" /> {{ $t('word.edit') }}</span> <span class="d-flex"><i class="mdi mdi-18px mdi-database-edit text-light pr-1" /> {{ $t('word.edit') }}</span>
</div> </div>
<div class="context-element" @click="showDeleteModal"> <div class="context-element" @click="showDeleteModal">
@@ -36,12 +64,12 @@
<ConfirmModal <ConfirmModal
v-if="isDeleteModal" v-if="isDeleteModal"
@confirm="deleteDatabase" @confirm="deleteSchema"
@hide="hideDeleteModal" @hide="hideDeleteModal"
> >
<template slot="header"> <template slot="header">
<div class="d-flex"> <div class="d-flex">
<i class="mdi mdi-24px mdi-database-remove mr-1" /> {{ $t('message.deleteDatabase') }} <i class="mdi mdi-24px mdi-database-remove mr-1" /> {{ $t('message.deleteSchema') }}
</div> </div>
</template> </template>
<div slot="body"> <div slot="body">
@@ -50,7 +78,7 @@
</div> </div>
</div> </div>
</ConfirmModal> </ConfirmModal>
<ModalEditDatabase <ModalEditSchema
v-if="isEditModal" v-if="isEditModal"
:selected-database="selectedDatabase" :selected-database="selectedDatabase"
@close="hideEditModal" @close="hideEditModal"
@@ -62,15 +90,15 @@
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import BaseContextMenu from '@/components/BaseContextMenu'; import BaseContextMenu from '@/components/BaseContextMenu';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
import ModalEditDatabase from '@/components/ModalEditDatabase'; import ModalEditSchema from '@/components/ModalEditSchema';
import Database from '@/ipc-api/Database'; import Schema from '@/ipc-api/Schema';
export default { export default {
name: 'WorkspaceExploreBarDatabaseContext', name: 'WorkspaceExploreBarSchemaContext',
components: { components: {
BaseContextMenu, BaseContextMenu,
ConfirmModal, ConfirmModal,
ModalEditDatabase ModalEditSchema
}, },
props: { props: {
contextEvent: MouseEvent, contextEvent: MouseEvent,
@@ -130,9 +158,9 @@ export default {
closeContext () { closeContext () {
this.$emit('close-context'); this.$emit('close-context');
}, },
async deleteDatabase () { async deleteSchema () {
try { try {
const { status, response } = await Database.deleteDatabase({ const { status, response } = await Schema.deleteSchema({
uid: this.selectedWorkspace, uid: this.selectedWorkspace,
database: this.selectedDatabase database: this.selectedDatabase
}); });

View File

@@ -41,7 +41,7 @@
> >
<div class="tile-icon"> <div class="tile-icon">
<div> <div>
<i class="mdi mdi-hexagon mdi-24px" :class="`type-${param.type.toLowerCase()}`" /> <i class="mdi mdi-hexagon mdi-24px" :class="typeClass(param.type)" />
</div> </div>
</div> </div>
<div class="tile-content"> <div class="tile-content">
@@ -183,6 +183,11 @@ export default {
window.removeEventListener('resize', this.getModalInnerHeight); window.removeEventListener('resize', this.getModalInnerHeight);
}, },
methods: { methods: {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
confirmParametersChange () { confirmParametersChange () {
this.$emit('parameters-update', this.parametersProxy); this.$emit('parameters-update', this.parametersProxy);
}, },

View File

@@ -41,7 +41,7 @@
> >
<div class="tile-icon"> <div class="tile-icon">
<div> <div>
<i class="mdi mdi-hexagon mdi-24px" :class="`type-${param.type.toLowerCase()}`" /> <i class="mdi mdi-hexagon mdi-24px" :class="typeClass(param.type)" />
</div> </div>
</div> </div>
<div class="tile-content"> <div class="tile-content">
@@ -214,6 +214,11 @@ export default {
window.removeEventListener('resize', this.getModalInnerHeight); window.removeEventListener('resize', this.getModalInnerHeight);
}, },
methods: { methods: {
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
confirmParametersChange () { confirmParametersChange () {
this.$emit('parameters-update', this.parametersProxy); this.$emit('parameters-update', this.parametersProxy);
}, },

View File

@@ -48,7 +48,7 @@
<span <span
v-if="!isInlineEditor.type" v-if="!isInlineEditor.type"
class="cell-content text-left" class="cell-content text-left"
:class="`type-${lowerCase(localRow.type)}`" :class="typeClass(localRow.type)"
@click="editON($event, localRow.type.toUpperCase(), 'type')" @click="editON($event, localRow.type.toUpperCase(), 'type')"
> >
{{ localRow.type }} {{ localRow.type }}
@@ -378,10 +378,10 @@ export default {
return 'UNKNOWN ' + key; return 'UNKNOWN ' + key;
} }
}, },
lowerCase (val) { typeClass (type) {
if (val) if (type)
return val.toLowerCase(); return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return val; return '';
}, },
initLocalRow () { initLocalRow () {
Object.keys(this.localRow).forEach(key => { Object.keys(this.localRow).forEach(key => {

View File

@@ -68,7 +68,7 @@
</template> </template>
<script> <script>
import Database from '@/ipc-api/Database'; import Schema from '@/ipc-api/Schema';
import QueryEditor from '@/components/QueryEditor'; import QueryEditor from '@/components/QueryEditor';
import BaseLoader from '@/components/BaseLoader'; import BaseLoader from '@/components/BaseLoader';
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable'; import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
@@ -150,7 +150,7 @@ export default {
query query
}; };
const { status, response } = await Database.rawQuery(params); const { status, response } = await Schema.rawQuery(params);
if (status === 'success') { if (status === 'success') {
this.results = Array.isArray(response) ? response : [response]; this.results = Array.isArray(response) ? response : [response];

View File

@@ -122,7 +122,12 @@ export default {
return this.getWorkspace(this.connUid).breadcrumbs.schema; return this.getWorkspace(this.connUid).breadcrumbs.schema;
}, },
primaryField () { primaryField () {
return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false; const primaryFields = this.fields.filter(field => ['pri', 'uni'].includes(field.key));
if (primaryFields.length > 1 || !primaryFields.length)
return false;
return primaryFields[0];
}, },
isSortable () { isSortable () {
return this.fields.every(field => field.name); return this.fields.every(field => field.name);
@@ -289,15 +294,17 @@ export default {
this.resizeResults(); this.resizeResults();
}, },
updateField (payload, row) { updateField (payload, row) {
const localRow = Object.assign({}, row); const orgRow = this.localResults.find(lr => lr._id === row._id);
delete localRow._id; delete row._id;
delete orgRow._id;
const params = { const params = {
primary: this.primaryField.name, primary: this.primaryField.name,
schema: this.getSchema(this.resultsetIndex), schema: this.getSchema(this.resultsetIndex),
table: this.getTable(this.resultsetIndex), table: this.getTable(this.resultsetIndex),
id: this.getPrimaryValue(localRow), id: this.getPrimaryValue(orgRow),
localRow, row,
orgRow,
...payload ...payload
}; };
this.$emit('update-field', params); this.$emit('update-field', params);
@@ -326,6 +333,7 @@ export default {
table: this.getTable(this.resultsetIndex), table: this.getTable(this.resultsetIndex),
id: this.getPrimaryValue(row), id: this.getPrimaryValue(row),
row, row,
orgRow: row,
field: this.selectedCell.field, field: this.selectedCell.field,
content: null content: null
}; };

View File

@@ -12,7 +12,7 @@
<span <span
v-if="!isInlineEditor[cKey]" v-if="!isInlineEditor[cKey]"
class="cell-content px-2" class="cell-content px-2"
:class="`${isNull(col)} type-${fields[cKey].type.toLowerCase()}`" :class="`${isNull(col)} ${typeClass(fields[cKey].type)}`"
@dblclick="editON($event, col, cKey)" @dblclick="editON($event, col, cKey)"
>{{ col | typeFormat(fields[cKey].type.toLowerCase(), fields[cKey].length) | cutText }}</span> >{{ col | typeFormat(fields[cKey].type.toLowerCase(), fields[cKey].length) | cutText }}</span>
<ForeignKeySelect <ForeignKeySelect
@@ -34,6 +34,15 @@
class="editable-field px-2" class="editable-field px-2"
@blur="editOFF" @blur="editOFF"
> >
<select
v-else-if="inputProps.type === 'boolean'"
v-model="editingContent"
class="form-select small-select editable-field"
@blur="editOFF"
>
<option>true</option>
<option>false</option>
</select>
<input <input
v-else v-else
ref="editField" ref="editField"
@@ -173,7 +182,7 @@ import { mimeFromHex } from 'common/libs/mimeFromHex';
import { formatBytes } from 'common/libs/formatBytes'; import { formatBytes } from 'common/libs/formatBytes';
import { bufferToBase64 } from 'common/libs/bufferToBase64'; import { bufferToBase64 } from 'common/libs/bufferToBase64';
import hexToBinary from 'common/libs/hexToBinary'; import hexToBinary from 'common/libs/hexToBinary';
import { TEXT, LONG_TEXT, NUMBER, FLOAT, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes'; import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BOOLEAN, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { VueMaskDirective } from 'v-mask'; import { VueMaskDirective } from 'v-mask';
import ConfirmModal from '@/components/BaseConfirmModal'; import ConfirmModal from '@/components/BaseConfirmModal';
import TextEditor from '@/components/BaseTextEditor'; import TextEditor from '@/components/BaseTextEditor';
@@ -204,6 +213,9 @@ export default {
return moment(val).isValid() ? moment(val).format('YYYY-MM-DD') : val; return moment(val).isValid() ? moment(val).format('YYYY-MM-DD') : val;
if (DATETIME.includes(type)) { if (DATETIME.includes(type)) {
if (typeof val === 'string')
return val;
let datePrecision = ''; let datePrecision = '';
for (let i = 0; i < precision; i++) for (let i = 0; i < precision; i++)
datePrecision += i === 0 ? '.S' : 'S'; datePrecision += i === 0 ? '.S' : 'S';
@@ -225,6 +237,12 @@ export default {
return hexToBinary(hex); return hexToBinary(hex);
} }
if (ARRAY.includes(type)) {
if (Array.isArray(val))
return JSON.stringify(val).replaceAll('[', '{').replaceAll(']', '}');
return val;
}
return val; return val;
} }
}, },
@@ -287,8 +305,8 @@ export default {
if (BLOB.includes(this.editingType)) if (BLOB.includes(this.editingType))
return { type: 'file', mask: false }; return { type: 'file', mask: false };
if (BIT.includes(this.editingType)) if (BOOLEAN.includes(this.editingType))
return { type: 'text', mask: false }; return { type: 'boolean', mask: false };
return { type: 'text', mask: false }; return { type: 'text', mask: false };
}, },
@@ -331,6 +349,11 @@ export default {
isNull (value) { isNull (value) {
return value === null ? ' is-null' : ''; return value === null ? ' is-null' : '';
}, },
typeClass (type) {
if (type)
return `type-${type.toLowerCase().replaceAll(' ', '_').replaceAll('"', '')}`;
return '';
},
bufferToBase64 (val) { bufferToBase64 (val) {
return bufferToBase64(val); return bufferToBase64(val);
}, },
@@ -345,7 +368,7 @@ export default {
this.editingField = field; this.editingField = field;
this.editingLength = this.fields[field].length; this.editingLength = this.fields[field].length;
if (LONG_TEXT.includes(type)) { if ([...LONG_TEXT, ...ARRAY, ...TEXT_SEARCH].includes(type)) {
this.isTextareaEditor = true; this.isTextareaEditor = true;
this.editingContent = this.$options.filters.typeFormat(content, type); this.editingContent = this.$options.filters.typeFormat(content, type);
return; return;
@@ -389,7 +412,13 @@ export default {
this.isInlineEditor[this.editingField] = false; this.isInlineEditor[this.editingField] = false;
let content; let content;
if (!BLOB.includes(this.editingType)) { if (!BLOB.includes(this.editingType)) {
if ([...DATETIME, ...TIME].includes(this.editingType)) {
if (this.editingContent.substring(this.editingContent.length - 1) === '.')
this.editingContent = this.editingContent.slice(0, -1);
}
if (this.editingContent === this.$options.filters.typeFormat(this.originalContent, this.editingType, this.editingLength)) return;// If not changed if (this.editingContent === this.$options.filters.typeFormat(this.originalContent, this.editingType, this.editingLength)) return;// If not changed
content = this.editingContent; content = this.editingContent;
} }
else { // Handle file upload else { // Handle file upload

View File

@@ -100,7 +100,8 @@ module.exports = {
paste: 'Paste', paste: 'Paste',
tools: 'Tools', tools: 'Tools',
variables: 'Variables', variables: 'Variables',
processes: 'Processes' processes: 'Processes',
database: 'Database'
}, },
message: { message: {
appWelcome: 'Welcome to Antares SQL Client!', appWelcome: 'Welcome to Antares SQL Client!',
@@ -199,7 +200,11 @@ module.exports = {
setNull: 'Set NULL', setNull: 'Set NULL',
processesList: 'Processes list', processesList: 'Processes list',
processInfo: 'Process info', processInfo: 'Process info',
manageUsers: 'Manage users' manageUsers: 'Manage users',
createNewSchema: 'Create new schema',
schemaName: 'Schema name',
editSchema: 'Edit schema',
deleteSchema: 'Delete schema'
}, },
faker: { faker: {
address: 'Address', address: 'Address',

View File

@@ -2,20 +2,20 @@
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
export default class { export default class {
static createDatabase (params) { static createSchema (params) {
return ipcRenderer.invoke('create-database', params); return ipcRenderer.invoke('create-schema', params);
} }
static updateDatabase (params) { static updateSchema (params) {
return ipcRenderer.invoke('update-database', params); return ipcRenderer.invoke('update-schema', params);
} }
static getDatabaseCollation (params) { static getDatabaseCollation (params) {
return ipcRenderer.invoke('get-database-collation', params); return ipcRenderer.invoke('get-schema-collation', params);
} }
static deleteDatabase (params) { static deleteSchema (params) {
return ipcRenderer.invoke('delete-database', params); return ipcRenderer.invoke('delete-schema', params);
} }
static getStructure (params) { static getStructure (params) {

View File

@@ -22,6 +22,14 @@
"mediumtext": $string-color, "mediumtext": $string-color,
"longtext": $string-color, "longtext": $string-color,
"json": $string-color, "json": $string-color,
"name": $string-color,
"character": $string-color,
"character_varying": $string-color,
"cidr": $string-color,
"inet": $string-color,
"macaddr": $string-color,
"macaddr8": $string-color,
"uuid": $string-color,
"int": $number-color, "int": $number-color,
"tinyint": $number-color, "tinyint": $number-color,
"smallint": $number-color, "smallint": $number-color,
@@ -31,12 +39,26 @@
"decimal": $number-color, "decimal": $number-color,
"bigint": $number-color, "bigint": $number-color,
"newdecimal": $number-color, "newdecimal": $number-color,
"integer": $number-color,
"numeric": $number-color,
"smallserial": $number-color,
"serial": $number-color,
"bigserial": $number-color,
"real": $number-color,
"double_precision": $number-color,
"oid": $number-color,
"xid": $number-color,
"money": $number-color,
"datetime": $date-color, "datetime": $date-color,
"date": $date-color, "date": $date-color,
"time": $date-color, "time": $date-color,
"time_with_time_zone": $date-color,
"year": $date-color, "year": $date-color,
"timestamp": $date-color, "timestamp": $date-color,
"timestamp_without_time_zone": $date-color,
"timestamp_with_time_zone": $date-color,
"bit": $bit-color, "bit": $bit-color,
"bit_varying": $bit-color,
"binary": $blob-color, "binary": $blob-color,
"varbinary": $blob-color, "varbinary": $blob-color,
"blob": $blob-color, "blob": $blob-color,
@@ -44,8 +66,16 @@
"mediumblob": $blob-color, "mediumblob": $blob-color,
"medium_blob": $blob-color, "medium_blob": $blob-color,
"longblob": $blob-color, "longblob": $blob-color,
"bytea": $blob-color,
"enum": $enum-color, "enum": $enum-color,
"set": $enum-color, "set": $enum-color,
"boolean": $enum-color,
"interval": $array-color,
"array": $array-color,
"anyarray": $array-color,
"tsvector": $array-color,
"tsquery": $array-color,
"pg_node_tree": $array-color,
"unknown": $unknown-color, "unknown": $unknown-color,
) )
); );

View File

@@ -15,7 +15,8 @@
} }
&.key-mul, &.key-mul,
&.key-INDEX { &.key-INDEX,
&.key-KEY {
color: palegreen; color: palegreen;
} }

View File

@@ -14,6 +14,7 @@ $number-color: cornflowerblue;
$date-color: coral; $date-color: coral;
$bit-color: lightskyblue; $bit-color: lightskyblue;
$blob-color: darkorchid; $blob-color: darkorchid;
$array-color: greenyellow;
$enum-color: gold; $enum-color: gold;
$unknown-color: gray; $unknown-color: gray;

View File

@@ -1,6 +1,6 @@
'use strict'; 'use strict';
import Connection from '@/ipc-api/Connection'; import Connection from '@/ipc-api/Connection';
import Database from '@/ipc-api/Database'; import Schema from '@/ipc-api/Schema';
import Users from '@/ipc-api/Users'; import Users from '@/ipc-api/Users';
import { uidGen } from 'common/libs/uidGen'; import { uidGen } from 'common/libs/uidGen';
const tabIndex = []; const tabIndex = [];
@@ -55,7 +55,7 @@ export default {
state.selected_workspace = uid; state.selected_workspace = uid;
}, },
ADD_CONNECTED (state, payload) { ADD_CONNECTED (state, payload) {
const { uid, client, dataTypes, indexTypes, structure, version } = payload; const { uid, client, dataTypes, indexTypes, customizations, structure, version } = payload;
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
? { ? {
@@ -63,6 +63,7 @@ export default {
client, client,
dataTypes, dataTypes,
indexTypes, indexTypes,
customizations,
structure, structure,
connected: true, connected: true,
version version
@@ -253,16 +254,23 @@ export default {
else { else {
let dataTypes = []; let dataTypes = [];
let indexTypes = []; let indexTypes = [];
let customizations = {};
switch (connection.client) { switch (connection.client) {
case 'mysql': case 'mysql':
case 'maria': case 'maria':
dataTypes = require('common/data-types/mysql'); dataTypes = require('common/data-types/mysql');
indexTypes = require('common/index-types/mysql'); indexTypes = require('common/index-types/mysql');
customizations = require('common/customizations/mysql');
break;
case 'pg':
dataTypes = require('common/data-types/postgresql');
indexTypes = require('common/index-types/postgresql');
customizations = require('common/customizations/postgresql');
break; break;
} }
const { status, response: version } = await Database.getVersion(connection.uid); const { status, response: version } = await Schema.getVersion(connection.uid);
if (status === 'error') if (status === 'error')
dispatch('notifications/addNotification', { status, message: version }, { root: true }); dispatch('notifications/addNotification', { status, message: version }, { root: true });
@@ -285,6 +293,7 @@ export default {
client: connection.client, client: connection.client,
dataTypes, dataTypes,
indexTypes, indexTypes,
customizations,
structure: response, structure: response,
version version
}); });
@@ -300,7 +309,7 @@ export default {
}, },
async refreshStructure ({ dispatch, commit, getters }, uid) { async refreshStructure ({ dispatch, commit, getters }, uid) {
try { try {
const { status, response } = await Database.getStructure({ uid, schemas: getters.getLoadedSchemas(uid) }); const { status, response } = await Schema.getStructure({ uid, schemas: getters.getLoadedSchemas(uid) });
if (status === 'error') if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true }); dispatch('notifications/addNotification', { status, message: response }, { root: true });
@@ -313,7 +322,7 @@ export default {
}, },
async refreshSchema ({ dispatch, commit }, { uid, schema }) { async refreshSchema ({ dispatch, commit }, { uid, schema }) {
try { try {
const { status, response } = await Database.getStructure({ uid, schemas: new Set([schema]) }); const { status, response } = await Schema.getStructure({ uid, schemas: new Set([schema]) });
if (status === 'error') if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true }); dispatch('notifications/addNotification', { status, message: response }, { root: true });
else else
@@ -325,7 +334,7 @@ export default {
}, },
async refreshCollations ({ dispatch, commit }, uid) { async refreshCollations ({ dispatch, commit }, uid) {
try { try {
const { status, response } = await Database.getCollations(uid); const { status, response } = await Schema.getCollations(uid);
if (status === 'error') if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true }); dispatch('notifications/addNotification', { status, message: response }, { root: true });
else else
@@ -337,7 +346,7 @@ export default {
}, },
async refreshVariables ({ dispatch, commit }, uid) { async refreshVariables ({ dispatch, commit }, uid) {
try { try {
const { status, response } = await Database.getVariables(uid); const { status, response } = await Schema.getVariables(uid);
if (status === 'error') if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true }); dispatch('notifications/addNotification', { status, message: response }, { root: true });
else else
@@ -349,7 +358,7 @@ export default {
}, },
async refreshEngines ({ dispatch, commit }, uid) { async refreshEngines ({ dispatch, commit }, uid) {
try { try {
const { status, response } = await Database.getEngines(uid); const { status, response } = await Schema.getEngines(uid);
if (status === 'error') if (status === 'error')
dispatch('notifications/addNotification', { status, message: response }, { root: true }); dispatch('notifications/addNotification', { status, message: response }, { root: true });
else else
@@ -421,7 +430,7 @@ export default {
if (lastBreadcrumbs.schema === payload.schema && hasLastChildren && !hasChildren) return; if (lastBreadcrumbs.schema === payload.schema && hasLastChildren && !hasChildren) return;
if (lastBreadcrumbs.schema !== payload.schema) if (lastBreadcrumbs.schema !== payload.schema)
Database.useSchema({ uid: getters.getSelected, schema: payload.schema }); Schema.useSchema({ uid: getters.getSelected, schema: payload.schema });
commit('CHANGE_BREADCRUMBS', { uid: getters.getSelected, breadcrumbs: { ...breadcrumbsObj, ...payload } }); commit('CHANGE_BREADCRUMBS', { uid: getters.getSelected, breadcrumbs: { ...breadcrumbsObj, ...payload } });
lastBreadcrumbs = { ...breadcrumbsObj, ...payload }; lastBreadcrumbs = { ...breadcrumbsObj, ...payload };