mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-26 21:25:59 +00:00
Merge branch 'feature/cloud'
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
DEVMODE=1
|
DEVMODE=1
|
||||||
SHELL_SCRIPTING=1
|
SHELL_SCRIPTING=1
|
||||||
|
# LOCAL_DBGATE_CLOUD=1
|
||||||
|
# LOCAL_DBGATE_IDENTITY=1
|
||||||
|
|
||||||
# CLOUD_UPGRADE_FILE=c:\test\upg\upgrade.zip
|
# CLOUD_UPGRADE_FILE=c:\test\upg\upgrade.zip
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const {
|
|||||||
} = require('../auth/authProvider');
|
} = require('../auth/authProvider');
|
||||||
const storage = require('./storage');
|
const storage = require('./storage');
|
||||||
const { decryptPasswordString } = require('../utility/crypting');
|
const { decryptPasswordString } = require('../utility/crypting');
|
||||||
|
const { createDbGateIdentitySession, startCloudTokenChecking } = require('../utility/cloudIntf');
|
||||||
|
const socket = require('../utility/socket');
|
||||||
|
|
||||||
const logger = getLogger('auth');
|
const logger = getLogger('auth');
|
||||||
|
|
||||||
@@ -135,5 +137,14 @@ module.exports = {
|
|||||||
return getAuthProviderById(amoid).redirect(params);
|
return getAuthProviderById(amoid).redirect(params);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createCloudLoginSession_meta: true,
|
||||||
|
async createCloudLoginSession({ client }) {
|
||||||
|
const res = await createDbGateIdentitySession(client);
|
||||||
|
startCloudTokenChecking(res.sid, tokenHolder => {
|
||||||
|
socket.emit('got-cloud-token', tokenHolder);
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
authMiddleware,
|
authMiddleware,
|
||||||
};
|
};
|
||||||
|
|||||||
250
packages/api/src/controllers/cloud.js
Normal file
250
packages/api/src/controllers/cloud.js
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
const {
|
||||||
|
getPublicCloudFiles,
|
||||||
|
getPublicFileData,
|
||||||
|
refreshPublicFiles,
|
||||||
|
callCloudApiGet,
|
||||||
|
callCloudApiPost,
|
||||||
|
getCloudFolderEncryptor,
|
||||||
|
getCloudContent,
|
||||||
|
putCloudContent,
|
||||||
|
removeCloudCachedConnection,
|
||||||
|
} = require('../utility/cloudIntf');
|
||||||
|
const connections = require('./connections');
|
||||||
|
const socket = require('../utility/socket');
|
||||||
|
const { recryptConnection, getInternalEncryptor, encryptConnection } = require('../utility/crypting');
|
||||||
|
const { getConnectionLabel, getLogger, extractErrorLogData } = require('dbgate-tools');
|
||||||
|
const logger = getLogger('cloud');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
publicFiles_meta: true,
|
||||||
|
async publicFiles() {
|
||||||
|
const res = await getPublicCloudFiles();
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
publicFileData_meta: true,
|
||||||
|
async publicFileData({ path }) {
|
||||||
|
const res = getPublicFileData(path);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshPublicFiles_meta: true,
|
||||||
|
async refreshPublicFiles({ isRefresh }) {
|
||||||
|
await refreshPublicFiles(isRefresh);
|
||||||
|
return {
|
||||||
|
status: 'ok',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
contentList_meta: true,
|
||||||
|
async contentList() {
|
||||||
|
try {
|
||||||
|
const resp = await callCloudApiGet('content-list');
|
||||||
|
return resp;
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(extractErrorLogData(err), 'Error getting cloud content list');
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getContent_meta: true,
|
||||||
|
async getContent({ folid, cntid }) {
|
||||||
|
const resp = await getCloudContent(folid, cntid);
|
||||||
|
return resp;
|
||||||
|
},
|
||||||
|
|
||||||
|
putContent_meta: true,
|
||||||
|
async putContent({ folid, cntid, content, name, type }) {
|
||||||
|
const resp = await putCloudContent(folid, cntid, content, name, type);
|
||||||
|
socket.emitChanged('cloud-content-changed');
|
||||||
|
socket.emit('cloud-content-updated');
|
||||||
|
return resp;
|
||||||
|
},
|
||||||
|
|
||||||
|
createFolder_meta: true,
|
||||||
|
async createFolder({ name }) {
|
||||||
|
const resp = await callCloudApiPost(`folders/create`, { name });
|
||||||
|
socket.emitChanged('cloud-content-changed');
|
||||||
|
socket.emit('cloud-content-updated');
|
||||||
|
return resp;
|
||||||
|
},
|
||||||
|
|
||||||
|
grantFolder_meta: true,
|
||||||
|
async grantFolder({ inviteLink }) {
|
||||||
|
const m = inviteLink.match(/^dbgate\:\/\/folder\/v1\/([a-zA-Z0-9]+)\?mode=(read|write|admin)$/);
|
||||||
|
if (!m) {
|
||||||
|
throw new Error('Invalid invite link format');
|
||||||
|
}
|
||||||
|
const invite = m[1];
|
||||||
|
const mode = m[2];
|
||||||
|
|
||||||
|
const resp = await callCloudApiPost(`folders/grant/${mode}`, { invite });
|
||||||
|
socket.emitChanged('cloud-content-changed');
|
||||||
|
socket.emit('cloud-content-updated');
|
||||||
|
return resp;
|
||||||
|
},
|
||||||
|
|
||||||
|
renameFolder_meta: true,
|
||||||
|
async renameFolder({ folid, name }) {
|
||||||
|
const resp = await callCloudApiPost(`folders/rename`, { folid, name });
|
||||||
|
socket.emitChanged('cloud-content-changed');
|
||||||
|
socket.emit('cloud-content-updated');
|
||||||
|
return resp;
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteFolder_meta: true,
|
||||||
|
async deleteFolder({ folid }) {
|
||||||
|
const resp = await callCloudApiPost(`folders/delete`, { folid });
|
||||||
|
socket.emitChanged('cloud-content-changed');
|
||||||
|
socket.emit('cloud-content-updated');
|
||||||
|
return resp;
|
||||||
|
},
|
||||||
|
|
||||||
|
getInviteToken_meta: true,
|
||||||
|
async getInviteToken({ folid, role }) {
|
||||||
|
const resp = await callCloudApiGet(`invite-token/${folid}/${role}`);
|
||||||
|
return resp;
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshContent_meta: true,
|
||||||
|
async refreshContent() {
|
||||||
|
socket.emitChanged('cloud-content-changed');
|
||||||
|
socket.emit('cloud-content-updated');
|
||||||
|
return {
|
||||||
|
status: 'ok',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
copyConnectionCloud_meta: true,
|
||||||
|
async copyConnectionCloud({ conid, folid }) {
|
||||||
|
const conn = await connections.getCore({ conid });
|
||||||
|
const folderEncryptor = await getCloudFolderEncryptor(folid);
|
||||||
|
const recryptedConn = recryptConnection(conn, getInternalEncryptor(), folderEncryptor);
|
||||||
|
const connToSend = _.omit(recryptedConn, ['_id']);
|
||||||
|
const resp = await putCloudContent(
|
||||||
|
folid,
|
||||||
|
undefined,
|
||||||
|
JSON.stringify(connToSend),
|
||||||
|
getConnectionLabel(conn),
|
||||||
|
'connection'
|
||||||
|
);
|
||||||
|
return resp;
|
||||||
|
},
|
||||||
|
|
||||||
|
saveConnection_meta: true,
|
||||||
|
async saveConnection({ folid, connection }) {
|
||||||
|
let cntid = undefined;
|
||||||
|
if (connection._id) {
|
||||||
|
const m = connection._id.match(/^cloud\:\/\/(.+)\/(.+)$/);
|
||||||
|
if (!m) {
|
||||||
|
throw new Error('Invalid cloud connection ID format');
|
||||||
|
}
|
||||||
|
folid = m[1];
|
||||||
|
cntid = m[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!folid) {
|
||||||
|
throw new Error('Missing cloud folder ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderEncryptor = await getCloudFolderEncryptor(folid);
|
||||||
|
const recryptedConn = encryptConnection(connection, folderEncryptor);
|
||||||
|
const resp = await putCloudContent(
|
||||||
|
folid,
|
||||||
|
cntid,
|
||||||
|
JSON.stringify(recryptedConn),
|
||||||
|
getConnectionLabel(recryptedConn),
|
||||||
|
'connection'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (resp.apiErrorMessage) {
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCloudCachedConnection(folid, resp.cntid);
|
||||||
|
cntid = resp.cntid;
|
||||||
|
socket.emitChanged('cloud-content-changed');
|
||||||
|
socket.emit('cloud-content-updated');
|
||||||
|
return {
|
||||||
|
...recryptedConn,
|
||||||
|
_id: `cloud://${folid}/${cntid}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
duplicateConnection_meta: true,
|
||||||
|
async duplicateConnection({ conid }) {
|
||||||
|
const m = conid.match(/^cloud\:\/\/(.+)\/(.+)$/);
|
||||||
|
if (!m) {
|
||||||
|
throw new Error('Invalid cloud connection ID format');
|
||||||
|
}
|
||||||
|
const folid = m[1];
|
||||||
|
const cntid = m[2];
|
||||||
|
const respGet = await getCloudContent(folid, cntid);
|
||||||
|
const conn = JSON.parse(respGet.content);
|
||||||
|
const conn2 = {
|
||||||
|
...conn,
|
||||||
|
displayName: getConnectionLabel(conn) + ' - copy',
|
||||||
|
};
|
||||||
|
const respPut = await putCloudContent(folid, undefined, JSON.stringify(conn2), conn2.displayName, 'connection');
|
||||||
|
return respPut;
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteConnection_meta: true,
|
||||||
|
async deleteConnection({ conid }) {
|
||||||
|
const m = conid.match(/^cloud\:\/\/(.+)\/(.+)$/);
|
||||||
|
if (!m) {
|
||||||
|
throw new Error('Invalid cloud connection ID format');
|
||||||
|
}
|
||||||
|
const folid = m[1];
|
||||||
|
const cntid = m[2];
|
||||||
|
const resp = await callCloudApiPost(`content/delete/${folid}/${cntid}`);
|
||||||
|
socket.emitChanged('cloud-content-changed');
|
||||||
|
socket.emit('cloud-content-updated');
|
||||||
|
return resp;
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteContent_meta: true,
|
||||||
|
async deleteContent({ folid, cntid }) {
|
||||||
|
const resp = await callCloudApiPost(`content/delete/${folid}/${cntid}`);
|
||||||
|
socket.emitChanged('cloud-content-changed');
|
||||||
|
socket.emit('cloud-content-updated');
|
||||||
|
return resp;
|
||||||
|
},
|
||||||
|
|
||||||
|
renameContent_meta: true,
|
||||||
|
async renameContent({ folid, cntid, name }) {
|
||||||
|
const resp = await callCloudApiPost(`content/rename/${folid}/${cntid}`, { name });
|
||||||
|
socket.emitChanged('cloud-content-changed');
|
||||||
|
socket.emit('cloud-content-updated');
|
||||||
|
return resp;
|
||||||
|
},
|
||||||
|
|
||||||
|
saveFile_meta: true,
|
||||||
|
async saveFile({ folid, cntid, fileName, data, contentFolder, format }) {
|
||||||
|
const resp = await putCloudContent(folid, cntid, data, fileName, 'file', contentFolder, format);
|
||||||
|
socket.emitChanged('cloud-content-changed');
|
||||||
|
socket.emit('cloud-content-updated');
|
||||||
|
return resp;
|
||||||
|
},
|
||||||
|
|
||||||
|
copyFile_meta: true,
|
||||||
|
async copyFile({ folid, cntid, name }) {
|
||||||
|
const resp = await callCloudApiPost(`content/duplicate/${folid}/${cntid}`, { name });
|
||||||
|
socket.emitChanged('cloud-content-changed');
|
||||||
|
socket.emit('cloud-content-updated');
|
||||||
|
return resp;
|
||||||
|
},
|
||||||
|
|
||||||
|
exportFile_meta: true,
|
||||||
|
async exportFile({ folid, cntid, filePath }, req) {
|
||||||
|
const { content } = await getCloudContent(folid, cntid);
|
||||||
|
if (!content) {
|
||||||
|
throw new Error('File not found');
|
||||||
|
}
|
||||||
|
await fs.writeFile(filePath, content);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -239,6 +239,19 @@ module.exports = {
|
|||||||
return (await this.datastore.find()).filter(x => connectionHasPermission(x, req));
|
return (await this.datastore.find()).filter(x => connectionHasPermission(x, req));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getUsedEngines() {
|
||||||
|
const storage = require('./storage');
|
||||||
|
|
||||||
|
const storageEngines = await storage.getUsedEngines();
|
||||||
|
if (storageEngines) {
|
||||||
|
return storageEngines;
|
||||||
|
}
|
||||||
|
if (portalConnections) {
|
||||||
|
return _.uniq(_.compact(portalConnections.map(x => x.engine)));
|
||||||
|
}
|
||||||
|
return _.uniq((await this.datastore.find()).map(x => x.engine));
|
||||||
|
},
|
||||||
|
|
||||||
test_meta: true,
|
test_meta: true,
|
||||||
test({ connection, requestDbList = false }) {
|
test({ connection, requestDbList = false }) {
|
||||||
const subprocess = fork(
|
const subprocess = fork(
|
||||||
@@ -410,6 +423,13 @@ module.exports = {
|
|||||||
return volatile;
|
return volatile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cloudMatch = conid.match(/^cloud\:\/\/(.+)\/(.+)$/);
|
||||||
|
if (cloudMatch) {
|
||||||
|
const { loadCachedCloudConnection } = require('../utility/cloudIntf');
|
||||||
|
const conn = await loadCachedCloudConnection(cloudMatch[1], cloudMatch[2]);
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
const storage = require('./storage');
|
const storage = require('./storage');
|
||||||
|
|
||||||
const storageConnection = await storage.getConnection({ conid });
|
const storageConnection = await storage.getConnection({ conid });
|
||||||
|
|||||||
@@ -148,6 +148,9 @@ module.exports = {
|
|||||||
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
const existing = this.opened.find(x => x.conid == conid && x.database == database);
|
||||||
if (existing) return existing;
|
if (existing) return existing;
|
||||||
const connection = await connections.getCore({ conid });
|
const connection = await connections.getCore({ conid });
|
||||||
|
if (!connection) {
|
||||||
|
throw new Error(`databaseConnections: Connection with conid="${conid}" not found`);
|
||||||
|
}
|
||||||
if (connection.passwordMode == 'askPassword' || connection.passwordMode == 'askUser') {
|
if (connection.passwordMode == 'askPassword' || connection.passwordMode == 'askUser') {
|
||||||
throw new MissingCredentialsError({ conid, passwordMode: connection.passwordMode });
|
throw new MissingCredentialsError({ conid, passwordMode: connection.passwordMode });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ module.exports = {
|
|||||||
if (existing) return existing;
|
if (existing) return existing;
|
||||||
const connection = await connections.getCore({ conid });
|
const connection = await connections.getCore({ conid });
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
throw new Error(`Connection with conid="${conid}" not found`);
|
throw new Error(`serverConnections: Connection with conid="${conid}" not found`);
|
||||||
}
|
}
|
||||||
if (connection.singleDatabase) {
|
if (connection.singleDatabase) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -32,4 +32,8 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
startRefreshLicense() {},
|
startRefreshLicense() {},
|
||||||
|
|
||||||
|
async getUsedEngines() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const plugins = require('./controllers/plugins');
|
|||||||
const files = require('./controllers/files');
|
const files = require('./controllers/files');
|
||||||
const scheduler = require('./controllers/scheduler');
|
const scheduler = require('./controllers/scheduler');
|
||||||
const queryHistory = require('./controllers/queryHistory');
|
const queryHistory = require('./controllers/queryHistory');
|
||||||
|
const cloud = require('./controllers/cloud');
|
||||||
const onFinished = require('on-finished');
|
const onFinished = require('on-finished');
|
||||||
const processArgs = require('./utility/processArgs');
|
const processArgs = require('./utility/processArgs');
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ const { getDefaultAuthProvider } = require('./auth/authProvider');
|
|||||||
const startCloudUpgradeTimer = require('./utility/cloudUpgrade');
|
const startCloudUpgradeTimer = require('./utility/cloudUpgrade');
|
||||||
const { isProApp } = require('./utility/checkLicense');
|
const { isProApp } = require('./utility/checkLicense');
|
||||||
const { getHealthStatus, getHealthStatusSprinx } = require('./utility/healthStatus');
|
const { getHealthStatus, getHealthStatusSprinx } = require('./utility/healthStatus');
|
||||||
|
const { startCloudFiles } = require('./utility/cloudIntf');
|
||||||
|
|
||||||
const logger = getLogger('main');
|
const logger = getLogger('main');
|
||||||
|
|
||||||
@@ -200,6 +202,8 @@ function start() {
|
|||||||
if (process.env.CLOUD_UPGRADE_FILE) {
|
if (process.env.CLOUD_UPGRADE_FILE) {
|
||||||
startCloudUpgradeTimer();
|
startCloudUpgradeTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startCloudFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
function useAllControllers(app, electron) {
|
function useAllControllers(app, electron) {
|
||||||
@@ -220,6 +224,7 @@ function useAllControllers(app, electron) {
|
|||||||
useController(app, electron, '/query-history', queryHistory);
|
useController(app, electron, '/query-history', queryHistory);
|
||||||
useController(app, electron, '/apps', apps);
|
useController(app, electron, '/apps', apps);
|
||||||
useController(app, electron, '/auth', auth);
|
useController(app, electron, '/auth', auth);
|
||||||
|
useController(app, electron, '/cloud', cloud);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setElectronSender(electronSender) {
|
function setElectronSender(electronSender) {
|
||||||
|
|||||||
@@ -28,14 +28,7 @@ function start() {
|
|||||||
let version = {
|
let version = {
|
||||||
version: 'Unknown',
|
version: 'Unknown',
|
||||||
};
|
};
|
||||||
try {
|
version = await driver.getVersion(dbhan);
|
||||||
version = await driver.getVersion(dbhan);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(extractErrorLogData(err), 'Error getting DB server version');
|
|
||||||
version = {
|
|
||||||
version: 'Unknown',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let databases = undefined;
|
let databases = undefined;
|
||||||
if (requestDbList) {
|
if (requestDbList) {
|
||||||
databases = await driver.listDatabases(dbhan);
|
databases = await driver.listDatabases(dbhan);
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ async function callRefactorSqlQueryApi(query, task, structure, dialect) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLicenseHttpHeaders() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
isAuthProxySupported,
|
isAuthProxySupported,
|
||||||
authProxyGetRedirectUrl,
|
authProxyGetRedirectUrl,
|
||||||
@@ -47,4 +51,5 @@ module.exports = {
|
|||||||
callTextToSqlApi,
|
callTextToSqlApi,
|
||||||
callCompleteOnCursorApi,
|
callCompleteOnCursorApi,
|
||||||
callRefactorSqlQueryApi,
|
callRefactorSqlQueryApi,
|
||||||
|
getLicenseHttpHeaders,
|
||||||
};
|
};
|
||||||
|
|||||||
380
packages/api/src/utility/cloudIntf.js
Normal file
380
packages/api/src/utility/cloudIntf.js
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const path = require('path');
|
||||||
|
const { getLicenseHttpHeaders } = require('./authProxy');
|
||||||
|
const { getLogger, extractErrorLogData, jsonLinesParse } = require('dbgate-tools');
|
||||||
|
const { datadir } = require('./directories');
|
||||||
|
const platformInfo = require('./platformInfo');
|
||||||
|
const connections = require('../controllers/connections');
|
||||||
|
const { isProApp } = require('./checkLicense');
|
||||||
|
const socket = require('./socket');
|
||||||
|
const config = require('../controllers/config');
|
||||||
|
const simpleEncryptor = require('simple-encryptor');
|
||||||
|
const currentVersion = require('../currentVersion');
|
||||||
|
const { getPublicIpInfo } = require('./hardwareFingerprint');
|
||||||
|
|
||||||
|
const logger = getLogger('cloudIntf');
|
||||||
|
|
||||||
|
let cloudFiles = null;
|
||||||
|
|
||||||
|
const DBGATE_IDENTITY_URL = process.env.LOCAL_DBGATE_IDENTITY
|
||||||
|
? 'http://localhost:3103'
|
||||||
|
: process.env.DEVWEB || process.env.DEVMODE
|
||||||
|
? 'https://identity.dbgate.udolni.net'
|
||||||
|
: 'https://identity.dbgate.io';
|
||||||
|
|
||||||
|
const DBGATE_CLOUD_URL = process.env.LOCAL_DBGATE_CLOUD
|
||||||
|
? 'http://localhost:3110'
|
||||||
|
: process.env.DEVWEB || process.env.DEVMODE
|
||||||
|
? 'https://cloud.dbgate.udolni.net'
|
||||||
|
: 'https://cloud.dbgate.io';
|
||||||
|
|
||||||
|
async function createDbGateIdentitySession(client) {
|
||||||
|
const resp = await axios.default.post(
|
||||||
|
`${DBGATE_IDENTITY_URL}/api/create-session`,
|
||||||
|
{
|
||||||
|
client,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
...getLicenseHttpHeaders(),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
sid: resp.data.sid,
|
||||||
|
url: `${DBGATE_IDENTITY_URL}/api/signin/${resp.data.sid}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCloudTokenChecking(sid, callback) {
|
||||||
|
const started = Date.now();
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
if (Date.now() - started > 60 * 1000) {
|
||||||
|
clearInterval(interval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// console.log(`Checking cloud token for session: ${DBGATE_IDENTITY_URL}/api/get-token/${sid}`);
|
||||||
|
const resp = await axios.default.get(`${DBGATE_IDENTITY_URL}/api/get-token/${sid}`, {
|
||||||
|
headers: {
|
||||||
|
...getLicenseHttpHeaders(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// console.log('CHECK RESP:', resp.data);
|
||||||
|
|
||||||
|
if (resp.data.email) {
|
||||||
|
clearInterval(interval);
|
||||||
|
callback(resp.data);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(extractErrorLogData(err), 'Error checking cloud token');
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCloudFiles() {
|
||||||
|
try {
|
||||||
|
const fileContent = await fs.readFile(path.join(datadir(), 'cloud-files.jsonl'), 'utf-8');
|
||||||
|
const parsedJson = jsonLinesParse(fileContent);
|
||||||
|
cloudFiles = _.sortBy(parsedJson, x => `${x.folder}/${x.title}`);
|
||||||
|
} catch (err) {
|
||||||
|
cloudFiles = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectCloudFilesSearchTags() {
|
||||||
|
const res = [];
|
||||||
|
if (platformInfo.isElectron) {
|
||||||
|
res.push('app');
|
||||||
|
} else {
|
||||||
|
res.push('web');
|
||||||
|
}
|
||||||
|
if (platformInfo.isWindows) {
|
||||||
|
res.push('windows');
|
||||||
|
}
|
||||||
|
if (platformInfo.isMac) {
|
||||||
|
res.push('mac');
|
||||||
|
}
|
||||||
|
if (platformInfo.isLinux) {
|
||||||
|
res.push('linux');
|
||||||
|
}
|
||||||
|
if (platformInfo.isAwsUbuntuLayout) {
|
||||||
|
res.push('aws');
|
||||||
|
}
|
||||||
|
if (platformInfo.isAzureUbuntuLayout) {
|
||||||
|
res.push('azure');
|
||||||
|
}
|
||||||
|
if (platformInfo.isSnap) {
|
||||||
|
res.push('snap');
|
||||||
|
}
|
||||||
|
if (platformInfo.isDocker) {
|
||||||
|
res.push('docker');
|
||||||
|
}
|
||||||
|
if (platformInfo.isNpmDist) {
|
||||||
|
res.push('npm');
|
||||||
|
}
|
||||||
|
const engines = await connections.getUsedEngines();
|
||||||
|
const engineTags = engines.map(engine => engine.split('@')[0]);
|
||||||
|
res.push(...engineTags);
|
||||||
|
|
||||||
|
// team-premium and trials will return the same cloud files as premium - no need to check
|
||||||
|
res.push(isProApp() ? 'premium' : 'community');
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCloudSigninHolder() {
|
||||||
|
const settingsValue = await config.getSettings();
|
||||||
|
const holder = settingsValue['cloudSigninTokenHolder'];
|
||||||
|
return holder;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCloudSigninHeaders(holder = null) {
|
||||||
|
if (!holder) {
|
||||||
|
holder = await getCloudSigninHolder();
|
||||||
|
}
|
||||||
|
if (holder) {
|
||||||
|
return {
|
||||||
|
'x-cloud-login': holder.token,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateCloudFiles(isRefresh) {
|
||||||
|
let lastCloudFilesTags;
|
||||||
|
try {
|
||||||
|
lastCloudFilesTags = await fs.readFile(path.join(datadir(), 'cloud-files-tags.txt'), 'utf-8');
|
||||||
|
} catch (err) {
|
||||||
|
lastCloudFilesTags = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const ipInfo = await getPublicIpInfo();
|
||||||
|
|
||||||
|
const tags = (await collectCloudFilesSearchTags()).join(',');
|
||||||
|
let lastCheckedTm = 0;
|
||||||
|
if (tags == lastCloudFilesTags && cloudFiles.length > 0) {
|
||||||
|
lastCheckedTm = _.max(cloudFiles.map(x => parseInt(x.modifiedTm)));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info({ tags, lastCheckedTm }, 'Downloading cloud files');
|
||||||
|
|
||||||
|
const resp = await axios.default.get(
|
||||||
|
`${DBGATE_CLOUD_URL}/public-cloud-updates?lastCheckedTm=${lastCheckedTm}&tags=${tags}&isRefresh=${
|
||||||
|
isRefresh ? 1 : 0
|
||||||
|
}&country=${ipInfo?.country || ''}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
...getLicenseHttpHeaders(),
|
||||||
|
...(await getCloudSigninHeaders()),
|
||||||
|
'x-app-version': currentVersion.version,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Downloaded ${resp.data.length} cloud files`);
|
||||||
|
|
||||||
|
const filesByPath = lastCheckedTm == 0 ? {} : _.keyBy(cloudFiles, 'path');
|
||||||
|
for (const file of resp.data) {
|
||||||
|
if (file.isDeleted) {
|
||||||
|
delete filesByPath[file.path];
|
||||||
|
} else {
|
||||||
|
filesByPath[file.path] = file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cloudFiles = Object.values(filesByPath);
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(datadir(), 'cloud-files.jsonl'), cloudFiles.map(x => JSON.stringify(x)).join('\n'));
|
||||||
|
await fs.writeFile(path.join(datadir(), 'cloud-files-tags.txt'), tags);
|
||||||
|
|
||||||
|
socket.emitChanged(`public-cloud-changed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startCloudFiles() {
|
||||||
|
loadCloudFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPublicCloudFiles() {
|
||||||
|
if (!loadCloudFiles) {
|
||||||
|
await loadCloudFiles();
|
||||||
|
}
|
||||||
|
return cloudFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPublicFileData(path) {
|
||||||
|
const resp = await axios.default.get(`${DBGATE_CLOUD_URL}/public/${path}`, {
|
||||||
|
headers: {
|
||||||
|
...getLicenseHttpHeaders(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshPublicFiles(isRefresh) {
|
||||||
|
if (!cloudFiles) {
|
||||||
|
await loadCloudFiles();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await updateCloudFiles(isRefresh);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(extractErrorLogData(err), 'Error updating cloud files');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callCloudApiGet(endpoint, signinHolder = null, additionalHeaders = {}) {
|
||||||
|
if (!signinHolder) {
|
||||||
|
signinHolder = await getCloudSigninHolder();
|
||||||
|
}
|
||||||
|
if (!signinHolder) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const signinHeaders = await getCloudSigninHeaders(signinHolder);
|
||||||
|
|
||||||
|
const resp = await axios.default.get(`${DBGATE_CLOUD_URL}/${endpoint}`, {
|
||||||
|
headers: {
|
||||||
|
...getLicenseHttpHeaders(),
|
||||||
|
...signinHeaders,
|
||||||
|
...additionalHeaders,
|
||||||
|
},
|
||||||
|
validateStatus: status => status < 500,
|
||||||
|
});
|
||||||
|
const { errorMessage } = resp.data;
|
||||||
|
if (errorMessage) {
|
||||||
|
return { apiErrorMessage: errorMessage };
|
||||||
|
}
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callCloudApiPost(endpoint, body, signinHolder = null) {
|
||||||
|
if (!signinHolder) {
|
||||||
|
signinHolder = await getCloudSigninHolder();
|
||||||
|
}
|
||||||
|
if (!signinHolder) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const signinHeaders = await getCloudSigninHeaders(signinHolder);
|
||||||
|
|
||||||
|
const resp = await axios.default.post(`${DBGATE_CLOUD_URL}/${endpoint}`, body, {
|
||||||
|
headers: {
|
||||||
|
...getLicenseHttpHeaders(),
|
||||||
|
...signinHeaders,
|
||||||
|
},
|
||||||
|
validateStatus: status => status < 500,
|
||||||
|
});
|
||||||
|
const { errorMessage, isLicenseLimit, limitedLicenseLimits } = resp.data;
|
||||||
|
if (errorMessage) {
|
||||||
|
return {
|
||||||
|
apiErrorMessage: errorMessage,
|
||||||
|
apiErrorIsLicenseLimit: isLicenseLimit,
|
||||||
|
apiErrorLimitedLicenseLimits: limitedLicenseLimits,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCloudFolderEncryptor(folid) {
|
||||||
|
const { encryptionKey } = await callCloudApiGet(`folder-key/${folid}`);
|
||||||
|
if (!encryptionKey) {
|
||||||
|
throw new Error('No encryption key for folder: ' + folid);
|
||||||
|
}
|
||||||
|
return simpleEncryptor.createEncryptor(encryptionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCloudContent(folid, cntid) {
|
||||||
|
const signinHolder = await getCloudSigninHolder();
|
||||||
|
if (!signinHolder) {
|
||||||
|
throw new Error('No signed in');
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptor = simpleEncryptor.createEncryptor(signinHolder.encryptionKey);
|
||||||
|
|
||||||
|
const { content, name, type, contentFolder, contentType, apiErrorMessage } = await callCloudApiGet(
|
||||||
|
`content/${folid}/${cntid}`,
|
||||||
|
signinHolder,
|
||||||
|
{
|
||||||
|
'x-kehid': signinHolder.kehid,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (apiErrorMessage) {
|
||||||
|
return { apiErrorMessage };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: encryptor.decrypt(content),
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
contentFolder,
|
||||||
|
contentType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns Promise<{ cntid: string } | { apiErrorMessage: string }>
|
||||||
|
*/
|
||||||
|
async function putCloudContent(folid, cntid, content, name, type, contentFolder = null, contentType = null) {
|
||||||
|
const signinHolder = await getCloudSigninHolder();
|
||||||
|
if (!signinHolder) {
|
||||||
|
throw new Error('No signed in');
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptor = simpleEncryptor.createEncryptor(signinHolder.encryptionKey);
|
||||||
|
|
||||||
|
const resp = await callCloudApiPost(
|
||||||
|
`put-content`,
|
||||||
|
{
|
||||||
|
folid,
|
||||||
|
cntid,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
kehid: signinHolder.kehid,
|
||||||
|
content: encryptor.encrypt(content),
|
||||||
|
contentFolder,
|
||||||
|
contentType,
|
||||||
|
},
|
||||||
|
signinHolder
|
||||||
|
);
|
||||||
|
socket.emitChanged('cloud-content-changed');
|
||||||
|
socket.emit('cloud-content-updated');
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloudConnectionCache = {};
|
||||||
|
async function loadCachedCloudConnection(folid, cntid) {
|
||||||
|
const cacheKey = `${folid}|${cntid}`;
|
||||||
|
if (!cloudConnectionCache[cacheKey]) {
|
||||||
|
const { content } = await getCloudContent(folid, cntid);
|
||||||
|
cloudConnectionCache[cacheKey] = {
|
||||||
|
...JSON.parse(content),
|
||||||
|
_id: `cloud://${folid}/${cntid}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return cloudConnectionCache[cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCloudCachedConnection(folid, cntid) {
|
||||||
|
const cacheKey = `${folid}|${cntid}`;
|
||||||
|
delete cloudConnectionCache[cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createDbGateIdentitySession,
|
||||||
|
startCloudTokenChecking,
|
||||||
|
startCloudFiles,
|
||||||
|
getPublicCloudFiles,
|
||||||
|
getPublicFileData,
|
||||||
|
refreshPublicFiles,
|
||||||
|
callCloudApiGet,
|
||||||
|
callCloudApiPost,
|
||||||
|
getCloudFolderEncryptor,
|
||||||
|
getCloudContent,
|
||||||
|
loadCachedCloudConnection,
|
||||||
|
putCloudContent,
|
||||||
|
removeCloudCachedConnection,
|
||||||
|
};
|
||||||
@@ -81,11 +81,11 @@ function decryptPasswordString(password) {
|
|||||||
return password;
|
return password;
|
||||||
}
|
}
|
||||||
|
|
||||||
function encryptObjectPasswordField(obj, field) {
|
function encryptObjectPasswordField(obj, field, encryptor = null) {
|
||||||
if (obj && obj[field] && !obj[field].startsWith('crypt:')) {
|
if (obj && obj[field] && !obj[field].startsWith('crypt:')) {
|
||||||
return {
|
return {
|
||||||
...obj,
|
...obj,
|
||||||
[field]: 'crypt:' + getInternalEncryptor().encrypt(obj[field]),
|
[field]: 'crypt:' + (encryptor || getInternalEncryptor()).encrypt(obj[field]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
@@ -101,11 +101,11 @@ function decryptObjectPasswordField(obj, field) {
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
function encryptConnection(connection) {
|
function encryptConnection(connection, encryptor = null) {
|
||||||
if (connection.passwordMode != 'saveRaw') {
|
if (connection.passwordMode != 'saveRaw') {
|
||||||
connection = encryptObjectPasswordField(connection, 'password');
|
connection = encryptObjectPasswordField(connection, 'password', encryptor);
|
||||||
connection = encryptObjectPasswordField(connection, 'sshPassword');
|
connection = encryptObjectPasswordField(connection, 'sshPassword', encryptor);
|
||||||
connection = encryptObjectPasswordField(connection, 'sshKeyfilePassword');
|
connection = encryptObjectPasswordField(connection, 'sshKeyfilePassword', encryptor);
|
||||||
}
|
}
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,4 +87,5 @@ module.exports = {
|
|||||||
getHardwareFingerprint,
|
getHardwareFingerprint,
|
||||||
getHardwareFingerprintHash,
|
getHardwareFingerprintHash,
|
||||||
getPublicHardwareFingerprint,
|
getPublicHardwareFingerprint,
|
||||||
|
getPublicIpInfo,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
:root {
|
:root {
|
||||||
--dim-widget-icon-size: 60px;
|
--dim-widget-icon-size: 50px;
|
||||||
--dim-statusbar-height: 22px;
|
--dim-statusbar-height: 22px;
|
||||||
--dim-left-panel-width: 300px;
|
--dim-left-panel-width: 300px;
|
||||||
--dim-tabs-height: 33px;
|
--dim-tabs-height: 33px;
|
||||||
|
|||||||
@@ -14,7 +14,12 @@
|
|||||||
// import { shouldWaitForElectronInitialize } from './utility/getElectron';
|
// import { shouldWaitForElectronInitialize } from './utility/getElectron';
|
||||||
import { subscribeConnectionPingers } from './utility/connectionsPinger';
|
import { subscribeConnectionPingers } from './utility/connectionsPinger';
|
||||||
import { subscribePermissionCompiler } from './utility/hasPermission';
|
import { subscribePermissionCompiler } from './utility/hasPermission';
|
||||||
import { apiCall, installNewVolatileConnectionListener } from './utility/api';
|
import {
|
||||||
|
apiCall,
|
||||||
|
installNewCloudTokenListener,
|
||||||
|
installNewVolatileConnectionListener,
|
||||||
|
refreshPublicCloudFiles,
|
||||||
|
} from './utility/api';
|
||||||
import { getConfig, getSettings, getUsedApps } from './utility/metadataLoaders';
|
import { getConfig, getSettings, getUsedApps } from './utility/metadataLoaders';
|
||||||
import AppTitleProvider from './utility/AppTitleProvider.svelte';
|
import AppTitleProvider from './utility/AppTitleProvider.svelte';
|
||||||
import getElectron from './utility/getElectron';
|
import getElectron from './utility/getElectron';
|
||||||
@@ -23,6 +28,7 @@
|
|||||||
import { handleAuthOnStartup } from './clientAuth';
|
import { handleAuthOnStartup } from './clientAuth';
|
||||||
import { initializeAppUpdates } from './utility/appUpdate';
|
import { initializeAppUpdates } from './utility/appUpdate';
|
||||||
import { _t } from './translations';
|
import { _t } from './translations';
|
||||||
|
import { installCloudListeners } from './utility/cloudListeners';
|
||||||
|
|
||||||
export let isAdminPage = false;
|
export let isAdminPage = false;
|
||||||
|
|
||||||
@@ -51,9 +57,13 @@
|
|||||||
subscribeConnectionPingers();
|
subscribeConnectionPingers();
|
||||||
subscribePermissionCompiler();
|
subscribePermissionCompiler();
|
||||||
installNewVolatileConnectionListener();
|
installNewVolatileConnectionListener();
|
||||||
|
installNewCloudTokenListener();
|
||||||
initializeAppUpdates();
|
initializeAppUpdates();
|
||||||
|
installCloudListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshPublicCloudFiles();
|
||||||
|
|
||||||
loadedApi = loadedApiValue;
|
loadedApi = loadedApiValue;
|
||||||
|
|
||||||
if (!loadedApi) {
|
if (!loadedApi) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
export let groupFunc;
|
export let groupFunc;
|
||||||
export let items;
|
export let items;
|
||||||
export let groupIconFunc = plusExpandIcon;
|
export let groupIconFunc = plusExpandIcon;
|
||||||
|
export let mapGroupTitle = undefined;
|
||||||
export let module;
|
export let module;
|
||||||
export let checkedObjectsStore = null;
|
export let checkedObjectsStore = null;
|
||||||
export let disableContextMenu = false;
|
export let disableContextMenu = false;
|
||||||
@@ -63,7 +64,7 @@
|
|||||||
<FontIcon icon={groupIconFunc(isExpanded)} />
|
<FontIcon icon={groupIconFunc(isExpanded)} />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{group}
|
{mapGroupTitle ? mapGroupTitle(group) : group}
|
||||||
{items && `(${countText})`}
|
{items && `(${countText})`}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
export let groupIconFunc = plusExpandIcon;
|
export let groupIconFunc = plusExpandIcon;
|
||||||
export let groupFunc = undefined;
|
export let groupFunc = undefined;
|
||||||
|
export let mapGroupTitle = undefined;
|
||||||
export let onDropOnGroup = undefined;
|
export let onDropOnGroup = undefined;
|
||||||
export let emptyGroupNames = [];
|
export let emptyGroupNames = [];
|
||||||
export let isExpandedBySearch = false;
|
export let isExpandedBySearch = false;
|
||||||
@@ -127,6 +128,7 @@
|
|||||||
{subItemsComponent}
|
{subItemsComponent}
|
||||||
{checkedObjectsStore}
|
{checkedObjectsStore}
|
||||||
{groupFunc}
|
{groupFunc}
|
||||||
|
{mapGroupTitle}
|
||||||
{disableContextMenu}
|
{disableContextMenu}
|
||||||
{filter}
|
{filter}
|
||||||
{passProps}
|
{passProps}
|
||||||
|
|||||||
139
packages/web/src/appobj/CloudContentAppObject.svelte
Normal file
139
packages/web/src/appobj/CloudContentAppObject.svelte
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
import { cloudConnectionsStore } 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);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { filterName, getConnectionLabel } 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';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
export let passProps;
|
||||||
|
|
||||||
|
function createMenu() {
|
||||||
|
const res = [];
|
||||||
|
switch (data.type) {
|
||||||
|
case 'connection':
|
||||||
|
res.push({
|
||||||
|
text: _t('connection.connect', { defaultMessage: 'Connect' }),
|
||||||
|
onClick: handleConnect,
|
||||||
|
isBold: true,
|
||||||
|
});
|
||||||
|
res.push({ divider: true });
|
||||||
|
res.push({
|
||||||
|
text: _t('connection.edit', { defaultMessage: 'Edit' }),
|
||||||
|
onClick: handleEditConnection,
|
||||||
|
});
|
||||||
|
res.push({
|
||||||
|
text: _t('connection.delete', { defaultMessage: 'Delete' }),
|
||||||
|
onClick: handleDeleteConnection,
|
||||||
|
});
|
||||||
|
res.push({
|
||||||
|
text: _t('connection.duplicate', { defaultMessage: 'Duplicate' }),
|
||||||
|
onClick: handleDuplicateConnection,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditConnection() {
|
||||||
|
openNewTab({
|
||||||
|
title: data.name,
|
||||||
|
icon: 'img cloud-connection',
|
||||||
|
tabComponent: 'ConnectionTab',
|
||||||
|
props: {
|
||||||
|
conid: data.conid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteConnection() {
|
||||||
|
showModal(ConfirmModal, {
|
||||||
|
message: `Really delete connection ${data.name}?`,
|
||||||
|
onConfirm: () => {
|
||||||
|
apiCall('cloud/delete-connection', { conid: data.conid });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDuplicateConnection() {
|
||||||
|
await apiCall('cloud/duplicate-connection', { conid: data.conid });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConnect() {
|
||||||
|
const conn = await apiCall('connections/get', { conid: data.conid });
|
||||||
|
$cloudConnectionsStore = {
|
||||||
|
...$cloudConnectionsStore,
|
||||||
|
[data.conid]: conn,
|
||||||
|
};
|
||||||
|
openConnection(conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleOpenContent() {
|
||||||
|
switch (data.type) {
|
||||||
|
case 'connection':
|
||||||
|
await handleConnect();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if data.conid && $cloudConnectionsStore[data.conid]}
|
||||||
|
<ConnectionAppObject
|
||||||
|
{...$$restProps}
|
||||||
|
{passProps}
|
||||||
|
data={{
|
||||||
|
...$cloudConnectionsStore[data.conid],
|
||||||
|
status: data.status,
|
||||||
|
}}
|
||||||
|
on:dblclick
|
||||||
|
on:expand
|
||||||
|
/>
|
||||||
|
{:else if data.type == 'file'}
|
||||||
|
<SavedFileAppObject
|
||||||
|
{...$$restProps}
|
||||||
|
{passProps}
|
||||||
|
data={{
|
||||||
|
file: data.name,
|
||||||
|
folder: data.contentFolder,
|
||||||
|
folid: data.folid,
|
||||||
|
cntid: data.cntid,
|
||||||
|
}}
|
||||||
|
on:dblclick
|
||||||
|
on:expand
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<AppObjectCore
|
||||||
|
{...$$restProps}
|
||||||
|
{data}
|
||||||
|
icon={'img cloud-connection'}
|
||||||
|
title={data.name}
|
||||||
|
menu={createMenu}
|
||||||
|
on:click={handleOpenContent}
|
||||||
|
on:dblclick
|
||||||
|
on:expand
|
||||||
|
></AppObjectCore>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.info {
|
||||||
|
margin-left: 30px;
|
||||||
|
margin-right: 5px;
|
||||||
|
color: var(--theme-font-3);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -108,6 +108,7 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import AppObjectCore from './AppObjectCore.svelte';
|
import AppObjectCore from './AppObjectCore.svelte';
|
||||||
import {
|
import {
|
||||||
|
cloudSigninTokenHolder,
|
||||||
currentDatabase,
|
currentDatabase,
|
||||||
DEFAULT_CONNECTION_SEARCH_SETTINGS,
|
DEFAULT_CONNECTION_SEARCH_SETTINGS,
|
||||||
expandedConnections,
|
expandedConnections,
|
||||||
@@ -160,7 +161,7 @@
|
|||||||
const handleOpenConnectionTab = () => {
|
const handleOpenConnectionTab = () => {
|
||||||
openNewTab({
|
openNewTab({
|
||||||
title: getConnectionLabel(data),
|
title: getConnectionLabel(data),
|
||||||
icon: 'img connection',
|
icon: data._id.startsWith('cloud://') ? 'img cloud-connection' : 'img connection',
|
||||||
tabComponent: 'ConnectionTab',
|
tabComponent: 'ConnectionTab',
|
||||||
props: {
|
props: {
|
||||||
conid: data._id,
|
conid: data._id,
|
||||||
@@ -261,11 +262,15 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
const handleDuplicate = () => {
|
const handleDuplicate = () => {
|
||||||
apiCall('connections/save', {
|
if (data._id.startsWith('cloud://')) {
|
||||||
...data,
|
apiCall('cloud/duplicate-connection', { conid: data._id });
|
||||||
_id: undefined,
|
} else {
|
||||||
displayName: `${getConnectionLabel(data)} - copy`,
|
apiCall('connections/save', {
|
||||||
});
|
...data,
|
||||||
|
_id: undefined,
|
||||||
|
displayName: `${getConnectionLabel(data)} - copy`,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const handleCreateDatabase = () => {
|
const handleCreateDatabase = () => {
|
||||||
showModal(InputTextModal, {
|
showModal(InputTextModal, {
|
||||||
@@ -332,6 +337,19 @@
|
|||||||
text: _t('connection.duplicate', { defaultMessage: 'Duplicate' }),
|
text: _t('connection.duplicate', { defaultMessage: 'Duplicate' }),
|
||||||
onClick: handleDuplicate,
|
onClick: handleDuplicate,
|
||||||
},
|
},
|
||||||
|
!$openedConnections.includes(data._id) &&
|
||||||
|
$cloudSigninTokenHolder &&
|
||||||
|
passProps?.cloudContentList?.length > 0 && {
|
||||||
|
text: _t('connection.copyToCloudFolder', { defaultMessage: 'Copy to cloud folder' }),
|
||||||
|
submenu: passProps?.cloudContentList
|
||||||
|
?.filter(x => x.role == 'write' || x.role == 'admin')
|
||||||
|
?.map(fld => ({
|
||||||
|
text: fld.name,
|
||||||
|
onClick: () => {
|
||||||
|
apiCall('cloud/copy-connection-cloud', { conid: data._id, folid: fld.folid });
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
!data.singleDatabase && [
|
!data.singleDatabase && [
|
||||||
@@ -416,7 +434,7 @@
|
|||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
{data}
|
{data}
|
||||||
title={getConnectionLabel(data, { showUnsaved: true })}
|
title={getConnectionLabel(data, { showUnsaved: true })}
|
||||||
icon={data.singleDatabase ? 'img database' : 'img server'}
|
icon={data._id.startsWith('cloud://') ? 'img cloud-connection' : data.singleDatabase ? 'img database' : 'img server'}
|
||||||
isBold={data.singleDatabase
|
isBold={data.singleDatabase
|
||||||
? $currentDatabase?.connection?._id == data._id && $currentDatabase?.name == data.defaultDatabase
|
? $currentDatabase?.connection?._id == data._id && $currentDatabase?.name == data.defaultDatabase
|
||||||
: $currentDatabase?.connection?._id == data._id}
|
: $currentDatabase?.connection?._id == data._id}
|
||||||
|
|||||||
52
packages/web/src/appobj/PublicCloudFileAppObject.svelte
Normal file
52
packages/web/src/appobj/PublicCloudFileAppObject.svelte
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
import AppObjectCore from './AppObjectCore.svelte';
|
||||||
|
|
||||||
|
export const extractKey = data => data.path;
|
||||||
|
export const createMatcher =
|
||||||
|
filter =>
|
||||||
|
({ title, description }) =>
|
||||||
|
filterName(filter, title, description);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { apiCall } from '../utility/api';
|
||||||
|
import newQuery from '../query/newQuery';
|
||||||
|
import { filterName } from 'dbgate-tools';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
async function handleOpenSqlFile() {
|
||||||
|
const fileData = await apiCall('cloud/public-file-data', { path: data.path });
|
||||||
|
newQuery({
|
||||||
|
initialData: fileData.text,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMenu() {
|
||||||
|
return [{ text: 'Open', onClick: handleOpenSqlFile }];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AppObjectCore
|
||||||
|
{...$$restProps}
|
||||||
|
{data}
|
||||||
|
icon={'img sql-file'}
|
||||||
|
title={data.title}
|
||||||
|
menu={createMenu}
|
||||||
|
on:click={handleOpenSqlFile}
|
||||||
|
>
|
||||||
|
{#if data.description}
|
||||||
|
<div class="info">
|
||||||
|
{data.description}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</AppObjectCore>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.info {
|
||||||
|
margin-left: 30px;
|
||||||
|
margin-right: 5px;
|
||||||
|
color: var(--theme-font-3);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -206,7 +206,14 @@
|
|||||||
showModal(ConfirmModal, {
|
showModal(ConfirmModal, {
|
||||||
message: `Really delete file ${data.file}?`,
|
message: `Really delete file ${data.file}?`,
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
apiCall('files/delete', data);
|
if (data.folid && data.cntid) {
|
||||||
|
apiCall('cloud/delete-content', {
|
||||||
|
folid: data.folid,
|
||||||
|
cntid: data.cntid,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
apiCall('files/delete', data);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -217,7 +224,15 @@
|
|||||||
label: 'New file name',
|
label: 'New file name',
|
||||||
header: 'Rename file',
|
header: 'Rename file',
|
||||||
onConfirm: newFile => {
|
onConfirm: newFile => {
|
||||||
apiCall('files/rename', { ...data, newFile });
|
if (data.folid && data.cntid) {
|
||||||
|
apiCall('cloud/rename-content', {
|
||||||
|
folid: data.folid,
|
||||||
|
cntid: data.cntid,
|
||||||
|
name: newFile,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
apiCall('files/rename', { ...data, newFile });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -226,9 +241,17 @@
|
|||||||
showModal(InputTextModal, {
|
showModal(InputTextModal, {
|
||||||
value: data.file,
|
value: data.file,
|
||||||
label: 'New file name',
|
label: 'New file name',
|
||||||
header: 'Rename file',
|
header: 'Copy file',
|
||||||
onConfirm: newFile => {
|
onConfirm: newFile => {
|
||||||
apiCall('files/copy', { ...data, newFile });
|
if (data.folid && data.cntid) {
|
||||||
|
apiCall('cloud/copy-file', {
|
||||||
|
folid: data.folid,
|
||||||
|
cntid: data.cntid,
|
||||||
|
name: newFile,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
apiCall('files/copy', { ...data, newFile });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -236,21 +259,38 @@
|
|||||||
const handleDownload = () => {
|
const handleDownload = () => {
|
||||||
saveFileToDisk(
|
saveFileToDisk(
|
||||||
async filePath => {
|
async filePath => {
|
||||||
await apiCall('files/export-file', {
|
if (data.folid && data.cntid) {
|
||||||
folder,
|
await apiCall('cloud/export-file', {
|
||||||
file: data.file,
|
folid: data.folid,
|
||||||
filePath,
|
cntid: data.cntid,
|
||||||
});
|
filePath,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await apiCall('files/export-file', {
|
||||||
|
folder,
|
||||||
|
file: data.file,
|
||||||
|
filePath,
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ formatLabel: handler.label, formatExtension: handler.format, defaultFileName: data.file }
|
{ formatLabel: handler.label, formatExtension: handler.format, defaultFileName: data.file }
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
async function openTab() {
|
async function openTab() {
|
||||||
const resp = await apiCall('files/load', { folder, file: data.file, format: handler.format });
|
let dataContent;
|
||||||
|
if (data.folid && data.cntid) {
|
||||||
|
const resp = await apiCall('cloud/get-content', {
|
||||||
|
folid: data.folid,
|
||||||
|
cntid: data.cntid,
|
||||||
|
});
|
||||||
|
dataContent = resp.content;
|
||||||
|
} else {
|
||||||
|
dataContent = await apiCall('files/load', { folder, file: data.file, format: handler.format });
|
||||||
|
}
|
||||||
|
|
||||||
const connProps: any = {};
|
|
||||||
let tooltip = undefined;
|
let tooltip = undefined;
|
||||||
|
const connProps: any = {};
|
||||||
|
|
||||||
if (handler.currentConnection) {
|
if (handler.currentConnection) {
|
||||||
const connection = _.get($currentDatabase, 'connection') || {};
|
const connection = _.get($currentDatabase, 'connection') || {};
|
||||||
@@ -270,10 +310,12 @@
|
|||||||
savedFile: data.file,
|
savedFile: data.file,
|
||||||
savedFolder: handler.folder,
|
savedFolder: handler.folder,
|
||||||
savedFormat: handler.format,
|
savedFormat: handler.format,
|
||||||
|
savedCloudFolderId: data.folid,
|
||||||
|
savedCloudContentId: data.cntid,
|
||||||
...connProps,
|
...connProps,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ editor: resp }
|
{ editor: dataContent }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
10
packages/web/src/appobj/SubCloudItemsList.svelte
Normal file
10
packages/web/src/appobj/SubCloudItemsList.svelte
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cloudConnectionsStore } from '../stores';
|
||||||
|
import SubDatabaseList from './SubDatabaseList.svelte';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if data.conid && $cloudConnectionsStore[data.conid]}
|
||||||
|
<SubDatabaseList {...$$props} data={$cloudConnectionsStore[data.conid]} />
|
||||||
|
{/if}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
cloudSigninTokenHolder,
|
||||||
currentDatabase,
|
currentDatabase,
|
||||||
currentTheme,
|
currentTheme,
|
||||||
emptyConnectionGroupNames,
|
emptyConnectionGroupNames,
|
||||||
@@ -123,6 +124,27 @@ registerCommand({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerCommand({
|
||||||
|
id: 'new.connectionOnCloud',
|
||||||
|
toolbar: true,
|
||||||
|
icon: 'img cloud-connection',
|
||||||
|
toolbarName: 'Add connection on cloud',
|
||||||
|
category: 'New',
|
||||||
|
toolbarOrder: 1,
|
||||||
|
name: 'Connection on Cloud',
|
||||||
|
testEnabled: () => !getCurrentConfig()?.runAsPortal && !getCurrentConfig()?.storageDatabase && isProApp(),
|
||||||
|
onClick: () => {
|
||||||
|
openNewTab({
|
||||||
|
title: 'New Connection on Cloud',
|
||||||
|
icon: 'img cloud-connection',
|
||||||
|
tabComponent: 'ConnectionTab',
|
||||||
|
props: {
|
||||||
|
saveOnCloud: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
registerCommand({
|
registerCommand({
|
||||||
id: 'new.connection.folder',
|
id: 'new.connection.folder',
|
||||||
toolbar: true,
|
toolbar: true,
|
||||||
@@ -662,6 +684,15 @@ if (hasPermission('settings/change')) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerCommand({
|
||||||
|
id: 'cloud.logout',
|
||||||
|
category: 'Cloud',
|
||||||
|
name: 'Logout',
|
||||||
|
onClick: () => {
|
||||||
|
cloudSigninTokenHolder.set(null);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
registerCommand({
|
registerCommand({
|
||||||
id: 'file.exit',
|
id: 'file.exit',
|
||||||
category: 'File',
|
category: 'File',
|
||||||
|
|||||||
27
packages/web/src/forms/FormCloudFolderSelect.svelte
Normal file
27
packages/web/src/forms/FormCloudFolderSelect.svelte
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { useCloudContentList } from '../utility/metadataLoaders';
|
||||||
|
|
||||||
|
import FormSelectField from './FormSelectField.svelte';
|
||||||
|
|
||||||
|
export let name;
|
||||||
|
export let requiredRoleVariants = ['read', 'write', 'admin'];
|
||||||
|
|
||||||
|
export let prependFolders = [];
|
||||||
|
|
||||||
|
const cloudContentList = useCloudContentList();
|
||||||
|
|
||||||
|
$: folderOptions = [
|
||||||
|
...prependFolders.map(folder => ({
|
||||||
|
value: folder.folid,
|
||||||
|
label: folder.name,
|
||||||
|
})),
|
||||||
|
...($cloudContentList || [])
|
||||||
|
.filter(folder => requiredRoleVariants.find(role => folder.role == role))
|
||||||
|
.map(folder => ({
|
||||||
|
value: folder.folid,
|
||||||
|
label: folder.name,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormSelectField {...$$props} options={folderOptions} />
|
||||||
@@ -39,6 +39,9 @@
|
|||||||
'icon minus-thick': 'mdi mdi-minus-thick',
|
'icon minus-thick': 'mdi mdi-minus-thick',
|
||||||
'icon invisible-box': 'mdi mdi-minus-box-outline icon-invisible',
|
'icon invisible-box': 'mdi mdi-minus-box-outline icon-invisible',
|
||||||
'icon cloud-upload': 'mdi mdi-cloud-upload',
|
'icon cloud-upload': 'mdi mdi-cloud-upload',
|
||||||
|
'icon cloud': 'mdi mdi-cloud',
|
||||||
|
'icon cloud-public': 'mdi mdi-cloud-search',
|
||||||
|
'icon cloud-private': 'mdi mdi-cloud-key',
|
||||||
'icon import': 'mdi mdi-application-import',
|
'icon import': 'mdi mdi-application-import',
|
||||||
'icon export': 'mdi mdi-application-export',
|
'icon export': 'mdi mdi-application-export',
|
||||||
'icon new-connection': 'mdi mdi-database-plus',
|
'icon new-connection': 'mdi mdi-database-plus',
|
||||||
@@ -112,6 +115,9 @@
|
|||||||
'icon square': 'mdi mdi-square',
|
'icon square': 'mdi mdi-square',
|
||||||
'icon data-deploy': 'mdi mdi-database-settings',
|
'icon data-deploy': 'mdi mdi-database-settings',
|
||||||
|
|
||||||
|
'icon cloud-account': 'mdi mdi-account-remove-outline',
|
||||||
|
'icon cloud-account-connected': 'mdi mdi-account-check-outline',
|
||||||
|
|
||||||
'icon edit': 'mdi mdi-pencil',
|
'icon edit': 'mdi mdi-pencil',
|
||||||
'icon delete': 'mdi mdi-delete',
|
'icon delete': 'mdi mdi-delete',
|
||||||
'icon arrow-up': 'mdi mdi-arrow-up',
|
'icon arrow-up': 'mdi mdi-arrow-up',
|
||||||
@@ -264,6 +270,7 @@
|
|||||||
'img role': 'mdi mdi-account-group color-icon-blue',
|
'img role': 'mdi mdi-account-group color-icon-blue',
|
||||||
'img admin': 'mdi mdi-security color-icon-blue',
|
'img admin': 'mdi mdi-security color-icon-blue',
|
||||||
'img auth': 'mdi mdi-account-key color-icon-blue',
|
'img auth': 'mdi mdi-account-key color-icon-blue',
|
||||||
|
'img cloud-connection': 'mdi mdi-cloud-lock color-icon-blue',
|
||||||
|
|
||||||
'img add': 'mdi mdi-plus-circle color-icon-green',
|
'img add': 'mdi mdi-plus-circle color-icon-green',
|
||||||
'img minus': 'mdi mdi-minus-circle color-icon-red',
|
'img minus': 'mdi mdi-minus-circle color-icon-red',
|
||||||
|
|||||||
40
packages/web/src/modals/ChooseCloudFolderModal.svelte
Normal file
40
packages/web/src/modals/ChooseCloudFolderModal.svelte
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||||
|
import FormCloudFolderSelect from '../forms/FormCloudFolderSelect.svelte';
|
||||||
|
|
||||||
|
import FormProvider from '../forms/FormProvider.svelte';
|
||||||
|
import FormSubmit from '../forms/FormSubmit.svelte';
|
||||||
|
import { useCloudContentList } from '../utility/metadataLoaders';
|
||||||
|
import ModalBase from './ModalBase.svelte';
|
||||||
|
import { closeCurrentModal } from './modalTools';
|
||||||
|
|
||||||
|
export let message = '';
|
||||||
|
export let onConfirm;
|
||||||
|
export let requiredRoleVariants;
|
||||||
|
|
||||||
|
const cloudContentList = useCloudContentList();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $cloudContentList}
|
||||||
|
<FormProvider initialValues={{ cloudFolder: $cloudContentList?.find(x => x.isPrivate)?.folid }}>
|
||||||
|
<ModalBase {...$$restProps}>
|
||||||
|
<svelte:fragment slot="header">Choose cloud folder</svelte:fragment>
|
||||||
|
|
||||||
|
<div>{message}</div>
|
||||||
|
|
||||||
|
<FormCloudFolderSelect label="Cloud folder" name="cloudFolder" isNative {requiredRoleVariants} />
|
||||||
|
|
||||||
|
<svelte:fragment slot="footer">
|
||||||
|
<FormSubmit
|
||||||
|
value="OK"
|
||||||
|
on:click={e => {
|
||||||
|
closeCurrentModal();
|
||||||
|
console.log('onConfirm', e.detail);
|
||||||
|
onConfirm(e.detail.cloudFolder);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormStyledButton type="button" value="Close" on:click={closeCurrentModal} />
|
||||||
|
</svelte:fragment>
|
||||||
|
</ModalBase>
|
||||||
|
</FormProvider>
|
||||||
|
{/if}
|
||||||
71
packages/web/src/modals/LicenseLimitMessageModal.svelte
Normal file
71
packages/web/src/modals/LicenseLimitMessageModal.svelte
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<script>
|
||||||
|
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||||
|
import FormProvider from '../forms/FormProvider.svelte';
|
||||||
|
import FormSubmit from '../forms/FormSubmit.svelte';
|
||||||
|
import FontIcon from '../icons/FontIcon.svelte';
|
||||||
|
import { isProApp } from '../utility/proTools';
|
||||||
|
import { openWebLink } from '../utility/simpleTools';
|
||||||
|
|
||||||
|
import ModalBase from './ModalBase.svelte';
|
||||||
|
import { closeCurrentModal } from './modalTools';
|
||||||
|
|
||||||
|
export let message;
|
||||||
|
export let licenseLimits;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormProvider>
|
||||||
|
<ModalBase {...$$restProps}>
|
||||||
|
<div slot="header">License limit error</div>
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="icon">
|
||||||
|
<FontIcon icon="img error" />
|
||||||
|
</div>
|
||||||
|
<div data-testid="LicenseLimitMessageModal_message">
|
||||||
|
<p>
|
||||||
|
Cloud operation ended with error:<br />
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This is a limitation of the free version of DbGate. To continue using cloud operations, please {#if !isProApp()}download
|
||||||
|
and{/if} purchase DbGate Premium.
|
||||||
|
</p>
|
||||||
|
<p>Free version limit:</p>
|
||||||
|
<ul>
|
||||||
|
{#each licenseLimits || [] as limit}
|
||||||
|
<li>{limit}</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div slot="footer">
|
||||||
|
<FormSubmit value="Close" on:click={closeCurrentModal} data-testid="LicenseLimitMessageModal_closeButton" />
|
||||||
|
{#if !isProApp()}
|
||||||
|
<FormStyledButton
|
||||||
|
value="Download DbGate Premium"
|
||||||
|
on:click={() => openWebLink('https://dbgate.io/download/')}
|
||||||
|
skipWidth
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<FormStyledButton
|
||||||
|
value="Purchase DbGate Premium"
|
||||||
|
on:click={() => openWebLink('https://dbgate.io/purchase/premium/')}
|
||||||
|
skipWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ModalBase>
|
||||||
|
</FormProvider>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: 20pt;
|
||||||
|
padding-top: 30px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||||
|
|
||||||
import FormProvider from '../forms/FormProvider.svelte';
|
import FormProviderCore from '../forms/FormProviderCore.svelte';
|
||||||
import FormSubmit from '../forms/FormSubmit.svelte';
|
import FormSubmit from '../forms/FormSubmit.svelte';
|
||||||
import FormTextField from '../forms/FormTextField.svelte';
|
import FormTextField from '../forms/FormTextField.svelte';
|
||||||
|
import { cloudSigninTokenHolder } from '../stores';
|
||||||
import { _t } from '../translations';
|
import { _t } from '../translations';
|
||||||
import { apiCall } from '../utility/api';
|
import { apiCall } from '../utility/api';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
import getElectron from '../utility/getElectron';
|
import getElectron from '../utility/getElectron';
|
||||||
import ModalBase from './ModalBase.svelte';
|
import ModalBase from './ModalBase.svelte';
|
||||||
import { closeCurrentModal } from './modalTools';
|
import { closeCurrentModal, showModal } from './modalTools';
|
||||||
|
import FormCloudFolderSelect from '../forms/FormCloudFolderSelect.svelte';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
export let name;
|
export let name;
|
||||||
@@ -18,19 +21,48 @@
|
|||||||
export let fileExtension;
|
export let fileExtension;
|
||||||
export let filePath;
|
export let filePath;
|
||||||
export let onSave = undefined;
|
export let onSave = undefined;
|
||||||
|
export let folid;
|
||||||
|
// export let cntid;
|
||||||
|
|
||||||
|
const values = writable({ name, cloudFolder: folid ?? '__local' });
|
||||||
|
|
||||||
const electron = getElectron();
|
const electron = getElectron();
|
||||||
|
|
||||||
const handleSubmit = async e => {
|
const handleSubmit = async e => {
|
||||||
const { name } = e.detail;
|
const { name, cloudFolder } = e.detail;
|
||||||
await apiCall('files/save', { folder, file: name, data, format });
|
if (cloudFolder === '__local') {
|
||||||
closeCurrentModal();
|
await apiCall('files/save', { folder, file: name, data, format });
|
||||||
if (onSave) {
|
closeCurrentModal();
|
||||||
onSave(name, {
|
if (onSave) {
|
||||||
savedFile: name,
|
onSave(name, {
|
||||||
savedFolder: folder,
|
savedFile: name,
|
||||||
savedFilePath: null,
|
savedFolder: folder,
|
||||||
|
savedFilePath: null,
|
||||||
|
savedCloudFolderId: null,
|
||||||
|
savedCloudContentId: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const resp = await apiCall('cloud/save-file', {
|
||||||
|
folid: cloudFolder,
|
||||||
|
fileName: name,
|
||||||
|
data,
|
||||||
|
contentFolder: folder,
|
||||||
|
format,
|
||||||
|
// cntid,
|
||||||
});
|
});
|
||||||
|
if (resp.cntid) {
|
||||||
|
closeCurrentModal();
|
||||||
|
if (onSave) {
|
||||||
|
onSave(name, {
|
||||||
|
savedFile: name,
|
||||||
|
savedFolder: folder,
|
||||||
|
savedFilePath: null,
|
||||||
|
savedCloudFolderId: cloudFolder,
|
||||||
|
// savedCloudContentId: resp.cntid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,15 +79,32 @@
|
|||||||
savedFile: null,
|
savedFile: null,
|
||||||
savedFolder: null,
|
savedFolder: null,
|
||||||
savedFilePath: filePath,
|
savedFilePath: filePath,
|
||||||
|
savedCloudFolderId: null,
|
||||||
|
savedCloudContentId: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FormProvider initialValues={{ name }}>
|
<FormProviderCore {values}>
|
||||||
<ModalBase {...$$restProps}>
|
<ModalBase {...$$restProps}>
|
||||||
<svelte:fragment slot="header">Save file</svelte:fragment>
|
<svelte:fragment slot="header">Save file</svelte:fragment>
|
||||||
<FormTextField label="File name" name="name" focused />
|
<FormTextField label="File name" name="name" focused />
|
||||||
|
{#if $cloudSigninTokenHolder}
|
||||||
|
<FormCloudFolderSelect
|
||||||
|
label="Choose cloud folder"
|
||||||
|
name="cloudFolder"
|
||||||
|
isNative
|
||||||
|
requiredRoleVariants={['write', 'admin']}
|
||||||
|
prependFolders={[
|
||||||
|
{
|
||||||
|
folid: '__local',
|
||||||
|
name: "Local folder (don't store on cloud)",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<svelte:fragment slot="footer">
|
<svelte:fragment slot="footer">
|
||||||
<FormSubmit value={_t('common.save', { defaultMessage: 'Save' })} on:click={handleSubmit} />
|
<FormSubmit value={_t('common.save', { defaultMessage: 'Save' })} on:click={handleSubmit} />
|
||||||
{#if electron}
|
{#if electron}
|
||||||
@@ -79,4 +128,4 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</ModalBase>
|
</ModalBase>
|
||||||
</FormProvider>
|
</FormProviderCore>
|
||||||
|
|||||||
@@ -182,6 +182,10 @@ export const focusedConnectionOrDatabase = writable<{ conid: string; database?:
|
|||||||
|
|
||||||
export const focusedTreeDbKey = writable<{ key: string; root: string; type: string; text: string }>(null);
|
export const focusedTreeDbKey = writable<{ key: string; root: string; type: string; text: string }>(null);
|
||||||
|
|
||||||
|
export const cloudSigninTokenHolder = writableSettingsValue(null, 'cloudSigninTokenHolder');
|
||||||
|
|
||||||
|
export const cloudConnectionsStore = writable({});
|
||||||
|
|
||||||
export const DEFAULT_OBJECT_SEARCH_SETTINGS = {
|
export const DEFAULT_OBJECT_SEARCH_SETTINGS = {
|
||||||
pureName: true,
|
pureName: true,
|
||||||
schemaName: false,
|
schemaName: false,
|
||||||
@@ -453,4 +457,10 @@ focusedTreeDbKey.subscribe(value => {
|
|||||||
});
|
});
|
||||||
export const getFocusedTreeDbKey = () => focusedTreeDbKeyValue;
|
export const getFocusedTreeDbKey = () => focusedTreeDbKeyValue;
|
||||||
|
|
||||||
|
let cloudConnectionsStoreValue = {};
|
||||||
|
cloudConnectionsStore.subscribe(value => {
|
||||||
|
cloudConnectionsStoreValue = value;
|
||||||
|
});
|
||||||
|
export const getCloudConnectionsStore = () => cloudConnectionsStoreValue;
|
||||||
|
|
||||||
window['__changeCurrentTheme'] = theme => currentTheme.set(theme);
|
window['__changeCurrentTheme'] = theme => currentTheme.set(theme);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
import ConnectionAdvancedDriverFields from '../settings/ConnectionAdvancedDriverFields.svelte';
|
import ConnectionAdvancedDriverFields from '../settings/ConnectionAdvancedDriverFields.svelte';
|
||||||
import DatabaseLoginModal from '../modals/DatabaseLoginModal.svelte';
|
import DatabaseLoginModal from '../modals/DatabaseLoginModal.svelte';
|
||||||
import { _t } from '../translations';
|
import { _t } from '../translations';
|
||||||
|
import ChooseCloudFolderModal from '../modals/ChooseCloudFolderModal.svelte';
|
||||||
|
|
||||||
export let connection;
|
export let connection;
|
||||||
export let tabid;
|
export let tabid;
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
export let inlineTabs = false;
|
export let inlineTabs = false;
|
||||||
|
|
||||||
export let onlyTestButton;
|
export let onlyTestButton;
|
||||||
|
export let saveOnCloud = false;
|
||||||
|
|
||||||
let isTesting;
|
let isTesting;
|
||||||
let sqlConnectResult;
|
let sqlConnectResult;
|
||||||
@@ -157,43 +159,96 @@
|
|||||||
$: currentConnection = getCurrentConnectionCore($values, driver);
|
$: currentConnection = getCurrentConnectionCore($values, driver);
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
let connection = getCurrentConnection();
|
if (saveOnCloud && !getCurrentConnection()?._id) {
|
||||||
connection = {
|
showModal(ChooseCloudFolderModal, {
|
||||||
...connection,
|
requiredRoleVariants: ['write', 'admin'],
|
||||||
unsaved: false,
|
message: 'Choose cloud folder to saved connection',
|
||||||
};
|
onConfirm: async folid => {
|
||||||
const saved = await apiCall('connections/save', connection);
|
let connection = getCurrentConnection();
|
||||||
$values = {
|
const saved = await apiCall('cloud/save-connection', { folid, connection });
|
||||||
...$values,
|
if (saved?._id) {
|
||||||
_id: saved._id,
|
$values = {
|
||||||
unsaved: false,
|
...$values,
|
||||||
};
|
_id: saved._id,
|
||||||
changeTab(tabid, tab => ({
|
unsaved: false,
|
||||||
...tab,
|
};
|
||||||
title: getConnectionLabel(saved),
|
changeTab(tabid, tab => ({
|
||||||
props: {
|
...tab,
|
||||||
...tab.props,
|
title: getConnectionLabel(saved),
|
||||||
conid: saved._id,
|
props: {
|
||||||
},
|
...tab.props,
|
||||||
}));
|
conid: saved._id,
|
||||||
showSnackbarSuccess('Connection saved');
|
},
|
||||||
|
}));
|
||||||
|
showSnackbarSuccess('Connection saved');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
// @ts-ignore
|
||||||
|
getCurrentConnection()?._id?.startsWith('cloud://')
|
||||||
|
) {
|
||||||
|
let connection = getCurrentConnection();
|
||||||
|
const resp = await apiCall('cloud/save-connection', { connection });
|
||||||
|
if (resp?._id) {
|
||||||
|
showSnackbarSuccess('Connection saved');
|
||||||
|
changeTab(tabid, tab => ({
|
||||||
|
...tab,
|
||||||
|
title: getConnectionLabel(connection),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let connection = getCurrentConnection();
|
||||||
|
connection = {
|
||||||
|
...connection,
|
||||||
|
unsaved: false,
|
||||||
|
};
|
||||||
|
const saved = await apiCall('connections/save', connection);
|
||||||
|
$values = {
|
||||||
|
...$values,
|
||||||
|
_id: saved._id,
|
||||||
|
unsaved: false,
|
||||||
|
};
|
||||||
|
changeTab(tabid, tab => ({
|
||||||
|
...tab,
|
||||||
|
title: getConnectionLabel(saved),
|
||||||
|
props: {
|
||||||
|
...tab.props,
|
||||||
|
conid: saved._id,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
showSnackbarSuccess('Connection saved');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleConnect() {
|
async function handleConnect() {
|
||||||
let connection = getCurrentConnection();
|
let connection = getCurrentConnection();
|
||||||
if (!connection._id) {
|
|
||||||
connection = {
|
if (
|
||||||
...connection,
|
// @ts-ignore
|
||||||
unsaved: true,
|
connection?._id?.startsWith('cloud://')
|
||||||
|
) {
|
||||||
|
const saved = await apiCall('cloud/save-connection', { connection });
|
||||||
|
changeTab(tabid, tab => ({
|
||||||
|
...tab,
|
||||||
|
title: getConnectionLabel(connection),
|
||||||
|
}));
|
||||||
|
openConnection(saved);
|
||||||
|
} else {
|
||||||
|
if (!connection._id) {
|
||||||
|
connection = {
|
||||||
|
...connection,
|
||||||
|
unsaved: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const saved = await apiCall('connections/save', connection);
|
||||||
|
$values = {
|
||||||
|
...$values,
|
||||||
|
unsaved: connection.unsaved,
|
||||||
|
_id: saved._id,
|
||||||
};
|
};
|
||||||
|
openConnection(saved);
|
||||||
}
|
}
|
||||||
const saved = await apiCall('connections/save', connection);
|
|
||||||
$values = {
|
|
||||||
...$values,
|
|
||||||
unsaved: connection.unsaved,
|
|
||||||
_id: saved._id,
|
|
||||||
};
|
|
||||||
openConnection(saved);
|
|
||||||
// closeMultipleTabs(x => x.tabid == tabid, true);
|
// closeMultipleTabs(x => x.tabid == tabid, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +342,9 @@
|
|||||||
{:else if isConnected}
|
{:else if isConnected}
|
||||||
<FormButton value="Disconnect" on:click={handleDisconnect} data-testid="ConnectionTab_buttonDisconnect" />
|
<FormButton value="Disconnect" on:click={handleDisconnect} data-testid="ConnectionTab_buttonDisconnect" />
|
||||||
{:else}
|
{:else}
|
||||||
<FormButton value="Connect" on:click={handleConnect} data-testid="ConnectionTab_buttonConnect" />
|
{#if $values._id || !saveOnCloud}
|
||||||
|
<FormButton value="Connect" on:click={handleConnect} data-testid="ConnectionTab_buttonConnect" />
|
||||||
|
{/if}
|
||||||
{#if isTesting}
|
{#if isTesting}
|
||||||
<FormButton value="Cancel test" on:click={handleCancelTest} />
|
<FormButton value="Cancel test" on:click={handleCancelTest} />
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { batchDispatchCacheTriggers, dispatchCacheChange } from './cache';
|
|||||||
import { isAdminPage, isOneOfPage } from './pageDefs';
|
import { isAdminPage, isOneOfPage } from './pageDefs';
|
||||||
import { openWebLink } from './simpleTools';
|
import { openWebLink } from './simpleTools';
|
||||||
import { serializeJsTypesReplacer } from 'dbgate-tools';
|
import { serializeJsTypesReplacer } from 'dbgate-tools';
|
||||||
|
import { cloudSigninTokenHolder } from '../stores';
|
||||||
|
import LicenseLimitMessageModal from '../modals/LicenseLimitMessageModal.svelte';
|
||||||
|
|
||||||
export const strmid = uuidv1();
|
export const strmid = uuidv1();
|
||||||
|
|
||||||
@@ -120,7 +122,14 @@ async function processApiResponse(route, args, resp) {
|
|||||||
// missingCredentials: true,
|
// missingCredentials: true,
|
||||||
// };
|
// };
|
||||||
} else if (resp?.apiErrorMessage) {
|
} else if (resp?.apiErrorMessage) {
|
||||||
showSnackbarError('API error:' + resp?.apiErrorMessage);
|
if (resp?.apiErrorIsLicenseLimit) {
|
||||||
|
showModal(LicenseLimitMessageModal, {
|
||||||
|
message: resp.apiErrorMessage,
|
||||||
|
licenseLimits: resp.apiErrorLimitedLicenseLimits,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showSnackbarError('API error:' + resp?.apiErrorMessage);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
errorMessage: resp.apiErrorMessage,
|
errorMessage: resp.apiErrorMessage,
|
||||||
};
|
};
|
||||||
@@ -279,6 +288,13 @@ export function installNewVolatileConnectionListener() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function installNewCloudTokenListener() {
|
||||||
|
apiOn('got-cloud-token', async tokenHolder => {
|
||||||
|
console.log('HOLDER', tokenHolder);
|
||||||
|
cloudSigninTokenHolder.set(tokenHolder);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function getAuthCategory(config) {
|
export function getAuthCategory(config) {
|
||||||
if (config.isBasicAuth) {
|
if (config.isBasicAuth) {
|
||||||
return 'basic';
|
return 'basic';
|
||||||
@@ -292,6 +308,15 @@ export function getAuthCategory(config) {
|
|||||||
return 'token';
|
return 'token';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function refreshPublicCloudFiles() {
|
||||||
|
if (sessionStorage.getItem('publicCloudFilesLoaded')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
apiCall('cloud/refresh-public-files');
|
||||||
|
sessionStorage.setItem('publicCloudFilesLoaded', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
function enableApiLog() {
|
function enableApiLog() {
|
||||||
apiLogging = true;
|
apiLogging = true;
|
||||||
console.log('API loggin enabled');
|
console.log('API loggin enabled');
|
||||||
|
|||||||
57
packages/web/src/utility/cloudListeners.ts
Normal file
57
packages/web/src/utility/cloudListeners.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { derived } from 'svelte/store';
|
||||||
|
import {
|
||||||
|
cloudConnectionsStore,
|
||||||
|
currentDatabase,
|
||||||
|
getCloudConnectionsStore,
|
||||||
|
openedConnections,
|
||||||
|
openedSingleDatabaseConnections,
|
||||||
|
} from '../stores';
|
||||||
|
import { apiCall, apiOn } from './api';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export const possibleCloudConnectionSources = derived(
|
||||||
|
[currentDatabase, openedSingleDatabaseConnections, openedConnections],
|
||||||
|
([$currentDatabase, $openedSingleDatabaseConnections, $openedConnections]) => {
|
||||||
|
const conids = new Set<string>();
|
||||||
|
if ($currentDatabase?.connection?._id) {
|
||||||
|
conids.add($currentDatabase.connection._id);
|
||||||
|
}
|
||||||
|
$openedSingleDatabaseConnections.forEach(x => conids.add(x));
|
||||||
|
$openedConnections.forEach(x => conids.add(x));
|
||||||
|
return Array.from(conids).filter(x => x?.startsWith('cloud://'));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
async function loadCloudConnection(conid) {
|
||||||
|
const conn = await apiCall('connections/get', { conid });
|
||||||
|
cloudConnectionsStore.update(store => ({
|
||||||
|
...store,
|
||||||
|
[conid]: conn,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureCloudConnectionsLoaded(...conids) {
|
||||||
|
const conns = getCloudConnectionsStore();
|
||||||
|
|
||||||
|
cloudConnectionsStore.update(store => _.pick(store, conids));
|
||||||
|
|
||||||
|
conids.forEach(conid => {
|
||||||
|
if (!conns[conid]) {
|
||||||
|
loadCloudConnection(conid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function installCloudListeners() {
|
||||||
|
possibleCloudConnectionSources.subscribe(conids => {
|
||||||
|
ensureCloudConnectionsLoaded(...conids);
|
||||||
|
});
|
||||||
|
|
||||||
|
apiOn('cloud-content-updated', () => {
|
||||||
|
const conids = Object.keys(getCloudConnectionsStore());
|
||||||
|
cloudConnectionsStore.set({});
|
||||||
|
for (const conn of conids) {
|
||||||
|
loadCloudConnection(conn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -166,6 +166,17 @@ const authTypesLoader = ({ engine }) => ({
|
|||||||
errorValue: null,
|
errorValue: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const publicCloudFilesLoader = () => ({
|
||||||
|
url: 'cloud/public-files',
|
||||||
|
params: {},
|
||||||
|
reloadTrigger: { key: `public-cloud-changed` },
|
||||||
|
});
|
||||||
|
const cloudContentListLoader = () => ({
|
||||||
|
url: 'cloud/content-list',
|
||||||
|
params: {},
|
||||||
|
reloadTrigger: { key: `cloud-content-changed` },
|
||||||
|
});
|
||||||
|
|
||||||
async function getCore(loader, args) {
|
async function getCore(loader, args) {
|
||||||
const { url, params, reloadTrigger, transform, onLoaded, errorValue } = loader(args);
|
const { url, params, reloadTrigger, transform, onLoaded, errorValue } = loader(args);
|
||||||
const key = stableStringify({ url, ...params });
|
const key = stableStringify({ url, ...params });
|
||||||
@@ -456,3 +467,17 @@ export function getSchemaList(args) {
|
|||||||
export function useSchemaList(args) {
|
export function useSchemaList(args) {
|
||||||
return useCore(schemaListLoader, args);
|
return useCore(schemaListLoader, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getPublicCloudFiles(args) {
|
||||||
|
return getCore(publicCloudFilesLoader, args);
|
||||||
|
}
|
||||||
|
export function usePublicCloudFiles(args = {}) {
|
||||||
|
return useCore(publicCloudFilesLoader, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCloudContentList(args) {
|
||||||
|
return getCore(cloudContentListLoader, args);
|
||||||
|
}
|
||||||
|
export function useCloudContentList(args = {}) {
|
||||||
|
return useCore(cloudContentListLoader, args);
|
||||||
|
}
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ async function openSavedElectronFile(filePath, parsed, folder) {
|
|||||||
props: {
|
props: {
|
||||||
savedFile: null,
|
savedFile: null,
|
||||||
savedFolder: null,
|
savedFolder: null,
|
||||||
|
savedCloudFolderId: null,
|
||||||
|
savedCloudContentId: null,
|
||||||
savedFilePath: filePath,
|
savedFilePath: filePath,
|
||||||
savedFormat: handler.format,
|
savedFormat: handler.format,
|
||||||
...connProps,
|
...connProps,
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ export default async function openNewTab(newTab, initialData: any = undefined, o
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { savedFile, savedFolder, savedFilePath, conid, database } = newTab.props || {};
|
const { savedFile, savedFolder, savedFilePath, savedCloudFolderId, savedCloudContentId, conid, database } =
|
||||||
|
newTab.props || {};
|
||||||
|
|
||||||
if (conid && database) {
|
if (conid && database) {
|
||||||
const connection = await getConnectionInfo({ conid });
|
const connection = await getConnectionInfo({ conid });
|
||||||
@@ -49,7 +50,9 @@ export default async function openNewTab(newTab, initialData: any = undefined, o
|
|||||||
x.closedTime == null &&
|
x.closedTime == null &&
|
||||||
x.props.savedFile == savedFile &&
|
x.props.savedFile == savedFile &&
|
||||||
x.props.savedFolder == savedFolder &&
|
x.props.savedFolder == savedFolder &&
|
||||||
x.props.savedFilePath == savedFilePath
|
x.props.savedFilePath == savedFilePath &&
|
||||||
|
x.props.savedCloudFolderId == savedCloudFolderId &&
|
||||||
|
x.props.savedCloudContentId == savedCloudContentId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,16 +15,31 @@ export default async function saveTabFile(editor, saveMode, folder, format, file
|
|||||||
const tabs = get(openedTabs);
|
const tabs = get(openedTabs);
|
||||||
const tabid = editor.activator.tabid;
|
const tabid = editor.activator.tabid;
|
||||||
const data = editor.getData();
|
const data = editor.getData();
|
||||||
const { savedFile, savedFilePath, savedFolder } = tabs.find(x => x.tabid == tabid).props || {};
|
const { savedFile, savedFilePath, savedFolder, savedCloudFolderId, savedCloudContentId } =
|
||||||
|
tabs.find(x => x.tabid == tabid).props || {};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (savedFile) {
|
if (savedCloudFolderId && savedCloudContentId) {
|
||||||
await apiCall('files/save', { folder: savedFolder || folder, file: savedFile, data, format });
|
const resp = await apiCall('cloud/save-file', {
|
||||||
|
folid: savedCloudFolderId,
|
||||||
|
fileName: savedFile,
|
||||||
|
data,
|
||||||
|
contentFolder: folder,
|
||||||
|
format,
|
||||||
|
cntid: savedCloudContentId,
|
||||||
|
});
|
||||||
|
if (resp.cntid) {
|
||||||
|
markTabSaved(tabid);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (savedFile) {
|
||||||
|
await apiCall('files/save', { folder: savedFolder || folder, file: savedFile, data, format });
|
||||||
|
}
|
||||||
|
if (savedFilePath) {
|
||||||
|
await apiCall('files/save-as', { filePath: savedFilePath, data, format });
|
||||||
|
}
|
||||||
|
markTabSaved(tabid);
|
||||||
}
|
}
|
||||||
if (savedFilePath) {
|
|
||||||
await apiCall('files/save-as', { filePath: savedFilePath, data, format });
|
|
||||||
}
|
|
||||||
markTabSaved(tabid);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSave = (title, newProps) => {
|
const onSave = (title, newProps) => {
|
||||||
@@ -60,6 +75,8 @@ export default async function saveTabFile(editor, saveMode, folder, format, file
|
|||||||
savedFile: null,
|
savedFile: null,
|
||||||
savedFolder: null,
|
savedFolder: null,
|
||||||
savedFilePath: file,
|
savedFilePath: file,
|
||||||
|
savedCloudFolderId: null,
|
||||||
|
savedCloudContentId: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if ((savedFile || savedFilePath) && saveMode == 'save') {
|
} else if ((savedFile || savedFilePath) && saveMode == 'save') {
|
||||||
@@ -73,6 +90,8 @@ export default async function saveTabFile(editor, saveMode, folder, format, file
|
|||||||
name: savedFile || 'newFile',
|
name: savedFile || 'newFile',
|
||||||
filePath: savedFilePath,
|
filePath: savedFilePath,
|
||||||
onSave,
|
onSave,
|
||||||
|
folid: savedCloudFolderId,
|
||||||
|
// cntid: savedCloudContentId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,40 @@
|
|||||||
import getElectron from './getElectron';
|
import getElectron from './getElectron';
|
||||||
|
|
||||||
export function openWebLink(href) {
|
export function openWebLink(href, usePopup = false) {
|
||||||
const electron = getElectron();
|
const electron = getElectron();
|
||||||
|
|
||||||
if (electron) {
|
if (electron) {
|
||||||
electron.send('open-link', href);
|
electron.send('open-link', href);
|
||||||
} else {
|
} else {
|
||||||
window.open(href, '_blank');
|
if (usePopup) {
|
||||||
|
const w = 500;
|
||||||
|
const h = 650;
|
||||||
|
|
||||||
|
const dualScreenLeft = window.screenLeft ?? window.screenX; // X of parent
|
||||||
|
const dualScreenTop = window.screenTop ?? window.screenY; // Y of parent
|
||||||
|
|
||||||
|
// 2. How big is the parent window?
|
||||||
|
const parentWidth = window.outerWidth;
|
||||||
|
const parentHeight = window.outerHeight;
|
||||||
|
|
||||||
|
// 3. Centre the popup inside that rectangle
|
||||||
|
const left = dualScreenLeft + (parentWidth - w) / 2;
|
||||||
|
const top = dualScreenTop + (parentHeight - h) / 2;
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
`width=${w}`,
|
||||||
|
`height=${h}`,
|
||||||
|
`left=${left}`,
|
||||||
|
`top=${top}`,
|
||||||
|
'scrollbars=yes',
|
||||||
|
'resizable=yes',
|
||||||
|
'noopener',
|
||||||
|
'noreferrer',
|
||||||
|
];
|
||||||
|
|
||||||
|
window.open(href, 'dbgateCloudLoginPopup', features.join(','));
|
||||||
|
} else {
|
||||||
|
window.open(href, '_blank');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -351,6 +351,7 @@
|
|||||||
isExpandable={data => $openedConnections.includes(data._id) && !data.singleDatabase}
|
isExpandable={data => $openedConnections.includes(data._id) && !data.singleDatabase}
|
||||||
{filter}
|
{filter}
|
||||||
passProps={{
|
passProps={{
|
||||||
|
...passProps,
|
||||||
connectionColorFactory: $connectionColorFactory,
|
connectionColorFactory: $connectionColorFactory,
|
||||||
showPinnedInsteadOfUnpin: true,
|
showPinnedInsteadOfUnpin: true,
|
||||||
searchSettings: $connectionAppObjectSearchSettings,
|
searchSettings: $connectionAppObjectSearchSettings,
|
||||||
|
|||||||
@@ -1,31 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { findEngineDriver } from 'dbgate-tools';
|
import { useCloudContentList, useConfig, useConnectionInfo } from '../utility/metadataLoaders';
|
||||||
import { currentDatabase, extensions, pinnedDatabases, pinnedTables } from '../stores';
|
|
||||||
import { useConfig, useConnectionInfo } from '../utility/metadataLoaders';
|
|
||||||
|
|
||||||
import ConnectionList from './ConnectionList.svelte';
|
import ConnectionList from './ConnectionList.svelte';
|
||||||
import PinnedObjectsList from './PinnedObjectsList.svelte';
|
|
||||||
import ErrorInfo from '../elements/ErrorInfo.svelte';
|
|
||||||
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
|
|
||||||
|
|
||||||
import WidgetColumnBar from './WidgetColumnBar.svelte';
|
import WidgetColumnBar from './WidgetColumnBar.svelte';
|
||||||
import WidgetColumnBarItem from './WidgetColumnBarItem.svelte';
|
import WidgetColumnBarItem from './WidgetColumnBarItem.svelte';
|
||||||
import SqlObjectList from './SqlObjectList.svelte';
|
|
||||||
import DbKeysTree from './DbKeysTree.svelte';
|
|
||||||
import SingleConnectionDatabaseList from './SingleConnectionDatabaseList.svelte';
|
import SingleConnectionDatabaseList from './SingleConnectionDatabaseList.svelte';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import FocusedConnectionInfoWidget from './FocusedConnectionInfoWidget.svelte';
|
|
||||||
import { _t } from '../translations';
|
import { _t } from '../translations';
|
||||||
|
import DatabaseWidgetDetailContent from './DatabaseWidgetDetailContent.svelte';
|
||||||
|
|
||||||
export let hidden = false;
|
export let hidden = false;
|
||||||
let domSqlObjectList = null;
|
let domSqlObjectList = null;
|
||||||
|
|
||||||
$: conid = $currentDatabase?.connection?._id;
|
|
||||||
$: connection = useConnectionInfo({ conid });
|
|
||||||
$: driver = findEngineDriver($connection, $extensions);
|
|
||||||
$: config = useConfig();
|
$: config = useConfig();
|
||||||
$: singleDatabase = $currentDatabase?.connection?.singleDatabase;
|
$: cloudContentList = useCloudContentList();
|
||||||
$: database = $currentDatabase?.name;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WidgetColumnBar {hidden}>
|
<WidgetColumnBar {hidden}>
|
||||||
@@ -45,69 +34,14 @@
|
|||||||
height="35%"
|
height="35%"
|
||||||
storageName="connectionsWidget"
|
storageName="connectionsWidget"
|
||||||
>
|
>
|
||||||
<ConnectionList passProps={{ onFocusSqlObjectList: () => domSqlObjectList.focus() }} />
|
<ConnectionList
|
||||||
|
passProps={{
|
||||||
|
onFocusSqlObjectList: () => domSqlObjectList.focus(),
|
||||||
|
cloudContentList: $cloudContentList,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</WidgetColumnBarItem>
|
</WidgetColumnBarItem>
|
||||||
{/if}
|
{/if}
|
||||||
<WidgetColumnBarItem
|
|
||||||
title={_t('widget.pinned', { defaultMessage: 'Pinned' })}
|
|
||||||
name="pinned"
|
|
||||||
height="15%"
|
|
||||||
storageName="pinnedItemsWidget"
|
|
||||||
skip={!_.compact($pinnedDatabases).length &&
|
|
||||||
!$pinnedTables.some(x => x && x.conid == conid && x.database == $currentDatabase?.name)}
|
|
||||||
>
|
|
||||||
<PinnedObjectsList />
|
|
||||||
</WidgetColumnBarItem>
|
|
||||||
|
|
||||||
<WidgetColumnBarItem
|
<DatabaseWidgetDetailContent bind:domSqlObjectList showCloudConnection={false} />
|
||||||
title={driver?.databaseEngineTypes?.includes('document')
|
|
||||||
? (driver?.collectionPluralLabel ?? 'Collections/containers')
|
|
||||||
: _t('widget.tablesViewsFunctions', { defaultMessage: 'Tables, views, functions' })}
|
|
||||||
name="dbObjects"
|
|
||||||
storageName="dbObjectsWidget"
|
|
||||||
skip={!(
|
|
||||||
conid &&
|
|
||||||
(database || singleDatabase) &&
|
|
||||||
(driver?.databaseEngineTypes?.includes('sql') || driver?.databaseEngineTypes?.includes('document'))
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SqlObjectList {conid} {database} bind:this={domSqlObjectList} />
|
|
||||||
</WidgetColumnBarItem>
|
|
||||||
|
|
||||||
<WidgetColumnBarItem
|
|
||||||
title={_t('widget.keys', { defaultMessage: 'Keys' })}
|
|
||||||
name="dbObjects"
|
|
||||||
storageName="dbObjectsWidget"
|
|
||||||
skip={!(conid && (database || singleDatabase) && driver?.databaseEngineTypes?.includes('keyvalue'))}
|
|
||||||
>
|
|
||||||
<DbKeysTree {conid} {database} treeKeySeparator={$connection?.treeKeySeparator || ':'} />
|
|
||||||
</WidgetColumnBarItem>
|
|
||||||
|
|
||||||
<WidgetColumnBarItem
|
|
||||||
title={_t('widget.databaseContent', { defaultMessage: 'Database content' })}
|
|
||||||
name="dbObjects"
|
|
||||||
storageName="dbObjectsWidget"
|
|
||||||
skip={conid && (database || singleDatabase)}
|
|
||||||
>
|
|
||||||
<WidgetsInnerContainer>
|
|
||||||
<FocusedConnectionInfoWidget {conid} {database} connection={$connection} />
|
|
||||||
|
|
||||||
<ErrorInfo message="Database not selected" icon="img alert" />
|
|
||||||
</WidgetsInnerContainer>
|
|
||||||
</WidgetColumnBarItem>
|
|
||||||
|
|
||||||
<WidgetColumnBarItem
|
|
||||||
title={_t('widget.databaseContent', { defaultMessage: 'Database content' })}
|
|
||||||
name="dbObjects"
|
|
||||||
storageName="dbObjectsWidget"
|
|
||||||
skip={!(conid && (database || singleDatabase) && !driver)}
|
|
||||||
>
|
|
||||||
<WidgetsInnerContainer>
|
|
||||||
<FocusedConnectionInfoWidget {conid} {database} connection={$connection} />
|
|
||||||
|
|
||||||
<ErrorInfo
|
|
||||||
message={_t('error.driverNotFound', { defaultMessage: 'Invalid database connection, driver not found' })}
|
|
||||||
/>
|
|
||||||
</WidgetsInnerContainer>
|
|
||||||
</WidgetColumnBarItem>
|
|
||||||
</WidgetColumnBar>
|
</WidgetColumnBar>
|
||||||
|
|||||||
135
packages/web/src/widgets/DatabaseWidgetDetailContent.svelte
Normal file
135
packages/web/src/widgets/DatabaseWidgetDetailContent.svelte
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { findEngineDriver } from 'dbgate-tools';
|
||||||
|
import { currentDatabase, extensions, pinnedDatabases, pinnedTables, selectedWidget } from '../stores';
|
||||||
|
import { useConnectionInfo } from '../utility/metadataLoaders';
|
||||||
|
|
||||||
|
import PinnedObjectsList from './PinnedObjectsList.svelte';
|
||||||
|
import ErrorInfo from '../elements/ErrorInfo.svelte';
|
||||||
|
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
|
||||||
|
|
||||||
|
import WidgetColumnBarItem from './WidgetColumnBarItem.svelte';
|
||||||
|
import SqlObjectList from './SqlObjectList.svelte';
|
||||||
|
import DbKeysTree from './DbKeysTree.svelte';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import FocusedConnectionInfoWidget from './FocusedConnectionInfoWidget.svelte';
|
||||||
|
import { _t } from '../translations';
|
||||||
|
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||||
|
|
||||||
|
export let domSqlObjectList = null;
|
||||||
|
export let showCloudConnection;
|
||||||
|
|
||||||
|
$: conid = $currentDatabase?.connection?._id;
|
||||||
|
$: connection = useConnectionInfo({ conid });
|
||||||
|
$: driver = findEngineDriver($connection, $extensions);
|
||||||
|
$: singleDatabase = $currentDatabase?.connection?.singleDatabase;
|
||||||
|
$: database = $currentDatabase?.name;
|
||||||
|
|
||||||
|
$: correctCloudStatus =
|
||||||
|
!conid ||
|
||||||
|
(!showCloudConnection && !conid?.startsWith('cloud://')) ||
|
||||||
|
(showCloudConnection && conid?.startsWith('cloud://'));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<WidgetColumnBarItem
|
||||||
|
title={_t('widget.pinned', { defaultMessage: 'Pinned' })}
|
||||||
|
name="pinned"
|
||||||
|
height="15%"
|
||||||
|
storageName="pinnedItemsWidget"
|
||||||
|
skip={!_.compact($pinnedDatabases).length &&
|
||||||
|
!$pinnedTables.some(x => x && x.conid == conid && x.database == $currentDatabase?.name)}
|
||||||
|
positiveCondition={correctCloudStatus}
|
||||||
|
>
|
||||||
|
<PinnedObjectsList />
|
||||||
|
</WidgetColumnBarItem>
|
||||||
|
|
||||||
|
<WidgetColumnBarItem
|
||||||
|
title={driver?.databaseEngineTypes?.includes('document')
|
||||||
|
? (driver?.collectionPluralLabel ?? 'Collections/containers')
|
||||||
|
: _t('widget.tablesViewsFunctions', { defaultMessage: 'Tables, views, functions' })}
|
||||||
|
name="dbObjects"
|
||||||
|
storageName="dbObjectsWidget"
|
||||||
|
skip={!(
|
||||||
|
conid &&
|
||||||
|
(database || singleDatabase) &&
|
||||||
|
(driver?.databaseEngineTypes?.includes('sql') || driver?.databaseEngineTypes?.includes('document'))
|
||||||
|
)}
|
||||||
|
positiveCondition={correctCloudStatus}
|
||||||
|
>
|
||||||
|
<SqlObjectList {conid} {database} bind:this={domSqlObjectList} />
|
||||||
|
</WidgetColumnBarItem>
|
||||||
|
|
||||||
|
<WidgetColumnBarItem
|
||||||
|
title={_t('widget.keys', { defaultMessage: 'Keys' })}
|
||||||
|
name="dbObjects"
|
||||||
|
storageName="dbObjectsWidget"
|
||||||
|
skip={!(conid && (database || singleDatabase) && driver?.databaseEngineTypes?.includes('keyvalue'))}
|
||||||
|
positiveCondition={correctCloudStatus}
|
||||||
|
>
|
||||||
|
<DbKeysTree {conid} {database} treeKeySeparator={$connection?.treeKeySeparator || ':'} />
|
||||||
|
</WidgetColumnBarItem>
|
||||||
|
|
||||||
|
<WidgetColumnBarItem
|
||||||
|
title={_t('widget.databaseContent', { defaultMessage: 'Database content' })}
|
||||||
|
name="dbObjects"
|
||||||
|
storageName="dbObjectsWidget"
|
||||||
|
skip={conid && (database || singleDatabase)}
|
||||||
|
positiveCondition={correctCloudStatus}
|
||||||
|
>
|
||||||
|
<WidgetsInnerContainer>
|
||||||
|
<FocusedConnectionInfoWidget {conid} {database} connection={$connection} />
|
||||||
|
|
||||||
|
<ErrorInfo message="Database not selected" icon="img alert" />
|
||||||
|
</WidgetsInnerContainer>
|
||||||
|
</WidgetColumnBarItem>
|
||||||
|
|
||||||
|
<WidgetColumnBarItem
|
||||||
|
title={_t('widget.databaseContent', { defaultMessage: 'Database content' })}
|
||||||
|
name="dbObjects"
|
||||||
|
storageName="dbObjectsWidget"
|
||||||
|
skip={!(conid && (database || singleDatabase) && !driver)}
|
||||||
|
positiveCondition={correctCloudStatus}
|
||||||
|
>
|
||||||
|
<WidgetsInnerContainer>
|
||||||
|
<FocusedConnectionInfoWidget {conid} {database} connection={$connection} />
|
||||||
|
|
||||||
|
<ErrorInfo
|
||||||
|
message={_t('error.driverNotFound', { defaultMessage: 'Invalid database connection, driver not found' })}
|
||||||
|
/>
|
||||||
|
</WidgetsInnerContainer>
|
||||||
|
</WidgetColumnBarItem>
|
||||||
|
|
||||||
|
<WidgetColumnBarItem
|
||||||
|
title={_t('widget.databaseContent', { defaultMessage: 'Database content' })}
|
||||||
|
name="incorrectClaudStatus"
|
||||||
|
height="15%"
|
||||||
|
storageName="incorrectClaudStatusWidget"
|
||||||
|
skip={correctCloudStatus}
|
||||||
|
>
|
||||||
|
<WidgetsInnerContainer>
|
||||||
|
<ErrorInfo
|
||||||
|
message={showCloudConnection
|
||||||
|
? _t('error.selectedNotCloudConnection', { defaultMessage: 'Selected connection is not from DbGate cloud' })
|
||||||
|
: _t('error.selectedCloudConnection', { defaultMessage: 'Selected connection is from DbGate cloud' })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="incorrect-cloud-status-wrapper">
|
||||||
|
<FormStyledButton
|
||||||
|
value={`Show database content`}
|
||||||
|
skipWidth
|
||||||
|
on:click={() => {
|
||||||
|
$selectedWidget = conid?.startsWith('cloud://') ? 'cloud-private' : 'database';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</WidgetsInnerContainer>
|
||||||
|
</WidgetColumnBarItem>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.incorrect-cloud-status-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
249
packages/web/src/widgets/PrivateCloudWidget.svelte
Normal file
249
packages/web/src/widgets/PrivateCloudWidget.svelte
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import WidgetColumnBar from './WidgetColumnBar.svelte';
|
||||||
|
import WidgetColumnBarItem from './WidgetColumnBarItem.svelte';
|
||||||
|
|
||||||
|
import AppObjectList from '../appobj/AppObjectList.svelte';
|
||||||
|
import * as cloudContentAppObject from '../appobj/CloudContentAppObject.svelte';
|
||||||
|
import { useCloudContentList, usePublicCloudFiles, useServerStatus } from '../utility/metadataLoaders';
|
||||||
|
import { _t } from '../translations';
|
||||||
|
|
||||||
|
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
|
||||||
|
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
|
||||||
|
import SearchInput from '../elements/SearchInput.svelte';
|
||||||
|
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
|
||||||
|
import InlineButton from '../buttons/InlineButton.svelte';
|
||||||
|
import FontIcon from '../icons/FontIcon.svelte';
|
||||||
|
import { apiCall } from '../utility/api';
|
||||||
|
import {
|
||||||
|
cloudConnectionsStore,
|
||||||
|
cloudSigninTokenHolder,
|
||||||
|
currentDatabase,
|
||||||
|
expandedConnections,
|
||||||
|
openedConnections,
|
||||||
|
openedSingleDatabaseConnections,
|
||||||
|
} from '../stores';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { plusExpandIcon } from '../icons/expandIcons';
|
||||||
|
import { volatileConnectionMapStore } from '../utility/api';
|
||||||
|
import SubCloudItemsList from '../appobj/SubCloudItemsList.svelte';
|
||||||
|
import DatabaseWidgetDetailContent from './DatabaseWidgetDetailContent.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import DropDownButton from '../buttons/DropDownButton.svelte';
|
||||||
|
import { showModal } from '../modals/modalTools';
|
||||||
|
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||||
|
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||||
|
import { showSnackbarInfo } from '../utility/snackbar';
|
||||||
|
|
||||||
|
let filter = '';
|
||||||
|
let domSqlObjectList = null;
|
||||||
|
|
||||||
|
const cloudContentList = useCloudContentList();
|
||||||
|
const serverStatus = useServerStatus();
|
||||||
|
|
||||||
|
$: emptyCloudContent = ($cloudContentList || []).filter(x => !x.items?.length).map(x => x.folid);
|
||||||
|
$: cloudContentFlat = _.sortBy(
|
||||||
|
($cloudContentList || [])
|
||||||
|
.flatMap(fld => fld.items ?? [])
|
||||||
|
.map(data => {
|
||||||
|
if (data.type == 'connection') {
|
||||||
|
const conid = `cloud://${data.folid}/${data.cntid}`;
|
||||||
|
const status = $serverStatus ? $serverStatus[$volatileConnectionMapStore[conid] || conid] : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
conid,
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}),
|
||||||
|
'name'
|
||||||
|
);
|
||||||
|
$: contentGroupMap = _.keyBy($cloudContentList || [], x => x.folid);
|
||||||
|
|
||||||
|
// $: console.log('cloudContentFlat', cloudContentFlat);
|
||||||
|
// $: console.log('contentGroupMap', contentGroupMap);
|
||||||
|
|
||||||
|
async function handleRefreshContent() {
|
||||||
|
await apiCall('cloud/refresh-content');
|
||||||
|
}
|
||||||
|
|
||||||
|
// async function loadCloudConnection(conid) {
|
||||||
|
// const conn = await apiCall('connections/get', { conid });
|
||||||
|
// $cloudConnectionsStore = {
|
||||||
|
// ...$cloudConnectionsStore,
|
||||||
|
// [conid]: conn,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function ensureCloudConnectionsLoaded(...conids) {
|
||||||
|
// _.uniq(conids).forEach(conid => {
|
||||||
|
// if (conid?.startsWith('cloud://') && !$cloudConnectionsStore[conid]) {
|
||||||
|
// loadCloudConnection(conid);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// $: ensureCloudConnectionsLoaded(
|
||||||
|
// $currentDatabase?.connection?._id,
|
||||||
|
// ...$openedSingleDatabaseConnections,
|
||||||
|
// ...$openedConnections
|
||||||
|
// );
|
||||||
|
|
||||||
|
// onMount(() => {
|
||||||
|
// const currentConid = $currentDatabase?.connection?._id;
|
||||||
|
// if (currentConid?.startsWith('cloud://') && !$cloudConnectionsStore[currentConid]) {
|
||||||
|
// loadCloudConnection(currentConid);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
function createAddMenu() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: 'New shared folder',
|
||||||
|
onClick: () => {
|
||||||
|
showModal(InputTextModal, {
|
||||||
|
label: 'New folder name',
|
||||||
|
header: 'New shared folder',
|
||||||
|
onConfirm: async newFolder => {
|
||||||
|
apiCall('cloud/create-folder', {
|
||||||
|
name: newFolder,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Add existing shared folder',
|
||||||
|
onClick: () => {
|
||||||
|
showModal(InputTextModal, {
|
||||||
|
label: 'Your invite link (in form dbgate://folder/xxx)',
|
||||||
|
header: 'Add existing shared folder',
|
||||||
|
onConfirm: async newFolder => {
|
||||||
|
apiCall('cloud/grant-folder', {
|
||||||
|
inviteLink: newFolder,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command: 'new.connectionOnCloud',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGroupContextMenu(folder) {
|
||||||
|
const handleRename = () => {
|
||||||
|
showModal(InputTextModal, {
|
||||||
|
value: contentGroupMap[folder]?.name,
|
||||||
|
label: 'New folder name',
|
||||||
|
header: 'Rename folder',
|
||||||
|
onConfirm: async name => {
|
||||||
|
apiCall('cloud/rename-folder', {
|
||||||
|
folid: folder,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
showModal(ConfirmModal, {
|
||||||
|
message: `Really delete folder ${contentGroupMap[folder]?.name}? All folder content will be deleted!`,
|
||||||
|
header: 'Delete folder',
|
||||||
|
onConfirm: () => {
|
||||||
|
apiCall('cloud/delete-folder', {
|
||||||
|
folid: folder,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyInviteLink = async role => {
|
||||||
|
const { inviteToken } = await apiCall(`cloud/get-invite-token`, {
|
||||||
|
folid: folder,
|
||||||
|
role,
|
||||||
|
});
|
||||||
|
const inviteLink = `dbgate://folder/v1/${inviteToken}?mode=${role}`;
|
||||||
|
navigator.clipboard.writeText(inviteLink);
|
||||||
|
showSnackbarInfo(`Invite link (${role}) copied to clipboard`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
contentGroupMap[folder]?.role == 'admin' && [
|
||||||
|
{ text: 'Rename', onClick: handleRename },
|
||||||
|
{ text: 'Delete', onClick: handleDelete },
|
||||||
|
],
|
||||||
|
contentGroupMap[folder]?.role == 'admin' &&
|
||||||
|
!contentGroupMap[folder]?.isPrivate && {
|
||||||
|
text: 'Copy invite link',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
text: 'Admin',
|
||||||
|
onClick: () => handleCopyInviteLink('admin'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Write',
|
||||||
|
onClick: () => handleCopyInviteLink('write'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Read',
|
||||||
|
onClick: () => handleCopyInviteLink('read'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<WidgetColumnBar>
|
||||||
|
<WidgetColumnBarItem
|
||||||
|
title="DbGate Cloud"
|
||||||
|
name="privateCloud"
|
||||||
|
height="50%"
|
||||||
|
storageName="privateCloudItems"
|
||||||
|
skip={!$cloudSigninTokenHolder}
|
||||||
|
>
|
||||||
|
<SearchBoxWrapper>
|
||||||
|
<SearchInput placeholder="Search cloud connections and files" bind:value={filter} />
|
||||||
|
<CloseSearchButton bind:filter />
|
||||||
|
<DropDownButton icon="icon plus-thick" menu={createAddMenu} />
|
||||||
|
<InlineButton
|
||||||
|
on:click={handleRefreshContent}
|
||||||
|
title="Refresh files"
|
||||||
|
data-testid="CloudItemsWidget_buttonRefreshContent"
|
||||||
|
>
|
||||||
|
<FontIcon icon="icon refresh" />
|
||||||
|
</InlineButton>
|
||||||
|
</SearchBoxWrapper>
|
||||||
|
<WidgetsInnerContainer>
|
||||||
|
<AppObjectList
|
||||||
|
list={cloudContentFlat || []}
|
||||||
|
module={cloudContentAppObject}
|
||||||
|
emptyGroupNames={emptyCloudContent}
|
||||||
|
groupFunc={data => data.folid}
|
||||||
|
mapGroupTitle={folid => `${contentGroupMap[folid]?.name} - ${contentGroupMap[folid]?.role}`}
|
||||||
|
{filter}
|
||||||
|
subItemsComponent={() => SubCloudItemsList}
|
||||||
|
expandIconFunc={plusExpandIcon}
|
||||||
|
isExpandable={data =>
|
||||||
|
data.conid &&
|
||||||
|
$cloudConnectionsStore[data.conid] &&
|
||||||
|
!$cloudConnectionsStore[data.conid].singleDatabase &&
|
||||||
|
$openedConnections.includes(data.conid)}
|
||||||
|
getIsExpanded={data => $expandedConnections.includes(data.conid)}
|
||||||
|
setIsExpanded={(data, value) => {
|
||||||
|
expandedConnections.update(old => (value ? [...old, data.conid] : old.filter(x => x != data.conid)));
|
||||||
|
}}
|
||||||
|
passProps={{
|
||||||
|
onFocusSqlObjectList: () => domSqlObjectList.focus(),
|
||||||
|
}}
|
||||||
|
groupContextMenu={createGroupContextMenu}
|
||||||
|
/>
|
||||||
|
</WidgetsInnerContainer>
|
||||||
|
</WidgetColumnBarItem>
|
||||||
|
|
||||||
|
<DatabaseWidgetDetailContent bind:domSqlObjectList showCloudConnection={true} />
|
||||||
|
</WidgetColumnBar>
|
||||||
53
packages/web/src/widgets/PublicCloudWidget.svelte
Normal file
53
packages/web/src/widgets/PublicCloudWidget.svelte
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SavedFilesList from './SavedFilesList.svelte';
|
||||||
|
|
||||||
|
import WidgetColumnBar from './WidgetColumnBar.svelte';
|
||||||
|
import WidgetColumnBarItem from './WidgetColumnBarItem.svelte';
|
||||||
|
|
||||||
|
import AppObjectList from '../appobj/AppObjectList.svelte';
|
||||||
|
import * as publicCloudFileAppObject from '../appobj/PublicCloudFileAppObject.svelte';
|
||||||
|
import * as cloudContentAppObject from '../appobj/CloudContentAppObject.svelte';
|
||||||
|
import { useCloudContentList, usePublicCloudFiles, useServerStatus } from '../utility/metadataLoaders';
|
||||||
|
import { _t } from '../translations';
|
||||||
|
|
||||||
|
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
|
||||||
|
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
|
||||||
|
import SearchInput from '../elements/SearchInput.svelte';
|
||||||
|
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
|
||||||
|
import InlineButton from '../buttons/InlineButton.svelte';
|
||||||
|
import FontIcon from '../icons/FontIcon.svelte';
|
||||||
|
import { apiCall } from '../utility/api';
|
||||||
|
import _ from 'lodash';
|
||||||
|
let filter = '';
|
||||||
|
|
||||||
|
const publicFiles = usePublicCloudFiles();
|
||||||
|
|
||||||
|
async function handleRefreshPublic() {
|
||||||
|
await apiCall('cloud/refresh-public-files?isRefresh=1');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<WidgetColumnBar>
|
||||||
|
<WidgetColumnBarItem title="Public Knowledge Base" name="publicCloud" storageName="publicCloudItems">
|
||||||
|
<WidgetsInnerContainer>
|
||||||
|
<SearchBoxWrapper>
|
||||||
|
<SearchInput placeholder="Search public files" bind:value={filter} />
|
||||||
|
<CloseSearchButton bind:filter />
|
||||||
|
<InlineButton
|
||||||
|
on:click={handleRefreshPublic}
|
||||||
|
title="Refresh files"
|
||||||
|
data-testid="CloudItemsWidget_buttonRefreshPublic"
|
||||||
|
>
|
||||||
|
<FontIcon icon="icon refresh" />
|
||||||
|
</InlineButton>
|
||||||
|
</SearchBoxWrapper>
|
||||||
|
|
||||||
|
<AppObjectList
|
||||||
|
list={$publicFiles || []}
|
||||||
|
module={publicCloudFileAppObject}
|
||||||
|
groupFunc={data => data.folder || undefined}
|
||||||
|
{filter}
|
||||||
|
/>
|
||||||
|
</WidgetsInnerContainer>
|
||||||
|
</WidgetColumnBarItem>
|
||||||
|
</WidgetColumnBar>
|
||||||
@@ -74,24 +74,24 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WidgetsInnerContainer>
|
<SearchBoxWrapper>
|
||||||
<SearchBoxWrapper>
|
<SearchInput placeholder="Search saved files" bind:value={filter} />
|
||||||
<SearchInput placeholder="Search saved files" bind:value={filter} />
|
<CloseSearchButton bind:filter />
|
||||||
<CloseSearchButton bind:filter />
|
<InlineUploadButton
|
||||||
<InlineUploadButton
|
filters={[
|
||||||
filters={[
|
{
|
||||||
{
|
name: `All supported files`,
|
||||||
name: `All supported files`,
|
extensions: ['sql'],
|
||||||
extensions: ['sql'],
|
},
|
||||||
},
|
{ name: `SQL files`, extensions: ['sql'] },
|
||||||
{ name: `SQL files`, extensions: ['sql'] },
|
]}
|
||||||
]}
|
onProcessFile={handleUploadedFile}
|
||||||
onProcessFile={handleUploadedFile}
|
/>
|
||||||
/>
|
<InlineButton on:click={handleRefreshFiles} title="Refresh files" data-testid="SavedFileList_buttonRefresh">
|
||||||
<InlineButton on:click={handleRefreshFiles} title="Refresh files" data-testid="SavedFileList_buttonRefresh">
|
<FontIcon icon="icon refresh" />
|
||||||
<FontIcon icon="icon refresh" />
|
</InlineButton>
|
||||||
</InlineButton>
|
</SearchBoxWrapper>
|
||||||
</SearchBoxWrapper>
|
|
||||||
|
|
||||||
|
<WidgetsInnerContainer>
|
||||||
<AppObjectList list={files} module={savedFileAppObject} groupFunc={data => dataFolderTitle(data.folder)} {filter} />
|
<AppObjectList list={files} module={savedFileAppObject} groupFunc={data => dataFolderTitle(data.folder)} {filter} />
|
||||||
</WidgetsInnerContainer>
|
</WidgetsInnerContainer>
|
||||||
|
|||||||
@@ -163,6 +163,8 @@
|
|||||||
($focusedConnectionOrDatabase.conid != conid ||
|
($focusedConnectionOrDatabase.conid != conid ||
|
||||||
($focusedConnectionOrDatabase?.database &&
|
($focusedConnectionOrDatabase?.database &&
|
||||||
$focusedConnectionOrDatabase?.database != extractDbNameFromComposite(database)));
|
$focusedConnectionOrDatabase?.database != extractDbNameFromComposite(database)));
|
||||||
|
|
||||||
|
// $: console.log('STATUS', $status);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $status && $status.name == 'error'}
|
{#if $status && $status.name == 'error'}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import {
|
import {
|
||||||
activeTabId,
|
activeTabId,
|
||||||
appUpdateStatus,
|
appUpdateStatus,
|
||||||
|
cloudSigninTokenHolder,
|
||||||
currentArchive,
|
currentArchive,
|
||||||
currentDatabase,
|
currentDatabase,
|
||||||
currentThemeDefinition,
|
currentThemeDefinition,
|
||||||
@@ -154,7 +155,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $currentArchive}
|
{#if $currentArchive && $currentArchive != 'default'}
|
||||||
<div
|
<div
|
||||||
class="item flex clickable"
|
class="item flex clickable"
|
||||||
title="Current archive"
|
title="Current archive"
|
||||||
@@ -184,6 +185,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if $cloudSigninTokenHolder?.email}
|
||||||
|
<div class="item">
|
||||||
|
<FontIcon icon="icon cloud" padRight />
|
||||||
|
{$cloudSigninTokenHolder?.email}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if $appUpdateStatus}
|
{#if $appUpdateStatus}
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<FontIcon icon={$appUpdateStatus.icon} padRight />
|
<FontIcon icon={$appUpdateStatus.icon} padRight />
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
const visibleItemsCount = defs.filter(x => !x.collapsed && !x.skip).length;
|
const visibleItemsCount = defs.filter(x => !x.collapsed && !x.skip).length;
|
||||||
for (let index = 0; index < defs.length; index++) {
|
for (let index = 0; index < defs.length; index++) {
|
||||||
const definition = defs[index];
|
const definition = defs[index];
|
||||||
const splitterVisible = !!defs.slice(index + 1).find(x => x && !x.collapsed && !x.skip);
|
const splitterVisible = !!defs.slice(index + 1).find(x => x && !x.collapsed && !x.skip && x.positiveCondition);
|
||||||
dynamicPropsCollection[index].set({ splitterVisible, visibleItemsCount });
|
dynamicPropsCollection[index].set({ splitterVisible, visibleItemsCount });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
export let title;
|
export let title;
|
||||||
export let name;
|
export let name;
|
||||||
export let skip = false;
|
export let skip = false;
|
||||||
|
export let positiveCondition = true;
|
||||||
export let height = null;
|
export let height = null;
|
||||||
export let collapsed = null;
|
export let collapsed = null;
|
||||||
|
|
||||||
@@ -33,11 +34,12 @@
|
|||||||
collapsed,
|
collapsed,
|
||||||
height,
|
height,
|
||||||
skip,
|
skip,
|
||||||
|
positiveCondition,
|
||||||
},
|
},
|
||||||
dynamicProps
|
dynamicProps
|
||||||
);
|
);
|
||||||
|
|
||||||
$: updateWidgetItemDefinition(widgetItemIndex, { collapsed: !visible, height, skip });
|
$: updateWidgetItemDefinition(widgetItemIndex, { collapsed: !visible, height, skip, positiveCondition });
|
||||||
|
|
||||||
$: setInitialSize(height, $widgetColumnBarHeight);
|
$: setInitialSize(height, $widgetColumnBarHeight);
|
||||||
|
|
||||||
@@ -67,7 +69,7 @@
|
|||||||
$: collapsible = $dynamicProps.visibleItemsCount != 1 || !visible;
|
$: collapsible = $dynamicProps.visibleItemsCount != 1 || !visible;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !skip}
|
{#if !skip && positiveCondition}
|
||||||
<WidgetTitle
|
<WidgetTitle
|
||||||
clickable={collapsible}
|
clickable={collapsible}
|
||||||
on:click={collapsible ? () => (visible = !visible) : null}
|
on:click={collapsible ? () => (visible = !visible) : null}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
import AppWidget from './AppWidget.svelte';
|
import AppWidget from './AppWidget.svelte';
|
||||||
import AdminMenuWidget from './AdminMenuWidget.svelte';
|
import AdminMenuWidget from './AdminMenuWidget.svelte';
|
||||||
import AdminPremiumPromoWidget from './AdminPremiumPromoWidget.svelte';
|
import AdminPremiumPromoWidget from './AdminPremiumPromoWidget.svelte';
|
||||||
|
import PublicCloudWidget from './PublicCloudWidget.svelte';
|
||||||
|
import PrivateCloudWidget from './PrivateCloudWidget.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DatabaseWidget hidden={$visibleSelectedWidget != 'database'} />
|
<DatabaseWidget hidden={$visibleSelectedWidget != 'database'} />
|
||||||
@@ -37,3 +39,9 @@
|
|||||||
{#if $visibleSelectedWidget == 'premium'}
|
{#if $visibleSelectedWidget == 'premium'}
|
||||||
<AdminPremiumPromoWidget />
|
<AdminPremiumPromoWidget />
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if $visibleSelectedWidget == 'cloud-public'}
|
||||||
|
<PublicCloudWidget />
|
||||||
|
{/if}
|
||||||
|
{#if $visibleSelectedWidget == 'cloud-private'}
|
||||||
|
<PrivateCloudWidget />
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -9,12 +9,17 @@
|
|||||||
visibleHamburgerMenuWidget,
|
visibleHamburgerMenuWidget,
|
||||||
lockedDatabaseMode,
|
lockedDatabaseMode,
|
||||||
getCurrentConfig,
|
getCurrentConfig,
|
||||||
|
cloudSigninTokenHolder,
|
||||||
} from '../stores';
|
} from '../stores';
|
||||||
import mainMenuDefinition from '../../../../app/src/mainMenuDefinition';
|
import mainMenuDefinition from '../../../../app/src/mainMenuDefinition';
|
||||||
import hasPermission from '../utility/hasPermission';
|
import hasPermission from '../utility/hasPermission';
|
||||||
import { isProApp } from '../utility/proTools';
|
import { isProApp } from '../utility/proTools';
|
||||||
|
import { openWebLink } from '../utility/simpleTools';
|
||||||
|
import { apiCall } from '../utility/api';
|
||||||
|
import getElectron from '../utility/getElectron';
|
||||||
|
|
||||||
let domSettings;
|
let domSettings;
|
||||||
|
let domCloudAccount;
|
||||||
let domMainMenu;
|
let domMainMenu;
|
||||||
|
|
||||||
const widgets = [
|
const widgets = [
|
||||||
@@ -28,6 +33,12 @@
|
|||||||
name: 'database',
|
name: 'database',
|
||||||
title: 'Database connections',
|
title: 'Database connections',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'cloud-private',
|
||||||
|
title: 'DbGate Cloud',
|
||||||
|
icon: 'icon cloud-private',
|
||||||
|
},
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// icon: 'fa-table',
|
// icon: 'fa-table',
|
||||||
// name: 'table',
|
// name: 'table',
|
||||||
@@ -58,9 +69,9 @@
|
|||||||
title: 'Selected cell data detail view',
|
title: 'Selected cell data detail view',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'icon app',
|
name: 'cloud-public',
|
||||||
name: 'app',
|
title: 'DbGate Cloud',
|
||||||
title: 'Application layers',
|
icon: 'icon cloud-public',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'icon premium',
|
icon: 'icon premium',
|
||||||
@@ -92,7 +103,26 @@
|
|||||||
const rect = domSettings.getBoundingClientRect();
|
const rect = domSettings.getBoundingClientRect();
|
||||||
const left = rect.right;
|
const left = rect.right;
|
||||||
const top = rect.bottom;
|
const top = rect.bottom;
|
||||||
const items = [{ command: 'settings.show' }, { command: 'theme.changeTheme' }, { command: 'settings.commands' }];
|
const items = [
|
||||||
|
{ command: 'settings.show' },
|
||||||
|
{ command: 'theme.changeTheme' },
|
||||||
|
{ command: 'settings.commands' },
|
||||||
|
{
|
||||||
|
text: 'View applications',
|
||||||
|
onClick: () => {
|
||||||
|
$selectedWidget = 'app';
|
||||||
|
$visibleWidgetSideBar = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
currentDropDownMenu.set({ left, top, items });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCloudAccountMenu() {
|
||||||
|
const rect = domCloudAccount.getBoundingClientRect();
|
||||||
|
const left = rect.right;
|
||||||
|
const top = rect.bottom;
|
||||||
|
const items = [{ command: 'cloud.logout' }];
|
||||||
currentDropDownMenu.set({ left, top, items });
|
currentDropDownMenu.set({ left, top, items });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +133,11 @@
|
|||||||
const items = mainMenuDefinition({ editMenu: false });
|
const items = mainMenuDefinition({ editMenu: false });
|
||||||
currentDropDownMenu.set({ left, top, items });
|
currentDropDownMenu.set({ left, top, items });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleOpenCloudLogin() {
|
||||||
|
const { url, sid } = await apiCall('auth/create-cloud-login-session', { client: getElectron() ? 'app' : 'web' });
|
||||||
|
openWebLink(url, true);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="main">
|
<div class="main">
|
||||||
@@ -113,7 +148,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#each widgets
|
{#each widgets
|
||||||
.filter(x => x && hasPermission(`widgets/${x.name}`))
|
.filter(x => x && hasPermission(`widgets/${x.name}`))
|
||||||
.filter(x => !x.isPremiumPromo || !isProApp()) as item}
|
.filter(x => !x.isPremiumPromo || !isProApp())
|
||||||
|
.filter(x => x.name != 'cloud-private' || $cloudSigninTokenHolder) as item}
|
||||||
<div
|
<div
|
||||||
class="wrapper"
|
class="wrapper"
|
||||||
class:selected={item.name == $visibleSelectedWidget}
|
class:selected={item.name == $visibleSelectedWidget}
|
||||||
@@ -129,7 +165,7 @@
|
|||||||
|
|
||||||
<div class="flex1"> </div>
|
<div class="flex1"> </div>
|
||||||
|
|
||||||
<div
|
<!-- <div
|
||||||
class="wrapper"
|
class="wrapper"
|
||||||
title={`Toggle whether tabs from all databases are visible. Currently - ${$lockedDatabaseMode ? 'NO' : 'YES'}`}
|
title={`Toggle whether tabs from all databases are visible. Currently - ${$lockedDatabaseMode ? 'NO' : 'YES'}`}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@@ -138,7 +174,22 @@
|
|||||||
data-testid="WidgetIconPanel_lockDb"
|
data-testid="WidgetIconPanel_lockDb"
|
||||||
>
|
>
|
||||||
<FontIcon icon={$lockedDatabaseMode ? 'icon locked-database-mode' : 'icon unlocked-database-mode'} />
|
<FontIcon icon={$lockedDatabaseMode ? 'icon locked-database-mode' : 'icon unlocked-database-mode'} />
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
|
{#if $cloudSigninTokenHolder}
|
||||||
|
<div
|
||||||
|
class="wrapper"
|
||||||
|
on:click={handleCloudAccountMenu}
|
||||||
|
bind:this={domCloudAccount}
|
||||||
|
data-testid="WidgetIconPanel_cloudAccount"
|
||||||
|
>
|
||||||
|
<FontIcon icon="icon cloud-account-connected" />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="wrapper" on:click={handleOpenCloudLogin} data-testid="WidgetIconPanel_cloudAccount">
|
||||||
|
<FontIcon icon="icon cloud-account" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="wrapper" on:click={handleSettingsMenu} bind:this={domSettings} data-testid="WidgetIconPanel_settings">
|
<div class="wrapper" on:click={handleSettingsMenu} bind:this={domSettings} data-testid="WidgetIconPanel_settings">
|
||||||
<FontIcon icon="icon settings" />
|
<FontIcon icon="icon settings" />
|
||||||
@@ -147,8 +198,8 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.wrapper {
|
.wrapper {
|
||||||
font-size: 23pt;
|
font-size: 20pt;
|
||||||
height: 60px;
|
height: 50px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user