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

Compare commits

..

20 Commits

Author SHA1 Message Date
69def94c88 chore(release): 0.0.5 2020-08-17 17:39:28 +02:00
e8141b6321 feat: badge on setting icon and update tab when update is available 2020-08-17 17:37:42 +02:00
0b6a188d19 feat: foreign key support in add/edit row 2020-08-17 15:10:19 +02:00
dca625fe5a Merge pull request #28 from EStarium/dependabot/npm_and_yarn/standard-version-9.0.0
build(deps-dev): bump standard-version from 8.0.2 to 9.0.0
2020-08-17 08:42:08 +02:00
dependabot[bot]
a4b94bc19c build(deps-dev): bump standard-version from 8.0.2 to 9.0.0
Bumps [standard-version](https://github.com/conventional-changelog/standard-version) from 8.0.2 to 9.0.0.
- [Release notes](https://github.com/conventional-changelog/standard-version/releases)
- [Changelog](https://github.com/conventional-changelog/standard-version/blob/master/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/standard-version/compare/v8.0.2...v9.0.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-17 06:28:16 +00:00
744728a14f refactor: moved table fields informations to vuex 2020-08-14 18:07:29 +02:00
6d0724dc90 fix: wrong schema passed in query tab when selected a different database 2020-08-14 11:25:50 +02:00
59e4a79f42 fix: newline replaced with undefined inside queries 2020-08-14 11:06:20 +02:00
7bc10092fe fix: query result table header didn't show just selected fields 2020-08-13 13:24:03 +02:00
eb348b3095 fix: update a row with a string key value 2020-08-13 13:22:04 +02:00
3c6e818ba0 fix: insert files via add row option 2020-08-13 12:42:19 +02:00
2f1dfdc654 feat: option to insert table rows 2020-08-12 18:12:30 +02:00
128a6cd9e8 style: UI improvements 2020-08-12 18:11:48 +02:00
5473858323 refactor: changed material design icon pack 2020-08-12 10:48:18 +02:00
7651d05b37 fix: window title not perfectly centered 2020-08-11 09:11:26 +02:00
c89c1ce83c docs: update README.md 2020-08-10 18:09:33 +02:00
771f8a2d68 fix: time and datetime precision 2020-08-10 18:07:16 +02:00
13b0816837 fix: table header not fixed on top when fast scrolling 2020-08-10 16:06:11 +02:00
a15e6249e1 chore: dependabot interval and minor changes in README.md 2020-08-07 17:27:25 +02:00
bbde2bd994 perf: improved scroll speed of result tables 2020-08-07 17:26:02 +02:00
47 changed files with 1222 additions and 361 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,6 +1,6 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
github: [fabio286]
patreon: fabio286
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username

View File

@@ -8,4 +8,4 @@ updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"

View File

@@ -2,6 +2,27 @@
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.0.5](https://github.com/EStarium/antares/compare/v0.0.4...v0.0.5) (2020-08-17)
### Features
* badge on setting icon and update tab when update is available ([e8141b6](https://github.com/EStarium/antares/commit/e8141b632154f765ca73fa50b9b7120dc592ead0))
* foreign key support in add/edit row ([0b6a188](https://github.com/EStarium/antares/commit/0b6a188d1959b80b4a66946cc79d2dd3853a428b))
* option to insert table rows ([2f1dfdc](https://github.com/EStarium/antares/commit/2f1dfdc6543b4a6c1d595f0daa00c0832be49c77))
### Bug Fixes
* insert files via add row option ([3c6e818](https://github.com/EStarium/antares/commit/3c6e818ba06f1b8b5db0ecf80c3b7498d6d2a841))
* newline replaced with undefined inside queries ([59e4a79](https://github.com/EStarium/antares/commit/59e4a79f42076b3fce98a764e9ad6a01c674555b))
* query result table header didn't show just selected fields ([7bc1009](https://github.com/EStarium/antares/commit/7bc10092fe4823e03133e69e0a7bf86e44fde43b))
* table header not fixed on top when fast scrolling ([13b0816](https://github.com/EStarium/antares/commit/13b0816837461119eaab79fdb7e92223e0950630))
* time and datetime precision ([771f8a2](https://github.com/EStarium/antares/commit/771f8a2d682c64105231e3fef199f05150596298))
* update a row with a string key value ([eb348b3](https://github.com/EStarium/antares/commit/eb348b3095b6905321b62eed6cea228374ebc3d1))
* window title not perfectly centered ([7651d05](https://github.com/EStarium/antares/commit/7651d05b37970574d6ae4bdf75c20c69d59c1e6d))
* wrong schema passed in query tab when selected a different database ([6d0724d](https://github.com/EStarium/antares/commit/6d0724dc90cdebb10e0342d2c472bdd07aa345f8))
### [0.0.4](https://github.com/EStarium/antares/compare/v0.0.3-alpha...v0.0.4) (2020-08-06)

View File

@@ -6,13 +6,38 @@
![GitHub package.json version](https://img.shields.io/github/package-json/v/estarium/antares) [![Build Status](https://travis-ci.com/EStarium/antares.svg?branch=master)](https://travis-ci.com/EStarium/antares) ![GitHub All Releases](https://img.shields.io/github/downloads/estarium/antares/total) ![GitHub](https://img.shields.io/github/license/estarium/antares)
Antares is an SQL client based on Electron.js and Vue.js 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.
At the moment this application is in a development state, it lacks many features, and is'nt ready as a main SQL client. However i'm actively working on it, hoping to provide all essential features as soon as possible.
**At the moment this application is in a development state, it lacks many features, and is''t ready as a main SQL client**. However i'm actively working on it, hoping to provide all essential features as soon as possible.
If you are curious to try this early state of Antares you can download and install the [latest release](https://github.com/EStarium/antares/releases), and stay tuned for updates. At moment i'm testing only on Windows.
<!--## Philosophy
Why am I developing an SQL client when there are a lot of thom on the market?-->
## How to contribute
- [Translate Antares](https://github.com/EStarium/antares/wiki/Translate-Antares)
## Roadmap
This is a roadmap with major features will come in near future.
- Improvements of query editor area.
- Multiple query tabs.
- Tables management (add/edit/delete).
- Stored procedures, views, schedulers and trigger support.
- Database tools.
- Context menu shortcuts.
- Keyboard shortcuts.
- More secure password storage.
- Query logs console.
- Fake data filler.
- Import/export and migration.
- Themes.
## Currently supported
### Databases
@@ -27,8 +52,8 @@ If you are curious to try this early state of Antares you can download and insta
### Operating Systems
- [x] Windows
- [ ] Linux
- [ ] MacOS
- [x] Linux
- [x] MacOS (needs tests)
## Translations

View File

@@ -1,9 +1,8 @@
{
"name": "antares",
"productName": "Antares",
"version": "0.0.4",
"version": "0.0.5",
"description": "A cross-platform easy to use SQL client.",
"main": "src/main/index.js",
"license": "MIT",
"repository": "https://github.com/EStarium/antares.git",
"scripts": {
@@ -11,20 +10,13 @@
"compile": "electron-webpack",
"build": "cross-env NODE_ENV=production npm run compile && electron-builder",
"release": "standard-version",
"release:pre": "npm run release -- --prerelease alpha"
"release:pre": "npm run release -- --prerelease alpha",
"lint": "eslint ."
},
"author": "Fabio Di Stasio <fabio286@gmail.com>",
"build": {
"npmRebuild": false,
"asar": true,
"appId": "com.estarium.antares",
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
"files": [
"dist/**/*",
"src/**/*",
"node_modules/**/*",
"package.json"
],
"dmg": {
"contents": [
{
@@ -39,11 +31,6 @@
}
]
},
"win": {
"target": [
"nsis"
]
},
"linux": {
"target": [
"deb",
@@ -61,11 +48,11 @@
}
},
"dependencies": {
"@mdi/font": "^5.5.55",
"codemirror": "^5.56.0",
"electron-log": "^4.2.2",
"electron-updater": "^4.3.4",
"lodash": "^4.17.19",
"material-design-icons": "^3.0.1",
"moment": "^2.27.0",
"mssql": "^6.2.1",
"mysql": "^2.18.1",
@@ -87,7 +74,7 @@
"electron-devtools-installer": "^3.1.1",
"electron-webpack": "^2.8.2",
"electron-webpack-vue": "^2.4.0",
"eslint": "^6.8.0",
"eslint": "^7.6.0",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-node": "^11.1.0",
@@ -96,7 +83,7 @@
"eslint-plugin-vue": "^6.2.2",
"node-sass": "^4.14.1",
"sass-loader": "^9.0.3",
"standard-version": "^8.0.2",
"standard-version": "^9.0.0",
"stylelint": "^13.6.1",
"stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0",

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-useless-escape */
// eslint-disable-next-line no-control-regex
const regex = new RegExp(/[\0\x08\x09\x1a\n\r"'\\\%]/g);
const regex = new RegExp(/[\0\x08\x09\x1a\n\r"'\\\%]/gm);
/**
* Escapes a string
@@ -9,10 +9,10 @@ const regex = new RegExp(/[\0\x08\x09\x1a\n\r"'\\\%]/g);
* @returns {String}
*/
function sqlEscaper (string) {
return string.replace(regex, (char) => {
return string.replace(regex, char => {
const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '"', '\\', '\\\\', '%'];
const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\'\'', '""', '\\\\', '\\\\\\\\', '\\%'];
return r[m.indexOf(char)];
return r[m.indexOf(char)] || char;
});
}

View File

@@ -1,4 +1,8 @@
'use strict';
export function uidGen () {
return Math.random().toString(36).substr(2, 9).toUpperCase();
/**
* @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

@@ -3,7 +3,6 @@
import { app, BrowserWindow, nativeImage } from 'electron';
import * as path from 'path';
import { format as formatUrl } from 'url';
import ipcHandlers from './ipc-handlers';
const isDevelopment = process.env.NODE_ENV !== 'production';

View File

@@ -25,6 +25,16 @@ export default (connections) => {
}
});
ipcMain.handle('get-key-usage', async (event, { uid, schema, table }) => {
try {
const result = await InformationSchema.getKeyUsage(connections[uid], schema, table);
return { status: 'success', response: result };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('updateTableCell', async (event, params) => {
try {
const result = await Tables.updateTableCell(connections[params.uid], params);
@@ -44,4 +54,24 @@ export default (connections) => {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('insertTableRows', async (event, params) => {
try {
await Tables.insertTableRows(connections[params.uid], params);
return { status: 'success' };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
ipcMain.handle('get-foreign-list', async (event, params) => {
try {
const results = await Tables.getForeignList(connections[params.uid], params);
return { status: 'success', response: results };
}
catch (err) {
return { status: 'error', response: err.toString() };
}
});
};

View File

@@ -32,7 +32,7 @@ export class AntaresConnector {
limit: [],
join: [],
update: [],
insert: [],
insert: {},
delete: false
};
this._query = Object.assign({}, this._queryDefaults);
@@ -108,6 +108,11 @@ export class AntaresConnector {
return this;
}
into (table) {
this._query.from = table;
return this;
}
delete (table) {
this._query.delete = true;
this.from(table);
@@ -162,6 +167,16 @@ export class AntaresConnector {
return this;
}
/**
* @param {Object} obj field: value
* @returns
* @memberof AntaresConnector
*/
insert (obj) {
this._query.insert = { ...this._query.insert, ...obj };
return this;
}
/**
* @returns {string} SQL string
* @memberof AntaresConnector
@@ -188,8 +203,10 @@ export class AntaresConnector {
// FROM
let fromRaw = '';
if (!this._query.update.length && !!this._query.from)
if (!this._query.update.length && !Object.keys(this._query.insert).length && !!this._query.from)
fromRaw = 'FROM';
else if (Object.keys(this._query.insert).length)
fromRaw = 'INTO';
switch (this._client) {
case 'maria':
@@ -209,6 +226,21 @@ export class AntaresConnector {
const updateArray = this._query.update.reduce(this._reducer, []);
const updateRaw = updateArray.length ? `SET ${updateArray.join(', ')} ` : '';
let insertRaw = '';
if (Object.keys(this._query.insert).length) {
const fieldsList = [];
const valueList = [];
const fields = this._query.insert;
for (const key in fields) {
if (fields[key] === null) continue;
fieldsList.push(key);
valueList.push(fields[key]);
}
insertRaw = `(${fieldsList.join(', ')}) VALUES (${valueList.join(', ')}) `;
}
const groupByArray = this._query.groupBy.reduce(this._reducer, []);
const groupByRaw = groupByArray.length ? `GROUP BY ${groupByArray.join(', ')} ` : '';
@@ -229,7 +261,7 @@ export class AntaresConnector {
break;
}
return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}`;
return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}${insertRaw}`;
}
/**

View File

@@ -27,7 +27,37 @@ export default class {
name: field.COLUMN_NAME,
key: field.COLUMN_KEY.toLowerCase(),
type: field.DATA_TYPE,
precision: field.DATETIME_PRECISION
numPrecision: field.NUMERIC_PRECISION,
datePrecision: field.DATETIME_PRECISION,
charLength: field.CHARACTER_MAXIMUM_LENGTH,
isNullable: field.IS_NULLABLE,
default: field.COLUMN_DEFAULT,
charset: field.CHARACTER_SET_NAME,
collation: field.COLLATION_NAME,
autoIncrement: field.EXTRA.includes('auto_increment')
};
});
}
static async getKeyUsage (connection, schema, table) {
const { rows } = await connection
.select('*')
.schema('information_schema')
.from('KEY_COLUMN_USAGE')
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'`, REFERENCED_TABLE_NAME: 'IS NOT NULL' })
.run();
return rows.map(field => {
return {
schema: field.TABLE_SCHEMA,
table: field.TABLE_NAME,
column: field.COLUMN_NAME,
position: field.ORDINAL_POSITION,
constraintPosition: field.POSITION_IN_UNIQUE_CONSTRAINT,
constraintName: field.CONSTRAINT_NAME,
refSchema: field.REFERENCED_TABLE_SCHEMA,
refTable: field.REFERENCED_TABLE_NAME,
refColumn: field.REFERENCED_COLUMN_NAME
};
});
}

View File

@@ -16,6 +16,7 @@ export default class {
static async updateTableCell (connection, params) {
let escapedParam;
let reload = false;
const id = typeof params.id === 'number' ? params.id : `"${params.id}"`;
if (NUMBER.includes(params.type))
escapedParam = params.content;
@@ -37,7 +38,7 @@ export default class {
.update({ [params.field]: `= ${escapedParam}` })
.schema(params.schema)
.from(params.table)
.where({ [params.primary]: `= ${params.id}` })
.where({ [params.primary]: `= ${id}` })
.run();
return { reload };
@@ -50,4 +51,52 @@ export default class {
.where({ [params.primary]: `IN (${params.rows.join(',')})` })
.run();
}
static async insertTableRows (connection, params) {
const insertObj = {};
for (const key in params.row) {
const type = params.fields[key];
let escapedParam;
if (params.row[key] === null)
escapedParam = 'NULL';
else if (NUMBER.includes(type))
escapedParam = params.row[key];
else if ([...TEXT, ...LONG_TEXT].includes(type))
escapedParam = `"${sqlEscaper(params.row[key])}"`;
else if (BLOB.includes(type)) {
if (params.row[key]) {
const fileBlob = fs.readFileSync(params.row[key]);
escapedParam = `0x${fileBlob.toString('hex')}`;
}
else
escapedParam = '""';
}
else
escapedParam = `"${sqlEscaper(params.row[key])}"`;
insertObj[key] = escapedParam;
}
for (let i = 0; i < params.repeat; i++) {
await connection
.schema(params.schema)
.into(params.table)
.insert(insertObj)
.run();
}
}
static async getForeignList (connection, params) {
const query = connection
.select(`${params.column} AS foreignColumn`)
.schema(params.schema)
.from(params.table)
.orderBy('foreignColumn ASC');
if (params.description)
query.select(`LEFT(${params.description}, 20) AS foreignDescription`);
return query.run();
}
}

View File

@@ -2,7 +2,7 @@
<div class="modal active" :class="modalSizeClass">
<a class="modal-overlay" @click="hideModal" />
<div class="modal-container">
<div v-if="hasHeader" class="modal-header">
<div v-if="hasHeader" class="modal-header pl-2">
<div class="modal-title h6">
<slot name="header" />
</div>

View File

@@ -31,7 +31,7 @@ export default {
},
methods: {
close () {
this.$emit('closeContext');
this.$emit('close-context');
}
}
};
@@ -57,7 +57,7 @@ export default {
.context-container {
min-width: 100px;
max-width: 150px;
z-index: 1;
z-index: 10;
box-shadow: 0 0 1px 0 #000;
padding: 0;
background: #1d1d1d;

View File

@@ -1,14 +1,15 @@
<template>
<div class="toast mt-2" :class="notificationStatus.className">
<span class="p-vcentered text-left" :class="{'expanded': isExpanded}">
<i class="material-icons mr-1">{{ notificationStatus.iconName }}</i>
<i class="mdi mdi-24px mr-2" :class="notificationStatus.iconName" />
<span class="notification-message">{{ message }}</span>
</span>
<i
v-if="isExpandable"
class="material-icons c-hand"
class="mdi mdi-24px c-hand expand-btn"
:class="isExpanded ? 'mdi-chevron-up' : 'mdi-chevron-down'"
@click="toggleExpand"
>{{ isExpanded ? 'expand_less' : 'expand_more' }}</i>
/>
<button class="btn btn-clear ml-2" @click="hideToast" />
</div>
</template>
@@ -38,19 +39,19 @@ export default {
switch (this.status) {
case 'success':
className = 'toast-success';
iconName = 'done';
iconName = 'mdi-check';
break;
case 'error':
className = 'toast-error';
iconName = 'error';
iconName = 'mdi-alert-rhombus';
break;
case 'warning':
className = 'toast-warning';
iconName = 'warning';
iconName = 'mdi-alert';
break;
case 'primary':
className = 'toast-primary';
iconName = 'info_outline';
iconName = 'mdi-information-outline';
break;
}
@@ -88,6 +89,10 @@ export default {
max-width: 30rem;
}
.expand-btn {
align-items: initial;
}
.expanded .notification-message {
white-space: initial;
}

View File

@@ -34,19 +34,19 @@ export default {
switch (this.status) {
case 'success':
className = 'toast-success';
iconTag = '<i class="material-icons mr-1">done</i>';
iconTag = '<i class="mdi mdi-24px mdi-check mr-1"></i>';
break;
case 'error':
className = 'toast-error';
iconTag = '<i class="material-icons mr-1">error</i>';
iconTag = '<i class="mdi mdi-24px mdi-alert-rhombus mr-1"></i>';
break;
case 'warning':
className = 'toast-warning';
iconTag = '<i class="material-icons mr-1">warning</i>';
iconTag = '<i class="mdi mdi-24px mdi-alert mr-1"></i>';
break;
case 'primary':
className = 'toast-primary';
iconTag = '<i class="material-icons mr-1">info_outline</i>';
iconTag = '<i class="mdi mdi-24px mdi-information-outline mr-1"></i>';
break;
}

View File

@@ -21,47 +21,50 @@
</template>
<script>
// credits: https://github.com/xrado 👼
export default {
name: 'BaseVirtualScroll',
props: {
items: Array,
itemHeight: Number
itemHeight: Number,
visibleHeight: Number,
scrollElement: {
type: HTMLDivElement,
default: null
}
},
data () {
return {
topHeight: 0,
bottomHeight: 0,
visibleItems: []
visibleItems: [],
renderTimeout: null,
localScrollElement: null
};
},
mounted () {
this._checkScrollPosition = this.checkScrollPosition.bind(this);
this.checkScrollPosition();
this.$el.addEventListener('scroll', this._checkScrollPosition);
this.$el.addEventListener('wheel', this._checkScrollPosition);
this.localScrollElement = this.scrollElement ? this.scrollElement : this.$el;
this.updateWindow();
this.localScrollElement.addEventListener('scroll', this._checkScrollPosition);
},
beforeDestroy () {
this.$el.removeEventListener('scroll', this._checkScrollPosition);
this.$el.removeEventListener('wheel', this._checkScrollPosition);
this.localScrollElement.removeEventListener('scroll', this._checkScrollPosition);
},
methods: {
checkScrollPosition (e = {}) {
const el = this.$el;
checkScrollPosition (e) {
clearTimeout(this.renderTimeout);
// prevent parent scroll
if ((el.scrollTop === 0 && e.deltaY < 0) || (Math.abs(el.scrollTop - (el.scrollHeight - el.clientHeight)) <= 1 && e.deltaY > 0))
e.preventDefault();
this.updateWindow(e);
this.renderTimeout = setTimeout(() => {
this.updateWindow(e);
}, 200);
},
updateWindow (e) {
const visibleItemsCount = Math.ceil(this.$el.clientHeight / this.itemHeight);
const visibleItemsCount = Math.ceil(this.visibleHeight / this.itemHeight);
const totalScrollHeight = this.items.length * this.itemHeight;
const offset = 50;
const scrollTop = this.localScrollElement.scrollTop;
const scrollTop = this.$el.scrollTop;
const offset = 5;
const firstVisibleIndex = Math.floor(scrollTop / this.itemHeight);
const lastVisibleIndex = firstVisibleIndex + visibleItemsCount;
const firstCutIndex = Math.max(firstVisibleIndex - offset, 0);

View File

@@ -0,0 +1,89 @@
<template>
<select
ref="editField"
class="px-1"
@change="onChange"
@blur="$emit('blur')"
>
<option
v-for="row in foreignList"
:key="row.foreignColumn"
:value="row.foreignColumn"
:selected="row.foreignColumn === value"
>
{{ row.foreignColumn }} {{ 'foreignDescription' in row ? ` - ${row.foreignDescription}` : '' | cutText }}
</option>
</select>
</template>
<script>
import Tables from '@/ipc-api/Tables';
import { mapGetters, mapActions } from 'vuex';
import { TEXT, LONG_TEXT } from 'common/fieldTypes';
export default {
name: 'ForeignKeySelect',
filters: {
cutText (val) {
if (typeof val !== 'string') return val;
return val.length > 15 ? `${val.substring(0, 15)}...` : val;
}
},
props: {
value: [String, Number],
keyUsage: Object
},
data () {
return {
foreignList: []
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected'
})
},
async created () {
let firstTextField;
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')
firstTextField = response.find(field => [...TEXT, ...LONG_TEXT].includes(field.type)).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.refColumn,
description: firstTextField
});
if (status === 'success')
this.foreignList = response.rows;
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
onChange () {
this.$emit('update:value', this.$refs.editField.value);
}
}
};
</script>

View File

@@ -2,9 +2,11 @@
<div class="modal active modal-sm">
<a class="modal-overlay" />
<div class="modal-container p-0">
<div class="modal-header">
<div class="modal-header pl-2">
<div class="modal-title h6">
{{ $t('word.credentials') }}
<div class="d-flex">
<i class="mdi mdi-24px mdi-key-variant mr-1" /> {{ $t('word.credentials') }}
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>

View File

@@ -2,9 +2,11 @@
<div class="modal active">
<a class="modal-overlay c-hand" @click="closeModal" />
<div class="modal-container">
<div class="modal-header">
<div class="modal-header pl-2">
<div class="modal-title h6">
{{ $t('message.editConnection') }}
<div class="d-flex">
<i class="mdi mdi-24px mdi-server mr-1" /> {{ $t('message.editConnection') }}
</div>
</div>
<a class="btn btn-clear c-hand" @click="closeModal" />
</div>

View File

@@ -2,9 +2,11 @@
<div class="modal active">
<a class="modal-overlay c-hand" @click="closeModal" />
<div class="modal-container">
<div class="modal-header">
<div class="modal-header pl-2">
<div class="modal-title h6">
{{ $t('message.createNewConnection') }}
<div class="d-flex">
<i class="mdi mdi-24px mdi-server-plus mr-1" /> {{ $t('message.createNewConnection') }}
</div>
</div>
<a class="btn btn-clear c-hand" @click="closeModal" />
</div>
@@ -168,7 +170,7 @@ export default {
user: 'root',
password: '',
ask: false,
uid: uidGen()
uid: uidGen('C')
},
toast: {
status: '',

View File

@@ -0,0 +1,326 @@
<template>
<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" /> {{ $t('message.addNewRow') }}
</div>
</div>
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
</div>
<div class="modal-body">
<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)"
class="form-select"
:value.sync="localRow[field.name]"
:key-usage="getKeyUsage(field.name)"
:disabled="fieldsToExclude.includes(field.name)"
/>
<input
v-else-if="inputProps(field).mask"
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'"
class="form-input"
type="file"
:disabled="fieldsToExclude.includes(field.name)"
:tabindex="key+1"
@change="filesChange($event,field.name)"
>
<input
v-else
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="`type-${field.type}`">
{{ field.type }} {{ fieldLength(field) | wrapNumber }}
</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 text-light">
<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>
</template>
<script>
import moment from 'moment';
import { TEXT, LONG_TEXT, NUMBER, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { mask } from 'vue-the-mask';
import { mapGetters, mapActions } from 'vuex';
import Tables from '@/ipc-api/Tables';
import ForeignKeySelect from '@/components/ForeignKeySelect';
export default {
name: 'ModalNewTableRow',
components: {
ForeignKeySelect
},
directives: {
mask
},
filters: {
wrapNumber (num) {
if (!num) return '';
return `(${num})`;
}
},
props: {
connection: Object,
tabUid: [String, Number]
},
data () {
return {
localRow: {},
fieldsToExclude: [],
nInserts: 1,
isInserting: false
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace',
getWorkspaceTab: 'workspaces/getWorkspaceTab'
}),
workspace () {
return this.getWorkspace(this.selectedWorkspace);
},
foreignKeys () {
return this.keyUsage.map(key => key.column);
},
fields () {
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).fields : [];
},
keyUsage () {
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).keyUsage : [];
}
},
watch: {
nInserts (val) {
if (!val || val < 1)
this.nInserts = 1;
}
},
mounted () {
const rowObj = {};
for (const field of this.fields) {
let fieldDefault;
if (field.default === 'NULL') fieldDefault = null;
else {
if (NUMBER.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 };
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
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;
return field.numPrecision || field.datePrecision || field.charLength || 0;
},
inputProps (field) {
if ([...TEXT, ...LONG_TEXT].includes(field.type))
return { type: 'text', mask: false };
if (NUMBER.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.column === keyName);
}
}
};
</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

@@ -2,9 +2,12 @@
<div id="settings" class="modal active">
<a class="modal-overlay c-hand" @click="closeModal" />
<div class="modal-container">
<div class="modal-header">
<div class="modal-title h5">
{{ $t('word.settings') }}
<div class="modal-header pl-2">
<div class="modal-title h6">
<div class="d-flex">
<i class="mdi mdi-24px mdi-cog mr-1" />
{{ $t('word.settings') }}
</div>
</div>
<a class="btn btn-clear c-hand" @click="closeModal" />
</div>
@@ -31,7 +34,7 @@
:class="{'active': selectedTab === 'update'}"
@click="selectTab('update')"
>
<a class="c-hand" :class="{'badge': isUpdate}">{{ $t('word.update') }}</a>
<a class="c-hand" :class="{'badge badge-update': hasUpdates}">{{ $t('word.update') }}</a>
</li>
<li
class="tab-item"
@@ -49,7 +52,7 @@
<div class="form-group mb-4">
<div class="col-6 col-sm-12">
<label class="form-label">
<i class="material-icons md-18 mr-1">translate</i>
<i class="mdi mdi-18px mdi-translate mr-1" />
{{ $t('word.language') }}:
</label>
</div>
@@ -132,7 +135,6 @@ export default {
},
data () {
return {
isUpdate: false,
localLocale: null,
localTimeout: null,
selectedTab: 'general'
@@ -144,7 +146,8 @@ export default {
appVersion: 'application/appVersion',
selectedSettingTab: 'application/selectedSettingTab',
selectedLocale: 'settings/getLocale',
notificationsTimeout: 'settings/getNotificationsTimeout'
notificationsTimeout: 'settings/getNotificationsTimeout',
updateStatus: 'application/getUpdateStatus'
}),
locales () {
const locales = [];
@@ -152,6 +155,9 @@ export default {
locales.push({ code: locale, name: localesNames[locale] });
return locales;
},
hasUpdates () {
return ['available', 'downloading', 'downloaded'].includes(this.updateStatus);
}
},
created () {
@@ -195,6 +201,11 @@ export default {
background: #32b643;
}
.badge-update::after {
bottom: initial;
background: $primary-color;
}
.form-label {
display: flex;
align-items: center;

View File

@@ -1,7 +1,7 @@
<template>
<div class="empty">
<div class="empty-icon">
<i class="material-icons md-48">system_update_alt</i>
<i class="mdi mdi-48px mdi-cloud-download" />
</div>
<p class="empty-title h5">
{{ updateMessage }}

View File

@@ -1,13 +1,13 @@
<template>
<BaseContextMenu
:context-event="contextEvent"
@closeContext="$emit('closeContext')"
@close-context="$emit('close-context')"
>
<div class="context-element" @click="showEditModal(contextConnection)">
<i class="material-icons md-18 text-light pr-1">edit</i> {{ $t('word.edit') }}
<i class="mdi mdi-18px mdi-pencil text-light pr-1" /> {{ $t('word.edit') }}
</div>
<div class="context-element" @click="showConfirmModal">
<i class="material-icons md-18 text-light pr-1">delete</i> {{ $t('word.delete') }}
<i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('word.delete') }}
</div>
<ConfirmModal
@@ -16,7 +16,9 @@
@hide="hideConfirmModal"
>
<template :slot="'header'">
{{ $t('message.deleteConnection') }}
<div class="d-flex">
<i class="mdi mdi-24px mdi-server-remove mr-1" /> {{ $t('message.deleteConnection') }}
</div>
</template>
<div :slot="'body'">
<div class="mb-2">

View File

@@ -2,7 +2,7 @@
<div class="columns">
<div class="column col-12 empty text-light">
<div class="empty-icon">
<i class="material-icons md-48">mood</i>
<i class="mdi mdi-48px mdi-emoticon" />
</div>
<p class="empty-title h5">
{{ $t('message.appWelcome') }}

View File

@@ -3,7 +3,7 @@
<div class="footer-left-elements">
<ul class="footer-elements">
<li class="footer-element">
<i class="material-icons md-18 mr-1">memory</i>
<i class="mdi mdi-18px mdi-memory mr-1" />
<small>{{ appVersion }}</small>
</li>
</ul>
@@ -12,14 +12,14 @@
<div class="footer-right-elements">
<ul class="footer-elements">
<li class="footer-element footer-link" @click="openOutside('https://www.patreon.com/fabio286')">
<i class="material-icons md-18 mr-1">favorite</i>
<i class="mdi mdi-18px mdi-coffee mr-1" />
<small>{{ $t('word.donate') }}</small>
</li>
<li class="footer-element footer-link" @click="openOutside('https://github.com/EStarium/antares/issues')">
<i class="material-icons md-18">bug_report</i>
<i class="mdi mdi-18px mdi-bug" />
</li>
<li class="footer-element footer-link" @click="showSettingModal('about')">
<i class="material-icons md-18">info_outline</i>
<i class="mdi mdi-18px mdi-information-outline" />
</li>
</ul>
</div>

View File

@@ -77,7 +77,7 @@ export default {
<style lang="scss">
#notifications-board {
position: absolute;
z-index: 9;
z-index: 999;
right: 1rem;
bottom: 1rem;
}

View File

@@ -5,7 +5,7 @@
v-if="isContext"
:context-event="contextEvent"
:context-connection="contextConnection"
@closeContext="isContext = false"
@close-context="isContext = false"
/>
<ul class="settingbar-elements">
<draggable v-model="connections">
@@ -28,7 +28,7 @@
@click="showNewConnModal"
@mouseover.self="tooltipPosition"
>
<i class="settingbar-element-icon material-icons text-light">add</i>
<i class="settingbar-element-icon mdi mdi-24px mdi-plus text-light" />
<span class="ex-tooltip-content">{{ $t('message.addConnection') }}</span>
</li>
</ul>
@@ -37,7 +37,7 @@
<div class="settingbar-bottom-elements">
<ul class="settingbar-elements">
<li class="settingbar-element btn btn-link ex-tooltip" @click="showSettingModal('general')">
<i class="settingbar-element-icon material-icons text-light">settings</i>
<i class="settingbar-element-icon mdi mdi-24px mdi-cog text-light" :class="{' badge badge-update': hasUpdates}" />
<span class="ex-tooltip-content">{{ $t('word.settings') }}</span>
</li>
</ul>
@@ -70,7 +70,8 @@ export default {
getConnections: 'connections/getConnections',
getConnectionName: 'connections/getConnectionName',
connected: 'workspaces/getConnected',
selectedWorkspace: 'workspaces/getSelected'
selectedWorkspace: 'workspaces/getSelected',
updateStatus: 'application/getUpdateStatus'
}),
connections: {
get () {
@@ -79,6 +80,9 @@ export default {
set (value) {
this.updateConnections(value);
}
},
hasUpdates () {
return ['available', 'downloading', 'downloaded'].includes(this.updateStatus);
}
},
methods: {
@@ -169,6 +173,11 @@ export default {
position: absolute;
background: $success-color;
}
&.badge-update::after {
bottom: initial;
background: $primary-color;
}
}
}
}

View File

@@ -4,7 +4,7 @@
<div class="titlebar-elements">
<img class="titlebar-logo" :src="require('@/images/logo.svg').default">
</div>
<div class="titlebar-elements">
<div class="titlebar-elements titlebar-title">
{{ windowTitle }}
</div>
<div class="titlebar-elements">
@@ -13,24 +13,24 @@
class="titlebar-element"
@click="openDevTools"
>
<i class="material-icons">code</i>
<i class="mdi mdi-24px mdi-code-tags" />
</div>
<div
v-if="isDevelopment"
class="titlebar-element"
@click="reload"
>
<i class="material-icons">refresh</i>
<i class="mdi mdi-24px mdi-refresh" />
</div>
<div class="titlebar-element" @click="minimizeApp">
<i class="material-icons">remove</i>
<i class="mdi mdi-24px mdi-minus" />
</div>
<div class="titlebar-element" @click="toggleFullScreen">
<i v-if="isMaximized" class="material-icons">fullscreen_exit</i>
<i v-else class="material-icons">fullscreen</i>
<i v-if="isMaximized" class="mdi mdi-24px mdi-fullscreen-exit" />
<i v-else class="mdi mdi-24px mdi-fullscreen" />
</div>
<div class="titlebar-element close-button" @click="closeApp">
<i class="material-icons">close</i>
<i class="mdi mdi-24px mdi-close" />
</div>
</div>
</div>
@@ -123,6 +123,15 @@ export default {
display: flex;
align-items: center;
&.titlebar-title {
position: absolute;
left: 0;
right: 0;
text-align: center;
display: block;
pointer-events: none;
}
.titlebar-logo {
height: $titlebar-height;
padding: 0 0.4rem;

View File

@@ -3,6 +3,15 @@
<WorkspaceExploreBar :connection="connection" :is-selected="isSelected" />
<div v-if="workspace.connected" class="workspace-tabs column columns col-gapless">
<ul class="tab tab-block column col-12">
<!-- <li
v-if="workspace.breadcrumbs.table"
class="tab-item"
>
<a class="tab-link">
<i class="mdi mdi-18px mdi-tune mr-1" />
<span :title="workspace.breadcrumbs.table">{{ $t('word.properties').toUpperCase() }}: {{ workspace.breadcrumbs.table }}</span>
</a>
</li> -->
<li
v-if="workspace.breadcrumbs.table"
class="tab-item"
@@ -10,8 +19,8 @@
@click="selectTab({uid: workspace.uid, tab: 1})"
>
<a class="tab-link">
<i class="material-icons md-18 mr-1">grid_on</i>
<span :title="workspace.breadcrumbs.table">{{ workspace.breadcrumbs.table }}</span>
<i class="mdi mdi-18px mdi-table mr-1" />
<span :title="workspace.breadcrumbs.table">{{ $t('word.data').toUpperCase() }}: {{ workspace.breadcrumbs.table }}</span>
</a>
</li>
<li
@@ -33,6 +42,7 @@
v-for="tab of queryTabs"
v-show="selectedTab === tab.uid"
:key="tab.uid"
:tab-uid="tab.uid"
:connection="connection"
/>
</div>
@@ -109,10 +119,6 @@ export default {
width: fit-content;
flex: initial;
&.active a {
opacity: 1;
}
> a {
padding: 0.2rem 0.8rem;
color: $body-font-color;
@@ -132,6 +138,10 @@ export default {
text-overflow: ellipsis;
}
}
&.active a {
opacity: 1;
}
}
}
}
@@ -155,6 +165,7 @@ export default {
padding: 0;
font-weight: 700;
font-size: 0.7rem;
z-index: 1;
> div {
padding: 0.1rem 0.4rem;
@@ -172,6 +183,7 @@ export default {
white-space: nowrap;
overflow: hidden;
font-size: 0.7rem;
position: relative;
&:focus {
box-shadow: inset 0 0 0 1px $body-font-color;

View File

@@ -2,7 +2,7 @@
<div class="columns">
<div class="column col-12 empty text-light">
<div class="empty-icon">
<i class="material-icons md-48">cloud_off</i>
<i class="mdi mdi-48px mdi-power-plug-off" />
</div>
<p class="empty-title h5">
{{ $t('word.disconnected') }}

View File

@@ -10,16 +10,16 @@
<span class="workspace-explorebar-title">{{ connectionName }}</span>
<span v-if="workspace.connected" class="workspace-explorebar-tools">
<i
class="material-icons md-18 c-hand"
class="mdi mdi-18px mdi-refresh c-hand"
:class="{'rotate':isRefreshing}"
:title="$t('word.refresh')"
@click="refresh"
>refresh</i>
/>
<i
class="material-icons md-18 c-hand mr-1 ml-2"
class="mdi mdi-18px mdi-power-plug-off c-hand mr-1 ml-2"
:title="$t('word.disconnect')"
@click="disconnectWorkspace(connection.uid)"
>exit_to_app</i>
/>
</span>
</div>
<WorkspaceConnectPanel

View File

@@ -5,8 +5,8 @@
:class="{'text-bold': breadcrumbs.schema === database.name}"
@click="changeBreadcrumbs({schema: database.name, table:null})"
>
<i class="icon material-icons md-18 mr-1">navigate_next</i>
<i class="material-icons md-18 mr-1">view_agenda</i>
<i class="icon mdi mdi-18px mdi-chevron-right" />
<i class="database-icon mdi mdi-18px mdi-database mr-1" />
<span>{{ database.name }}</span>
</summary>
<div class="accordion-body">
@@ -20,7 +20,7 @@
@click="changeBreadcrumbs({schema: database.name, table: table.TABLE_NAME})"
>
<a class="table-name">
<i class="material-icons md-18 mr-1">grid_on</i>
<i class="table-icon mdi mdi-18px mdi-table mr-1" />
<span>{{ table.TABLE_NAME }}</span>
</a>
</li>
@@ -77,6 +77,11 @@ export default {
background: rgba($color: #fff, $alpha: 0.05);
border-radius: 2px;
}
.database-icon,
.table-icon {
opacity: 0.7;
}
}
.menu-item {

View File

@@ -11,7 +11,7 @@
@click="runQuery(query)"
>
<span>{{ $t('word.run') }}</span>
<i class="material-icons text-success">play_arrow</i>
<i class="mdi mdi-24px mdi-play text-success" />
</button>
</div>
<div class="workspace-query-info">
@@ -27,9 +27,10 @@
<div class="workspace-query-results column col-12">
<WorkspaceQueryTable
v-if="results"
v-show="!isQuering"
ref="queryTable"
:results="results"
:fields="fields"
:tab-uid="tabUid"
@updateField="updateField"
@deleteSelected="deleteSelected"
/>
@@ -53,7 +54,8 @@ export default {
},
mixins: [tableTabs],
props: {
connection: Object
connection: Object,
tabUid: String
},
data () {
return {
@@ -61,7 +63,7 @@ export default {
lastQuery: '',
isQuering: false,
results: {},
fields: []
selectedFields: []
};
},
computed: {
@@ -72,30 +74,40 @@ export default {
return this.getWorkspace(this.connection.uid);
},
table () {
if (this.results.fields.length)
if ('fields' in this.results && this.results.fields.length)
return this.results.fields[0].orgTable;
return '';
},
schema () {
if ('fields' in this.results && this.results.fields.length)
return this.results.fields[0].db;
return this.workspace.breadcrumbs.schema;
}
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
addNotification: 'notifications/addNotification',
setTabFields: 'workspaces/setTabFields',
setTabKeyUsage: 'workspaces/setTabKeyUsage'
}),
async runQuery (query) {
if (!query) return;
this.isQuering = true;
this.results = {};
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: [] });
try {
try { // Query Data
const params = {
uid: this.connection.uid,
query,
schema: this.workspace.breadcrumbs.schema
schema: this.schema,
query
};
const { status, response } = await Connection.rawQuery(params);
if (status === 'success')
if (status === 'success') {
this.results = response;
this.selectedFields = response.fields.map(field => field.orgName);
}
else
this.addNotification({ status: 'error', message: response });
}
@@ -103,16 +115,35 @@ export default {
this.addNotification({ status: 'error', message: err.stack });
}
try {
try { // Table data
const params = {
uid: this.connection.uid,
schema: this.workspace.breadcrumbs.schema,
schema: this.schema,
table: this.table
};
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') {
const fields = response.filter(field => this.selectedFields.includes(field.name));
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
try { // Key usage (foreign keys)
const params = {
uid: this.connection.uid,
schema: this.schema,
table: this.table
};
const { status, response } = await Tables.getKeyUsage(params);
if (status === 'success')
this.fields = response;
this.setTabKeyUsage({ cUid: this.connection.uid, tUid: this.tabUid, keyUsage: response });
else
this.addNotification({ status: 'error', message: response });
}

View File

@@ -1,87 +1,88 @@
<template>
<div>
<div
ref="tableWrapper"
class="vscroll"
:style="{'height': resultsSize+'px'}"
>
<TableContext
v-if="isContext"
:context-event="contextEvent"
:selected-rows="selectedRows"
@deleteSelected="deleteSelected"
@closeContext="isContext = false"
@close-context="isContext = false"
/>
<BaseVirtualScroll
v-if="results.rows"
ref="resultTable"
:items="sortedResults"
:item-height="25"
class="vscroll"
:style="{'height': resultsSize+'px'}"
>
<template slot-scope="{ items }">
<div class="table table-hover">
<div class="thead">
<div class="tr">
<div
v-for="field in fields"
:key="field.name"
class="th c-hand"
>
<div ref="columnResize" class="column-resizable">
<div class="table-column-title" @click="sort(field.name)">
<i
v-if="field.key"
class="material-icons column-key c-help"
:class="`key-${field.key}`"
:title="keyName(field.key)"
>vpn_key</i>
<span>{{ field.name }}</span>
<i v-if="currentSort === field.name" class="material-icons sort-icon">{{ currentSortDir === 'asc' ? 'arrow_upward':'arrow_downward' }}</i>
</div>
</div>
<div ref="table" class="table table-hover">
<div class="thead">
<div class="tr">
<div
v-for="field in fields"
:key="field.name"
class="th c-hand"
>
<div ref="columnResize" class="column-resizable">
<div class="table-column-title" @click="sort(field.name)">
<i
v-if="field.key"
class="mdi mdi-key column-key c-help"
:class="`key-${field.key}`"
:title="keyName(field.key)"
/>
<span>{{ field.name }}</span>
<i
v-if="currentSort === field.name"
class="mdi sort-icon"
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
/>
</div>
</div>
</div>
<div class="tbody">
<div
v-for="row in items"
:key="row._id"
class="tr"
:class="{'selected': selectedRows.includes(row._id)}"
@click="selectRow($event, row._id)"
>
<WorkspaceQueryTableCell
v-for="(col, cKey) in row"
:key="cKey"
:content="col"
:field="cKey"
:precision="fieldPrecision(cKey)"
:type="fieldType(cKey)"
@updateField="updateField($event, row[primaryField.name])"
@contextmenu="contextMenu($event, {id: row._id, field: cKey})"
/>
</div>
</div>
</div>
</template>
</BaseVirtualScroll>
</div>
<BaseVirtualScroll
v-if="results.rows"
ref="resultTable"
:items="sortedResults"
:item-height="22"
class="tbody"
:visible-height="resultsSize"
:scroll-element="scrollElement"
>
<template slot-scope="{ items }">
<WorkspaceQueryTableRow
v-for="row in items"
:key="row._id"
:row="row"
:fields="fields"
:key-usage="keyUsage"
class="tr"
:class="{'selected': selectedRows.includes(row._id)}"
@selectRow="selectRow($event, row._id)"
@updateField="updateField($event, row[primaryField.name])"
@contextmenu="contextMenu"
/>
</template>
</basevirtualscroll>
</div>
</div>
</template>
<script>
import { uidGen } from 'common/libs/uidGen';
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
import WorkspaceQueryTableCell from '@/components/WorkspaceQueryTableCell';
import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow';
import TableContext from '@/components/WorkspaceQueryTableContext';
import { mapActions } from 'vuex';
import { mapActions, mapGetters } from 'vuex';
export default {
name: 'WorkspaceQueryTable',
components: {
BaseVirtualScroll,
WorkspaceQueryTableCell,
WorkspaceQueryTableRow,
TableContext
},
props: {
results: Object,
fields: Array
tabUid: [String, Number]
},
data () {
return {
@@ -96,8 +97,11 @@ export default {
};
},
computed: {
...mapGetters({
getWorkspaceTab: 'workspaces/getWorkspaceTab'
}),
primaryField () {
return this.fields.filter(field => field.key === 'pri')[0] || false;
return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false;
},
sortedResults () {
if (this.currentSort) {
@@ -113,6 +117,15 @@ export default {
}
else
return this.localResults;
},
fields () {
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).fields : [];
},
keyUsage () {
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).keyUsage : [];
},
scrollElement () {
return this.$refs.tableWrapper;
}
},
watch: {
@@ -124,7 +137,7 @@ export default {
}
},
updated () {
if (this.$refs.resultTable)
if (this.$refs.table)
this.refreshScroller();
},
mounted () {
@@ -149,7 +162,7 @@ export default {
let length = 0;
const field = this.fields.filter(field => field.name === cKey)[0];
if (field)
length = field.precision;
length = field.datePrecision;
return length;
},
@@ -167,10 +180,10 @@ export default {
},
resizeResults () {
if (this.$refs.resultTable) {
const el = this.$refs.resultTable.$el;
const footer = document.getElementById('footer');
const el = this.$refs.table;
if (el) {
const footer = document.getElementById('footer');
const size = window.innerHeight - el.getBoundingClientRect().top - footer.offsetHeight;
this.resultsSize = size;
}
@@ -292,23 +305,4 @@ export default {
line-height: 1;
margin-left: 0.2rem;
}
.column-key {
transform: rotate(90deg);
font-size: 0.7rem;
line-height: 1.5;
margin-right: 0.2rem;
&.key-pri {
color: goldenrod;
}
&.key-uni {
color: deepskyblue;
}
&.key-mul {
color: palegreen;
}
}
</style>

View File

@@ -1,10 +1,10 @@
<template>
<BaseContextMenu
:context-event="contextEvent"
@closeContext="closeContext"
@close-context="closeContext"
>
<div class="context-element" @click="showConfirmModal">
<i class="material-icons md-18 text-light pr-1">delete</i> {{ $tc('message.deleteRows', selectedRows.length) }}
<i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}
</div>
<ConfirmModal
@@ -13,7 +13,9 @@
@hide="hideConfirmModal"
>
<template :slot="'header'">
{{ $tc('message.deleteRows', selectedRows.length) }}
<div class="d-flex">
<i class="mdi mdi-24px mdi-delete mr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}
</div>
</template>
<div :slot="'body'">
<div class="mb-2">
@@ -58,7 +60,7 @@ export default {
this.isConfirmModal = false;
},
closeContext () {
this.$emit('closeContext');
this.$emit('close-context');
},
deleteRows () {
this.$emit('deleteSelected');

View File

@@ -1,38 +1,51 @@
<template>
<div
v-if="field !== '_id'"
ref="cell"
class="td p-0"
tabindex="0"
@contextmenu.prevent="$emit('contextmenu', $event)"
>
<span
v-if="!isInlineEditor"
class="cell-content px-2"
:class="`${isNull(content)} type-${type}`"
@dblclick="editON"
>{{ content | typeFormat(type, precision) | cutText }}</span>
<template v-else>
<input
v-if="inputProps.mask"
ref="editField"
v-model="localContent"
v-mask="inputProps.mask"
:type="inputProps.type"
autofocus
class="editable-field px-2"
@blur="editOFF"
>
<input
v-else
ref="editField"
v-model="localContent"
:type="inputProps.type"
autofocus
class="editable-field px-2"
@blur="editOFF"
>
</template>
<div class="tr" @click="selectRow($event, row._id)">
<div
v-for="(col, cKey) in row"
v-show="cKey !== '_id'"
:key="cKey"
class="td p-0"
tabindex="0"
@contextmenu.prevent="$emit('contextmenu', $event, {id: row._id, field: cKey})"
@updateField="updateField($event, row[primaryField.name])"
>
<template v-if="cKey !== '_id'">
<span
v-if="!isInlineEditor[cKey]"
class="cell-content px-2"
:class="`${isNull(col)} type-${fieldType(cKey)}`"
@dblclick="editON($event, col, cKey)"
>{{ col | typeFormat(fieldType(cKey), fieldPrecision(cKey)) | cutText }}</span>
<ForeignKeySelect
v-else-if="foreignKeys.includes(cKey)"
class="editable-field"
:value.sync="editingContent"
:key-usage="getKeyUsage(cKey)"
@blur="editOFF"
/>
<template v-else>
<input
v-if="inputProps.mask"
ref="editField"
v-model="editingContent"
v-mask="inputProps.mask"
:type="inputProps.type"
autofocus
class="editable-field px-2"
@blur="editOFF"
>
<input
v-else
ref="editField"
v-model="editingContent"
:type="inputProps.type"
autofocus
class="editable-field px-2"
@blur="editOFF"
>
</template>
</template>
</div>
<ConfirmModal
v-if="isTextareaEditor"
:confirm-text="$t('word.update')"
@@ -41,19 +54,21 @@
@hide="hideEditorModal"
>
<template :slot="'header'">
{{ $t('word.edit') }} "{{ field }}"
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> {{ $t('word.edit') }} "{{ editingField }}"
</div>
</template>
<div :slot="'body'">
<div class="mb-2">
<div>
<textarea
v-model="localContent"
v-model="editingContent"
class="form-input textarea-editor"
/>
</div>
<div class="editor-field-info">
<div><b>{{ $t('word.size') }}</b>: {{ localContent.length }}</div>
<div><b>{{ $t('word.type') }}</b>: {{ type.toUpperCase() }}</div>
<div><b>{{ $t('word.size') }}</b>: {{ editingContent.length }}</div>
<div><b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}</div>
</div>
</div>
</div>
@@ -65,7 +80,9 @@
@hide="hideEditorModal"
>
<template :slot="'header'">
{{ $t('word.edit') }} "{{ field }}"
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> {{ $t('word.edit') }} "{{ editingField }}"
</div>
</template>
<div :slot="'body'">
<div class="mb-2">
@@ -73,30 +90,30 @@
<div v-if="contentInfo.size">
<img
v-if="isImage"
:src="`data:${contentInfo.mime};base64, ${bufferToBase64(localContent)}`"
:src="`data:${contentInfo.mime};base64, ${bufferToBase64(editingContent)}`"
class="img-responsive p-centered bg-checkered"
>
<div v-else class="text-center">
<i class="material-icons md-36">insert_drive_file</i>
<i class="mdi mdi-36px mdi-file" />
</div>
<div class="editor-buttons mt-2">
<button class="btn btn-link btn-sm" @click="downloadFile">
<span>{{ $t('word.download') }}</span>
<i class="material-icons ml-1">file_download</i>
<i class="mdi mdi-24px mdi-download ml-1" />
</button>
<button class="btn btn-link btn-sm" @click="prepareToDelete">
<span>{{ $t('word.delete') }}</span>
<i class="material-icons ml-1">delete_forever</i>
<i class="mdi mdi-24px mdi-delete-forever ml-1" />
</button>
</div>
</div>
</transition>
<div class="editor-field-info">
<div>
<b>{{ $t('word.size') }}</b>: {{ localContent.length | formatBytes }}<br>
<b>{{ $t('word.size') }}</b>: {{ editingContent.length | formatBytes }}<br>
<b>{{ $t('word.mimeType') }}</b>: {{ contentInfo.mime }}
</div>
<div><b>{{ $t('word.type') }}</b>: {{ type.toUpperCase() }}</div>
<div><b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}</div>
</div>
<div class="mt-3">
<label>{{ $t('message.uploadFile') }}</label>
@@ -121,11 +138,16 @@ import hexToBinary from 'common/libs/hexToBinary';
import { TEXT, LONG_TEXT, NUMBER, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
import { mask } from 'vue-the-mask';
import ConfirmModal from '@/components/BaseConfirmModal';
import ForeignKeySelect from '@/components/ForeignKeySelect';
export default {
name: 'WorkspaceQueryTableCell',
name: 'WorkspaceQueryTableRow',
components: {
ConfirmModal
ConfirmModal,
ForeignKeySelect
},
directives: {
mask
},
filters: {
formatBytes,
@@ -163,22 +185,21 @@ export default {
return val;
}
},
directives: {
mask
},
props: {
type: String,
field: String,
precision: [Number, null],
content: [String, Number, Object, Date, Uint8Array]
row: Object,
fields: Array,
keyUsage: Array
},
data () {
return {
isInlineEditor: false,
isInlineEditor: {},
isTextareaEditor: false,
isBlobEditor: false,
willBeDeleted: false,
localContent: null,
originalContent: null,
editingContent: null,
editingType: null,
editingField: null,
contentInfo: {
ext: '',
mime: '',
@@ -189,66 +210,105 @@ export default {
},
computed: {
inputProps () {
if ([...TEXT, ...LONG_TEXT].includes(this.type))
if ([...TEXT, ...LONG_TEXT].includes(this.editingType))
return { type: 'text', mask: false };
if (NUMBER.includes(this.type))
if (NUMBER.includes(this.editingType))
return { type: 'number', mask: false };
if (TIME.includes(this.type))
return { type: 'number', mask: false };
if (TIME.includes(this.editingType)) {
let timeMask = '##:##:##';
const precision = this.fieldPrecision(this.editingField);
if (DATE.includes(this.type))
for (let i = 0; i < precision; i++)
timeMask += i === 0 ? '.#' : '#';
return { type: 'text', mask: timeMask };
}
if (DATE.includes(this.editingType))
return { type: 'text', mask: '####-##-##' };
if (DATETIME.includes(this.type)) {
if (DATETIME.includes(this.editingType)) {
let datetimeMask = '####-##-## ##:##:##';
for (let i = 0; i < this.precision; i++)
const precision = this.fieldPrecision(this.editingField);
for (let i = 0; i < precision; i++)
datetimeMask += i === 0 ? '.#' : '#';
return { type: 'text', mask: datetimeMask };
}
if (BLOB.includes(this.type))
if (BLOB.includes(this.editingType))
return { type: 'file', mask: false };
if (BIT.includes(this.type))
if (BIT.includes(this.editingType))
return { type: 'text', mask: false };
return { type: 'text', mask: false };
},
isImage () {
return ['gif', 'jpg', 'png', 'bmp', 'ico', 'tif'].includes(this.contentInfo.ext);
},
foreignKeys () {
return this.keyUsage.map(key => key.column);
}
},
created () {
this.fields.forEach(field => {
this.isInlineEditor[field.name] = false;
});
},
methods: {
fieldType (cKey) {
let type = 'unknown';
const field = this.fields.filter(field => field.name === cKey)[0];
if (field)
type = field.type;
return type;
},
fieldPrecision (cKey) {
let length = 0;
const field = this.fields.filter(field => field.name === cKey)[0];
if (field)
length = field.datePrecision;
return length;
},
isNull (value) {
return value === null ? ' is-null' : '';
},
bufferToBase64 (val) {
return bufferToBase64(val);
},
editON () {
if (LONG_TEXT.includes(this.type)) {
editON (event, content, field) {
const type = this.fieldType(field);
this.originalContent = content;
this.editingType = type;
this.editingField = field;
if (LONG_TEXT.includes(type)) {
this.isTextareaEditor = true;
this.localContent = this.$options.filters.typeFormat(this.content, this.type);
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type);
return;
}
if (BLOB.includes(this.type)) {
if (BLOB.includes(type)) {
this.isBlobEditor = true;
this.localContent = this.content ? this.content : '';
this.editingContent = this.originalContent || '';
this.fileToUpload = null;
this.willBeDeleted = false;
if (this.content !== null) {
const buff = Buffer.from(this.localContent);
if (this.originalContent !== null) {
const buff = Buffer.from(this.editingContent);
if (buff.length) {
const hex = buff.toString('hex').substring(0, 8).toUpperCase();
const { ext, mime } = mimeFromHex(hex);
this.contentInfo = {
ext,
mime,
size: this.localContent.length
size: this.editingContent.length
};
}
}
@@ -256,20 +316,24 @@ export default {
}
// Inline editable fields
this.localContent = this.$options.filters.typeFormat(this.content, this.type);
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type, this.fieldPrecision(field));
this.$nextTick(() => { // Focus on input
this.$refs.cell.blur();
event.target.blur();
this.$nextTick(() => this.$refs.editField.focus());
this.$nextTick(() => document.querySelector('.editable-field').focus());
});
this.isInlineEditor = true;
const obj = {
[field]: true
};
this.isInlineEditor = { ...this.isInlineEditor, ...obj };
},
editOFF () {
this.isInlineEditor = false;
this.isInlineEditor[this.editingField] = false;
let content;
if (!['blob', 'mediumblob', 'longblob'].includes(this.type)) {
if (this.localContent === this.$options.filters.typeFormat(this.content, this.type)) return;// If not changed
content = this.localContent;
if (!BLOB.includes(this.editingType)) {
if (this.editingContent === this.$options.filters.typeFormat(this.originalContent, this.editingType)) return;// If not changed
content = this.editingContent;
}
else { // Handle file upload
if (this.willBeDeleted) {
@@ -283,10 +347,13 @@ export default {
}
this.$emit('updateField', {
field: this.field,
type: this.type,
field: this.editingField,
type: this.editingType,
content
});
this.editingType = null;
this.editingField = null;
},
hideEditorModal () {
this.isTextareaEditor = false;
@@ -295,8 +362,8 @@ export default {
downloadFile () {
const downloadLink = document.createElement('a');
downloadLink.href = `data:${this.contentInfo.mime};base64, ${bufferToBase64(this.localContent)}`;
downloadLink.setAttribute('download', `${this.field}.${this.contentInfo.ext}`);
downloadLink.href = `data:${this.contentInfo.mime};base64, ${bufferToBase64(this.editingContent)}`;
downloadLink.setAttribute('download', `${this.editingField}.${this.contentInfo.ext}`);
document.body.appendChild(downloadLink);
downloadLink.click();
@@ -310,13 +377,25 @@ export default {
this.willBeDeleted = false;
},
prepareToDelete () {
this.localContent = '';
this.editingContent = '';
this.contentInfo = {
ext: '',
mime: '',
size: null
};
this.willBeDeleted = true;
},
updateField (event, id) {
this.$emit('updateField', event, id);
},
contextMenu (event, cell) {
this.$emit('updateField', event, cell);
},
selectRow (event, row) {
this.$emit('selectRow', event, row);
},
getKeyUsage (keyName) {
return this.keyUsage.find(key => key.column === keyName);
}
}
};
@@ -328,6 +407,9 @@ export default {
border: none;
line-height: 1;
width: 100%;
position: absolute;
left: 0;
right: 0;
}
.cell-content {

View File

@@ -9,7 +9,7 @@
@click="reloadTable"
>
<span>{{ $t('word.refresh') }}</span>
<i class="material-icons ml-1">refresh</i>
<i class="mdi mdi-24px mdi-refresh ml-1" />
</button>
<button
class="btn btn-link btn-sm"
@@ -17,7 +17,7 @@
@click="showAddModal"
>
<span>{{ $t('word.add') }}</span>
<i class="material-icons ml-1">playlist_add</i>
<i class="mdi mdi-24px mdi-playlist-plus ml-1" />
</button>
</div>
<div class="workspace-query-info">
@@ -35,24 +35,32 @@
v-if="results"
ref="queryTable"
:results="results"
:fields="fields"
:tab-uid="tabUid"
@updateField="updateField"
@deleteSelected="deleteSelected"
/>
</div>
<ModalNewTableRow
v-if="isAddModal"
:tab-uid="tabUid"
@hide="hideAddModal"
@reload="reloadTable"
/>
</div>
</template>
<script>
import Tables from '@/ipc-api/Tables';
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
import ModalNewTableRow from '@/components/ModalNewTableRow';
import { mapGetters, mapActions } from 'vuex';
import tableTabs from '@/mixins/tableTabs';
export default {
name: 'WorkspaceTableTab',
components: {
WorkspaceQueryTable
WorkspaceQueryTable,
ModalNewTableRow
},
mixins: [tableTabs],
props: {
@@ -61,10 +69,13 @@ export default {
},
data () {
return {
tabUid: 1,
isQuering: false,
results: {},
fields: [],
lastTable: null
keyUsage: [],
lastTable: null,
isAddModal: false
};
},
computed: {
@@ -97,12 +108,15 @@ export default {
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
addNotification: 'notifications/addNotification',
setTabFields: 'workspaces/setTabFields',
setTabKeyUsage: 'workspaces/setTabKeyUsage'
}),
async getTableData () {
if (!this.table) return;
this.isQuering = true;
this.results = {};
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: [] });
const params = {
uid: this.connection.uid,
@@ -110,10 +124,12 @@ export default {
table: this.workspace.breadcrumbs.table
};
try {
try { // Columns data
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success')
this.fields = response;
if (status === 'success') {
this.fields = response;// Needed to add new rows
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: response });
}
else
this.addNotification({ status: 'error', message: response });
}
@@ -121,7 +137,7 @@ export default {
this.addNotification({ status: 'error', message: err.stack });
}
try {
try { // Table data
const { status, response } = await Tables.getTableData(params);
if (status === 'success')
@@ -133,12 +149,30 @@ export default {
this.addNotification({ status: 'error', message: err.stack });
}
try { // Key usage (foreign keys)
const { status, response } = await Tables.getKeyUsage(params);
if (status === 'success') {
this.keyUsage = response;// Needed to add new rows
this.setTabKeyUsage({ cUid: this.connection.uid, tUid: this.tabUid, keyUsage: response });
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isQuering = false;
},
reloadTable () {
this.getTableData();
},
showAddModal () {}
showAddModal () {
this.isAddModal = true;
},
hideAddModal () {
this.isAddModal = false;
}
}
};
</script>

View File

@@ -35,7 +35,10 @@ module.exports = {
type: 'Type',
mimeType: 'Mime-Type',
download: 'Download',
add: 'Add'
add: 'Add',
data: 'Data',
properties: 'Properties',
insert: 'Insert'
},
message: {
appWelcome: 'Welcome to Antares SQL Client!',
@@ -63,7 +66,9 @@ module.exports = {
deleteRows: 'Delete row | Delete {count} rows',
confirmToDeleteRows: 'Do you confirm to delete one row? | Do you confirm to delete {count} rows?',
notificationsTimeout: 'Notifications timeout',
uploadFile: 'Upload file'
uploadFile: 'Upload file',
addNewRow: 'Add new row',
numberOfInserts: 'Number of inserts'
},
// Date and Time
short: {

View File

@@ -10,6 +10,10 @@ export default class {
return ipcRenderer.invoke('getTableData', params);
}
static getKeyUsage (params) {
return ipcRenderer.invoke('get-key-usage', params);
}
static updateTableCell (params) {
return ipcRenderer.invoke('updateTableCell', params);
}
@@ -17,4 +21,12 @@ export default class {
static deleteTableRows (params) {
return ipcRenderer.invoke('deleteTableRows', params);
}
static insertTableRows (params) {
return ipcRenderer.invoke('insertTableRows', params);
}
static getForeignList (params) {
return ipcRenderer.invoke('get-foreign-list', params);
}
}

View File

@@ -2,7 +2,7 @@
import Vue from 'vue';
import 'material-design-icons/iconfont/material-icons.css';
import '@mdi/font/css/materialdesignicons.css';
import '@/scss/main.scss';
import App from '@/App.vue';

View File

@@ -1,38 +1,9 @@
.material-icons {
// TODO: rewrite with rem
.mdi {
display: flex;
align-items: center;
justify-content: center;
/* Rules for sizing the icon. */
&.md-18 {
font-size: 18px;
}
&.md-24 {
font-size: 24px;
}
&.md-36 {
font-size: 36px;
}
&.md-48 {
font-size: 48px;
}
/* Rules for using icons as black on a light background. */
&.md-dark {
color: rgba(0, 0, 0, 0.54);
}
&.md-dark.md-inactive {
color: rgba(0, 0, 0, 0.26);
}
/* Rules for using icons as white on a dark background. */
&.md-light {
color: rgba(255, 255, 255, 1);
}
&.md-light.md-inactive {
color: rgba(255, 255, 255, 0.3);
&::before {
line-height: 1;
}
}

View File

@@ -0,0 +1,18 @@
.column-key {
transform: rotate(90deg);
font-size: 0.7rem;
line-height: 1.5;
margin-right: 0.2rem;
&.key-pri {
color: goldenrod;
}
&.key-uni {
color: deepskyblue;
}
&.key-mul {
color: palegreen;
}
}

View File

@@ -2,6 +2,7 @@
@import "variables";
@import "transitions";
@import "data-types";
@import "table-keys";
@import "fake-tables";
@import "mdi-additions";
@import "db-icons";
@@ -133,6 +134,10 @@ body {
background: $bg-color-gray;
}
.form-input:not(:placeholder-shown):invalid:focus {
background: $bg-color-gray;
}
.form-select:not([multiple]):not([size]):focus {
border-color: $primary-color;
}
@@ -156,7 +161,7 @@ body {
}
.btn.loading {
> .material-icons,
> .mdi,
> span {
visibility: hidden;
}

View File

@@ -20,7 +20,7 @@ export default {
},
actions: {
addNotification ({ commit }, payload) {
payload.uid = uidGen();
payload.uid = uidGen('N');
commit('ADD_NOTIFICATION', payload);
},
removeNotification ({ commit }, uid) {

View File

@@ -2,7 +2,7 @@
import Connection from '@/ipc-api/Connection';
import { uidGen } from 'common/libs/uidGen';
function remapStructure (structure) {
function remapStructure (structure) { // TODO: move to main process and add fields (for autocomplete purpose)
const databases = structure.map(table => table.TABLE_SCHEMA)
.filter((value, index, self) => self.indexOf(value) === index);
@@ -28,8 +28,14 @@ export default {
return null;
},
getWorkspace: state => uid => {
const workspace = state.workspaces.filter(workspace => workspace.uid === uid);
return workspace.length ? workspace[0] : {};
return state.workspaces.find(workspace => workspace.uid === uid);
},
getWorkspaceTab: (state, getters) => tUid => {
if (!getters.getSelected) return;
const workspace = state.workspaces.find(workspace => workspace.uid === getters.getSelected);
if ('tabs' in workspace)
return workspace.tabs.find(tab => tab.uid === tUid);
return {};
},
getConnected: state => {
return state.workspaces
@@ -58,9 +64,11 @@ export default {
},
NEW_TAB (state, uid) {
const newTab = {
uid: uidGen(),
uid: uidGen('T'),
selected: false,
type: 'query'
type: 'query',
fields: [],
keyUsage: []
};
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === uid) {
@@ -75,6 +83,40 @@ export default {
},
SELECT_TAB (state, { uid, tab }) {
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, selected_tab: tab } : workspace);
},
SET_TAB_FIELDS (state, { cUid, tUid, fields }) {
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === cUid) {
return {
...workspace,
tabs: workspace.tabs.map(tab => {
if (tab.uid === tUid)
return { ...tab, fields };
else
return tab;
})
};
}
else
return workspace;
});
},
SET_TAB_KEY_USAGE (state, { cUid, tUid, keyUsage }) {
state.workspaces = state.workspaces.map(workspace => {
if (workspace.uid === cUid) {
return {
...workspace,
tabs: workspace.tabs.map(tab => {
if (tab.uid === tUid)
return { ...tab, keyUsage };
else
return tab;
})
};
}
else
return workspace;
});
}
},
actions: {
@@ -115,7 +157,12 @@ export default {
uid,
connected: false,
selected_tab: 0,
tabs: [{ uid: 1, type: 'table' }],
tabs: [{
uid: 1,
type: 'table',
fields: [],
keyUsage: []
}],
structure: {},
breadcrumbs: {}
};
@@ -133,6 +180,12 @@ export default {
},
selectTab ({ commit }, payload) {
commit('SELECT_TAB', payload);
},
setTabFields ({ commit }, payload) {
commit('SET_TAB_FIELDS', payload);
},
setTabKeyUsage ({ commit }, payload) {
commit('SET_TAB_KEY_USAGE', payload);
}
}
};