diff --git a/packages/api/src/controllers/serverConnections.js b/packages/api/src/controllers/serverConnections.js
index 7dd16f226..aeaf1fdb0 100644
--- a/packages/api/src/controllers/serverConnections.js
+++ b/packages/api/src/controllers/serverConnections.js
@@ -1,24 +1,31 @@
const connections = require('./connections');
const socket = require('../utility/socket');
const { fork } = require('child_process');
+const _ = require('lodash');
module.exports = {
opened: [],
+ closed: [],
handle_databases(conid, { databases }) {
- const existing = this.opened.find(x => x.conid == conid);
+ const existing = this.opened.find((x) => x.conid == conid);
if (!existing) return;
existing.databases = databases;
socket.emitChanged(`database-list-changed-${conid}`);
},
+ handle_status(conid, { status }) {
+ const existing = this.opened.find((x) => x.conid == conid);
+ if (!existing) return;
+ existing.status = status;
+ socket.emitChanged(`server-status-changed`);
+ },
handle_error(conid, { error }) {
console.log(`Error in server connection ${conid}: ${error}`);
},
- handle_ping() {
- },
+ handle_ping() {},
async ensureOpened(conid) {
- const existing = this.opened.find(x => x.conid == conid);
+ const existing = this.opened.find((x) => x.conid == conid);
if (existing) return existing;
const connection = await connections.get({ conid });
const subprocess = fork(process.argv[1], ['serverConnectionProcess']);
@@ -27,19 +34,75 @@ module.exports = {
subprocess,
databases: [],
connection,
+ status: {
+ name: 'pending',
+ },
+ disconnected: false,
};
this.opened.push(newOpened);
+ this.closed = this.closed.filter((x) => x != conid);
+ socket.emitChanged(`server-status-changed`);
// @ts-ignore
subprocess.on('message', ({ msgtype, ...message }) => {
+ if (newOpened.disconnected) return;
this[`handle_${msgtype}`](conid, message);
});
+ subprocess.on('exit', () => {
+ if (newOpened.disconnected) return;
+ this.opened = this.opened.filter((x) => x.conid != conid);
+ this.closed.push(conid);
+ socket.emitChanged(`server-status-changed`);
+ });
subprocess.send({ msgtype: 'connect', ...connection });
return newOpened;
},
+ close(conid) {
+ const existing = this.opened.find((x) => x.conid == conid);
+ if (existing) {
+ existing.disconnected = true;
+ existing.subprocess.kill();
+ this.opened = this.opened.filter((x) => x.conid != conid);
+ this.closed.push(conid);
+ }
+ },
+
listDatabases_meta: 'get',
async listDatabases({ conid }) {
const opened = await this.ensureOpened(conid);
return opened.databases;
},
+
+ serverStatus_meta: 'get',
+ async serverStatus() {
+ return {
+ ...this.closed.reduce(
+ (res, conid) => ({
+ ...res,
+ [conid]: {
+ name: 'error',
+ },
+ }),
+ {}
+ ),
+ ..._.mapValues(_.keyBy(this.opened, 'conid'), 'status'),
+ };
+ },
+
+ ping_meta: 'post',
+ async ping({ connections }) {
+ for (const conid of connections) {
+ const opened = await this.ensureOpened(conid);
+ opened.subprocess.send({ msgtype: 'ping' });
+ }
+ return { status: 'ok' };
+ },
+
+ refresh_meta: 'post',
+ async refresh({ conid }) {
+ this.close(conid);
+
+ await this.ensureOpened(conid);
+ return { status: 'ok' };
+ },
};
diff --git a/packages/api/src/proc/serverConnectionProcess.js b/packages/api/src/proc/serverConnectionProcess.js
index 13ee8e087..745875ac6 100644
--- a/packages/api/src/proc/serverConnectionProcess.js
+++ b/packages/api/src/proc/serverConnectionProcess.js
@@ -1,27 +1,67 @@
const engines = require('@dbgate/engines');
const driverConnect = require('../utility/driverConnect');
const childProcessChecker = require('../utility/childProcessChecker');
+const stableStringify = require('json-stable-stringify');
let systemConnection;
let storedConnection;
+let lastDatabases = null;
+let lastStatus = null;
+let lastPing = null;
-async function handleRefreshDatabases() {
+async function handleRefresh() {
const driver = engines(storedConnection);
- const databases = await driver.listDatabases(systemConnection);
- process.send({ msgtype: 'databases', databases });
+ try {
+ const databases = await driver.listDatabases(systemConnection);
+ setStatusName('ok');
+ const databasesString = stableStringify(databases);
+ if (lastDatabases != databasesString) {
+ process.send({ msgtype: 'databases', databases });
+ lastDatabases = databasesString;
+ }
+ } catch (err) {
+ setStatusName('error');
+ console.error(err);
+ process.exit(1);
+ }
+}
+
+function setStatus(status) {
+ const statusString = stableStringify(status);
+ if (lastStatus != statusString) {
+ process.send({ msgtype: 'status', status });
+ lastStatus = statusString;
+ }
+}
+
+function setStatusName(name) {
+ setStatus({ name });
}
async function handleConnect(connection) {
storedConnection = connection;
+ setStatusName('pending');
+ lastPing = new Date().getTime();
const driver = engines(storedConnection);
- systemConnection = await driverConnect(driver, storedConnection);
- handleRefreshDatabases();
- setInterval(handleRefreshDatabases, 30 * 1000);
+ try {
+ systemConnection = await driverConnect(driver, storedConnection);
+ handleRefresh();
+ setInterval(handleRefresh, 30 * 1000);
+ } catch (err) {
+ setStatusName('error');
+ console.error(err);
+ process.exit(1);
+ }
+}
+
+function handlePing() {
+ lastPing = new Date().getTime();
}
const messageHandlers = {
connect: handleConnect,
+ ping: handlePing,
};
async function handleMessage({ msgtype, ...other }) {
@@ -31,6 +71,14 @@ async function handleMessage({ msgtype, ...other }) {
function start() {
childProcessChecker();
+
+ setInterval(() => {
+ const time = new Date().getTime();
+ if (time - lastPing > 60 * 1000) {
+ process.exit(0);
+ }
+ }, 60 * 1000);
+
process.on('message', async (message) => {
try {
await handleMessage(message);
diff --git a/packages/web/src/App.js b/packages/web/src/App.js
index 9e862b759..fdbf5e5a5 100644
--- a/packages/web/src/App.js
+++ b/packages/web/src/App.js
@@ -6,8 +6,10 @@ import {
CurrentDatabaseProvider,
OpenedTabsProvider,
SavedSqlFilesProvider,
+ OpenedConnectionsProvider,
} from './utility/globalState';
import { SocketProvider } from './utility/SocketProvider';
+import OpenedConnectionsPinger from './utility/OpnedConnectionsPinger';
function App() {
return (
@@ -16,7 +18,11 @@ function App() {
-
+
+
+
+
+
diff --git a/packages/web/src/appobj/AppObjectList.js b/packages/web/src/appobj/AppObjectList.js
index e388300a7..c36da4cbf 100644
--- a/packages/web/src/appobj/AppObjectList.js
+++ b/packages/web/src/appobj/AppObjectList.js
@@ -38,7 +38,11 @@ function AppObjectListItem({ makeAppObj, data, filter, appobj, onObjectClick, Su
// if (matcher && !matcher(filter)) return null;
if (onObjectClick) appobj.onClick = onObjectClick;
if (SubItems) {
- appobj.onClick = () => setIsExpanded(!isExpanded);
+ const oldClick = appobj.onClick;
+ appobj.onClick = () => {
+ if (oldClick) oldClick();
+ setIsExpanded(!isExpanded);
+ };
}
let res = (
@@ -51,7 +55,11 @@ function AppObjectListItem({ makeAppObj, data, filter, appobj, onObjectClick, Su
prefix={
SubItems ? (
-
+ {appobj.isExpandable ? (
+
+ ) : (
+
+ )}
) : null
}
@@ -83,9 +91,9 @@ function AppObjectGroup({ group, items }) {
- {group} {items && `(${items.filter(x => x.component).length})`}
+ {group} {items && `(${items.filter((x) => x.component).length})`}
- {isExpanded && items.map(x => x.component)}
+ {isExpanded && items.map((x) => x.component)}
>
);
}
@@ -115,7 +123,7 @@ export function AppObjectList({
if (groupFunc) {
const listGrouped = _.compact(
- (list || []).map(data => {
+ (list || []).map((data) => {
const appobj = makeAppObj(data, appObjectParams);
const { matcher } = appobj;
if (matcher && !matcher(filter)) return null;
@@ -125,12 +133,12 @@ export function AppObjectList({
})
);
const groups = _.groupBy(listGrouped, 'group');
- return (groupOrdered || _.keys(groups)).map(group => (
+ return (groupOrdered || _.keys(groups)).map((group) => (
));
}
- return (list || []).map(data => {
+ return (list || []).map((data) => {
const appobj = makeAppObj(data, appObjectParams);
const { matcher } = appobj;
if (matcher && !matcher(filter)) return null;
diff --git a/packages/web/src/appobj/AppObjects.js b/packages/web/src/appobj/AppObjects.js
index 1c49f680f..e52281c49 100644
--- a/packages/web/src/appobj/AppObjects.js
+++ b/packages/web/src/appobj/AppObjects.js
@@ -5,6 +5,7 @@ import React from 'react';
import styled from 'styled-components';
import { showMenu } from '../modals/DropDownMenu';
import { useSetOpenedTabs, useAppObjectParams } from '../utility/globalState';
+import { FontIcon } from '../icons';
const AppObjectDiv = styled.div`
padding: 5px;
@@ -25,6 +26,10 @@ const IconWrap = styled.span`
margin-right: 10px;
`;
+const StatusIconWrap = styled.span`
+ margin-left: 5px;
+`;
+
export function AppObjectCore({
title,
Icon,
@@ -36,6 +41,7 @@ export function AppObjectCore({
isBusy,
component = 'div',
prefix = null,
+ statusIcon,
...other
}) {
const appObjectParams = useAppObjectParams();
@@ -63,6 +69,11 @@ export function AppObjectCore({
{prefix}
{isBusy ? : }
{title}
+ {statusIcon && (
+
+
+
+ )}
);
}
diff --git a/packages/web/src/appobj/connectionAppObject.js b/packages/web/src/appobj/connectionAppObject.js
index 75e2cb2b3..f51b6b0de 100644
--- a/packages/web/src/appobj/connectionAppObject.js
+++ b/packages/web/src/appobj/connectionAppObject.js
@@ -7,34 +7,67 @@ import ConnectionModal from '../modals/ConnectionModal';
import axios from '../utility/axios';
import { filterName } from '@dbgate/datalib';
-function Menu({ data, makeAppObj }) {
+function Menu({ data, setOpenedConnections }) {
const handleEdit = () => {
- showModal(modalState => );
+ showModal((modalState) => );
};
const handleDelete = () => {
axios.post('connections/delete', data);
};
+ const handleRefresh = () => {
+ axios.post('server-connections/refresh', { conid: data._id });
+ };
+ const handleDisconnect = () => {
+ setOpenedConnections((list) => list.filter((x) => x != data._id));
+ };
return (
<>
Edit
Delete
+ Refresh
+ Disconnect
>
);
}
-const connectionAppObject = flags => ({ _id, server, displayName, engine }) => {
+const connectionAppObject = (flags) => (
+ { _id, server, displayName, engine, status },
+ { openedConnections, setOpenedConnections }
+) => {
const title = displayName || server;
const key = _id;
+ const isExpandable = openedConnections.includes(_id);
const Icon = getEngineIcon(engine);
- const matcher = filter => filterName(filter, displayName, server);
+ const matcher = (filter) => filterName(filter, displayName, server);
const { boldCurrentDatabase } = flags || {};
const isBold = boldCurrentDatabase
? ({ currentDatabase }) => {
return _.get(currentDatabase, 'connection._id') == _id;
}
: null;
+ const onClick = () => setOpenedConnections((c) => [...c, _id]);
- return { title, key, Icon, Menu, matcher, isBold };
+ // let isBusy = false;
+ let statusIcon = null;
+ if (openedConnections.includes(_id)) {
+ if (!status) statusIcon = 'fas fa-spinner fa-spin';
+ else if (status.name == 'pending') statusIcon = 'fas fa-spinner fa-spin';
+ else if (status.name == 'ok') statusIcon = 'fas fa-check-circle green';
+ else statusIcon = 'fas fa-times-circle red';
+ }
+
+ return {
+ title,
+ key,
+ Icon,
+ Menu,
+ matcher,
+ isBold,
+ isExpandable,
+ onClick,
+ // isBusy,
+ statusIcon,
+ };
};
export default connectionAppObject;
diff --git a/packages/web/src/icons.js b/packages/web/src/icons.js
index 37c9b7385..2748bd9f7 100644
--- a/packages/web/src/icons.js
+++ b/packages/web/src/icons.js
@@ -27,7 +27,7 @@ export function FontIcon({ icon, ...props }) {
let className = props.className || '';
// if (_.startsWith(name, 'bs-')) className += ` glyphicon glyphicon-${name.substr(3)}`;
- if (type == 'fas' || type == 'far') className += ` ${type} ${name}`;
+ if (type == 'fas' || type == 'far') className += ` ${type} ${name} ${parts.join(' ')}`;
if (_.includes(parts, 'spin')) className += ' fa-spin';
@@ -41,67 +41,73 @@ export function FontIcon({ icon, ...props }) {
return ;
}
-export function ExpandIcon({ isBlank = false, isExpanded = false, isSelected = false, ...other }) {
+export function ExpandIcon({
+ isBlank = false,
+ isExpanded = false,
+ isSelected = false,
+ blankColor = 'white',
+ ...other
+}) {
if (isBlank) {
- return ;
+ return ;
}
return ;
}
-export const TableIcon = props => getIconImage('table2.svg', props);
-export const ViewIcon = props => getIconImage('view2.svg', props);
-export const DatabaseIcon = props => getIconImage('database.svg', props);
-export const ServerIcon = props => getIconImage('server.svg', props);
+export const TableIcon = (props) => getIconImage('table2.svg', props);
+export const ViewIcon = (props) => getIconImage('view2.svg', props);
+export const DatabaseIcon = (props) => getIconImage('database.svg', props);
+export const ServerIcon = (props) => getIconImage('server.svg', props);
-export const MicrosoftIcon = props => getIconImage('microsoft.svg', props);
-export const MySqlIcon = props => getIconImage('mysql.svg', props);
-export const PostgreSqlIcon = props => getIconImage('postgresql.svg', props);
-export const SqliteIcon = props => getIconImage('sqlite.svg', props);
+export const MicrosoftIcon = (props) => getIconImage('microsoft.svg', props);
+export const MySqlIcon = (props) => getIconImage('mysql.svg', props);
+export const PostgreSqlIcon = (props) => getIconImage('postgresql.svg', props);
+export const SqliteIcon = (props) => getIconImage('sqlite.svg', props);
-export const ProcedureIcon = props => getIconImage('procedure2.svg', props);
-export const FunctionIcon = props => getIconImage('function.svg', props);
-export const TriggerIcon = props => getIconImage('trigger.svg', props);
+export const ProcedureIcon = (props) => getIconImage('procedure2.svg', props);
+export const FunctionIcon = (props) => getIconImage('function.svg', props);
+export const TriggerIcon = (props) => getIconImage('trigger.svg', props);
-export const HomeIcon = props => getIconImage('home.svg', props);
-export const PrimaryKeyIcon = props => getIconImage('primarykey.svg', props);
-export const ForeignKeyIcon = props => getIconImage('foreignkey.svg', props);
-export const ComplexKeyIcon = props => getIconImage('complexkey.svg', props);
-export const VariableIcon = props => getIconImage('variable.svg', props);
-export const UniqueIcon = props => getIconImage('unique.svg', props);
-export const IndexIcon = props => getIconImage('index.svg', props);
+export const HomeIcon = (props) => getIconImage('home.svg', props);
+export const PrimaryKeyIcon = (props) => getIconImage('primarykey.svg', props);
+export const ForeignKeyIcon = (props) => getIconImage('foreignkey.svg', props);
+export const ComplexKeyIcon = (props) => getIconImage('complexkey.svg', props);
+export const VariableIcon = (props) => getIconImage('variable.svg', props);
+export const UniqueIcon = (props) => getIconImage('unique.svg', props);
+export const IndexIcon = (props) => getIconImage('index.svg', props);
-export const StartIcon = props => getIconImage('start.svg', props);
-export const DownCircleIcon = props => getIconImage('down_circle.svg', props);
+export const StartIcon = (props) => getIconImage('start.svg', props);
+export const DownCircleIcon = (props) => getIconImage('down_circle.svg', props);
-export const ColumnIcon = props => getIconImage('column.svg', props);
+export const ColumnIcon = (props) => getIconImage('column.svg', props);
-export const SqlIcon = props => getIconImage('sql.svg', props);
-export const ExcelIcon = props => getIconImage('excel.svg', props);
-export const DiagramIcon = props => getIconImage('diagram.svg', props);
-export const QueryDesignIcon = props => getIconImage('querydesign.svg', props);
-export const LocalDbIcon = props => getIconImage('localdb.svg', props);
-export const CsvIcon = props => getIconImage('csv.svg', props);
-export const ChangeSetIcon = props => getIconImage('changeset.svg', props);
-export const BinaryFileIcon = props => getIconImage('binaryfile.svg', props);
+export const SqlIcon = (props) => getIconImage('sql.svg', props);
+export const ExcelIcon = (props) => getIconImage('excel.svg', props);
+export const DiagramIcon = (props) => getIconImage('diagram.svg', props);
+export const QueryDesignIcon = (props) => getIconImage('querydesign.svg', props);
+export const LocalDbIcon = (props) => getIconImage('localdb.svg', props);
+export const CsvIcon = (props) => getIconImage('csv.svg', props);
+export const ChangeSetIcon = (props) => getIconImage('changeset.svg', props);
+export const BinaryFileIcon = (props) => getIconImage('binaryfile.svg', props);
-export const ReferenceIcon = props => getIconImage('reference.svg', props);
-export const LinkIcon = props => getIconImage('link.svg', props);
+export const ReferenceIcon = (props) => getIconImage('reference.svg', props);
+export const LinkIcon = (props) => getIconImage('link.svg', props);
-export const SequenceIcon = props => getIconImage('sequence.svg', props);
-export const CheckIcon = props => getIconImage('check.svg', props);
+export const SequenceIcon = (props) => getIconImage('sequence.svg', props);
+export const CheckIcon = (props) => getIconImage('check.svg', props);
-export const LinkedServerIcon = props => getIconImage('linkedserver.svg', props);
+export const LinkedServerIcon = (props) => getIconImage('linkedserver.svg', props);
-export const EmptyIcon = props => getIconImage('data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=', props);
+export const EmptyIcon = (props) => getIconImage('data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=', props);
-export const TimesRedIcon = props => ;
-export const TimesGreenCircleIcon = props => ;
-export const GrayFilterIcon = props => ;
-export const ExclamationTriangleIcon = props => ;
-export const HourGlassIcon = props => ;
-export const InfoBlueCircleIcon = props => ;
+export const TimesRedIcon = (props) => ;
+export const TimesGreenCircleIcon = (props) => ;
+export const GrayFilterIcon = (props) => ;
+export const ExclamationTriangleIcon = (props) => ;
+export const HourGlassIcon = (props) => ;
+export const InfoBlueCircleIcon = (props) => ;
-export const SpinnerIcon = props => ;
+export const SpinnerIcon = (props) => ;
export function getEngineIcon(engine) {
switch (engine) {
diff --git a/packages/web/src/utility/OpnedConnectionsPinger.js b/packages/web/src/utility/OpnedConnectionsPinger.js
new file mode 100644
index 000000000..a2c9a371f
--- /dev/null
+++ b/packages/web/src/utility/OpnedConnectionsPinger.js
@@ -0,0 +1,14 @@
+import React from 'react';
+import { useOpenedConnections } from './globalState';
+import axios from './axios';
+
+export default function OpenedConnectionsPinger({ children }) {
+ const openedConnections = useOpenedConnections();
+ React.useEffect(() => {
+ const handle = window.setInterval(() => {
+ axios.post('server-connections/ping', { connections: openedConnections });
+ }, 30 * 1000);
+ return () => window.clearInterval(handle);
+ }, [openedConnections]);
+ return children;
+}
diff --git a/packages/web/src/utility/globalState.js b/packages/web/src/utility/globalState.js
index 26d34b04a..39b3dc972 100644
--- a/packages/web/src/utility/globalState.js
+++ b/packages/web/src/utility/globalState.js
@@ -84,15 +84,23 @@ export function useAppObjectParams() {
const newQuery = useNewQuery();
const openedTabs = useOpenedTabs();
const setSavedSqlFiles = useSetSavedSqlFiles();
-
+ const openedConnections = useOpenedConnections();
+ const setOpenedConnections = useSetOpenedConnections();
+
return {
setOpenedTabs,
currentDatabase,
newQuery,
openedTabs,
setSavedSqlFiles,
+ openedConnections,
+ setOpenedConnections,
};
}
const [SavedSqlFilesProvider, useSavedSqlFiles, useSetSavedSqlFiles] = createStorageState('savedSqlFiles', []);
export { SavedSqlFilesProvider, useSavedSqlFiles, useSetSavedSqlFiles };
+
+const [OpenedConnectionsProvider, useOpenedConnections, useSetOpenedConnections] = createGlobalState([]);
+
+export { OpenedConnectionsProvider, useOpenedConnections, useSetOpenedConnections };
diff --git a/packages/web/src/utility/metadataLoaders.js b/packages/web/src/utility/metadataLoaders.js
index 64d446360..2616ea25c 100644
--- a/packages/web/src/utility/metadataLoaders.js
+++ b/packages/web/src/utility/metadataLoaders.js
@@ -33,6 +33,12 @@ const databaseListLoader = ({ conid }) => ({
reloadTrigger: `database-list-changed-${conid}`,
});
+const serverStatusLoader = () => ({
+ url: 'server-connections/server-status',
+ params: {},
+ reloadTrigger: `server-status-changed`,
+});
+
const connectionListLoader = () => ({
url: 'connections/list',
params: {},
@@ -126,6 +132,13 @@ export function useDatabaseList(args) {
return useCore(databaseListLoader, args);
}
+export function getServerStatus() {
+ return getCore(serverStatusLoader, {});
+}
+export function useServerStatus() {
+ return useCore(serverStatusLoader, {});
+}
+
export function getConnectionList() {
return getCore(connectionListLoader, {});
}
diff --git a/packages/web/src/widgets/DatabaseWidget.js b/packages/web/src/widgets/DatabaseWidget.js
index 3186783fb..50c699804 100644
--- a/packages/web/src/widgets/DatabaseWidget.js
+++ b/packages/web/src/widgets/DatabaseWidget.js
@@ -7,7 +7,7 @@ import databaseAppObject from '../appobj/databaseAppObject';
import { useSetCurrentDatabase, useCurrentDatabase } from '../utility/globalState';
import InlineButton from './InlineButton';
import databaseObjectAppObject from '../appobj/databaseObjectAppObject';
-import { useSqlObjectList, useDatabaseList, useConnectionList } from '../utility/metadataLoaders';
+import { useSqlObjectList, useDatabaseList, useConnectionList, useServerStatus } from '../utility/metadataLoaders';
import { SearchBoxWrapper, InnerContainer, Input, MainContainer, OuterContainer, WidgetTitle } from './WidgetStyles';
function SubDatabaseList({ data }) {
@@ -31,6 +31,9 @@ function SubDatabaseList({ data }) {
function ConnectionList() {
const connections = useConnectionList();
+ const serverStatus = useServerStatus();
+ const connectionsWithStatus =
+ connections && serverStatus && connections.map((conn) => ({ ...conn, status: serverStatus[conn._id] }));
const [filter, setFilter] = React.useState('');
return (
@@ -43,7 +46,7 @@ function ConnectionList() {