Merge pull request #31 from dbeaver/feat-data-export

Feat data export
This commit is contained in:
Alexey
2020-05-03 20:49:14 +03:00
committed by GitHub
78 changed files with 2306 additions and 569 deletions
@@ -3,13 +3,9 @@ scalar Object
# Date/Time
scalar DateTime
type Query {
type Query
}
type Mutation {
}
type Mutation
schema {
query: Query
@@ -7,61 +7,11 @@
*/
import { observer } from 'mobx-react';
import styled, { css } from 'reshadow';
import { useCallback } from 'react';
import { DBDriver, DBSource } from '@dbeaver/core/app';
import { StaticImage } from '@dbeaver/core/blocks';
import { useStyles, composes } from '@dbeaver/core/theming';
import { ListItem } from '@dbeaver/core/blocks';
const styles = composes(
css`
db-source-name {
composes: theme-border-color-secondary from global;
}
`,
css`
StaticImage {
box-sizing: border-box;
width: 24px;
}
db-source {
position: relative;
cursor: pointer;
display: flex;
box-sizing: border-box;
align-items: center;
padding: 8px 12px;
}
db-source-icon {
display: flex;
align-items: center;
box-sizing: border-box;
padding: 0 12px;
}
db-source-name {
composes: theme-typography--body1 from global;
box-sizing: border-box;
font-weight: 500;
min-width: 250px;
padding: 0 24px 0 12px;
border-right: 1px solid;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
db-source-description {
composes: theme-typography--body2 from global;
box-sizing: border-box;
max-width: 460px;
padding: 0 12px 0 24px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`
);
type DBSourceItemProps = {
dbSource: DBSource;
@@ -70,12 +20,7 @@ type DBSourceItemProps = {
}
export const DBSourceItem = observer(function DBSourceItem({ dbSource, dbDriver, onSelect }: DBSourceItemProps) {
const select = useCallback(() => onSelect(dbSource.id), [dbSource]);
return styled(useStyles(styles))(
<db-source as="div" onClick={() => onSelect(dbSource.id)}>
<db-source-icon as="div"><StaticImage icon={dbDriver?.icon}/></db-source-icon>
<db-source-name as="div">{dbSource.name}</db-source-name>
<db-source-description as="div" title={dbSource.description}>{dbSource.description}</db-source-description>
</db-source>
);
return <ListItem icon={dbDriver?.icon} name={dbSource.name} description={dbSource.description} onClick={select}/>;
});
@@ -8,52 +8,12 @@
import { observer } from 'mobx-react';
import { useState, useMemo } from 'react';
import styled, { css, use } from 'reshadow';
import { DBDriver, DBSource } from '@dbeaver/core/app';
import { StaticImage } from '@dbeaver/core/blocks';
import { useTranslate } from '@dbeaver/core/localization';
import { useStyles, composes } from '@dbeaver/core/theming';
import { ItemList, ItemListSearch } from '@dbeaver/core/blocks';
import { DBSourceItem } from './DBSourceItem';
const styles = composes(
css`
list-item {
composes: theme-ripple theme-background-surface theme-border-color-secondary from global;
}
list-search {
composes: theme-background-secondary from global;
}
`,
css`
list {
box-sizing: border-box;
border-collapse: collapse;
}
list-item {
border-bottom: 1px solid;
}
list-search {
position: sticky;
top: 0;
padding: 8px 24px;
z-index: 1;
& input {
padding: 4px 24px;
padding-left: 32px;
}
& StaticImage {
position: absolute;
top: 15px;
left: 32px;
}
}
`
);
type DBSourceSelectorProps = {
dbSources: DBSource[];
dbDrivers: Map<string, DBDriver>;
@@ -68,7 +28,6 @@ export const DBSourceSelector = observer(function DBSourceSelector({
onSelect,
}: DBSourceSelectorProps) {
const [search, setSearch] = useState('');
const translate = useTranslate();
const filteredDBSources = useMemo(() => {
if (!search) {
return dbSources;
@@ -76,26 +35,17 @@ export const DBSourceSelector = observer(function DBSourceSelector({
return dbSources.filter(source => source.name.toUpperCase().includes(search.toUpperCase()));
}, [search, dbSources]);
return styled(useStyles(styles))(
<list as="div" className={className}>
<list-search as="div">
<StaticImage icon='/icons/search.svg' />
<input
name='search'
placeholder={translate('ui_search')}
onChange={event => setSearch(event.target.value)}
{...use({ mod: 'surface' })}
/>
</list-search>
return (
<ItemList className={className}>
<ItemListSearch onSearch={setSearch} />
{filteredDBSources.map(dbSource => (
<list-item as="div" key={dbSource.id}>
<DBSourceItem
dbSource={dbSource}
dbDriver={dbDrivers.get(dbSource.driverId)}
onSelect={onSelect}
/>
</list-item>
<DBSourceItem
key={dbSource.id}
dbSource={dbSource}
dbDriver={dbDrivers.get(dbSource.driverId)}
onSelect={onSelect}
/>
))}
</list>
</ItemList>
);
});
+8 -3
View File
@@ -17,9 +17,6 @@ module.exports = {
customConnection_connectionType_url: 'URL',
customConnection_options: 'Options',
customConnection_properties: 'Driver Properties',
customConnection_properties_name: 'Name',
customConnection_properties_value: 'Value',
customConnection_properties_add: 'Add',
customConnection_custom_host: 'Host',
customConnection_custom_obligatory: '(obligatory)',
customConnection_custom_port: 'Port',
@@ -31,6 +28,11 @@ module.exports = {
customConnection_test: 'Test Connection',
customConnection_create: 'Create',
data_transfer_dialog_title: 'Export data',
data_transfer_dialog_export: 'Export',
data_transfer_dialog_exporting_object: 'Exporting:',
data_transfer_dialog_configuration_title: 'Export configuration',
ui_stepper_back: 'Back',
ui_processing_loading: 'Loading...',
ui_processing_cancel: 'Cancel',
@@ -39,6 +41,9 @@ module.exports = {
ui_errors_details: 'Details',
ui_search: 'Search...',
ui_data_saving_error: 'Error when saving changes',
block_properties_table_name: 'Name',
block_properties_table_value: 'Value',
block_properties_table_add: 'Add',
app_root_session_expired_title: 'Session expired',
app_root_session_expired_message: 'Session has been expired. Do you want to reload?',
+9 -4
View File
@@ -16,9 +16,6 @@ module.exports = {
customConnection_connectionType_url: 'URL',
customConnection_options: 'Подключение',
customConnection_properties: 'Параметры драйвера',
customConnection_properties_name: 'Название',
customConnection_properties_value: 'Значение',
customConnection_properties_add: 'Добавить',
customConnection_custom_host: 'Хост',
customConnection_custom_obligatory: '(обязательное)',
customConnection_custom_port: 'Порт',
@@ -30,14 +27,22 @@ module.exports = {
customConnection_test: 'Проверить',
customConnection_create: 'Создать',
data_transfer_dialog_title: 'Экспорт данных',
data_transfer_dialog_export: 'Экспортировать',
data_transfer_dialog_exporting_object: 'Экспортировать:',
data_transfer_dialog_configuration_title: 'Настройка экспорта',
ui_stepper_back: 'Назад',
ui_processing_loading: 'Загрузка...',
ui_processing_cancel: 'Отменить...',
ui_processing_cancel: 'Отменить',
ui_processing_retry: 'Повторить',
ui_error_message: 'Ошибка:',
ui_errors_details: 'Информация',
ui_search: 'Поиск...',
ui_data_saving_error: 'Ошибка при сохранении изменений',
block_properties_name: 'Название',
block_properties_value: 'Значение',
block_properties_add: 'Добавить',
app_root_session_expired_title: 'Сесстия истекла',
app_root_session_expired_message: 'Сессия истекла. Хотите перезагрузить страницу?',
+3 -1
View File
@@ -1,8 +1,10 @@
schema:
- ../../../server/bundles/io.cloudbeaver.server/schema/schema.graphqls
- ../../../server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls
- ../../../server/bundles/io.cloudbeaver.server/schema/service.core.graphqls
- ../../../server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls
- ../../../server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls
- ../../../server/bundles/io.cloudbeaver.service.metadata/schema/service.metadata.graphqls
- ../../../server/bundles/io.cloudbeaver.service.data.transfer/schema/service.data.transfer.graphqls
documents:
- ./src/**/*.gql
generates:
@@ -42,8 +42,8 @@ export type ObjectContainer = Pick<DatabaseObjectInfo, 'name' | 'description' |
export class ConnectionsManagerService {
@observable private connectionsMap: Map<string, Connection> = new Map();
dbDrivers = new CachedResource(new Map(), this.refreshDriversAsync.bind(this));
connectionObjectContainers = new CachedResource(new Map(), this.refreshObjectContainersAsync.bind(this));
readonly dbDrivers = new CachedResource(new Map(), this.refreshDriversAsync.bind(this));
readonly connectionObjectContainers = new CachedResource(new Map(), this.refreshObjectContainersAsync.bind(this));
@computed get connections(): Connection[] {
return Array.from(this.connectionsMap.values());
@@ -14,9 +14,10 @@ import { StaticImage } from './StaticImage';
export type IconOrImageProps = {
icon: string;
className?: string;
viewBox?: string;
};
export const IconOrImage = function IconOrImage({ icon, className }: IconOrImageProps) {
export const IconOrImage = function IconOrImage({ icon, className, viewBox }: IconOrImageProps) {
const isStaticIcon = useMemo(
() => icon && icon.startsWith('platform:'),
@@ -27,5 +28,5 @@ export const IconOrImage = function IconOrImage({ icon, className }: IconOrImage
return <StaticImage icon={icon} className={className}/>;
}
return <Icon name={icon} className={className} viewBox="0 0 32 32" />;
return <Icon name={icon} className={className} viewBox={viewBox || '0 0 32 32'} />;
};
@@ -0,0 +1,31 @@
/*
* 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 styled from 'reshadow';
import { useStyles, Style } from '@dbeaver/core/theming';
import { Styles, ITEM_LIST_STYLES_ARRAY } from './styles';
type ItemListProps = React.PropsWithChildren<{
className?: string;
styles?: Style[];
}>
export function ItemList({
children, className, styles,
}: ItemListProps) {
return styled(useStyles(...(styles || ITEM_LIST_STYLES_ARRAY)))(
<item-list as="div" className={className}>
<Styles.Provider value={styles || ITEM_LIST_STYLES_ARRAY}>
{children}
</Styles.Provider>
</item-list>
);
}
@@ -0,0 +1,48 @@
/*
* 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 {
useState, useCallback, useContext
} from 'react';
import styled, { use } from 'reshadow';
import { StaticImage } from '@dbeaver/core/blocks';
import { useTranslate } from '@dbeaver/core/localization';
import { useStyles } from '@dbeaver/core/theming';
import { Styles } from './styles';
type ItemListProps = React.PropsWithChildren<{
onSearch?(value: string): void;
className?: string;
}>
export function ItemListSearch({ onSearch, className }: ItemListProps) {
const styles = useContext(Styles);
const [search, setSearch] = useState('');
const translate = useTranslate();
const searchHandler = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setSearch(event.target.value);
if (onSearch) {
onSearch(event.target.value);
}
}, [onSearch]);
return styled(useStyles(...(styles || [])))(
<list-search as="div" className={className}>
<StaticImage icon='/icons/search.svg' />
<input
name='search'
placeholder={translate('ui_search')}
value={search}
onChange={searchHandler}
{...use({ mod: 'surface' })}
/>
</list-search>
);
}
@@ -0,0 +1,41 @@
/*
* 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 { useContext } from 'react';
import styled from 'reshadow';
import { StaticImage } from '@dbeaver/core/blocks';
import { useStyles } from '@dbeaver/core/theming';
import { Styles } from './styles';
type ListItemProps = {
name?: string;
icon?: string;
description?: string;
onClick(): void;
className?: string;
}
export function ListItem({
name,
icon,
description,
onClick,
className,
}: ListItemProps) {
const styles = useContext(Styles);
return styled(useStyles(...styles))(
<list-item as="div" onClick={onClick} className={className}>
<list-item-icon as="div"><StaticImage icon={icon}/></list-item-icon>
<list-item-name as="div">{name}</list-item-name>
<list-item-description as="div" title={description}>{description}</list-item-description>
</list-item>
);
}
@@ -0,0 +1,4 @@
export * from './ItemList';
export * from './ItemListSearch';
export * from './ListItem';
export { ITEM_LIST_STYLES } from './styles';
@@ -0,0 +1,98 @@
/*
* 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 { createContext } from 'react';
import { css } from 'reshadow';
import { composes, Style } from '@dbeaver/core/theming';
export const ITEM_LIST_STYLES = composes(
css`
list-item {
composes: theme-ripple theme-background-surface theme-border-color-secondary from global;
}
list-search {
composes: theme-background-secondary theme-text-on-secondary from global;
}
list-item-name {
composes: theme-border-color-secondary from global;
}
`,
css`
item-list {
box-sizing: border-box;
border-collapse: collapse;
z-index: 0;
}
list-search {
position: sticky;
top: 0;
padding: 8px 24px;
z-index: 1;
& input {
padding: 4px 24px;
padding-left: 32px;
}
& StaticImage {
position: absolute;
top: 15px;
left: 32px;
}
}
list-item {
border-bottom: 1px solid;
}
list-item {
position: relative;
cursor: pointer;
display: flex;
box-sizing: border-box;
align-items: center;
padding: 8px 12px;
}
list-item-icon {
display: flex;
align-items: center;
box-sizing: border-box;
padding: 0 12px;
& StaticImage {
box-sizing: border-box;
width: 24px;
}
}
list-item-name {
composes: theme-typography--body1 from global;
box-sizing: border-box;
font-weight: 500;
min-width: 250px;
padding: 0 24px 0 12px;
border-right: 1px solid;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
list-item-description {
composes: theme-typography--body2 from global;
box-sizing: border-box;
max-width: 460px;
padding: 0 12px 0 24px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`
);
export const ITEM_LIST_STYLES_ARRAY = [ITEM_LIST_STYLES];
export const Styles = createContext<Style[]>(ITEM_LIST_STYLES_ARRAY);
@@ -0,0 +1,15 @@
/*
* 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 interface IProperty {
id: string;
name: string;
defaultValue?: string;
description?: string;
validValues?: string[];
}
@@ -0,0 +1,129 @@
/*
* 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, useLocalStore } from 'mobx-react';
import { useCallback } from 'react';
import styled from 'reshadow';
import { Button } from '@dbeaver/core/blocks';
import { useTranslate } from '@dbeaver/core/localization';
import { useStyles } from '@dbeaver/core/theming';
import { IProperty } from './IProperty';
import { PropertyItem } from './PropertyItem';
import { PROPERTIES_TABLE_STYLES } from './styles';
type PropertiesState = {
[key: string]: string;
}
type PropertiesTableProps = {
properties: IProperty[];
propertiesState?: PropertiesState;
onNameChange?(id: string, name: string): void;
onChange?(state: PropertiesState): void;
onAdd?(): void;
onRemove?(id: string): void;
className?: string;
}
export const PropertiesTable = observer(function PropertiesTable({
propertiesState,
properties,
onNameChange,
onChange,
onAdd,
onRemove,
className,
}: PropertiesTableProps) {
const translate = useTranslate();
const state = useLocalStore<PropertiesState>(() => (propertiesState || {}));
const changeName = useCallback((id: string, name: string) => {
const property = properties.find(property => property.id === id);
if (!property) {
return;
}
if (state[property.name] !== undefined) {
state[name] = state[property.name];
delete state[property.name];
}
if (onNameChange) {
onNameChange(property.name, name);
}
property.name = name;
}, [properties]);
const changeValue = useCallback((id: string, value: string) => {
const property = properties.find(property => property.id === id);
if (!property) {
return;
}
state[property.name] = value;
if (onChange) {
onChange(state);
}
}, [properties, onChange]);
const removeProperty = useCallback((id: string) => {
const property = properties.find(property => property.id === id);
if (!property) {
return;
}
if (state[property.name] !== undefined) {
delete state[property.name];
}
if (onRemove) {
onRemove(id);
}
properties.splice(properties.indexOf(property), 1);
}, [properties, onRemove]);
const isNameUnique = useCallback(
(name: string) => properties.filter(property => property.name === name).length === 1,
[]
);
return styled(useStyles(PROPERTIES_TABLE_STYLES))(
<properties as="div" className={className}>
<properties-header as="div">
<properties-header-name as="div">
{translate('block_properties_table_name')}
</properties-header-name>
<properties-header-value as="div">
{translate('block_properties_table_value')}
</properties-header-value>
<properties-header-right as="div">
{onAdd && <Button type='button' mod={['outlined']} onClick={onAdd}>{translate('block_properties_table_add')}</Button> }
</properties-header-right>
</properties-header>
<properties-list as="div">
{properties.map(property => (
<PropertyItem
key={property.id}
property={property}
value={state[property.name]}
onNameChange={changeName}
onValueChange={changeValue}
onRemove={removeProperty}
error={!isNameUnique(property.name)}
/>
))}
</properties-list>
</properties>
);
});
@@ -15,8 +15,8 @@ import styled, { css, use } from 'reshadow';
import { ShadowInput, Icon } from '@dbeaver/core/blocks';
import { composes, useStyles } from '@dbeaver/core/theming';
import { DriverPropertyInfoWithStaticId } from './DriverPropertiesController';
import { DriverPropertyValueSelector } from './DriverPropertyValueSelector';
import { IProperty } from './IProperty';
const styles = composes(
css`
@@ -104,8 +104,8 @@ const styles = composes(
`
);
type DriverPropertyProps = {
property: DriverPropertyInfoWithStaticId;
type PropertyItemProps = {
property: IProperty;
value?: string;
onNameChange(staticId: string, newId: string): void;
onValueChange(staticId: string, value: string): void;
@@ -113,24 +113,24 @@ type DriverPropertyProps = {
error?: boolean;
}
export const DriverProperty = observer(function DriverProperty({
export const PropertyItem = observer(function PropertyItem({
property,
value,
onNameChange,
onValueChange,
onRemove,
error,
}: DriverPropertyProps) {
const isEditable = !property.displayName;
}: PropertyItemProps) {
const isEditable = property.name !== property.id;
const edited = value !== undefined && value !== property.defaultValue;
const [focus, setFocus] = useState(false);
const nameInputRef = useRef<HTMLInputElement>(null);
const handleNameChange = useCallback((value: string) => onNameChange(property.staticId, value), [property.staticId]);
const handleNameChange = useCallback((value: string) => onNameChange(property.id, value), [property]);
const handleValueChange = useCallback(
(value: string) => onValueChange(property.staticId, value),
[property.staticId]
(value: string) => onValueChange(property.id, value),
[property]
);
const handleRemove = useCallback(() => onRemove(property.staticId), [property.staticId]);
const handleRemove = useCallback(() => onRemove(property.id), [property]);
useEffect(() => {
if (nameInputRef.current && isEditable) {
@@ -149,7 +149,7 @@ export const DriverProperty = observer(function DriverProperty({
readOnly={!isEditable}
autoComplete='none'
>
{property.displayName || property.id}
{property.name || property.id}
</ShadowInput>
</property-name>
<property-value as='div'>
@@ -0,0 +1,2 @@
export * from './PropertiesTable';
export * from './IProperty';
@@ -0,0 +1,60 @@
/*
* 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 { css } from 'reshadow';
import { composes } from '@dbeaver/core/theming';
export const PROPERTIES_TABLE_STYLES = composes(
css`
properties-header {
composes: theme-background-secondary theme-text-on-secondary from global;
}
properties-header-name, properties-header-value {
composes: theme-background-surface theme-text-on-surface from global;
}
`,
css`
properties {
display: flex;
flex: 1;
flex-direction: column;
}
properties-header {
box-sizing: border-box;
display: inline-flex;
padding: 5px 1px;
position: sticky;
z-index: 1;
top: 0;
}
properties-header-name, properties-header-value {
composes: theme-typography--caption from global;
text-transform: uppercase;
box-sizing: border-box;
flex: 1;
padding: 4px 36px;
margin: 0px 1px;
}
properties-header-name {
flex: 0 0 auto;
width: 300px;
}
properties-header-right {
flex: 0 0 auto;
margin: 0px 1px;
}
properties-list {
flex: 1;
display: flex;
flex-direction: column;
padding-bottom: 24px;
}
`
);
+2
View File
@@ -15,3 +15,5 @@ export * from './TextPlaceholder';
export * from './useClipboard';
export * from './SubmittingForm';
export * from './SanitizedHTML/SanitizedHTML';
export * from './ItemList';
export * from './PropertiesTable';
@@ -28,11 +28,15 @@ export class ContextMenuService {
return ContextMenuService.rootPanelId;
}
addPanel(panelId: string) {
this.contextMenu.addRootPanel(panelId);
}
addMenuItem<T>(panelId: string, menuItem: IContextMenuItem<T>) {
this.contextMenu.addMenuItem(panelId, menuItem);
}
createContextMenu<T>(context: IMenuContext<T>): IMenuPanel {
return this.contextMenu.constructMenuWithContext(ContextMenuService.rootPanelId, context);
createContextMenu<T>(context: IMenuContext<T>, panelId?: string): IMenuPanel {
return this.contextMenu.constructMenuWithContext(panelId || ContextMenuService.rootPanelId, context);
}
}
@@ -0,0 +1,20 @@
query exportDataFromContainer(
$connectionId: ID!,
$containerNodePath: ID!,
$parameters: DataTransferParameters!
) {
taskInfo: dataTransferExportDataFromContainer(
connectionId: $connectionId
containerNodePath: $containerNodePath
parameters: $parameters
) {
id
running
taskResult
error {
message
errorCode
stackTrace
}
}
}
@@ -0,0 +1,22 @@
query exportDataFromResults(
$connectionId: ID!,
$contextId: ID!,
$resultsId: ID!,
$parameters: DataTransferParameters!
) {
taskInfo: dataTransferExportDataFromResults(
connectionId: $connectionId
contextId: $contextId
resultsId: $resultsId
parameters: $parameters
) {
id
running
taskResult
error {
message
errorCode
stackTrace
}
}
}
@@ -0,0 +1,24 @@
query getDataTransferProcessors {
processors: dataTransferAvailableStreamProcessors {
id
name
description
fileExtension
appFileExtension
appName
order
icon
properties {
id
displayName
description
category
dataType
value
validValues
features
}
isBinary
isHTML
}
}
@@ -0,0 +1,5 @@
query removeDataTransferFile(
$dataFileId: String!
) {
result: dataTransferRemoveDataFile(dataFileId: $dataFileId)
}
@@ -4,7 +4,7 @@ mutation asyncSqlExecuteQuery(
$query: String!
$filter: SQLDataFilter
) {
result: asyncSqlExecuteQuery(
taskInfo: asyncSqlExecuteQuery(
connectionId: $connectionId
contextId: $contextId
sql: $query
@@ -1,7 +1,7 @@
mutation asyncTaskStatus(
$taskId: String!
) {
result: asyncTaskStatus(
taskInfo: asyncTaskStatus(
id: $taskId
) {
id
+241 -4
View File
@@ -117,6 +117,34 @@ export type DataSourceInfo = {
properties?: Maybe<Scalars["String"]>;
};
export type DataTransferParameters = {
/** Processor ID */
processorId: Scalars["ID"];
/**
* General settings:
* - openNewConnection: opens new database connection for data transfer task
*/
settings?: Maybe<Scalars["Object"]>;
/** Processor properties. See DataTransferProcessorInfo.properties */
processorProperties: Scalars["Object"];
/** Data filter settings */
filter?: Maybe<SqlDataFilter>;
};
export type DataTransferProcessorInfo = {
id: Scalars["ID"];
name?: Maybe<Scalars["String"]>;
description?: Maybe<Scalars["String"]>;
fileExtension?: Maybe<Scalars["String"]>;
appFileExtension?: Maybe<Scalars["String"]>;
appName?: Maybe<Scalars["String"]>;
order: Scalars["Int"];
icon?: Maybe<Scalars["String"]>;
properties?: Maybe<Array<Maybe<ObjectPropertyInfo>>>;
isBinary?: Maybe<Scalars["Boolean"]>;
isHTML?: Maybe<Scalars["Boolean"]>;
};
export type DriverInfo = {
id: Scalars["ID"];
name?: Maybe<Scalars["String"]>;
@@ -357,6 +385,13 @@ export type Query = {
sqlDialectInfo?: Maybe<SqlDialectInfo>;
sqlListContexts?: Maybe<Array<Maybe<SqlContextInfo>>>;
sqlCompletionProposals?: Maybe<Array<Maybe<SqlCompletionProposal>>>;
/** Available transfer processors */
dataTransferAvailableStreamProcessors?: Maybe<
Array<Maybe<DataTransferProcessorInfo>>
>;
dataTransferExportDataFromContainer: AsyncTaskInfo;
dataTransferExportDataFromResults: AsyncTaskInfo;
dataTransferRemoveDataFile?: Maybe<Scalars["Boolean"]>;
/** Get child nodes */
metadataGetNodeDDL?: Maybe<Scalars["String"]>;
};
@@ -410,6 +445,23 @@ export type QuerySqlCompletionProposalsArgs = {
maxResults?: Maybe<Scalars["Int"]>;
};
export type QueryDataTransferExportDataFromContainerArgs = {
connectionId: Scalars["ID"];
containerNodePath: Scalars["ID"];
parameters: DataTransferParameters;
};
export type QueryDataTransferExportDataFromResultsArgs = {
connectionId: Scalars["ID"];
contextId: Scalars["ID"];
resultsId: Scalars["ID"];
parameters: DataTransferParameters;
};
export type QueryDataTransferRemoveDataFileArgs = {
dataFileId: Scalars["String"];
};
export type QueryMetadataGetNodeDdlArgs = {
nodeId: Scalars["ID"];
options?: Maybe<Scalars["Object"]>;
@@ -451,6 +503,7 @@ export type SessionInfo = {
createTime: Scalars["String"];
lastAccessTime: Scalars["String"];
locale: Scalars["String"];
cacheExpired: Scalars["Boolean"];
serverMessages?: Maybe<Array<Maybe<ServerMessage>>>;
connections: Array<Maybe<ConnectionInfo>>;
};
@@ -799,6 +852,81 @@ export type ChangeSessionLanguageMutation = Pick<
"changeSessionLanguage"
>;
export type ExportDataFromContainerQueryVariables = {
connectionId: Scalars["ID"];
containerNodePath: Scalars["ID"];
parameters: DataTransferParameters;
};
export type ExportDataFromContainerQuery = {
taskInfo: Pick<AsyncTaskInfo, "id" | "running" | "taskResult"> & {
error: Maybe<Pick<ServerError, "message" | "errorCode" | "stackTrace">>;
};
};
export type ExportDataFromResultsQueryVariables = {
connectionId: Scalars["ID"];
contextId: Scalars["ID"];
resultsId: Scalars["ID"];
parameters: DataTransferParameters;
};
export type ExportDataFromResultsQuery = {
taskInfo: Pick<AsyncTaskInfo, "id" | "running" | "taskResult"> & {
error: Maybe<Pick<ServerError, "message" | "errorCode" | "stackTrace">>;
};
};
export type GetDataTransferProcessorsQueryVariables = {};
export type GetDataTransferProcessorsQuery = {
processors: Maybe<
Array<
Maybe<
Pick<
DataTransferProcessorInfo,
| "id"
| "name"
| "description"
| "fileExtension"
| "appFileExtension"
| "appName"
| "order"
| "icon"
| "isBinary"
| "isHTML"
> & {
properties: Maybe<
Array<
Maybe<
Pick<
ObjectPropertyInfo,
| "id"
| "displayName"
| "description"
| "category"
| "dataType"
| "value"
| "validValues"
| "features"
>
>
>
>;
}
>
>
>;
};
export type RemoveDataTransferFileQueryVariables = {
dataFileId: Scalars["String"];
};
export type RemoveDataTransferFileQuery = {
result: Query["dataTransferRemoveDataFile"];
};
export type AsyncSqlExecuteQueryMutationVariables = {
connectionId: Scalars["ID"];
contextId: Scalars["ID"];
@@ -807,7 +935,7 @@ export type AsyncSqlExecuteQueryMutationVariables = {
};
export type AsyncSqlExecuteQueryMutation = {
result: Pick<AsyncTaskInfo, "id" | "running"> & {
taskInfo: Pick<AsyncTaskInfo, "id" | "running"> & {
result: Maybe<
Pick<SqlExecuteInfo, "duration" | "statusMessage"> & {
results: Maybe<
@@ -860,7 +988,7 @@ export type AsyncTaskStatusMutationVariables = {
};
export type AsyncTaskStatusMutation = {
result: Pick<AsyncTaskInfo, "id" | "running"> & {
taskInfo: Pick<AsyncTaskInfo, "id" | "running"> & {
result: Maybe<
Pick<SqlExecuteInfo, "duration" | "statusMessage"> & {
results: Maybe<
@@ -1357,6 +1485,83 @@ export const ChangeSessionLanguageDocument = gql`
changeSessionLanguage(locale: $locale)
}
`;
export const ExportDataFromContainerDocument = gql`
query exportDataFromContainer(
$connectionId: ID!
$containerNodePath: ID!
$parameters: DataTransferParameters!
) {
taskInfo: dataTransferExportDataFromContainer(
connectionId: $connectionId
containerNodePath: $containerNodePath
parameters: $parameters
) {
id
running
taskResult
error {
message
errorCode
stackTrace
}
}
}
`;
export const ExportDataFromResultsDocument = gql`
query exportDataFromResults(
$connectionId: ID!
$contextId: ID!
$resultsId: ID!
$parameters: DataTransferParameters!
) {
taskInfo: dataTransferExportDataFromResults(
connectionId: $connectionId
contextId: $contextId
resultsId: $resultsId
parameters: $parameters
) {
id
running
taskResult
error {
message
errorCode
stackTrace
}
}
}
`;
export const GetDataTransferProcessorsDocument = gql`
query getDataTransferProcessors {
processors: dataTransferAvailableStreamProcessors {
id
name
description
fileExtension
appFileExtension
appName
order
icon
properties {
id
displayName
description
category
dataType
value
validValues
features
}
isBinary
isHTML
}
}
`;
export const RemoveDataTransferFileDocument = gql`
query removeDataTransferFile($dataFileId: String!) {
result: dataTransferRemoveDataFile(dataFileId: $dataFileId)
}
`;
export const AsyncSqlExecuteQueryDocument = gql`
mutation asyncSqlExecuteQuery(
$connectionId: ID!
@@ -1364,7 +1569,7 @@ export const AsyncSqlExecuteQueryDocument = gql`
$query: String!
$filter: SQLDataFilter
) {
result: asyncSqlExecuteQuery(
taskInfo: asyncSqlExecuteQuery(
connectionId: $connectionId
contextId: $contextId
sql: $query
@@ -1413,7 +1618,7 @@ export const AsyncTaskCancelDocument = gql`
`;
export const AsyncTaskStatusDocument = gql`
mutation asyncTaskStatus($taskId: String!) {
result: asyncTaskStatus(id: $taskId) {
taskInfo: asyncTaskStatus(id: $taskId) {
id
running
result {
@@ -1835,6 +2040,38 @@ export function getSdk(client: GraphQLClient) {
variables,
);
},
exportDataFromContainer(
variables: ExportDataFromContainerQueryVariables,
): Promise<ExportDataFromContainerQuery> {
return client.request<ExportDataFromContainerQuery>(
print(ExportDataFromContainerDocument),
variables,
);
},
exportDataFromResults(
variables: ExportDataFromResultsQueryVariables,
): Promise<ExportDataFromResultsQuery> {
return client.request<ExportDataFromResultsQuery>(
print(ExportDataFromResultsDocument),
variables,
);
},
getDataTransferProcessors(
variables?: GetDataTransferProcessorsQueryVariables,
): Promise<GetDataTransferProcessorsQuery> {
return client.request<GetDataTransferProcessorsQuery>(
print(GetDataTransferProcessorsDocument),
variables,
);
},
removeDataTransferFile(
variables: RemoveDataTransferFileQueryVariables,
): Promise<RemoveDataTransferFileQuery> {
return client.request<RemoveDataTransferFileQuery>(
print(RemoveDataTransferFileDocument),
variables,
);
},
asyncSqlExecuteQuery(
variables: AsyncSqlExecuteQueryMutationVariables,
): Promise<AsyncSqlExecuteQueryMutation> {
@@ -57,6 +57,8 @@ export class Deferred<T> {
return this.rejectionReason;
}
cancel() {}
@action
protected toResolved(value: T): void {
this.state = EDeferredState.RESOLVED;
@@ -144,7 +144,7 @@ export const ConnectionFormDialog = observer(
<BaseTabPanel {...tab} stopId='driver_properties'>
<DriverProperties
driver={driver}
properties={controller.config.properties}
state={controller.config.properties}
isSelected={tab.selectedId === 'driver_properties'}
/>
</BaseTabPanel>
@@ -11,76 +11,36 @@ import { useEffect } from 'react';
import styled, { css } from 'reshadow';
import { DBDriver } from '@dbeaver/core/app';
import { Button, Loader } from '@dbeaver/core/blocks';
import { Loader, PropertiesTable } from '@dbeaver/core/blocks';
import { useController } from '@dbeaver/core/di';
import { useTranslate } from '@dbeaver/core/localization';
import { useStyles, composes } from '@dbeaver/core/theming';
import { useStyles } from '@dbeaver/core/theming';
import { DriverPropertiesController, DriverPropertyState } from './DriverPropertiesController';
import { DriverProperty } from './DriverProperty';
import { DriverPropertiesController } from './DriverPropertiesController';
const styles = composes(
css`
properties-header {
composes: theme-background-secondary theme-text-on-secondary from global;
}
properties-header-name, properties-header-value {
composes: theme-background-surface theme-text-on-surface from global;
}
`,
css`
properties {
display: flex;
flex: 1;
flex-direction: column;
}
properties-header {
box-sizing: border-box;
display: inline-flex;
padding: 5px 1px;
position: sticky;
z-index: 1;
top: 0;
}
properties-header-name, properties-header-value {
composes: theme-typography--caption from global;
text-transform: uppercase;
box-sizing: border-box;
flex: 1;
padding: 4px 36px;
margin: 0px 1px;
}
properties-header-name {
flex: 0 0 auto;
width: 300px;
}
properties-header-right {
flex: 0 0 auto;
margin: 0px 1px;
}
const styles = css`
properties {
display: flex;
flex: 1;
flex-direction: column;
}
`;
properties-list {
flex: 1;
display: flex;
flex-direction: column;
padding-bottom: 24px;
}
`
);
type DriverPropertyState = {
[key: string]: string;
}
type DriverPropertiesProps = {
driver: DBDriver;
properties: DriverPropertyState;
state: DriverPropertyState;
isSelected: boolean;
}
export const DriverProperties = observer(function DriverProperties({
driver,
properties,
state,
isSelected,
}: DriverPropertiesProps) {
const translate = useTranslate();
const controller = useController(DriverPropertiesController, driver, properties);
const controller = useController(DriverPropertiesController, driver);
useEffect(() => {
if (isSelected) {
@@ -90,32 +50,13 @@ export const DriverProperties = observer(function DriverProperties({
return styled(useStyles(styles))(
<properties as="div">
<properties-header as="div">
<properties-header-name as="div">
{translate('customConnection_properties_name')}
</properties-header-name>
<properties-header-value as="div">
{translate('customConnection_properties_value')}
</properties-header-value>
<properties-header-right as="div">
<Button type='button' mod={['outlined']} onClick={controller.onAddProperty}>{translate('customConnection_properties_add')}</Button>
</properties-header-right>
</properties-header>
{controller.isLoading && <Loader />}
{!controller.isLoading && (
<properties-list as="div">
{controller.driverProperties.map(property => (
<DriverProperty
key={property.staticId}
property={property}
value={controller.state[property.id]}
onNameChange={controller.onNameChange}
onValueChange={controller.onValueChange}
onRemove={controller.onRemove}
error={controller.getPropertyIdCount(property.id) > 1}
/>
))}
</properties-list>
<PropertiesTable
properties={controller.driverProperties}
propertiesState={state}
onAdd={controller.onAddProperty}
/>
)}
</properties>
);
@@ -6,9 +6,10 @@
* you may not use this file except in compliance with the License.
*/
import { observable, action } from 'mobx';
import { observable } from 'mobx';
import { DBDriver } from '@dbeaver/core/app';
import { IProperty } from '@dbeaver/core/blocks';
import { injectable, IInitializableController } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { DriverPropertyInfo } from '@dbeaver/core/sdk';
@@ -32,70 +33,25 @@ export class DriverPropertiesController implements IInitializableController {
@observable driver!: DBDriver
@observable hasDetails = false
@observable responseMessage: string | null = null
@observable driverProperties = observable<DriverPropertyInfoWithStaticId>([])
@observable state: DriverPropertyState = {}
@observable driverProperties = observable<IProperty>([])
private loaded = false;
constructor(private customConnectionService: CustomConnectionService,
private notificationService: NotificationService) { }
init(driver: DBDriver, state: DriverPropertyState) {
init(driver: DBDriver) {
this.driver = driver;
this.state = state;
}
getPropertyIdCount(propertyId: string): number {
return this.driverProperties.filter(property => property.id === propertyId).length;
}
onAddProperty = () => {
this.driverProperties.unshift({
staticId: uuid(),
id: 'property',
id: uuid(),
name: 'property',
defaultValue: '',
});
}
onValueChange = (staticId: string, value: string) => {
const property = this.driverProperties.find(property => property.staticId === staticId);
if (!property) {
return;
}
this.state[property.id] = value;
}
@action
onNameChange = (staticId: string, newId: string) => {
const property = this.driverProperties.find(property => property.staticId === staticId);
if (!property) {
return;
}
if (this.state[property.id] !== undefined) {
this.state[newId] = this.state[property.id];
delete this.state[property.id];
}
property.id = newId;
}
@action
onRemove = (staticId: string) => {
const property = this.driverProperties.find(property => property.staticId === staticId);
if (!property) {
return;
}
if (this.state[property.id] !== undefined) {
delete this.state[property.id];
}
this.driverProperties.remove(property);
}
async loadDriverProperties() {
if (this.isLoading || this.loaded) {
return;
@@ -103,7 +59,13 @@ export class DriverPropertiesController implements IInitializableController {
this.isLoading = true;
try {
const driverProperties = await this.customConnectionService.loadDriverProperties(this.driver.id);
this.driverProperties = observable(driverProperties.map(property => ({ ...property, staticId: property.id })));
this.driverProperties = observable(driverProperties.map(property => ({
id: property.displayName!,
name: property.displayName!,
defaultValue: property.defaultValue,
description: property.description,
validValues: property.validValues,
})));
this.loaded = true;
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t load driver properties');
@@ -7,60 +7,9 @@
*/
import { observer } from 'mobx-react';
import styled, { css } from 'reshadow';
import { useCallback } from 'react';
import { StaticImage } from '@dbeaver/core/blocks';
import { useStyles, composes } from '@dbeaver/core/theming';
const styles = composes(
css`
db-driver-name {
composes: theme-border-color-secondary from global;
}
`,
css`
StaticImage {
box-sizing: border-box;
width: 24px;
}
db-driver {
position: relative;
cursor: pointer;
display: flex;
box-sizing: border-box;
align-items: center;
padding: 8px 12px;
}
db-driver-icon {
display: flex;
align-items: center;
box-sizing: border-box;
padding: 0 12px;
}
db-driver-name {
composes: theme-typography--body1 from global;
box-sizing: border-box;
font-weight: 500;
min-width: 250px;
padding: 0 24px 0 12px;
border-right: 1px solid;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
db-driver-description {
composes: theme-typography--body2 from global;
box-sizing: border-box;
max-width: 460px;
padding: 0 12px 0 24px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`
);
import { ListItem } from '@dbeaver/core/blocks';
export interface IDriver {
id: string;
@@ -75,12 +24,7 @@ type DriverProps = {
}
export const Driver = observer(function Driver({ driver, onSelect }: DriverProps) {
const select = useCallback(() => onSelect(driver.id), [driver]);
return styled(useStyles(styles))(
<db-driver as="div" onClick={() => onSelect(driver.id)}>
<db-driver-icon as="div"><StaticImage icon={driver.icon}/></db-driver-icon>
<db-driver-name as="div">{driver.name}</db-driver-name>
<db-driver-description as="div" title={driver.description}>{driver.description}</db-driver-description>
</db-driver>
);
return <ListItem icon={driver.icon} name={driver.name} description={driver.description} onClick={select}/>;
});
@@ -8,52 +8,11 @@
import { observer } from 'mobx-react';
import { useState, useMemo } from 'react';
import styled, { css, use } from 'reshadow';
import { StaticImage } from '@dbeaver/core/blocks';
import { useTranslate } from '@dbeaver/core/localization';
import { useStyles, composes } from '@dbeaver/core/theming';
import { ItemListSearch, ItemList } from '@dbeaver/core/blocks';
import { Driver, IDriver } from './Driver';
const styles = composes(
css`
list-item {
composes: theme-ripple theme-background-surface theme-border-color-secondary from global;
}
list-search {
composes: theme-background-secondary theme-text-on-secondary from global;
}
`,
css`
list {
box-sizing: border-box;
border-collapse: collapse;
z-index: 0;
}
list-item {
border-bottom: 1px solid;
}
list-search {
position: sticky;
top: 0;
padding: 8px 24px;
z-index: 1;
& input {
padding: 4px 24px;
padding-left: 32px;
}
& StaticImage {
position: absolute;
top: 15px;
left: 32px;
}
}
`
);
type DriverSelectorProps = {
drivers: IDriver[];
className?: string;
@@ -62,7 +21,6 @@ type DriverSelectorProps = {
export const DriverSelector = observer(function DriverSelector({ drivers, className, onSelect }: DriverSelectorProps) {
const [search, setSearch] = useState('');
const translate = useTranslate();
const filteredDrivers = useMemo(() => {
if (!search) {
return drivers;
@@ -70,22 +28,10 @@ export const DriverSelector = observer(function DriverSelector({ drivers, classN
return drivers.filter(driver => driver.name?.toUpperCase().includes(search.toUpperCase()));
}, [search, drivers]);
return styled(useStyles(styles))(
<list as="div" className={className}>
<list-search as="div">
<StaticImage icon='/icons/search.svg' />
<input
name='search'
placeholder={translate('ui_search')}
onChange={event => setSearch(event.target.value)}
{...use({ mod: 'surface' })}
/>
</list-search>
{filteredDrivers.map(driver => (
<list-item as="div" key={driver.id}>
<Driver driver={driver} onSelect={onSelect}/>
</list-item>
))}
</list>
return (
<ItemList className={className}>
<ItemListSearch onSearch={setSearch} />
{filteredDrivers.map(driver => <Driver key={driver.id} driver={driver} onSelect={onSelect}/>)}
</ItemList>
);
});
@@ -0,0 +1,18 @@
# dependencies
/node_modules
# testing
/coverage
# production
/dist
# misc
.DS_Store
.env*
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
@@ -0,0 +1,25 @@
{
"name": "@dbeaver/data-export-plugin",
"version": "0.1.0",
"description": "",
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/types",
"scripts": {
"build-plugin": "npm run clean-dist && webpack -p --config=../../configs/webpack/build-plugin.js",
"clean-dist": "rimraf dist/*",
"rollup": "npm run clean-dist && rollup --config ../../configs/rollup.config.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"peerDependencies": {
"@dbeaver/data-viewer-plugin": "^0.1.0",
"@dbeaver/core": "^0.1.0",
"mobx": "^5.15.4",
"mobx-react": "^6.2.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"reakit": "^1.0.0-beta.11",
"reshadow": "0.0.1-alpha.75",
"rxjs": "^6.5.5"
}
}
@@ -0,0 +1,21 @@
/*
* 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 { injectable } from '@dbeaver/core/di';
import { DataExportMenuService } from './DataExportMenuService';
@injectable()
export class Bootstrap {
constructor(private dataExportMenuService: DataExportMenuService) { }
bootstrap() {
this.dataExportMenuService.register();
}
}
@@ -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 { injectable } from '@dbeaver/core/di';
import { IContextMenuItem, IMenuContext, CommonDialogService } from '@dbeaver/core/dialogs';
import { TableFooterMenuService, TableViewerModel } from '@dbeaver/data-viewer-plugin';
import { DataExportDialog } from './Dialog/DataExportDialog';
@injectable()
export class DataExportMenuService {
constructor(
private commonDialogService: CommonDialogService,
private tableFooterMenuService: TableFooterMenuService,
) { }
register() {
const exportData: IContextMenuItem<TableViewerModel> = {
id: 'export ',
isPresent(context) {
return context.contextType === TableFooterMenuService.nodeContextType;
},
order: 1,
title: 'Export',
icon: 'export',
onClick: this.exportData.bind(this),
// isPanel: true,
};
this.tableFooterMenuService.registerMenuItem(exportData);
}
private exportData(context: IMenuContext<TableViewerModel>) {
console.log({
connectionId: context.data.connectionId,
contextId: context.data.executionContext?.contextId,
containerNodePath: context.data.containerNodePath,
resultId: context.data.resultId,
sourceName: context.data.sourceName,
});
this.commonDialogService.open(DataExportDialog, {
connectionId: context.data.connectionId,
contextId: context.data.executionContext?.contextId,
containerNodePath: context.data.containerNodePath,
resultId: context.data.resultId,
sourceName: context.data.sourceName,
});
}
}
@@ -0,0 +1,110 @@
/*
* 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 { injectable } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import {
CachedResource, GraphQLService, DataTransferProcessorInfo, DataTransferParameters
} from '@dbeaver/core/sdk';
import { Deferred } from '@dbeaver/core/utils';
import { ExportFromContainerProcess } from './ExportFromContainerProcess';
import { ExportFromResultsProcess } from './ExportFromResultsProcess';
@injectable()
export class DataExportService {
readonly processors = new CachedResource(new Map(), this.refreshProcessorsAsync.bind(this));
private exportProcesses = new Map<string, Deferred<string>>();
constructor(
private graphQLService: GraphQLService,
private notificationService: NotificationService
) { }
async cancel(exportId: string) {
const process = this.exportProcesses.get(exportId);
if (!process) {
return;
}
process.cancel();
}
async delete(exportId: string) {
const process = this.exportProcesses.get(exportId);
if (!process) {
return;
}
const dataFileId = process.getPayload();
if (!dataFileId) {
return;
}
try {
await this.graphQLService.gql.removeDataTransferFile({ dataFileId });
this.exportProcesses.delete(exportId);
} catch (e) {
}
}
download(exportId: string) {
const process = this.exportProcesses.get(exportId);
if (!process) {
return;
}
const dataFileId = process.getPayload();
if (!dataFileId) {
return;
}
this.exportProcesses.delete(exportId);
return `/dbeaver/data/${dataFileId}`;
}
async exportFromContainer(
connectionId: string,
containerNodePath: string,
parameters: DataTransferParameters
): Promise<string | undefined> {
const process = new ExportFromContainerProcess(this.graphQLService, this.notificationService);
const taskId = await process.start(connectionId, containerNodePath, parameters);
if (taskId) {
this.exportProcesses.set(taskId, process);
await process.promise;
window.open(this.download(taskId), '_blank');
return taskId;
}
}
async exportFromResults(
connectionId: string,
contextId: string,
resultsId: string,
parameters: DataTransferParameters
): Promise<string | undefined> {
const process = new ExportFromResultsProcess(this.graphQLService, this.notificationService);
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');
return taskId;
}
}
private async refreshProcessorsAsync(
data: Map<string, DataTransferProcessorInfo>
): Promise<Map<string, DataTransferProcessorInfo>> {
const { processors } = await this.graphQLService.gql.getDataTransferProcessors();
data.clear();
for (const processor of processors) {
data.set(processor.id, processor);
}
return data;
}
}
@@ -0,0 +1,117 @@
/*
* 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 { observable, computed } from 'mobx';
import { IProperty } from '@dbeaver/core/blocks';
import { injectable, IInitializableController } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { DataTransferProcessorInfo } from '@dbeaver/core/sdk';
import { DataExportService } from '../DataExportService';
import { IExportContext } from '../IExportContext';
export enum DataExportStep {
DataTransferProcessor,
Configure
}
@injectable()
export class DataExportController implements IInitializableController {
@observable step = DataExportStep.DataTransferProcessor
get isLoading() {
return !this.dataExportService.processors.isLoaded();
}
@observable processor: DataTransferProcessorInfo | null = null
@computed get processors(): DataTransferProcessorInfo[] {
return Array
.from(
this.dataExportService.processors.data.values()
)
.sort((a, b) => this.sortProcessors(a, b));
}
@observable readonly processorProperties: any = {}
@observable properties: IProperty[] = []
private context!: IExportContext;
constructor(
private dataExportService: DataExportService,
private notificationService: NotificationService
) { }
init(context: IExportContext) {
this.context = context;
this.loadProcessors();
}
export = () => {
if (!this.processor) {
return;
}
if (this.context.containerNodePath) {
this.dataExportService.exportFromContainer(
this.context.connectionId,
this.context.containerNodePath,
{
processorId: this.processor.id,
processorProperties: this.processorProperties,
}
);
} else if (this.context.contextId && this.context.resultId) {
this.dataExportService.exportFromResults(
this.context.connectionId,
this.context.contextId,
this.context.resultId,
{
processorId: this.processor.id,
processorProperties: this.processorProperties,
}
);
}
}
setStep = (step: DataExportStep) => {
this.step = step;
}
selectProcessor = (processorId: string) => {
this.processor = this.dataExportService
.processors
.data
.get(processorId)!;
this.properties = this.processor.properties?.map(property => ({
id: property.displayName!,
name: property.displayName!,
description: property.description,
validValues: property.validValues,
})) || [];
this.step = DataExportStep.Configure;
}
private async loadProcessors() {
try {
await this.dataExportService.processors.load();
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t load data export processors');
}
}
private sortProcessors(processorA: DataTransferProcessorInfo, processorB: DataTransferProcessorInfo): number {
if (processorA.order === processorB.order)
{
return (processorA.name || '').localeCompare((processorB.name || ''));
}
return (processorB.order || 0) - (processorA.order || 0);
}
}
@@ -0,0 +1,47 @@
/*
* 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 { useController } from '@dbeaver/core/di';
import { DialogComponent } from '@dbeaver/core/dialogs';
import { IExportContext } from '../IExportContext';
import { DataExportController, DataExportStep } from './DataExportController';
import { ProcessorConfigureDialog } from './ProcessorConfigureDialog';
import { ProcessorSelectDialog } from './ProcessorSelectDialog';
export const DataExportDialog: DialogComponent<IExportContext, null> = observer(
function DataExportDialog(props) {
const controller = useController(DataExportController, props.payload);
if (controller.step === DataExportStep.Configure && controller.processor) {
return (
<ProcessorConfigureDialog
processor={controller.processor}
properties={controller.properties}
processorProperties={controller.processorProperties}
isExporting={false}
onBack={() => controller.setStep(DataExportStep.DataTransferProcessor)}
onClose={props.rejectDialog}
onExport={controller.export}
/>
);
}
return (
<ProcessorSelectDialog
context={props.payload}
processors={controller.processors}
onSelect={controller.selectProcessor}
isLoading={controller.isLoading}
onClose={props.rejectDialog}
/>
);
}
);
@@ -0,0 +1,33 @@
/*
* 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 { DataTransferProcessorInfo } from '@dbeaver/core/sdk';
import { ItemList } from '@dbeaver/core/src/blocks';
import { ProcessorItem } from './ProcessorItem';
type ExportProcessorListProps = {
processors: DataTransferProcessorInfo[];
onSelect(processorId: string): void;
className?: string;
}
export const ExportProcessorList = observer(function ExportProcessorList({
processors,
onSelect,
className,
}: ExportProcessorListProps) {
return (
<ItemList className={className}>
{processors.map(processor => <ProcessorItem key={processor.id} processor={processor} onSelect={onSelect}/>)}
</ItemList>
);
});
@@ -0,0 +1,26 @@
/*
* 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 '@dbeaver/core/blocks';
import { DataTransferProcessorInfo } from '@dbeaver/core/sdk';
type ProcessorItemProps = {
processor: DataTransferProcessorInfo;
onSelect(processorId: string): void;
}
export const ProcessorItem = observer(function ProcessorItem({
processor,
onSelect,
}: ProcessorItemProps) {
const select = useCallback(() => onSelect(processor.id), [processor]);
return <ListItem icon={processor.icon} name={processor.name} description={processor.description} onClick={select}/>;
});
@@ -0,0 +1,75 @@
/*
* 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 { Loader, IProperty, PropertiesTable } from '@dbeaver/core/blocks';
import { CommonDialogWrapper } from '@dbeaver/core/dialogs';
import { useTranslate } from '@dbeaver/core/localization';
import { DataTransferProcessorInfo } from '@dbeaver/core/sdk';
import { ProcessorConfigureDialogFooter } from './ProcessorConfigureDialogFooter';
const styles = css`
CommonDialogWrapper {
display: flex;
min-height: 330px;
}
PropertiesTable {
flex: 1;
}
`;
type ProcessorSelectDialogProps = {
processor: DataTransferProcessorInfo;
properties: IProperty[];
processorProperties: any;
isExporting: boolean;
onClose(): void;
onBack(): void;
onExport(): void;
}
export const ProcessorConfigureDialog = observer(
function ProcessorConfigureDialog({
processor,
properties,
processorProperties,
isExporting,
onClose,
onBack,
onExport,
}: ProcessorSelectDialogProps) {
const title = `${useTranslate('data_transfer_dialog_configuration_title')} (${processor.name})`;
return styled(styles)(
<CommonDialogWrapper
title={title}
noBodyPadding
footer={
<ProcessorConfigureDialogFooter
isExporting={isExporting}
onExport={onExport}
onBack={onBack}
onCancel={onClose}
/>
}
onReject={onClose}
>
{isExporting && <Loader />}
{!isExporting && (
<PropertiesTable
properties={properties}
propertiesState={processorProperties}
/>
)}
</CommonDialogWrapper>
);
}
);
@@ -0,0 +1,77 @@
/*
* 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 } from '@dbeaver/core/blocks';
import { useTranslate } from '@dbeaver/core/localization';
const styles = css`
controls {
display: flex;
height: 100%;
align-items: center;
}
fill {
flex: 1;
}
Button:not(:first-child) {
margin-left: 24px;
}
`;
type Props = {
isExporting: boolean;
onCancel(): void;
onExport(): void;
onBack(): void;
}
export const ProcessorConfigureDialogFooter = observer(
function ProcessorConfigureDialogFooter({
isExporting,
onCancel,
onExport,
onBack,
}: Props) {
const translate = useTranslate();
return styled(styles)(
<controls as="div">
<Button
type="button"
mod={['outlined']}
onClick={onBack}
disabled={isExporting}
>
{translate('ui_stepper_back')}
</Button>
<fill as="div"/>
<Button
type="button"
mod={['outlined']}
onClick={onCancel}
disabled={isExporting}
>
{translate('ui_processing_cancel')}
</Button>
<Button
type="button"
mod={['unelevated']}
onClick={onExport}
disabled={isExporting}
>
{translate('data_transfer_dialog_export')}
</Button>
</controls>
);
}
);
@@ -0,0 +1,67 @@
/*
* 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 { Loader } from '@dbeaver/core/blocks';
import { CommonDialogWrapper } from '@dbeaver/core/dialogs';
import { useTranslate } from '@dbeaver/core/localization';
import { DataTransferProcessorInfo } from '@dbeaver/core/sdk';
import { useNode } from '@dbeaver/core/src/app';
import { IExportContext } from '../IExportContext';
import { ExportProcessorList } from './ExportProcessorList/ExportProcessorList';
const styles = css`
CommonDialogWrapper {
display: flex;
flex-direction: column;
min-height: 330px;
}
ExportProcessorList {
flex: 1;
}
export-object {
composes: theme-typography--body2 from global;
padding: 16px;
}
`;
type ProcessorSelectDialogProps = {
context: IExportContext;
processors: DataTransferProcessorInfo[];
isLoading: boolean;
onSelect(processorId: string): void;
onClose(): void;
}
export const ProcessorSelectDialog = observer(
function ProcessorSelectDialog({
context,
processors,
isLoading,
onSelect,
onClose,
}: ProcessorSelectDialogProps) {
const translate = useTranslate();
const node = useNode(context.containerNodePath || '');
return styled(styles)(
<CommonDialogWrapper
title={translate('data_transfer_dialog_title')}
noBodyPadding
onReject={onClose}
>
<export-object as="div">{translate('data_transfer_dialog_exporting_object')} {context.sourceName || node?.name}</export-object>
{isLoading && <Loader />}
{!isLoading && <ExportProcessorList processors={processors} onSelect={onSelect}/>}
</CommonDialogWrapper>
);
}
);
@@ -0,0 +1,143 @@
/*
* 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 { NotificationService } from '@dbeaver/core/eventsLog';
import {
AsyncTaskInfo, GraphQLService, ServerInternalError, DataTransferParameters,
} from '@dbeaver/core/sdk';
import {
CancellablePromise, cancellableTimeout, Deferred, EDeferredState,
} from '@dbeaver/core/utils';
const DELAY_BETWEEN_TRIES = 1000;
// TODO: seems we need special abstraction over async tasks to manage it
export class ExportFromContainerProcess extends Deferred<string> {
private taskId?: string;
private timeout?: CancellablePromise<void>;
private isCancelConfirmed = false; // true when server successfully executed cancelQueryAsync
constructor(private graphQLService: GraphQLService,
private notificationService: NotificationService) {
super();
}
async start(
connectionId: string,
containerNodePath: string,
parameters: DataTransferParameters
): Promise<string | undefined> {
// start async task
try {
const { taskInfo } = await this.graphQLService.gql.exportDataFromContainer({
connectionId,
containerNodePath,
parameters,
});
this.applyResult(taskInfo);
this.taskId = taskInfo.id;
if (this.getState() === EDeferredState.CANCELLING) {
await this.cancelAsync(this.taskId);
}
} catch (e) {
this.onError(e);
return;
}
this.statusUpdateProcess();
return this.taskId;
}
/**
* this method just mark process as cancelling
* to avoid racing conditions the server request will be executed in synchronous manner in start method
*/
cancel() {
if (this.getState() !== EDeferredState.PENDING) {
return;
}
this.toCancelling();
if (this.timeout) {
this.timeout.cancel();
}
}
private async statusUpdateProcess() {
if (this.isFinished || !this.taskId) {
return;
}
// check async task status until execution finished
while (this.isInProgress) {
if (this.getState() === EDeferredState.CANCELLING) {
await this.cancelAsync(this.taskId);
}
// run the first check immediately because usually the query execution is fast
try {
const { taskInfo } = await this.graphQLService.gql.asyncTaskStatus({ taskId: this.taskId });
this.applyResult(taskInfo);
if (this.isFinished) {
return;
}
} catch (e) {
this.notificationService.logException(e, 'Failed to check async task status');
}
try {
this.timeout = cancellableTimeout(DELAY_BETWEEN_TRIES);
await this.timeout;
} catch { }
}
}
private async cancelAsync(taskId: string) {
if (this.isCancelConfirmed) {
return;
}
try {
await this.graphQLService.gql.asyncTaskCancel({ taskId });
this.isCancelConfirmed = true;
} catch (e) {
if (this.getState() === EDeferredState.CANCELLING) {
this.toPending();
this.notificationService.logException(e, 'Failed to cancel async task');
}
}
}
private applyResult(taskInfo: AsyncTaskInfo): void {
// task is running
if (taskInfo.running) {
return;
}
// task failed to execute
if (taskInfo.error) {
const serverError = new ServerInternalError(taskInfo.error);
this.onError(serverError, taskInfo.status);
return;
}
if (!taskInfo.taskResult) {
this.onError(new Error('Tasks execution returns no taskResult'), taskInfo.status);
return;
}
// task execution successful
this.toResolved(taskInfo.taskResult);
}
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 export has been canceled${status ? `: ${status}` : ''}`;
this.notificationService.logException(error, message);
} else {
this.toRejected(error);
}
}
}
@@ -0,0 +1,145 @@
/*
* 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 { NotificationService } from '@dbeaver/core/eventsLog';
import {
AsyncTaskInfo, GraphQLService, ServerInternalError, DataTransferParameters,
} from '@dbeaver/core/sdk';
import {
CancellablePromise, cancellableTimeout, Deferred, EDeferredState,
} from '@dbeaver/core/utils';
const DELAY_BETWEEN_TRIES = 1000;
// TODO: seems we need special abstraction over async tasks to manage it
export class ExportFromResultsProcess extends Deferred<string> {
private taskId?: string;
private timeout?: CancellablePromise<void>;
private isCancelConfirmed = false; // true when server successfully executed cancelQueryAsync
constructor(private graphQLService: GraphQLService,
private notificationService: NotificationService) {
super();
}
async start(
connectionId: string,
contextId: string,
resultsId: string,
parameters: DataTransferParameters
): Promise<string | undefined> {
// start async task
try {
const { taskInfo } = await this.graphQLService.gql.exportDataFromResults({
connectionId,
contextId,
resultsId,
parameters,
});
this.applyResult(taskInfo);
this.taskId = taskInfo.id;
if (this.getState() === EDeferredState.CANCELLING) {
await this.cancelAsync(this.taskId);
}
} catch (e) {
this.onError(e);
return;
}
this.statusUpdateProcess();
return this.taskId;
}
/**
* this method just mark process as cancelling
* to avoid racing conditions the server request will be executed in synchronous manner in start method
*/
cancel() {
if (this.getState() !== EDeferredState.PENDING) {
return;
}
this.toCancelling();
if (this.timeout) {
this.timeout.cancel();
}
}
private async statusUpdateProcess() {
if (this.isFinished || !this.taskId) {
return;
}
// check async task status until execution finished
while (this.isInProgress) {
if (this.getState() === EDeferredState.CANCELLING) {
await this.cancelAsync(this.taskId);
}
// run the first check immediately because usually the query execution is fast
try {
const { taskInfo } = await this.graphQLService.gql.asyncTaskStatus({ taskId: this.taskId });
this.applyResult(taskInfo);
if (this.isFinished) {
return;
}
} catch (e) {
this.notificationService.logException(e, 'Failed to check async task status');
}
try {
this.timeout = cancellableTimeout(DELAY_BETWEEN_TRIES);
await this.timeout;
} catch { }
}
}
private async cancelAsync(taskId: string) {
if (this.isCancelConfirmed) {
return;
}
try {
await this.graphQLService.gql.asyncTaskCancel({ taskId });
this.isCancelConfirmed = true;
} catch (e) {
if (this.getState() === EDeferredState.CANCELLING) {
this.toPending();
this.notificationService.logException(e, 'Failed to cancel async task');
}
}
}
private applyResult(taskInfo: AsyncTaskInfo): void {
// task is running
if (taskInfo.running) {
return;
}
// task failed to execute
if (taskInfo.error) {
const serverError = new ServerInternalError(taskInfo.error);
this.onError(serverError, taskInfo.status);
return;
}
if (!taskInfo.taskResult) {
this.onError(new Error('Tasks execution returns no taskResult'), taskInfo.status);
return;
}
// task execution successful
this.toResolved(taskInfo.taskResult);
}
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 export has been canceled${status ? `: ${status}` : ''}`;
this.notificationService.logException(error, message);
} else {
this.toRejected(error);
}
}
}
@@ -0,0 +1,15 @@
/*
* 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 interface IExportContext {
connectionId: string;
contextId?: string;
containerNodePath?: string;
resultId?: string | null;
sourceName?: string;
}
@@ -0,0 +1,6 @@
import { manifest } from './manifest';
export default manifest;
export * from './DataExportMenuService';
export * from './DataExportService';
@@ -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 { IServiceInjector, PluginManifest } from '@dbeaver/core/di';
import { Bootstrap } from './Bootstrap';
import { DataExportMenuService } from './DataExportMenuService';
import { DataExportService } from './DataExportService';
export const manifest: PluginManifest = {
info: {
name: 'Data Viewer Plugin',
},
providers: [
DataExportMenuService,
DataExportService,
],
async initialize(services: IServiceInjector) {
services
.resolveServiceByClass(Bootstrap)
.bootstrap();
},
};
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"declarationDir": "dist/types",
"declaration": true,
"noEmit": false,
"outDir": "dist/js"
},
"exclude": [
"node_modules",
"@dbeaver/**"
],
"include": ["src/**/*"]
}
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"@dbeaver/**"
]
}
@@ -14,5 +14,5 @@ export const DataViewerPanel = function DataViewerPanel({
tab,
}: ObjectPagePanelProps) {
return <TableViewer tableId={tab.handlerState.objectId}/>;
return <TableViewer tableId={tab.id}/>;
};
@@ -12,6 +12,7 @@ import {
IContextProvider,
ITab,
NavigationType,
NodeManagerUtils,
} from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
@@ -87,7 +88,23 @@ export class DataViewerTabService {
if (!this.nodesManagerService.isNodeHasData(objectInfo)) {
return;
}
this.dataViewerTableService.createTableModelIfNotExists(tab.handlerState.objectId);
if (this.dataViewerTableService.has(tab.id)) {
return;
}
const nodeInfo = this.nodesManagerService
.getNodeContainerInfo(tab.handlerState.objectId);
if (!nodeInfo.connectionId) {
return;
}
this.dataViewerTableService.create(
tab.id,
NodeManagerUtils.connectionNodeIdToConnectionId(nodeInfo.connectionId),
tab.handlerState.objectId
);
}
private async handleTabRestore(tab: ITab<IObjectViewerTabState>) {
@@ -6,73 +6,50 @@
* you may not use this file except in compliance with the License.
*/
import { ConnectionsManagerService } from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { CommonDialogService } from '@dbeaver/core/dialogs';
import { GraphQLService } from '@dbeaver/core/sdk';
import { IExecutionContext } from './IExecutionContext';
import { RowDiff } from './TableViewer/TableDataModel/EditedRow';
import { IRequestDataResult, ITableViewerModelInit, TableViewerModel } from './TableViewer/TableViewerModel';
import { IRequestDataResult, TableViewerModel } from './TableViewer/TableViewerModel';
import { TableViewerStorageService } from './TableViewer/TableViewerStorageService';
interface ISqlContextParams {
contextId: string;
connectionId: string;
objectCatalogId?: string;
objectSchemaId?: string;
}
export interface IDataViewerTableModel {
tableId: string;
connectionId: string;
resultId: string | null;
sqlContextParams: ISqlContextParams | null;
}
@injectable()
export class DataViewerTableService {
constructor(private tableViewerStorageService: TableViewerStorageService,
private connectionsManagerService: ConnectionsManagerService,
private commonDialogService: CommonDialogService,
private graphQLService: GraphQLService) {
}
createTableModelIfNotExists(tableId: string): void {
if (!this.tableViewerStorageService.hasTableModel(tableId)) {
const tableModel = this.createTableModel(tableId);
this.tableViewerStorageService.addTableModel(tableId, tableModel);
}
has(tableId: string) {
return this.tableViewerStorageService.has(tableId);
}
removeTableModel(tableId: string): void {
this.tableViewerStorageService.removeTableModel(tableId);
this.tableViewerStorageService.remove(tableId);
}
private createTableModel(tableId: string): TableViewerModel {
// todo delete this dirty trick. pass connection id with tableId
const connection = this.connectionsManagerService.connections.find(c => tableId.includes(c.id));
const tableModel: IDataViewerTableModel = {
tableId,
connectionId: connection!.id,
sqlContextParams: null, // will be filled before fist data fetch
resultId: null, // will be filled after fist data fetch
};
const callbacks: ITableViewerModelInit = {
requestDataAsync: (rowOffset, count: number) => this.requestDataAsync(tableModel, rowOffset, count),
saveChanges: diffs => this.saveChanges(tableModel, diffs),
};
return new TableViewerModel(callbacks, this.commonDialogService);
create(
tabId: string,
connectionId: string,
containerNodePath?: string,
): TableViewerModel {
return this.tableViewerStorageService.create(
{
tableId: tabId,
connectionId,
containerNodePath,
requestDataAsync: this.requestDataAsync.bind(this),
saveChanges: this.saveChanges.bind(this),
}
);
}
private async createSqlContext(
private async createExecutionContext(
connectionId: string,
defaultCatalog?: string,
defaultSchema?: string
): Promise<ISqlContextParams> {
): Promise<IExecutionContext> {
const response = await this.graphQLService.gql.sqlContextCreate({
connectionId,
@@ -87,20 +64,20 @@ export class DataViewerTableService {
};
}
private async saveChanges(data: IDataViewerTableModel, rows: RowDiff[]): Promise<IRequestDataResult> {
private async saveChanges(data: TableViewerModel, rows: RowDiff[]): Promise<IRequestDataResult> {
const firstRow = rows[0]; // we support updating only one value
if (!data.resultId) {
throw new Error('It is expected that resultId was set after first fetch');
}
if (!data.sqlContextParams) {
if (!data.executionContext) {
throw new Error('It is expected that data.sqlContextParams was set after first fetch');
}
const response = await this.graphQLService.gql.updateResultsData({
connectionId: data.sqlContextParams.connectionId,
contextId: data.sqlContextParams.contextId,
connectionId: data.executionContext.connectionId,
contextId: data.executionContext.contextId,
resultsId: data.resultId,
sourceRowValues: firstRow.source,
values: firstRow.values,
@@ -118,20 +95,25 @@ export class DataViewerTableService {
return result;
}
private async requestDataAsync(data: IDataViewerTableModel,
rowOffset: number,
count: number): Promise<IRequestDataResult> {
if (!data.sqlContextParams) {
private async requestDataAsync(
data: TableViewerModel,
rowOffset: number,
count: number
): Promise<IRequestDataResult> {
if (!data.containerNodePath) {
throw new Error('containerNodePath must be provided for table');
}
if (!data.executionContext) {
// it is first data request
const sqlContextParams: ISqlContextParams = await this.createSqlContext(data.connectionId);
data.sqlContextParams = sqlContextParams;
const executionContext: IExecutionContext = await this.createExecutionContext(data.connectionId);
data.executionContext = executionContext;
}
const { readDataFromContainer } = await this.graphQLService.gql.readDataFromContainer({
connectionId: data.sqlContextParams.connectionId,
contextId: data.sqlContextParams.contextId,
containerNodePath: data.tableId,
connectionId: data.executionContext.connectionId,
contextId: data.executionContext.contextId,
containerNodePath: data.containerNodePath,
filter: {
offset: rowOffset,
limit: count,
@@ -0,0 +1,14 @@
/*
* 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 interface IExecutionContext {
connectionId: string;
contextId: string;
objectCatalogId?: string;
objectSchemaId?: string;
}
@@ -6,19 +6,23 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react';
import { useCallback } from 'react';
import styled, { css, use } from 'reshadow';
import { Icon } from '@dbeaver/core/blocks';
import { composes, useStyles } from '@dbeaver/core/theming';
import { TableViewerModel } from '../TableViewerModel';
import { TableFooterMenu } from './TableFooterMenu/TableFooterMenu';
const tableFooterStyles = composes(
css`
table-footer {
composes: theme-background-secondary theme-text-on-secondary from global;
}
button {
composes: theme-text-primary from global;
composes: theme-text-primary theme-ripple from global;
}
`,
css`
@@ -51,53 +55,51 @@ const tableFooterStyles = composes(
font-size: 13px;
line-height: 24px;
}
time placeholder {
height: 16px;
width: 150px;
reload,
reload button {
height: 100%;
}
reload,
count,
time {
TableFooterMenu {
margin-left: 16px;
}
time {
composes: theme-typography--caption from global;
margin-left: auto;
margin-right: 16px;
}
`
);
type TableFooterProps = {
chunkSize: number;
requestStatusMessage: string;
queryDuration: number;
onRefresh: () => void;
onDataChange: (value: number) => void;
model: TableViewerModel;
}
export function TableFooter({
chunkSize,
requestStatusMessage,
queryDuration,
onRefresh,
onDataChange,
export const TableFooter = observer(function TableFooter({
model,
}: TableFooterProps) {
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => onDataChange(parseInt(e.target.value, 10)),
[onDataChange]
(e: React.ChangeEvent<HTMLInputElement>) => model.setChunkSize(parseInt(e.target.value, 10)),
[model]
);
return styled(useStyles(tableFooterStyles))(
<table-footer as="div">
<reload as="div">
<button type="button" onClick={onRefresh}>
<button type="button" onClick={model.handleRefresh}>
<Icon name="reload" />
</button>
</reload>
<count as="div">
<input type="number" value={chunkSize} onBlur={handleChange} {...use({ mod: 'surface' })} />
<input type="number" value={model.getChunkSize()} onBlur={handleChange} {...use({ mod: 'surface' })} />
</count>
{requestStatusMessage.length > 0 && (
<TableFooterMenu model={model}/>
{model.requestStatusMessage.length > 0 && (
<time>
{requestStatusMessage} - {queryDuration}ms
{model.requestStatusMessage} - {model.queryDuration}ms
</time>
)}
</table-footer>
);
}
});
@@ -0,0 +1,44 @@
/*
* 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 { useService } from '@dbeaver/core/di';
import { TableViewerModel } from '../../TableViewerModel';
import { TableFooterMenuItem } from './TableFooterMenuItem';
import { TableFooterMenuService } from './TableFooterMenuService';
const styles = css`
menu-wrapper {
display: flex;
height: 100%;
}
TableFooterMenuItem {
text-transform: uppercase;
font-weight: 700;
}
`;
type TableFooterMenuProps = {
model: TableViewerModel;
className?: string;
}
export const TableFooterMenu = observer(function TableFooterMenu({ model, className }: TableFooterMenuProps) {
const mainMenuService = useService(TableFooterMenuService);
return styled(styles)(
<menu-wrapper as="div" className={className}>
{mainMenuService.constructMenuWithContext(model).map((topItem, i) => (
<TableFooterMenuItem key={i} menuItem={topItem}/>
))}
</menu-wrapper>
);
});
@@ -0,0 +1,95 @@
/*
* 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 { ButtonHTMLAttributes } from 'react';
import { Button } from 'reakit';
import styled, { css } from 'reshadow';
import { IconOrImage } from '@dbeaver/core/blocks';
import { IMenuItem, MenuTrigger } from '@dbeaver/core/dialogs';
import { useTranslate } from '@dbeaver/core/localization';
import { composes, useStyles } from '@dbeaver/core/theming';
type TableFooterMenuItemProps = ButtonHTMLAttributes<any> & {
menuItem: IMenuItem;
};
export const tableFooterMenuStyles = composes(
css`
Menu {
composes: theme-text-on-surface from global;
}
Button {
composes: theme-text-on-secondary from global;
}
MenuTrigger, Button {
composes: theme-ripple from global;
}
`,
css`
MenuTrigger, Button {
height: 100%;
padding: 0 8px !important;
display: flex;
align-items: center;
cursor: pointer;
}
Button {
outline: none;
}
menu-trigger-icon IconOrImage {
display: block;
width: 24px;
}
menu-trigger-title {
display: block;
padding-left: 8px;
}
`
);
export const TableFooterMenuItem = observer(function TableFooterMenuItem({
menuItem,
...props
}: TableFooterMenuItemProps) {
const translate = useTranslate();
if (!menuItem.panel) {
return styled(useStyles(tableFooterMenuStyles))(
<Button
as="button" {...props}
disabled={menuItem.isDisabled}
onClick={() => menuItem.onClick && menuItem.onClick()}>
{menuItem.icon && (
<menu-trigger-icon as="div">
<IconOrImage icon={menuItem.icon} viewBox="0 0 32 32" />
</menu-trigger-icon>
)}
{menuItem.title && <menu-trigger-title as="div">{translate(menuItem.title)}</menu-trigger-title>}
</Button>
);
}
return styled(useStyles(tableFooterMenuStyles))(
<MenuTrigger
{...props}
panel={menuItem.panel}
disabled={menuItem.isDisabled}
style={[tableFooterMenuStyles]}
>
{menuItem.icon && (
<menu-trigger-icon as="div">
<IconOrImage icon={menuItem.icon} viewBox="0 0 32 32" />
</menu-trigger-icon>
)}
{menuItem.title && <menu-trigger-title as="div">{translate(menuItem.title)}</menu-trigger-title>}
</MenuTrigger>
);
});
@@ -0,0 +1,39 @@
/*
* 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 { injectable } from '@dbeaver/core/di';
import {
ContextMenuService, IMenuContext, IContextMenuItem, IMenuItem
} from '@dbeaver/core/dialogs';
import { TableViewerModel } from '../../TableViewerModel';
@injectable()
export class TableFooterMenuService {
static nodeContextType = 'NodeWithParent';
private tableFooterMenuToken = 'tableFooterMenu';
constructor(private contextMenuService: ContextMenuService) {
this.contextMenuService.addPanel(this.tableFooterMenuToken);
}
constructMenuWithContext(model: TableViewerModel): IMenuItem[] {
const context: IMenuContext<TableViewerModel> = {
menuId: this.tableFooterMenuToken,
contextId: model.tableId,
contextType: TableFooterMenuService.nodeContextType,
data: model,
};
return this.contextMenuService.createContextMenu(context, this.tableFooterMenuToken).menuItems;
}
registerMenuItem(options: IContextMenuItem<TableViewerModel>, panelId?: string): void {
this.contextMenuService.addMenuItem<TableViewerModel>(panelId || this.tableFooterMenuToken, options);
}
}
@@ -13,7 +13,7 @@ import styled, { css } from 'reshadow';
import { AgGridTable } from '@dbeaver/ag-grid-plugin';
import { Button } from '@dbeaver/core/blocks';
import { TableViewerModel } from '../TableViewerModel';
import { TableViewerModel } from './TableViewerModel';
type TableGridProps = PropsWithChildren<{
model: TableViewerModel;
@@ -14,7 +14,7 @@ import { Loader } from '@dbeaver/core/blocks';
import { useService } from '@dbeaver/core/di';
import { TableFooter } from './TableFooter/TableFooter';
import { TableGrid } from './TableGrid/TableGrid';
import { TableGrid } from './TableGrid';
import { TableViewerStorageService } from './TableViewerStorageService';
const viewerStyles = css`
@@ -37,7 +37,7 @@ export const TableViewer = observer(function TableViewer({
}: TableViewerProps) {
const tableViewerStorageService = useService(TableViewerStorageService);
const tableViewerModel = tableViewerStorageService.getTableModel(tableId);
const tableViewerModel = tableViewerStorageService.get(tableId);
if (!tableViewerModel) {
return <Loader />;
@@ -46,13 +46,7 @@ export const TableViewer = observer(function TableViewer({
return styled(viewerStyles)(
<table-viewer as="div" className={className}>
<TableGrid model={tableViewerModel} />
<TableFooter
chunkSize={tableViewerModel.getChunkSize()}
requestStatusMessage={tableViewerModel.requestStatusMessage}
queryDuration={tableViewerModel.queryDuration}
onRefresh={tableViewerModel.handleRefresh}
onDataChange={tableViewerModel.setChunkSize}
/>
<TableFooter model={tableViewerModel} />
<Loader loading={tableViewerModel.isLoaderVisible} overlay/>
</table-viewer>
);
@@ -12,7 +12,9 @@ import { IAgGridModel, IRequestedData } from '@dbeaver/ag-grid-plugin';
import { ErrorDetailsDialog } from '@dbeaver/core/app';
import { CommonDialogService } from '@dbeaver/core/dialogs';
import { GQLError } from '@dbeaver/core/sdk';
import { uuid } from '@dbeaver/core/utils';
import { IExecutionContext } from '../IExecutionContext';
import { ErrorDialog } from './ErrorDialog';
import { RowDiff } from './TableDataModel/EditedRow';
import { TableColumn } from './TableDataModel/TableColumn';
@@ -26,6 +28,19 @@ export const fetchingSettings = {
fetchDefault: 200,
};
export interface ITableViewerModelOptions {
tableId?: string;
connectionId: string;
containerNodePath?: string;
resultId?: string | null; // will be filled after fist data fetch
executionContext?: IExecutionContext | null; // will be filled before fist data fetch
sourceName?: string;
initialState?: IRequestDataResult;
noLoaderWhileRequestingDataAsync?: boolean;
requestDataAsync(model: TableViewerModel, rowOffset: number, count: number): Promise<IRequestDataResult>;
saveChanges(model: TableViewerModel, diffs: RowDiff[]): Promise<IRequestDataResult>;
}
export interface IRequestDataResult {
rows: TableRow[];
columns: TableColumn[];
@@ -34,14 +49,17 @@ export interface IRequestDataResult {
statusMessage: string;
}
export interface ITableViewerModelInit {
export class TableViewerModel implements ITableViewerModelOptions {
tableId: string;
connectionId: string;
containerNodePath?: string;
resultId: string | null;
executionContext: IExecutionContext | null;
sourceName?: string;
initialState?: IRequestDataResult;
requestDataAsync(rowOffset: number, count: number): Promise<IRequestDataResult>;
noLoaderWhileRequestingDataAsync?: boolean;
saveChanges(diffs: RowDiff[]): Promise<IRequestDataResult>;
}
export class TableViewerModel {
requestDataAsync: (model: TableViewerModel, rowOffset: number, count: number) => Promise<IRequestDataResult>;
saveChanges: (model: TableViewerModel, diffs: RowDiff[]) => Promise<IRequestDataResult>;
agGridModel: IAgGridModel = {
initialRows: [],
@@ -84,16 +102,21 @@ export class TableViewerModel {
private tableEditor = new TableEditor(this.tableDataModel);
constructor(
private init: ITableViewerModelInit,
options: ITableViewerModelOptions,
private commonDialogService: CommonDialogService
) {
if (init.initialState) {
this.insertRows(0, init.initialState.rows, !init.initialState.isFullyLoaded);
this.tableDataModel.overWrite(init.initialState.columns);
this.updateInfo(init.initialState.statusMessage, init.initialState.duration);
this.agGridModel.initialRows = this.tableDataModel.getRows();
this.agGridModel.initialColumns = this.tableDataModel.getColumns();
}
this.tableId = options.tableId || uuid();
this.connectionId = options.connectionId;
this.containerNodePath = options.containerNodePath;
this.resultId = options.resultId || null;
this.executionContext = options.executionContext || null;
this.sourceName = options.sourceName;
this.initialState = options.initialState;
this.noLoaderWhileRequestingDataAsync = options.noLoaderWhileRequestingDataAsync;
this.requestDataAsync = options.requestDataAsync;
this.saveChanges = options.saveChanges;
this.init();
}
cancelFetch = () => {
@@ -105,6 +128,16 @@ export class TableViewerModel {
}
}
private init() {
if (this.initialState) {
this.insertRows(0, this.initialState.rows, !this.initialState.isFullyLoaded);
this.tableDataModel.overWrite(this.initialState.columns);
this.updateInfo(this.initialState.statusMessage, this.initialState.duration);
this.agGridModel.initialRows = this.tableDataModel.getRows();
this.agGridModel.initialColumns = this.tableDataModel.getColumns();
}
}
private async onCellEditingStopped(rowNumber: number, colNumber: number, value: any): Promise<void> {
this.tableEditor.editCellValue(rowNumber, colNumber, value);
}
@@ -120,10 +153,10 @@ export class TableViewerModel {
return data;
}
this._isLoaderVisible = !this.init.noLoaderWhileRequestingDataAsync;
this._isLoaderVisible = !this.noLoaderWhileRequestingDataAsync;
try {
const response = await this.init.requestDataAsync(rowOffset, count);
const response = await this.requestDataAsync(this, rowOffset, count);
this.insertRows(rowOffset, response.rows, !response.isFullyLoaded);
if (!this.tableDataModel.getColumns().length) {
@@ -234,7 +267,7 @@ export class TableViewerModel {
this._isLoaderVisible = true;
try {
const data = await this.init.saveChanges(diffs);
const data = await this.saveChanges(this, diffs);
const someRows = this.zipDiffAndResults(diffs, data.rows);
this.tableEditor.cancelChanges();
@@ -9,26 +9,33 @@
import { observable } from 'mobx';
import { injectable } from '@dbeaver/core/di';
import { CommonDialogService } from '@dbeaver/core/dialogs';
import { TableViewerModel } from './TableViewerModel';
import { TableViewerModel, ITableViewerModelOptions } from './TableViewerModel';
// todo this service must be removed. Store TableViewerModel directly where it is required
@injectable()
export class TableViewerStorageService {
@observable private tableModelMap: Map<string, TableViewerModel> = new Map();
hasTableModel(tableId: string): boolean {
constructor(private commonDialogService: CommonDialogService) {}
has(tableId: string): boolean {
return this.tableModelMap.has(tableId);
}
getTableModel(tableId: string) {
get(tableId: string) {
return this.tableModelMap.get(tableId);
}
addTableModel(tableId: string, tableModel: TableViewerModel) {
this.tableModelMap.set(tableId, tableModel);
create(
options: ITableViewerModelOptions,
): TableViewerModel {
const tableModel = new TableViewerModel(options, this.commonDialogService);
this.tableModelMap.set(tableModel.tableId, tableModel);
return tableModel;
}
removeTableModel(tableId: string) {
remove(tableId: string) {
this.tableModelMap.delete(tableId);
}
}
@@ -6,8 +6,11 @@ export default manifest;
export * from './TableViewer/TableViewerStorageService';
export * from './TableViewer/TableViewer';
export * from './TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService';
export * from './TableViewer/TableViewerModel';
export * from './TableViewer/TableDataModel/TableColumn';
export * from './TableViewer/TableDataModel/TableRow';
export * from './TableViewer/TableDataModel/EditedRow';
export * from './IExecutionContext';
@@ -11,6 +11,7 @@ import { IServiceInjector, PluginManifest } from '@dbeaver/core/di';
import { DataViewerBootstrap } from './DataViewerBootstrap';
import { DataViewerTableService } from './DataViewerTableService';
import { DataViewerTabService } from './DataViewerTabService';
import { TableFooterMenuService } from './TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService';
import { TableViewerStorageService } from './TableViewer/TableViewerStorageService';
export const manifest: PluginManifest = {
@@ -22,6 +23,7 @@ export const manifest: PluginManifest = {
DataViewerTabService,
DataViewerTableService,
TableViewerStorageService,
TableFooterMenuService,
],
async initialize(services: IServiceInjector) {
+1
View File
@@ -28,6 +28,7 @@
"@dbeaver/ddl-viewer-plugin": "^0.1.0",
"@dbeaver/object-viewer-plugin": "^0.1.0",
"@dbeaver/sql-editor": "^0.1.0",
"@dbeaver/data-export-plugin": "^0.1.0",
"@material/button": "^4.0.0",
"@material/elevation": "^4.0.0",
"@material/ripple": "^4.0.0",
+1
View File
@@ -10,4 +10,5 @@ module.exports = [
"@dbeaver/object-viewer-plugin",
"@dbeaver/sql-editor",
"@dbeaver/ddl-viewer-plugin",
"@dbeaver/data-export-plugin",
];
@@ -36,6 +36,19 @@
<path fill-rule="evenodd" d="M15.597 0L8 7.596.404 0 0 .403 7.596 8 0 15.595.404 16 8 8.404 15.597 16l.403-.405L8.404 8 16 .403z"/>
</symbol>
<symbol id="export" viewBox="0 0 32 32">
<g id="export" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Page-1" transform="translate(3.000000, 2.000000)" fill="#338ECC">
<polygon id="Fill-1" points="12.7422 -0.0003 5.4502 7.2927 9.6172 7.2927 9.6172 20.4797 15.8672 20.4797 15.8672 7.2927 20.0342 7.2927"></polygon>
<polygon id="Fill-2" points="3.641 28.6602 21.844 28.6602 21.844 25.0192 3.641 25.0192"></polygon>
<polygon id="Fill-3" points="0 25.0192 3.641 25.0192 3.641 18.7792 0 18.7792"></polygon>
<polygon id="Fill-4" points="0 28.6602 3.641 28.6602 3.641 25.0192 0 25.0192"></polygon>
<polygon id="Fill-5" points="21.844 25.0192 25.485 25.0192 25.485 18.7792 21.844 18.7792"></polygon>
<polygon id="Fill-6" points="21.844 28.6602 25.485 28.6602 25.485 25.0192 21.844 25.0192"></polygon>
</g>
</g>
</symbol>
<symbol id="angle" viewBox="0 0 15 8">
<path class="icon-fill" fill-rule="evenodd" d="M14.5.954l-.875-.88L7.5 6.238 1.375 0 .5.88 7.5 8z"/>
</symbol>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -7,23 +7,17 @@
*/
import { IExecutionContext } from '@dbeaver/data-viewer-plugin';
import { SqlExecutionState } from './SqlExecutionState';
import { SQLQueryExecutionProcess } from './SqlResultTabs/SQLQueryExecutionProcess';
export interface ISqlContextParams {
contextId: string;
connectionId: string;
objectCatalogId?: string;
objectSchemaId?: string;
}
export interface ISqlQueryParams extends ISqlContextParams {
export interface ISqlQueryParams extends IExecutionContext {
query: string;
}
export interface ISqlResultPanelParams {
resultTabId: string; // to store tableView in tableViewStore
resultId: string | null; // returns from the server, new id returns after each data fetch
indexInResultSet: number;
sqlQueryParams: ISqlQueryParams;
sqlExecutionState: SqlExecutionState;
@@ -40,7 +34,7 @@ export interface IResultsTabState {
panelParams: ISqlResultPanelParams;
}
export interface ISqlEditorTabState extends ISqlContextParams {
export interface ISqlEditorTabState extends IExecutionContext {
order: number;
query: string;
currentResultTabId?: string;
@@ -21,8 +21,9 @@ import {
import { injectable } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { GraphQLService } from '@dbeaver/core/sdk';
import { IExecutionContext } from '@dbeaver/data-viewer-plugin';
import { ISqlEditorTabState, ISqlContextParams } from './ISqlEditorTabState';
import { ISqlEditorTabState } from './ISqlEditorTabState';
import { SqlDialectInfoService } from './SqlDialectInfoService';
import { sqlEditorTabHandlerKey } from './sqlEditorTabHandlerKey';
import { SqlExecutionState } from './SqlExecutionState';
@@ -201,7 +202,7 @@ export class SqlEditorNavigatorService {
connectionId: string,
defaultCatalog?: string,
defaultSchema?: string
): Promise<ISqlContextParams> {
): Promise<IExecutionContext> {
const response = await this.gql.gql.sqlContextCreate({
connectionId,
@@ -21,8 +21,9 @@ import {
import { injectable } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { GraphQLService } from '@dbeaver/core/sdk';
import { IExecutionContext } from '@dbeaver/data-viewer-plugin';
import { ISqlEditorTabState, ISqlContextParams } from './ISqlEditorTabState';
import { ISqlEditorTabState } from './ISqlEditorTabState';
import { SqlDialectInfoService } from './SqlDialectInfoService';
import { SqlEditorPanel } from './SqlEditorPanel';
import { SqlEditorTab } from './SqlEditorTab';
@@ -174,7 +175,7 @@ export class SqlEditorTabService {
connectionId: string,
defaultCatalog?: string,
defaultSchema?: string
): Promise<ISqlContextParams> {
): Promise<IExecutionContext> {
const response = await this.gql.gql.sqlContextCreate({
connectionId,
@@ -105,7 +105,7 @@ export class SQLQueryExecutionProcess extends Deferred<SqlExecuteInfo> {
private async executeQueryAsync(sqlQueryParams: ISqlQueryParams,
rowOffset: number,
count: number): Promise<AsyncTaskInfo> {
const response = await this.graphQLService.gql.asyncSqlExecuteQuery({
const { taskInfo } = await this.graphQLService.gql.asyncSqlExecuteQuery({
connectionId: sqlQueryParams.connectionId,
contextId: sqlQueryParams.contextId,
query: sqlQueryParams.query,
@@ -115,12 +115,12 @@ export class SQLQueryExecutionProcess extends Deferred<SqlExecuteInfo> {
limit: count,
},
});
return response.result;
return taskInfo;
}
private async getQueryStatusAsync(taskId: string): Promise<AsyncTaskInfo> {
const response = await this.graphQLService.gql.asyncTaskStatus({ taskId });
return response.result;
const { taskInfo } = await this.graphQLService.gql.asyncTaskStatus({ taskId });
return taskInfo;
}
private async cancelQueryAsync(taskId: string): Promise<void> {
@@ -17,9 +17,10 @@ import { PromiseCancelledError } from '@dbeaver/core/utils';
import {
fetchingSettings,
IRequestDataResult,
ITableViewerModelInit, RowDiff,
TableViewerModel,
RowDiff,
TableViewerStorageService,
TableViewerModel,
IExecutionContext,
} from '@dbeaver/data-viewer-plugin';
import { ISqlResultPanelParams } from '../../ISqlEditorTabState';
@@ -35,7 +36,7 @@ export enum EPanelState {
@injectable()
export class SqlResultPanelController
implements IInitializableController<[ISqlResultPanelParams]>, IDestructibleController {
implements IInitializableController, IDestructibleController {
@observable state: EPanelState = EPanelState.PENDING;
@observable executionResult = '';
@@ -66,13 +67,18 @@ implements IInitializableController<[ISqlResultPanelParams]>, IDestructibleContr
if (!dataSet.resultSet) {
this.state = EPanelState.MESSAGE_RESULT;
this.executionResult = `Query executed: ${response.statusMessage || ''} (${response.duration} ms)`;
} else {
this.state = EPanelState.TABLE_RESULT;
this.panelInit.resultId = dataSet.resultSet.id;
const initialState = this.sqlResultService
.sqlExecuteInfoToData(response, this.panelInit.indexInResultSet, fetchingSettings.fetchDefault);
this.createTableModel(initialState, panelInit.sqlExecutionState);
this.createTableModel(
panelInit.sqlQueryParams.query,
panelInit.sqlQueryParams,
initialState,
panelInit.sqlQueryParams.connectionId,
dataSet.resultSet.id,
panelInit.sqlExecutionState
);
}
} catch (exception) {
@@ -109,7 +115,7 @@ implements IInitializableController<[ISqlResultPanelParams]>, IDestructibleContr
destruct() {
if (this.state === EPanelState.TABLE_RESULT) {
this.tableViewerStorageService.removeTableModel(this.getTableId());
this.tableViewerStorageService.remove(this.getTableId());
}
}
@@ -123,20 +129,33 @@ implements IInitializableController<[ISqlResultPanelParams]>, IDestructibleContr
}
}
private createTableModel(initialState: IRequestDataResult, sqlExecutingState: SqlExecutionState): void {
const callbacks: ITableViewerModelInit = {
private createTableModel(
sourceName: string,
executionContext: IExecutionContext,
initialState: IRequestDataResult,
connectionId: string,
resultId: string,
sqlExecutingState: SqlExecutionState
) {
return this.tableViewerStorageService.create({
tableId: this.getTableId(),
connectionId,
executionContext,
resultId,
sourceName,
initialState,
requestDataAsync: this.requestDataAsync.bind(this, sqlExecutingState),
noLoaderWhileRequestingDataAsync: true,
saveChanges: this.saveChanges.bind(this),
};
const table = new TableViewerModel(callbacks, this.commonDialogService);
this.tableViewerStorageService.addTableModel(this.getTableId(), table);
});
}
private async requestDataAsync(sqlExecutingState: SqlExecutionState,
rowOffset: number,
count: number): Promise<IRequestDataResult> {
private async requestDataAsync(
sqlExecutingState: SqlExecutionState,
model: TableViewerModel,
rowOffset: number,
count: number,
): Promise<IRequestDataResult> {
const queryExecutionProcess = this.sqlResultService.asyncSqlQuery(this.panelInit.sqlQueryParams, rowOffset, count);
sqlExecutingState.setCurrentlyExecutingQuery(queryExecutionProcess);
@@ -147,14 +166,16 @@ implements IInitializableController<[ISqlResultPanelParams]>, IDestructibleContr
* Note that each data fetching overwrites resultId
*/
const dataSet = response.results![this.panelInit.indexInResultSet]!.resultSet!;
this.panelInit.resultId = dataSet.id;
model.resultId = dataSet.id;
return dataResults;
}
async saveChanges(diffs: RowDiff[]): Promise<IRequestDataResult> {
async saveChanges(model: TableViewerModel, diffs: RowDiff[]): Promise<IRequestDataResult> {
if (!model.resultId) {
throw new Error('resultId must be provided before saving changes');
}
const resultId = this.panelInit.resultId!; // must be set after first data request;
const response = await this.sqlResultService.saveChanges(this.panelInit.sqlQueryParams, resultId, diffs);
const response = await this.sqlResultService.saveChanges(this.panelInit.sqlQueryParams, model.resultId, diffs);
return this.sqlResultService.sqlExecuteInfoToData(response, this.panelInit.indexInResultSet);
}
@@ -23,7 +23,6 @@ export class SqlResultService {
constructor(private graphQLService: GraphQLService,
private notificationService: NotificationService) { }
/**
* @deprecated use asyncSqlQuery
*/
@@ -7,7 +7,7 @@
*/
import { injectable } from '@dbeaver/core/di';
import { isPromiseCancelledError, PromiseCancelledError, uuid } from '@dbeaver/core/utils';
import { isPromiseCancelledError, uuid } from '@dbeaver/core/utils';
import { fetchingSettings } from '@dbeaver/data-viewer-plugin';
import {
@@ -38,10 +38,9 @@ export class SqlResultTabsService {
editorState.sqlExecutionState.setCurrentlyExecutingQuery(queryExecutionProcess);
function createTabPanelParams(resultId: string | null, indexInResultSet: number): ISqlResultPanelParams {
function createTabPanelParams(indexInResultSet: number): ISqlResultPanelParams {
const params: ISqlResultPanelParams = {
resultTabId: uuid(),
resultId,
indexInResultSet,
sqlQueryParams,
firstDataPortion: queryExecutionProcess,
@@ -50,7 +49,7 @@ export class SqlResultTabsService {
return params;
}
const firstTabPanelParams = createTabPanelParams(null, 0);
const firstTabPanelParams = createTabPanelParams(0);
const currentTab = editorState.resultTabs.find(tab => tab.resultTabId === editorState.currentResultTabId);
const groupToReplace = inNewTab ? undefined : currentTab?.groupId;
@@ -75,7 +74,7 @@ export class SqlResultTabsService {
if (data.results && data.results?.length > 1) {
// ignore first dataset, it was already added
for (let i = 1; i < data.results.length; i++) {
const tabPanelParams = createTabPanelParams(data.results[i].resultSet?.id || null, i);
const tabPanelParams = createTabPanelParams(i);
this.addTabToGroup(editorState.resultTabs, tabPanelParams, newGroupId);
}
}
+1
View File
@@ -10,4 +10,5 @@ module.exports = [
"@dbeaver/object-viewer-plugin",
"@dbeaver/sql-editor",
"@dbeaver/ddl-viewer-plugin",
"@dbeaver/data-export-plugin",
];