Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,10 @@ export class DBObjectService extends CachedMapResource<string, DBObject> {
}

async loadChildren(parentId: string, key: ResourceKey<string>) {
if (this.isLoaded(key) && !this.isOutdated(key)) {
return this.data;
}

await this.performUpdate(key, async () => {
if (this.isLoaded(key) && !this.isOutdated(key)) {
return;
}

await this.setActivePromise(key, this.loadFromChildren(parentId));
});
}, () => this.isLoaded(key) && !this.isOutdated(key));

return this.data;
}

Expand Down
29 changes: 28 additions & 1 deletion webapp/packages/core-blocks/src/Table/TableColumnValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,52 @@
*/

import { observer } from 'mobx-react';
import { useCallback, useContext } from 'react';
import styled, { use } from 'reshadow';

import { useStyles } from '@cloudbeaver/core-theming';

import { TableContext } from './TableContext';
import { TableItemContext } from './TableItemContext';

type Props = React.PropsWithChildren<{
align?: 'left' | 'center' | 'right' | 'justify' | 'char';
className?: string;
centerContent?: boolean;
flex?: boolean;
expand?: boolean;
}>

export const TableColumnValue = observer(function TableColumnValue({
align,
children,
centerContent,
flex,
expand,
className,
}: Props) {
const tableContext = useContext(TableContext);
const context = useContext(TableItemContext);
if (!context) {
return null;
}

const handleClick = useCallback((event: React.MouseEvent<HTMLTableDataCellElement, MouseEvent>) => {
if (!expand) {
return;
}

event.stopPropagation();

const state = !context.isExpanded();

tableContext?.setItemExpand(context.item, state);
}, [tableContext, context, expand]);

return styled(useStyles())(
<td align={align} className={className} {...use({ centerContent, flex })}>{children}</td>
<td align={align} className={className} {...use({ centerContent })} onClick={handleClick}>
{flex && <td-flex as='div'>{children}</td-flex>}
{!flex && children}
</td>
);
});
36 changes: 36 additions & 0 deletions webapp/packages/core-blocks/src/Table/TableItemSeparator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/

import { observer } from 'mobx-react';
import styled, { use } from 'reshadow';

import { useStyles } from '@cloudbeaver/core-theming';

type Props = React.PropsWithChildren<{
colSpan?: number;
className?: string;
onClick?: () => void;
onDoubleClick?: () => void;
}>

export const TableItemSeparator = observer(function TableItemSeparator({
colSpan,
children,
className,
onClick,
onDoubleClick,
}: Props) {

return styled(useStyles())(
<tr {...use({ noHover: true })} className={className} onClick={onClick} onDoubleClick={onDoubleClick}>
<td colSpan={colSpan} {...use({ expandArea: true })}>
{children}
</td>
</tr>
);
});
1 change: 1 addition & 0 deletions webapp/packages/core-blocks/src/Table/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './TableItem';
export * from './TableItemContext';
export * from './TableItemExpand';
export * from './TableItemSelect';
export * from './TableItemSeparator';
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import { observer } from 'mobx-react';
import styled, { css, use } from 'reshadow';

import { AdministrationTools } from '@cloudbeaver/core-administration';
import { Loader, IconButton } from '@cloudbeaver/core-blocks';
import { Loader, IconButton, Button } from '@cloudbeaver/core-blocks';
import { useController } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles, composes } from '@cloudbeaver/core-theming';

import { ConnectionsAdministrationController } from './ConnectionsAdministrationController';
import { ConnectionsTable } from './ConnectionsTable/ConnectionsTable';
import { DatabasesSearch } from './DatabasesSearch';

const styles = composes(
css`
Expand Down Expand Up @@ -52,23 +54,48 @@ const styles = composes(
width: 32px;
margin-right: 16px;
}

actions {
padding: 0 12px;
padding-right: 24px;
}
`
);

