mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
feat: data table actions disable behavior
This commit is contained in:
@@ -33,6 +33,7 @@ export interface InlineEditorProps {
|
||||
hideSave?: boolean;
|
||||
hideCancel?: boolean;
|
||||
edited?: boolean;
|
||||
disabled?: boolean;
|
||||
autofocus?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
onSave: () => void;
|
||||
@@ -52,6 +53,7 @@ export const InlineEditor = observer(function InlineEditor({
|
||||
hideSave,
|
||||
hideCancel,
|
||||
edited = false,
|
||||
disabled,
|
||||
autofocus,
|
||||
onChange,
|
||||
onSave,
|
||||
@@ -103,6 +105,7 @@ export const InlineEditor = observer(function InlineEditor({
|
||||
tabIndex={tabIndex}
|
||||
placeholder={placeholder}
|
||||
autoComplete="off"
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
|
||||
@@ -48,6 +48,7 @@ export * from './StaticImage';
|
||||
export * from './TextPlaceholder';
|
||||
export * from './useClipboard';
|
||||
export * from './useFocus';
|
||||
export * from './useStateDelay';
|
||||
export * from './useErrorDetails';
|
||||
export * from './useAppLoadingScreen';
|
||||
export * from './BlocksLocaleService';
|
||||
|
||||
@@ -14,16 +14,18 @@ export function useStateDelay(state: boolean, delay: number, callback?: () => vo
|
||||
callbackRef.current = callback;
|
||||
|
||||
useEffect(() => {
|
||||
if (!state) {
|
||||
if (state === delayedState) {
|
||||
return;
|
||||
}
|
||||
const timerId = setTimeout(() => {
|
||||
setState(true);
|
||||
callbackRef.current?.();
|
||||
setState(state);
|
||||
if (state) {
|
||||
callbackRef.current?.();
|
||||
}
|
||||
}, delay);
|
||||
|
||||
return () => clearTimeout(timerId);
|
||||
}, [state, delay]);
|
||||
}, [state, delayedState, delay]);
|
||||
|
||||
return delayedState;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export const defaultENLocale = [
|
||||
['ui_processing_create', 'Create'],
|
||||
['ui_processing_save', 'Save'],
|
||||
['ui_error_message', 'Error:'],
|
||||
['ui_error_close', 'Close'],
|
||||
['ui_errors_details', 'Details'],
|
||||
['ui_search', 'Search...'],
|
||||
['ui_delete', 'Delete'],
|
||||
|
||||
@@ -12,6 +12,7 @@ export const defaultRULocale = [
|
||||
['ui_processing_create', 'Создать'],
|
||||
['ui_processing_save', 'Сохранить'],
|
||||
['ui_error_message', 'Ошибка:'],
|
||||
['ui_error_close', 'Закрыть'],
|
||||
['ui_errors_details', 'Информация'],
|
||||
['ui_search', 'Поиск...'],
|
||||
['ui_delete', 'Удалить'],
|
||||
|
||||
+2
-2
@@ -40,13 +40,13 @@ export class ErrorModel {
|
||||
});
|
||||
|
||||
this.textToCopy = error.isTextBody
|
||||
? error.errorText
|
||||
? error.errorMessage
|
||||
: this.textToCopy = this.errors
|
||||
.map(error => `${error.message}\n${error.stackTrace}`)
|
||||
.join('------------------\n');
|
||||
|
||||
if (error.isTextBody) {
|
||||
this.htmlBody = error.errorText;
|
||||
this.htmlBody = error.errorMessage;
|
||||
}
|
||||
} else if (error instanceof ServerInternalError) {
|
||||
this.errors = [
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export abstract class DetailsError extends Error {
|
||||
errorMessage: string;
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = 'Details Error';
|
||||
this.errorMessage = message || 'Error';
|
||||
}
|
||||
|
||||
abstract hasDetails(): boolean;
|
||||
}
|
||||
@@ -11,10 +11,12 @@
|
||||
|
||||
import type { GraphQLResponse, GraphQLRequestContext, ClientError } from 'graphql-request/dist/src/types';
|
||||
|
||||
export class GQLError extends Error {
|
||||
import { DetailsError } from './DetailsError';
|
||||
|
||||
export class GQLError extends DetailsError {
|
||||
response: GraphQLResponse;
|
||||
request: GraphQLRequestContext;
|
||||
errorText: string;
|
||||
errorMessage: string;
|
||||
errorCode?: string;
|
||||
isTextBody = false; // true when server returns not GQLError object but plain text or html error
|
||||
|
||||
@@ -25,9 +27,9 @@ export class GQLError extends Error {
|
||||
this.request = clientError.request;
|
||||
if (typeof clientError.response.error === 'string') {
|
||||
this.isTextBody = true;
|
||||
this.errorText = clientError.response.error;
|
||||
this.errorMessage = clientError.response.error;
|
||||
} else {
|
||||
this.errorText = clientError.response.errors?.map(e => e.message).join('\n') || 'unknown error';
|
||||
this.errorMessage = clientError.response.errors?.map(e => e.message).join('\n') || 'unknown error';
|
||||
|
||||
const firstError = clientError.response.errors?.[0];
|
||||
this.errorCode = firstError?.extensions?.webErrorCode;
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
|
||||
import { observable, action, makeObservable } from 'mobx';
|
||||
|
||||
import { GQLError } from './GQLError';
|
||||
import { DetailsError } from './DetailsError';
|
||||
|
||||
export class GQLErrorCatcher {
|
||||
hasDetails = false;
|
||||
responseMessage: string | null = null;
|
||||
exception: GQLError | null = null;
|
||||
exception: DetailsError | null = null;
|
||||
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
@@ -26,8 +26,8 @@ export class GQLErrorCatcher {
|
||||
}
|
||||
|
||||
catch(exception: any): boolean {
|
||||
if (exception instanceof GQLError) {
|
||||
this.responseMessage = exception.errorText;
|
||||
if (exception instanceof DetailsError) {
|
||||
this.responseMessage = exception.errorMessage;
|
||||
this.hasDetails = exception.hasDetails();
|
||||
this.exception = exception;
|
||||
return true;
|
||||
|
||||
@@ -6,17 +6,23 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { DetailsError } from './DetailsError';
|
||||
import type { ServerError } from './sdk';
|
||||
|
||||
export class ServerInternalError extends Error implements ServerError {
|
||||
export class ServerInternalError extends DetailsError implements ServerError {
|
||||
readonly errorCode?: string;
|
||||
readonly stackTrace?: string;
|
||||
readonly causedBy?: ServerError;
|
||||
|
||||
constructor(error: ServerError) {
|
||||
super(error.message);
|
||||
this.name = 'Server Internal Error';
|
||||
this.errorCode = error.errorCode;
|
||||
this.stackTrace = error.stackTrace;
|
||||
this.causedBy = error.causedBy;
|
||||
}
|
||||
|
||||
hasDetails(): boolean {
|
||||
return this.stackTrace !== undefined && this.stackTrace.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { GQLError } from './GQLError';
|
||||
import { ServerInternalError } from './ServerInternalError';
|
||||
import { DetailsError } from './DetailsError';
|
||||
|
||||
export interface IErrorDetails {
|
||||
name: string;
|
||||
@@ -15,12 +14,12 @@ export interface IErrorDetails {
|
||||
hasDetails: boolean;
|
||||
}
|
||||
|
||||
export function hasDetails(error: Error): error is GQLError | ServerInternalError {
|
||||
return error instanceof GQLError || error instanceof ServerInternalError;
|
||||
export function hasDetails(error: Error): error is DetailsError {
|
||||
return error instanceof DetailsError && error.hasDetails();
|
||||
}
|
||||
|
||||
export function getErrorDetails(error: Error | GQLError): IErrorDetails {
|
||||
const exceptionMessage = hasDetails(error) ? error.errorText : error.message || error.name;
|
||||
export function getErrorDetails(error: Error | DetailsError): IErrorDetails {
|
||||
const exceptionMessage = hasDetails(error) ? error.errorMessage : error.message || error.name;
|
||||
return {
|
||||
name: error.name,
|
||||
message: exceptionMessage,
|
||||
|
||||
@@ -3,6 +3,7 @@ export * from './Resource/CachedMapResource';
|
||||
export * from './Resource/CachedResource';
|
||||
export * from './Resource/ResourceKeyList';
|
||||
export * from './CustomGraphQLClient';
|
||||
export * from './DetailsError';
|
||||
export * from './EnvironmentService';
|
||||
export * from './EServerErrorCode';
|
||||
export * from './getErrorDetails';
|
||||
|
||||
@@ -90,7 +90,7 @@ export class Deferred<T> {
|
||||
protected toCancelled(reason?: any): void {
|
||||
this.state = EDeferredState.CANCELLED;
|
||||
this.rejectionReason = reason;
|
||||
this.promiseExecutor.reject(new PromiseCancelledError(reason));
|
||||
this.promiseExecutor.reject(reason);
|
||||
}
|
||||
|
||||
protected toCancelling(): void {
|
||||
|
||||
+4
-4
@@ -15,7 +15,7 @@ import { injectable, IInitializableController, IDestructibleController } from '@
|
||||
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications';
|
||||
import { GQLError, DatabaseAuthModel } from '@cloudbeaver/core-sdk';
|
||||
import { DatabaseAuthModel, DetailsError } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import { TemplateConnectionsResource } from '../TemplateConnectionsResource';
|
||||
|
||||
@@ -43,7 +43,7 @@ implements IInitializableController, IDestructibleController, IConnectionControl
|
||||
hasDetails = false;
|
||||
responseMessage: string | null = null;
|
||||
|
||||
private exception: GQLError | null = null;
|
||||
private exception: DetailsError | null = null;
|
||||
private onClose!: () => void;
|
||||
private isDistructed = false;
|
||||
|
||||
@@ -152,8 +152,8 @@ implements IInitializableController, IDestructibleController, IConnectionControl
|
||||
}
|
||||
|
||||
private showError(exception: Error, message: string) {
|
||||
if (exception instanceof GQLError && !this.isDistructed) {
|
||||
this.responseMessage = exception.errorText;
|
||||
if (exception instanceof DetailsError && !this.isDistructed) {
|
||||
this.responseMessage = exception.errorMessage;
|
||||
this.hasDetails = exception.hasDetails();
|
||||
this.exception = exception;
|
||||
} else {
|
||||
|
||||
@@ -31,6 +31,9 @@ export class DataExportMenuService {
|
||||
isPresent(context) {
|
||||
return context.contextType === TableFooterMenuService.nodeContextType;
|
||||
},
|
||||
isDisabled(context) {
|
||||
return context.data.model.isLoading() || !context.data.model.getResult(context.data.resultIndex);
|
||||
},
|
||||
order: 5,
|
||||
title: 'data_transfer_dialog_export',
|
||||
icon: 'table-export',
|
||||
|
||||
@@ -158,9 +158,7 @@ export class FetchTableDataAsyncProcess extends Deferred<SqlExecuteInfo> {
|
||||
private onError(error: Error, status?: string) {
|
||||
// if task failed to execute during cancelling - it means it was cancelled successfully
|
||||
if (this.getState() === EDeferredState.CANCELLING) {
|
||||
this.toCancelled();
|
||||
const message = `Data fetch has been canceled${status ? `: ${status}` : ''}`;
|
||||
this.notificationService.logException(error, 'Data fetch Error', message);
|
||||
this.toCancelled(error);
|
||||
} else {
|
||||
this.toRejected(error);
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ import { observable, makeObservable } from 'mobx';
|
||||
|
||||
import type { CommonDialogService } from '@cloudbeaver/core-dialogs';
|
||||
import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications';
|
||||
import { GQLError } from '@cloudbeaver/core-sdk';
|
||||
import { DetailsError } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import type { IDataContainerOptions } from '../ContainerDataSource';
|
||||
import { DatabaseDataModel } from '../DatabaseDataModel/DatabaseDataModel';
|
||||
import type { DatabaseDataAccessMode } from '../DatabaseDataModel/IDatabaseDataModel';
|
||||
import { DatabaseDataAccessMode } from '../DatabaseDataModel/IDatabaseDataModel';
|
||||
import type { IDatabaseDataResult } from '../DatabaseDataModel/IDatabaseDataResult';
|
||||
import type { IDatabaseDataSource } from '../DatabaseDataModel/IDatabaseDataSource';
|
||||
import type { RowDiff } from './TableDataModel/EditedRow';
|
||||
@@ -128,17 +128,34 @@ export class DataModelWrapper extends DatabaseDataModel<IDataContainerOptions, I
|
||||
async requestDataPortion(offset: number, count: number): Promise<void> {
|
||||
if (!this.isDataAvailable(offset, count)) {
|
||||
this.source.setSlice(offset, count);
|
||||
this.results = await this.source.requestData(this.results);
|
||||
await this.setDeprecatedModelData();
|
||||
this.clearErrors();
|
||||
try {
|
||||
this.results = await this.source.requestData(this.results);
|
||||
await this.setDeprecatedModelData();
|
||||
} catch (exception) {
|
||||
this.showError(exception);
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated will be refactored
|
||||
*/
|
||||
showDetails = (): void => {
|
||||
if (this.exception) {
|
||||
this.commonDialogService.open(ErrorDetailsDialog, this.exception);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated will be refactored
|
||||
*/
|
||||
clearErrors = (): void => {
|
||||
this.errorMessage = '';
|
||||
this.exception = null;
|
||||
};
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
await this.source.dispose();
|
||||
}
|
||||
@@ -199,14 +216,17 @@ export class DataModelWrapper extends DatabaseDataModel<IDataContainerOptions, I
|
||||
model.refresh();
|
||||
model.setColumns(result.data.columns);
|
||||
model.insertRows(0, result.data.rows, !result.loadedFully);
|
||||
model.access = this.results.length > 1
|
||||
? DatabaseDataAccessMode.Readonly
|
||||
: this.access;
|
||||
}
|
||||
}
|
||||
|
||||
private showError(exception: any) {
|
||||
this.exception = null;
|
||||
this.hasDetails = false;
|
||||
if (exception instanceof GQLError) {
|
||||
this.errorMessage = exception.errorText;
|
||||
if (exception instanceof DetailsError) {
|
||||
this.errorMessage = exception.errorMessage;
|
||||
this.exception = exception;
|
||||
this.hasDetails = exception.hasDetails();
|
||||
} else {
|
||||
@@ -214,11 +234,6 @@ export class DataModelWrapper extends DatabaseDataModel<IDataContainerOptions, I
|
||||
}
|
||||
}
|
||||
|
||||
private clearErrors() {
|
||||
this.errorMessage = '';
|
||||
this.exception = null;
|
||||
}
|
||||
|
||||
private getDefaultRowsCount(count?: number) {
|
||||
return count
|
||||
? Math.max(
|
||||
|
||||
@@ -94,11 +94,24 @@ export const TableFooter = observer(function TableFooter({
|
||||
return styled(useStyles(tableFooterStyles))(
|
||||
<table-footer as="div">
|
||||
<reload as="div">
|
||||
<IconButton type="button" name='reload' viewBox="0 0 16 16" onClick={() => model.refresh()} />
|
||||
<IconButton
|
||||
type="button"
|
||||
name='reload'
|
||||
viewBox="0 0 16 16"
|
||||
disabled={model.isLoading()}
|
||||
onClick={() => model.refresh()}
|
||||
/>
|
||||
</reload>
|
||||
<count as="div">
|
||||
<SubmittingForm onSubmit={handleChange}>
|
||||
<input ref={ref} type="number" value={model.countGain} onBlur={handleChange} {...use({ mod: 'surface' })} />
|
||||
<input
|
||||
ref={ref}
|
||||
type="number"
|
||||
value={model.countGain}
|
||||
disabled={model.isLoading()}
|
||||
onBlur={handleChange}
|
||||
{...use({ mod: 'surface' })}
|
||||
/>
|
||||
</SubmittingForm>
|
||||
</count>
|
||||
<TableFooterMenu model={model} resultIndex={resultIndex} />
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ export class TableFooterMenuService {
|
||||
if (context.data.model.deprecatedModels.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return !context.data.model.getOldModel(context.data.resultIndex)?.isEdited();
|
||||
return context.data.model.isLoading() || !context.data.model.getOldModel(context.data.resultIndex)?.isEdited();
|
||||
},
|
||||
order: 1,
|
||||
title: 'ui_processing_save',
|
||||
|
||||
@@ -44,21 +44,6 @@ export const TableGrid = observer(function TableGrid({
|
||||
const depModel = model.getOldModel(resultIndex);
|
||||
const translate = useTranslate();
|
||||
|
||||
// TODO: probably must be implemented in presentation component
|
||||
if (model.message.length > 0) {
|
||||
return styled(styles)(
|
||||
<error as="div">
|
||||
{model.message}
|
||||
<br /><br />
|
||||
{model.details && (
|
||||
<Button type='button' mod={['outlined']} onClick={model.showDetails}>
|
||||
{translate('ui_errors_details')}
|
||||
</Button>
|
||||
)}
|
||||
</error>
|
||||
);
|
||||
}
|
||||
|
||||
if (dataFormat !== presentation.dataFormat) {
|
||||
if (model.isLoading()) {
|
||||
return null;
|
||||
@@ -74,5 +59,7 @@ export const TableGrid = observer(function TableGrid({
|
||||
return styled(styles)(<TextPlaceholder>{translate('data_viewer_nodata_message')}</TextPlaceholder>);
|
||||
}
|
||||
|
||||
return styled(styles)(<Presentation model={model} resultIndex={resultIndex} />);
|
||||
return styled(styles)(
|
||||
<Presentation model={model} resultIndex={resultIndex} />
|
||||
);
|
||||
});
|
||||
|
||||
+15
-11
@@ -48,23 +48,26 @@ export const TableWhereFilter = observer(function TableWhereFilter({
|
||||
const [filterValue, setValue] = useState(() => context.source.options?.whereFilter || '');
|
||||
|
||||
const handleApply = useCallback(() => {
|
||||
if (context.isLoading()) {
|
||||
return;
|
||||
}
|
||||
context.source.options!.whereFilter = filterValue;
|
||||
context.refresh();
|
||||
}, [context, filterValue]);
|
||||
|
||||
const resetFilter = useCallback(
|
||||
() => {
|
||||
const applyNeeded = context.source.options?.whereFilter === filterValue;
|
||||
const resetFilter = useCallback(() => {
|
||||
if (context.isLoading()) {
|
||||
return;
|
||||
}
|
||||
const applyNeeded = context.source.options?.whereFilter === filterValue;
|
||||
|
||||
setValue('');
|
||||
setValue('');
|
||||
|
||||
if (applyNeeded) {
|
||||
context.source.options!.whereFilter = '';
|
||||
context.refresh();
|
||||
}
|
||||
},
|
||||
[context, filterValue]
|
||||
);
|
||||
if (applyNeeded) {
|
||||
context.source.options!.whereFilter = '';
|
||||
context.refresh();
|
||||
}
|
||||
}, [context, filterValue]);
|
||||
|
||||
return styled(useStyles(styles))(
|
||||
<InlineEditor
|
||||
@@ -73,6 +76,7 @@ export const TableWhereFilter = observer(function TableWhereFilter({
|
||||
placeholder={translate('table_header_sql_expression')}
|
||||
controlsPosition='inside'
|
||||
edited={!!filterValue}
|
||||
disabled={context.isLoading() || context.results.length > 1}
|
||||
simple
|
||||
onSave={handleApply}
|
||||
onUndo={resetFilter}
|
||||
|
||||
@@ -42,11 +42,16 @@ export const PresentationTab = observer(function PresentationTab({
|
||||
style={verticalRotatedTabStyles}
|
||||
model={model}
|
||||
presentation={presentation}
|
||||
disabled={model.isLoading()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return styled(styles)(
|
||||
<Tab tabId={presentation.id} style={[verticalRotatedTabStyles, style]}>
|
||||
<Tab
|
||||
tabId={presentation.id}
|
||||
style={[verticalRotatedTabStyles, style]}
|
||||
disabled={model.isLoading()}
|
||||
>
|
||||
<TabIcon icon={presentation.icon} />
|
||||
<TabTitle>{presentation.title}</TabTitle>
|
||||
</Tab>
|
||||
|
||||
@@ -71,7 +71,12 @@ export const TableLeftBar = observer(function TableLeftBar({
|
||||
<TabsState currentTabId={presentationId} onChange={changePresentation}>
|
||||
<TabList>
|
||||
{presentations.map(presentation => (
|
||||
<Tab key={presentation.id} presentation={presentation} model={model} style={styles} />
|
||||
<Tab
|
||||
key={presentation.id}
|
||||
presentation={presentation}
|
||||
model={model}
|
||||
style={styles}
|
||||
/>
|
||||
))}
|
||||
</TabList>
|
||||
</TabsState>
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { PropsWithChildren, useCallback } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
import styled, { css, use } from 'reshadow';
|
||||
|
||||
import { Loader, TextPlaceholder } from '@cloudbeaver/core-blocks';
|
||||
import { Button, Loader, TextPlaceholder, useStateDelay } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { useTranslate } from '@cloudbeaver/core-localization';
|
||||
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
|
||||
import { composes, useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { DataPresentationService } from '../DataPresentationService';
|
||||
import { TableFooter } from './TableFooter/TableFooter';
|
||||
@@ -21,26 +23,52 @@ import { TableHeader } from './TableHeader/TableHeader';
|
||||
import { TableLeftBar } from './TableLeftBar/TableLeftBar';
|
||||
import { TableViewerStorageService } from './TableViewerStorageService';
|
||||
|
||||
const viewerStyles = css`
|
||||
table-viewer {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
table-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
table-data, table-box {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
const viewerStyles = composes(
|
||||
css`
|
||||
error {
|
||||
composes: theme-background-surface from global;
|
||||
}
|
||||
`,
|
||||
css`
|
||||
table-viewer {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
table-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
table-data, table-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
error {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
white-space: pre-wrap;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
error[|animated] {
|
||||
opacity: 1;
|
||||
}
|
||||
Button {
|
||||
margin-right: 16px;
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
type TableViewerProps = PropsWithChildren<{
|
||||
tableId: string;
|
||||
@@ -57,10 +85,13 @@ export const TableViewer = observer(function TableViewer({
|
||||
className,
|
||||
onPresentationChange,
|
||||
}: TableViewerProps) {
|
||||
const styles = useStyles(viewerStyles);
|
||||
const dataPresentationService = useService(DataPresentationService);
|
||||
const tableViewerStorageService = useService(TableViewerStorageService);
|
||||
const dataModel = tableViewerStorageService.get(tableId);
|
||||
const result = dataModel?.getResult(resultIndex);
|
||||
const translate = useTranslate();
|
||||
const animated = useStateDelay(!!dataModel && dataModel.message.length !== 0, 1);
|
||||
|
||||
const handlePresentationChange = useCallback((id: string) => {
|
||||
const presentation = dataPresentationService.get(id);
|
||||
@@ -87,7 +118,7 @@ export const TableViewer = observer(function TableViewer({
|
||||
return <TextPlaceholder>There are no available presentation for data format: {dataFormat}</TextPlaceholder>;
|
||||
}
|
||||
|
||||
return styled(viewerStyles)(
|
||||
return styled(styles)(
|
||||
<table-viewer as="div" className={className}>
|
||||
<TableHeader model={dataModel} />
|
||||
<table-content as='div'>
|
||||
@@ -105,16 +136,28 @@ export const TableViewer = observer(function TableViewer({
|
||||
presentation={presentation}
|
||||
resultIndex={resultIndex}
|
||||
/>
|
||||
<error as="div" hidden={dataModel.message.length === 0} {...use({ animated })}>
|
||||
{dataModel.message}
|
||||
<br /><br />
|
||||
<Button type='button' mod={['outlined']} onClick={dataModel.clearErrors}>
|
||||
{translate('ui_error_close')}
|
||||
</Button>
|
||||
{dataModel.details && (
|
||||
<Button type='button' mod={['unelevated']} onClick={dataModel.showDetails}>
|
||||
{translate('ui_errors_details')}
|
||||
</Button>
|
||||
)}
|
||||
</error>
|
||||
<Loader
|
||||
loading={dataModel.isLoading()}
|
||||
cancelDisabled={!dataModel.source.canCancel}
|
||||
overlay={dataModel.results.length > 0 && presentation.dataFormat === dataFormat}
|
||||
onCancel={() => dataModel.source.cancel()}
|
||||
/>
|
||||
</table-box>
|
||||
<TableFooter model={dataModel} resultIndex={resultIndex} />
|
||||
</table-data>
|
||||
</table-content>
|
||||
<Loader
|
||||
loading={dataModel.isLoading()}
|
||||
cancelDisabled={!dataModel.source.canCancel}
|
||||
overlay
|
||||
onCancel={() => dataModel.source.cancel()}
|
||||
/>
|
||||
</table-viewer>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Subject, Observable } from 'rxjs';
|
||||
|
||||
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
|
||||
import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications';
|
||||
import { GQLError, SqlDataFilterConstraint } from '@cloudbeaver/core-sdk';
|
||||
import { DetailsError, SqlDataFilterConstraint } from '@cloudbeaver/core-sdk';
|
||||
import { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { DatabaseDataAccessMode } from '../DatabaseDataModel/IDatabaseDataModel';
|
||||
@@ -218,9 +218,9 @@ export class TableViewerModel {
|
||||
let hasDetails = false;
|
||||
let message = `${exception.name}: ${exception.message}`;
|
||||
|
||||
if (exception instanceof GQLError) {
|
||||
if (exception instanceof DetailsError) {
|
||||
hasDetails = exception.hasDetails();
|
||||
message = exception.errorText;
|
||||
message = exception.errorMessage;
|
||||
}
|
||||
|
||||
const state = await this.commonDialogService.open(
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
* 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 { ResultDataFormat, SqlDataFilter } from '@cloudbeaver/core-sdk';
|
||||
|
||||
import { ISqlQueryParams } from './ISqlEditorTabState';
|
||||
import { SqlExecutionState } from './SqlExecutionState';
|
||||
import { SQLQueryExecutionProcess } from './SqlResultTabs/SQLQueryExecutionProcess';
|
||||
|
||||
export interface ISqlEditorGroupMetadata {
|
||||
resultDataProcess: SQLQueryExecutionProcess;
|
||||
start: (
|
||||
context: SqlExecutionState,
|
||||
sqlQueryParams: ISqlQueryParams,
|
||||
filter: SqlDataFilter,
|
||||
dataFormat: ResultDataFormat,
|
||||
) => Promise<SQLQueryExecutionProcess>;
|
||||
dispose: (params: ISqlQueryParams) => Promise<void>;
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { IExecutionContext } from '@cloudbeaver/plugin-data-viewer';
|
||||
import type { IExecutionContext } from '@cloudbeaver/plugin-data-viewer';
|
||||
|
||||
export interface ISqlQueryParams extends IExecutionContext {
|
||||
query: string;
|
||||
|
||||
@@ -153,9 +153,7 @@ export class SQLQueryExecutionProcess extends Deferred<SqlExecuteInfo> {
|
||||
private onError(error: Error, status?: string) {
|
||||
// if task failed to execute during cancelling - it means it was cancelled successfully
|
||||
if (this.getState() === EDeferredState.CANCELLING) {
|
||||
this.toCancelled();
|
||||
const message = `Query execution has been canceled${status ? `: ${status}` : ''}`;
|
||||
this.notificationService.logException(error, 'Query execution Error', message);
|
||||
this.toCancelled(error);
|
||||
} else {
|
||||
this.toRejected(error);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ConnectionInfoResource } from '@cloudbeaver/core-connections';
|
||||
import { injectable } from '@cloudbeaver/core-di';
|
||||
import { NotificationService } from '@cloudbeaver/core-events';
|
||||
import { GraphQLService } from '@cloudbeaver/core-sdk';
|
||||
import { uuid, MetadataMap, isPromiseCancelledError } from '@cloudbeaver/core-utils';
|
||||
import { uuid, MetadataMap, EDeferredState } from '@cloudbeaver/core-utils';
|
||||
import { DatabaseDataAccessMode, DataModelWrapper, fetchingSettings, TableViewerStorageService } from '@cloudbeaver/plugin-data-viewer';
|
||||
|
||||
import type {
|
||||
@@ -146,8 +146,10 @@ export class SqlResultTabsService {
|
||||
);
|
||||
} catch (exception) {
|
||||
// remove first panel if execution was cancelled
|
||||
if (isPromiseCancelledError(exception) && isNewTabCreated) {
|
||||
if (source.queryExecutionProcess?.getState() === EDeferredState.CANCELLED && isNewTabCreated) {
|
||||
this.removeGroup(editorState, tabGroup.groupId);
|
||||
const message = `Query execution has been canceled${status ? `: ${status}` : ''}`;
|
||||
this.notificationService.logException(exception, 'Query execution Error', message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user