mirror of
https://github.com/Fabio286/antares.git
synced 2025-06-05 21:59:22 +02:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
14c64c537c | |||
97b3563e25 | |||
e834fe31ac | |||
065de3a0a2 | |||
1573de5b1f | |||
04fc1bbee0 | |||
dea378014d | |||
3abff36136 | |||
70354aa828 | |||
372049ae64 | |||
5d271be062 | |||
07ee1ae828 | |||
cbe0e2980a | |||
0fba50c6ec | |||
420255cdd4 | |||
a8a47ed5f7 |
31
CHANGELOG.md
31
CHANGELOG.md
@@ -2,6 +2,37 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
### [0.3.2](https://github.com/Fabio286/antares/compare/v0.3.1...v0.3.2) (2021-08-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* contextual menu option to duplicate table fields ([3abff36](https://github.com/Fabio286/antares/commit/3abff3613618ddc86e1d6c898f83bcb360e8e5d9))
|
||||
* **UI:** automatic scroll on selected tab ([e834fe3](https://github.com/Fabio286/antares/commit/e834fe31ac8b73f249e3ad8654e97da834b28cfe))
|
||||
* **UI:** automatic scroll to selected tab element in left bar ([04fc1bb](https://github.com/Fabio286/antares/commit/04fc1bbee0cdc3b02288ff5d3b2c402633cae1f1))
|
||||
* **UI:** button to clear sidebar search input ([1573de5](https://github.com/Fabio286/antares/commit/1573de5b1f545328523aaaaac541f5a8046617be))
|
||||
* **UI:** query tab name based on content ([065de3a](https://github.com/Fabio286/antares/commit/065de3a0a28a9e6667df4e41ac1c5fa5561d2171))
|
||||
* **UI:** shortcuts info on empty query tab ([70354aa](https://github.com/Fabio286/antares/commit/70354aa828121004f70e91d65cb9f0a4811dce57))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* tab selected when clicking closing cross ([07ee1ae](https://github.com/Fabio286/antares/commit/07ee1ae828bb5f9971a263f2e39e73c760fed50a))
|
||||
|
||||
|
||||
### Improvements
|
||||
|
||||
* approximate table total updated on table refresh ([dea3780](https://github.com/Fabio286/antares/commit/dea378014dd45bf90f51a599b4a801e0bc22059f))
|
||||
* **UI:** loading animation on tables and table context menu improvements ([372049a](https://github.com/Fabio286/antares/commit/372049ae64232e8e61a974edc8be5b319c5c0811))
|
||||
* update italian translation ([cbe0e29](https://github.com/Fabio286/antares/commit/cbe0e2980a9d4562941aec37a57cc936a46f41d8))
|
||||
|
||||
### [0.3.1](https://github.com/Fabio286/antares/compare/v0.3.0...v0.3.1) (2021-07-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **UI:** tabs or explorebar elements selected with mouse wheel or right button ([a8a47ed](https://github.com/Fabio286/antares/commit/a8a47ed5f7d5d8cbbdd6c33ef8307d9dce5b193b))
|
||||
|
||||
## [0.3.0](https://github.com/Fabio286/antares/compare/v0.2.1...v0.3.0) (2021-07-23)
|
||||
|
||||
|
||||
|
74
README.md
74
README.md
@@ -7,7 +7,7 @@
|
||||
 [](https://actions-badge.atrox.dev/fabio286/antares/goto)   [](https://snapcraft.io/antares) [](https://snapcraft.io/antares) [](https://twitter.com/AntaresSQL) [](https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet)
|
||||
|
||||
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.
|
||||
Our 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 development state, many features will come in future updates**, and supports only MySQL/MariaDB and PostgreSQL.
|
||||
At the moment, however, there are all the features necessary to have a pleasant database management experience, so give it a chance and send us your feedback, we would really appreciate it.
|
||||
@@ -17,38 +17,49 @@ We are actively working on it, hoping to provide new cool features, improvements
|
||||
👁 To stay tuned for new releases [follow Antares SQL](https://twitter.com/AntaresSQL) on Twitter.
|
||||
🌟 Don't forget to **leave a star** if you appreciate this project.
|
||||
|
||||
## Current key features
|
||||
|
||||
- Multiple database connections at same time.
|
||||
- Database management (add/edit/delete).
|
||||
- Full tables management, including indexes and foreign keys.
|
||||
- Views, triggers, stored routines, functions and schedulers management (add/edit/delete).
|
||||
- A modern and friendly tab system; keep open every kind of tab you need in your workspace.
|
||||
- Fake table data filler to generate tons of data for test purpose.
|
||||
- Query suggestions and auto complete.
|
||||
- SSH tunnel support.
|
||||
- Dark and light theme.
|
||||
- Editor themes.
|
||||
- Scratchpad.
|
||||
- Secure password storage.
|
||||
|
||||
## Philosophy
|
||||
|
||||
Why are we developing an SQL client when there are a lot of them on the market?
|
||||
The main goal is to develop a totally free, full featured, cross platform and open source alternative, empowered by JavaScript's ecosystem.
|
||||
A modern application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons, nested tabs or submenu; productivity comes first.
|
||||
|
||||
## Installation
|
||||
|
||||
Based on your operating system you can have one or more distribution formats to choose based on your preferences.
|
||||
Since Antares SQL is a free software we haven't a budget to spend in annual licenses or certificates. This can result that on some platforms you need some additional passages to install this app.
|
||||
|
||||
### Linux
|
||||
|
||||
On Linux you can simply download and run `.AppImage` distributions, install from Snap Store or from AUR.
|
||||
|
||||
### Windows
|
||||
|
||||
On Windows you can choose between Microsoft Store and download `.exe` distribution. The latter lacks of a certificate, so to install you need to click on "More info" and then "Run anyway" on SmartScreen prompt.
|
||||
|
||||
### MacOS
|
||||
|
||||
On macOS you can run `.dmg` distribution following [this guide](https://support.apple.com/guide/mac-help/mh40616/mac) to install apps from unknown developers.
|
||||
|
||||
## Download
|
||||
|
||||
[](https://snapcraft.io/antares) [](https://aur.archlinux.org/packages/antares-sql/) [](https://www.microsoft.com/p/antares-sql-client/9nhtb9sq51r1?cid=storebadge&ocid=badge&rtc=1&activetab=pivot:overviewtab)
|
||||
🚀 **[Other Downloads](https://github.com/Fabio286/antares/releases/latest)**
|
||||
|
||||
## How to contribute
|
||||
|
||||
- 🌍 [Translate Antares](https://github.com/Fabio286/antares/wiki/Translate-Antares)
|
||||
- 📖 [Contributors Guide](https://github.com/Fabio286/antares/wiki/Contributors-Guide)
|
||||
- 🚧 [Project Board](https://github.com/users/Fabio286/projects/1)
|
||||
|
||||
## Current main features
|
||||
|
||||
- Multiple database connections at same time.
|
||||
- Database management (add/edit/delete).
|
||||
- Full tables management, including indexes and foreign keys.
|
||||
- Views, triggers, stored routines, functions and schedulers management (add/edit/delete).
|
||||
- Fake table data filler.
|
||||
- Run queries on multiple tabs.
|
||||
- Query suggestions and auto complete.
|
||||
- SSH tunnel support.
|
||||
- Dark and light theme.
|
||||
- Scratchpad.
|
||||
- Multi language.
|
||||
- Secure password storage.
|
||||
|
||||
## Coming soon
|
||||
|
||||
This is a roadmap with major features will come in near future.
|
||||
@@ -60,6 +71,7 @@ This is a roadmap with major features will come in near future.
|
||||
- More context menu shortcuts.
|
||||
- More keyboard shortcuts.
|
||||
- Import/export and migration.
|
||||
- Apple Silicon distribution
|
||||
|
||||
## Currently supported
|
||||
|
||||
@@ -78,7 +90,7 @@ This is a roadmap with major features will come in near future.
|
||||
|
||||
- [x] Windows
|
||||
- [x] Linux
|
||||
- [x] MacOS (not tested due lack of hardware)
|
||||
- [x] MacOS
|
||||
|
||||
#### • ARM
|
||||
|
||||
@@ -86,15 +98,17 @@ This is a roadmap with major features will come in near future.
|
||||
- [x] Linux
|
||||
- [ ] MacOS
|
||||
|
||||
## How to contribute
|
||||
|
||||
- 🌍 [Translate Antares](https://github.com/Fabio286/antares/wiki/Translate-Antares)
|
||||
- 📖 [Contributors Guide](https://github.com/Fabio286/antares/wiki/Contributors-Guide)
|
||||
- 🚧 [Project Board](https://github.com/users/Fabio286/projects/1)
|
||||
|
||||
## Translations
|
||||
|
||||
**Italian Translation** / [Giuseppe Gigliotti](https://github.com/ReverbOD) [[#20](https://github.com/Fabio286/antares/pull/20)]
|
||||
**Arabic Translation** / [Mohd-PH](https://github.com/Mohd-PH) [[#29](https://github.com/Fabio286/antares/pull/29)]
|
||||
**Spanish Translation** / [hongkfui](https://github.com/hongkfui) [[#32](https://github.com/Fabio286/antares/pull/32)]
|
||||
**French Translation** / [MrAnyx](https://github.com/MrAnyx) [[#44](https://github.com/Fabio286/antares/pull/44)]
|
||||
**Arabic Translation** (needs updates) / [Mohd-PH](https://github.com/Mohd-PH) [[#29](https://github.com/Fabio286/antares/pull/29)]
|
||||
**Spanish Translation** (needs updates) / [hongkfui](https://github.com/hongkfui) [[#32](https://github.com/Fabio286/antares/pull/32)]
|
||||
**French Translation** (needs updates) / [MrAnyx](https://github.com/MrAnyx) [[#44](https://github.com/Fabio286/antares/pull/44)]
|
||||
**Portugues (Brasil)** / [Daniel Eduardo](https://github.com/daeleduardo) [[#54](https://github.com/Fabio286/antares/pull/54)]
|
||||
**Deutsch (Deutschland)** / [Christian Ratz](https://github.com/digitalgopnik) [[#74](https://github.com/Fabio286/antares/pull/74)]
|
||||
|
||||
## Reviews
|
||||
|
||||
<a target="_blank" href="https://www.softx64.com/windows/antares-sql-client.html" title="Antares SQL Client review"><img src="https://www.softx64.com/softx64-review.png" alt="Antares SQL Client review" /></a>
|
||||
|
22
package.json
22
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "antares",
|
||||
"productName": "Antares",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.2",
|
||||
"description": "A cross-platform easy to use SQL client.",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/Fabio286/antares.git",
|
||||
@@ -87,17 +87,17 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "^1.2.0",
|
||||
"@electron/remote": "^1.2.1",
|
||||
"@mdi/font": "^5.9.55",
|
||||
"ace-builds": "^1.4.12",
|
||||
"electron-log": "^4.3.5",
|
||||
"electron-log": "^4.4.1",
|
||||
"electron-store": "^8.0.0",
|
||||
"electron-updater": "^4.3.9",
|
||||
"faker": "^5.5.3",
|
||||
"marked": "^2.1.1",
|
||||
"moment": "^2.29.1",
|
||||
"mysql2": "^2.2.5",
|
||||
"pg": "^8.5.1",
|
||||
"mysql2": "^2.3.0",
|
||||
"pg": "^8.7.1",
|
||||
"pgsql-ast-parser": "^7.2.1",
|
||||
"source-map-support": "^0.5.16",
|
||||
"spectre.css": "^0.5.9",
|
||||
@@ -109,22 +109,22 @@
|
||||
"vuex": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.14.5",
|
||||
"@babel/eslint-parser": "^7.15.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"electron": "^13.1.2",
|
||||
"electron": "^13.1.8",
|
||||
"electron-builder": "^22.11.7",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-webpack": "^2.8.2",
|
||||
"electron-webpack-vue": "^2.4.0",
|
||||
"eslint": "^7.29.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-vue": "^7.11.1",
|
||||
"sass": "^1.35.1",
|
||||
"eslint-plugin-vue": "^7.15.1",
|
||||
"sass": "^1.37.5",
|
||||
"sass-loader": "^10.2.0",
|
||||
"standard-version": "^9.3.0",
|
||||
"standard-version": "^9.3.1",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
"stylelint-scss": "^3.20.1",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
import { app, BrowserWindow, /* session, */ nativeImage } from 'electron';
|
||||
import { app, BrowserWindow, /* session, */ nativeImage, Menu } from 'electron';
|
||||
import * as path from 'path';
|
||||
import Store from 'electron-store';
|
||||
|
||||
@@ -96,6 +96,7 @@ else {
|
||||
// create main BrowserWindow when electron is ready
|
||||
app.on('ready', async () => {
|
||||
mainWindow = await createMainWindow();
|
||||
Menu.setApplicationMenu(null);
|
||||
if (isDevelopment)
|
||||
mainWindow.webContents.openDevTools();
|
||||
});
|
||||
|
@@ -38,6 +38,16 @@ export default (connections) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-table-count', async (event, params) => {
|
||||
try {
|
||||
const result = await connections[params.uid].getTableApproximateCount(params);
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-table-indexes', async (event, params) => {
|
||||
try {
|
||||
const result = await connections[params.uid].getTableIndexes(params);
|
||||
|
@@ -424,6 +424,19 @@ export class MySQLClient extends AntaresCore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {String} params.schema
|
||||
* @param {String} params.table
|
||||
* @returns {Object} table row count
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async getTableApproximateCount ({ schema, table }) {
|
||||
const { rows } = await this.raw(`SELECT table_rows "count" FROM information_schema.tables WHERE table_name = "${table}" AND table_schema = "${schema}"`);
|
||||
|
||||
return rows.length ? rows[0].count : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {String} params.schema
|
||||
|
@@ -293,6 +293,19 @@ export class PostgreSQLClient extends AntaresCore {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {String} params.schema
|
||||
* @param {String} params.table
|
||||
* @returns {Object} table row count
|
||||
* @memberof PostgreSQLClient
|
||||
*/
|
||||
async getTableApproximateCount ({ schema, table }) {
|
||||
const { rows } = await this.raw(`SELECT reltuples AS count FROM pg_class WHERE relname = '${table}'`);
|
||||
|
||||
return rows.length ? rows[0].count : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {String} params.schema
|
||||
|
@@ -29,18 +29,23 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
position () {
|
||||
const { clientY, clientX } = this.contextEvent;
|
||||
let topCord = `${clientY + 2}px`;
|
||||
let leftCord = `${clientX + 5}px`;
|
||||
let topCord = 0;
|
||||
let leftCord = 0;
|
||||
|
||||
if (this.contextSize) {
|
||||
if (clientY + (this.contextSize.height < 200 ? 200 : this.contextSize.height) + 5 >= window.innerHeight) {
|
||||
topCord = `${clientY + 3 - this.contextSize.height}px`;
|
||||
this.isBottom = true;
|
||||
if (this.contextEvent) {
|
||||
const { clientY, clientX } = this.contextEvent;
|
||||
topCord = `${clientY + 2}px`;
|
||||
leftCord = `${clientX + 5}px`;
|
||||
|
||||
if (this.contextSize) {
|
||||
if (clientY + (this.contextSize.height < 200 ? 200 : this.contextSize.height) + 5 >= window.innerHeight) {
|
||||
topCord = `${clientY + 3 - this.contextSize.height}px`;
|
||||
this.isBottom = true;
|
||||
}
|
||||
|
||||
if (clientX + this.contextSize.width + 5 >= window.innerWidth)
|
||||
leftCord = `${clientX - this.contextSize.width}px`;
|
||||
}
|
||||
|
||||
if (clientX + this.contextSize.width + 5 >= window.innerWidth)
|
||||
leftCord = `${clientX - this.contextSize.width}px`;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -53,7 +58,8 @@ export default {
|
||||
window.addEventListener('keydown', this.onKey);
|
||||
},
|
||||
mounted () {
|
||||
this.contextSize = this.$refs.contextContent.getBoundingClientRect();
|
||||
if (this.$refs.contextContent)
|
||||
this.contextSize = this.$refs.contextContent.getBoundingClientRect();
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('keydown', this.onKey);
|
||||
|
@@ -20,9 +20,9 @@
|
||||
title="F5"
|
||||
@click="getProcessesList"
|
||||
>
|
||||
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh mr-1" />
|
||||
<i v-else class="mdi mdi-24px mdi-history mdi-flip-h mr-1" />
|
||||
<span>{{ $t('word.refresh') }}</span>
|
||||
<i v-if="!+autorefreshTimer" class="mdi mdi-24px mdi-refresh ml-1" />
|
||||
<i v-else class="mdi mdi-24px mdi-history mdi-flip-h ml-1" />
|
||||
</button>
|
||||
<div class="btn btn-dark btn-sm dropdown-toggle pl-0 pr-0" tabindex="0">
|
||||
<i class="mdi mdi-24px mdi-menu-down" />
|
||||
|
@@ -59,15 +59,15 @@
|
||||
<div class="column col-12 h6 text-uppercase mb-1">
|
||||
{{ $t('word.application') }}
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12 mb-2">
|
||||
<div class="column col-9 col-sm-12 mb-2">
|
||||
<div class="form-group">
|
||||
<div class="col-6 col-sm-12">
|
||||
<div class="col-7 col-sm-12">
|
||||
<label class="form-label">
|
||||
<i class="mdi mdi-18px mdi-translate mr-1" />
|
||||
{{ $t('word.language') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6 col-sm-12">
|
||||
<div class="col-5 col-sm-12">
|
||||
<select
|
||||
v-model="localLocale"
|
||||
class="form-select"
|
||||
@@ -84,12 +84,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-6 col-sm-12">
|
||||
<div class="col-7 col-sm-12">
|
||||
<label class="form-label">
|
||||
{{ $t('message.dataTabPageSize') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6 col-sm-12">
|
||||
<div class="col-5 col-sm-12">
|
||||
<select
|
||||
v-model="localPageSize"
|
||||
class="form-select"
|
||||
@@ -104,13 +104,13 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-6 col-sm-12">
|
||||
<div class="form-group mb-0">
|
||||
<div class="col-7 col-sm-12">
|
||||
<label class="form-label">
|
||||
{{ $t('message.restorePreviourSession') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6 col-sm-12">
|
||||
<div class="col-5 col-sm-12">
|
||||
<label class="form-switch d-inline-block" @click.prevent="toggleRestoreSession">
|
||||
<input type="checkbox" :checked="restoreTabs">
|
||||
<i class="form-icon" />
|
||||
@@ -118,12 +118,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-6 col-sm-12">
|
||||
<div class="col-7 col-sm-12">
|
||||
<label class="form-label">
|
||||
{{ $t('message.notificationsTimeout') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6 col-sm-12">
|
||||
<div class="col-5 col-sm-12">
|
||||
<div class="input-group">
|
||||
<input
|
||||
v-model="localTimeout"
|
||||
@@ -141,29 +141,27 @@
|
||||
<div class="column col-12 h6 mt-4 text-uppercase mb-1">
|
||||
{{ $t('word.editor') }}
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<div class="form-group">
|
||||
<div class="col-6 col-sm-12">
|
||||
<div class="column col-9 col-sm-12">
|
||||
<div class="form-group mb-0">
|
||||
<div class="col-7 col-sm-12">
|
||||
<label class="form-label">
|
||||
{{ $t('word.autoCompletion') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6 col-sm-12">
|
||||
<div class="col-5 col-sm-12">
|
||||
<label class="form-switch d-inline-block" @click.prevent="toggleAutoComplete">
|
||||
<input type="checkbox" :checked="selectedAutoComplete">
|
||||
<i class="form-icon" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-8 col-sm-12">
|
||||
<div class="form-group">
|
||||
<div class="col-6 col-sm-12">
|
||||
<div class="form-group mb-0">
|
||||
<div class="col-7 col-sm-12">
|
||||
<label class="form-label">
|
||||
{{ $t('message.wrapLongLines') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6 col-sm-12">
|
||||
<div class="col-5 col-sm-12">
|
||||
<label class="form-switch d-inline-block" @click.prevent="toggleLineWrap">
|
||||
<input type="checkbox" :checked="selectedLineWrap">
|
||||
<i class="form-icon" />
|
||||
@@ -320,7 +318,7 @@ export default {
|
||||
localTimeout: null,
|
||||
localEditorTheme: null,
|
||||
selectedTab: 'general',
|
||||
pageSizes: [40, 100, 250, 500, 1000],
|
||||
pageSizes: [30, 40, 100, 250, 500, 1000],
|
||||
editorThemes: [
|
||||
{
|
||||
group: this.$t('word.light'),
|
||||
|
@@ -117,7 +117,7 @@ export default {
|
||||
},
|
||||
getStatusBadge (uid) {
|
||||
if (this.getWorkspace(uid)) {
|
||||
const status = this.getWorkspace(uid).connection_status;
|
||||
const status = this.getWorkspace(uid).connectionStatus;
|
||||
|
||||
switch (status) {
|
||||
case 'connected':
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div v-show="isSelected" class="workspace column columns col-gapless">
|
||||
<WorkspaceExploreBar
|
||||
v-if="workspace.connection_status === 'connected'"
|
||||
v-if="workspace.connectionStatus === 'connected'"
|
||||
:connection="connection"
|
||||
:is-selected="isSelected"
|
||||
/>
|
||||
<div v-if="workspace.connection_status === 'connected'" class="workspace-tabs column columns col-gapless">
|
||||
<div v-if="workspace.connectionStatus === 'connected'" class="workspace-tabs column columns col-gapless">
|
||||
<Draggable
|
||||
ref="tabWrap"
|
||||
v-model="draggableTabs"
|
||||
@@ -18,19 +18,21 @@
|
||||
<li
|
||||
v-for="(tab, i) of draggableTabs"
|
||||
:key="i"
|
||||
:ref="selectedTab === tab.uid ? 'tab-selected' : ''"
|
||||
class="tab-item tab-draggable"
|
||||
draggable="true"
|
||||
:class="{'active': selectedTab === tab.uid}"
|
||||
@mousedown="selectTab({uid: workspace.uid, tab: tab.uid})"
|
||||
@mousedown.left="selectTab({uid: workspace.uid, tab: tab.uid})"
|
||||
@mouseup.middle="closeTab(tab)"
|
||||
>
|
||||
<a v-if="tab.type === 'query'" class="tab-link">
|
||||
<i class="mdi mdi-18px mdi-code-tags mr-1" />
|
||||
<span>
|
||||
Query #{{ tab.index }}
|
||||
<span>{{ tab.content || 'Query' | cutText }} #{{ tab.index }}</span>
|
||||
<span
|
||||
class="btn btn-clear"
|
||||
:title="$t('word.close')"
|
||||
@mousedown.left.stop
|
||||
@click.stop="closeTab(tab)"
|
||||
/>
|
||||
</span>
|
||||
@@ -42,11 +44,12 @@
|
||||
@dblclick="openAsPermanentTab(tab)"
|
||||
>
|
||||
<i class="mdi mdi-18px mr-1" :class="tab.elementType === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
|
||||
<span :title="`${$t('word.data').toUpperCase()}: ${tab.elementType}`">
|
||||
<span class=" text-italic">{{ tab.elementName }}</span>
|
||||
<span :title="`${$t('word.data').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
|
||||
<span class=" text-italic">{{ tab.elementName | cutText }}</span>
|
||||
<span
|
||||
class="btn btn-clear"
|
||||
:title="$t('word.close')"
|
||||
@mousedown.left.stop
|
||||
@click.stop="closeTab(tab)"
|
||||
/>
|
||||
</span>
|
||||
@@ -54,11 +57,12 @@
|
||||
|
||||
<a v-else-if="tab.type === 'data'" class="tab-link">
|
||||
<i class="mdi mdi-18px mr-1" :class="tab.elementType === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
|
||||
<span :title="`${$t('word.data').toUpperCase()}: ${tab.elementType}`">
|
||||
{{ tab.elementName }}
|
||||
<span :title="`${$t('word.data').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
|
||||
{{ tab.elementName | cutText }}
|
||||
<span
|
||||
class="btn btn-clear"
|
||||
:title="$t('word.close')"
|
||||
@mousedown.left.stop
|
||||
@click.stop="closeTab(tab)"
|
||||
/>
|
||||
</span>
|
||||
@@ -70,11 +74,12 @@
|
||||
:class="{'badge': tab.isChanged}"
|
||||
>
|
||||
<i class="mdi mdi-tune-vertical-variant mdi-18px mr-1" />
|
||||
<span :title="`${$t('word.settings').toUpperCase()}: ${tab.elementType}`">
|
||||
{{ tab.elementName }}
|
||||
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
|
||||
{{ tab.elementName | cutText }}
|
||||
<span
|
||||
class="btn btn-clear"
|
||||
:title="$t('word.close')"
|
||||
@mousedown.left.stop
|
||||
@click.stop="closeTab(tab)"
|
||||
/>
|
||||
</span>
|
||||
@@ -86,11 +91,12 @@
|
||||
:class="{'badge': tab.isChanged}"
|
||||
>
|
||||
<i class="mdi mdi-tune-vertical-variant mdi-18px mr-1" />
|
||||
<span :title="`${$t('word.settings').toUpperCase()}: ${tab.elementType}`">
|
||||
{{ tab.elementName }}
|
||||
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
|
||||
{{ tab.elementName | cutText }}
|
||||
<span
|
||||
class="btn btn-clear"
|
||||
:title="$t('word.close')"
|
||||
@mousedown.left.stop
|
||||
@click.stop="closeTab(tab)"
|
||||
/>
|
||||
</span>
|
||||
@@ -103,11 +109,12 @@
|
||||
@dblclick="openAsPermanentTab(tab)"
|
||||
>
|
||||
<i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
|
||||
<span :title="`${$t('word.settings').toUpperCase()}: ${tab.elementType}`">
|
||||
<span class=" text-italic">{{ tab.elementName }}</span>
|
||||
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
|
||||
<span class=" text-italic">{{ tab.elementName | cutText }}</span>
|
||||
<span
|
||||
class="btn btn-clear"
|
||||
:title="$t('word.close')"
|
||||
@mousedown.left.stop
|
||||
@click.stop="closeTab(tab)"
|
||||
/>
|
||||
</span>
|
||||
@@ -119,48 +126,16 @@
|
||||
:class="{'badge': tab.isChanged}"
|
||||
>
|
||||
<i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
|
||||
<span :title="`${$t('word.settings').toUpperCase()}: ${tab.elementType}`">
|
||||
{{ tab.elementName }}
|
||||
<span :title="`${$t('word.settings').toUpperCase()}: ${$tc(`word.${tab.elementType}`)}`">
|
||||
{{ tab.elementName | cutText }}
|
||||
<span
|
||||
class="btn btn-clear"
|
||||
:title="$t('word.close')"
|
||||
@mousedown.left.stop
|
||||
@click.stop="closeTab(tab)"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<!-- <a
|
||||
v-else-if="tab.type === 'temp-trigger-function-props'"
|
||||
class="tab-link"
|
||||
:class="{'badge': tab.isChanged}"
|
||||
@dblclick="openAsPermanentTab(tab)"
|
||||
>
|
||||
<i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
|
||||
<span :title="`${$t('word.settings').toUpperCase()}: ${tab.elementType}`">
|
||||
<span class=" text-italic">{{ tab.elementName }}</span>
|
||||
<span
|
||||
class="btn btn-clear"
|
||||
:title="$t('word.close')"
|
||||
@click.stop="closeTab(tab)"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-else-if="tab.type === 'trigger-function-props'"
|
||||
class="tab-link"
|
||||
:class="{'badge': tab.isChanged}"
|
||||
>
|
||||
<i class="mdi mdi-18px mdi-tune-vertical-variant mr-1" />
|
||||
<span :title="`${$t('word.settings').toUpperCase()}: ${tab.elementType}`">
|
||||
{{ tab.elementName }}
|
||||
<span
|
||||
class="btn btn-clear"
|
||||
:title="$t('word.close')"
|
||||
@click.stop="closeTab(tab)"
|
||||
/>
|
||||
</span>
|
||||
</a> -->
|
||||
</li>
|
||||
<li slot="header" class="tab-item dropdown tools-dropdown">
|
||||
<a
|
||||
@@ -209,12 +184,6 @@
|
||||
</a>
|
||||
</li>
|
||||
</Draggable>
|
||||
<!--<WorkspacePropsTabScheduler
|
||||
v-show="selectedTab === 'prop' && workspace.breadcrumbs.scheduler"
|
||||
:is-selected="selectedTab === 'prop'"
|
||||
:connection="connection"
|
||||
:scheduler="workspace.breadcrumbs.scheduler"
|
||||
/> -->
|
||||
<WorkspaceEmptyState v-if="!workspace.tabs.length" @new-tab="addQueryTab" />
|
||||
<template v-for="tab of workspace.tabs">
|
||||
<WorkspaceQueryTab
|
||||
@@ -344,6 +313,15 @@ export default {
|
||||
ModalProcessesList,
|
||||
ModalDiscardChanges
|
||||
},
|
||||
filters: {
|
||||
cutText (string) {
|
||||
const limit = 20;
|
||||
const escapedString = string.replace(/\s{2,}/g, ' ');
|
||||
if (escapedString.length > limit)
|
||||
return `${escapedString.substr(0, limit)}...`;
|
||||
return escapedString;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
connection: Object
|
||||
},
|
||||
@@ -384,7 +362,7 @@ export default {
|
||||
return false;
|
||||
},
|
||||
selectedTab () {
|
||||
return this.workspace.selected_tab;
|
||||
return this.workspace ? this.workspace.selectedTab : null;
|
||||
},
|
||||
queryTabs () {
|
||||
return this.workspace.tabs.filter(tab => tab.type === 'query');
|
||||
@@ -397,6 +375,20 @@ export default {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
selectedTab (newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
setTimeout(() => {
|
||||
const element = this.$refs['tab-selected'] ? this.$refs['tab-selected'][0] : null;
|
||||
if (element) {
|
||||
element.setAttribute('tabindex', '-1');
|
||||
element.focus();
|
||||
element.removeAttribute('tabindex');
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
await this.addWorkspace(this.connection.uid);
|
||||
const isInitiated = await Connection.checkConnection(this.connection.uid);
|
||||
|
@@ -8,7 +8,7 @@
|
||||
>
|
||||
<div class="workspace-explorebar-header">
|
||||
<span class="workspace-explorebar-title">{{ connectionName }}</span>
|
||||
<span v-if="workspace.connection_status === 'connected'" class="workspace-explorebar-tools">
|
||||
<span v-if="workspace.connectionStatus === 'connected'" class="workspace-explorebar-tools">
|
||||
<i
|
||||
class="mdi mdi-18px mdi-database-plus c-hand mr-2"
|
||||
:title="$t('message.createNewSchema')"
|
||||
@@ -28,14 +28,19 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="workspace-explorebar-search">
|
||||
<div v-if="workspace.connection_status === 'connected'" class="has-icon-right">
|
||||
<div v-if="workspace.connectionStatus === 'connected'" class="has-icon-right">
|
||||
<input
|
||||
v-model="searchTerm"
|
||||
class="form-input input-sm"
|
||||
type="text"
|
||||
:placeholder="$t('message.searchForElements')"
|
||||
>
|
||||
<i class="form-icon mdi mdi-magnify mdi-18px" />
|
||||
<i v-if="!searchTerm" class="form-icon mdi mdi-magnify mdi-18px" />
|
||||
<i
|
||||
v-else
|
||||
class="form-icon c-hand mdi mdi-backspace mdi-18px pr-1"
|
||||
@click="searchTerm = ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workspace-explorebar-body">
|
||||
@@ -113,7 +118,7 @@
|
||||
@reload="refresh"
|
||||
/>
|
||||
<TableContext
|
||||
v-if="isTableContext"
|
||||
v-show="isTableContext"
|
||||
:selected-schema="selectedSchema"
|
||||
:selected-table="selectedTable"
|
||||
:context-event="tableContextEvent"
|
||||
|
@@ -16,15 +16,21 @@
|
||||
<ul class="menu menu-nav pt-0">
|
||||
<li
|
||||
v-for="table of filteredTables"
|
||||
:ref="breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name) ? 'explorebar-selected' : ''"
|
||||
:key="table.name"
|
||||
class="menu-item"
|
||||
:class="{'text-bold': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}"
|
||||
@mousedown="selectTable({schema: database.name, table})"
|
||||
:class="{'selected': breadcrumbs.schema === database.name && [breadcrumbs.table, breadcrumbs.view].includes(table.name)}"
|
||||
@mousedown.left="selectTable({schema: database.name, table})"
|
||||
@dblclick="openDataTab({schema: database.name, table})"
|
||||
@contextmenu.prevent="showTableContext($event, table)"
|
||||
>
|
||||
<a class="table-name">
|
||||
<i class="table-icon mdi mdi-18px mr-1" :class="table.type === 'view' ? 'mdi-table-eye' : 'mdi-table'" />
|
||||
<div v-if="checkLoadingStatus(table.name, 'table')" class="icon loading mr-1" />
|
||||
<i
|
||||
v-else
|
||||
class="table-icon mdi mdi-18px mr-1"
|
||||
:class="table.type === 'view' ? 'mdi-table-eye' : 'mdi-table'"
|
||||
/>
|
||||
<span v-html="highlightWord(table.name)" />
|
||||
</a>
|
||||
<div
|
||||
@@ -54,8 +60,9 @@
|
||||
<li
|
||||
v-for="trigger of filteredTriggers"
|
||||
:key="trigger.name"
|
||||
:ref="breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name ? 'explorebar-selected' : ''"
|
||||
class="menu-item"
|
||||
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}"
|
||||
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.trigger === trigger.name}"
|
||||
@mousedown="selectMisc({schema: database.name, misc: trigger, type: 'trigger'})"
|
||||
@dblclick="openMiscPermanentTab({schema: database.name, misc: trigger, type: 'trigger'})"
|
||||
@contextmenu.prevent="showMiscContext($event, {...trigger, type: 'trigger'})"
|
||||
@@ -87,8 +94,9 @@
|
||||
<li
|
||||
v-for="(procedure, i) of filteredProcedures"
|
||||
:key="`${procedure.name}-${i}`"
|
||||
:ref="breadcrumbs.schema === database.name && breadcrumbs.routine === procedure.name ? 'explorebar-selected' : ''"
|
||||
class="menu-item"
|
||||
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.routine === procedure.name}"
|
||||
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.routine === procedure.name}"
|
||||
@mousedown="selectMisc({schema: database.name, misc: procedure, type: 'routine'})"
|
||||
@dblclick="openMiscPermanentTab({schema: database.name, misc: procedure, type: 'routine'})"
|
||||
@contextmenu.prevent="showMiscContext($event, {...procedure, type: 'procedure'})"
|
||||
@@ -120,8 +128,9 @@
|
||||
<li
|
||||
v-for="(func, i) of filteredTriggerFunctions"
|
||||
:key="`${func.name}-${i}`"
|
||||
:ref="breadcrumbs.schema === database.name && breadcrumbs.triggerFunction === func.name ? 'explorebar-selected' : ''"
|
||||
class="menu-item"
|
||||
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.triggerFunction === func.name}"
|
||||
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.triggerFunction === func.name}"
|
||||
@mousedown="selectMisc({schema: database.name, misc: func, type: 'triggerFunction'})"
|
||||
@dblclick="openMiscPermanentTab({schema: database.name, misc: func, type: 'triggerFunction'})"
|
||||
@contextmenu.prevent="showMiscContext($event, {...func, type: 'triggerFunction'})"
|
||||
@@ -153,8 +162,9 @@
|
||||
<li
|
||||
v-for="(func, i) of filteredFunctions"
|
||||
:key="`${func.name}-${i}`"
|
||||
:ref="breadcrumbs.schema === database.name && breadcrumbs.function === func.name ? 'explorebar-selected' : ''"
|
||||
class="menu-item"
|
||||
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
|
||||
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.function === func.name}"
|
||||
@mousedown="selectMisc({schema: database.name, misc: func, type: 'function'})"
|
||||
@dblclick="openMiscPermanentTab({schema: database.name, misc: func, type: 'function'})"
|
||||
@contextmenu.prevent="showMiscContext($event, {...func, type: 'function'})"
|
||||
@@ -186,8 +196,9 @@
|
||||
<li
|
||||
v-for="scheduler of filteredSchedulers"
|
||||
:key="scheduler.name"
|
||||
:ref="breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name ? 'explorebar-selected' : ''"
|
||||
class="menu-item"
|
||||
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}"
|
||||
:class="{'selected': breadcrumbs.schema === database.name && breadcrumbs.scheduler === scheduler.name}"
|
||||
@mousedown="selectMisc({schema: database.name, misc: scheduler, type: 'scheduler'})"
|
||||
@dblclick="openMiscPermanentTab({schema: database.name, misc: scheduler, type: 'scheduler'})"
|
||||
@contextmenu.prevent="showMiscContext($event, {...scheduler, type: 'scheduler'})"
|
||||
@@ -251,11 +262,14 @@ export default {
|
||||
filteredSchedulers () {
|
||||
return this.database.schedulers.filter(scheduler => scheduler.name.search(this.searchTerm) >= 0);
|
||||
},
|
||||
workspace () {
|
||||
return this.getWorkspace(this.connection.uid);
|
||||
},
|
||||
breadcrumbs () {
|
||||
return this.getWorkspace(this.connection.uid).breadcrumbs;
|
||||
return this.workspace.breadcrumbs;
|
||||
},
|
||||
customizations () {
|
||||
return this.getWorkspace(this.connection.uid).customizations;
|
||||
return this.workspace.customizations;
|
||||
},
|
||||
loadedSchemas () {
|
||||
return this.getLoadedSchemas(this.connection.uid);
|
||||
@@ -270,6 +284,27 @@ export default {
|
||||
return this.database.tables.reduce((acc, curr) => acc + curr.size, 0);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
breadcrumbs (newVal, oldVal) {
|
||||
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
|
||||
setTimeout(() => {
|
||||
const element = this.$refs['explorebar-selected'] ? this.$refs['explorebar-selected'][0] : null;
|
||||
if (element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const elemTop = rect.top;
|
||||
const elemBottom = rect.bottom;
|
||||
const isVisible = (elemTop >= 0) && (elemBottom <= window.innerHeight);
|
||||
|
||||
if (!isVisible) {
|
||||
element.setAttribute('tabindex', '-1');
|
||||
element.focus();
|
||||
element.removeAttribute('tabindex');
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
changeBreadcrumbs: 'workspaces/changeBreadcrumbs',
|
||||
@@ -365,6 +400,12 @@ export default {
|
||||
}
|
||||
else
|
||||
return string;
|
||||
},
|
||||
checkLoadingStatus (name, type) {
|
||||
return this.workspace.loadingElements.some(el =>
|
||||
el.name === name &&
|
||||
el.type === type &&
|
||||
el.schema === this.database.name);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -372,6 +413,10 @@ export default {
|
||||
|
||||
<style lang="scss">
|
||||
.workspace-explorebar-database {
|
||||
.selected {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.database-name {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@@ -428,7 +473,8 @@ export default {
|
||||
line-height: 1.2;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
&:hover,
|
||||
&.selected {
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
}
|
||||
|
@@ -4,28 +4,28 @@
|
||||
@close-context="closeContext"
|
||||
>
|
||||
<div
|
||||
v-if="selectedTable.type === 'table' && workspace.customizations.tableSettings"
|
||||
v-if="selectedTable && selectedTable.type === 'table' && customizations.tableSettings"
|
||||
class="context-element"
|
||||
@click="openTableSettingTab"
|
||||
>
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-tune-vertical-variant text-light pr-1" /> {{ $t('word.settings') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedTable.type === 'view' && workspace.customizations.viewSettings"
|
||||
v-if="selectedTable && selectedTable.type === 'view' && customizations.viewSettings"
|
||||
class="context-element"
|
||||
@click="openViewSettingTab"
|
||||
>
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-tune-vertical-variant text-light pr-1" /> {{ $t('word.settings') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedTable.type === 'table'"
|
||||
v-if="selectedTable && selectedTable.type === 'table'"
|
||||
class="context-element"
|
||||
@click="duplicateTable"
|
||||
>
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-table-multiple text-light pr-1" /> {{ $t('message.duplicateTable') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedTable.type === 'table'"
|
||||
v-if="selectedTable && selectedTable.type === 'table'"
|
||||
class="context-element"
|
||||
@click="showEmptyModal"
|
||||
>
|
||||
@@ -102,6 +102,9 @@ export default {
|
||||
}),
|
||||
workspace () {
|
||||
return this.getWorkspace(this.selectedWorkspace);
|
||||
},
|
||||
customizations () {
|
||||
return this.workspace && this.workspace.customizations ? this.workspace.customizations : {};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -109,6 +112,8 @@ export default {
|
||||
addNotification: 'notifications/addNotification',
|
||||
newTab: 'workspaces/newTab',
|
||||
removeTabs: 'workspaces/removeTabs',
|
||||
addLoadingElement: 'workspaces/addLoadingElement',
|
||||
removeLoadingElement: 'workspaces/removeLoadingElement',
|
||||
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
|
||||
}),
|
||||
showCreateTableModal () {
|
||||
@@ -162,6 +167,14 @@ export default {
|
||||
this.closeContext();
|
||||
},
|
||||
async duplicateTable () {
|
||||
this.closeContext();
|
||||
|
||||
this.addLoadingElement({
|
||||
name: this.selectedTable.name,
|
||||
schema: this.selectedSchema,
|
||||
type: 'table'
|
||||
});
|
||||
|
||||
try {
|
||||
const { status, response } = await Tables.duplicateTable({
|
||||
uid: this.selectedWorkspace,
|
||||
@@ -169,18 +182,30 @@ export default {
|
||||
schema: this.selectedSchema
|
||||
});
|
||||
|
||||
if (status === 'success') {
|
||||
this.closeContext();
|
||||
if (status === 'success')
|
||||
this.$emit('reload');
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
this.removeLoadingElement({
|
||||
name: this.selectedTable.name,
|
||||
schema: this.selectedSchema,
|
||||
type: 'table'
|
||||
});
|
||||
},
|
||||
async emptyTable () {
|
||||
this.closeContext();
|
||||
|
||||
this.addLoadingElement({
|
||||
name: this.selectedTable.name,
|
||||
schema: this.selectedSchema,
|
||||
type: 'table'
|
||||
});
|
||||
|
||||
try {
|
||||
const { status, response } = await Tables.truncateTable({
|
||||
uid: this.selectedWorkspace,
|
||||
@@ -188,18 +213,30 @@ export default {
|
||||
schema: this.selectedSchema
|
||||
});
|
||||
|
||||
if (status === 'success') {
|
||||
this.closeContext();
|
||||
if (status === 'success')
|
||||
this.$emit('reload');
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
this.removeLoadingElement({
|
||||
name: this.selectedTable.name,
|
||||
schema: this.selectedSchema,
|
||||
type: 'table'
|
||||
});
|
||||
},
|
||||
async deleteTable () {
|
||||
this.closeContext();
|
||||
|
||||
this.addLoadingElement({
|
||||
name: this.selectedTable.name,
|
||||
schema: this.selectedSchema,
|
||||
type: 'table'
|
||||
});
|
||||
|
||||
try {
|
||||
let res;
|
||||
|
||||
@@ -228,7 +265,6 @@ export default {
|
||||
schema: this.selectedSchema
|
||||
});
|
||||
|
||||
this.closeContext();
|
||||
this.$emit('reload');
|
||||
}
|
||||
else
|
||||
@@ -237,6 +273,12 @@ export default {
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
this.removeLoadingElement({
|
||||
name: this.selectedTable.name,
|
||||
schema: this.selectedSchema,
|
||||
type: 'table'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -81,6 +81,7 @@
|
||||
:table="table"
|
||||
:schema="schema"
|
||||
mode="table"
|
||||
@duplicate-field="duplicateField"
|
||||
@remove-field="removeField"
|
||||
@add-new-index="addNewIndex"
|
||||
@add-to-index="addToIndex"
|
||||
@@ -527,6 +528,13 @@ export default {
|
||||
return key;
|
||||
});
|
||||
},
|
||||
duplicateField (uid) {
|
||||
const fieldToClone = Object.assign({}, this.localFields.find(field => field._id === uid));
|
||||
fieldToClone._id = uidGen();
|
||||
fieldToClone.name = `${fieldToClone.name}_copy`;
|
||||
fieldToClone.order = this.localFields.length + 1;
|
||||
this.localFields = [...this.localFields, fieldToClone];
|
||||
},
|
||||
removeField (uid) {
|
||||
this.localFields = this.localFields.filter(field => field._id !== uid);
|
||||
},
|
||||
|
@@ -11,6 +11,7 @@
|
||||
:index-types="indexTypes"
|
||||
:indexes="indexes"
|
||||
@delete-selected="removeField"
|
||||
@duplicate-selected="duplicateField"
|
||||
@close-context="isContext = false"
|
||||
@add-new-index="$emit('add-new-index', $event)"
|
||||
@add-to-index="$emit('add-to-index', $event)"
|
||||
@@ -220,6 +221,9 @@ export default {
|
||||
this.contextEvent = event;
|
||||
this.isContext = true;
|
||||
},
|
||||
duplicateField () {
|
||||
this.$emit('duplicate-field', this.selectedField._id);
|
||||
},
|
||||
removeField () {
|
||||
this.$emit('remove-field', this.selectedField._id);
|
||||
},
|
||||
|
@@ -33,6 +33,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="context-element" @click="duplicateField">
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-content-duplicate text-light pr-1" /> {{ $t('word.duplicate') }}</span>
|
||||
</div>
|
||||
<div class="context-element" @click="deleteField">
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('message.deleteField') }}</span>
|
||||
</div>
|
||||
@@ -62,6 +65,10 @@ export default {
|
||||
closeContext () {
|
||||
this.$emit('close-context');
|
||||
},
|
||||
duplicateField () {
|
||||
this.$emit('duplicate-selected');
|
||||
this.closeContext();
|
||||
},
|
||||
deleteField () {
|
||||
this.$emit('delete-selected');
|
||||
this.closeContext();
|
||||
|
@@ -459,7 +459,6 @@ export default {
|
||||
: '';
|
||||
}
|
||||
else if (this.defaultValue.type === 'expression') {
|
||||
console.log(this.localRow.default);
|
||||
if (this.localRow.default.toUpperCase().includes('ON UPDATE'))
|
||||
this.defaultValue.expression = this.localRow.default.replace(/ on update.*$/i, '');
|
||||
else
|
||||
|
41
src/renderer/components/WorkspaceQueryEmptyState.vue
Normal file
41
src/renderer/components/WorkspaceQueryEmptyState.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="column col-16 text-right">
|
||||
<div class="mb-4">
|
||||
{{ $t('message.runQuery') }}
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
{{ $t('word.format') }}
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
{{ $t('word.clear') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column col-16">
|
||||
<div class="mb-4">
|
||||
<code>F5</code>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<code>CTRL</code> + <code>F8</code>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<code>CTRL</code> + <code>W</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'WorkspaceQueryEmptyState'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding-top: 15vh;
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
@@ -100,6 +100,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<WorkspaceQueryEmptyState v-if="!results.length && !isQuering" />
|
||||
<div class="workspace-query-results p-relative column col-12">
|
||||
<BaseLoader v-if="isQuering" />
|
||||
<WorkspaceQueryTable
|
||||
@@ -124,6 +125,7 @@ import Schema from '@/ipc-api/Schema';
|
||||
import QueryEditor from '@/components/QueryEditor';
|
||||
import BaseLoader from '@/components/BaseLoader';
|
||||
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
|
||||
import WorkspaceQueryEmptyState from '@/components/WorkspaceQueryEmptyState';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import tableTabs from '@/mixins/tableTabs';
|
||||
|
||||
@@ -132,7 +134,8 @@ export default {
|
||||
components: {
|
||||
BaseLoader,
|
||||
QueryEditor,
|
||||
WorkspaceQueryTable
|
||||
WorkspaceQueryTable,
|
||||
WorkspaceQueryEmptyState
|
||||
},
|
||||
mixins: [tableTabs],
|
||||
props: {
|
||||
|
@@ -130,7 +130,7 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
resultsSize: 1000,
|
||||
resultsSize: 0,
|
||||
localResults: [],
|
||||
isContext: false,
|
||||
isDeleteConfirmModal: false,
|
||||
|
@@ -112,8 +112,8 @@
|
||||
<div v-if="results.length && results[0].rows">
|
||||
{{ $t('word.results') }}: <b>{{ results[0].rows.length | localeString }}</b>
|
||||
</div>
|
||||
<div v-if="hasApproximately || (page > 1 && tableInfo.rows)">
|
||||
{{ $t('word.total') }}: <b>{{ tableInfo.rows | localeString }}</b> <small>({{ $t('word.approximately') }})</small>
|
||||
<div v-if="hasApproximately || (page > 1 && approximateCount)">
|
||||
{{ $t('word.total') }}: <b :title="$t('word.approximately')">≈ {{ approximateCount | localeString }}</b>
|
||||
</div>
|
||||
<div class="d-flex" :title="$t('word.schema')">
|
||||
<i class="mdi mdi-18px mdi-database mr-1" /><b>{{ schema }}</b>
|
||||
@@ -175,7 +175,7 @@ export default {
|
||||
},
|
||||
filters: {
|
||||
localeString (val) {
|
||||
if (val)
|
||||
if (val !== null)
|
||||
return val.toLocaleString();
|
||||
}
|
||||
},
|
||||
@@ -200,7 +200,8 @@ export default {
|
||||
refreshInterval: null,
|
||||
sortParams: {},
|
||||
page: 1,
|
||||
pageProxy: 1
|
||||
pageProxy: 1,
|
||||
approximateCount: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -232,15 +233,15 @@ export default {
|
||||
hasApproximately () {
|
||||
return this.results.length &&
|
||||
this.results[0].rows &&
|
||||
this.tableInfo &&
|
||||
this.results[0].rows.length === this.limit &&
|
||||
this.results[0].rows.length < this.tableInfo.rows;
|
||||
this.results[0].rows.length < this.approximateCount;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
schema () {
|
||||
if (this.isSelected) {
|
||||
this.page = 1;
|
||||
this.approximateCount = 0;
|
||||
this.sortParams = {};
|
||||
this.getTableData();
|
||||
this.lastTable = this.table;
|
||||
@@ -250,6 +251,7 @@ export default {
|
||||
table () {
|
||||
if (this.isSelected) {
|
||||
this.page = 1;
|
||||
this.approximateCount = 0;
|
||||
this.sortParams = {};
|
||||
this.getTableData();
|
||||
this.lastTable = this.table;
|
||||
@@ -315,6 +317,20 @@ export default {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
if (this.results.length && this.results[0].rows.length === this.limit) {
|
||||
try { // Table approximate count
|
||||
const { status, response } = await Tables.getTableApproximateCount(params);
|
||||
|
||||
if (status === 'success')
|
||||
this.approximateCount = response;
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
}
|
||||
|
||||
this.isQuering = false;
|
||||
},
|
||||
getTable () {
|
||||
|
@@ -115,7 +115,8 @@ module.exports = {
|
||||
cell: 'Cell | Cells',
|
||||
triggerFunction: 'Trigger function | Trigger functions',
|
||||
all: 'All',
|
||||
duplicate: 'Duplicate'
|
||||
duplicate: 'Duplicate',
|
||||
routine: 'Routine'
|
||||
},
|
||||
message: {
|
||||
appWelcome: 'Welcome to Antares SQL Client!',
|
||||
@@ -227,7 +228,8 @@ module.exports = {
|
||||
duplicateTable: 'Duplicate table',
|
||||
noOpenTabs: 'There are no open tabs, navigate on the left bar or:',
|
||||
noSchema: 'No schema',
|
||||
restorePreviourSession: 'Restore previous session'
|
||||
restorePreviourSession: 'Restore previous session',
|
||||
runQuery: 'Run query'
|
||||
},
|
||||
faker: {
|
||||
address: 'Address',
|
||||
|
@@ -106,7 +106,16 @@ module.exports = {
|
||||
array: 'Array',
|
||||
changelog: 'Changelog',
|
||||
format: 'Formatta',
|
||||
sshTunnel: 'SSH tunnel'
|
||||
sshTunnel: 'SSH tunnel',
|
||||
structure: 'Structure',
|
||||
small: 'Piccolo',
|
||||
medium: 'Medio',
|
||||
large: 'Largo',
|
||||
row: 'Riga | Righe',
|
||||
cell: 'Cella | Celle',
|
||||
triggerFunction: 'Funzione di trigger | Funzioni di trigger',
|
||||
all: 'Tutto',
|
||||
duplicate: 'Duplica'
|
||||
},
|
||||
message: {
|
||||
appWelcome: 'Benvenuto in Antares SQL Client!',
|
||||
@@ -212,7 +221,13 @@ module.exports = {
|
||||
deleteSchema: 'Elimina schema',
|
||||
markdownSupported: 'Markdown supportato',
|
||||
plantATree: 'Pianta un albero',
|
||||
enableSsh: 'Abilita SSH'
|
||||
dataTabPageSize: 'Grandezza pagina tab DATI',
|
||||
enableSsh: 'Abilita SSH',
|
||||
pageNumber: 'Numero pagina',
|
||||
duplicateTable: 'Duplica tabella',
|
||||
noOpenTabs: 'Non ci sono tab aperte, naviga nella barra sinistra o:',
|
||||
noSchema: 'Nessuno schema',
|
||||
restorePreviourSession: 'Ripristina sessione precedente'
|
||||
},
|
||||
faker: {
|
||||
address: 'Indirizzo',
|
||||
|
@@ -10,6 +10,10 @@ export default class {
|
||||
return ipcRenderer.invoke('get-table-data', params);
|
||||
}
|
||||
|
||||
static getTableApproximateCount (params) {
|
||||
return ipcRenderer.invoke('get-table-count', params);
|
||||
}
|
||||
|
||||
static getTableIndexes (params) {
|
||||
return ipcRenderer.invoke('get-table-indexes', params);
|
||||
}
|
||||
|
@@ -135,6 +135,11 @@
|
||||
background: $bg-color-light-dark;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #000;
|
||||
color: $body-font-color-dark;
|
||||
}
|
||||
|
||||
// Antares
|
||||
.workspace {
|
||||
.workspace-explorebar {
|
||||
@@ -162,7 +167,8 @@
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
&:hover {
|
||||
&:hover,
|
||||
&.selected {
|
||||
color: $body-font-color-dark;
|
||||
background: rgba($color: #fff, $alpha: 0.05);
|
||||
}
|
||||
|
@@ -18,20 +18,11 @@
|
||||
background: #ababab;
|
||||
}
|
||||
|
||||
#titlebar {
|
||||
background: $bg-color-light;
|
||||
box-shadow: 0 0 1px 0 #000;
|
||||
|
||||
.titlebar-elements {
|
||||
.titlebar-element {
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background: rgba($color: rgb(172, 172, 172), $alpha: 0.2);
|
||||
}
|
||||
|
||||
&.close-button:hover {
|
||||
background: red;
|
||||
}
|
||||
.menu {
|
||||
.menu-item a {
|
||||
&:hover {
|
||||
color: $body-font-color;
|
||||
background: rgba($color: #000, $alpha: 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,6 +102,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
#titlebar {
|
||||
background: $bg-color-light;
|
||||
box-shadow: 0 0 1px 0 #000;
|
||||
|
||||
.titlebar-elements {
|
||||
.titlebar-element {
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background: rgba($color: rgb(172, 172, 172), $alpha: 0.2);
|
||||
}
|
||||
|
||||
&.close-button:hover {
|
||||
background: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#settingbar {
|
||||
width: $settingbar-width;
|
||||
height: calc(100vh - #{$excluding-size});
|
||||
@@ -180,6 +189,13 @@
|
||||
background: $bg-color-light-gray;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
&:hover,
|
||||
&.selected {
|
||||
background: rgba($color: #000, $alpha: 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.table-size {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
@@ -35,14 +35,14 @@ export default {
|
||||
},
|
||||
getConnected: state => {
|
||||
return state.workspaces
|
||||
.filter(workspace => workspace.connection_status === 'connected')
|
||||
.filter(workspace => workspace.connectionStatus === 'connected')
|
||||
.map(workspace => workspace.uid);
|
||||
},
|
||||
getLoadedSchemas: state => uid => {
|
||||
return state.workspaces.find(workspace => workspace.uid === uid).loaded_schemas;
|
||||
return state.workspaces.find(workspace => workspace.uid === uid).loadedSchemas;
|
||||
},
|
||||
getSearchTerm: state => uid => {
|
||||
return state.workspaces.find(workspace => workspace.uid === uid).search_term;
|
||||
return state.workspaces.find(workspace => workspace.uid === uid).searchTerm;
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
@@ -72,9 +72,9 @@ export default {
|
||||
indexTypes,
|
||||
customizations,
|
||||
structure,
|
||||
connection_status: 'connected',
|
||||
connectionStatus: 'connected',
|
||||
tabs: cachedTabs,
|
||||
selected_tab: cachedTabs.length ? cachedTabs[0].uid : null,
|
||||
selectedTab: cachedTabs.length ? cachedTabs[0].uid : null,
|
||||
version
|
||||
}
|
||||
: workspace);
|
||||
@@ -85,8 +85,8 @@ export default {
|
||||
...workspace,
|
||||
structure: {},
|
||||
breadcrumbs: {},
|
||||
loaded_schemas: new Set(),
|
||||
connection_status: 'connecting'
|
||||
loadedSchemas: new Set(),
|
||||
connectionStatus: 'connecting'
|
||||
}
|
||||
: workspace);
|
||||
},
|
||||
@@ -96,8 +96,8 @@ export default {
|
||||
...workspace,
|
||||
structure: {},
|
||||
breadcrumbs: {},
|
||||
loaded_schemas: new Set(),
|
||||
connection_status: 'failed'
|
||||
loadedSchemas: new Set(),
|
||||
connectionStatus: 'failed'
|
||||
}
|
||||
: workspace);
|
||||
},
|
||||
@@ -107,8 +107,8 @@ export default {
|
||||
...workspace,
|
||||
structure: {},
|
||||
breadcrumbs: {},
|
||||
loaded_schemas: new Set(),
|
||||
connection_status: 'disconnected'
|
||||
loadedSchemas: new Set(),
|
||||
connectionStatus: 'disconnected'
|
||||
}
|
||||
: workspace);
|
||||
},
|
||||
@@ -180,7 +180,7 @@ export default {
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid
|
||||
? {
|
||||
...workspace,
|
||||
search_term: term
|
||||
searchTerm: term
|
||||
}
|
||||
: workspace);
|
||||
},
|
||||
@@ -292,7 +292,7 @@ export default {
|
||||
persistentStore.set(uid, state.workspaces.find(workspace => workspace.uid === uid).tabs);
|
||||
},
|
||||
SELECT_TAB (state, { uid, tab }) {
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, selected_tab: tab } : workspace);
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, selectedTab: tab } : workspace);
|
||||
},
|
||||
UPDATE_TABS (state, { uid, tabs }) {
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, tabs } : workspace);
|
||||
@@ -356,7 +356,28 @@ export default {
|
||||
ADD_LOADED_SCHEMA (state, payload) {
|
||||
state.workspaces = state.workspaces.map(workspace => {
|
||||
if (workspace.uid === payload.uid)
|
||||
workspace.loaded_schemas.add(payload.schema);
|
||||
workspace.loadedSchemas.add(payload.schema);
|
||||
return workspace;
|
||||
});
|
||||
},
|
||||
ADD_LOADING_ELEMENT (state, payload) {
|
||||
state.workspaces = state.workspaces.map(workspace => {
|
||||
if (workspace.uid === payload.uid)
|
||||
workspace.loadingElements.push(payload.element);
|
||||
return workspace;
|
||||
});
|
||||
},
|
||||
REMOVE_LOADING_ELEMENT (state, payload) {
|
||||
state.workspaces = state.workspaces.map(workspace => {
|
||||
if (workspace.uid === payload.uid) {
|
||||
const loadingElements = workspace.loadingElements.filter(el =>
|
||||
el.schema !== payload.element.schema &&
|
||||
el.name !== payload.element.name &&
|
||||
el.type !== payload.element.type
|
||||
);
|
||||
|
||||
workspace = { ...workspace, loadingElements };
|
||||
}
|
||||
return workspace;
|
||||
});
|
||||
}
|
||||
@@ -513,16 +534,17 @@ export default {
|
||||
addWorkspace ({ commit }, uid) {
|
||||
const workspace = {
|
||||
uid,
|
||||
connection_status: 'disconnected',
|
||||
selected_tab: 0,
|
||||
search_term: '',
|
||||
connectionStatus: 'disconnected',
|
||||
selectedTab: 0,
|
||||
searchTerm: '',
|
||||
tabs: [],
|
||||
structure: {},
|
||||
variables: [],
|
||||
collations: [],
|
||||
users: [],
|
||||
breadcrumbs: {},
|
||||
loaded_schemas: new Set()
|
||||
loadingElements: [],
|
||||
loadedSchemas: new Set()
|
||||
};
|
||||
|
||||
commit('ADD_WORKSPACE', workspace);
|
||||
@@ -545,6 +567,12 @@ export default {
|
||||
addLoadedSchema ({ commit, getters }, schema) {
|
||||
commit('ADD_LOADED_SCHEMA', { uid: getters.getSelected, schema });
|
||||
},
|
||||
addLoadingElement ({ commit, getters }, element) {
|
||||
commit('ADD_LOADING_ELEMENT', { uid: getters.getSelected, element });
|
||||
},
|
||||
removeLoadingElement ({ commit, getters }, element) {
|
||||
commit('REMOVE_LOADING_ELEMENT', { uid: getters.getSelected, element });
|
||||
},
|
||||
setSearchTerm ({ commit, getters }, term) {
|
||||
commit('SET_SEARCH_TERM', { uid: getters.getSelected, term });
|
||||
},
|
||||
@@ -697,7 +725,7 @@ export default {
|
||||
checkSelectedTabExists ({ state, commit }, uid) {
|
||||
const workspace = state.workspaces.find(workspace => workspace.uid === uid);
|
||||
const isSelectedExistent = workspace
|
||||
? workspace.tabs.some(tab => tab.uid === workspace.selected_tab)
|
||||
? workspace.tabs.some(tab => tab.uid === workspace.selectedTab)
|
||||
: false;
|
||||
|
||||
if (!isSelectedExistent && workspace.tabs.length)
|
||||
|
Reference in New Issue
Block a user