export const ConnectionsAdministration = observer(function ConnectionsAdministration() {
const translate = useTranslate();
const controller = useController(ConnectionsAdministrationController);

return styled(useStyles(styles))(
<layout-grid as="div">
<layout-grid-inner as="div">
<layout-grid-cell as='div' {...use({ span: 12 })}>
<AdministrationTools>
<actions as='div'>
<Button
type="button"
disabled={controller.isLoading}
mod={['outlined']}
onClick={controller.findDatabase}
>
{translate('connections_connection_edit_search')}
</Button>
</actions>
<IconButton name="add" viewBox="0 0 28 28" onClick={controller.create} />
<IconButton name="trash" viewBox="0 0 28 28" onClick={controller.delete} />
<IconButton name="reload" viewBox="0 0 28 28" onClick={controller.update} />
</AdministrationTools>
{controller.isSearching && (
<DatabasesSearch
hosts={controller.hosts}
onChange={controller.onSearchChange}
onSearch={controller.search}
disabled={controller.isLoading}
/>
)}
<ConnectionsTable
connections={controller.connections}
findConnections={controller.findConnections}
selectedItems={controller.selectedItems}
expandedItems={controller.expandedItems}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,55 @@
* you may not use this file except in compliance with the License.
*/

import { observable } from 'mobx';
import { observable, computed } from 'mobx';

import { injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, ConfirmationDialog } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications';
import { GQLErrorCatcher, resourceKeyList } from '@cloudbeaver/core-sdk';

import { ConnectionsResource } from '../ConnectionsResource';
import { DriverSelectDialog } from '../../DriverSelectDialog/DriverSelectDialog';
import { ConnectionsResource, isSearchedConnection } from '../ConnectionsResource';

@injectable()
export class ConnectionsAdministrationController {
@observable isDeleting = false;
@observable hosts = 'localhost';
@observable isProcessing = false;
@observable isSearching = false;
readonly selectedItems = observable<string, boolean>(new Map())
readonly expandedItems = observable<string, boolean>(new Map())
readonly error = new GQLErrorCatcher();

@computed
get findConnections() {
return Array.from(this.connectionsResource.data.values())
.filter(isSearchedConnection);
}

@computed
get connections() {
return Array.from(this.connectionsResource.data.values())
.filter(connection => !isSearchedConnection(connection))
.sort((a, b) => {
if (this.connectionsResource.isNew(a.id) === this.connectionsResource.isNew(b.id)) {
const isANew = this.connectionsResource.isNew(a.id);
const isBNew = this.connectionsResource.isNew(b.id);

if (isANew === isBNew) {
return 0;
}
if (this.connectionsResource.isNew(a.id)) {
return -1;

if (isBNew) {
return 1;
}
return 1;

return -1;
});
}

@computed
get isLoading() {
return this.connectionsResource.isLoading();
return this.connectionsResource.isLoading() || this.isProcessing;
}

constructor(
Expand All @@ -44,11 +63,45 @@ export class ConnectionsAdministrationController {
private commonDialogService: CommonDialogService,
) { }

create = () => {
const connectionInfo = this.connectionsResource.addNew();
create = async () => {
const driverId = await this.commonDialogService.open(DriverSelectDialog, null);
if (!driverId) {
return;
}

const connectionInfo = this.connectionsResource.addNew(driverId);
this.expandedItems.set(connectionInfo.id, true);
}

findDatabase = () => {
this.isSearching = !this.isSearching;
}

search = async () => {
if (this.isProcessing || !this.hosts || !this.hosts.trim()) {
return;
}

this.isProcessing = true;
for (const connection of this.findConnections) {
this.expandedItems.delete(connection.id);
}

try {
await this.connectionsResource.searchDatabases(this.hosts.trim().split(' '));
} catch (exception) {
if (!this.error.catch(exception)) {
this.notificationService.logException(exception, 'Databases search failed');
}
} finally {
this.isProcessing = false;
}
}

onSearchChange = (hosts: string) => {
this.hosts = hosts;
}

update = async () => {
try {
await this.connectionsResource.refresh('all');
Expand All @@ -60,17 +113,18 @@ export class ConnectionsAdministrationController {
}

delete = async () => {
if (this.isDeleting) {
if (this.isProcessing) {
return;
}

this.isDeleting = true;
this.isProcessing = true;

try {
const deletionList = Array
.from(this.selectedItems)
.filter(([_, value]) => value)
.map(([connectionId]) => connectionId);

if (deletionList.length === 0) {
return;
}
Expand All @@ -86,13 +140,18 @@ export class ConnectionsAdministrationController {

await this.connectionsResource.delete(resourceKeyList(deletionList));
this.selectedItems.clear();

for (const id of deletionList) {
this.expandedItems.delete(id);
}

await this.connectionsResource.refresh('all');
} catch (exception) {
if (!this.error.catch(exception)) {
this.notificationService.logException(exception, 'Connections delete failed');
}
} finally {
this.isDeleting = false;
this.isProcessing = false;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { useTranslate } from '@cloudbeaver/core-localization';
import { ConnectionInfo } from '@cloudbeaver/core-sdk';
import { useStyles } from '@cloudbeaver/core-theming';

import { ConnectionsResource } from '../../ConnectionsResource';
import { ConnectionsResource, isSearchedConnection, SEARCH_CONNECTION_SYMBOL } from '../../ConnectionsResource';
import { ConnectionEdit } from './ConnectionEdit';

type Props = {
Expand All @@ -29,14 +29,30 @@ const styles = css`
StaticImage {
display: flex;
width: 24px;

&:not(:last-child) {
margin-right: 16px;
}
}
TableColumnValue[expand] {
cursor: pointer;
}
`;

export const Connection = observer(function Connection({ connection }: Props) {
const translate = useTranslate();
const connectionInfoResource = useService(ConnectionsResource);
const driversResource = useService(DBDriverResource);
const driver = driversResource.get(connection.driverId);
let drivers = [connection.driverId];

if (isSearchedConnection(connection)) {
drivers = connection[SEARCH_CONNECTION_SYMBOL].possibleDrivers;
}

const icons = drivers
.map(driverId => driversResource.get(driverId)?.icon)
.filter(Boolean);

const isNew = connectionInfoResource.isNew(connection.id);

return styled(useStyles(styles))(
Expand All @@ -45,11 +61,19 @@ export const Connection = observer(function Connection({ connection }: Props) {
<TableItemSelect />
<TableItemExpand />
</TableColumnValue>
<TableColumnValue><StaticImage icon={driver?.icon} /></TableColumnValue>
<TableColumnValue>{connection.name}</TableColumnValue>
<TableColumnValue centerContent flex>
{icons.map(icon => <StaticImage key={icon} icon={icon} />)}
</TableColumnValue>
<TableColumnValue expand>{connection.name}</TableColumnValue>
<TableColumnValue>{connection.host}{connection.host && connection.port && `:${connection.port}`}</TableColumnValue>
<TableColumnValue><input type="checkbox" checked={connection.template} disabled/></TableColumnValue>
<TableColumnValue align='right'>{isNew && <tag as='div' {...use({ mod: 'positive' })}>{translate('ui_tag_new')}</tag>}</TableColumnValue>
<TableColumnValue align='right'>
{isNew && (
<tag as='div' {...use({ mod: 'positive' })}>
{translate('ui_tag_new')}
</tag>)
}
</TableColumnValue>
</TableItem>
);
});
Loading