mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
@@ -3,13 +3,9 @@ scalar Object
|
||||
# Date/Time
|
||||
scalar DateTime
|
||||
|
||||
type Query {
|
||||
type Query
|
||||
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
|
||||
}
|
||||
type Mutation
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
|
||||
+4
-59
@@ -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}/>;
|
||||
});
|
||||
|
||||
+11
-61
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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?',
|
||||
|
||||
@@ -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: 'Сессия истекла. Хотите перезагрузить страницу?',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
+11
-11
@@ -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;
|
||||
}
|
||||
`
|
||||
);
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+21
-80
@@ -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>
|
||||
);
|
||||
|
||||
+14
-52
@@ -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');
|
||||
|
||||
+4
-60
@@ -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}/>;
|
||||
});
|
||||
|
||||
+6
-60
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
+33
@@ -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>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
+44
@@ -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>
|
||||
);
|
||||
});
|
||||
+95
@@ -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>
|
||||
);
|
||||
});
|
||||
+39
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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> {
|
||||
|
||||
+40
-19
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,4 +10,5 @@ module.exports = [
|
||||
"@dbeaver/object-viewer-plugin",
|
||||
"@dbeaver/sql-editor",
|
||||
"@dbeaver/ddl-viewer-plugin",
|
||||
"@dbeaver/data-export-plugin",
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user