mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
feat(core): connections search
CB-231
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,15 +7,20 @@
|
||||
*/
|
||||
|
||||
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({
|
||||
@@ -23,9 +28,31 @@ export const TableColumnValue = observer(function TableColumnValue({
|
||||
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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -8,3 +8,4 @@ export * from './TableItem';
|
||||
export * from './TableItemContext';
|
||||
export * from './TableItemExpand';
|
||||
export * from './TableItemSelect';
|
||||
export * from './TableItemSeparator';
|
||||
|
||||
+28
-1
@@ -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`
|
||||
@@ -52,10 +54,16 @@ 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))(
|
||||
@@ -63,12 +71,31 @@ export const ConnectionsAdministration = observer(function ConnectionsAdministra
|
||||
<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}
|
||||
/>
|
||||
|
||||
+72
-13
@@ -6,7 +6,7 @@
|
||||
* 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';
|
||||
@@ -14,28 +14,47 @@ 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(
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+29
-5
@@ -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 = {
|
||||
@@ -29,6 +29,13 @@ const styles = css`
|
||||
StaticImage {
|
||||
display: flex;
|
||||
width: 24px;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
TableColumnValue[expand] {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -36,7 +43,16 @@ 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))(
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
+4
-4
@@ -119,19 +119,19 @@ export const ConnectionEdit = observer(function ConnectionEdit({
|
||||
item,
|
||||
}: Props) {
|
||||
const tableContext = useContext(TableContext);
|
||||
const context = useContext(TableItemContext);
|
||||
const collapse = useCallback(() => tableContext?.setItemExpand(item, false), [tableContext]);
|
||||
|
||||
const translate = useTranslate();
|
||||
const controller = useController(ConnectionEditController, item);
|
||||
const controller = useController(ConnectionEditController, item, collapse);
|
||||
const connectionsResource = useService(ConnectionsResource);
|
||||
const [loadProperties, setLoadProperties] = useState(false);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
tableContext?.setItemExpand(context?.item, false);
|
||||
collapse();
|
||||
if (controller.isNew) {
|
||||
connectionsResource.delete(item);
|
||||
}
|
||||
}, [tableContext, context]);
|
||||
}, [collapse]);
|
||||
|
||||
return styled(useStyles(styles))(
|
||||
<TabsState selectedId='options'>
|
||||
|
||||
+43
-8
@@ -10,7 +10,7 @@ import { observable, action, computed } from 'mobx';
|
||||
|
||||
import { UsersResource, RolesResource } from '@cloudbeaver/core-authentication';
|
||||
import {
|
||||
injectable, IInitializableController, IDestructibleController, Bootstrap
|
||||
injectable, IInitializableController, IDestructibleController
|
||||
} from '@cloudbeaver/core-di';
|
||||
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
|
||||
import { DatabaseAuthModelsResource } from '../../../DatabaseAuthModelsResource';
|
||||
import { DBDriver, DBDriverResource } from '../../../DBDriverResource';
|
||||
import { ConnectionsResource } from '../../ConnectionsResource';
|
||||
import { ConnectionsResource, isSearchedConnection, SEARCH_CONNECTION_SYMBOL } from '../../ConnectionsResource';
|
||||
|
||||
export enum ConnectionType {
|
||||
Attributes,
|
||||
@@ -62,12 +62,23 @@ implements IInitializableController, IDestructibleController {
|
||||
return this.isLoading || this.isSaving;
|
||||
}
|
||||
|
||||
get isSearched() {
|
||||
return this.connectionsResource.isSearched(this.connectionId);
|
||||
}
|
||||
|
||||
get isNew() {
|
||||
return this.connectionsResource.isNew(this.connectionId);
|
||||
}
|
||||
|
||||
get drivers() {
|
||||
return Array.from(this.dbDriverResource.data.values());
|
||||
return Array.from(this.dbDriverResource.data.values())
|
||||
.filter(({ id }) => {
|
||||
if (!isSearchedConnection(this.connectionInfo)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.connectionInfo[SEARCH_CONNECTION_SYMBOL].possibleDrivers.includes(id);
|
||||
});
|
||||
}
|
||||
|
||||
connectionId!: string;
|
||||
@@ -78,6 +89,7 @@ implements IInitializableController, IDestructibleController {
|
||||
private accessLoaded = false;
|
||||
private isDistructed = false;
|
||||
private connectionInfo!: ConnectionInfo;
|
||||
private collapse!: () => void;
|
||||
|
||||
constructor(
|
||||
private connectionsResource: ConnectionsResource,
|
||||
@@ -89,8 +101,9 @@ implements IInitializableController, IDestructibleController {
|
||||
private dbDriverResource: DBDriverResource,
|
||||
) { }
|
||||
|
||||
init(id: string) {
|
||||
init(id: string, collapse: () => void) {
|
||||
this.connectionId = id;
|
||||
this.collapse = collapse;
|
||||
this.loadConnectionInfo();
|
||||
}
|
||||
|
||||
@@ -112,6 +125,7 @@ implements IInitializableController, IDestructibleController {
|
||||
|
||||
onSelectDriver = (driver: DBDriver | null) => {
|
||||
this.driver = driver;
|
||||
this.onChange('driverId', this.driver?.id);
|
||||
if (driver) {
|
||||
this.loadDriver(driver.id);
|
||||
} else {
|
||||
@@ -126,7 +140,7 @@ implements IInitializableController, IDestructibleController {
|
||||
if (this.isNew) {
|
||||
const connection = await this.connectionsResource.create(this.getConnectionConfig(), this.connectionId);
|
||||
await this.saveSubjectPermissions(connection.id);
|
||||
|
||||
this.collapse();
|
||||
this.notificationService.logInfo({ title: `Connection ${connection.name} created` });
|
||||
} else {
|
||||
const connection = await this.connectionsResource.update(this.connectionId, this.getConnectionConfig());
|
||||
@@ -216,19 +230,40 @@ implements IInitializableController, IDestructibleController {
|
||||
|
||||
@action
|
||||
private setDefaults() {
|
||||
this.onChange('name', this.connectionInfo?.name || (this.driver ? `${this.driver?.name} (custom)` : ''));
|
||||
this.onChange('name', this.getNameTemplate());
|
||||
this.onChange('description', this.connectionInfo?.description || '');
|
||||
this.onChange('template', this.connectionInfo?.template);
|
||||
this.onChange('driverId', this.connectionInfo?.driverId || this.driver?.id || '');
|
||||
this.onChange('host', this.connectionInfo?.host || '');
|
||||
this.onChange('host', this.connectionInfo?.host || this.driver?.defaultServer || '');
|
||||
this.onChange('port', this.connectionInfo?.port || this.driver?.defaultPort || '');
|
||||
this.onChange('databaseName', this.connectionInfo?.databaseName || '');
|
||||
this.onChange('databaseName', this.connectionInfo?.databaseName || this.driver?.defaultDatabase || '');
|
||||
this.onChange('url', this.connectionInfo?.url || this.driver?.sampleURL || '');
|
||||
this.onChange('properties', this.connectionInfo?.properties || {});
|
||||
this.onChange('authModelId', this.connectionInfo?.authModel || this.driver?.defaultAuthModel);
|
||||
this.onChange('credentials', {});
|
||||
}
|
||||
|
||||
private getNameTemplate() {
|
||||
if (this.connectionInfo.name) {
|
||||
return this.connectionInfo.name;
|
||||
}
|
||||
|
||||
if (this.driver) {
|
||||
const address = this.getConnectionAddress();
|
||||
return `${this.driver.name}${address ? ` (${address})` : ' connection'}`;
|
||||
}
|
||||
|
||||
return 'New connection';
|
||||
}
|
||||
|
||||
private getConnectionAddress() {
|
||||
if (!this.connectionInfo.host) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `${this.connectionInfo.host}${this.connectionInfo.port ? `:${this.connectionInfo.port}` : ''}`;
|
||||
}
|
||||
|
||||
private showError(exception: Error, message: string) {
|
||||
if (!this.error.catch(exception) || this.isDistructed) {
|
||||
this.notificationService.logException(exception, message);
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ export const ConnectionForm = observer(function ConnectionForm({
|
||||
keySelector={driver => driver.id}
|
||||
valueSelector={driver => driver?.name!}
|
||||
onSelect={controller.onSelectDriver}
|
||||
readOnly={!controller.isNew}
|
||||
readOnly={!controller.isSearched || controller.drivers.length < 2}
|
||||
mod={'surface'}
|
||||
>
|
||||
{translate('connections_connection_driver')}
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ import { ConnectionConfig, DatabaseAuthModel } from '@cloudbeaver/core-sdk';
|
||||
import { ConnectionType } from '../ConnectionEditController';
|
||||
|
||||
export interface IFormController {
|
||||
isSearched: boolean;
|
||||
isNew: boolean;
|
||||
connectionId: string;
|
||||
drivers: DBDriver[];
|
||||
|
||||
+15
-2
@@ -10,35 +10,46 @@ import { observer } from 'mobx-react';
|
||||
import styled, { css, use } from 'reshadow';
|
||||
|
||||
import {
|
||||
Table, TableHeader, TableColumnHeader, TableBody
|
||||
Table, TableHeader, TableColumnHeader, TableBody, TableItemSeparator
|
||||
} from '@cloudbeaver/core-blocks';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { ConnectionInfo } from '@cloudbeaver/core-sdk';
|
||||
import { useStyles, composes } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { ConnectionSearch } from '../../ConnectionsResource';
|
||||
import { Connection } from './Connection';
|
||||
|
||||
const styles = composes(
|
||||
css``,
|
||||
css`
|
||||
TableItemSeparator {
|
||||
composes: theme-background-secondary from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
TableColumnHeader {
|
||||
border-top: solid 1px;
|
||||
}
|
||||
TableItemSeparator {
|
||||
text-align: center;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
type Props = {
|
||||
connections: ConnectionInfo[];
|
||||
findConnections: ConnectionSearch[];
|
||||
selectedItems: Map<string, boolean>;
|
||||
expandedItems: Map<string, boolean>;
|
||||
}
|
||||
|
||||
export const ConnectionsTable = observer(function ConnectionsTable({
|
||||
connections,
|
||||
findConnections,
|
||||
selectedItems,
|
||||
expandedItems,
|
||||
}: Props) {
|
||||
const translate = useTranslate();
|
||||
|
||||
return styled(useStyles(styles))(
|
||||
<Table selectedItems={selectedItems} expandedItems={expandedItems} {...use({ size: 'big' })}>
|
||||
<TableHeader>
|
||||
@@ -50,6 +61,8 @@ export const ConnectionsTable = observer(function ConnectionsTable({
|
||||
<TableColumnHeader></TableColumnHeader>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{findConnections.map(connection => <Connection key={connection.id} connection={connection}/>)}
|
||||
{!!findConnections.length && <TableItemSeparator key='search' colSpan={6}></TableItemSeparator>}
|
||||
{connections.map(connection => <Connection key={connection.id} connection={connection}/>)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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, { css } from 'reshadow';
|
||||
|
||||
import { Button, InputField } from '@cloudbeaver/core-blocks';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { useStyles, composes } from '@cloudbeaver/core-theming';
|
||||
|
||||
const styles = composes(
|
||||
css`
|
||||
database-search {
|
||||
composes: theme-border-color-background from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
database-search {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
border-top: solid 1px;
|
||||
}
|
||||
|
||||
group {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
action {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
InputField {
|
||||
width: 450px;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
type Props = {
|
||||
hosts: string;
|
||||
className?: string;
|
||||
onChange(hosts: string): void;
|
||||
onSearch(): void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const DatabasesSearch = observer(function DatabasesSearch({
|
||||
hosts,
|
||||
className,
|
||||
onChange,
|
||||
onSearch,
|
||||
disabled,
|
||||
}: Props) {
|
||||
const translate = useTranslate();
|
||||
|
||||
return styled(useStyles(styles))(
|
||||
<database-search as='div' className={className}>
|
||||
<group as="div">
|
||||
<InputField
|
||||
name="hosts"
|
||||
value={hosts}
|
||||
placeholder='localhost 127.0.0.1'
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
mod='surface'
|
||||
>
|
||||
{translate('connections_connection_edit_search_hosts')}
|
||||
</InputField>
|
||||
</group>
|
||||
<action as='div'>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
mod={['outlined']}
|
||||
onClick={onSearch}
|
||||
>
|
||||
{translate('connections_connection_edit_search')}
|
||||
</Button>
|
||||
</action>
|
||||
</database-search>
|
||||
);
|
||||
});
|
||||
@@ -15,18 +15,30 @@ import {
|
||||
ResourceKey,
|
||||
isResourceKeyList,
|
||||
AdminConnectionGrantInfo,
|
||||
AdminConnectionSearchInfo,
|
||||
} from '@cloudbeaver/core-sdk';
|
||||
import { uuid, MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
const NEW_CONNECTION_SYMBOL = Symbol('new-connection');
|
||||
type ConnectionNew = ConnectionInfo & { [NEW_CONNECTION_SYMBOL]: boolean }
|
||||
import { DBDriverResource } from '../DBDriverResource';
|
||||
|
||||
export const NEW_CONNECTION_SYMBOL = Symbol('new-connection');
|
||||
export const SEARCH_CONNECTION_SYMBOL = Symbol('search-connection');
|
||||
|
||||
export type ConnectionNew = ConnectionInfo & { [NEW_CONNECTION_SYMBOL]: boolean }
|
||||
export type ConnectionSearch = ConnectionNew & { [SEARCH_CONNECTION_SYMBOL]: AdminConnectionSearchInfo }
|
||||
|
||||
@injectable()
|
||||
export class ConnectionsResource extends CachedMapResource<string, ConnectionInfo> {
|
||||
private metadata: MetadataMap<string, boolean>;
|
||||
constructor(private graphQLService: GraphQLService) {
|
||||
private searchedDatabases: string[];
|
||||
|
||||
constructor(
|
||||
private graphQLService: GraphQLService,
|
||||
private dbDriverResource: DBDriverResource
|
||||
) {
|
||||
super(new Map());
|
||||
this.metadata = new MetadataMap(() => false);
|
||||
this.searchedDatabases = [];
|
||||
}
|
||||
|
||||
has(id: string) {
|
||||
@@ -44,10 +56,15 @@ export class ConnectionsResource extends CachedMapResource<string, ConnectionInf
|
||||
return NEW_CONNECTION_SYMBOL in this.get(id)!;
|
||||
}
|
||||
|
||||
addNew() {
|
||||
isSearched(id: string) {
|
||||
return isSearchedConnection(this.get(id));
|
||||
}
|
||||
|
||||
addNew(driverId: string) {
|
||||
const connectionInfo = {
|
||||
id: `new-${uuid()}`,
|
||||
name: 'New connection',
|
||||
id: uuid(),
|
||||
driverId,
|
||||
// name: 'New connection',
|
||||
[NEW_CONNECTION_SYMBOL]: true,
|
||||
} as ConnectionNew;
|
||||
|
||||
@@ -62,6 +79,10 @@ export class ConnectionsResource extends CachedMapResource<string, ConnectionInf
|
||||
return this.data;
|
||||
}
|
||||
|
||||
async searchDatabases(hosts: string[]) {
|
||||
await this.performUpdate('search', () => this.searchConnections(hosts));
|
||||
}
|
||||
|
||||
async create(config: ConnectionConfig, id?: string) {
|
||||
const { connection } = await this.graphQLService.gql.createConnectionConfiguration({ config });
|
||||
|
||||
@@ -74,28 +95,12 @@ export class ConnectionsResource extends CachedMapResource<string, ConnectionInf
|
||||
}
|
||||
|
||||
async update(id: string, config: ConnectionConfig) {
|
||||
await this.performUpdate(id, async () => {
|
||||
await this.setActivePromise<void>(id, this.updateConnection(id, config));
|
||||
});
|
||||
await this.performUpdate(id, () => this.updateConnection(id, config));
|
||||
return this.get(id)!;
|
||||
}
|
||||
|
||||
async delete(key: ResourceKey<string>) {
|
||||
if (isResourceKeyList(key)) {
|
||||
for (let i = 0; i < key.list.length; i++) {
|
||||
this.data.delete(key.list[i]);
|
||||
if (!this.isNew(key.list[i])) {
|
||||
await this.graphQLService.gql.deleteConnectionConfiguration({ id: key.list[i] });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.data.delete(key);
|
||||
if (!this.isNew(key)) {
|
||||
await this.graphQLService.gql.deleteConnectionConfiguration({ id: key });
|
||||
}
|
||||
}
|
||||
this.markUpdated(key);
|
||||
this.itemDeleteSubject.next(key);
|
||||
await this.performUpdate(key, () => this.deleteConnectionTask(key));
|
||||
}
|
||||
|
||||
async loadAccessSubjects(connectionId: string): Promise<AdminConnectionGrantInfo[]> {
|
||||
@@ -112,6 +117,12 @@ export class ConnectionsResource extends CachedMapResource<string, ConnectionInf
|
||||
await this.graphQLService.gql.setConnectionAccess({ connectionId, subjects });
|
||||
}
|
||||
|
||||
cleanSearchDatabases() {
|
||||
for (const id of this.searchedDatabases) {
|
||||
this.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
protected async loader(key: ResourceKey<string>): Promise<Map<string, ConnectionInfo>> {
|
||||
const { connections } = await this.graphQLService.gql.getConnections();
|
||||
this.data.clear();
|
||||
@@ -128,9 +139,67 @@ export class ConnectionsResource extends CachedMapResource<string, ConnectionInf
|
||||
return this.data;
|
||||
}
|
||||
|
||||
private async searchConnections(hosts: string[]) {
|
||||
const { databases } = await this.graphQLService.gql.searchDatabases({ hosts });
|
||||
|
||||
this.cleanSearchDatabases();
|
||||
|
||||
for (const database of databases) {
|
||||
const connectionInfo = {
|
||||
id: uuid(),
|
||||
driverId: database.defaultDriver,
|
||||
name: await this.getNameTemplate(database),
|
||||
host: database.host,
|
||||
port: `${database.port}`,
|
||||
[NEW_CONNECTION_SYMBOL]: true,
|
||||
[SEARCH_CONNECTION_SYMBOL]: database,
|
||||
} as ConnectionSearch;
|
||||
|
||||
this.data.set(connectionInfo.id, connectionInfo);
|
||||
this.markUpdated(connectionInfo.id);
|
||||
this.searchedDatabases.push(connectionInfo.id);
|
||||
}
|
||||
}
|
||||
|
||||
private async getNameTemplate(connection: AdminConnectionSearchInfo) {
|
||||
const driver = await this.dbDriverResource.load(connection.defaultDriver);
|
||||
|
||||
return `${driver.name} (${connection.host}:${connection.port})`;
|
||||
}
|
||||
|
||||
private async deleteConnectionTask(key: ResourceKey<string>) {
|
||||
if (isResourceKeyList(key)) {
|
||||
for (let i = 0; i < key.list.length; i++) {
|
||||
await this.deleteConnection(key.list[i]);
|
||||
}
|
||||
} else {
|
||||
await this.deleteConnection(key);
|
||||
}
|
||||
this.itemDeleteSubject.next(key);
|
||||
}
|
||||
|
||||
private async deleteConnection(connectionId: string) {
|
||||
if (!this.data.has(connectionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isNew = this.isNew(connectionId);
|
||||
this.data.delete(connectionId);
|
||||
|
||||
if (!isNew) {
|
||||
await this.graphQLService.gql.deleteConnectionConfiguration({ id: connectionId });
|
||||
}
|
||||
}
|
||||
|
||||
private async updateConnection(id: string, config: ConnectionConfig) {
|
||||
const { connection } = await this.graphQLService.gql.updateConnectionConfiguration({ id, config });
|
||||
|
||||
this.set(id, connection as ConnectionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
export function isSearchedConnection(
|
||||
connection: ConnectionInfo | undefined
|
||||
): connection is ConnectionSearch {
|
||||
return !!connection && SEARCH_CONNECTION_SYMBOL in connection;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export class ConnectionInfoResource extends CachedMapResource<string, Connection
|
||||
|
||||
async init(id: string, credentials?: any): Promise<Connection> {
|
||||
await this.performUpdate(id, async () => {
|
||||
const connection = await this.setActivePromise(id, this.initConnection(id, credentials));
|
||||
const connection = await this.initConnection(id, credentials);
|
||||
this.set(id, connection);
|
||||
return connection;
|
||||
});
|
||||
@@ -41,7 +41,7 @@ export class ConnectionInfoResource extends CachedMapResource<string, Connection
|
||||
|
||||
async close(connectionId: string) {
|
||||
await this.performUpdate(connectionId, async () => {
|
||||
const connection = await this.setActivePromise(connectionId, this.closeConnection(connectionId));
|
||||
const connection = await this.closeConnection(connectionId);
|
||||
this.set(connectionId, connection);
|
||||
});
|
||||
|
||||
@@ -63,7 +63,7 @@ export class ConnectionInfoResource extends CachedMapResource<string, Connection
|
||||
}
|
||||
|
||||
return this.performUpdate(connectionId, async () => {
|
||||
connection.authProperties = await this.setActivePromise(connectionId, this.getAuthProperties(connectionId));
|
||||
connection.authProperties = await this.getAuthProperties(connectionId);
|
||||
this.set(connectionId, connection);
|
||||
|
||||
return connection.authProperties!;
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
CachedMapResource,
|
||||
DriverInfo,
|
||||
} from '@cloudbeaver/core-sdk';
|
||||
import { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
export type DBDriver = Pick<
|
||||
DriverInfo,
|
||||
@@ -20,6 +21,9 @@ export type DBDriver = Pick<
|
||||
| 'icon'
|
||||
| 'description'
|
||||
| 'defaultPort'
|
||||
| 'defaultDatabase'
|
||||
| 'defaultServer'
|
||||
| 'defaultUser'
|
||||
| 'sampleURL'
|
||||
| 'embedded'
|
||||
| 'anonymousAccess'
|
||||
@@ -29,9 +33,19 @@ export type DBDriver = Pick<
|
||||
|
||||
@injectable()
|
||||
export class DBDriverResource extends CachedMapResource<string, DBDriver> {
|
||||
private metadata: MetadataMap<string, boolean>;
|
||||
|
||||
constructor(private graphQLService: GraphQLService) {
|
||||
super(new Map());
|
||||
this.metadata = new MetadataMap(() => false);
|
||||
}
|
||||
|
||||
has(id: string) {
|
||||
if (this.metadata.has(id)) {
|
||||
return this.metadata.get(id);
|
||||
}
|
||||
|
||||
return this.data.has(id);
|
||||
}
|
||||
|
||||
async loadAll() {
|
||||
@@ -47,8 +61,12 @@ export class DBDriverResource extends CachedMapResource<string, DBDriver> {
|
||||
for (const driver of driverList) {
|
||||
this.set(driver.id, driver);
|
||||
}
|
||||
// this.data.set('all', {} as any);
|
||||
this.markUpdated(key);
|
||||
|
||||
// TODO: driverList must accept driverId, so we can update some drivers or all drivers,
|
||||
// here we should check is it's was a full update
|
||||
this.metadata.set('all', true);
|
||||
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 { useCallback } from 'react';
|
||||
|
||||
import { ListItem } from '@cloudbeaver/core-blocks';
|
||||
|
||||
export interface IDriver {
|
||||
id: string;
|
||||
icon?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
type DriverProps = {
|
||||
driver: IDriver;
|
||||
onSelect(driverId: string): void;
|
||||
}
|
||||
|
||||
export const Driver = observer(function Driver({ driver, onSelect }: DriverProps) {
|
||||
const select = useCallback(() => onSelect(driver.id), [driver]);
|
||||
|
||||
return <ListItem icon={driver.icon} name={driver.name} description={driver.description} onClick={select}/>;
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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 { computed } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { Loader } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { CommonDialogWrapper, DialogComponentProps } from '@cloudbeaver/core-dialogs';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
|
||||
import { DBDriverResource } from '../DBDriverResource';
|
||||
import { DriverSelector } from './DriverSelector';
|
||||
|
||||
const styles = css`
|
||||
CommonDialogWrapper {
|
||||
max-height: 550px;
|
||||
min-height: 550px;
|
||||
}
|
||||
DriverSelector {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DriverSelectDialog = observer(function DriverSelectDialog({
|
||||
resolveDialog,
|
||||
rejectDialog,
|
||||
}: DialogComponentProps<null, string>) {
|
||||
const dbDriverResource = useService(DBDriverResource);
|
||||
const title = useTranslate('connections_administration_new_connection');
|
||||
|
||||
useEffect(() => { dbDriverResource.loadAll(); }, []);
|
||||
const isLoading = dbDriverResource.isLoading();
|
||||
const drivers = useMemo(() => computed(() => Array.from(dbDriverResource.data.values())), [dbDriverResource.data]);
|
||||
|
||||
return styled(styles)(
|
||||
<CommonDialogWrapper
|
||||
title={title}
|
||||
noBodyPadding
|
||||
onReject={rejectDialog}
|
||||
>
|
||||
{isLoading && <Loader />}
|
||||
{!isLoading && <DriverSelector drivers={drivers.get()} onSelect={resolveDialog}/>}
|
||||
</CommonDialogWrapper>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 { useState, useMemo } from 'react';
|
||||
|
||||
import { ItemListSearch, ItemList } from '@cloudbeaver/core-blocks';
|
||||
|
||||
import { Driver, IDriver } from './Driver';
|
||||
|
||||
type DriverSelectorProps = {
|
||||
drivers: IDriver[];
|
||||
className?: string;
|
||||
onSelect(driverId: string): void;
|
||||
}
|
||||
|
||||
export const DriverSelector = observer(function DriverSelector({ drivers, className, onSelect }: DriverSelectorProps) {
|
||||
const [search, setSearch] = useState('');
|
||||
const filteredDrivers = useMemo(() => {
|
||||
if (!search) {
|
||||
return drivers;
|
||||
}
|
||||
return drivers.filter(driver => driver.name?.toUpperCase().includes(search.toUpperCase()));
|
||||
}, [search, drivers]);
|
||||
|
||||
return (
|
||||
<ItemList className={className}>
|
||||
<ItemListSearch onSearch={setSearch} />
|
||||
{filteredDrivers.map(driver => <Driver key={driver.id} driver={driver} onSelect={onSelect}/>)}
|
||||
</ItemList>
|
||||
);
|
||||
});
|
||||
@@ -1,9 +1,12 @@
|
||||
export default [
|
||||
['connections_administration_item', 'Connection Management'],
|
||||
['connections_administration_new_connection', 'New connection'],
|
||||
['connections_connection_edit_authentication', 'Authentication'],
|
||||
['connections_connection_edit_access', 'Access'],
|
||||
['connections_connection_edit_access_load_failed', 'Connection access loading failed'],
|
||||
['connections_connection_edit_access_role', 'Role'],
|
||||
['connections_connection_edit_search', 'Search'],
|
||||
['connections_connection_edit_search_hosts', 'Hosts'],
|
||||
['connections_connection_address', 'Address'],
|
||||
['connections_connection_name', 'Name'],
|
||||
['connections_connection_description', 'Description'],
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
export default [
|
||||
['connections_administration_item', 'Управление подключениями'],
|
||||
['connections_administration_new_connection', 'Создание подключения'],
|
||||
['connections_connection_edit_authentication', 'Авторизация'],
|
||||
['connections_connection_edit_access', 'Доступ'],
|
||||
['connections_connection_edit_access_load_failed', 'Не удалось загрузить информацию доступа'],
|
||||
['connections_connection_edit_access_role', 'Роль'],
|
||||
['connections_connection_edit_search', 'Поиск'],
|
||||
['connections_connection_edit_search_hosts', 'Хосты'],
|
||||
['connections_connection_address', 'Адрес'],
|
||||
['connections_connection_name', 'Название'],
|
||||
['connections_connection_description', 'Описапние'],
|
||||
|
||||
@@ -68,12 +68,32 @@ export abstract class CachedResource<
|
||||
|
||||
protected abstract loader(param: TParam): Promise<TData>;
|
||||
|
||||
protected async performUpdate<T>(param: TParam, update: (param: TParam) => Promise<T>): Promise<T> {
|
||||
protected async performUpdate<T>(
|
||||
param: TParam,
|
||||
update: (param: TParam) => Promise<T>,
|
||||
): Promise<T>
|
||||
protected async performUpdate<T>(
|
||||
param: TParam,
|
||||
update: (param: TParam) => Promise<T>,
|
||||
exitCheck: () => boolean
|
||||
): Promise<T | undefined>;
|
||||
|
||||
protected async performUpdate<T>(
|
||||
param: TParam,
|
||||
update: (param: TParam) => Promise<T>,
|
||||
exitCheck?: () => boolean
|
||||
): Promise<T | undefined> {
|
||||
if (exitCheck && exitCheck()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.waitActive();
|
||||
this.markOutdated(param);
|
||||
|
||||
if (exitCheck && exitCheck()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.setActivePromise(param, update(param));
|
||||
this.markUpdated(param);
|
||||
this.dataSubject.next(this.data);
|
||||
|
||||
return result;
|
||||
@@ -99,7 +119,11 @@ export abstract class CachedResource<
|
||||
this.activePromise = promise;
|
||||
this.activePromiseParam = param;
|
||||
try {
|
||||
return await this.activePromise;
|
||||
this.markOutdated(param);
|
||||
const result = await this.activePromise;
|
||||
this.markUpdated(param);
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
this.activePromise = null;
|
||||
this.activePromiseParam = null;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
query searchDatabases($hosts: [String!]!) {
|
||||
databases: searchConnections(hostNames: $hosts) {
|
||||
host
|
||||
port
|
||||
possibleDrivers
|
||||
defaultDriver
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ query driverList {
|
||||
icon
|
||||
description
|
||||
defaultPort
|
||||
defaultDatabase
|
||||
defaultServer
|
||||
defaultUser
|
||||
sampleURL
|
||||
embedded
|
||||
anonymousAccess
|
||||
|
||||
@@ -468,6 +468,9 @@ export type DriverInfo = {
|
||||
providerId?: Maybe<Scalars['ID']>;
|
||||
driverClassName?: Maybe<Scalars['String']>;
|
||||
defaultPort?: Maybe<Scalars['String']>;
|
||||
defaultDatabase?: Maybe<Scalars['String']>;
|
||||
defaultServer?: Maybe<Scalars['String']>;
|
||||
defaultUser?: Maybe<Scalars['String']>;
|
||||
sampleURL?: Maybe<Scalars['String']>;
|
||||
driverInfoURL?: Maybe<Scalars['String']>;
|
||||
driverPropertiesURL?: Maybe<Scalars['String']>;
|
||||
@@ -910,6 +913,12 @@ export type GetConnectionsQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type GetConnectionsQuery = { connections: Array<Pick<ConnectionInfo, 'id' | 'name' | 'description' | 'driverId' | 'template' | 'connected' | 'readOnly' | 'host' | 'port' | 'databaseName' | 'url' | 'properties' | 'features' | 'authNeeded' | 'authModel'>> };
|
||||
|
||||
export type SearchDatabasesQueryVariables = Exact<{
|
||||
hosts: Array<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
export type SearchDatabasesQuery = { databases: Array<Pick<AdminConnectionSearchInfo, 'host' | 'port' | 'possibleDrivers' | 'defaultDriver'>> };
|
||||
|
||||
export type SetConnectionAccessQueryVariables = Exact<{
|
||||
connectionId: Scalars['ID'];
|
||||
subjects: Array<Scalars['ID']>;
|
||||
@@ -962,7 +971,7 @@ export type DeleteConnectionMutation = Pick<Mutation, 'deleteConnection'>;
|
||||
|
||||
export type DriverListQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type DriverListQuery = { driverList: Array<Pick<DriverInfo, 'id' | 'name' | 'icon' | 'description' | 'defaultPort' | 'sampleURL' | 'embedded' | 'anonymousAccess' | 'promotedScore' | 'defaultAuthModel'>> };
|
||||
export type DriverListQuery = { driverList: Array<Pick<DriverInfo, 'id' | 'name' | 'icon' | 'description' | 'defaultPort' | 'defaultDatabase' | 'defaultServer' | 'defaultUser' | 'sampleURL' | 'embedded' | 'anonymousAccess' | 'promotedScore' | 'defaultAuthModel'>> };
|
||||
|
||||
export type DriverPropertiesQueryVariables = Exact<{
|
||||
driverId: Scalars['ID'];
|
||||
@@ -1457,6 +1466,16 @@ export const GetConnectionsDocument = `
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const SearchDatabasesDocument = `
|
||||
query searchDatabases($hosts: [String!]!) {
|
||||
databases: searchConnections(hostNames: $hosts) {
|
||||
host
|
||||
port
|
||||
possibleDrivers
|
||||
defaultDriver
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const SetConnectionAccessDocument = `
|
||||
query setConnectionAccess($connectionId: ID!, $subjects: [ID!]!) {
|
||||
setConnectionSubjectAccess(connectionId: $connectionId, subjects: $subjects)
|
||||
@@ -1573,6 +1592,9 @@ export const DriverListDocument = `
|
||||
icon
|
||||
description
|
||||
defaultPort
|
||||
defaultDatabase
|
||||
defaultServer
|
||||
defaultUser
|
||||
sampleURL
|
||||
embedded
|
||||
anonymousAccess
|
||||
@@ -2171,6 +2193,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
|
||||
getConnections(variables?: GetConnectionsQueryVariables): Promise<GetConnectionsQuery> {
|
||||
return withWrapper(() => client.request<GetConnectionsQuery>(GetConnectionsDocument, variables));
|
||||
},
|
||||
searchDatabases(variables: SearchDatabasesQueryVariables): Promise<SearchDatabasesQuery> {
|
||||
return withWrapper(() => client.request<SearchDatabasesQuery>(SearchDatabasesDocument, variables));
|
||||
},
|
||||
setConnectionAccess(variables: SetConnectionAccessQueryVariables): Promise<SetConnectionAccessQuery> {
|
||||
return withWrapper(() => client.request<SetConnectionAccessQuery>(SetConnectionAccessDocument, variables));
|
||||
},
|
||||
|
||||
@@ -78,12 +78,12 @@
|
||||
padding: 0 16px;
|
||||
transition: padding ease-in-out 0.24s;
|
||||
|
||||
&[use|centerContent] {
|
||||
&[use|centerContent] > td-flex {
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&[use|flex] {
|
||||
> td-flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -86,7 +86,10 @@ export class UsersAdministrationController {
|
||||
|
||||
await this.usersResource.delete(resourceKeyList(deletionList));
|
||||
this.selectedItems.clear();
|
||||
await this.usersResource.loadAll();
|
||||
|
||||
for (const id of deletionList) {
|
||||
this.expandedItems.delete(id);
|
||||
}
|
||||
} catch (exception) {
|
||||
if (!this.error.catch(exception)) {
|
||||
this.notificationService.logException(exception, 'User delete failed');
|
||||
|
||||
@@ -16,10 +16,16 @@ import {
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { AdminUserInfo } from '@cloudbeaver/core-sdk';
|
||||
import { useStyles, composes } from '@cloudbeaver/core-theming';
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { UserEdit } from './UserEdit';
|
||||
|
||||
const styles = css`
|
||||
TableColumnValue[expand] {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
user: AdminUserInfo;
|
||||
}
|
||||
@@ -29,13 +35,13 @@ export const User = observer(function User({ user }: Props) {
|
||||
const usersResource = useService(UsersResource);
|
||||
const isNew = usersResource.isNew(user.userId);
|
||||
|
||||
return styled(useStyles())(
|
||||
return styled(useStyles(styles))(
|
||||
<TableItem item={user.userId} expandElement={UserEdit}>
|
||||
<TableColumnValue centerContent flex>
|
||||
<TableItemSelect />
|
||||
<TableItemExpand />
|
||||
</TableColumnValue>
|
||||
<TableColumnValue>{isNew ? translate('authentication_administration_user_connections_user_new') : user.userId}</TableColumnValue>
|
||||
<TableColumnValue expand>{isNew ? translate('authentication_administration_user_connections_user_new') : user.userId}</TableColumnValue>
|
||||
<TableColumnValue>{user.grantedRoles.join(', ')}</TableColumnValue>
|
||||
<TableColumnValue align='right'>{isNew && <tag as='div' {...use({ mod: 'positive' })}>{translate('ui_tag_new')}</tag>}</TableColumnValue>
|
||||
</TableItem>
|
||||
|
||||
+5
-3
@@ -137,18 +137,20 @@ export const UserEdit = observer(function UserEdit({
|
||||
}: Props) {
|
||||
const tableContext = useContext(TableContext);
|
||||
const context = useContext(TableItemContext);
|
||||
const collapse = useCallback(() => tableContext?.setItemExpand(item, false), [tableContext]);
|
||||
|
||||
const translate = useTranslate();
|
||||
const controller = useController(UserEditController, item);
|
||||
const controller = useController(UserEditController, item, collapse);
|
||||
const usersResource = useService(UsersResource);
|
||||
const [focusedRef] = useFocus({ focusFirstChild: true });
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
tableContext?.setItemExpand(context?.item, false);
|
||||
collapse();
|
||||
|
||||
if (controller.isNew) {
|
||||
usersResource.delete(item);
|
||||
}
|
||||
}, [tableContext, context]);
|
||||
}, [collapse]);
|
||||
|
||||
const handleLoginChange = useCallback(
|
||||
(value: string) => controller.credentials.login = value,
|
||||
|
||||
+4
-1
@@ -53,6 +53,7 @@ export class UserEditController implements IInitializableController, IDestructib
|
||||
private userId!: string;
|
||||
private connectionAccessChanged = false;
|
||||
private connectionAccessLoaded = false;
|
||||
private collapse!: () => void;
|
||||
|
||||
constructor(
|
||||
private notificationService: NotificationService,
|
||||
@@ -63,8 +64,9 @@ export class UserEditController implements IInitializableController, IDestructib
|
||||
private dbDriverResource: DBDriverResource
|
||||
) { }
|
||||
|
||||
init(id: string) {
|
||||
init(id: string, collapse: () => void) {
|
||||
this.userId = id;
|
||||
this.collapse = collapse;
|
||||
this.loadRoles();
|
||||
}
|
||||
|
||||
@@ -90,6 +92,7 @@ export class UserEditController implements IInitializableController, IDestructib
|
||||
roles: this.getGrantedRoles(),
|
||||
grantedConnections: this.getGrantedConnections(),
|
||||
});
|
||||
this.collapse();
|
||||
this.notificationService.logInfo({ title: 'authentication_administration_user_created' });
|
||||
} else {
|
||||
if (this.credentials.password) {
|
||||
|
||||
Reference in New Issue
Block a user