mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge remote-tracking branch 'origin/devel' into devel
This commit is contained in:
@@ -32,10 +32,15 @@ module.exports = {
|
||||
data_transfer_dialog_export: 'Export',
|
||||
data_transfer_dialog_exporting_object: 'Exporting:',
|
||||
data_transfer_dialog_configuration_title: 'Export configuration',
|
||||
data_transfer_dialog_preparation: 'We prepare your file for export. Please wait...',
|
||||
data_transfer_notification_preparation: 'We prepare your file for download. Please wait...',
|
||||
data_transfer_notification_ready: 'File ready to download',
|
||||
data_transfer_notification_error: 'Preparation failed or canceled',
|
||||
|
||||
ui_stepper_back: 'Back',
|
||||
ui_processing_loading: 'Loading...',
|
||||
ui_processing_cancel: 'Cancel',
|
||||
ui_processing_canceling: 'Canceling...',
|
||||
ui_processing_retry: 'Retry',
|
||||
ui_error_message: 'Error:',
|
||||
ui_errors_details: 'Details',
|
||||
|
||||
@@ -31,10 +31,15 @@ module.exports = {
|
||||
data_transfer_dialog_export: 'Экспортировать',
|
||||
data_transfer_dialog_exporting_object: 'Экспортировать:',
|
||||
data_transfer_dialog_configuration_title: 'Настройка экспорта',
|
||||
data_transfer_dialog_preparation: 'Подготавливаем файл к экспорту. Пожалуйста подождите...',
|
||||
data_transfer_notification_preparation: 'Подготавливаем файл к загрузке. Пожалуйста подождите...',
|
||||
data_transfer_notification_ready: 'Файл готов к загрузке',
|
||||
data_transfer_notification_error: 'Подготовка файла к загрузке не удалась или была отменена',
|
||||
|
||||
ui_stepper_back: 'Назад',
|
||||
ui_processing_loading: 'Загрузка...',
|
||||
ui_processing_cancel: 'Отменить',
|
||||
ui_processing_canceling: 'Отмена...',
|
||||
ui_processing_retry: 'Повторить',
|
||||
ui_error_message: 'Ошибка:',
|
||||
ui_errors_details: 'Информация',
|
||||
|
||||
+3
-3
@@ -20,18 +20,18 @@ const FadeTimeout = 5000;
|
||||
export class NotificationItemController implements IInitializableController {
|
||||
closeAfter = 0;
|
||||
@observable isDetailsDialogOpen = false;
|
||||
private notification!: INotification;
|
||||
private notification!: INotification<any>;
|
||||
|
||||
constructor(private commonDialogService: CommonDialogService) {}
|
||||
|
||||
init(notification: INotification) {
|
||||
init(notification: INotification<any>) {
|
||||
this.notification = notification;
|
||||
|
||||
if (this.notification.details) {
|
||||
this.handleShowDetails = this.showDetails.bind(this);
|
||||
}
|
||||
|
||||
if (notification.type !== ENotificationType.Error) {
|
||||
if (notification.type !== ENotificationType.Error && !notification.persistent) {
|
||||
this.closeAfter = FadeTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { NotificationItemController } from './NotificationItemController';
|
||||
import { Snackbar } from './Snackbar/Snackbar';
|
||||
|
||||
type NotificationProps = {
|
||||
notification: INotification;
|
||||
notification: INotification<any>;
|
||||
}
|
||||
|
||||
export const NotificationsItem = observer(function Notification({ notification }: NotificationProps) {
|
||||
@@ -24,6 +24,11 @@ export const NotificationsItem = observer(function Notification({ notification }
|
||||
}
|
||||
const controller = useController(NotificationItemController, notification);
|
||||
|
||||
if (notification.customComponent) {
|
||||
const Custom = notification.customComponent();
|
||||
return <Custom notification={notification} onClose={controller.handleClose} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
text={notification.title}
|
||||
|
||||
@@ -12,27 +12,33 @@ import styled, { use } from 'reshadow';
|
||||
|
||||
import { Icon, Button } from '@dbeaver/core/blocks';
|
||||
import { ENotificationType } from '@dbeaver/core/eventsLog';
|
||||
import { useTranslate } from '@dbeaver/core/localization';
|
||||
import { useStyles } from '@dbeaver/core/theming';
|
||||
|
||||
import { NotificationMark } from './NotificationMark';
|
||||
import { snackbarStyles } from './styles';
|
||||
import { SNACKBAR_STYLES } from './styles';
|
||||
|
||||
type SnackbarProps = {
|
||||
type?: ENotificationType;
|
||||
text: string;
|
||||
closeAfter?: number;
|
||||
type?: ENotificationType;
|
||||
disableShowDetails?: boolean;
|
||||
onClose?: () => void;
|
||||
onShowDetails?: () => void;
|
||||
}
|
||||
|
||||
export function Snackbar(props: SnackbarProps) {
|
||||
const styles = useStyles(snackbarStyles);
|
||||
export function Snackbar({
|
||||
type,
|
||||
text,
|
||||
closeAfter,
|
||||
disableShowDetails,
|
||||
onClose,
|
||||
onShowDetails,
|
||||
}: SnackbarProps) {
|
||||
const styles = useStyles(SNACKBAR_STYLES);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [closing, setClosing] = useState(false);
|
||||
const {
|
||||
closeAfter, onClose, onShowDetails, type, text,
|
||||
} = props;
|
||||
const translate = useTranslate();
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
@@ -72,9 +78,9 @@ export function Snackbar(props: SnackbarProps) {
|
||||
type="button"
|
||||
mod={['outlined']}
|
||||
onClick={onShowDetails}
|
||||
disabled={props.disableShowDetails}
|
||||
disabled={disableShowDetails}
|
||||
>
|
||||
Details
|
||||
{translate('ui_errors_details')}
|
||||
</Button>
|
||||
</actions>
|
||||
)}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { css } from 'reshadow';
|
||||
|
||||
import { composes } from '@dbeaver/core/theming';
|
||||
|
||||
export const snackbarStyles = composes(
|
||||
export const SNACKBAR_STYLES = composes(
|
||||
css`
|
||||
notification {
|
||||
composes: theme-background-surface theme-text-on-surface from global;
|
||||
|
||||
@@ -18,6 +18,8 @@ export * from './NavigationTree/NavigationTreeService';
|
||||
export * from './NavigationTree/NavigationTreeContextMenuService';
|
||||
export * from './shared/ToolsPanel/LogViewTab/LogViewerMenuService';
|
||||
export * from './shared/ToolsPanel/LogViewTab/LogViewerService';
|
||||
export * from './Notifications/NotificationsItem/Snackbar/styles';
|
||||
export * from './Notifications/NotificationsItem/Snackbar/NotificationMark';
|
||||
|
||||
// Models
|
||||
export * from './shared/NavigationTabs/ITab';
|
||||
|
||||
+37
-12
@@ -38,12 +38,25 @@ export type DBSource = Pick<DataSourceInfo, 'id' | 'name' | 'driverId' | 'descri
|
||||
export type Connection = Pick<ConnectionInfo, 'id' | 'name' | 'connected' | 'driverId'>
|
||||
export type ObjectContainer = Pick<DatabaseObjectInfo, 'name' | 'description' | 'type' | 'features'>
|
||||
|
||||
type DBDriversMetadata = {
|
||||
loaded: boolean;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ConnectionsManagerService {
|
||||
|
||||
@observable private connectionsMap: Map<string, Connection> = new Map();
|
||||
readonly dbDrivers = new CachedResource(new Map(), this.refreshDriversAsync.bind(this));
|
||||
readonly connectionObjectContainers = new CachedResource(new Map(), this.refreshObjectContainersAsync.bind(this));
|
||||
readonly dbDrivers = new CachedResource(
|
||||
new Map(),
|
||||
this.refreshDriversAsync.bind(this),
|
||||
(_, { loaded }) => loaded,
|
||||
{ loaded: false }
|
||||
);
|
||||
readonly connectionObjectContainers = new CachedResource(
|
||||
new Map(),
|
||||
this.refreshObjectContainersAsync.bind(this),
|
||||
this.isObjectContainersLoaded.bind(this)
|
||||
);
|
||||
|
||||
@computed get connections(): Connection[] {
|
||||
return Array.from(this.connectionsMap.values());
|
||||
@@ -129,24 +142,36 @@ export class ConnectionsManagerService {
|
||||
await this.nodesManagerService.updateRootChildren();
|
||||
}
|
||||
|
||||
private isObjectContainersLoaded(
|
||||
data: Map<string, ObjectContainer[]>,
|
||||
metadata: {},
|
||||
connectionId: string,
|
||||
catalogId?: string,
|
||||
) {
|
||||
return data.has(connectionId);
|
||||
}
|
||||
|
||||
private async refreshObjectContainersAsync(
|
||||
data: Map<string, ObjectContainer[]>,
|
||||
refresh: boolean,
|
||||
metadata: {},
|
||||
update: boolean,
|
||||
connectionId: string,
|
||||
catalogId?: string,
|
||||
): Promise<Map<string, ObjectContainer[]>> {
|
||||
if (refresh || !data.has(connectionId)) {
|
||||
const { navGetStructContainers } = await this.graphQLService.gql.navGetStructContainers({
|
||||
connectionId,
|
||||
catalogId,
|
||||
});
|
||||
data.set(connectionId, [...navGetStructContainers.schemaList, ...navGetStructContainers.catalogList]);
|
||||
}
|
||||
const { navGetStructContainers } = await this.graphQLService.gql.navGetStructContainers({
|
||||
connectionId,
|
||||
catalogId,
|
||||
});
|
||||
data.set(connectionId, [...navGetStructContainers.schemaList, ...navGetStructContainers.catalogList]);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private async refreshDriversAsync(data: Map<string, DBDriver>): Promise<Map<string, DBDriver>> {
|
||||
private async refreshDriversAsync(
|
||||
data: Map<string, DBDriver>,
|
||||
metadata: DBDriversMetadata,
|
||||
update: boolean
|
||||
): Promise<Map<string, DBDriver>> {
|
||||
const { driverList } = await this.graphQLService.gql.driverList();
|
||||
|
||||
data.clear();
|
||||
@@ -154,7 +179,7 @@ export class ConnectionsManagerService {
|
||||
for (const driver of driverList) {
|
||||
data.set(driver.id, driver);
|
||||
}
|
||||
|
||||
metadata.loaded = true;
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,52 +12,69 @@ import { useStyles, composes } from '@dbeaver/core/theming';
|
||||
|
||||
const buttonStyles = composes(
|
||||
css`
|
||||
button {
|
||||
composes: theme-button from global;
|
||||
}
|
||||
ripple {
|
||||
composes: theme-button_ripple from global;
|
||||
}
|
||||
Button {
|
||||
composes: theme-button from global;
|
||||
}
|
||||
ripple {
|
||||
composes: theme-button_ripple from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
Button {
|
||||
display: flex;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
const buttonMod = {
|
||||
raised: composes(
|
||||
css`
|
||||
button {
|
||||
Button {
|
||||
composes: theme-button_raised from global;
|
||||
}
|
||||
`
|
||||
),
|
||||
unelevated: composes(
|
||||
css`
|
||||
button {
|
||||
Button {
|
||||
composes: theme-button_unelevated from global;
|
||||
}
|
||||
`
|
||||
),
|
||||
outlined: composes(
|
||||
css`
|
||||
button {
|
||||
Button {
|
||||
composes: theme-button_outlined from global;
|
||||
}
|
||||
`
|
||||
),
|
||||
secondary: composes(
|
||||
css`
|
||||
button {
|
||||
Button {
|
||||
composes: theme-button_secondary from global;
|
||||
}
|
||||
`
|
||||
),
|
||||
};
|
||||
|
||||
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
type ButtonProps = (
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement | HTMLAnchorElement>
|
||||
& React.LinkHTMLAttributes<HTMLLinkElement | HTMLButtonElement>
|
||||
) & {
|
||||
mod?: (keyof typeof buttonMod)[];
|
||||
tag?: 'button' | 'a';
|
||||
href?: string;
|
||||
download?: boolean;
|
||||
}
|
||||
|
||||
export function Button({ children, mod, ...rest }: ButtonProps) {
|
||||
export function Button({
|
||||
children,
|
||||
mod,
|
||||
tag = 'button',
|
||||
...rest
|
||||
}: ButtonProps) {
|
||||
const Button = tag;
|
||||
return styled(useStyles(buttonStyles, ...(mod || []).map(mod => buttonMod[mod])))(
|
||||
<button {...rest}><ripple as="div"/>{children}</button>
|
||||
<Button {...rest}><ripple as="div"/>{children}</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ export type LoaderProps = {
|
||||
loading?: boolean;
|
||||
/** disable cancel button */
|
||||
cancelDisabled?: boolean;
|
||||
/** hides message */
|
||||
hideMessage?: boolean;
|
||||
/** render loader as overlay with white spinner */
|
||||
overlay?: boolean;
|
||||
/** loader with white spinner */
|
||||
@@ -38,6 +40,7 @@ const spinnerType = {
|
||||
export function Loader({
|
||||
cancelDisabled,
|
||||
overlay,
|
||||
hideMessage,
|
||||
secondary,
|
||||
small,
|
||||
className,
|
||||
@@ -66,7 +69,7 @@ export function Loader({
|
||||
return styled(useStyles(loaderStyles, ...(overlay ? [overlayStyles] : [])))(
|
||||
<loader as="div" className={className} {...use({ small })}>
|
||||
<icon as="div"><img src={spinnerURL}/></icon>
|
||||
<message as="div"><Translate token='ui_processing_loading' /></message>
|
||||
{!hideMessage && <message as="div"><Translate token='ui_processing_loading' /></message>}
|
||||
{onCancel && (
|
||||
<actions as='div'>
|
||||
<Button type="button" mod={['unelevated']}
|
||||
|
||||
@@ -11,20 +11,32 @@ export enum ENotificationType {
|
||||
Error = 'Error',
|
||||
}
|
||||
|
||||
export interface INotification {
|
||||
export type NotificationComponentProps<T = undefined> = {
|
||||
notification: INotification<T>;
|
||||
onClose: () => void;
|
||||
}
|
||||
export type NotificationComponent<T = undefined> = React.FunctionComponent<NotificationComponentProps<T>>
|
||||
|
||||
export interface INotification<T = undefined> {
|
||||
readonly id: number;
|
||||
type: ENotificationType;
|
||||
title: string;
|
||||
message?: string;
|
||||
details?: string | Error;
|
||||
persistent: boolean;
|
||||
isSilent: boolean;
|
||||
customComponent?: () => NotificationComponent<T>;
|
||||
source: T;
|
||||
close: () => void;
|
||||
showDetails: () => void;
|
||||
}
|
||||
|
||||
export interface INotificationOptions {
|
||||
export interface INotificationOptions<T = undefined> {
|
||||
title: string;
|
||||
message?: string;
|
||||
details?: string | Error;
|
||||
isSilent?: boolean;
|
||||
persistent?: boolean;
|
||||
customComponent?: () => NotificationComponent<T>;
|
||||
source?: T;
|
||||
}
|
||||
|
||||
@@ -10,44 +10,47 @@ import { computed } from 'mobx';
|
||||
|
||||
import { injectable } from '@dbeaver/core/di';
|
||||
import { GQLError, ServerInternalError } from '@dbeaver/core/sdk';
|
||||
import { OrderedMap } from '@dbeaver/core/utils';
|
||||
import { EntityList } from '@dbeaver/core/utils';
|
||||
|
||||
import { ENotificationType, INotification, INotificationOptions } from './INotification';
|
||||
|
||||
@injectable()
|
||||
export class NotificationService {
|
||||
private notificationList = new OrderedMap<number, INotification>();
|
||||
private notificationList = new EntityList<number, INotification<any>>(({ id }) => id);
|
||||
private notificationNextId = 0
|
||||
|
||||
/**
|
||||
* By default filtered without silent notifications
|
||||
*/
|
||||
@computed get notifications(): INotification[] {
|
||||
@computed get notifications(): INotification<any>[] {
|
||||
return this.notificationList.values
|
||||
.filter(notification => !notification.isSilent);
|
||||
}
|
||||
|
||||
notify(options: INotificationOptions, type: ENotificationType) {
|
||||
notify<T = never>(options: INotificationOptions<T>, type: ENotificationType) {
|
||||
const id = this.notificationNextId++;
|
||||
|
||||
const notification: INotification = {
|
||||
const notification: INotification<T> = {
|
||||
id,
|
||||
title: options.title,
|
||||
message: options.message,
|
||||
details: options.details,
|
||||
isSilent: !!options.isSilent,
|
||||
persistent: !!options.persistent,
|
||||
customComponent: options.customComponent,
|
||||
source: options.source!,
|
||||
type,
|
||||
close: this.close.bind(this, id),
|
||||
showDetails: this.showDetails.bind(this, id),
|
||||
};
|
||||
this.notificationList.add(notification.id, notification);
|
||||
this.notificationList.set(notification);
|
||||
}
|
||||
|
||||
logInfo(notification: INotificationOptions) {
|
||||
logInfo<T>(notification: INotificationOptions<T>) {
|
||||
this.notify(notification, ENotificationType.Info);
|
||||
}
|
||||
|
||||
logError(notification: INotificationOptions) {
|
||||
logError<T>(notification: INotificationOptions<T>) {
|
||||
this.notify(notification, ENotificationType.Error);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { SessionService } from './SessionService';
|
||||
|
||||
@injectable()
|
||||
export class ServerService {
|
||||
readonly config = new CachedResource(undefined, this.refreshConfigAsync.bind(this));
|
||||
readonly config = new CachedResource(undefined, this.refreshConfigAsync.bind(this), data => !!data);
|
||||
readonly settings = new ServerSettingsService(this.sessionService.settings);
|
||||
|
||||
private lastConfig: any = null
|
||||
@@ -26,10 +26,6 @@ export class ServerService {
|
||||
}
|
||||
|
||||
private async refreshConfigAsync(data: ServerConfig | undefined): Promise<ServerConfig> {
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const { serverConfig } = await this.graphQLService.gql.serverConfig();
|
||||
|
||||
if (serverConfig.productConfiguration !== this.lastConfig) {
|
||||
|
||||
@@ -19,16 +19,12 @@ export type SessionState = Pick<SessionInfo, 'id' | 'createTime' | 'cacheExpired
|
||||
|
||||
@injectable()
|
||||
export class SessionService {
|
||||
readonly session = new CachedResource(undefined, this.refreshSessionStateAsync.bind(this));
|
||||
readonly session = new CachedResource(undefined, this.refreshSessionStateAsync.bind(this), data => !!data);
|
||||
readonly settings = new SessionSettingsService('session_settings');
|
||||
|
||||
constructor(private graphQLService: GraphQLService) {}
|
||||
|
||||
private async refreshSessionStateAsync(data: SessionState | undefined): Promise<SessionState> {
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const { session } = await this.graphQLService.gql.openSession();
|
||||
|
||||
return session;
|
||||
|
||||
@@ -8,24 +8,42 @@
|
||||
|
||||
import { observable } from 'mobx';
|
||||
|
||||
export type Loader<TData, TArgs extends any[]> = (current: TData, update: boolean, ...args: TArgs) => Promise<TData>
|
||||
export type Loader<TData, TMetadata = {}, TArgs extends any[] = []> = (
|
||||
current: TData,
|
||||
metadata: TMetadata,
|
||||
update: boolean,
|
||||
...args: TArgs
|
||||
) => Promise<TData>
|
||||
|
||||
export class CachedResource<TData, TArgs extends any[] = []> {
|
||||
export type IsLoaded<TData, TMetadata = {}, TArgs extends any[] = []> = (
|
||||
current: TData,
|
||||
metadata: TMetadata,
|
||||
...args: TArgs
|
||||
) => boolean
|
||||
|
||||
export class CachedResource<TData, TMetadata = {}, TArgs extends any[] = []> {
|
||||
@observable data: TData;
|
||||
|
||||
@observable private loaded = false;
|
||||
@observable private loading = false;
|
||||
private refreshPromise: Promise<TData> | null = null;
|
||||
private singleElementPromise: Promise<TData> | null = null;
|
||||
private loader: Loader<TData, TArgs>;
|
||||
@observable private metadata: TMetadata;
|
||||
private loader: Loader<TData, TMetadata, TArgs>;
|
||||
private isLoadedCheck: IsLoaded<TData, TMetadata, TArgs>;
|
||||
private activePromise: Promise<TData> | null = null;
|
||||
|
||||
constructor(defaultValue: TData, loader: Loader<TData, TArgs>) {
|
||||
constructor(
|
||||
defaultValue: TData,
|
||||
loader: Loader<TData, TMetadata, TArgs>,
|
||||
isLoadedCheck: IsLoaded<TData, TMetadata, TArgs>,
|
||||
metadata?: TMetadata,
|
||||
) {
|
||||
this.data = defaultValue;
|
||||
this.loader = loader;
|
||||
this.isLoadedCheck = isLoadedCheck;
|
||||
this.metadata = metadata || {} as TMetadata;
|
||||
}
|
||||
|
||||
isLoaded(): boolean {
|
||||
return this.loaded;
|
||||
isLoaded(...args: TArgs): boolean {
|
||||
return this.isLoadedCheck(this.data, this.metadata, ...args);
|
||||
}
|
||||
|
||||
isLoading(): boolean {
|
||||
@@ -33,64 +51,44 @@ export class CachedResource<TData, TArgs extends any[] = []> {
|
||||
}
|
||||
|
||||
async refresh(...args: TArgs): Promise<TData> {
|
||||
if (args.length > 0) {
|
||||
await this.loadSingle(true, args);
|
||||
} else {
|
||||
await this.loadAll(true, args);
|
||||
}
|
||||
return this.data;
|
||||
return this.loadAll(true, args);
|
||||
}
|
||||
|
||||
async load(...args: TArgs): Promise<TData> {
|
||||
if (!this.loaded) {
|
||||
if (args.length > 0) {
|
||||
await this.loadSingle(false, args);
|
||||
} else {
|
||||
await this.loadAll(false, args);
|
||||
}
|
||||
}
|
||||
return this.data;
|
||||
return this.loadAll(false, args);
|
||||
}
|
||||
|
||||
private async loadAll(update: boolean, args: TArgs) {
|
||||
if (this.refreshPromise) {
|
||||
return this.refreshPromise;
|
||||
}
|
||||
this.refreshPromise = this.loadingTask(update, args);
|
||||
await this.waitActive();
|
||||
this.activePromise = this.loadingTask(update, args);
|
||||
try {
|
||||
await this.refreshPromise;
|
||||
return await this.activePromise;
|
||||
} finally {
|
||||
this.refreshPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadSingle(update: boolean, args: TArgs) {
|
||||
if (this.singleElementPromise) {
|
||||
return this.singleElementPromise;
|
||||
}
|
||||
this.singleElementPromise = this.loadingTask(update, args);
|
||||
try {
|
||||
await this.singleElementPromise;
|
||||
} finally {
|
||||
this.singleElementPromise = null;
|
||||
this.activePromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async loadingTask(update: boolean, args: TArgs): Promise<TData> {
|
||||
if (args.length === 0) {
|
||||
this.loaded = false;
|
||||
}
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
this.data = await this.loader(this.data, update, ...args);
|
||||
if (this.isLoaded(...args) && !update) {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
this.data = await this.loader(this.data, this.metadata, update, ...args);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
if (args.length === 0) {
|
||||
this.loaded = true;
|
||||
}
|
||||
return this.data;
|
||||
}
|
||||
|
||||
private async waitActive() {
|
||||
if (this.activePromise) {
|
||||
try {
|
||||
await this.activePromise;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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 { action, computed, observable } from 'mobx';
|
||||
|
||||
export class EntityList<K, V> {
|
||||
private identifiers = observable.array<K>([]);
|
||||
@observable private map: Map<K, V> = new Map<K, V>();
|
||||
|
||||
@computed get keys(): K[] {
|
||||
return this.identifiers;
|
||||
}
|
||||
|
||||
@computed get values(): V[] {
|
||||
return this.identifiers.map(i => this.map.get(i)!);
|
||||
}
|
||||
|
||||
constructor(private keySelector: (val: V) => K) { }
|
||||
|
||||
get(key: K): V | undefined {
|
||||
return this.map.get(key);
|
||||
}
|
||||
|
||||
has(key: K): boolean {
|
||||
return this.map.has(key);
|
||||
}
|
||||
|
||||
@action
|
||||
set(value: V): void {
|
||||
const key = this.keySelector(value);
|
||||
this.identifiers.push(key);
|
||||
this.map.set(key, value);
|
||||
}
|
||||
|
||||
@action
|
||||
remove(key: K): void {
|
||||
if (!this.map.has(key)) {
|
||||
return;
|
||||
}
|
||||
this.map.delete(key);
|
||||
this.identifiers.remove(key);
|
||||
}
|
||||
|
||||
@action
|
||||
clear(): void {
|
||||
this.identifiers.clear();
|
||||
this.map.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './OrderedMap';
|
||||
export * from './uuid';
|
||||
export * from './parseJSONFlat';
|
||||
export * from './EntityList';
|
||||
|
||||
export * from './Promises/CancellablePromise';
|
||||
export * from './Promises/Deferred';
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { ErrorDetailsDialog } from '@dbeaver/core/app';
|
||||
import { injectable } from '@dbeaver/core/di';
|
||||
import { CommonDialogService } from '@dbeaver/core/dialogs';
|
||||
import { NotificationService } from '@dbeaver/core/eventsLog';
|
||||
import {
|
||||
CachedResource, GraphQLService, DataTransferProcessorInfo, DataTransferParameters
|
||||
@@ -15,15 +17,25 @@ import { Deferred } from '@dbeaver/core/utils';
|
||||
|
||||
import { ExportFromContainerProcess } from './ExportFromContainerProcess';
|
||||
import { ExportFromResultsProcess } from './ExportFromResultsProcess';
|
||||
import { PendingNotification } from './PendingNotification';
|
||||
|
||||
type ProcessorsResourceMetadata = {
|
||||
loaded: boolean;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DataExportService {
|
||||
readonly processors = new CachedResource(new Map(), this.refreshProcessorsAsync.bind(this));
|
||||
private exportProcesses = new Map<string, Deferred<string>>();
|
||||
readonly processors = new CachedResource(
|
||||
new Map(),
|
||||
this.refreshProcessorsAsync.bind(this),
|
||||
(_, { loaded }) => loaded
|
||||
);
|
||||
readonly exportProcesses = new Map<string, Deferred<string>>();
|
||||
|
||||
constructor(
|
||||
private graphQLService: GraphQLService,
|
||||
private notificationService: NotificationService
|
||||
private notificationService: NotificationService,
|
||||
private commonDialogService: CommonDialogService,
|
||||
) { }
|
||||
|
||||
async cancel(exportId: string) {
|
||||
@@ -46,7 +58,19 @@ export class DataExportService {
|
||||
try {
|
||||
await this.graphQLService.gql.removeDataTransferFile({ dataFileId });
|
||||
this.exportProcesses.delete(exportId);
|
||||
} catch (e) {
|
||||
} catch (exception) {
|
||||
this.notificationService.logException(exception, 'Error occurred while deleting file');
|
||||
}
|
||||
}
|
||||
|
||||
async showDetails(exportId: string) {
|
||||
const process = this.exportProcesses.get(exportId);
|
||||
if (!process) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.commonDialogService.open(ErrorDetailsDialog, process.getRejectionReason());
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +84,17 @@ export class DataExportService {
|
||||
return;
|
||||
}
|
||||
this.exportProcesses.delete(exportId);
|
||||
}
|
||||
|
||||
downloadUrl(exportId: string) {
|
||||
const process = this.exportProcesses.get(exportId);
|
||||
if (!process) {
|
||||
return;
|
||||
}
|
||||
const dataFileId = process.getPayload();
|
||||
if (!dataFileId) {
|
||||
return;
|
||||
}
|
||||
return `/dbeaver/data/${dataFileId}`;
|
||||
}
|
||||
|
||||
@@ -72,9 +107,7 @@ export class DataExportService {
|
||||
const taskId = await process.start(connectionId, containerNodePath, parameters);
|
||||
if (taskId) {
|
||||
this.exportProcesses.set(taskId, process);
|
||||
// this.notificationService.logInfo({ title: 'We prepare your file for export. Please wait' });
|
||||
await process.promise;
|
||||
window.open(this.download(taskId), '_blank');
|
||||
this.showPendingNotification(taskId);
|
||||
return taskId;
|
||||
}
|
||||
}
|
||||
@@ -89,14 +122,22 @@ export class DataExportService {
|
||||
const taskId = await process.start(connectionId, contextId, resultsId, parameters);
|
||||
if (taskId) {
|
||||
this.exportProcesses.set(taskId, process);
|
||||
await process.promise;
|
||||
window.open(this.download(taskId), '_blank');
|
||||
this.showPendingNotification(taskId);
|
||||
return taskId;
|
||||
}
|
||||
}
|
||||
|
||||
private showPendingNotification(taskId: string) {
|
||||
this.notificationService.logInfo({
|
||||
title: 'We prepare your file for export. Please wait',
|
||||
source: taskId,
|
||||
customComponent: () => PendingNotification,
|
||||
});
|
||||
}
|
||||
|
||||
private async refreshProcessorsAsync(
|
||||
data: Map<string, DataTransferProcessorInfo>
|
||||
data: Map<string, DataTransferProcessorInfo>,
|
||||
metadata: ProcessorsResourceMetadata,
|
||||
): Promise<Map<string, DataTransferProcessorInfo>> {
|
||||
const { processors } = await this.graphQLService.gql.getDataTransferProcessors();
|
||||
|
||||
@@ -105,7 +146,7 @@ export class DataExportService {
|
||||
for (const processor of processors) {
|
||||
data.set(processor.id, processor);
|
||||
}
|
||||
|
||||
metadata.loaded = true;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export class DataExportController implements IInitializableController {
|
||||
.sort((a, b) => this.sortProcessors(a, b));
|
||||
}
|
||||
|
||||
@observable readonly processorProperties: any = {}
|
||||
@observable processorProperties: any = {}
|
||||
@observable properties: IProperty[] = []
|
||||
|
||||
private context!: IExportContext;
|
||||
@@ -108,6 +108,9 @@ export class DataExportController implements IInitializableController {
|
||||
validValues: property.validValues,
|
||||
defaultValue: property.defaultValue,
|
||||
})) || [];
|
||||
|
||||
this.processorProperties = {};
|
||||
|
||||
this.step = DataExportStep.Configure;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { Loader, IProperty, PropertiesTable } from '@dbeaver/core/blocks';
|
||||
import { IProperty, PropertiesTable } from '@dbeaver/core/blocks';
|
||||
import { CommonDialogWrapper } from '@dbeaver/core/dialogs';
|
||||
import { useTranslate } from '@dbeaver/core/localization';
|
||||
import { DataTransferProcessorInfo } from '@dbeaver/core/sdk';
|
||||
@@ -49,7 +49,8 @@ export const ProcessorConfigureDialog = observer(
|
||||
onBack,
|
||||
onExport,
|
||||
}: ProcessorSelectDialogProps) {
|
||||
const title = `${useTranslate('data_transfer_dialog_configuration_title')} (${processor.name})`;
|
||||
const translate = useTranslate();
|
||||
const title = `${translate('data_transfer_dialog_configuration_title')} (${processor.name})`;
|
||||
|
||||
return styled(styles)(
|
||||
<CommonDialogWrapper
|
||||
@@ -65,7 +66,7 @@ export const ProcessorConfigureDialog = observer(
|
||||
}
|
||||
onReject={onClose}
|
||||
>
|
||||
{isExporting && <message as="div">We prepare your file for export. Please wait...</message>}
|
||||
{isExporting && <message as="div">{translate('data_transfer_dialog_preparation')}</message>}
|
||||
{!isExporting && (
|
||||
<PropertiesTable
|
||||
properties={properties}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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 {
|
||||
useEffect, useState, useCallback
|
||||
} from 'react';
|
||||
import { Button as ReakitButton } from 'reakit/Button';
|
||||
import styled, { use, css } from 'reshadow';
|
||||
|
||||
import { SNACKBAR_STYLES } from '@dbeaver/core/app';
|
||||
import { Icon, Button, Loader } from '@dbeaver/core/blocks';
|
||||
import { useService } from '@dbeaver/core/di';
|
||||
import { NotificationComponentProps } from '@dbeaver/core/eventsLog';
|
||||
import { useTranslate } from '@dbeaver/core/localization';
|
||||
import { useStyles } from '@dbeaver/core/theming';
|
||||
import { EDeferredState } from '@dbeaver/core/utils';
|
||||
|
||||
import { DataExportService } from './DataExportService';
|
||||
|
||||
const styles = css`
|
||||
Loader {
|
||||
margin-right: 16px;
|
||||
}
|
||||
actions {
|
||||
display: flex;
|
||||
}
|
||||
fill {
|
||||
flex: 1;
|
||||
}
|
||||
Button {
|
||||
margin-left: 16px !important;
|
||||
}
|
||||
actions Button {
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
|
||||
export const PendingNotification = observer(function PendingNotification({
|
||||
notification,
|
||||
onClose,
|
||||
}: NotificationComponentProps<string>) {
|
||||
const translate = useTranslate();
|
||||
const dataExportService = useService(DataExportService);
|
||||
const task = dataExportService.exportProcesses.get(notification.source);
|
||||
if (!task) {
|
||||
return null;
|
||||
}
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [idDetailsOpen, setDetails] = useState(false);
|
||||
|
||||
const getDownloadUrl = useCallback(
|
||||
() => dataExportService.downloadUrl(notification.source),
|
||||
[dataExportService, notification]
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
dataExportService.delete(notification.source);
|
||||
onClose();
|
||||
}, [dataExportService, notification, onClose]);
|
||||
|
||||
const handleDownload = useCallback(() => {
|
||||
dataExportService.download(notification.source);
|
||||
onClose();
|
||||
}, [dataExportService, notification, onClose]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
dataExportService.cancel(notification.source);
|
||||
}, [dataExportService, notification]);
|
||||
|
||||
const handleDetails = useCallback(async () => {
|
||||
try {
|
||||
setDetails(true);
|
||||
await dataExportService.showDetails(notification.source);
|
||||
} finally {
|
||||
setDetails(false);
|
||||
}
|
||||
}, [dataExportService, notification]);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const status = useCallback(() => {
|
||||
switch (task.getState()) {
|
||||
case EDeferredState.PENDING:
|
||||
return translate('data_transfer_notification_preparation');
|
||||
case EDeferredState.CANCELLING:
|
||||
return translate('ui_processing_canceling');
|
||||
case EDeferredState.RESOLVED:
|
||||
return translate('data_transfer_notification_ready');
|
||||
default:
|
||||
return translate('data_transfer_notification_error');
|
||||
}
|
||||
}, [task]);
|
||||
|
||||
const isSuccess = task.getState() === EDeferredState.RESOLVED;
|
||||
|
||||
return styled(useStyles(SNACKBAR_STYLES, styles))(
|
||||
<notification as="div" {...use({ mounted })} >
|
||||
<notification-header as="div">
|
||||
<Loader loading={task.isInProgress} hideMessage />
|
||||
<message as="div">{status()}</message>
|
||||
{task.isFinished && (
|
||||
<ReakitButton onClick={handleDelete}>
|
||||
<Icon name="cross" viewBox="0 0 16 16" />
|
||||
</ReakitButton>
|
||||
)}
|
||||
</notification-header>
|
||||
<notification-body as="div">
|
||||
<actions as="div">
|
||||
<fill as="div" />
|
||||
{task.isFinished && isSuccess && (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
mod={['outlined']}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
tag='a'
|
||||
href={getDownloadUrl()}
|
||||
mod={['unelevated']}
|
||||
onClick={handleDownload}
|
||||
download
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{task.isFinished && !isSuccess && (
|
||||
<Button
|
||||
type="button"
|
||||
mod={['outlined']}
|
||||
onClick={handleDetails}
|
||||
disabled={idDetailsOpen}
|
||||
>
|
||||
{translate('ui_errors_details')}
|
||||
</Button>
|
||||
)}
|
||||
{!task.isFinished && (
|
||||
<Button
|
||||
type="button"
|
||||
mod={['outlined']}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{translate('ui_processing_cancel')}
|
||||
</Button>
|
||||
)}
|
||||
</actions>
|
||||
</notification-body>
|
||||
</notification>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user