Compare commits
172 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e29a7206d | |||
| 689b3f299c | |||
| 02ccb990bd | |||
| 61fe4f0d57 | |||
| 1953578a33 | |||
| 543bdd79d9 | |||
| e0e1a3c8e4 | |||
| f1d84f448e | |||
| afcc9e096a | |||
| f4df1fbff4 | |||
| f54b18e652 | |||
| b1210d19ad | |||
| 721fdf09b3 | |||
| bd4a52318b | |||
| 3978865902 | |||
| c1228ee426 | |||
| d0c39dc932 | |||
| 63a8586d7c | |||
| e0a79c033e | |||
| fa12f127ce | |||
| 10916eadd5 | |||
| 7f4e8e9c8f | |||
| d06840f934 | |||
| 75f4df8b51 | |||
| e9ea6d27ae | |||
| 48019d43c3 | |||
| 04dbeb633d | |||
| 71631865c4 | |||
| d4a39cf481 | |||
| 3a71dfff64 | |||
| d8c865b3ce | |||
| 71356b798c | |||
| 66ddd1741f | |||
| 8a60f3c8a7 | |||
| 25d2a40c50 | |||
| 17b389146c | |||
| bd1f609b39 | |||
| 01e1831d57 | |||
| c341baa781 | |||
| 995a0c33c3 | |||
| 75845cb42d | |||
| 822d6acfb0 | |||
| 2c4510a717 | |||
| ac2391c91a | |||
| e805563ce5 | |||
| 88fb1d920e | |||
| dca60fad7a | |||
| 8226b05e7e | |||
| 0106331978 | |||
| f5c0e7d2e9 | |||
| 4568b24351 | |||
| 042502f41f | |||
| d342d73818 | |||
| 3b922216c1 | |||
| 10fa9b6812 | |||
| 33ccbf790b | |||
| 50b4baee4b | |||
| be57a56095 | |||
| f351453b9c | |||
| dda67d3351 | |||
| 23e1e744e8 | |||
| 4b0affe182 | |||
| ab836bc747 | |||
| cf86d7e352 | |||
| 05a36d3878 | |||
| 0417084a39 | |||
| cdfe39f226 | |||
| b73dde3a48 | |||
| 0dea597226 | |||
| 2f38928c89 | |||
| 35c7b5e952 | |||
| ba28b17263 | |||
| 51b0e004fa | |||
| 8cb59b02a8 | |||
| 38bfd130a3 | |||
| 369d90e057 | |||
| c27cdd1734 | |||
| e7e4f39311 | |||
| 0443a21e05 | |||
| 50c01886ec | |||
| a9e1219f6c | |||
| 7bc31dde70 | |||
| 65f2f1d08f | |||
| 684027eaab | |||
| 1c3ec9c3bb | |||
| 3a8ff2c05d | |||
| d5bd179c68 | |||
| 8b938a39cf | |||
| c9610cbc39 | |||
| 931733d605 | |||
| 44e5d0e195 | |||
| 08b83dc3fd | |||
| b7f261a836 | |||
| d0b4ca33c2 | |||
| 160391f5a9 | |||
| dfe4a96b02 | |||
| a3f67eb519 | |||
| 0f9d52552b | |||
| a217de4c39 | |||
| d2d85e63f6 | |||
| 7a6077b5ff | |||
| d48c4d9729 | |||
| 6d677401bf | |||
| a3d4fa2f86 | |||
| 59e19b6a22 | |||
| 1a76da40d1 | |||
| cb15ba01f0 | |||
| 78af7f136e | |||
| cc6a95b579 | |||
| 4b3f723bdc | |||
| d372e2ff76 | |||
| 4201d1cb1e | |||
| afed70ba63 | |||
| be488346c5 | |||
| eeeb688439 | |||
| b84ce77326 | |||
| 30fca423dc | |||
| fabbb31572 | |||
| ac76ac004e | |||
| 9d2051183a | |||
| 942fdb51d5 | |||
| d2600a3168 | |||
| c4248cce22 | |||
| 16f16f9fed | |||
| d49cb976bc | |||
| 6fae6a9865 | |||
| 06f3730756 | |||
| 30e1333f75 | |||
| 0d6fa98767 | |||
| ae6c9edd0d | |||
| 35de1f1c4e | |||
| 57142f4afb | |||
| cd72d65b89 | |||
| 2199ab0513 | |||
| e93f058109 | |||
| b68de49cbd | |||
| 3f05934b6b | |||
| 3a5713dbb7 | |||
| 4c43158285 | |||
| daa743b3b3 | |||
| 41f0ae18c4 | |||
| e6b8aefe5b | |||
| 8b2437cb16 | |||
| 292495ab0d | |||
| 017b137d7f | |||
| 7969030313 | |||
| c8efad4c3f | |||
| 7bf9d8f675 | |||
| e275f15f00 | |||
| 30017a5217 | |||
| 64c5cbe8c3 | |||
| b2b226573c | |||
| 69f796998f | |||
| 4d64be3ac7 | |||
| 4408b794d6 | |||
| 666da8a879 | |||
| b166342579 | |||
| 433f5bf7d2 | |||
| b8ae153ef5 | |||
| bb59c2bab7 | |||
| ab7c6c5118 | |||
| 85c1ea449e | |||
| b51d679b78 | |||
| 2a2bc9e625 | |||
| d00f059567 | |||
| 81a840347c | |||
| e691675bf9 | |||
| 9cd57c3ae1 | |||
| 0e3310a39b | |||
| 447818ac2a | |||
| dd0eb846b0 | |||
| 1b62ca4b21 |
@@ -1,12 +1,14 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve DbGate
|
||||
about: Create a report to help us improve DbGate (in ENGLISH)
|
||||
title: 'BUG: Say something here'
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Please keep communication in ENGLISH to reach more contributors.
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for DbGate
|
||||
about: Suggest an idea for DbGate (in ENGLISH)
|
||||
title: 'FEAT: '
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Please keep communication in ENGLISH to reach more contributors.
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask a question about how to do something
|
||||
about: Ask a question about how to do something (in ENGLISH)
|
||||
title: 'QUESTION: Summary of your question'
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Please keep communication in ENGLISH to reach more contributors.
|
||||
|
||||
**Details:**
|
||||
Details about your question
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: a5f52768cea7e98cae5e5b1f5fef3c47a475b8a6
|
||||
ref: 3ef5290af3820e8376e2304051b439611f789a07
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: a5f52768cea7e98cae5e5b1f5fef3c47a475b8a6
|
||||
ref: 3ef5290af3820e8376e2304051b439611f789a07
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: a5f52768cea7e98cae5e5b1f5fef3c47a475b8a6
|
||||
ref: 3ef5290af3820e8376e2304051b439611f789a07
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: a5f52768cea7e98cae5e5b1f5fef3c47a475b8a6
|
||||
ref: 3ef5290af3820e8376e2304051b439611f789a07
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: a5f52768cea7e98cae5e5b1f5fef3c47a475b8a6
|
||||
ref: 3ef5290af3820e8376e2304051b439611f789a07
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: a5f52768cea7e98cae5e5b1f5fef3c47a475b8a6
|
||||
ref: 3ef5290af3820e8376e2304051b439611f789a07
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -8,6 +8,58 @@ Builds:
|
||||
- linux - application for linux
|
||||
- win - application for Windows
|
||||
|
||||
## 7.0.6
|
||||
- ADDED: Reset password for Team Premium edition
|
||||
- ADDED: Encrypting passwords sent to frontend when using SHELL_CONNECTION=1 in Docker Community edition #1357
|
||||
|
||||
## 7.0.4
|
||||
- FIXED: MS SQL server export to CSV does not convert bit FALSE to 0 #1276
|
||||
- ADDED: MySQL FULLTEXT support #1305
|
||||
- FIXED: Error messages in Chinese will display garbled characters(MS SQL over ODBC) #1321
|
||||
- FIXED: Table's Show SQL fails to display precision and scale for NUMERIC/DECIMAL types in PostgreSQL #1325
|
||||
- FIXED: Export to Excel/CSV is broken for certain data types in v7.0.0 #1327
|
||||
- ADDED: Null value with keyboard shortcut in form view #1332
|
||||
- FIXED: Clicking into active form cell discards changes #1334
|
||||
- FIXED: Remember selection after filtering #1335
|
||||
- FIXED: Unable to use 'Group By' or one of the aggregate functions on tables containing text columns #1348
|
||||
- CHANGED: Improved custom connection color palette
|
||||
|
||||
## 7.0.3
|
||||
- FIXED: Optimalized loading MySQL primary keys #1261
|
||||
- FIXED: Test connection now works for MS Entra authentication #1315
|
||||
- FIXED: SQL Server - Unable to use 'Is Empty or Null' or 'Has Not Empty Value' filters on a field with data type TEXT #1338
|
||||
- FIXED: Play triangle too large for text-wrapped queries #1337
|
||||
- FIXED: Text wraps mid-word in form view, making it illegible #1333
|
||||
- FIXED: Cell View autodetects Form instead of Map for geometry/geography #1330
|
||||
- FIXED: Search for database in cloud connection #1329
|
||||
- ADDED: Toolstrip could be configured to the bottom of the tab #1326
|
||||
- CHANGED: Upgraded node for DbGate AWS distribution
|
||||
|
||||
## 7.0.1
|
||||
- FIXED: Foreign key actions not detected on PostgreSQL #1323
|
||||
- FIXED: Vulnerabilities in bundled dependencies: axios, cross-spawn, glob #1322
|
||||
- FIXED: The JsonB field in the cell data view always displays as null. #1320
|
||||
- ADDED: Possibility to skip computed coumn in SQL generator
|
||||
- ADDED: Improved team file editing, move between team folders
|
||||
- ADDED: Korean localization
|
||||
- FIXED: Added missing localization strings
|
||||
- ADDED: Default editor theme is part of application theme now
|
||||
|
||||
## 7.0.0
|
||||
- CHANGED: New design of application, new theme system
|
||||
- ADDED: Theme AI assistant - create custom themes using AI (Premium)
|
||||
- CHANGED: Themes are now defined in JSON files, custom themes could be shared via DbGate Cloud
|
||||
- REMOVED: Custom themes are no longer part of plugins
|
||||
- CHANGED: Huge improvements of Redis support
|
||||
- ADDED: Support for Redis JSON and Stream types
|
||||
- ADDED: Editing Redis values (Strings, Hashes, Lists, Sets, Sorted Sets, JSON, Streams)
|
||||
- ADDED: Support for Team Folders (Team Premium)
|
||||
- CHANGED: Upgraded Svelte to version 4
|
||||
- ADDED: Differentiate pinned database with same name #1306
|
||||
- ADDED: Database icons/logos for faster visual recognition #1222
|
||||
- CHANGED: Reorganized left sidebar widgets
|
||||
- ADDED: Widget for currently opened tabs
|
||||
|
||||
## 6.8.2
|
||||
- FIXED: Initialize storage database from envoronment variables failed with PostgreSQL
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ DbGate is licensed under GPL-3.0 license and is free to use for any purpose.
|
||||
* Edit table schema, indexes, primary and foreign keys
|
||||
* Compare and synchronize database structure
|
||||
* ER diagram
|
||||
* Light and dark theme, next themes available as plugins from github community
|
||||
* Light and dark theme, next themes available from DbGate Cloud
|
||||
* Huge support for work with related data - master/detail views, foreign key lookups, expanding columns from related tables in flat data view
|
||||
* Query designer - visual SQL query builder without writing SQL code. Complex conditions like WHERE NOT EXISTS.
|
||||
* Query perspectives – innovative nested table view over complex relational data, something like query designer on MongoDB databases
|
||||
@@ -94,7 +94,8 @@ Any contributions are welcome. If you want to contribute without coding, conside
|
||||
* Create some tutorial video on [youtube](https://www.youtube.com/playlist?list=PLCo7KjCVXhr0RfUSjM9wJMsp_ShL1q61A)
|
||||
* Become a backer on [GitHub sponsors](https://github.com/sponsors/dbgate) or [Open collective](https://opencollective.com/dbgate)
|
||||
* Add a SQL script to [Public Knowledge Base](https://github.com/dbgate/dbgate-knowledge-base)
|
||||
* Where a small coding is acceptable for you, you could [create plugin](https://docs.dbgate.io/plugin-development). Plugins for new themes can be created actually without JS coding
|
||||
* Where a small coding is acceptable for you, you could [create plugin](https://docs.dbgate.io/plugin-development)
|
||||
* Create a new custom theme and share it on [DbGate Cloud](https://github.com/dbgate/dbgate-knowledge-base/tree/master/folder-Themes)
|
||||
|
||||
Thank you!
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dbgate",
|
||||
"version": "6.0.0-alpha.1",
|
||||
"version": "7.0.0-alpha.1",
|
||||
"private": true,
|
||||
"author": "Jan Prochazka <jenasoft.database@gmail.com>",
|
||||
"description": "Opensource database administration tool",
|
||||
|
||||
@@ -15,6 +15,7 @@ const languageNames = {
|
||||
'fr.json': 'French',
|
||||
'it.json': 'Italian',
|
||||
'ja.json': 'Japanese',
|
||||
'ko.json': 'Korean',
|
||||
'pt.json': 'Portuguese',
|
||||
'sk.json': 'Slovak',
|
||||
'zh.json': 'Chinese'
|
||||
|
||||
@@ -49,6 +49,9 @@ module.exports = defineConfig({
|
||||
case 'charts':
|
||||
serverProcess = exec('yarn start:charts');
|
||||
break;
|
||||
case 'redis':
|
||||
serverProcess = exec('yarn start:redis');
|
||||
break;
|
||||
}
|
||||
|
||||
await waitOn({ resources: ['http://localhost:3000'] });
|
||||
|
||||
@@ -225,7 +225,6 @@ describe('Charts', () => {
|
||||
cy.contains('Default Actions').click();
|
||||
cy.get('[data-testid=DefaultActionsSettings_useLastUsedAction]').uncheck();
|
||||
|
||||
|
||||
// Themes
|
||||
cy.contains('Themes').click();
|
||||
cy.themeshot('app-settings-themes');
|
||||
@@ -256,7 +255,6 @@ describe('Charts', () => {
|
||||
cy.contains('OK').click();
|
||||
cy.contains('Ctrl+G');
|
||||
|
||||
|
||||
cy.contains('AI').click();
|
||||
cy.themeshot('app-settings-ai');
|
||||
cy.get('[data-testid=AISettings_addProviderButton]').click();
|
||||
@@ -266,4 +264,22 @@ describe('Charts', () => {
|
||||
cy.contains('OK').click();
|
||||
cy.contains('Provider 1').should('not.exist');
|
||||
});
|
||||
|
||||
it('Custom theme', () => {
|
||||
cy.testid('WidgetIconPanel_settings').click();
|
||||
cy.contains('Themes').click();
|
||||
cy.testid('ThemeSettings-themeList').contains('Green-Sample').click();
|
||||
cy.testid('WidgetIconPanel_file').click();
|
||||
cy.themeshot('green-theme', { keepTheme: true });
|
||||
|
||||
cy.testid('ThemeSettings-themeList').contains('Solarized-light').click();
|
||||
cy.testid('WidgetIconPanel_database').click();
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('MyChinook').click();
|
||||
cy.contains('Customer').click();
|
||||
cy.contains('Leonie');
|
||||
cy.testid('WidgetIconPanel_file').click();
|
||||
|
||||
cy.themeshot('solarized-theme', { keepTheme: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
Cypress.on('uncaught:exception', (err, runnable) => {
|
||||
// if the error message matches the one about WorkerGlobalScope importScripts
|
||||
if (err.message.includes("Failed to execute 'importScripts' on 'WorkerGlobalScope'")) {
|
||||
// return false to let Cypress know we intentionally want to ignore this error
|
||||
return false;
|
||||
}
|
||||
// otherwise let Cypress throw the error
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('http://localhost:3000');
|
||||
cy.viewport(1250, 900);
|
||||
});
|
||||
|
||||
describe('Redis data', () => {
|
||||
it('String test', () => {
|
||||
cy.contains('Redis-connection').click();
|
||||
cy.contains('db1').click();
|
||||
cy.contains('app').click();
|
||||
cy.contains('version').click();
|
||||
cy.testid('RedisValueDetail_AceEditor').click().realPress('Backspace').realType('1');
|
||||
cy.contains('Save').click();
|
||||
cy.contains('OK').click();
|
||||
});
|
||||
|
||||
it('Hash test', () => {
|
||||
cy.contains('Redis-connection').click();
|
||||
cy.contains('db1').click();
|
||||
cy.contains('user').click();
|
||||
cy.contains('alice').click();
|
||||
cy.testid('RedisKeyDetailTab_RenameKeyButton').click();
|
||||
cy.themeshot('redis-rename-key');
|
||||
cy.realType('3');
|
||||
cy.contains('OK').click();
|
||||
cy.contains('age').click();
|
||||
cy.testid('RedisValueHashDetail_ValueSection').click().realPress('Backspace').realType('8');
|
||||
cy.contains('Add field').click();
|
||||
cy.testid('RedisValueListLikeEdit_key').click().realType('phone');
|
||||
cy.testid('RedisValueListLikeEdit_value').click().realType('123-456-7890');
|
||||
cy.contains('Refresh').click();
|
||||
cy.themeshot('redis-hash-edit');
|
||||
cy.contains('Save').click();
|
||||
cy.themeshot('redis-hash-script-edit');
|
||||
cy.contains('OK').click();
|
||||
});
|
||||
|
||||
it('List test', () => {
|
||||
cy.contains('Redis-connection').click();
|
||||
cy.contains('db1').click();
|
||||
cy.contains('queue').click();
|
||||
cy.contains('emails').click();
|
||||
cy.contains('Add field').click();
|
||||
cy.testid('RedisValueListLikeEdit_value').click().realType('reset');
|
||||
cy.contains('Save').click();
|
||||
cy.contains('OK').click();
|
||||
});
|
||||
|
||||
it('Set test', () => {
|
||||
cy.contains('Redis-connection').click();
|
||||
cy.contains('db1').click();
|
||||
cy.contains('tags').click();
|
||||
cy.contains('Add field').click();
|
||||
cy.testid('RedisValueListLikeEdit_value').click().realType('newtag');
|
||||
cy.contains('Save').click();
|
||||
cy.contains('OK').click();
|
||||
});
|
||||
|
||||
it('ZSet test', () => {
|
||||
cy.contains('Redis-connection').click();
|
||||
cy.contains('db1').click();
|
||||
cy.contains('leaderboard').click();
|
||||
cy.contains('alice').click();
|
||||
cy.testid('RedisValueZSetDetail_score')
|
||||
.click()
|
||||
.realPress('Backspace')
|
||||
.realPress('Backspace')
|
||||
.realPress('Backspace')
|
||||
.realType('35');
|
||||
cy.contains('Save').click();
|
||||
cy.contains('OK').click();
|
||||
cy.contains('35').should('exist');
|
||||
});
|
||||
|
||||
it('JSON test', () => {
|
||||
cy.contains('Redis-connection').click();
|
||||
cy.contains('db1').click();
|
||||
cy.contains('user').click();
|
||||
cy.contains('1:*').click();
|
||||
cy.contains('json').click();
|
||||
cy.testid('RedisValueDetail_displaySelect').select('JSON view');
|
||||
cy.themeshot('redis-json-detail');
|
||||
});
|
||||
|
||||
it('Stream test', () => {
|
||||
cy.contains('Redis-connection').click();
|
||||
cy.contains('db1').click();
|
||||
cy.contains('events').click();
|
||||
cy.contains('Add field').click();
|
||||
cy.testid('RedisValueListLikeEdit_field').click().realType('message');
|
||||
cy.testid('RedisValueListLikeEdit_value').click().realType('Hello, World!');
|
||||
cy.contains('Save').click();
|
||||
cy.contains('OK').click();
|
||||
cy.themeshot('redis-stream');
|
||||
});
|
||||
|
||||
it('Add key', () => {
|
||||
cy.contains('Redis-connection').click();
|
||||
cy.contains('db1').click();
|
||||
cy.testid('RedisKeysTree_addKeyDropdown').click();
|
||||
cy.contains('String').click();
|
||||
cy.testid('NewRedisKeyTab_keyName').click().realType('newstringkey');
|
||||
cy.testid('RedisValueDetail_AceEditor').click().realType('This is a new string key.');
|
||||
cy.contains('Save').click();
|
||||
cy.contains('OK').click();
|
||||
cy.contains('newstringkey').should('exist');
|
||||
cy.testid('RedisKeysTree_addKeyDropdown').click();
|
||||
cy.contains('Hash').click();
|
||||
cy.themeshot('redis-add-hash-key');
|
||||
});
|
||||
});
|
||||
@@ -36,9 +36,11 @@ Cypress.Commands.add(
|
||||
prevSubject: 'optional',
|
||||
},
|
||||
(subject, file, options) => {
|
||||
cy.window().then(win => {
|
||||
win.__changeCurrentTheme('dark');
|
||||
});
|
||||
if (!options?.keepTheme) {
|
||||
cy.window().then(win => {
|
||||
win.__changeCurrentTheme('dark');
|
||||
});
|
||||
}
|
||||
|
||||
// cy.screenshot(`${file}-dark`, {
|
||||
// onAfterScreenshot: (doc, props) => {
|
||||
@@ -63,9 +65,11 @@ Cypress.Commands.add(
|
||||
// });
|
||||
// });
|
||||
|
||||
cy.window().then(win => {
|
||||
win.__changeCurrentTheme('light');
|
||||
});
|
||||
if (!options?.keepTheme) {
|
||||
cy.window().then(win => {
|
||||
win.__changeCurrentTheme('light');
|
||||
});
|
||||
}
|
||||
|
||||
if (subject) {
|
||||
cy.wrap(subject).screenshot(`${file}-light`, options);
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
{
|
||||
"themeName": "Green-Sample",
|
||||
"themeType": "light",
|
||||
"themeVariables": {
|
||||
"--theme-generic-font": "oklch(27% 0.07 130)",
|
||||
"--theme-generic-font-hover": "oklch(40% 0.15 130)",
|
||||
"--theme-generic-font-grayed": "oklch(65% 0.05 130)",
|
||||
"--theme-link-foreground": "oklch(40% 0.25 130)",
|
||||
"--theme-content-background": "oklch(95% 0.05 130)",
|
||||
"--theme-widget-panel-background": "oklch(80% 0.1 130)",
|
||||
"--theme-widget-panel-foreground": "oklch(27% 0.07 130)",
|
||||
"--theme-widget-icon-background-active": "oklch(50% 0.12 130)",
|
||||
"--theme-widget-icon-foreground-active": "white",
|
||||
"--theme-widget-icon-foreground-hover": "white",
|
||||
"--theme-widget-icon-border-active": "1px solid white",
|
||||
"--theme-scrollbar-background": "oklch(90% 0.08 130)",
|
||||
"--theme-scrollbar-thumb-background": "oklch(70% 0.12 130)",
|
||||
"--theme-scrollbar-thumb-background-hover": "oklch(40% 0.15 130)",
|
||||
"--theme-scrollbar-corner-background": "oklch(85% 0.1 130)",
|
||||
"--theme-tabs-panel-border": "1px solid oklch(95% 0.05 130)",
|
||||
"--theme-tabs-panel-foreground": "oklch(20% 0.06 130)",
|
||||
"--theme-tabs-panel-active-foreground": "oklch(10% 0.06 130)",
|
||||
"--theme-tabs-panel-background": "oklch(95.5% 0.04 130)",
|
||||
"--theme-tabs-panel-active-background": "oklch(80% 0.12 130)",
|
||||
"--theme-tabs-panel-item-background": "oklch(90% 0.1 130)",
|
||||
"--theme-tabs-panel-active-border": "1px solid oklch(50% 0.2 130)",
|
||||
"--theme-splitter-active": "oklch(50% 0.2 130)",
|
||||
"--theme-splitter-button-background": "oklch(90% 0.1 130)",
|
||||
"--theme-splitter-button-background-active": "oklch(85% 0.15 130)",
|
||||
"--theme-splitter-button-foreground": "oklch(10% 0.06 130)",
|
||||
"--theme-sidebar-background": "oklch(90% 0.1 130)",
|
||||
"--theme-sidebar-background-hover": "oklch(80% 0.12 130)",
|
||||
"--theme-sidebar-background-active": "oklch(75% 0.14 130)",
|
||||
"--theme-sidebar-background-focused": "oklch(70% 0.18 130)",
|
||||
"--theme-sidebar-foreground": "oklch(20% 0.06 130)",
|
||||
"--theme-sidebar-foreground-button": "oklch(40% 0.12 130)",
|
||||
"--theme-sidebar-foreground-grayed": "oklch(65% 0.05 130)",
|
||||
"--theme-sidebar-foreground-hover": "oklch(50% 0.25 130)",
|
||||
"--theme-sidebar-section-background": "oklch(65% 0.05 130)",
|
||||
"--theme-sidebar-section-border": "none",
|
||||
"--theme-sidebar-section-border-top": "1px solid oklch(80% 0.1 130)",
|
||||
"--theme-sidebar-section-foreground": "oklch(10% 0.06 130)",
|
||||
"--theme-sidebar-border": "none",
|
||||
"--theme-altsidebar-background": "oklch(95% 0.05 130)",
|
||||
"--theme-altsidebar-background-grayed": "oklch(97% 0.02 130)",
|
||||
"--theme-altsidebar-background-hover": "oklch(85% 0.1 130)",
|
||||
"--theme-altsidebar-background-active": "oklch(80% 0.12 130)",
|
||||
"--theme-altsidebar-background-focused": "oklch(75% 0.15 130)",
|
||||
"--theme-altsidebar-foreground": "oklch(20% 0.06 130)",
|
||||
"--theme-altsidebar-foreground-button": "oklch(40% 0.12 130)",
|
||||
"--theme-altsidebar-foreground-grayed": "oklch(65% 0.05 130)",
|
||||
"--theme-altsidebar-foreground-hover": "oklch(50% 0.25 130)",
|
||||
"--theme-altsidebar-section-background": "oklch(97% 0.02 130)",
|
||||
"--theme-altsidebar-section-border": "none",
|
||||
"--theme-altsidebar-section-border-top": "1px solid oklch(85% 0.1 130)",
|
||||
"--theme-altsidebar-section-foreground": "oklch(10% 0.06 130)",
|
||||
"--theme-altsidebar-border": "1px solid oklch(90% 0.1 130)",
|
||||
"--theme-searchbox-background": "oklch(80% 0.12 130)",
|
||||
"--theme-searchbox-placeholder": "oklch(65% 0.05 130)",
|
||||
"--theme-searchbox-border": "1px solid oklch(70% 0.15 130)",
|
||||
"--theme-searchbox-background-filtered": "oklch(95% 0.04 110)",
|
||||
"--theme-altsearchbox-background": "oklch(90% 0.1 130)",
|
||||
"--theme-altsearchbox-placeholder": "oklch(65% 0.05 130)",
|
||||
"--theme-altsearchbox-border": "1px solid oklch(80% 0.1 130)",
|
||||
"--theme-inlinebutton-foreground": "oklch(40% 0.12 130)",
|
||||
"--theme-inlinebutton-foreground-disabled": "oklch(65% 0.05 130)",
|
||||
"--theme-inlinebutton-foreground-hover": "black",
|
||||
"--theme-inlinebutton-circle-hover-background": "oklch(85% 0.1 130)",
|
||||
"--theme-inlinebutton-bordered-border": "1px solid oklch(85% 0.1 130)",
|
||||
"--theme-inlinebutton-bordered-hover-border": "1px solid oklch(70% 0.15 130)",
|
||||
"--theme-inlinebutton-bordered-background": "linear-gradient(to bottom, oklch(95% 0.04 130) 5%, oklch(90% 0.1 130) 100%)",
|
||||
"--theme-inlinebutton-bordered-hover-background": "linear-gradient(to bottom, oklch(90% 0.1 130) 5%, oklch(95% 0.04 130) 100%)",
|
||||
"--theme-datagrid-background": "oklch(95% 0.04 130)",
|
||||
"--theme-datagrid-foreground": "oklch(20% 0.06 130)",
|
||||
"--theme-datagrid-foreground-grayed": "oklch(65% 0.05 130)",
|
||||
"--theme-datagrid-border-horizontal": "1px solid oklch(90% 0.1 130)",
|
||||
"--theme-datagrid-border-vertical": "1px solid oklch(95% 0.04 130)",
|
||||
"--theme-datagrid-cell-background": "oklch(97% 0.02 130)",
|
||||
"--theme-datagrid-headercell-background": "oklch(95% 0.04 130)",
|
||||
"--theme-datagrid-cell-background-alt": "oklch(95% 0.04 130)",
|
||||
"--theme-datagrid-cell-background-alt2": "oklch(90% 0.1 130)",
|
||||
"--theme-datagrid-filter-background": "oklch(90% 0.1 130)",
|
||||
"--theme-datagrid-filter-border": "1px solid oklch(85% 0.1 130)",
|
||||
"--theme-datagrid-filter-ok-background": "oklch(95% 0.1 135)",
|
||||
"--theme-datagrid-filter-error-background": "oklch(95% 0.12 30)",
|
||||
"--theme-datagrid-modified-row-background": "oklch(95% 0.1 135)",
|
||||
"--theme-datagrid-modified-cell-background": "oklch(90% 0.15 135)",
|
||||
"--theme-datagrid-inserted-row-background": "oklch(95% 0.1 110)",
|
||||
"--theme-datagrid-deleted-row-background": "oklch(95% 0.1 25)",
|
||||
"--theme-datagrid-selected-cell-background": "oklch(80% 0.1 130)",
|
||||
"--theme-datagrid-focused-cell-background": "oklch(75% 0.15 130)",
|
||||
"--theme-datagrid-focused-cell-border-horizontal": "1px solid oklch(70% 0.2 130)",
|
||||
"--theme-datagrid-focused-cell-border-vertical": "1px solid oklch(70% 0.2 130)",
|
||||
"--theme-datagrid-selected-point-marker": "oklch(50% 0.25 130)",
|
||||
"--theme-datagrid-corner-label-background": "oklch(75% 0.15 130)",
|
||||
"--theme-datagrid-corner-label-border": "1px solid oklch(70% 0.2 130)",
|
||||
"--theme-datagrid-detail-header-background": "oklch(85% 0.05 130)",
|
||||
"--theme-datagrid-detail-header-border": "1px solid oklch(80% 0.1 130)",
|
||||
"--theme-datagrid-cell-foreground-value-green": "oklch(45% 0.2 140)",
|
||||
"--theme-checkbox-check": "oklch(90% 0.1 130)",
|
||||
"--theme-checkbox-background": "oklch(40% 0.25 130)",
|
||||
"--theme-checkbox-border": "1px solid oklch(70% 0.15 130)",
|
||||
"--theme-checkbox-mark": "white",
|
||||
"--theme-checkbox-background-disabled": "oklch(95% 0.04 130)",
|
||||
"--theme-checkbox-background-disabled-before": "oklch(70% 0.15 130)",
|
||||
"--theme-checkbox-hover-not-disabled": "oklch(65% 0.05 130)",
|
||||
"--theme-checkbox-background-inherited": "oklch(85% 0.1 130)",
|
||||
"--theme-table-border": "1px solid oklch(85% 0.1 130)",
|
||||
"--theme-table-cell-background": "oklch(97% 0.02 130)",
|
||||
"--theme-table-cell-empty-background": "oklch(95% 0.04 130)",
|
||||
"--theme-table-cell-empty-foreground": "oklch(65% 0.05 130)",
|
||||
"--theme-table-header-background": "oklch(95% 0.04 130)",
|
||||
"--theme-table-selected-background": "oklch(75% 0.15 130)",
|
||||
"--theme-table-active-background": "oklch(80% 0.1 130)",
|
||||
"--theme-table-hover-background": "oklch(95% 0.04 130)",
|
||||
"--theme-table-added-background": "oklch(95% 0.1 110)",
|
||||
"--theme-table-changed-background": "oklch(95% 0.1 135)",
|
||||
"--theme-table-deleted-background": "oklch(95% 0.1 25)",
|
||||
"--theme-cell-active-border": "2px solid oklch(50% 0.25 130)",
|
||||
"--theme-object-header-background": "oklch(95% 0.04 130)",
|
||||
"--theme-modal-background": "oklch(97% 0.02 130)",
|
||||
"--theme-modal-header-background": "oklch(85% 0.1 130)",
|
||||
"--theme-modal-footer-background": "oklch(97% 0.02 130)",
|
||||
"--theme-modal-border": "1px solid oklch(85% 0.1 130)",
|
||||
"--theme-modal-overlay-background": "color-mix(in srgb, #124012 40%, transparent)",
|
||||
"--theme-modal-shadow": "0 20px 25px -5px color-mix(in srgb, #124012 10%, transparent)",
|
||||
"--theme-modal-close-hover-background": "oklch(70% 0.15 130)",
|
||||
"--theme-formbutton-foreground": "white",
|
||||
"--theme-formbutton-border": "1px solid oklch(40% 0.25 130)",
|
||||
"--theme-formbutton-border-hover": "1px solid oklch(50% 0.3 130)",
|
||||
"--theme-formbutton-border-active": "2px solid oklch(55% 0.35 130)",
|
||||
"--theme-formbutton-background": "oklch(40% 0.25 130)",
|
||||
"--theme-formbutton-background-disabled": "oklch(85% 0.1 130)",
|
||||
"--theme-formbutton-border-disabled": "1px solid oklch(85% 0.1 130)",
|
||||
"--theme-formbutton-foreground-disabled": "oklch(65% 0.05 130)",
|
||||
"--theme-formbutton-background-hover": "oklch(35% 0.3 130)",
|
||||
"--theme-formbutton-background-active": "oklch(35% 0.3 130)",
|
||||
"--theme-outlinebutton-foreground": "oklch(10% 0.06 130)",
|
||||
"--theme-outlinebutton-border": "1px solid oklch(40% 0.25 130)",
|
||||
"--theme-outlinebutton-hover-foreground": "oklch(40% 0.25 130)",
|
||||
"--theme-outlinebutton-hover-border": "2px solid oklch(50% 0.3 130)",
|
||||
"--theme-tabs-control-background": "oklch(95% 0.04 130)",
|
||||
"--theme-tabs-control-border": "1px solid oklch(90% 0.1 130)",
|
||||
"--theme-tabs-control-selected-background": "oklch(98% 0.01 130)",
|
||||
"--theme-tabs-control-selected-border": "2px solid oklch(50% 0.25 130)",
|
||||
"--theme-inline-tabs-border": "1px solid oklch(90% 0.1 130)",
|
||||
"--theme-inline-tabs-border-active": "2px solid oklch(50% 0.25 130)",
|
||||
"--theme-toolstrip-background": "oklch(97% 0.02 130)",
|
||||
"--theme-toolstrip-border": "1px solid oklch(90% 0.1 130)",
|
||||
"--theme-toolstrip-button-foreground": "oklch(27% 0.07 130)",
|
||||
"--theme-panel-border-subtle": "1px solid color-mix(in srgb, oklch(20% 0.06 130) 5%, transparent)",
|
||||
"--theme-panel-type-label-color": "oklch(65% 0.05 130)",
|
||||
"--theme-toolstrip-button-foreground-disabled": "oklch(65% 0.05 130)",
|
||||
"--theme-toolstrip-button-foreground-icon": "oklch(40% 0.12 130)",
|
||||
"--theme-toolstrip-button-background": "oklch(97% 0.02 130)",
|
||||
"--theme-toolstrip-button-background-hover": "oklch(95% 0.04 130)",
|
||||
"--theme-toolstrip-button-background-active": "oklch(90% 0.1 130)",
|
||||
"--theme-toolstrip-button-border": "1px solid oklch(90% 0.1 130)",
|
||||
"--theme-toolstrip-button-border-hover": "1px solid oklch(85% 0.1 130)",
|
||||
"--theme-toolstrip-button-border-disabled": "1px solid oklch(90% 0.1 130)",
|
||||
"--theme-toolstrip-button-split-separator-border": "1px solid oklch(85% 0.1 130)",
|
||||
"--theme-designer-background": "oklch(97% 0.02 130)",
|
||||
"--theme-designer-item-background": "oklch(95% 0.04 130)",
|
||||
"--theme-designer-selection-marker": "oklch(35% 0.3 130)",
|
||||
"--theme-designer-item-border": "1px solid oklch(90% 0.1 130)",
|
||||
"--theme-designer-stroke-color": "oklch(65% 0.05 130)",
|
||||
"--theme-designer-arrow-color": "oklch(27% 0.07 130)",
|
||||
"--theme-designer-select-reactangle-foreground": "oklch(50% 0.25 130)",
|
||||
"--theme-designer-header-background-1": "oklch(70% 0.15 130)",
|
||||
"--theme-designer-header-background-2": "oklch(70% 0.18 180)",
|
||||
"--theme-designer-header-background-3": "oklch(68% 0.15 100)",
|
||||
"--theme-designer-header-background-grayed": "oklch(85% 0.1 130)",
|
||||
"--theme-designer-close-background": "oklch(90% 0.1 130)",
|
||||
"--theme-designer-close-background-hover": "oklch(85% 0.1 130)",
|
||||
"--theme-designer-close-background-active": "oklch(70% 0.15 130)",
|
||||
"--theme-designer-drag-column-background": "oklch(90% 0.2 110)",
|
||||
"--theme-designer-select-column-background": "oklch(90% 0.1 130)",
|
||||
"--theme-statusbar-background": "oklch(40% 0.25 130)",
|
||||
"--theme-statusbar-foreground": "oklch(95% 0.04 130)",
|
||||
"--theme-statusbar-background-hover": "oklch(35% 0.3 130)",
|
||||
"--theme-statusbar-button-background": "oklch(85% 0.1 130)",
|
||||
"--theme-statusbar-button-foreground": "oklch(27% 0.07 130)",
|
||||
"--theme-statusbar-icon-error": "oklch(80% 0.1 25)",
|
||||
"--theme-statusbar-icon-ok": "oklch(85% 0.2 130)",
|
||||
"--theme-aichat-user-background": "oklch(93% 0.06 130)",
|
||||
"--theme-aichat-assistant-background": "oklch(95% 0.04 130)",
|
||||
"--theme-applog-details-background": "oklch(98% 0.01 130)",
|
||||
"--theme-input-border": "1px solid oklch(85% 0.1 130)",
|
||||
"--theme-input-border-hover": "1px solid oklch(70% 0.15 130)",
|
||||
"--theme-input-border-hover-color": "oklch(70% 0.15 130)",
|
||||
"--theme-input-border-focus": "1px solid oklch(50% 0.25 130)",
|
||||
"--theme-input-border-focus-color": "oklch(50% 0.25 130)",
|
||||
"--theme-input-border-disabled": "1px solid oklch(90% 0.1 130)",
|
||||
"--theme-input-background": "white",
|
||||
"--theme-input-foreground": "oklch(20% 0.06 130)",
|
||||
"--theme-input-placeholder": "oklch(65% 0.05 130)",
|
||||
"--theme-input-background-disabled": "oklch(95% 0.04 130)",
|
||||
"--theme-input-foreground-disabled": "oklch(65% 0.05 130)",
|
||||
"--theme-input-focus-ring": "0 0 0 3px color-mix(in srgb, oklch(50% 0.25 130) 10%, transparent)",
|
||||
"--theme-input-multi-clear-background": "oklch(90% 0.1 130)",
|
||||
"--theme-input-multi-clear-foreground": "oklch(40% 0.12 130)",
|
||||
"--theme-input-multi-clear-hover": "oklch(85% 0.1 130)",
|
||||
"--theme-input-shadow": "0 1px 2px 0 color-mix(in srgb, oklch(20% 0.06 130) 5%, transparent)",
|
||||
"--theme-input-shadow-hover": "0 4px 6px -2px color-mix(in srgb, oklch(20% 0.06 130) 8%, transparent)",
|
||||
"--theme-input-shadow-focus": "0 1px 2px 0 color-mix(in srgb, oklch(20% 0.06 130) 5%, transparent)",
|
||||
"--theme-input-inplace-select-shadow": "0 1px 10px 1px oklch(40% 0.12 130)",
|
||||
"--theme-color-selected-border": "2px solid oklch(27% 0.07 130)",
|
||||
"--theme-new-object-button-background": "oklch(90% 0.1 130)",
|
||||
"--theme-new-object-button-background-hover": "oklch(85% 0.1 130)",
|
||||
"--theme-status-valid-background": "oklch(95% 0.1 110)",
|
||||
"--theme-status-testing-background": "oklch(95% 0.1 135)",
|
||||
"--theme-status-error-background": "oklch(95% 0.1 25)",
|
||||
"--theme-status-unconfigured-background": "oklch(95% 0.04 130)",
|
||||
"--theme-status-untested-background": "oklch(94% 0.1 65)",
|
||||
"--theme-dropdown-icon-hover": "oklch(45% 0.3 130)",
|
||||
"--theme-icon-picker-background": "oklch(90% 0.1 130)",
|
||||
"--theme-icon-picker-border": "1px solid oklch(85% 0.1 130)",
|
||||
"--theme-icon-picker-hover": "oklch(85% 0.1 130)",
|
||||
"--theme-icon-picker-selected": "oklch(80% 0.15 130)",
|
||||
"--theme-dbkey-background": "oklch(98% 0.01 130)",
|
||||
"--theme-dbkey-border": "1px solid oklch(90% 0.1 130)",
|
||||
"--theme-dbkey-icon-hover": "oklch(70% 0.15 130)",
|
||||
"--theme-chip-background": "oklch(85% 0.1 130)",
|
||||
"--theme-titlebar-background": "oklch(85% 0.1 130)",
|
||||
"--theme-titlebar-button-hover": "oklch(70% 0.15 130)",
|
||||
"--theme-card-background": "oklch(90% 0.1 130)",
|
||||
"--theme-card-border": "1px solid oklch(85% 0.1 130)",
|
||||
"--theme-content-background-hover": "oklch(95% 0.04 130)",
|
||||
"--theme-admin-menu-item-hover": "oklch(95% 0.04 130)",
|
||||
"--theme-admin-menu-item-active": "oklch(85% 0.1 130)",
|
||||
"--theme-admin-menu-background": "oklch(90% 0.1 130)",
|
||||
"--theme-admin-menu-border": "1px solid oklch(90% 0.1 130)",
|
||||
"--theme-json-tree-string-color": "oklch(45% 0.3 110)",
|
||||
"--theme-json-tree-symbol-color": "oklch(45% 0.3 110)",
|
||||
"--theme-json-tree-boolean-color": "oklch(40% 0.25 130)",
|
||||
"--theme-json-tree-function-color": "oklch(40% 0.25 130)",
|
||||
"--theme-json-tree-number-color": "oklch(50% 0.3 130)",
|
||||
"--theme-json-tree-label-color": "oklch(55% 0.3 140)",
|
||||
"--theme-json-tree-arrow-color": "oklch(65% 0.05 130)",
|
||||
"--theme-json-tree-null-color": "oklch(65% 0.05 130)",
|
||||
"--theme-json-tree-undefined-color": "oklch(65% 0.05 130)",
|
||||
"--theme-json-tree-date-color": "oklch(65% 0.05 130)",
|
||||
"--theme-json-tree-deleted-background": "oklch(95% 0.1 25)",
|
||||
"--theme-json-tree-modified-background": "oklch(95% 0.1 135)",
|
||||
"--theme-json-tree-inserted-background": "oklch(95% 0.1 110)",
|
||||
"--theme-icon-blue": "oklch(40% 0.25 130)",
|
||||
"--theme-icon-green": "oklch(45% 0.2 140)",
|
||||
"--theme-icon-red": "oklch(40% 0.3 25)",
|
||||
"--theme-icon-gold": "oklch(50% 0.2 60)",
|
||||
"--theme-icon-yellow": "oklch(50% 0.15 80)",
|
||||
"--theme-icon-magenta": "oklch(45% 0.3 135)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
HSET "actor:1000" "first_name" "Sandra"
|
||||
HSET "actor:1000" "last_name" "Bullock"
|
||||
HSET "actor:1000" "date_of_birth" "1964"
|
||||
|
||||
HSET "actor:1001" "first_name" "Jon"
|
||||
HSET "actor:1001" "last_name" "Hamm"
|
||||
HSET "actor:1001" "date_of_birth" "1971"
|
||||
|
||||
HSET "actor:1002" "first_name" "Allison"
|
||||
HSET "actor:1002" "last_name" "Janney"
|
||||
HSET "actor:1002" "date_of_birth" "1959"
|
||||
|
||||
HSET "actor:1003" "first_name" "Steve"
|
||||
HSET "actor:1003" "last_name" "Coogan"
|
||||
HSET "actor:1003" "date_of_birth" "1965"
|
||||
@@ -0,0 +1,14 @@
|
||||
SET app:name "App"
|
||||
SET app:version "1.0.0"
|
||||
SET app:env "test"
|
||||
SET user:1:json "{\"id\":1,\"name\":\"Alice\",\"email\":\"alice@app.test\",\"roles\":[\"admin\",\"user\"],\"settings\":{\"theme\":\"dark\",\"language\":\"sk\"}}"
|
||||
SET user:2:json "{\"id\":2,\"name\":\"Bob\",\"email\":\"bob@app.test\",\"roles\":[\"user\"],\"settings\":{\"theme\":\"light\",\"language\":\"en\"}}"
|
||||
RPUSH queue:emails "welcome" "reset-password" "newsletter" "promotion" "weekly-digest"
|
||||
HSET user:alice name "Alice" email "alice@app.test" active "true" age "29" country "SK"
|
||||
HSET user:bob name "Bob" email "bob@app.test" active "false" age "34" country "CZ"
|
||||
SADD tags "app" "backend" "database" "redis" "test" "production"
|
||||
ZADD leaderboard 100 "alice" 250 "bob" 180 "carol" 90 "dave" 300 "eve"
|
||||
XADD events * type "login" userId "1" ip "127.0.0.1" device "web"
|
||||
XADD events * type "update-profile" userId "1" field "email" old "alice@app.test" new "alice@new.app"
|
||||
XADD events * type "login" userId "2" ip "10.0.0.5" device "mobile"
|
||||
XADD events * type "logout" userId "1" reason "manual"
|
||||
Vendored
+1
-6
@@ -1,4 +1,4 @@
|
||||
CONNECTIONS=mysql,postgres,mongo,redis
|
||||
CONNECTIONS=mysql,postgres,mongo
|
||||
|
||||
LABEL_mysql=MySql-connection
|
||||
SERVER_mysql=localhost
|
||||
@@ -22,8 +22,3 @@ USER_mongo=root
|
||||
PASSWORD_mongo=Pwd2020Db
|
||||
PORT_mongo=16010
|
||||
ENGINE_mongo=mongo@dbgate-plugin-mongo
|
||||
|
||||
LABEL_redis=Redis-connection
|
||||
SERVER_redis=localhost
|
||||
ENGINE_redis=redis@dbgate-plugin-redis
|
||||
PORT_redis=16011
|
||||
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
CONNECTIONS=redis
|
||||
|
||||
LABEL_redis=Redis-connection
|
||||
SERVER_redis=localhost
|
||||
ENGINE_redis=redis@dbgate-plugin-redis
|
||||
PORT_redis=16011
|
||||
@@ -125,46 +125,6 @@ async function initMongoDatabase(dbname, inputDirectory) {
|
||||
// });
|
||||
}
|
||||
|
||||
async function initRedisDatabase(inputDirectory) {
|
||||
await dbgateApi.executeQuery({
|
||||
connection: {
|
||||
server: process.env.SERVER_redis,
|
||||
user: process.env.USER_redis,
|
||||
password: process.env.PASSWORD_redis,
|
||||
port: process.env.PORT_redis,
|
||||
engine: 'redis@dbgate-plugin-redis',
|
||||
},
|
||||
sql: 'FLUSHALL',
|
||||
});
|
||||
|
||||
for (const file of fs.readdirSync(inputDirectory)) {
|
||||
await dbgateApi.executeQuery({
|
||||
connection: {
|
||||
server: process.env.SERVER_redis,
|
||||
user: process.env.USER_redis,
|
||||
password: process.env.PASSWORD_redis,
|
||||
port: process.env.PORT_redis,
|
||||
engine: 'redis@dbgate-plugin-redis',
|
||||
database: 0,
|
||||
},
|
||||
sqlFile: path.join(inputDirectory, file),
|
||||
// logScriptItems: true,
|
||||
});
|
||||
}
|
||||
|
||||
// await dbgateApi.importDatabase({
|
||||
// connection: {
|
||||
// server: process.env.SERVER_postgres,
|
||||
// user: process.env.USER_postgres,
|
||||
// password: process.env.PASSWORD_postgres,
|
||||
// port: process.env.PORT_postgres,
|
||||
// database: dbname,
|
||||
// engine: 'postgres@dbgate-plugin-postgres',
|
||||
// },
|
||||
// inputFile,
|
||||
// });
|
||||
}
|
||||
|
||||
const baseDir = path.join(os.homedir(), '.dbgate');
|
||||
|
||||
async function copyFolder(source, target) {
|
||||
@@ -188,8 +148,6 @@ async function run() {
|
||||
await initMongoDatabase('MgChinook', path.resolve(path.join(__dirname, '../data/chinook-jsonl')));
|
||||
await initMongoDatabase('MgRivers', path.resolve(path.join(__dirname, '../data/rivers-jsonl')));
|
||||
|
||||
await initRedisDatabase(path.resolve(path.join(__dirname, '../data/redis')));
|
||||
|
||||
await copyFolder(
|
||||
path.resolve(path.join(__dirname, '../data/chinook-jsonl')),
|
||||
path.join(baseDir, 'archive-e2etests', 'default')
|
||||
|
||||
@@ -90,6 +90,11 @@ async function run() {
|
||||
path.join(baseDir, 'files-e2etests', 'sql')
|
||||
);
|
||||
|
||||
await copyFolder(
|
||||
path.resolve(path.join(__dirname, '../data/files/themes')),
|
||||
path.join(baseDir, 'files-e2etests', 'themes')
|
||||
);
|
||||
|
||||
await initMySqlDatabase('MyChinook', path.resolve(path.join(__dirname, '../data/chinook-mysql.sql')));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const dbgateApi = require('dbgate-api');
|
||||
dbgateApi.initializeApiEnvironment();
|
||||
const dbgatePluginRedis = require('dbgate-plugin-redis');
|
||||
dbgateApi.registerPlugins(dbgatePluginRedis);
|
||||
|
||||
async function initRedisDatabase() {
|
||||
await dbgateApi.executeQuery({
|
||||
connection: {
|
||||
server: process.env.SERVER_redis,
|
||||
user: process.env.USER_redis,
|
||||
password: process.env.PASSWORD_redis,
|
||||
port: process.env.PORT_redis,
|
||||
engine: 'redis@dbgate-plugin-redis',
|
||||
},
|
||||
sql: 'FLUSHALL',
|
||||
});
|
||||
|
||||
const files = [
|
||||
{
|
||||
file: path.resolve(__dirname, '../data/redis-db1.redis'),
|
||||
database: 0,
|
||||
},
|
||||
{
|
||||
file: path.resolve(__dirname, '../data/redis-db2.redis'),
|
||||
database: 1,
|
||||
},
|
||||
];
|
||||
|
||||
for (const { file, database } of files) {
|
||||
await dbgateApi.executeQuery({
|
||||
connection: {
|
||||
server: process.env.SERVER_redis,
|
||||
user: process.env.USER_redis,
|
||||
password: process.env.PASSWORD_redis,
|
||||
port: process.env.PORT_redis,
|
||||
engine: 'redis@dbgate-plugin-redis',
|
||||
database,
|
||||
},
|
||||
sqlFile: file,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
await initRedisDatabase();
|
||||
}
|
||||
|
||||
dbgateApi.runScript(run);
|
||||
|
||||
module.exports = {
|
||||
initRedisDatabase,
|
||||
};
|
||||
@@ -23,6 +23,7 @@
|
||||
"cy:run:multi-sql": "cypress run --spec cypress/e2e/multi-sql.cy.js",
|
||||
"cy:run:cloud": "cypress run --spec cypress/e2e/cloud.cy.js",
|
||||
"cy:run:charts": "cypress run --spec cypress/e2e/charts.cy.js",
|
||||
"cy:run:redis": "cypress run --spec cypress/e2e/redis.cy.js",
|
||||
"start:add-connection": "node clearTestingData && cd .. && node packer/build/bundle.js --listen-api --run-e2e-tests",
|
||||
"start:portal": "node clearTestingData && cd .. && env-cmd -f e2e-tests/env/portal/.env node e2e-tests/init/portal.js && env-cmd -f e2e-tests/env/portal/.env node packer/build/bundle.js --listen-api --run-e2e-tests",
|
||||
"start:oauth": "node clearTestingData && cd .. && env-cmd -f e2e-tests/env/oauth/.env node packer/build/bundle.js --listen-api --run-e2e-tests",
|
||||
@@ -31,6 +32,7 @@
|
||||
"start:multi-sql": "node clearTestingData && cd .. && env-cmd -f e2e-tests/env/multi-sql/.env node e2e-tests/init/multi-sql.js && env-cmd -f e2e-tests/env/multi-sql/.env node packer/build/bundle.js --listen-api --run-e2e-tests",
|
||||
"start:cloud": "node clearTestingData && cd .. && env-cmd -f e2e-tests/env/cloud/.env node packer/build/bundle.js --listen-api --run-e2e-tests",
|
||||
"start:charts": "node clearTestingData && cd .. && env-cmd -f e2e-tests/env/charts/.env node e2e-tests/init/charts.js && env-cmd -f e2e-tests/env/charts/.env node packer/build/bundle.js --listen-api --run-e2e-tests",
|
||||
"start:redis": "node clearTestingData && cd .. && env-cmd -f e2e-tests/env/redis/.env node e2e-tests/init/redis.js && env-cmd -f e2e-tests/env/redis/.env node packer/build/bundle.js --listen-api --run-e2e-tests",
|
||||
"test:add-connection": "start-server-and-test start:add-connection http://localhost:3000 cy:run:add-connection",
|
||||
"test:portal": "start-server-and-test start:portal http://localhost:3000 cy:run:portal",
|
||||
"test:oauth": "start-server-and-test start:oauth http://localhost:3000 cy:run:oauth",
|
||||
@@ -39,7 +41,8 @@
|
||||
"test:multi-sql": "start-server-and-test start:multi-sql http://localhost:3000 cy:run:multi-sql",
|
||||
"test:cloud": "start-server-and-test start:cloud http://localhost:3000 cy:run:cloud",
|
||||
"test:charts": "start-server-and-test start:charts http://localhost:3000 cy:run:charts",
|
||||
"test": "yarn test:add-connection && yarn test:portal && yarn test:oauth && yarn test:browse-data && yarn test:team && yarn test:multi-sql && yarn test:cloud && yarn test:charts",
|
||||
"test:redis": "start-server-and-test start:redis http://localhost:3000 cy:run:redis",
|
||||
"test": "yarn test:add-connection && yarn test:portal && yarn test:oauth && yarn test:browse-data && yarn test:team && yarn test:multi-sql && yarn test:cloud && yarn test:charts && yarn test:redis",
|
||||
"test:ci": "yarn test"
|
||||
},
|
||||
"dependencies": {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dbgate-integration-tests",
|
||||
"version": "6.0.0-alpha.1",
|
||||
"version": "7.0.0-alpha.1",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "7.0.0-premium-beta.5",
|
||||
"version": "7.0.6",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "dbgate-api",
|
||||
"main": "src/index.js",
|
||||
"version": "6.0.0-alpha.1",
|
||||
"version": "7.0.0-alpha.1",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -24,16 +24,16 @@
|
||||
"activedirectory2": "^2.1.0",
|
||||
"archiver": "^7.0.1",
|
||||
"async-lock": "^1.2.6",
|
||||
"axios": "^0.21.1",
|
||||
"axios": "^1.13.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"byline": "^5.0.0",
|
||||
"compare-versions": "^3.6.0",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^6.0.3",
|
||||
"dbgate-datalib": "^6.0.0-alpha.1",
|
||||
"dbgate-datalib": "^7.0.0-alpha.1",
|
||||
"dbgate-query-splitter": "^4.11.9",
|
||||
"dbgate-sqltree": "^6.0.0-alpha.1",
|
||||
"dbgate-tools": "^6.0.0-alpha.1",
|
||||
"dbgate-sqltree": "^7.0.0-alpha.1",
|
||||
"dbgate-tools": "^7.0.0-alpha.1",
|
||||
"debug": "^4.3.4",
|
||||
"diff": "^5.0.0",
|
||||
"diff2html": "^3.4.13",
|
||||
@@ -87,7 +87,7 @@
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^9.0.11",
|
||||
"@types/lodash": "^4.14.149",
|
||||
"dbgate-types": "^6.0.0-alpha.1",
|
||||
"dbgate-types": "^7.0.0-alpha.1",
|
||||
"env-cmd": "^10.1.0",
|
||||
"jsdoc-to-markdown": "^9.0.5",
|
||||
"node-loader": "^1.0.2",
|
||||
|
||||
@@ -55,6 +55,8 @@ function authMiddleware(req, res, next) {
|
||||
'/stream',
|
||||
'/storage/get-connections-for-login-page',
|
||||
'/storage/set-admin-password',
|
||||
'/storage/request-password-reset',
|
||||
'/storage/reset-password',
|
||||
'/auth/get-providers',
|
||||
'/connections/dblogin-web',
|
||||
'/connections/dblogin-app',
|
||||
|
||||
@@ -24,10 +24,12 @@ const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
const { getAuthProviderById } = require('../auth/authProvider');
|
||||
const { startTokenChecking } = require('../utility/authProxy');
|
||||
const { extractConnectionsFromEnv } = require('../utility/envtools');
|
||||
const { MissingCredentialsError } = require('../utility/exceptions');
|
||||
|
||||
const logger = getLogger('connections');
|
||||
|
||||
let volatileConnections = {};
|
||||
let pendingTestSubprocesses = {}; // Map of conid -> subprocess for MS Entra auth flows
|
||||
|
||||
function getNamedArgs() {
|
||||
const res = {};
|
||||
@@ -203,7 +205,7 @@ module.exports = {
|
||||
return storageConnections;
|
||||
}
|
||||
if (portalConnections) {
|
||||
if (platformInfo.allowShellConnection) return portalConnections;
|
||||
if (platformInfo.allowShellConnection) return portalConnections.map(x => encryptConnection(x));
|
||||
return portalConnections.map(maskConnection).filter(x => connectionHasPermission(x, loadedPermissions));
|
||||
}
|
||||
return (await this.datastore.find()).filter(x => connectionHasPermission(x, loadedPermissions));
|
||||
@@ -239,14 +241,60 @@ module.exports = {
|
||||
);
|
||||
pipeForkLogs(subprocess);
|
||||
subprocess.send({ ...connection, requestDbList });
|
||||
return new Promise(resolve => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let isWaitingForVolatile = false;
|
||||
|
||||
const cleanup = () => {
|
||||
if (connection._id && pendingTestSubprocesses[connection._id]) {
|
||||
delete pendingTestSubprocesses[connection._id];
|
||||
}
|
||||
};
|
||||
|
||||
subprocess.on('message', resp => {
|
||||
if (handleProcessCommunication(resp, subprocess)) return;
|
||||
// @ts-ignore
|
||||
const { msgtype } = resp;
|
||||
const { msgtype, missingCredentialsDetail } = resp;
|
||||
if (msgtype == 'connected' || msgtype == 'error') {
|
||||
cleanup();
|
||||
resolve(resp);
|
||||
}
|
||||
if (msgtype == 'missingCredentials') {
|
||||
if (missingCredentialsDetail?.redirectToDbLogin) {
|
||||
// Store the subprocess for later when volatile connection is ready
|
||||
isWaitingForVolatile = true;
|
||||
pendingTestSubprocesses[connection._id] = {
|
||||
subprocess,
|
||||
requestDbList,
|
||||
};
|
||||
// Return immediately with redirectToDbLogin status in the old format
|
||||
resolve({
|
||||
missingCredentials: true,
|
||||
detail: {
|
||||
...missingCredentialsDetail,
|
||||
keepErrorResponseFromApi: true,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
reject(new MissingCredentialsError(missingCredentialsDetail));
|
||||
}
|
||||
});
|
||||
|
||||
subprocess.on('exit', code => {
|
||||
// If exit happens while waiting for volatile, that's expected
|
||||
if (isWaitingForVolatile && code === 0) {
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
cleanup();
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Test subprocess exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
subprocess.on('error', err => {
|
||||
cleanup();
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -279,6 +327,38 @@ module.exports = {
|
||||
return testRes;
|
||||
} else {
|
||||
volatileConnections[res._id] = res;
|
||||
|
||||
// Check if there's a pending test subprocess waiting for this volatile connection
|
||||
const pendingTest = pendingTestSubprocesses[conid];
|
||||
if (pendingTest) {
|
||||
const { subprocess, requestDbList } = pendingTest;
|
||||
try {
|
||||
// Send the volatile connection to the waiting subprocess
|
||||
subprocess.send({ ...res, requestDbList, isVolatileResolved: true });
|
||||
|
||||
// Wait for the test result and emit it as an event
|
||||
subprocess.once('message', resp => {
|
||||
if (handleProcessCommunication(resp, subprocess)) return;
|
||||
const { msgtype } = resp;
|
||||
if (msgtype == 'connected' || msgtype == 'error') {
|
||||
// Emit SSE event with test result
|
||||
socket.emit(`connection-test-result-${conid}`, {
|
||||
...resp,
|
||||
volatileConId: res._id,
|
||||
});
|
||||
delete pendingTestSubprocesses[conid];
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err), 'DBGM-00118 Error sending volatile connection to test subprocess');
|
||||
socket.emit(`connection-test-result-${conid}`, {
|
||||
msgtype: 'error',
|
||||
error: err.message,
|
||||
});
|
||||
delete pendingTestSubprocesses[conid];
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
},
|
||||
@@ -409,7 +489,7 @@ module.exports = {
|
||||
|
||||
if (portalConnections) {
|
||||
const res = portalConnections.find(x => x._id == conid) || null;
|
||||
return mask && !platformInfo.allowShellConnection ? maskConnection(res) : res;
|
||||
return mask && !platformInfo.allowShellConnection ? maskConnection(res) : encryptConnection(res);
|
||||
}
|
||||
const res = await this.datastore.get(conid);
|
||||
return res || null;
|
||||
|
||||
@@ -393,6 +393,12 @@ module.exports = {
|
||||
return null;
|
||||
},
|
||||
|
||||
dispatchRedisKeysChanged_meta: true,
|
||||
dispatchRedisKeysChanged({ conid, database }) {
|
||||
socket.emit(`redis-keys-changed-${conid}-${database}`);
|
||||
return null;
|
||||
},
|
||||
|
||||
loadKeys_meta: true,
|
||||
async loadKeys({ conid, database, root, filter, limit }, req) {
|
||||
await testConnectionPermission(conid, req);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
module.exports = {
|
||||
version: '6.0.0-alpha.1',
|
||||
version: '7.0.0-alpha.1',
|
||||
buildTime: '2024-12-01T00:00:00Z'
|
||||
};
|
||||
|
||||
@@ -18,13 +18,36 @@ Platform: ${process.platform}
|
||||
|
||||
function start() {
|
||||
childProcessChecker();
|
||||
process.on('message', async connection => {
|
||||
|
||||
let isWaitingForVolatile = false;
|
||||
|
||||
const handleConnection = async connection => {
|
||||
// @ts-ignore
|
||||
const { requestDbList } = connection;
|
||||
if (handleProcessCommunication(connection)) return;
|
||||
|
||||
try {
|
||||
const driver = requireEngineDriver(connection);
|
||||
const dbhan = await connectUtility(driver, connection, 'app');
|
||||
const connectionChanged = driver?.beforeConnectionSave ? driver.beforeConnectionSave(connection) : connection;
|
||||
|
||||
if (!connection.isVolatileResolved) {
|
||||
if (connectionChanged.useRedirectDbLogin) {
|
||||
process.send({
|
||||
msgtype: 'missingCredentials',
|
||||
missingCredentialsDetail: {
|
||||
// @ts-ignore
|
||||
conid: connection._id,
|
||||
redirectToDbLogin: true,
|
||||
keepErrorResponseFromApi: true,
|
||||
},
|
||||
});
|
||||
// Don't exit - wait for volatile connection to be sent
|
||||
isWaitingForVolatile = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const dbhan = await connectUtility(driver, connectionChanged, 'app');
|
||||
let version = {
|
||||
version: 'Unknown',
|
||||
};
|
||||
@@ -45,6 +68,16 @@ function start() {
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('message', async connection => {
|
||||
// If we're waiting for volatile and receive a new connection, use it
|
||||
if (isWaitingForVolatile) {
|
||||
isWaitingForVolatile = false;
|
||||
await handleConnection(connection);
|
||||
} else {
|
||||
await handleConnection(connection);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -850,6 +850,84 @@ module.exports = {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"pureName": "password_reset_tokens",
|
||||
"columns": [
|
||||
{
|
||||
"pureName": "password_reset_tokens",
|
||||
"columnName": "id",
|
||||
"dataType": "int",
|
||||
"autoIncrement": true,
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"pureName": "password_reset_tokens",
|
||||
"columnName": "user_id",
|
||||
"dataType": "int",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"pureName": "password_reset_tokens",
|
||||
"columnName": "token",
|
||||
"dataType": "varchar(500)",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"pureName": "password_reset_tokens",
|
||||
"columnName": "created_at",
|
||||
"dataType": "datetime",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"pureName": "password_reset_tokens",
|
||||
"columnName": "expires_at",
|
||||
"dataType": "datetime",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"pureName": "password_reset_tokens",
|
||||
"columnName": "used_at",
|
||||
"dataType": "datetime",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"constraintType": "foreignKey",
|
||||
"constraintName": "FK_password_reset_tokens_user_id",
|
||||
"pureName": "password_reset_tokens",
|
||||
"refTableName": "users",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "user_id",
|
||||
"refColumnName": "id"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"constraintName": "idx_token",
|
||||
"pureName": "password_reset_tokens",
|
||||
"constraintType": "index",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "token"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"pureName": "password_reset_tokens",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_password_reset_tokens",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pureName": "roles",
|
||||
"columns": [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const getDiagramExport = (html, css, themeType, themeVariables, watermark) => {
|
||||
const watermarkHtml = watermark
|
||||
? `
|
||||
<div style="position: fixed; bottom: 0; right: 0; padding: 5px; font-size: 12px; color: var(--theme-font-2); background-color: var(--theme-bg-2); border-top-left-radius: 5px; border: 1px solid var(--theme-border);">
|
||||
<div style="position: fixed; bottom: 0; right: 0; padding: 5px; font-size: 12px; color: var(--theme-generic-font-grayed); background-color: var(--theme-datagrid-background); border-top-left-radius: 5px; border: var(--theme-card-border);">
|
||||
${watermark}
|
||||
</div>
|
||||
`
|
||||
@@ -22,8 +22,8 @@ const getDiagramExport = (html, css, themeType, themeVariables, watermark) => {
|
||||
${css}
|
||||
|
||||
body {
|
||||
background: var(--theme-bg-1);
|
||||
color: var(--theme-font-1);
|
||||
background: var(--theme-datagrid-background);
|
||||
color: var(--theme-generic-font);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "6.0.0-alpha.1",
|
||||
"version": "7.0.0-alpha.1",
|
||||
"name": "dbgate-datalib",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
@@ -19,14 +19,14 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"date-fns": "^4.1.0",
|
||||
"dbgate-filterparser": "^6.0.0-alpha.1",
|
||||
"dbgate-sqltree": "^6.0.0-alpha.1",
|
||||
"dbgate-tools": "^6.0.0-alpha.1",
|
||||
"dbgate-filterparser": "^7.0.0-alpha.1",
|
||||
"dbgate-sqltree": "^7.0.0-alpha.1",
|
||||
"dbgate-tools": "^7.0.0-alpha.1",
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^13.7.0",
|
||||
"dbgate-types": "^6.0.0-alpha.1",
|
||||
"dbgate-types": "^7.0.0-alpha.1",
|
||||
"jest": "^28.1.3",
|
||||
"ts-jest": "^28.0.7",
|
||||
"typescript": "^4.4.3"
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface ChangeSetRedis_JSON {
|
||||
export interface ChangeSetRedis_Hash {
|
||||
key: string;
|
||||
type: 'hash';
|
||||
inserts: { key: string; value: string; ttl: number }[];
|
||||
inserts: { key: string; value: string; ttl: number; editorRowId: string }[];
|
||||
updates: { key: string; value: string; ttl: number }[];
|
||||
deletes: string[];
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export interface ChangeSetRedis_Hash {
|
||||
export interface ChangeSetRedis_List {
|
||||
key: string;
|
||||
type: 'list';
|
||||
inserts: { index: number; value: string }[];
|
||||
inserts: { value: string; editorRowId: string }[];
|
||||
updates: { index: number; value: string }[];
|
||||
deletes: number[];
|
||||
}
|
||||
@@ -31,25 +31,34 @@ export interface ChangeSetRedis_List {
|
||||
export interface ChangeSetRedis_Set {
|
||||
key: string;
|
||||
type: 'set';
|
||||
inserts: string[];
|
||||
inserts: { value: string; editorRowId: string }[];
|
||||
deletes: string[];
|
||||
}
|
||||
|
||||
export interface ChangeSetRedis_ZSet {
|
||||
key: string;
|
||||
type: 'zset';
|
||||
inserts: { member: string; score: number }[];
|
||||
inserts: { member: string; score: number; editorRowId: string }[];
|
||||
updates: { member: string; score: number }[];
|
||||
deletes: string[];
|
||||
}
|
||||
|
||||
export interface ChangeSetRedis_Stream {
|
||||
key: string;
|
||||
type: 'stream';
|
||||
generatedId?: string;
|
||||
inserts: { field: string; value: string; editorRowId: string }[];
|
||||
deletes: string[];
|
||||
}
|
||||
|
||||
export type ChangeSetRedisType =
|
||||
| ChangeSetRedis_String
|
||||
| ChangeSetRedis_JSON
|
||||
| ChangeSetRedis_Hash
|
||||
| ChangeSetRedis_List
|
||||
| ChangeSetRedis_Set
|
||||
| ChangeSetRedis_ZSet;
|
||||
| ChangeSetRedis_ZSet
|
||||
| ChangeSetRedis_Stream;
|
||||
|
||||
export interface ChangeSetRedis {
|
||||
changes: ChangeSetRedisType[];
|
||||
@@ -160,7 +169,7 @@ export function redisChangeSetToRedisCommands(changeSet: ChangeSetRedis): Databa
|
||||
for (const insert of change.inserts) {
|
||||
calls.push({
|
||||
method: 'SADD',
|
||||
args: [change.key, insert],
|
||||
args: [change.key, insert.value],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -173,6 +182,19 @@ export function redisChangeSetToRedisCommands(changeSet: ChangeSetRedis): Databa
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (change.type === 'stream') {
|
||||
if (change.inserts.length > 0) {
|
||||
calls.push({
|
||||
method: 'XADD',
|
||||
args: [change.key, change.generatedId || '*', ...change.inserts.flatMap(f => [f.field, f.value])],
|
||||
});
|
||||
}
|
||||
for (const delValue of change.deletes) {
|
||||
calls.push({
|
||||
method: 'XDEL',
|
||||
args: [change.key, delValue],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +204,7 @@ export function redisChangeSetToRedisCommands(changeSet: ChangeSetRedis): Databa
|
||||
export function convertRedisCallListToScript(callList: DatabaseMethodCallList): string {
|
||||
let script = '';
|
||||
for (const call of callList.calls) {
|
||||
script += `${call.method} ${call.args.map((arg) => (typeof arg === 'string' ? `"${arg}"` : arg)).join(' ')}\n`;
|
||||
script += `${call.method} ${call.args.map(arg => (typeof arg === 'string' ? `"${arg}"` : arg)).join(' ')}\n`;
|
||||
}
|
||||
return script;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,7 +451,7 @@ export abstract class GridDisplay {
|
||||
...cfg,
|
||||
filters: _.omit(cfg.filters, [uniqueName]),
|
||||
formFilterColumns: (cfg.formFilterColumns || []).filter(x => x != uniqueName),
|
||||
disabledFilterColumns: (cfg.disabledFilterColumns).filter(x => x != uniqueName),
|
||||
disabledFilterColumns: cfg.disabledFilterColumns.filter(x => x != uniqueName),
|
||||
}));
|
||||
this.reload();
|
||||
}
|
||||
@@ -541,6 +541,7 @@ export abstract class GridDisplay {
|
||||
const column = (this.baseTable || this.baseView)?.columns?.find(x => x.columnName == uniqueName);
|
||||
if (isTypeLogical(column?.dataType)) return 'COUNT DISTINCT';
|
||||
if (column?.autoIncrement) return 'COUNT';
|
||||
if (this.driver?.dialect?.disableGroupingForDataType?.(column?.dataType)) return 'NULL';
|
||||
return 'MAX';
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dbmodel",
|
||||
"version": "6.0.0-alpha.1",
|
||||
"version": "7.0.0-alpha.1",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -30,16 +30,16 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"commander": "^10.0.0",
|
||||
"dbgate-api": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-csv": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-excel": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-mongo": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-mssql": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-mysql": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-postgres": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-xml": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-oracle": "^6.0.0-alpha.1",
|
||||
"dbgate-web": "^6.0.0-alpha.1",
|
||||
"dbgate-api": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-csv": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-excel": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-mongo": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-mssql": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-mysql": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-postgres": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-xml": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-oracle": "^7.0.0-alpha.1",
|
||||
"dbgate-web": "^7.0.0-alpha.1",
|
||||
"dotenv": "^16.0.0",
|
||||
"pinomin": "^1.0.5"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "6.0.0-alpha.1",
|
||||
"version": "7.0.0-alpha.1",
|
||||
"name": "dbgate-filterparser",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
@@ -17,7 +17,7 @@
|
||||
"lib"
|
||||
],
|
||||
"devDependencies": {
|
||||
"dbgate-types": "^6.0.0-alpha.1",
|
||||
"dbgate-types": "^7.0.0-alpha.1",
|
||||
"@types/jest": "^25.1.4",
|
||||
"@types/node": "^13.7.0",
|
||||
"jest": "^28.1.3",
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/parsimmon": "^1.10.1",
|
||||
"dbgate-tools": "^6.0.0-alpha.1",
|
||||
"dbgate-tools": "^7.0.0-alpha.1",
|
||||
"lodash": "^4.17.21",
|
||||
"date-fns": "^4.1.0",
|
||||
"moment": "^2.24.0",
|
||||
|
||||
+14
-14
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dbgate-serve",
|
||||
"version": "6.0.0-alpha.1",
|
||||
"version": "7.0.0-alpha.1",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -18,19 +18,19 @@
|
||||
"web"
|
||||
],
|
||||
"dependencies": {
|
||||
"dbgate-api": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-clickhouse": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-csv": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-excel": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-mongo": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-mssql": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-mysql": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-oracle": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-postgres": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-redis": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-sqlite": "^6.0.0-alpha.1",
|
||||
"dbgate-plugin-xml": "^6.0.0-alpha.1",
|
||||
"dbgate-web": "^6.0.0-alpha.1",
|
||||
"dbgate-api": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-clickhouse": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-csv": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-excel": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-mongo": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-mssql": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-mysql": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-oracle": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-postgres": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-redis": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-sqlite": "^7.0.0-alpha.1",
|
||||
"dbgate-plugin-xml": "^7.0.0-alpha.1",
|
||||
"dbgate-web": "^7.0.0-alpha.1",
|
||||
"dotenv": "^16.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "6.0.0-alpha.1",
|
||||
"version": "7.0.0-alpha.1",
|
||||
"name": "dbgate-sqltree",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
@@ -27,7 +27,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^13.7.0",
|
||||
"dbgate-types": "^6.0.0-alpha.1",
|
||||
"dbgate-types": "^7.0.0-alpha.1",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -19,14 +19,28 @@ export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
|
||||
dmp.put(' ^is ^not ^null');
|
||||
break;
|
||||
case 'isEmpty':
|
||||
dmp.put('^trim(');
|
||||
dumpSqlExpression(dmp, condition.expr);
|
||||
dmp.put(") = ''");
|
||||
// Use DATALENGTH for MSSQL TEXT/NTEXT/IMAGE columns to avoid TRIM error
|
||||
if (dmp.dialect.useDatalengthForEmptyString?.(condition.expr?.['dataType'])) {
|
||||
dmp.put('^datalength(');
|
||||
dumpSqlExpression(dmp, condition.expr);
|
||||
dmp.put(') = 0');
|
||||
} else {
|
||||
dmp.put('^trim(');
|
||||
dumpSqlExpression(dmp, condition.expr);
|
||||
dmp.put(") = ''");
|
||||
}
|
||||
break;
|
||||
case 'isNotEmpty':
|
||||
dmp.put('^trim(');
|
||||
dumpSqlExpression(dmp, condition.expr);
|
||||
dmp.put(") <> ''");
|
||||
// Use DATALENGTH for MSSQL TEXT/NTEXT/IMAGE columns to avoid TRIM error
|
||||
if (dmp.dialect.useDatalengthForEmptyString?.(condition.expr?.['dataType'])) {
|
||||
dmp.put('^datalength(');
|
||||
dumpSqlExpression(dmp, condition.expr);
|
||||
dmp.put(') > 0');
|
||||
} else {
|
||||
dmp.put('^trim(');
|
||||
dumpSqlExpression(dmp, condition.expr);
|
||||
dmp.put(") <> ''");
|
||||
}
|
||||
break;
|
||||
case 'and':
|
||||
case 'or':
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "6.0.0-alpha.1",
|
||||
"version": "7.0.0-alpha.1",
|
||||
"name": "dbgate-tools",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
@@ -26,7 +26,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^13.7.0",
|
||||
"dbgate-types": "^6.0.0-alpha.1",
|
||||
"dbgate-types": "^7.0.0-alpha.1",
|
||||
"jest": "^28.1.3",
|
||||
"ts-jest": "^28.0.7",
|
||||
"typescript": "^4.4.3"
|
||||
@@ -34,7 +34,7 @@
|
||||
"dependencies": {
|
||||
"blueimp-md5": "^2.19.0",
|
||||
"dbgate-query-splitter": "^4.11.9",
|
||||
"dbgate-sqltree": "^6.0.0-alpha.1",
|
||||
"dbgate-sqltree": "^7.0.0-alpha.1",
|
||||
"debug": "^4.3.4",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
@@ -544,9 +544,14 @@ export class SqlDumper implements AlterProcessor {
|
||||
}
|
||||
this.endCommand();
|
||||
}
|
||||
indexType(ix: IndexInfo) {
|
||||
if (ix.isUnique) {
|
||||
this.put(' ^unique');
|
||||
}
|
||||
}
|
||||
createIndex(ix: IndexInfo) {
|
||||
this.put('^create');
|
||||
if (ix.isUnique) this.put(' ^unique');
|
||||
this.indexType(ix);
|
||||
this.put(' ^index %i &n^on %f (&>&n', ix.constraintName, ix);
|
||||
this.putCollection(',&n', ix.columns, col => {
|
||||
this.put('%i %k', col.columnName, col.isDescending == true ? 'DESC' : 'ASC');
|
||||
|
||||
@@ -27,6 +27,7 @@ interface SqlGeneratorOptions {
|
||||
createIndexes: boolean;
|
||||
insert: boolean;
|
||||
skipAutoincrementColumn: boolean;
|
||||
skipComputedColumns: boolean;
|
||||
disableConstraints: boolean;
|
||||
omitNulls: boolean;
|
||||
truncate: boolean;
|
||||
@@ -260,9 +261,12 @@ export class SqlGenerator {
|
||||
}
|
||||
|
||||
processReadable(table: TableInfo, readable) {
|
||||
const columnsFiltered = this.options.skipAutoincrementColumn
|
||||
const columnsFilteredPre = this.options.skipAutoincrementColumn
|
||||
? table.columns.filter(x => !x.autoIncrement)
|
||||
: table.columns;
|
||||
const columnsFiltered = this.options.skipComputedColumns
|
||||
? columnsFilteredPre.filter(x => !x.computedExpression)
|
||||
: columnsFilteredPre;
|
||||
const columnNames = columnsFiltered.map(x => x.columnName);
|
||||
let isClosed = false;
|
||||
let isHeaderRead = false;
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
import _omit from 'lodash/omit';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
|
||||
export const DB_KEYS_SHOW_INCREMENT = 100;
|
||||
|
||||
export interface DbKeysNodeModelBase {
|
||||
text?: string;
|
||||
sortKey: string;
|
||||
key: string;
|
||||
count?: number;
|
||||
level: number;
|
||||
keyPath: string[];
|
||||
parentKey: string;
|
||||
}
|
||||
|
||||
export interface DbKeysLeafNodeModel extends DbKeysNodeModelBase {
|
||||
type: 'string' | 'hash' | 'set' | 'list' | 'zset' | 'stream' | 'binary' | 'ReJSON-RL';
|
||||
}
|
||||
|
||||
export interface DbKeysFolderNodeModel extends DbKeysNodeModelBase {
|
||||
// root: string;
|
||||
type: 'dir';
|
||||
// visibleCount?: number;
|
||||
// isExpanded?: boolean;
|
||||
}
|
||||
|
||||
export interface DbKeysFolderStateMode {
|
||||
key: string;
|
||||
visibleCount?: number;
|
||||
isExpanded?: boolean;
|
||||
}
|
||||
|
||||
export interface DbKeysTreeModel {
|
||||
treeKeySeparator: string;
|
||||
root: DbKeysFolderNodeModel;
|
||||
dirsByKey: { [key: string]: DbKeysFolderNodeModel };
|
||||
dirStateByKey: { [key: string]: DbKeysFolderStateMode };
|
||||
childrenByKey: { [key: string]: DbKeysNodeModel[] };
|
||||
keyObjectsByKey: { [key: string]: DbKeysNodeModel };
|
||||
scannedKeys: number;
|
||||
loadCount: number;
|
||||
dbsize: number;
|
||||
cursor: string;
|
||||
loadedAll: boolean;
|
||||
// refreshAll?: boolean;
|
||||
}
|
||||
|
||||
export type DbKeysNodeModel = DbKeysLeafNodeModel | DbKeysFolderNodeModel;
|
||||
|
||||
export interface DbKeyLoadedModel {
|
||||
key: string;
|
||||
|
||||
type: 'string' | 'hash' | 'set' | 'list' | 'zset' | 'stream' | 'binary' | 'ReJSON-RL';
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface DbKeysLoadResult {
|
||||
nextCursor: string;
|
||||
keys: DbKeyLoadedModel[];
|
||||
dbsize: number;
|
||||
}
|
||||
|
||||
// export type DbKeysLoadFunction = (root: string, limit: number) => Promise<DbKeysLoadResult>;
|
||||
|
||||
export type DbKeysChangeModelFunction = (
|
||||
func: (model: DbKeysTreeModel) => DbKeysTreeModel,
|
||||
loadNextPage: boolean
|
||||
) => void;
|
||||
|
||||
// function dbKeys_findFolderNode(node: DbKeysNodeModel, root: string) {
|
||||
// if (node.type != 'dir') {
|
||||
// return null;
|
||||
// }
|
||||
// if (node.root === root) {
|
||||
// return node;
|
||||
// }
|
||||
// for (const child of node.children ?? []) {
|
||||
// const res = dbKeys_findFolderNode(child, root);
|
||||
// if (res) {
|
||||
// return res;
|
||||
// }
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// export async function dbKeys_loadKeysFromNode(
|
||||
// tree: DbKeysTreeModel,
|
||||
// callingRoot: string,
|
||||
// separator: string,
|
||||
// loader: DbKeysLoadFunction
|
||||
// ): Promise<DbKeysTreeModel> {
|
||||
// const callingRootNode = tree.dirsByKey[callingRoot];
|
||||
// if (!callingRootNode) {
|
||||
// return tree;
|
||||
// }
|
||||
// const newItems = await loader(callingRoot, callingRootNode.maxShowCount ?? SHOW_INCREMENT);
|
||||
|
||||
// return {
|
||||
// ...tree,
|
||||
// childrenByKey: {
|
||||
// ...tree.childrenByKey,
|
||||
// [callingRoot]: newItems,
|
||||
// },
|
||||
// };
|
||||
// }
|
||||
|
||||
// export async function dbKeys_loadMissing(tree: DbKeysTreeModel, loader: DbKeysLoadFunction): Promise<DbKeysTreeModel> {
|
||||
// const childrenByKey = { ...tree.childrenByKey };
|
||||
// const dirsByKey = { ...tree.dirsByKey };
|
||||
|
||||
// for (const root in tree.dirsByKey) {
|
||||
// const dir = tree.dirsByKey[root];
|
||||
|
||||
// if (dir.isExpanded && dir.shouldLoadNext) {
|
||||
// if (!tree.childrenByKey[root] || dir.hasNext) {
|
||||
// const loadCount = dir.maxShowCount && dir.shouldLoadNext ? dir.maxShowCount + SHOW_INCREMENT : SHOW_INCREMENT;
|
||||
// const items = await loader(root, loadCount + 1);
|
||||
|
||||
// childrenByKey[root] = items.slice(0, loadCount);
|
||||
// dirsByKey[root] = {
|
||||
// ...dir,
|
||||
// shouldLoadNext: false,
|
||||
// maxShowCount: loadCount,
|
||||
// hasNext: items.length > loadCount,
|
||||
// };
|
||||
|
||||
// for (const child of items.slice(0, loadCount)) {
|
||||
// if (child.type == 'dir' && !dirsByKey[child.root]) {
|
||||
// dirsByKey[child.root] = {
|
||||
// shouldLoadNext: false,
|
||||
// maxShowCount: null,
|
||||
// hasNext: false,
|
||||
// isExpanded: false,
|
||||
// type: 'dir',
|
||||
// level: dir.level + 1,
|
||||
// root: child.root,
|
||||
// text: child.text,
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// dirsByKey[root] = {
|
||||
// ...dir,
|
||||
// shouldLoadNext: false,
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return {
|
||||
// ...tree,
|
||||
// dirsByKey,
|
||||
// childrenByKey,
|
||||
// refreshAll: false,
|
||||
// };
|
||||
// }
|
||||
|
||||
export function dbKeys_mergeNextPage(tree: DbKeysTreeModel, nextPage: DbKeysLoadResult): DbKeysTreeModel {
|
||||
const keyObjectsByKey = { ...tree.keyObjectsByKey };
|
||||
|
||||
for (const keyObj of nextPage.keys) {
|
||||
const keyPath = keyObj.key.split(tree.treeKeySeparator);
|
||||
keyObjectsByKey[keyObj.key] = {
|
||||
...keyObj,
|
||||
level: keyPath.length,
|
||||
text: keyPath[keyPath.length - 1],
|
||||
sortKey: keyPath[keyPath.length - 1],
|
||||
keyPath,
|
||||
parentKey: keyPath.slice(0, -1).join(tree.treeKeySeparator),
|
||||
};
|
||||
}
|
||||
|
||||
const dirsByKey: { [key: string]: DbKeysFolderNodeModel } = {};
|
||||
const childrenByKey: { [key: string]: DbKeysNodeModel[] } = {};
|
||||
|
||||
dirsByKey[''] = tree.root;
|
||||
|
||||
for (const keyObj of Object.values(keyObjectsByKey)) {
|
||||
const dirPath = keyObj.keyPath.slice(0, -1);
|
||||
const dirKey = dirPath.join(tree.treeKeySeparator);
|
||||
|
||||
let dirDepth = keyObj.keyPath.length - 1;
|
||||
|
||||
while (dirDepth > 0) {
|
||||
const newDirPath = keyObj.keyPath.slice(0, dirDepth);
|
||||
const newDirKey = newDirPath.join(tree.treeKeySeparator);
|
||||
if (!dirsByKey[newDirKey]) {
|
||||
dirsByKey[newDirKey] = {
|
||||
level: keyObj.level - 1,
|
||||
keyPath: newDirPath,
|
||||
parentKey: newDirPath.slice(0, -1).join(tree.treeKeySeparator),
|
||||
type: 'dir',
|
||||
key: newDirKey,
|
||||
text: `${newDirPath[newDirPath.length - 1]}${tree.treeKeySeparator}*`,
|
||||
sortKey: newDirPath[newDirPath.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
dirDepth -= 1;
|
||||
}
|
||||
|
||||
if (!childrenByKey[dirKey]) {
|
||||
childrenByKey[dirKey] = [];
|
||||
}
|
||||
|
||||
childrenByKey[dirKey].push(keyObj);
|
||||
}
|
||||
|
||||
for (const dirObj of Object.values(dirsByKey)) {
|
||||
if (dirObj.key == '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!childrenByKey[dirObj.parentKey]) {
|
||||
childrenByKey[dirObj.parentKey] = [];
|
||||
}
|
||||
childrenByKey[dirObj.parentKey].push(dirObj);
|
||||
|
||||
// set key count
|
||||
dirsByKey[dirObj.key].count = childrenByKey[dirObj.key].length;
|
||||
}
|
||||
|
||||
for (const key in childrenByKey) {
|
||||
childrenByKey[key] = _sortBy(childrenByKey[key], 'sortKey');
|
||||
}
|
||||
|
||||
return {
|
||||
...tree,
|
||||
cursor: nextPage.nextCursor,
|
||||
dirsByKey,
|
||||
childrenByKey,
|
||||
keyObjectsByKey,
|
||||
scannedKeys: tree.scannedKeys + tree.loadCount,
|
||||
loadedAll: nextPage.nextCursor == '0',
|
||||
dbsize: nextPage.dbsize,
|
||||
};
|
||||
}
|
||||
|
||||
export function dbKeys_markNodeExpanded(tree: DbKeysTreeModel, root: string, isExpanded: boolean): DbKeysTreeModel {
|
||||
const node = tree.dirStateByKey[root];
|
||||
return {
|
||||
...tree,
|
||||
dirStateByKey: {
|
||||
...tree.dirStateByKey,
|
||||
[root]: {
|
||||
...node,
|
||||
isExpanded,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function dbKeys_showNextItems(tree: DbKeysTreeModel, root: string): DbKeysTreeModel {
|
||||
const node = tree.dirStateByKey[root];
|
||||
return {
|
||||
...tree,
|
||||
dirStateByKey: {
|
||||
...tree.dirStateByKey,
|
||||
[root]: {
|
||||
...node,
|
||||
visibleCount: (node?.visibleCount ?? DB_KEYS_SHOW_INCREMENT) + DB_KEYS_SHOW_INCREMENT,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function dbKeys_createNewModel(treeKeySeparator: string): DbKeysTreeModel {
|
||||
const root: DbKeysFolderNodeModel = {
|
||||
level: 0,
|
||||
type: 'dir',
|
||||
keyPath: [],
|
||||
parentKey: '',
|
||||
key: '',
|
||||
sortKey: '',
|
||||
};
|
||||
return {
|
||||
treeKeySeparator,
|
||||
childrenByKey: {},
|
||||
keyObjectsByKey: {},
|
||||
dirsByKey: {
|
||||
'': root,
|
||||
},
|
||||
dirStateByKey: {
|
||||
'': {
|
||||
key: '',
|
||||
visibleCount: DB_KEYS_SHOW_INCREMENT,
|
||||
isExpanded: true,
|
||||
},
|
||||
},
|
||||
scannedKeys: 0,
|
||||
dbsize: 0,
|
||||
loadCount: 2000,
|
||||
cursor: '0',
|
||||
root,
|
||||
loadedAll: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function dbKeys_clearLoadedData(tree: DbKeysTreeModel): DbKeysTreeModel {
|
||||
return {
|
||||
...tree,
|
||||
childrenByKey: {},
|
||||
keyObjectsByKey: {},
|
||||
dirsByKey: {
|
||||
'': tree.root,
|
||||
},
|
||||
scannedKeys: 0,
|
||||
dbsize: 0,
|
||||
cursor: '0',
|
||||
loadedAll: false,
|
||||
};
|
||||
}
|
||||
|
||||
// export function dbKeys_reloadFolder(tree: DbKeysTreeModel, root: string): DbKeysTreeModel {
|
||||
// return {
|
||||
// ...tree,
|
||||
// childrenByKey: _omit(tree.childrenByKey, root),
|
||||
// dirsByKey: {
|
||||
// ...tree.dirsByKey,
|
||||
// [root]: {
|
||||
// ...tree.dirsByKey[root],
|
||||
// shouldLoadNext: true,
|
||||
// hasNext: undefined,
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
// }
|
||||
|
||||
function addFlatItems(tree: DbKeysTreeModel, root: string, res: DbKeysNodeModel[], visitedRoots: string[] = []) {
|
||||
const item = tree.dirStateByKey[root];
|
||||
if (!item?.isExpanded) {
|
||||
return false;
|
||||
}
|
||||
const children = tree.childrenByKey[root] || [];
|
||||
for (const child of children) {
|
||||
res.push(child);
|
||||
if (child.type == 'dir') {
|
||||
if (visitedRoots.includes(child.key)) {
|
||||
console.warn('Redis: preventing infinite loop for root', child.key);
|
||||
return false;
|
||||
}
|
||||
addFlatItems(tree, child.key, res, [...visitedRoots, root]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function dbKeys_getFlatList(tree: DbKeysTreeModel) {
|
||||
const res: DbKeysNodeModel[] = [];
|
||||
addFlatItems(tree, '', res);
|
||||
return res;
|
||||
}
|
||||
@@ -24,6 +24,6 @@ export * from './getConnectionLabel';
|
||||
export * from './detectSqlFilterBehaviour';
|
||||
export * from './filterBehaviours';
|
||||
export * from './schemaInfoTools';
|
||||
export * from './dbKeysLoader';
|
||||
export * from './redisKeysLoader';
|
||||
export * from './rowProgressReporter';
|
||||
export * from './diagramTools';
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
import _omit from 'lodash/omit';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
|
||||
export const DB_KEYS_SHOW_INCREMENT = 100;
|
||||
|
||||
export interface RedisNodeModelBase {
|
||||
text?: string;
|
||||
sortKey: string;
|
||||
key: string;
|
||||
count?: number;
|
||||
level: number;
|
||||
keyPath: string[];
|
||||
parentKey: string;
|
||||
}
|
||||
|
||||
export interface RedisLeafNodeModel extends RedisNodeModelBase {
|
||||
type: 'string' | 'hash' | 'set' | 'list' | 'zset' | 'stream' | 'binary' | 'ReJSON-RL';
|
||||
}
|
||||
|
||||
export interface RedisFolderNodeModel extends RedisNodeModelBase {
|
||||
// root: string;
|
||||
type: 'dir';
|
||||
// visibleCount?: number;
|
||||
// isExpanded?: boolean;
|
||||
}
|
||||
|
||||
export interface RedisFolderStateMode {
|
||||
key: string;
|
||||
visibleCount?: number;
|
||||
isExpanded?: boolean;
|
||||
}
|
||||
|
||||
export interface RedisTreeModel {
|
||||
treeKeySeparator: string;
|
||||
root: RedisFolderNodeModel;
|
||||
dirsByKey: { [key: string]: RedisFolderNodeModel };
|
||||
dirStateByKey: { [key: string]: RedisFolderStateMode };
|
||||
childrenByKey: { [key: string]: RedisNodeModel[] };
|
||||
keyObjectsByKey: { [key: string]: RedisNodeModel };
|
||||
scannedKeys: number;
|
||||
loadCount: number;
|
||||
dbsize: number;
|
||||
cursor: string;
|
||||
loadedAll: boolean;
|
||||
// refreshAll?: boolean;
|
||||
}
|
||||
|
||||
export type RedisNodeModel = RedisLeafNodeModel | RedisFolderNodeModel;
|
||||
|
||||
export interface RedisLoadedModel {
|
||||
key: string;
|
||||
|
||||
type: 'string' | 'hash' | 'set' | 'list' | 'zset' | 'stream' | 'binary' | 'ReJSON-RL';
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface RedisLoadResult {
|
||||
nextCursor: string;
|
||||
keys: RedisLoadedModel[];
|
||||
dbsize: number;
|
||||
}
|
||||
|
||||
export type RedisChangeModelFunction = (func: (model: RedisTreeModel) => RedisTreeModel, loadNextPage: boolean) => void;
|
||||
|
||||
export function redis_mergeNextPage(tree: RedisTreeModel, nextPage: RedisLoadResult): RedisTreeModel {
|
||||
const keyObjectsByKey = { ...tree.keyObjectsByKey };
|
||||
|
||||
for (const keyObj of nextPage.keys) {
|
||||
const keyPath = keyObj.key.split(tree.treeKeySeparator);
|
||||
keyObjectsByKey[keyObj.key] = {
|
||||
...keyObj,
|
||||
level: keyPath.length,
|
||||
text: keyPath[keyPath.length - 1],
|
||||
sortKey: keyPath[keyPath.length - 1],
|
||||
keyPath,
|
||||
parentKey: keyPath.slice(0, -1).join(tree.treeKeySeparator),
|
||||
};
|
||||
}
|
||||
|
||||
const dirsByKey: { [key: string]: RedisFolderNodeModel } = {};
|
||||
const childrenByKey: { [key: string]: RedisNodeModel[] } = {};
|
||||
|
||||
dirsByKey[''] = tree.root;
|
||||
|
||||
for (const keyObj of Object.values(keyObjectsByKey)) {
|
||||
const dirPath = keyObj.keyPath.slice(0, -1);
|
||||
const dirKey = dirPath.join(tree.treeKeySeparator);
|
||||
|
||||
let dirDepth = keyObj.keyPath.length - 1;
|
||||
|
||||
while (dirDepth > 0) {
|
||||
const newDirPath = keyObj.keyPath.slice(0, dirDepth);
|
||||
const newDirKey = newDirPath.join(tree.treeKeySeparator);
|
||||
if (!dirsByKey[newDirKey]) {
|
||||
dirsByKey[newDirKey] = {
|
||||
level: keyObj.level - 1,
|
||||
keyPath: newDirPath,
|
||||
parentKey: newDirPath.slice(0, -1).join(tree.treeKeySeparator),
|
||||
type: 'dir',
|
||||
key: newDirKey,
|
||||
text: `${newDirPath[newDirPath.length - 1]}${tree.treeKeySeparator}*`,
|
||||
sortKey: newDirPath[newDirPath.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
dirDepth -= 1;
|
||||
}
|
||||
|
||||
if (!childrenByKey[dirKey]) {
|
||||
childrenByKey[dirKey] = [];
|
||||
}
|
||||
|
||||
childrenByKey[dirKey].push(keyObj);
|
||||
}
|
||||
|
||||
for (const dirObj of Object.values(dirsByKey)) {
|
||||
if (dirObj.key == '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!childrenByKey[dirObj.parentKey]) {
|
||||
childrenByKey[dirObj.parentKey] = [];
|
||||
}
|
||||
childrenByKey[dirObj.parentKey].push(dirObj);
|
||||
|
||||
// set key count
|
||||
dirsByKey[dirObj.key].count = childrenByKey[dirObj.key].length;
|
||||
}
|
||||
|
||||
for (const key in childrenByKey) {
|
||||
childrenByKey[key] = _sortBy(childrenByKey[key], 'sortKey');
|
||||
}
|
||||
|
||||
return {
|
||||
...tree,
|
||||
cursor: nextPage.nextCursor,
|
||||
dirsByKey,
|
||||
childrenByKey,
|
||||
keyObjectsByKey,
|
||||
scannedKeys: tree.scannedKeys + tree.loadCount,
|
||||
loadedAll: nextPage.nextCursor == '0',
|
||||
dbsize: nextPage.dbsize,
|
||||
};
|
||||
}
|
||||
|
||||
export function redis_markNodeExpanded(tree: RedisTreeModel, root: string, isExpanded: boolean): RedisTreeModel {
|
||||
const node = tree.dirStateByKey[root];
|
||||
return {
|
||||
...tree,
|
||||
dirStateByKey: {
|
||||
...tree.dirStateByKey,
|
||||
[root]: {
|
||||
...node,
|
||||
isExpanded,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function redis_showNextItems(tree: RedisTreeModel, root: string): RedisTreeModel {
|
||||
const node = tree.dirStateByKey[root];
|
||||
return {
|
||||
...tree,
|
||||
dirStateByKey: {
|
||||
...tree.dirStateByKey,
|
||||
[root]: {
|
||||
...node,
|
||||
visibleCount: (node?.visibleCount ?? DB_KEYS_SHOW_INCREMENT) + DB_KEYS_SHOW_INCREMENT,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function redis_createNewModel(treeKeySeparator: string): RedisTreeModel {
|
||||
const root: RedisFolderNodeModel = {
|
||||
level: 0,
|
||||
type: 'dir',
|
||||
keyPath: [],
|
||||
parentKey: '',
|
||||
key: '',
|
||||
sortKey: '',
|
||||
};
|
||||
return {
|
||||
treeKeySeparator,
|
||||
childrenByKey: {},
|
||||
keyObjectsByKey: {},
|
||||
dirsByKey: {
|
||||
'': root,
|
||||
},
|
||||
dirStateByKey: {
|
||||
'': {
|
||||
key: '',
|
||||
visibleCount: DB_KEYS_SHOW_INCREMENT,
|
||||
isExpanded: true,
|
||||
},
|
||||
},
|
||||
scannedKeys: 0,
|
||||
dbsize: 0,
|
||||
loadCount: 2000,
|
||||
cursor: '0',
|
||||
root,
|
||||
loadedAll: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function redis_clearLoadedData(tree: RedisTreeModel): RedisTreeModel {
|
||||
return {
|
||||
...tree,
|
||||
childrenByKey: {},
|
||||
keyObjectsByKey: {},
|
||||
dirsByKey: {
|
||||
'': tree.root,
|
||||
},
|
||||
scannedKeys: 0,
|
||||
dbsize: 0,
|
||||
cursor: '0',
|
||||
loadedAll: false,
|
||||
};
|
||||
}
|
||||
|
||||
function addFlatItems(tree: RedisTreeModel, root: string, res: RedisNodeModel[], visitedRoots: string[] = []) {
|
||||
const item = tree.dirStateByKey[root];
|
||||
if (!item?.isExpanded) {
|
||||
return false;
|
||||
}
|
||||
const children = tree.childrenByKey[root] || [];
|
||||
for (const child of children) {
|
||||
res.push(child);
|
||||
if (child.type == 'dir') {
|
||||
if (visitedRoots.includes(child.key)) {
|
||||
console.warn('Redis: preventing infinite loop for root', child.key);
|
||||
return false;
|
||||
}
|
||||
addFlatItems(tree, child.key, res, [...visitedRoots, root]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function redis_getFlatList(tree: RedisTreeModel) {
|
||||
const res: RedisNodeModel[] = [];
|
||||
addFlatItems(tree, '', res);
|
||||
return res;
|
||||
}
|
||||
|
||||
export interface SupportedRedisKeyType {
|
||||
name: string;
|
||||
label: string;
|
||||
dbKeyFields: {
|
||||
name: string;
|
||||
cols?: number;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
}[];
|
||||
dbKeyFieldsForGrid?: {
|
||||
name: string;
|
||||
cols?: number;
|
||||
label?: string;
|
||||
}[];
|
||||
keyColumn?: string;
|
||||
showItemList?: boolean;
|
||||
showGeneratedId?: boolean;
|
||||
}
|
||||
|
||||
export const supportedRedisKeyTypes: SupportedRedisKeyType[] = [
|
||||
{
|
||||
name: 'string',
|
||||
label: 'String',
|
||||
dbKeyFields: [{ name: 'value' }],
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
label: 'List',
|
||||
dbKeyFields: [{ name: 'value', cols: 12 }],
|
||||
showItemList: true,
|
||||
},
|
||||
{
|
||||
name: 'set',
|
||||
label: 'Set',
|
||||
dbKeyFields: [{ name: 'value', cols: 12 }],
|
||||
keyColumn: 'value',
|
||||
showItemList: true,
|
||||
},
|
||||
{
|
||||
name: 'zset',
|
||||
label: 'Sorted Set',
|
||||
dbKeyFields: [
|
||||
{ name: 'member', cols: 8 },
|
||||
{ name: 'score', cols: 4 },
|
||||
],
|
||||
keyColumn: 'member',
|
||||
showItemList: true,
|
||||
},
|
||||
{
|
||||
name: 'hash',
|
||||
label: 'Hash',
|
||||
dbKeyFields: [
|
||||
{ name: 'key', cols: 3, label: 'Field' },
|
||||
{ name: 'value', cols: 7 },
|
||||
{ name: 'ttl', cols: 2, label: 'TTL' },
|
||||
],
|
||||
keyColumn: 'key',
|
||||
showItemList: true,
|
||||
},
|
||||
{
|
||||
name: 'stream',
|
||||
label: 'Stream',
|
||||
dbKeyFields: [
|
||||
{ name: 'field', cols: 6 },
|
||||
{ name: 'value', cols: 6 },
|
||||
],
|
||||
dbKeyFieldsForGrid: [
|
||||
{ name: 'id', cols: 6 },
|
||||
{ name: 'value', cols: 6 },
|
||||
],
|
||||
keyColumn: 'id',
|
||||
showItemList: true,
|
||||
showGeneratedId: true,
|
||||
},
|
||||
{
|
||||
name: 'json',
|
||||
label: 'JSON',
|
||||
dbKeyFields: [{ name: 'value' }],
|
||||
},
|
||||
];
|
||||
|
||||
export function findSupportedRedisKeyType(type: string): SupportedRedisKeyType | undefined {
|
||||
return supportedRedisKeyTypes.find(t => t.name === type);
|
||||
}
|
||||
Vendored
+8
@@ -10,6 +10,8 @@ export interface SqlDialect {
|
||||
offsetFetchRangeSyntax?: boolean;
|
||||
offsetFirstSkipSyntax?: boolean;
|
||||
offsetNotSupported?: boolean;
|
||||
useDatalengthForEmptyString?(dataType: string): boolean;
|
||||
disableGroupingForDataType?(dataType: string): boolean;
|
||||
quoteIdentifier(s: string): string;
|
||||
fallbackDataType?: string;
|
||||
explicitDropConstraint?: boolean;
|
||||
@@ -48,6 +50,12 @@ export interface SqlDialect {
|
||||
multipleSchema?: boolean;
|
||||
filteredIndexes?: boolean;
|
||||
namedDefaultConstraint?: boolean;
|
||||
indexTypes?: {
|
||||
value: string;
|
||||
label: string;
|
||||
isUnique?: boolean;
|
||||
indexType?: string;
|
||||
}[];
|
||||
|
||||
specificNullabilityImplementation?: boolean;
|
||||
implicitNullDeclaration?: boolean;
|
||||
|
||||
Vendored
+3
-2
@@ -15,6 +15,8 @@ import {
|
||||
} from './dbinfo';
|
||||
import { FilterBehaviour } from './filter-type';
|
||||
|
||||
export type EngineDriverIcon = string | { light: string; dark?: string };
|
||||
|
||||
export interface StreamOptions {
|
||||
recordset: (columns) => void;
|
||||
row: (row) => void;
|
||||
@@ -240,7 +242,6 @@ export interface EngineDriver<TClient = any, TDataBase = any> extends FilterBeha
|
||||
databaseEngineTypes: string[];
|
||||
editorMode?: string;
|
||||
readOnlySessions: boolean;
|
||||
supportedKeyTypes: SupportedDbKeyType[];
|
||||
dataEditorTypesBehaviour: DataEditorTypesBehaviour;
|
||||
supportsDatabaseUrl?: boolean;
|
||||
supportsDatabaseBackup?: boolean;
|
||||
@@ -262,7 +263,7 @@ export interface EngineDriver<TClient = any, TDataBase = any> extends FilterBeha
|
||||
collectionPluralLabel?: string;
|
||||
collectionNameLabel?: string;
|
||||
newCollectionFormParams?: any[];
|
||||
icon?: any;
|
||||
icon?: EngineDriverIcon;
|
||||
|
||||
supportedCreateDatabase?: boolean;
|
||||
showConnectionField?: (
|
||||
|
||||
Vendored
+1
@@ -28,6 +28,7 @@ export interface ThemeDefinition {
|
||||
isBuiltInTheme?: boolean;
|
||||
themeVariables?: { [key: string]: string };
|
||||
themePublicCloudPath?: string;
|
||||
editorTheme?: string;
|
||||
}
|
||||
|
||||
export interface PluginDefinition {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "6.0.0-alpha.1",
|
||||
"version": "7.0.0-alpha.1",
|
||||
"name": "dbgate-types",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
|
||||
@@ -13,6 +13,8 @@ for (const page of [
|
||||
'admin-license',
|
||||
'set-admin-password',
|
||||
'redirect',
|
||||
'forgot-password',
|
||||
'reset-password',
|
||||
]) {
|
||||
const text = template.replace(/{{page}}/g, page);
|
||||
fs.writeFileSync(`public/${page || 'index'}.html`, text);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dbgate-web",
|
||||
"version": "6.0.0-alpha.1",
|
||||
"version": "7.0.0-alpha.1",
|
||||
"scripts": {
|
||||
"build": "yarn build:index && rollup -c",
|
||||
"dev": "yarn build:index && cross-env API_URL=http://localhost:3000 rollup -c -w",
|
||||
@@ -17,7 +17,6 @@
|
||||
"public"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ant-design/colors": "^5.0.0",
|
||||
"@energiency/chartjs-plugin-piechart-outlabels": "^1.3.4",
|
||||
"@mdi/font": "^7.1.96",
|
||||
"@rollup/plugin-commonjs": "^20.0.0",
|
||||
@@ -33,11 +32,11 @@
|
||||
"chartjs-adapter-moment": "^1.0.0",
|
||||
"chartjs-plugin-datalabels": "^2.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dbgate-datalib": "^6.0.0-alpha.1",
|
||||
"dbgate-datalib": "^7.0.0-alpha.1",
|
||||
"dbgate-query-splitter": "^4.11.9",
|
||||
"dbgate-sqltree": "^6.0.0-alpha.1",
|
||||
"dbgate-tools": "^6.0.0-alpha.1",
|
||||
"dbgate-types": "^6.0.0-alpha.1",
|
||||
"dbgate-sqltree": "^7.0.0-alpha.1",
|
||||
"dbgate-tools": "^7.0.0-alpha.1",
|
||||
"dbgate-types": "^7.0.0-alpha.1",
|
||||
"diff": "^5.0.0",
|
||||
"diff2html": "^3.4.13",
|
||||
"file-selector": "^0.2.4",
|
||||
|
||||
@@ -91,19 +91,7 @@ body {
|
||||
overflow: scroll;
|
||||
}
|
||||
.bg-0 {
|
||||
background-color: var(--theme-bg-0);
|
||||
}
|
||||
.bg-1 {
|
||||
background-color: var(--theme-bg-1);
|
||||
}
|
||||
.bg-2 {
|
||||
background-color: var(--theme-bg-2);
|
||||
}
|
||||
.bg-3 {
|
||||
background-color: var(--theme-bg-3);
|
||||
}
|
||||
.bg-4 {
|
||||
background-color: var(--theme-bg-4);
|
||||
background-color: var(--theme-content-background);
|
||||
}
|
||||
|
||||
.col-10 {
|
||||
@@ -272,6 +260,7 @@ textarea[disabled] {
|
||||
.ace_gutter-cell.ace-gutter-sql-run {
|
||||
background-repeat: no-repeat;
|
||||
background-position: 2px center;
|
||||
background-size: 12px 12px;
|
||||
|
||||
/* content: '▶';
|
||||
margin-right: 3px; */
|
||||
@@ -290,13 +279,12 @@ textarea[disabled] {
|
||||
}
|
||||
|
||||
.ace_gutter-cell.ace-gutter-sql-run:hover {
|
||||
background-color: var(--theme-bg-2);
|
||||
background-color: var(--theme-datagrid-cell-background-alt);
|
||||
}
|
||||
|
||||
.ace_gutter-cell.ace-gutter-current-part {
|
||||
/* background-color: var(--theme-bg-2); */
|
||||
font-weight: bold;
|
||||
color: var(--theme-font-hover);
|
||||
color: var(--theme-generic-font-hover);
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--theme-bg-selected);
|
||||
background: var(--theme-table-selected-background);
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
z-index: 1000;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<script lang="ts">
|
||||
import { writable } from 'svelte/store';
|
||||
import FormTextField from './forms/FormTextField.svelte';
|
||||
import FormSubmit from './forms/FormSubmit.svelte';
|
||||
import SpecialPageLayout from './widgets/SpecialPageLayout.svelte';
|
||||
import FormProviderCore from './forms/FormProviderCore.svelte';
|
||||
import { apiCall } from './utility/api';
|
||||
import ErrorInfo from './elements/ErrorInfo.svelte';
|
||||
import Link from './elements/Link.svelte';
|
||||
|
||||
const values = writable({});
|
||||
|
||||
let error = null;
|
||||
let success = false;
|
||||
let isSubmitting = false;
|
||||
</script>
|
||||
|
||||
<SpecialPageLayout>
|
||||
<FormProviderCore {values}>
|
||||
<div class="heading">Reset Password</div>
|
||||
|
||||
{#if success}
|
||||
<div class="success-message">
|
||||
If an account with that email exists, we've sent a password reset link to your email address. Please check your
|
||||
inbox and follow the instructions.
|
||||
</div>
|
||||
<div class="back-link">
|
||||
<Link internalRedirect="/login.html" data-testid="ForgotPasswordPage_backToLogin">
|
||||
Back to Login
|
||||
</Link>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text">
|
||||
Enter your email address and we'll send you a link to reset your password.
|
||||
</div>
|
||||
|
||||
<FormTextField
|
||||
label="Email"
|
||||
name="email"
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
saveOnInput
|
||||
data-testid="ForgotPasswordPage_email"
|
||||
/>
|
||||
|
||||
{#if error}
|
||||
<ErrorInfo message={error} />
|
||||
{/if}
|
||||
|
||||
<div class="submit">
|
||||
<FormSubmit
|
||||
value={isSubmitting ? 'Sending...' : 'Send Reset Link'}
|
||||
disabled={isSubmitting}
|
||||
on:click={async e => {
|
||||
error = null;
|
||||
isSubmitting = true;
|
||||
const resp = await apiCall('storage/request-password-reset', e.detail);
|
||||
isSubmitting = false;
|
||||
if (resp?.error) {
|
||||
error = resp.error;
|
||||
return;
|
||||
}
|
||||
success = true;
|
||||
}}
|
||||
data-testid="ForgotPasswordPage_submit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="back-link">
|
||||
<Link internalRedirect="/login.html" data-testid="ForgotPasswordPage_backToLogin">
|
||||
Back to Login
|
||||
</Link>
|
||||
</div>
|
||||
{/if}
|
||||
</FormProviderCore>
|
||||
</SpecialPageLayout>
|
||||
|
||||
<style>
|
||||
.heading {
|
||||
text-align: center;
|
||||
margin: 1em;
|
||||
font-size: xx-large;
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin: var(--dim-large-form-margin);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.submit :global(input) {
|
||||
flex: 1;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.submit :global(input:disabled) {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: var(--dim-large-form-margin);
|
||||
margin-right: var(--dim-large-form-margin);
|
||||
margin-bottom: var(--dim-large-form-margin);
|
||||
}
|
||||
|
||||
.success-message {
|
||||
margin: var(--dim-large-form-margin);
|
||||
padding: 15px;
|
||||
background-color: var(--theme-bg-green);
|
||||
border: 1px solid var(--theme-border);
|
||||
border-radius: 4px;
|
||||
color: var(--theme-generic-font);
|
||||
}
|
||||
|
||||
.back-link {
|
||||
margin: var(--dim-large-form-margin);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -5,7 +5,7 @@
|
||||
import FormSubmit from './forms/FormSubmit.svelte';
|
||||
import FormTextField from './forms/FormTextField.svelte';
|
||||
import { apiCall, enableApi, strmid } from './utility/api';
|
||||
import { useConfig } from './utility/metadataLoaders';
|
||||
import { useConfig, useSettings } from './utility/metadataLoaders';
|
||||
import ErrorInfo from './elements/ErrorInfo.svelte';
|
||||
import FormSelectField from './forms/FormSelectField.svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
@@ -18,6 +18,7 @@
|
||||
export let isAdminPage;
|
||||
|
||||
const config = useConfig();
|
||||
const settings = useSettings();
|
||||
|
||||
let availableConnections = null;
|
||||
let availableProviders = [];
|
||||
@@ -199,6 +200,13 @@
|
||||
saveOnInput
|
||||
data-testid="LoginPage_password"
|
||||
/>
|
||||
{#if selectedProvider?.type == 'local' && $settings?.['storage.allowForgottenPasswordReset']}
|
||||
<div class="forgot-password-link">
|
||||
<Link internalRedirect="/forgot-password.html" data-testid="LoginPage_forgotPassword">
|
||||
Don't remember your password?
|
||||
</Link>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -349,4 +357,11 @@
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.forgot-password-link {
|
||||
margin: var(--dim-large-form-margin);
|
||||
margin-top: 5px;
|
||||
margin-bottom: 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
<script lang="ts">
|
||||
import { writable } from 'svelte/store';
|
||||
import { onMount } from 'svelte';
|
||||
import FormPasswordField from './forms/FormPasswordField.svelte';
|
||||
import FormSubmit from './forms/FormSubmit.svelte';
|
||||
import SpecialPageLayout from './widgets/SpecialPageLayout.svelte';
|
||||
import FormProviderCore from './forms/FormProviderCore.svelte';
|
||||
import { apiCall } from './utility/api';
|
||||
import ErrorInfo from './elements/ErrorInfo.svelte';
|
||||
import Link from './elements/Link.svelte';
|
||||
|
||||
const values = writable({});
|
||||
|
||||
let error = null;
|
||||
let success = false;
|
||||
let isSubmitting = false;
|
||||
let token = null;
|
||||
|
||||
onMount(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
token = urlParams.get('token');
|
||||
|
||||
if (!token) {
|
||||
error = 'Invalid reset link';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<SpecialPageLayout>
|
||||
<FormProviderCore {values}>
|
||||
<div class="heading">Set New Password</div>
|
||||
|
||||
{#if success}
|
||||
<div class="success-message">
|
||||
Your password has been reset successfully! You can now log in with your new password.
|
||||
</div>
|
||||
<div class="back-link">
|
||||
<Link internalRedirect="/login.html" data-testid="ResetPasswordPage_backToLogin">
|
||||
Go to Login
|
||||
</Link>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text">
|
||||
Enter your new password below.
|
||||
</div>
|
||||
|
||||
<FormPasswordField
|
||||
label="New Password"
|
||||
name="newPassword"
|
||||
autocomplete="new-password"
|
||||
saveOnInput
|
||||
data-testid="ResetPasswordPage_newPassword"
|
||||
/>
|
||||
|
||||
<FormPasswordField
|
||||
label="Confirm Password"
|
||||
name="confirmPassword"
|
||||
autocomplete="new-password"
|
||||
saveOnInput
|
||||
data-testid="ResetPasswordPage_confirmPassword"
|
||||
/>
|
||||
|
||||
{#if error}
|
||||
<ErrorInfo message={error} />
|
||||
{/if}
|
||||
|
||||
<div class="submit">
|
||||
<FormSubmit
|
||||
value={isSubmitting ? 'Resetting...' : 'Reset Password'}
|
||||
disabled={isSubmitting || !token}
|
||||
on:click={async e => {
|
||||
error = null;
|
||||
|
||||
if (!e.detail.newPassword || !e.detail.confirmPassword) {
|
||||
error = 'Please fill in all fields';
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.detail.newPassword !== e.detail.confirmPassword) {
|
||||
error = 'Passwords do not match';
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.detail.newPassword.length < 6) {
|
||||
error = 'Password must be at least 6 characters long';
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting = true;
|
||||
const resp = await apiCall('storage/reset-password', {
|
||||
token,
|
||||
newPassword: e.detail.newPassword,
|
||||
});
|
||||
isSubmitting = false;
|
||||
|
||||
if (resp?.error) {
|
||||
error = resp.error;
|
||||
return;
|
||||
}
|
||||
|
||||
success = true;
|
||||
}}
|
||||
data-testid="ResetPasswordPage_submit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="back-link">
|
||||
<Link internalRedirect="/login.html" data-testid="ResetPasswordPage_backToLogin">
|
||||
Back to Login
|
||||
</Link>
|
||||
</div>
|
||||
{/if}
|
||||
</FormProviderCore>
|
||||
</SpecialPageLayout>
|
||||
|
||||
<style>
|
||||
.heading {
|
||||
text-align: center;
|
||||
margin: 1em;
|
||||
font-size: xx-large;
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin: var(--dim-large-form-margin);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.submit :global(input) {
|
||||
flex: 1;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.submit :global(input:disabled) {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: var(--dim-large-form-margin);
|
||||
margin-right: var(--dim-large-form-margin);
|
||||
margin-bottom: var(--dim-large-form-margin);
|
||||
}
|
||||
|
||||
.success-message {
|
||||
margin: var(--dim-large-form-margin);
|
||||
padding: 15px;
|
||||
background-color: var(--theme-bg-green);
|
||||
border: 1px solid var(--theme-border);
|
||||
border-radius: 4px;
|
||||
color: var(--theme-generic-font);
|
||||
}
|
||||
|
||||
.back-link {
|
||||
margin: var(--dim-large-form-margin);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -155,7 +155,7 @@
|
||||
height: var(--dim-toolbar-height);
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--theme-bg-1);
|
||||
background: var(--theme-toolstrip-background);
|
||||
}
|
||||
|
||||
.left-splitter {
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
<script lang="ts" context="module">
|
||||
import { cloudConnectionsStore } from '../stores';
|
||||
import { cloudConnectionsStore, DEFAULT_CONNECTION_SEARCH_SETTINGS } from '../stores';
|
||||
import { apiCall } from '../utility/api';
|
||||
import AppObjectCore from './AppObjectCore.svelte';
|
||||
|
||||
export const extractKey = data => data.cntid;
|
||||
|
||||
export const createMatcher =
|
||||
filter =>
|
||||
({ name }) =>
|
||||
filterName(filter, name);
|
||||
(filter, cfg = DEFAULT_CONNECTION_SEARCH_SETTINGS) =>
|
||||
props => {
|
||||
const { conid, name } = props;
|
||||
const databases = getLocalStorage(`database_list_${conid}`) || [];
|
||||
|
||||
return filterNameCompoud(filter, [name], cfg.database ? databases.map(x => x.name) : []);
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { filterName, getConnectionLabel } from 'dbgate-tools';
|
||||
import { filterNameCompoud } from 'dbgate-tools';
|
||||
import ConnectionAppObject, { openConnection } from './ConnectionAppObject.svelte';
|
||||
import { _t } from '../translations';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||
import SavedFileAppObject from './SavedFileAppObject.svelte';
|
||||
import { getLocalStorage } from '../utility/storageCache';
|
||||
|
||||
export let data;
|
||||
export let passProps;
|
||||
|
||||
@@ -143,6 +143,8 @@
|
||||
import { getConnectionClickActionSetting } from '../settings/settingsTools';
|
||||
import { _t } from '../translations';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import { currentThemeType } from '../plugins/themes';
|
||||
import { getDriverIcon } from '../utility/driverIcons';
|
||||
|
||||
export let data;
|
||||
export let passProps;
|
||||
@@ -152,6 +154,8 @@
|
||||
let extInfo = null;
|
||||
let engineStatusIcon = null;
|
||||
let engineStatusTitle = null;
|
||||
let driverIcon = null;
|
||||
let connectionIcon = null;
|
||||
|
||||
$: isPinned = data.singleDatabase && !!$pinnedDatabases.find(x => x?.connection?._id == data?._id);
|
||||
|
||||
@@ -432,13 +436,17 @@
|
||||
|
||||
$: apps = useAllApps();
|
||||
$: driver = $extensions.drivers.find(x => x.engine == data.engine);
|
||||
$: driverIcon = getDriverIcon(driver, $currentThemeType);
|
||||
$: connectionIcon =
|
||||
driverIcon ||
|
||||
(data._id.startsWith('cloud://') ? 'img cloud-connection' : data.singleDatabase ? 'img database' : 'img server');
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
{...$$restProps}
|
||||
{data}
|
||||
title={getConnectionLabel(data, { showUnsaved: true })}
|
||||
icon={driver?.icon || (data._id.startsWith('cloud://') ? 'img cloud-connection' : data.singleDatabase ? 'img database' : 'img server')}
|
||||
icon={connectionIcon}
|
||||
isBold={data.singleDatabase
|
||||
? $currentDatabase?.connection?._id == data._id && $currentDatabase?.name == data.defaultDatabase
|
||||
: $currentDatabase?.connection?._id == data._id}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts" context="module">
|
||||
import DatabaseAppObject from './DatabaseAppObject.svelte';
|
||||
import DatabaseObjectAppObject from './DatabaseObjectAppObject.svelte';
|
||||
import { extensions } from '../stores';
|
||||
|
||||
export const extractKey = data => {
|
||||
if (data.objectTypeField) {
|
||||
@@ -27,11 +26,19 @@
|
||||
|
||||
<script lang="ts">
|
||||
import _, { values } from 'lodash';
|
||||
import { draggedPinnedObject, pinnedDatabases, pinnedTables } from '../stores';
|
||||
import { draggedPinnedObject, extensions, pinnedDatabases, pinnedTables } from '../stores';
|
||||
import { getConnectionLabel } from 'dbgate-tools';
|
||||
import { currentThemeType } from '../plugins/themes';
|
||||
import { getDriverIcon } from '../utility/driverIcons';
|
||||
|
||||
export let data;
|
||||
export let passProps;
|
||||
|
||||
let pinnedDriver = null;
|
||||
let pinnedDriverIcon = null;
|
||||
|
||||
$: pinnedDriver = $extensions?.drivers?.find(x => x.engine == data?.connection?.engine);
|
||||
$: pinnedDriverIcon = getDriverIcon(pinnedDriver, $currentThemeType);
|
||||
</script>
|
||||
|
||||
{#if data}
|
||||
@@ -73,7 +80,7 @@
|
||||
$draggedPinnedObject = null;
|
||||
}}
|
||||
passExtInfo={getConnectionLabel(data.connection)}
|
||||
passIcon={$extensions.drivers.find(x => x.engine == data.connection.engine)?.icon}
|
||||
passIcon={pinnedDriverIcon}
|
||||
passColorMark={passProps?.connectionColorFactory && passProps?.connectionColorFactory({ conid: data.connection._id })}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
import SubDatabaseList from './SubDatabaseList.svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
import { getLocalStorage } from '../utility/storageCache';
|
||||
</script>
|
||||
|
||||
{#if data.conid && $cloudConnectionsStore[data.conid]}
|
||||
<SubDatabaseList {...$$props} data={$cloudConnectionsStore[data.conid]} />
|
||||
{:else if data.conid && getLocalStorage(`database_list_${data.conid}`)}
|
||||
<SubDatabaseList {...$$props} data={{ _id: data.conid }} />
|
||||
{/if}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
}
|
||||
|
||||
.outer.useBorder {
|
||||
background-color: var(--theme-inlinebutton-bordered-background);
|
||||
background: var(--theme-inlinebutton-bordered-background);
|
||||
border: var(--theme-inlinebutton-bordered-border);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { get_current_component } from 'svelte/internal';
|
||||
import createActivator, { isComponentActiveStore } from '../utility/createActivator';
|
||||
import { useSettings } from '../utility/metadataLoaders';
|
||||
|
||||
const thisInstance = get_current_component();
|
||||
const settings = useSettings();
|
||||
|
||||
export let showAlways = false;
|
||||
export const activator = showAlways ? null : createActivator('ToolStripContainer', true);
|
||||
@@ -13,13 +15,19 @@
|
||||
|
||||
export let scrollContent = false;
|
||||
export let hideToolStrip = false;
|
||||
export let toolstripPosition = 'top'; // 'top' | 'bottom'
|
||||
export let toolstripPosition = 'auto'; // 'top' | 'bottom'
|
||||
|
||||
$: isComponentActive = showAlways || ($isComponentActiveStore('ToolStripContainer', thisInstance) && !hideToolStrip);
|
||||
|
||||
$: realToolstripPosition =
|
||||
toolstripPosition == 'auto' ? ($settings?.['settings.toolbarPosition'] ?? 'top') : toolstripPosition;
|
||||
|
||||
$: realToolstripPositionFixed =
|
||||
realToolstripPosition == 'top' || realToolstripPosition == 'bottom' ? realToolstripPosition : 'top';
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
{#if isComponentActive && toolstripPosition === 'top'}
|
||||
{#if isComponentActive && realToolstripPositionFixed === 'top'}
|
||||
<div class="toolstrip">
|
||||
<slot name="toolstrip" />
|
||||
</div>
|
||||
@@ -29,7 +37,7 @@
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
{#if isComponentActive && toolstripPosition === 'bottom'}
|
||||
{#if isComponentActive && realToolstripPositionFixed === 'bottom'}
|
||||
<div class="toolstrip">
|
||||
<slot name="toolstrip" />
|
||||
</div>
|
||||
|
||||
@@ -125,7 +125,16 @@
|
||||
}
|
||||
|
||||
function startEditing(field) {
|
||||
if (editingColumn === field.uniqueName) {
|
||||
tick().then(() => {
|
||||
if (!domEditor) return;
|
||||
domEditor.focus();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!editable || !grider) return;
|
||||
|
||||
editingColumn = field.uniqueName;
|
||||
editValue = field.hasMultipleValues ? '' : stringifyCellValue(field.value, 'inlineEditorIntent', editorTypes).value;
|
||||
isChangedRef.set(false);
|
||||
@@ -149,6 +158,13 @@
|
||||
editingColumn = null;
|
||||
event.preventDefault();
|
||||
break;
|
||||
case keycodes.n0:
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
setCellValue(field.uniqueName, null);
|
||||
editingColumn = null;
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
case keycodes.tab:
|
||||
case keycodes.upArrow:
|
||||
case keycodes.downArrow:
|
||||
@@ -227,6 +243,7 @@
|
||||
|
||||
function getJsonParsedValue(value) {
|
||||
if (editorTypes?.explicitDataType) return null;
|
||||
if (_.isPlainObject(value) || _.isArray(value)) return value;
|
||||
if (!isJsonLikeLongString(value)) return null;
|
||||
return safeJsonParse(value);
|
||||
}
|
||||
@@ -362,8 +379,10 @@
|
||||
padding: 6px 8px;
|
||||
background: var(--theme-table-cell-background);
|
||||
min-height: 20px;
|
||||
word-break: break-all;
|
||||
position: relative;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.field-value.editable {
|
||||
|
||||
@@ -212,7 +212,14 @@ export async function handleAuthOnStartup(config) {
|
||||
if (page == 'set-admin-password') return;
|
||||
if (checkAdminPasswordSet()) return;
|
||||
|
||||
if (page == 'login' || page == 'admin-login' || page == 'not-logged') return;
|
||||
if (
|
||||
page == 'login' ||
|
||||
page == 'admin-login' ||
|
||||
page == 'not-logged' ||
|
||||
page == 'forgot-password' ||
|
||||
page == 'reset-password'
|
||||
)
|
||||
return;
|
||||
if (checkLoggedUser()) return;
|
||||
|
||||
if (page == 'license' || page == 'admin-license') return;
|
||||
@@ -277,7 +284,13 @@ export async function redirectToLogin(config = null, force = false) {
|
||||
if (getAuthCategory(config) == 'token') {
|
||||
if (!force) {
|
||||
const page = window['dbgate_page'];
|
||||
if (page == 'login' || page == 'admin-login' || page == 'not-logged') {
|
||||
if (
|
||||
page == 'login' ||
|
||||
page == 'admin-login' ||
|
||||
page == 'not-logged' ||
|
||||
page == 'forgot-password' ||
|
||||
page == 'reset-password'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
testEnabled: () => getVisibleCommandPalette() != 'database',
|
||||
});
|
||||
|
||||
function extractDbItems(db, dbConnectionInfo, connectionList, $extensions) {
|
||||
function extractDbItems(db, dbConnectionInfo, connectionList, $extensions, currentThemeType) {
|
||||
const objectList = _.flatten(
|
||||
['tables', 'collections', 'views', 'matviews', 'procedures', 'functions'].map(objectTypeField =>
|
||||
_.sortBy(
|
||||
@@ -44,7 +44,8 @@
|
||||
const databases = getLocalStorage(`database_list_${conid}`) || [];
|
||||
|
||||
const driver = findEngineDriver(connection, $extensions);
|
||||
const connectionIcon = driver?.icon || 'img database';
|
||||
const driverIcon = getDriverIcon(driver, currentThemeType);
|
||||
const connectionIcon = driverIcon || 'img database';
|
||||
|
||||
for (const db of databases) {
|
||||
databaseList.push({
|
||||
@@ -87,6 +88,8 @@
|
||||
import registerCommand from './registerCommand';
|
||||
import { formatKeyText, switchCurrentDatabase } from '../utility/common';
|
||||
import { _tval, __t, _t } from '../translations';
|
||||
import { getDriverIcon } from '../utility/driverIcons';
|
||||
import { currentThemeType } from '../plugins/themes';
|
||||
|
||||
let domInput;
|
||||
let filter = '';
|
||||
@@ -117,7 +120,7 @@
|
||||
.filter(
|
||||
filter,
|
||||
($visibleCommandPalette == 'database'
|
||||
? extractDbItems($databaseInfo, { conid, database }, $connectionList, $extensions)
|
||||
? extractDbItems($databaseInfo, { conid, database }, $connectionList, $extensions, $currentThemeType)
|
||||
: parentCommand
|
||||
? parentCommand.getSubCommands()
|
||||
: sortedComands
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
export let setGrouping;
|
||||
export let seachInColumns = '';
|
||||
export let onReload = undefined;
|
||||
export let driver = null;
|
||||
|
||||
const openReferencedTable = () => {
|
||||
openDatabaseObjectDetail('TableDataTab', null, {
|
||||
@@ -74,7 +75,8 @@
|
||||
column.foreignKey && [{ divider: true }, { onClick: openReferencedTable, text: column.foreignKey.refTableName }],
|
||||
|
||||
isProApp() &&
|
||||
setGrouping && [
|
||||
setGrouping &&
|
||||
!driver?.dialect?.disableGroupingForDataType?.(column.dataType) && [
|
||||
{ divider: true },
|
||||
{ onClick: () => setGrouping('GROUP'), text: 'Group by' },
|
||||
{ onClick: () => setGrouping('MAX'), text: 'MAX' },
|
||||
@@ -172,12 +174,6 @@
|
||||
align-self: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
/* .resizer {
|
||||
background-color: var(--theme-border);
|
||||
width: 2px;
|
||||
cursor: col-resize;
|
||||
z-index: 1;
|
||||
} */
|
||||
.grouping {
|
||||
color: var(--theme-datagrid-cell-foreground-value-green);
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -305,6 +305,6 @@
|
||||
}
|
||||
|
||||
.selectwrap {
|
||||
border-bottom: 1px solid var(--theme-border);
|
||||
border-bottom: var(--theme-table-border);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -298,7 +298,10 @@
|
||||
onClose={() => {
|
||||
cellDataViewVisible = false;
|
||||
}}
|
||||
selection={publishedCells}
|
||||
selection={publishedCells.map(cell => ({
|
||||
...cell,
|
||||
value: cell.value?.$bigint ?? cell.value?.$decimal ?? cell.value,
|
||||
}))}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</HorizontalSplitter>
|
||||
|
||||
@@ -186,11 +186,11 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
td.isFrameSelected {
|
||||
outline: 3px solid var(--theme-bg-selected);
|
||||
outline: 3px solid var(--theme-table-selected-background);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
td.isAutofillSelected {
|
||||
outline: 3px solid var(--theme-bg-selected);
|
||||
outline: 3px solid var(--theme-table-selected-background);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
td.isFocusedColumn {
|
||||
@@ -235,7 +235,7 @@
|
||||
.autoFillMarker {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--theme-bg-selected-point);
|
||||
background: var(--theme-datagrid-selected-point-marker);
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
@@ -268,7 +268,7 @@
|
||||
}
|
||||
|
||||
td.isMissingOverlayField {
|
||||
background: var(--theme-bg-orange);
|
||||
background: var(--theme-datagrid-modified-cell-background);
|
||||
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAEElEQVQImWNgIAX8x4KJBAD+agT8INXz9wAAAABJRU5ErkJggg==');
|
||||
background-repeat: repeat-x;
|
||||
|
||||
@@ -540,6 +540,78 @@
|
||||
|
||||
$: gridColoringMode = $settingsValue?.['dataGrid.coloringMode'];
|
||||
|
||||
let previousFilters = '';
|
||||
let previousMultiColumnFilter = undefined;
|
||||
let selectedRows = [];
|
||||
|
||||
$: if (display?.config) {
|
||||
const currentFilters = JSON.stringify(display.config.filters);
|
||||
const currentMultiColumnFilter = display.config.multiColumnFilter;
|
||||
if (
|
||||
previousFilters !== '' &&
|
||||
(previousFilters !== currentFilters || previousMultiColumnFilter !== currentMultiColumnFilter)
|
||||
) {
|
||||
const pkColumns = display?.baseTable?.primaryKey?.columns?.map(col => col.columnName) || [];
|
||||
const usePK = pkColumns.length > 0;
|
||||
|
||||
selectedRows = selectedCells
|
||||
.map(cell => {
|
||||
const rowIndex = _.isNumber(cell?.[0]) ? cell[0] : null;
|
||||
const rowData = _.isNumber(rowIndex) && grider ? grider.getRowData(rowIndex) : null;
|
||||
if (!rowData) return null;
|
||||
|
||||
return {
|
||||
rowData: usePK ? _.pick(rowData, pkColumns) : rowData,
|
||||
columnIndex: cell?.[1],
|
||||
usePK,
|
||||
};
|
||||
})
|
||||
.filter(item => item !== null);
|
||||
}
|
||||
|
||||
previousFilters = currentFilters;
|
||||
previousMultiColumnFilter = currentMultiColumnFilter;
|
||||
}
|
||||
|
||||
$: if (selectedRows.length > 0 && loadedTime && grider && grider.rowCount > 0) {
|
||||
const foundCells = [];
|
||||
|
||||
for (const selectedItem of selectedRows) {
|
||||
for (let i = 0; i < grider.rowCount; i++) {
|
||||
const rowData = grider.getRowData(i);
|
||||
if (!rowData) continue;
|
||||
|
||||
const dataToCompare = selectedItem.usePK ? _.pick(rowData, _.keys(selectedItem.rowData)) : rowData;
|
||||
|
||||
if (_.isEqual(dataToCompare, selectedItem.rowData)) {
|
||||
foundCells.push([i, selectedItem.columnIndex || 0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundCells.length === selectedRows.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundCells.length > 0) {
|
||||
currentCell = foundCells[0];
|
||||
selectedCells = foundCells;
|
||||
} else {
|
||||
currentCell = topLeftCell;
|
||||
selectedCells = [topLeftCell];
|
||||
}
|
||||
|
||||
selectedRows = [];
|
||||
}
|
||||
|
||||
$: if (grider && grider.rowCount === 0 && isLoadedAll) {
|
||||
if (currentCell && _.isNumber(currentCell[0])) {
|
||||
currentCell = nullCell;
|
||||
selectedCells = [];
|
||||
}
|
||||
}
|
||||
|
||||
export function refresh() {
|
||||
if (onCustomGridRefresh) onCustomGridRefresh();
|
||||
else display.reload();
|
||||
@@ -599,8 +671,8 @@
|
||||
export function addNewColumn() {
|
||||
showModal(InputTextModal, {
|
||||
value: '',
|
||||
label: 'Column name',
|
||||
header: 'Add new column',
|
||||
label: _t('datagrid.columnName', { defaultMessage: 'Column name' }),
|
||||
header: _t('datagrid.addNewColumn', { defaultMessage: 'Add new column' }),
|
||||
onConfirm: name => {
|
||||
display.addDynamicColumn(name);
|
||||
tick().then(() => {
|
||||
@@ -744,19 +816,23 @@
|
||||
export function openSelectionInMap() {
|
||||
const selection = getCellsPublished(selectedCells);
|
||||
if (!selectionCouldBeShownOnMap(selection)) {
|
||||
showModal(ErrorMessageModal, { message: 'There is nothing to be shown on map' });
|
||||
showModal(ErrorMessageModal, {
|
||||
message: _t('datagrid.mapError.noSelection', { defaultMessage: 'There is nothing to be shown on map' }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const geoJson = createGeoJsonFromSelection(selection);
|
||||
if (!geoJson) {
|
||||
showModal(ErrorMessageModal, { message: 'There is nothing to be shown on map' });
|
||||
showModal(ErrorMessageModal, {
|
||||
message: _t('datagrid.mapError.noGeoJson', { defaultMessage: 'There is nothing to be shown on map' }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Map',
|
||||
title: _t('tabs.map', { defaultMessage: 'Map' }),
|
||||
icon: 'img map',
|
||||
tabComponent: 'MapTab',
|
||||
},
|
||||
@@ -806,7 +882,7 @@
|
||||
const electron = getElectron();
|
||||
const files = await electron.showOpenDialog({
|
||||
properties: ['showHiddenFiles', 'openFile'],
|
||||
filters: [{ name: 'All Files', extensions: ['*'] }],
|
||||
filters: [{ name: _t('common.files.allFiles', { defaultMessage: 'All Files' }), extensions: ['*'] }],
|
||||
});
|
||||
const file = files && files[0];
|
||||
if (file) {
|
||||
@@ -1955,7 +2031,7 @@
|
||||
isProApp() &&
|
||||
hasPermission('dbops/charts') &&
|
||||
onOpenChart && {
|
||||
text: 'Open chart',
|
||||
text: _t('datagrid.openChart', { defaultMessage: 'Open chart' }),
|
||||
onClick: () => onOpenChart(),
|
||||
},
|
||||
{ command: 'dataGrid.generateSqlFromData' },
|
||||
@@ -1991,17 +2067,31 @@
|
||||
|
||||
{#if !display || (!isDynamicStructure && (!columns || columns.length == 0))}
|
||||
{#if $databaseStatus?.name == 'pending' || $databaseStatus?.name == 'checkStructure' || $databaseStatus?.name == 'loadStructure'}
|
||||
<LoadingInfo wrapper message="Waiting for structure" />
|
||||
<LoadingInfo wrapper message={_t('datagrid.structure.waiting', { defaultMessage: 'Waiting for structure' })} />
|
||||
{:else}
|
||||
<ErrorInfo alignTop message="No structure was loaded, probably table doesn't exist in current database" />
|
||||
<ErrorInfo
|
||||
alignTop
|
||||
message={_t('datagrid.structure.notLoaded', {
|
||||
defaultMessage: "No structure was loaded, probably table doesn't exist in current database",
|
||||
})}
|
||||
/>
|
||||
{/if}
|
||||
{:else if errorMessage}
|
||||
<div>
|
||||
<ErrorInfo message={errorMessage} alignTop />
|
||||
<FormStyledButton value="Reset filter" on:click={() => display.clearFilters()} />
|
||||
<FormStyledButton value="Reset view" on:click={() => display.resetConfig()} />
|
||||
<FormStyledButton
|
||||
value={_t('datagrid.resetFilter', { defaultMessage: 'Reset filter' })}
|
||||
on:click={() => display.clearFilters()}
|
||||
/>
|
||||
<FormStyledButton
|
||||
value={_t('datagrid.resetView', { defaultMessage: 'Reset view' })}
|
||||
on:click={() => display.resetConfig()}
|
||||
/>
|
||||
{#if onOpenQueryOnError ?? onOpenQuery}
|
||||
<FormStyledButton value="Open Query" on:click={() => (onOpenQueryOnError ?? onOpenQuery)()} />
|
||||
<FormStyledButton
|
||||
value={_t('datagrid.openQuery', { defaultMessage: 'Open Query' })}
|
||||
on:click={() => (onOpenQueryOnError ?? onOpenQuery)()}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if isDynamicStructure && isLoadedAll && grider?.rowCount == 0}
|
||||
@@ -2009,17 +2099,29 @@
|
||||
<ErrorInfo
|
||||
alignTop
|
||||
message={grider.editable
|
||||
? 'No rows loaded, check filter or add new documents. You could copy documents from other collections/tables with Copy advanved/Copy as JSON command.'
|
||||
: 'No rows loaded'}
|
||||
? _t('datagrid.noRows.withEditable', {
|
||||
defaultMessage:
|
||||
'No rows loaded, check filter or add new documents. You could copy documents from other collections/tables with Copy advanved/Copy as JSON command.',
|
||||
})
|
||||
: _t('datagrid.noRows', { defaultMessage: 'No rows loaded' })}
|
||||
/>
|
||||
{#if display.filterCount > 0}
|
||||
<FormStyledButton value="Reset filter" on:click={() => display.clearFilters()} />
|
||||
<FormStyledButton
|
||||
value={_t('datagrid.resetFilter', { defaultMessage: 'Reset filter' })}
|
||||
on:click={() => display.clearFilters()}
|
||||
/>
|
||||
{/if}
|
||||
{#if grider.editable}
|
||||
<FormStyledButton value="Add document" on:click={addJsonDocument} />
|
||||
<FormStyledButton
|
||||
value={_t('datagrid.addDocument', { defaultMessage: 'Add document' })}
|
||||
on:click={addJsonDocument}
|
||||
/>
|
||||
{/if}
|
||||
{#if onOpenQuery}
|
||||
<FormStyledButton value="Open Query" on:click={() => onOpenQuery()} />
|
||||
<FormStyledButton
|
||||
value={_t('datagrid.openQuery', { defaultMessage: 'Open Query' })}
|
||||
on:click={() => onOpenQuery()}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if grider.errors && grider.errors.length > 0}
|
||||
@@ -2109,6 +2211,7 @@
|
||||
{allowDefineVirtualReferences}
|
||||
seachInColumns={display.config?.searchInColumns}
|
||||
onReload={refresh}
|
||||
driver={display?.driver}
|
||||
/>
|
||||
</td>
|
||||
{/each}
|
||||
@@ -2205,16 +2308,22 @@
|
||||
{#if !isDynamicStructure && isLoadedAll && grider?.rowCount == 0}
|
||||
<div class="no-rows-info ml-2">
|
||||
<div class="mb-3">
|
||||
<ErrorInfo alignTop message="No rows loaded" icon="img info" />
|
||||
<ErrorInfo alignTop message={_t('datagrid.noRows', { defaultMessage: 'No rows loaded' })} icon="img info" />
|
||||
</div>
|
||||
{#if display.filterCount > 0}
|
||||
<FormStyledButton value="Reset filter" on:click={() => display.clearFilters()} />
|
||||
<FormStyledButton
|
||||
value={_t('datagrid.resetFilter', { defaultMessage: 'Reset filter' })}
|
||||
on:click={() => display.clearFilters()}
|
||||
/>
|
||||
{/if}
|
||||
{#if grider.editable}
|
||||
<FormStyledButton value="Add row" on:click={insertNewRow} />
|
||||
<FormStyledButton value={_t('datagrid.addRow', { defaultMessage: 'Add row' })} on:click={insertNewRow} />
|
||||
{/if}
|
||||
{#if onOpenQuery}
|
||||
<FormStyledButton value="Open Query" on:click={() => onOpenQuery()} />
|
||||
<FormStyledButton
|
||||
value={_t('datagrid.openQuery', { defaultMessage: 'Open Query' })}
|
||||
on:click={() => onOpenQuery()}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -2264,7 +2373,7 @@
|
||||
</div>
|
||||
{:else if allRowCount != null && multipleGridsOnTab}
|
||||
<div class="row-count-label">
|
||||
Rows: {allRowCount.toLocaleString()}
|
||||
{_t('datagrid.rows', { defaultMessage: 'Rows' })}: {allRowCount.toLocaleString()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -2273,7 +2382,7 @@
|
||||
{/if}
|
||||
|
||||
{#if !tabControlHiddenTab && !multipleGridsOnTab && allRowCount != null}
|
||||
<StatusBarTabItem text={`Rows: ${allRowCount.toLocaleString()}`} />
|
||||
<StatusBarTabItem text={`${_t('datagrid.rows', { defaultMessage: 'Rows' })}: ${allRowCount.toLocaleString()}`} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -112,10 +112,10 @@
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--theme-bg-alt);
|
||||
background-color: var(--theme-toolstrip-button-background);
|
||||
max-height: 150px;
|
||||
overflow: auto;
|
||||
box-shadow: 0 1px 10px 1px var(--theme-bg-inv-3);
|
||||
box-shadow: var(--theme-input-inplace-select-shadow);
|
||||
}
|
||||
|
||||
.value {
|
||||
@@ -124,7 +124,7 @@
|
||||
left: 0;
|
||||
z-index: 20;
|
||||
min-height: 17px;
|
||||
background-color: var(--theme-bg-0);
|
||||
background-color: var(--theme-toolstrip-button-background);
|
||||
height: 100%;
|
||||
width: calc(100% - 4px);
|
||||
padding: 0 2px;
|
||||
@@ -146,12 +146,12 @@
|
||||
|
||||
label {
|
||||
padding: 2px 3px;
|
||||
border-bottom: 1px solid var(--theme-border);
|
||||
border-bottom: var(--theme-toolstrip-button-border);
|
||||
display: block;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
label:hover {
|
||||
background-color: var(--theme-bg-hover);
|
||||
background-color: var(--theme-toolstrip-button-background-hover);
|
||||
}
|
||||
</style>
|
||||
|
||||
+107
-74
@@ -5,6 +5,9 @@
|
||||
import { apiCall } from '../utility/api';
|
||||
import createRef from '../utility/createRef';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import { supportedRedisKeyTypes } from 'dbgate-tools';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
import _ from 'lodash';
|
||||
|
||||
export let conid;
|
||||
export let database;
|
||||
@@ -45,7 +48,7 @@
|
||||
if (keyInfo.keyColumn && newRows.find(x => x[keyInfo.keyColumn] == row[keyInfo.keyColumn])) {
|
||||
continue;
|
||||
}
|
||||
newRows.push({ rowNumber: newRows.length + 1, ...row });
|
||||
newRows.push({ rowNumber: newRows.length, editorRowId: uuidv1(), ...row });
|
||||
}
|
||||
|
||||
rows = newRows;
|
||||
@@ -63,10 +66,10 @@
|
||||
}
|
||||
|
||||
$: {
|
||||
if (onChangeSelected && rows[selectedIndex]) {
|
||||
if (onChangeSelected && displayRows[selectedIndex]) {
|
||||
if (oldIndexRef.get() != selectedIndex) {
|
||||
oldIndexRef.set(selectedIndex);
|
||||
onChangeSelected(rows[selectedIndex]);
|
||||
onChangeSelected(displayRows[selectedIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,22 +82,20 @@
|
||||
});
|
||||
|
||||
$: displayRows = createDisplayRows(rows, changeSetRedis, keyInfo, modifyRow);
|
||||
|
||||
|
||||
function createDisplayRows(sourceRows, changeSet, keyInfoParam, modifyRowFunc) {
|
||||
let result = modifyRowFunc ? sourceRows.map(row => modifyRowFunc(row)) : sourceRows;
|
||||
|
||||
|
||||
// Mark deleted rows and add inserted rows from changeset
|
||||
if (changeSet && keyInfoParam) {
|
||||
const existingChange = changeSet.changes.find(
|
||||
c => c.key === keyInfoParam.key && c.type === keyInfoParam.type
|
||||
);
|
||||
|
||||
const existingChange = changeSet.changes.find(c => c.key === keyInfoParam.key && c.type === keyInfoParam.type);
|
||||
|
||||
if (existingChange) {
|
||||
// Mark existing rows as deleted if they're in the deletes array
|
||||
if (existingChange.deletes) {
|
||||
result = result.map(row => {
|
||||
let isDeleted = false;
|
||||
|
||||
|
||||
if (keyInfoParam.type === 'hash') {
|
||||
isDeleted = existingChange.deletes.includes(row.key);
|
||||
} else if (keyInfoParam.type === 'set') {
|
||||
@@ -104,41 +105,57 @@
|
||||
} else if (keyInfoParam.type === 'stream') {
|
||||
isDeleted = existingChange.deletes.includes(row.id);
|
||||
}
|
||||
|
||||
|
||||
return isDeleted ? { ...row, __isDeleted: true } : row;
|
||||
});
|
||||
}
|
||||
|
||||
// Add inserted rows from changeset
|
||||
if (existingChange.inserts) {
|
||||
const insertedRows = existingChange.inserts.map((insert, index) => {
|
||||
let row = {
|
||||
rowNumber: result.length + index + 1,
|
||||
__isAdded: true
|
||||
};
|
||||
|
||||
|
||||
// Mark rows updated in the changeset
|
||||
if (existingChange.updates) {
|
||||
result = result.map(row => {
|
||||
let isChanged = false;
|
||||
|
||||
if (keyInfoParam.type === 'hash') {
|
||||
row.key = insert.key || '';
|
||||
row.value = insert.value || '';
|
||||
if (insert.ttl !== undefined) row.TTL = insert.ttl;
|
||||
} else if (keyInfoParam.type === 'list' || keyInfoParam.type === 'set') {
|
||||
row.value = insert.value || '';
|
||||
const originalKey = row._originalKey || row.key;
|
||||
isChanged = existingChange.updates.some(update => update.originalKey === originalKey);
|
||||
} else if (keyInfoParam.type === 'list') {
|
||||
isChanged = existingChange.updates.some(update => update.index === row.rowNumber);
|
||||
} else if (keyInfoParam.type === 'zset') {
|
||||
row.member = insert.member || '';
|
||||
row.score = insert.score || '';
|
||||
isChanged = existingChange.updates.some(update => update.member === row.member);
|
||||
} else if (keyInfoParam.type === 'stream') {
|
||||
row.id = insert.id || '';
|
||||
row.value = insert.value || '';
|
||||
isChanged = existingChange.updates.some(update => update.id === row.id);
|
||||
}
|
||||
|
||||
return row;
|
||||
|
||||
return isChanged ? { ...row, __isChanged: true } : row;
|
||||
});
|
||||
|
||||
result = [...result, ...insertedRows];
|
||||
}
|
||||
|
||||
// Add inserted rows from changeset
|
||||
if (existingChange.inserts?.length > 0) {
|
||||
if (existingChange.type == 'stream') {
|
||||
result = [
|
||||
...result,
|
||||
{
|
||||
__isAdded: true,
|
||||
rowNumber: result.length,
|
||||
id: existingChange.generatedId || '*',
|
||||
value: JSON.stringify(_.fromPairs(existingChange.inserts.map(i => [i.field, i.value]))),
|
||||
},
|
||||
];
|
||||
} else {
|
||||
result = [
|
||||
...result,
|
||||
...existingChange.inserts.map((insert, index) => ({
|
||||
...insert,
|
||||
rowNumber: result.length + index,
|
||||
__isAdded: true,
|
||||
})),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -149,50 +166,66 @@
|
||||
onRemoveItem(row);
|
||||
}
|
||||
}
|
||||
|
||||
$: keyType = supportedRedisKeyTypes.find(t => t.name === keyInfo?.type);
|
||||
</script>
|
||||
|
||||
<ScrollableTableControl
|
||||
columns={[
|
||||
{
|
||||
fieldName: 'rowNumber',
|
||||
header: 'num',
|
||||
width: '60px',
|
||||
},
|
||||
...keyInfo.keyType.dbKeyFields.map(column => ({
|
||||
fieldName: column.name,
|
||||
header: column.name,
|
||||
})),
|
||||
...(shouldShowRemoveColumn ? [{
|
||||
fieldName: '__remove',
|
||||
header: '',
|
||||
width: '30px',
|
||||
slot: 0
|
||||
}] : []),
|
||||
]}
|
||||
rows={displayRows}
|
||||
onLoadNext={isLoadedAll ? null : loadNextRows}
|
||||
selectable
|
||||
singleLineRow
|
||||
bind:selectedIndex
|
||||
>
|
||||
<div slot="0" let:row>
|
||||
<button
|
||||
class="delete-button"
|
||||
on:click={() => handleRemoveItem(row)}
|
||||
>
|
||||
<FontIcon icon="icon delete" />
|
||||
</button>
|
||||
</div>
|
||||
</ScrollableTableControl>
|
||||
<div class="table-wrapper">
|
||||
<ScrollableTableControl
|
||||
columns={[
|
||||
{
|
||||
fieldName: 'rowNumber',
|
||||
header: 'num',
|
||||
width: '60px',
|
||||
slot: 1,
|
||||
},
|
||||
...(keyType.dbKeyFieldsForGrid ?? keyType.dbKeyFields).map(column => ({
|
||||
fieldName: column.name,
|
||||
header: column.name,
|
||||
})),
|
||||
...(shouldShowRemoveColumn
|
||||
? [
|
||||
{
|
||||
fieldName: '__remove',
|
||||
header: '',
|
||||
width: '30px',
|
||||
slot: 0,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
rows={displayRows}
|
||||
onLoadNext={isLoadedAll ? null : loadNextRows}
|
||||
selectable
|
||||
singleLineRow
|
||||
bind:selectedIndex
|
||||
disableFocusOutline
|
||||
>
|
||||
<div slot="0" let:row>
|
||||
<button class="delete-button" on:click={() => handleRemoveItem(row)}>
|
||||
<FontIcon icon="icon delete" />
|
||||
</button>
|
||||
</div>
|
||||
<div slot="1" let:row>
|
||||
{row['rowNumber'] + 1}
|
||||
</div>
|
||||
</ScrollableTableControl>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(tr.isDeleted td) {
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAEElEQVQImWNgIAX8x4KJBAD+agT8INXz9wAAAABJRU5ErkJggg==') !important;
|
||||
background-repeat: repeat-x !important;
|
||||
background-position: 50% 50% !important;
|
||||
opacity: 0.7;
|
||||
.table-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
padding: 8px 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.table-wrapper :global(.wrapper) {
|
||||
position: relative !important;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
background: none;
|
||||
border: none;
|
||||
@@ -200,7 +233,7 @@
|
||||
padding: 4px;
|
||||
color: var(--theme-generic-font-grayed);
|
||||
}
|
||||
|
||||
|
||||
.delete-button:hover {
|
||||
color: var(--theme-generic-font-hover);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import DbKeyValueDetail from './DbKeyValueDetail.svelte';
|
||||
|
||||
export let dbKeyFields;
|
||||
export let item;
|
||||
export let onChangeItem = null;
|
||||
export let keyColumn = null;
|
||||
|
||||
$: console.log('DbKeyItemEdit', { item, dbKeyFields, keyColumn, onChangeItem: !!onChangeItem });
|
||||
|
||||
function getValueAsString(value) {
|
||||
if (value === null || value === undefined) return undefined;
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value === 'number') return String(value);
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="props">
|
||||
{#each dbKeyFields as column}
|
||||
<div class="field-wrapper">
|
||||
<DbKeyValueDetail
|
||||
value={getValueAsString(item?.[column.name])}
|
||||
columnTitle={_.startCase(column.name)}
|
||||
onChangeValue={onChangeItem && column.name !== keyColumn
|
||||
? value => {
|
||||
onChangeItem?.({
|
||||
...item,
|
||||
[column.name]: value,
|
||||
});
|
||||
}
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.props {
|
||||
flex: 1;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.field-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
max-height: 100px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,190 +0,0 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import TextField from '../forms/TextField.svelte';
|
||||
import FormFieldTemplateLarge from '../forms/FormFieldTemplateLarge.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
|
||||
export let dbKeyFields;
|
||||
export let item;
|
||||
export let onChangeItem = null;
|
||||
export let keyColumn = null;
|
||||
|
||||
let records = [{ key: '', value: '', ttl: '' }];
|
||||
let lastItem = null;
|
||||
|
||||
$: if (item !== lastItem) {
|
||||
if (item?.records && Array.isArray(item.records)) {
|
||||
records = [...item.records];
|
||||
} else if (!item) {
|
||||
records = [{ key: '', value: '', ttl: '' }];
|
||||
}
|
||||
lastItem = item;
|
||||
}
|
||||
|
||||
$: console.log('DbKeyItemEdit', { item, dbKeyFields, keyColumn, onChangeItem: !!onChangeItem });
|
||||
|
||||
function getValueAsString(value) {
|
||||
if (value === null || value === undefined) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value === 'number') return String(value);
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function handleFieldChange(index, fieldName, value) {
|
||||
records = records.map((record, idx) => (idx === index ? { ...record, [fieldName]: value } : record));
|
||||
|
||||
if (onChangeItem && fieldName !== keyColumn) {
|
||||
onChangeItem?.({
|
||||
...item,
|
||||
records: records,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addRecord() {
|
||||
records = [...records, { key: '', value: '', ttl: '' }];
|
||||
if (onChangeItem) {
|
||||
onChangeItem({
|
||||
...item,
|
||||
records: records,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
{#each records as record, index}
|
||||
<div class="props flex">
|
||||
<div class="field-wrapper col-3">
|
||||
<FormFieldTemplateLarge label="Key" type="text" noMargin>
|
||||
<TextField
|
||||
value={record.key}
|
||||
on:change={e => {
|
||||
if (e.target['value'] != record.key) {
|
||||
handleFieldChange(index, 'key', e.target['value']);
|
||||
}
|
||||
}}
|
||||
disabled={keyColumn === 'key'}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
</div>
|
||||
<div class="field-wrapper col-7">
|
||||
<FormFieldTemplateLarge label="Value" type="text" noMargin>
|
||||
<TextField
|
||||
value={record.value}
|
||||
on:change={e => {
|
||||
if (e.target['value'] != record.value) {
|
||||
handleFieldChange(index, 'value', e.target['value']);
|
||||
}
|
||||
}}
|
||||
disabled={keyColumn === 'value'}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
</div>
|
||||
<div class="field-wrapper col-2">
|
||||
<FormFieldTemplateLarge label="TTL" type="text" noMargin>
|
||||
<TextField
|
||||
value={record.ttl}
|
||||
on:change={e => {
|
||||
if (e.target['value'] != record.ttl) {
|
||||
handleFieldChange(index, 'ttl', e.target['value']);
|
||||
}
|
||||
}}
|
||||
disabled={keyColumn === 'ttl'}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
</div>
|
||||
<div class="delete-wrapper col-1">
|
||||
<button
|
||||
class="delete-button"
|
||||
on:click={() => {
|
||||
records = records.filter((_, idx) => idx !== index);
|
||||
if (onChangeItem) {
|
||||
onChangeItem({
|
||||
...item,
|
||||
records: records,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontIcon icon="icon delete" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="add-button-wrapper">
|
||||
<button class="add-button" on:click={addRecord}>
|
||||
<FontIcon icon="icon add" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.props {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.delete-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--theme-generic-font-grayed);
|
||||
transition: color 0.2s;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.delete-button:hover {
|
||||
color: var(--theme-dbkey-icon-hover);
|
||||
}
|
||||
|
||||
.add-button-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--theme-generic-font-grayed);
|
||||
transition: color 0.2s;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.add-button:hover {
|
||||
color: var(--theme-dbkey-icon-hover);
|
||||
}
|
||||
</style>
|
||||
@@ -1,151 +0,0 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import TextField from '../forms/TextField.svelte';
|
||||
import FormFieldTemplateLarge from '../forms/FormFieldTemplateLarge.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
|
||||
export let dbKeyFields;
|
||||
export let item;
|
||||
export let onChangeItem = null;
|
||||
export let keyColumn = null;
|
||||
|
||||
let records = [{ value: '' }];
|
||||
let lastItem = null;
|
||||
|
||||
$: if (item !== lastItem) {
|
||||
if (item?.records && Array.isArray(item.records)) {
|
||||
records = [...item.records];
|
||||
} else if (!item) {
|
||||
records = [{ value: '' }];
|
||||
}
|
||||
lastItem = item;
|
||||
}
|
||||
|
||||
$: console.log('DbKeyValueListEdit', { item, dbKeyFields, keyColumn, onChangeItem: !!onChangeItem });
|
||||
|
||||
function handleFieldChange(index, fieldName, value) {
|
||||
records = records.map((record, idx) =>
|
||||
idx === index ? { ...record, [fieldName]: value } : record
|
||||
);
|
||||
|
||||
if (onChangeItem && fieldName !== keyColumn) {
|
||||
onChangeItem?.({
|
||||
...item,
|
||||
records: records,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addRecord() {
|
||||
records = [...records, { value: '' }];
|
||||
if (onChangeItem) {
|
||||
onChangeItem({
|
||||
...item,
|
||||
records: records,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
{#each records as record, index}
|
||||
<div class="props flex">
|
||||
<div class="field-wrapper col-11">
|
||||
<FormFieldTemplateLarge label="Value" type="text" noMargin>
|
||||
<TextField
|
||||
value={record.value}
|
||||
on:change={e => handleFieldChange(index, 'value', e.target.value)}
|
||||
disabled={keyColumn === 'value'}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
</div>
|
||||
<div class="delete-wrapper col-1">
|
||||
<button class="delete-button" on:click={() => {
|
||||
records = records.filter((_, idx) => idx !== index);
|
||||
if (onChangeItem) {
|
||||
onChangeItem({
|
||||
...item,
|
||||
records: records,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<FontIcon icon="icon delete" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="add-button-wrapper">
|
||||
<button class="add-button" on:click={addRecord}>
|
||||
<FontIcon icon="icon add" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.props {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.field-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.delete-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--theme-generic-font-grayed);
|
||||
transition: color 0.2s;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.delete-button:hover {
|
||||
color: var(--theme-dbkey-icon-hover);
|
||||
}
|
||||
|
||||
.add-button-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--theme-generic-font-grayed);
|
||||
transition: color 0.2s;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.add-button:hover {
|
||||
color: var(--theme-dbkey-icon-hover);
|
||||
}
|
||||
</style>
|
||||
@@ -1,151 +0,0 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import TextField from '../forms/TextField.svelte';
|
||||
import FormFieldTemplateLarge from '../forms/FormFieldTemplateLarge.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
|
||||
export let dbKeyFields;
|
||||
export let item;
|
||||
export let onChangeItem = null;
|
||||
export let keyColumn = null;
|
||||
|
||||
let records = [{ value: '' }];
|
||||
let lastItem = null;
|
||||
|
||||
$: if (item !== lastItem) {
|
||||
if (item?.records && Array.isArray(item.records)) {
|
||||
records = [...item.records];
|
||||
} else if (!item) {
|
||||
records = [{ value: '' }];
|
||||
}
|
||||
lastItem = item;
|
||||
}
|
||||
|
||||
$: console.log('DbKeyValueSetEdit', { item, dbKeyFields, keyColumn, onChangeItem: !!onChangeItem });
|
||||
|
||||
function handleFieldChange(index, fieldName, value) {
|
||||
records = records.map((record, idx) =>
|
||||
idx === index ? { ...record, [fieldName]: value } : record
|
||||
);
|
||||
|
||||
if (onChangeItem && fieldName !== keyColumn) {
|
||||
onChangeItem?.({
|
||||
...item,
|
||||
records: records,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addRecord() {
|
||||
records = [...records, { value: '' }];
|
||||
if (onChangeItem) {
|
||||
onChangeItem({
|
||||
...item,
|
||||
records: records,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
{#each records as record, index}
|
||||
<div class="props flex">
|
||||
<div class="field-wrapper col-11">
|
||||
<FormFieldTemplateLarge label="Value" type="text" noMargin>
|
||||
<TextField
|
||||
value={record.value}
|
||||
on:change={e => handleFieldChange(index, 'value', e.target.value)}
|
||||
disabled={keyColumn === 'value'}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
</div>
|
||||
<div class="delete-wrapper col-1">
|
||||
<button class="delete-button" on:click={() => {
|
||||
records = records.filter((_, idx) => idx !== index);
|
||||
if (onChangeItem) {
|
||||
onChangeItem({
|
||||
...item,
|
||||
records: records,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<FontIcon icon="icon delete" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="add-button-wrapper">
|
||||
<button class="add-button" on:click={addRecord}>
|
||||
<FontIcon icon="icon add" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.props {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.field-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.delete-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--theme-generic-font-grayed);
|
||||
transition: color 0.2s;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.delete-button:hover {
|
||||
color: var(--theme-dbkey-icon-hover);
|
||||
}
|
||||
|
||||
.add-button-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--theme-generic-font-grayed);
|
||||
transition: color 0.2s;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.add-button:hover {
|
||||
color: var(--theme-dbkey-icon-hover);
|
||||
}
|
||||
</style>
|
||||
@@ -1,166 +0,0 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import TextField from '../forms/TextField.svelte';
|
||||
import FormFieldTemplateLarge from '../forms/FormFieldTemplateLarge.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
|
||||
export let dbKeyFields;
|
||||
export let item;
|
||||
export let onChangeItem = null;
|
||||
export let keyColumn = null;
|
||||
|
||||
let records = [{ id: '', value: '' }];
|
||||
let lastItem = null;
|
||||
|
||||
$: if (item !== lastItem) {
|
||||
if (item?.records && Array.isArray(item.records)) {
|
||||
records = [...item.records];
|
||||
} else if (!item) {
|
||||
records = [{ id: '', value: '' }];
|
||||
}
|
||||
lastItem = item;
|
||||
}
|
||||
|
||||
$: console.log('DbKeyValueStreamEdit', { item, dbKeyFields, keyColumn, onChangeItem: !!onChangeItem });
|
||||
|
||||
function getValueAsString(value) {
|
||||
if (value === null || value === undefined) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value === 'number') return String(value);
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function handleFieldChange(index, fieldName, value) {
|
||||
records = records.map((record, idx) => (idx === index ? { ...record, [fieldName]: value } : record));
|
||||
|
||||
if (onChangeItem && fieldName !== keyColumn) {
|
||||
onChangeItem?.({
|
||||
...item,
|
||||
records: records,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addRecord() {
|
||||
records = [...records, { id: '', value: '' }];
|
||||
if (onChangeItem) {
|
||||
onChangeItem({
|
||||
...item,
|
||||
records: records,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
{#each records as record, index}
|
||||
<div class="props flex">
|
||||
<div class="field-wrapper col-3">
|
||||
<FormFieldTemplateLarge label="ID" type="text" noMargin>
|
||||
<TextField
|
||||
value={record.id}
|
||||
on:change={e => handleFieldChange(index, 'id', e.target.value)}
|
||||
disabled={keyColumn === 'id'}
|
||||
placeholder="* for auto"
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
</div>
|
||||
<div class="field-wrapper col-9">
|
||||
<FormFieldTemplateLarge label="Value" type="text" noMargin>
|
||||
<TextField
|
||||
value={record.value}
|
||||
on:change={e => handleFieldChange(index, 'value', e.target.value)}
|
||||
disabled={keyColumn === 'value'}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
</div>
|
||||
<div class="delete-wrapper col-1">
|
||||
<button class="delete-button" on:click={() => {
|
||||
records = records.filter((_, idx) => idx !== index);
|
||||
if (onChangeItem) {
|
||||
onChangeItem({
|
||||
...item,
|
||||
records: records,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<FontIcon icon="icon delete" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="add-button-wrapper">
|
||||
<button class="add-button" on:click={addRecord}>
|
||||
<FontIcon icon="icon add" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.props {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.field-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.delete-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--theme-generic-font-grayed);
|
||||
transition: color 0.2s;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.delete-button:hover {
|
||||
color: var(--theme-dbkey-icon-hover);
|
||||
}
|
||||
|
||||
.add-button-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--theme-generic-font-grayed);
|
||||
transition: color 0.2s;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.add-button:hover {
|
||||
color: var(--theme-dbkey-icon-hover);
|
||||
}
|
||||
</style>
|
||||
@@ -1,166 +0,0 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import TextField from '../forms/TextField.svelte';
|
||||
import FormFieldTemplateLarge from '../forms/FormFieldTemplateLarge.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
|
||||
export let dbKeyFields;
|
||||
export let item;
|
||||
export let onChangeItem = null;
|
||||
export let keyColumn = null;
|
||||
|
||||
let records = [{ member: '', score: '' }];
|
||||
let lastItem = null;
|
||||
|
||||
$: if (item !== lastItem) {
|
||||
if (item?.records && Array.isArray(item.records)) {
|
||||
records = [...item.records];
|
||||
} else if (!item) {
|
||||
records = [{ member: '', score: '' }];
|
||||
}
|
||||
lastItem = item;
|
||||
}
|
||||
|
||||
$: console.log('DbKeyValueZSetEdit', { item, dbKeyFields, keyColumn, onChangeItem: !!onChangeItem });
|
||||
|
||||
function getValueAsString(value) {
|
||||
if (value === null || value === undefined) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value === 'number') return String(value);
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function handleFieldChange(index, fieldName, value) {
|
||||
records = records.map((record, idx) => (idx === index ? { ...record, [fieldName]: value } : record));
|
||||
|
||||
if (onChangeItem && fieldName !== keyColumn) {
|
||||
onChangeItem?.({
|
||||
...item,
|
||||
records: records,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addRecord() {
|
||||
records = [...records, { member: '', score: '' }];
|
||||
if (onChangeItem) {
|
||||
onChangeItem({
|
||||
...item,
|
||||
records: records,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
{#each records as record, index}
|
||||
<div class="props flex">
|
||||
<div class="field-wrapper col-8">
|
||||
<FormFieldTemplateLarge label="Member" type="text" noMargin>
|
||||
<TextField
|
||||
value={record.member}
|
||||
on:change={e => handleFieldChange(index, 'member', e.target.value)}
|
||||
disabled={keyColumn === 'member'}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
</div>
|
||||
<div class="field-wrapper col-4">
|
||||
<FormFieldTemplateLarge label="Score" type="text" noMargin>
|
||||
<TextField
|
||||
value={record.score}
|
||||
on:change={e => handleFieldChange(index, 'score', e.target.value)}
|
||||
disabled={keyColumn === 'score'}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
</div>
|
||||
<div class="delete-wrapper col-1">
|
||||
<button class="delete-button" on:click={() => {
|
||||
records = records.filter((_, idx) => idx !== index);
|
||||
if (onChangeItem) {
|
||||
onChangeItem({
|
||||
...item,
|
||||
records: records,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<FontIcon icon="icon delete" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="add-button-wrapper">
|
||||
<button class="add-button" on:click={addRecord}>
|
||||
<FontIcon icon="icon add" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.props {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.delete-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--theme-generic-font-grayed);
|
||||
transition: color 0.2s;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.delete-button:hover {
|
||||
color: var(--theme-dbkey-icon-hover);
|
||||
}
|
||||
|
||||
.add-button-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--theme-generic-font-grayed);
|
||||
transition: color 0.2s;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.add-button:hover {
|
||||
color: var(--theme-dbkey-icon-hover);
|
||||
}
|
||||
</style>
|
||||
@@ -453,6 +453,7 @@
|
||||
|
||||
const handleChangeTableColor = table => {
|
||||
showModal(ChooseColorModal, {
|
||||
header: _t('designer.chooseTableColor', { defaultMessage: 'Choose table color' }),
|
||||
onChange: color => {
|
||||
callChange(current => {
|
||||
return {
|
||||
@@ -795,7 +796,7 @@
|
||||
const replaceLinks = text =>
|
||||
text.replace(
|
||||
/\[([^\]]+)\]\(([^)]+)\)/g,
|
||||
'<a href="$2" style="color: var(--theme-font-link)" target="_blank">$1</a>'
|
||||
'<a href="$2" style="color: var(--theme-link-foreground)" target="_blank">$1</a>'
|
||||
);
|
||||
|
||||
if (value?.style?.omitExportWatermark) return null;
|
||||
@@ -848,17 +849,26 @@
|
||||
text: _t('designer.columnProperties', { defaultMessage: 'Column properties' }),
|
||||
submenu: [
|
||||
{
|
||||
text: _t('designer.nullabilityYesNo', { defaultMessage: 'Nullability: {show}', values: { show: value?.style?.showNullability ? 'YES' : 'NO' } }),
|
||||
text: _t('designer.nullabilityYesNo', {
|
||||
defaultMessage: 'Nullability: {show}',
|
||||
values: { show: value?.style?.showNullability ? 'YES' : 'NO' },
|
||||
}),
|
||||
onClick: changeStyleFunc('showNullability', !value?.style?.showNullability),
|
||||
},
|
||||
{
|
||||
text: _t('designer.dataTypeYesNo', { defaultMessage: 'Data type: {show}', values: { show: value?.style?.showDataType ? 'YES' : 'NO' } }),
|
||||
text: _t('designer.dataTypeYesNo', {
|
||||
defaultMessage: 'Data type: {show}',
|
||||
values: { show: value?.style?.showDataType ? 'YES' : 'NO' },
|
||||
}),
|
||||
onClick: changeStyleFunc('showDataType', !value?.style?.showDataType),
|
||||
},
|
||||
],
|
||||
},
|
||||
isProApp() && {
|
||||
text: _t('designer.columns', { defaultMessage: 'Columns - { filterColumns }', values: { filterColumns: _.startCase(value?.style?.filterColumns || 'all') } }),
|
||||
text: _t('designer.columns', {
|
||||
defaultMessage: 'Columns - { filterColumns }',
|
||||
values: { filterColumns: _.startCase(value?.style?.filterColumns || 'all') },
|
||||
}),
|
||||
submenu: [
|
||||
{
|
||||
text: _t('designer.all', { defaultMessage: 'All' }),
|
||||
@@ -883,7 +893,10 @@
|
||||
],
|
||||
},
|
||||
{
|
||||
text: _t('designer.zoom', { defaultMessage: 'Zoom - {zoom}%', values: { zoom: ((value?.style?.zoomKoef || 1) * 100) } }),
|
||||
text: _t('designer.zoom', {
|
||||
defaultMessage: 'Zoom - {zoom}%',
|
||||
values: { zoom: (value?.style?.zoomKoef || 1) * 100 },
|
||||
}),
|
||||
submenu: DIAGRAM_ZOOMS.map(koef => ({
|
||||
text: `${koef * 100} %`,
|
||||
onClick: changeStyleFunc('zoomKoef', koef.toString()),
|
||||
@@ -1012,11 +1025,16 @@
|
||||
use:dragScroll={handleDragScroll}
|
||||
>
|
||||
{#if !(tables?.length > 0)}
|
||||
<div class="empty">{_t('designer.dragDropTables', { defaultMessage: 'Drag & drop tables or views from left panel here' })}</div>
|
||||
<div class="empty">
|
||||
{_t('designer.dragDropTables', { defaultMessage: 'Drag & drop tables or views from left panel here' })}
|
||||
</div>
|
||||
|
||||
{#if allowAddTablesButton}
|
||||
<div class="addAllTables">
|
||||
<FormStyledButton value={_t('designer.addAllTables', { defaultMessage: 'Add all tables' })} on:click={handleAddAllTables} />
|
||||
<FormStyledButton
|
||||
value={_t('designer.addAllTables', { defaultMessage: 'Add all tables' })}
|
||||
on:click={handleAddAllTables}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -1115,7 +1133,10 @@
|
||||
<div class="panel">
|
||||
<DragColumnMemory {settings} {sourceDragColumn$} {targetDragColumn$} />
|
||||
<div class="searchbox">
|
||||
<SearchInput bind:value={columnFilter} placeholder={_t('designer.filterColumns', { defaultMessage: 'Filter columns' })} />
|
||||
<SearchInput
|
||||
bind:value={columnFilter}
|
||||
placeholder={_t('designer.filterColumns', { defaultMessage: 'Filter columns' })}
|
||||
/>
|
||||
<CloseSearchButton bind:filter={columnFilter} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { presetDarkPalettes, presetPalettes } from '@ant-design/colors';
|
||||
import { filterName, stringFilterBehaviour } from 'dbgate-tools';
|
||||
|
||||
import { tick } from 'svelte';
|
||||
@@ -10,7 +9,6 @@
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import { currentThemeDefinition } from '../plugins/themes';
|
||||
import VirtualForeignKeyEditorModal from '../tableeditor/VirtualForeignKeyEditorModal.svelte';
|
||||
import { isCtrlOrCommandKey } from '../utility/common';
|
||||
import contextMenu from '../utility/contextMenu';
|
||||
@@ -18,6 +16,7 @@
|
||||
import ColumnLine from './ColumnLine.svelte';
|
||||
import DomTableRef from './DomTableRef';
|
||||
import { _t } from '../translations';
|
||||
import { getNormalizedUserColorName } from '../utility/userColors';
|
||||
|
||||
export let conid;
|
||||
export let database;
|
||||
@@ -169,12 +168,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getTableColorStyle(themeDef, table, colorIndex = 3) {
|
||||
function getTableColorStyle(table) {
|
||||
if (!table?.tableColor) return null;
|
||||
const palettes = themeDef?.themeType == 'dark' ? presetDarkPalettes : presetPalettes;
|
||||
const palette = palettes[table?.tableColor];
|
||||
if (!palette) return null;
|
||||
return `background: ${palette[colorIndex]}`;
|
||||
return `background: var(--theme-usercolor-background-${getNormalizedUserColorName(table?.tableColor)})`;
|
||||
}
|
||||
|
||||
export function getDomTable() {
|
||||
@@ -215,7 +211,10 @@
|
||||
{ divider: true },
|
||||
settings?.allowTableAlias &&
|
||||
!isMultipleTableSelection && [
|
||||
{ text: _t('designerTable.setTableAlias', { defaultMessage: 'Set table alias' }), onClick: handleSetTableAlias },
|
||||
{
|
||||
text: _t('designerTable.setTableAlias', { defaultMessage: 'Set table alias' }),
|
||||
onClick: handleSetTableAlias,
|
||||
},
|
||||
alias && {
|
||||
text: _t('designerTable.removeTableAlias', { defaultMessage: 'Remove table alias' }),
|
||||
onClick: () =>
|
||||
@@ -226,8 +225,14 @@
|
||||
},
|
||||
],
|
||||
settings?.allowAddAllReferences &&
|
||||
!isMultipleTableSelection && { text: _t('designerTable.addReferences', { defaultMessage: 'Add references' }), onClick: () => onAddAllReferences(table) },
|
||||
settings?.allowChangeColor && { text: _t('designerTable.changeColor', { defaultMessage: 'Change color' }), onClick: () => onChangeTableColor(table) },
|
||||
!isMultipleTableSelection && {
|
||||
text: _t('designerTable.addReferences', { defaultMessage: 'Add references' }),
|
||||
onClick: () => onAddAllReferences(table),
|
||||
},
|
||||
settings?.allowChangeColor && {
|
||||
text: _t('designerTable.changeColor', { defaultMessage: 'Change color' }),
|
||||
onClick: () => onChangeTableColor(table),
|
||||
},
|
||||
settings?.allowDefineVirtualReferences &&
|
||||
!isMultipleTableSelection && {
|
||||
text: _t('designerTable.defineVirtualForeignKey', { defaultMessage: 'Define virtual foreign key' }),
|
||||
@@ -267,7 +272,7 @@
|
||||
class:isCollection={objectTypeField == 'collections'}
|
||||
use:moveDrag={settings?.canSelectColumns ? [handleMoveStart, handleMove, handleMoveEnd] : null}
|
||||
use:contextMenu={settings?.canSelectColumns ? createMenu : '__no_menu'}
|
||||
style={getTableColorStyle($currentThemeDefinition, table)}
|
||||
style={getTableColorStyle(table)}
|
||||
on:click={settings?.onClickTableHeader ? () => settings?.onClickTableHeader(designerId) : null}
|
||||
>
|
||||
<div>
|
||||
@@ -348,9 +353,6 @@
|
||||
background-color: var(--theme-designer-item-background);
|
||||
border: var(--theme-designer-item-border);
|
||||
}
|
||||
/* :global(.dbgate-screen) .isSelectedTable {
|
||||
border: 3px solid var(--theme-border);
|
||||
} */
|
||||
.selection-marker {
|
||||
display: none;
|
||||
position: absolute;
|
||||
|
||||
@@ -221,6 +221,7 @@
|
||||
table {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
background-color: var(--theme-table-header-background);
|
||||
}
|
||||
|
||||
table thead,
|
||||
@@ -274,7 +275,9 @@
|
||||
padding: 5px;
|
||||
}
|
||||
tbody td {
|
||||
border: var(--theme-table-border);
|
||||
border-left: var(--theme-table-border);
|
||||
border-right: var(--theme-table-border);
|
||||
border-bottom: var(--theme-table-border);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
@@ -292,4 +295,10 @@
|
||||
tr.isDeleted {
|
||||
background: var(--theme-table-deleted-background);
|
||||
}
|
||||
|
||||
tr.isDeleted td {
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAEElEQVQImWNgIAX8x4KJBAD+agT8INXz9wAAAABJRU5ErkJggg==') !important;
|
||||
background-repeat: repeat-x !important;
|
||||
background-position: 50% 50% !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { presetPrimaryColors } from '@ant-design/colors';
|
||||
import { presetPalettes, presetDarkPalettes } from '@ant-design/colors';
|
||||
import { currentThemeDefinition } from '../plugins/themes';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { USER_COLOR_NAMES } from '../utility/userColors';
|
||||
|
||||
export let value;
|
||||
export let disabled = false;
|
||||
|
||||
function colorValue(color, colorIndex, themeDef) {
|
||||
const palettes = themeDef?.themeType == 'dark' ? presetDarkPalettes : presetPalettes;
|
||||
return palettes[color][colorIndex];
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
@@ -28,9 +21,9 @@
|
||||
>
|
||||
<FontIcon icon={value ? 'icon palette' : 'icon no-color'} />
|
||||
</div>
|
||||
{#each _.keys(presetPrimaryColors) as color}
|
||||
{#each USER_COLOR_NAMES as color}
|
||||
<div
|
||||
style={`background:${colorValue(color, 3, $currentThemeDefinition)}`}
|
||||
style={`background: var(--theme-usercolor-background-${color})`}
|
||||
class="item"
|
||||
class:disabled
|
||||
class:selected={color == value}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
.label.disabled {
|
||||
cursor: not-allowed;
|
||||
color: var(--theme-font-3);
|
||||
color: var(--theme-input-foreground-disabled);
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@@ -67,26 +67,26 @@
|
||||
-moz-appearance: none;
|
||||
-o-appearance: none;
|
||||
appearance: none;
|
||||
outline: 1px solid var(--theme-border);
|
||||
outline: var(--theme-input-border);
|
||||
box-shadow: none;
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
line-height: 1em;
|
||||
background: var(--theme-bg-0);
|
||||
background: var(--theme-input-background);
|
||||
}
|
||||
|
||||
.checked:after {
|
||||
content: '✔';
|
||||
color: var(--theme-font-1);
|
||||
color: var(--theme-input-foreground);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.isInherited {
|
||||
background: var(--theme-bg-2) !important;
|
||||
background: var(--theme-checkbox-background-inherited) !important;
|
||||
}
|
||||
|
||||
.checkbox.disabled {
|
||||
background: var(--theme-bg-2) !important;
|
||||
background: var(--theme-input-background-disabled) !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
import FormSelectField from './FormSelectField.svelte';
|
||||
import ColorSelector from './ColorSelector.svelte';
|
||||
import { presetPrimaryColors } from '@ant-design/colors';
|
||||
import { getFormContext } from './FormProviderCore.svelte';
|
||||
import { USER_COLOR_NAMES } from '../utility/userColors';
|
||||
|
||||
export let emptyLabel;
|
||||
export let useSelector = false;
|
||||
@@ -37,7 +37,7 @@
|
||||
{name}
|
||||
options={[
|
||||
{ value: '', label: emptyLabel },
|
||||
..._.keys(presetPrimaryColors).map(color => ({ value: color, label: _.startCase(color) })),
|
||||
...USER_COLOR_NAMES.map(color => ({ value: color, label: _.startCase(color) })),
|
||||
]}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<script lang="ts">
|
||||
import { getFormContext } from './FormProviderCore.svelte';
|
||||
import FormSelectFieldRaw from './FormSelectFieldRaw.svelte';
|
||||
import InlineButton from '../buttons/InlineButton.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import { openedTabs } from '../stores';
|
||||
|
||||
export let label;
|
||||
export let name;
|
||||
export let options;
|
||||
export let templateProps = {};
|
||||
|
||||
const { template } = getFormContext();
|
||||
|
||||
function handleOpenDriverSettings() {
|
||||
// Check if SettingsTab is already open
|
||||
const existingSettingsTab = $openedTabs.find(tab => tab.tabComponent === 'SettingsTab' && !tab.closedTime);
|
||||
|
||||
if (existingSettingsTab) {
|
||||
openedTabs.update(tabs =>
|
||||
tabs.map(tab =>
|
||||
tab.tabid === existingSettingsTab.tabid
|
||||
? { ...tab, selected: true, props: { ...tab.props, selectedItem: 'drivers' } }
|
||||
: { ...tab, selected: false }
|
||||
)
|
||||
);
|
||||
} else {
|
||||
openNewTab({
|
||||
title: 'Settings',
|
||||
icon: 'icon settings',
|
||||
tabComponent: 'SettingsTab',
|
||||
props: {
|
||||
selectedItem: 'drivers',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:component this={template} type="select" {label} {...templateProps}>
|
||||
<div class="flex connection-type-selector">
|
||||
<FormSelectFieldRaw {name} {options} defaultSelectValue={undefined} {...$$restProps} on:change />
|
||||
<div class="driver-settings-button">
|
||||
<InlineButton on:click={handleOpenDriverSettings} useBorder>
|
||||
<FontIcon icon="icon dots-horizontal" />
|
||||
</InlineButton>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:component>
|
||||
|
||||
<style>
|
||||
.connection-type-selector {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.driver-settings-button {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.driver-settings-button :global(.outer) {
|
||||
width: 24px;
|
||||
height: 34px;
|
||||
}
|
||||
</style>
|
||||
@@ -4,6 +4,7 @@
|
||||
import { _tval } from '../translations';
|
||||
|
||||
export let name;
|
||||
export let saveOnlyName = null;
|
||||
export let defaultValue;
|
||||
export let saveOnInput = false;
|
||||
|
||||
@@ -13,10 +14,18 @@
|
||||
<TextField
|
||||
{...$$restProps}
|
||||
value={$values?.[name] ? _tval($values[name]) : defaultValue}
|
||||
on:input={e => setFieldValue(name, e.target['value'])}
|
||||
on:input={e => {
|
||||
setFieldValue(name, e.target['value']);
|
||||
if (saveOnlyName) {
|
||||
setFieldValue(saveOnlyName, e.target['value']);
|
||||
}
|
||||
}}
|
||||
on:input={e => {
|
||||
if (saveOnInput) {
|
||||
setFieldValue(name, e.target['value']);
|
||||
if (saveOnlyName) {
|
||||
setFieldValue(saveOnlyName, e.target['value']);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -5,22 +5,37 @@
|
||||
export let focused = false;
|
||||
export let domEditor = undefined;
|
||||
export let autocomplete = 'new-password';
|
||||
export let isTextArea = false;
|
||||
|
||||
if (focused) onMount(() => domEditor.focus());
|
||||
</script>
|
||||
|
||||
<input
|
||||
class="text-input"
|
||||
type="text"
|
||||
{...$$restProps}
|
||||
bind:value
|
||||
on:change
|
||||
on:input
|
||||
on:click
|
||||
bind:this={domEditor}
|
||||
on:keydown
|
||||
{autocomplete}
|
||||
/>
|
||||
{#if isTextArea}
|
||||
<textarea
|
||||
class="text-input"
|
||||
{...$$restProps}
|
||||
bind:value
|
||||
on:change
|
||||
on:input
|
||||
on:click
|
||||
bind:this={domEditor}
|
||||
on:keydown
|
||||
{autocomplete}
|
||||
></textarea>
|
||||
{:else}
|
||||
<input
|
||||
class="text-input text-input-one-line"
|
||||
type="text"
|
||||
{...$$restProps}
|
||||
bind:value
|
||||
on:change
|
||||
on:input
|
||||
on:click
|
||||
bind:this={domEditor}
|
||||
on:keydown
|
||||
{autocomplete}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.text-input {
|
||||
@@ -32,11 +47,13 @@
|
||||
font-size: 13px;
|
||||
transition: all 0.15s ease;
|
||||
font-family: inherit;
|
||||
height: 40px;
|
||||
box-shadow: var(--theme-input-shadow);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.text-input-one-line {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.text-input::placeholder {
|
||||
color: var(--theme-input-placeholder);
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
export let padRight = false;
|
||||
export let style = null;
|
||||
export let colorClass = null;
|
||||
$: isSvgString = icon && icon.trim().startsWith('<svg');
|
||||
$: iconValue = typeof icon === 'string' ? icon : icon?.light || icon?.dark || '';
|
||||
$: isSvgString = iconValue.trim().startsWith('<svg');
|
||||
|
||||
const iconNames = {
|
||||
'icon minus-box': 'mdi mdi-minus-box-outline',
|
||||
@@ -368,11 +369,11 @@
|
||||
|
||||
{#if isSvgString}
|
||||
<span class="svg-inline" class:padLeft class:padRight {title} {style} on:click data-testid={$$props['data-testid']}>
|
||||
{@html icon}
|
||||
{@html iconValue}
|
||||
</span>
|
||||
{:else}
|
||||
<span
|
||||
class={`${iconNames[icon] || icon} ${colorClass || ''}`}
|
||||
class={`${iconNames[iconValue] || iconValue} ${colorClass || ''}`}
|
||||
{title}
|
||||
class:padLeft
|
||||
class:padRight
|
||||
@@ -388,8 +389,8 @@
|
||||
line-height: 1;
|
||||
}
|
||||
.svg-inline :global(svg) {
|
||||
width: 1.125em;
|
||||
height: 1.125em;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
vertical-align: middle;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@@ -136,10 +136,10 @@
|
||||
.load-more {
|
||||
margin-left: 2em;
|
||||
font-style: italic;
|
||||
color: var(--theme-font-link);
|
||||
color: var(--theme-link-foreground);
|
||||
}
|
||||
.load-more a {
|
||||
color: var(--theme-font-link);
|
||||
color: var(--theme-link-foreground);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -85,29 +85,17 @@
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
:global(.theme-type-dark) ul {
|
||||
--json-tree-string-color: #ffc5c5;
|
||||
--json-tree-symbol-color: #ffc5c5;
|
||||
--json-tree-boolean-color: #b6c3ff;
|
||||
--json-tree-function-color: #b6c3ff;
|
||||
--json-tree-number-color: #bfbdff;
|
||||
--json-tree-label-color: #e9aaed;
|
||||
--json-tree-arrow-color: #d4d4d4;
|
||||
--json-tree-null-color: #dcdcdc;
|
||||
--json-tree-undefined-color: #dcdcdc;
|
||||
--json-tree-date-color: #dcdcdc;
|
||||
}
|
||||
ul {
|
||||
--string-color: var(--json-tree-string-color, #cb3f41);
|
||||
--symbol-color: var(--json-tree-symbol-color, #cb3f41);
|
||||
--boolean-color: var(--json-tree-boolean-color, #112aa7);
|
||||
--function-color: var(--json-tree-function-color, #112aa7);
|
||||
--number-color: var(--json-tree-number-color, #3029cf);
|
||||
--label-color: var(--json-tree-label-color, #871d8f);
|
||||
--arrow-color: var(--json-tree-arrow-color, #727272);
|
||||
--null-color: var(--json-tree-null-color, #8d8d8d);
|
||||
--undefined-color: var(--json-tree-undefined-color, #8d8d8d);
|
||||
--date-color: var(--json-tree-date-color, #8d8d8d);
|
||||
--string-color: var(--theme-json-tree-string-color);
|
||||
--symbol-color: var(--theme-json-tree-symbol-color);
|
||||
--boolean-color: var(--theme-json-tree-boolean-color);
|
||||
--function-color: var(--theme-json-tree-function-color);
|
||||
--number-color: var(--theme-json-tree-number-color);
|
||||
--label-color: var(--theme-json-tree-label-color);
|
||||
--arrow-color: var(--theme-json-tree-arrow-color);
|
||||
--null-color: var(--theme-json-tree-null-color);
|
||||
--undefined-color: var(--theme-json-tree-undefined-color);
|
||||
--date-color: var(--theme-json-tree-date-color);
|
||||
--li-identation: var(--json-tree-li-indentation, 1em);
|
||||
--li-line-height: var(--json-tree-li-line-height, 1.3);
|
||||
--li-colon-space: 0.3em;
|
||||
@@ -130,15 +118,15 @@
|
||||
margin: 0;
|
||||
}
|
||||
ul.isDeleted {
|
||||
background: var(--theme-bg-volcano);
|
||||
background: var(--theme-json-tree-deleted-background);
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAEElEQVQImWNgIAX8x4KJBAD+agT8INXz9wAAAABJRU5ErkJggg==');
|
||||
background-repeat: repeat-x;
|
||||
background-position: 50% 50%;
|
||||
}
|
||||
ul.isModified {
|
||||
background: var(--theme-bg-gold);
|
||||
background: var(--theme-json-tree-modified-background);
|
||||
}
|
||||
ul.isInserted {
|
||||
background: var(--theme-bg-green);
|
||||
background: var(--theme-json-tree-inserted-background);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,6 +11,8 @@ import ErrorPage from './ErrorPage.svelte';
|
||||
import EnterLicensePage from './EnterLicensePage.svelte';
|
||||
import SetAdminPasswordPage from './SetAdminPasswordPage.svelte';
|
||||
import RedirectPage from './RedirectPage.svelte';
|
||||
import ForgotPasswordPage from './ForgotPasswordPage.svelte';
|
||||
import ResetPasswordPage from './ResetPasswordPage.svelte';
|
||||
|
||||
const isOauthCallback = handleOauthCallback();
|
||||
|
||||
@@ -61,6 +63,16 @@ function createApp() {
|
||||
target: document.body,
|
||||
props: {},
|
||||
});
|
||||
case 'forgot-password':
|
||||
return new ForgotPasswordPage({
|
||||
target: document.body,
|
||||
props: {},
|
||||
});
|
||||
case 'reset-password':
|
||||
return new ResetPasswordPage({
|
||||
target: document.body,
|
||||
props: {},
|
||||
});
|
||||
case 'admin':
|
||||
return new App({
|
||||
target: document.body,
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
<script lang="ts">
|
||||
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||
import DbKeyItemDetail from '../dbkeyvalue/DbKeyItemDetail.svelte';
|
||||
import DbKeyValueHashEdit from '../dbkeyvalue/DbKeyValueHashEdit.svelte';
|
||||
import FormFieldTemplateLarge from '../forms/FormFieldTemplateLarge.svelte';
|
||||
|
||||
import FormProvider from '../forms/FormProvider.svelte';
|
||||
import SelectField from '../forms/SelectField.svelte';
|
||||
import TextField from '../forms/TextField.svelte';
|
||||
import ModalBase from './ModalBase.svelte';
|
||||
import { closeCurrentModal } from './modalTools';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let conid;
|
||||
export let database;
|
||||
export let driver;
|
||||
export let onConfirm;
|
||||
|
||||
let item = {};
|
||||
let keyName = '';
|
||||
let type = driver.supportedKeyTypes[0].name;
|
||||
|
||||
const handleSubmit = async () => {
|
||||
closeCurrentModal();
|
||||
onConfirm({ type, keyName, ...item });
|
||||
};
|
||||
</script>
|
||||
|
||||
<FormProvider>
|
||||
<ModalBase {...$$restProps}>
|
||||
<svelte:fragment slot="header">{_t('addDbKeyModal.addKey', { defaultMessage: 'Add key' })}</svelte:fragment>
|
||||
|
||||
<div class="container">
|
||||
<FormFieldTemplateLarge label={_t('addDbKeyModal.key', { defaultMessage: 'Key' })} type="text" noMargin>
|
||||
<TextField
|
||||
value={keyName}
|
||||
on:change={e => {
|
||||
// @ts-ignore
|
||||
keyName = e.target.value;
|
||||
}}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
|
||||
<div class="m-3" />
|
||||
|
||||
<FormFieldTemplateLarge label={_t('addDbKeyModal.type', { defaultMessage: 'Type' })} type="combo" noMargin>
|
||||
<SelectField
|
||||
options={driver.supportedKeyTypes.map(t => ({ value: t.name, label: t.label }))}
|
||||
value={type}
|
||||
isNative
|
||||
on:change={e => {
|
||||
type = e.detail;
|
||||
}}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
|
||||
{#if type === 'hash'}
|
||||
<DbKeyValueHashEdit
|
||||
dbKeyFields={driver.supportedKeyTypes.find(x => x.name == type).dbKeyFields}
|
||||
{item}
|
||||
onChangeItem={value => {
|
||||
item = value;
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<DbKeyItemDetail
|
||||
dbKeyFields={driver.supportedKeyTypes.find(x => x.name == type).dbKeyFields}
|
||||
{item}
|
||||
onChangeItem={value => {
|
||||
item = value;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<FormStyledButton value={_t('common.ok', { defaultMessage: 'OK' })} on:click={e => handleSubmit()} />
|
||||
<FormStyledButton type="button" value={_t('common.cancel', { defaultMessage: 'Cancel' })} on:click={closeCurrentModal} />
|
||||
</svelte:fragment>
|
||||
</ModalBase>
|
||||
</FormProvider>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 30vh;
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
export let color;
|
||||
export let header;
|
||||
export let text;
|
||||
export let onChange;
|
||||
|
||||
$: value = color;
|
||||
@@ -13,10 +12,6 @@
|
||||
<ModalBase {...$$restProps}>
|
||||
<svelte:fragment slot="header">{header}</svelte:fragment>
|
||||
|
||||
<div class="m-2">
|
||||
{text}
|
||||
</div>
|
||||
|
||||
<ColorSelector
|
||||
{value}
|
||||
on:change={e => {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
export let header;
|
||||
export let text;
|
||||
|
||||
const initialColor = useConnectionColor({ conid, database }, null, null, false, false);
|
||||
const initialColor = useConnectionColor({ conid, database }, 'foreground', '@rawColorWithoutVariable', false);
|
||||
|
||||
$: value = $initialColor;
|
||||
</script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user