feat(plugin-data-viewer): CB-1657 auto refresh

This commit is contained in:
Wroud
2022-05-18 19:57:43 +03:00
parent 98823b735f
commit 69342f80b7
33 changed files with 979 additions and 3 deletions
@@ -50,7 +50,6 @@ export const TopMenuItem = observer<IProps>(function TopMenuItem({ menuItem, sty
disclosure
onClick={() => menuItem.onClick?.()}
>
<TopNavButton
title={title}
icon={menuItem.icon}
@@ -0,0 +1,5 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="root">
<path d="M3 10.5V1.5L9 5.78571L3 10.5Z" fill="#4FA843"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 179 B

@@ -0,0 +1,5 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="root">
<path d="M4 9.5V2.5L8 5.83333L4 9.5Z" fill="#4FA843"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 177 B

@@ -0,0 +1,5 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="root">
<path d="M4.5 9V3L7.5 5.85714L4.5 9Z" fill="#4FA843"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 177 B

@@ -0,0 +1,5 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="root">
<rect x="3" y="3" width="6" height="6" fill="#E73E52"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 178 B

@@ -0,0 +1,5 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="root">
<rect x="4" y="4" width="4" height="4" fill="#E73E52"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 178 B

@@ -0,0 +1,14 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="root">
<g clip-path="url(#clip0_3835_1301)">
<rect x="14.5" y="12" width="2" height="8" fill="#C4C4C4"/>
<rect x="19.5371" y="22.1221" width="2" height="5.05974" transform="rotate(135 19.5371 22.1221)" fill="#C4C4C4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.65685 2L1 7.65685L3.12132 9.77817L8.77817 4.12132L6.65685 2ZM15.5 32.0001C22.9558 32.0001 29 25.956 29 18.5001C29 11.0443 22.9558 5.00012 15.5 5.00012C8.04416 5.00012 2 11.0443 2 18.5001C2 25.956 8.04416 32.0001 15.5 32.0001ZM26 18.5001C26 24.2991 21.299 29.0001 15.5 29.0001C9.70101 29.0001 5 24.2991 5 18.5001C5 12.7011 9.70101 8.00012 15.5 8.00012C21.299 8.00012 26 12.7011 26 18.5001Z" fill="#338ECC"/>
</g>
<defs>
<clipPath id="clip0_3835_1301">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</g>
</svg>

After

Width:  |  Height:  |  Size: 901 B

@@ -0,0 +1,7 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="root">
<path fill-rule="evenodd" clip-rule="evenodd" d="M23 13C23 19.0751 18.0751 24 12 24C5.92487 24 1 19.0751 1 13C1 6.92487 5.92487 2 12 2C18.0751 2 23 6.92487 23 13ZM12 22C16.9706 22 21 17.9706 21 13C21 8.02944 16.9706 4 12 4C7.02944 4 3 8.02944 3 13C3 17.9706 7.02944 22 12 22Z" fill="#338ECC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 7H11V13H11.0139L14.578 16.5641L15.9922 15.1499L13 12.1577V7Z" fill="#C4C4C4"/>
<rect y="5.24268" width="6" height="2" transform="rotate(-45 0 5.24268)" fill="#338ECC"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 638 B

@@ -0,0 +1,14 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="root">
<g clip-path="url(#clip0_3835_1532)">
<rect y="3.82837" width="4" height="1.2" transform="rotate(-45 0 3.82837)" fill="#338ECC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.1998 8.8001C15.1998 12.7765 11.9763 16.0001 7.9998 16.0001C4.02335 16.0001 0.799805 12.7765 0.799805 8.8001C0.799805 4.82365 4.02335 1.6001 7.9998 1.6001C11.9763 1.6001 15.1998 4.82365 15.1998 8.8001ZM7.9998 14.8001C11.3135 14.8001 13.9998 12.1138 13.9998 8.8001C13.9998 5.48639 11.3135 2.8001 7.9998 2.8001C4.6861 2.8001 1.9998 5.48639 1.9998 8.8001C1.9998 12.1138 4.6861 14.8001 7.9998 14.8001Z" fill="#338ECC"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.60051 4.83997H7.40051V9.83709L7.40039 9.83721L7.40051 9.83733V9.83997H7.40315L9.52171 11.9585L10.3702 11.11L8.60051 9.34027V4.83997Z" fill="#C4C4C4"/>
</g>
<defs>
<clipPath id="clip0_3835_1532">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,5 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="root">
<rect x="3" y="3" width="6" height="6" fill="#E73E52"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 178 B

@@ -0,0 +1,88 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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-lite';
import React, { useEffect } from 'react';
import { MenuButton, MenuInitialState, useMenuState } from 'reakit/Menu';
import styled from 'reshadow';
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
import { useObjectRef } from '../useObjectRef';
import { MenuPanel } from './MenuPanel';
import { menuPanelStyles } from './menuPanelStyles';
import { MenuStateContext } from './MenuStateContext';
interface IMenuProps extends Omit<React.ButtonHTMLAttributes<any>, 'style'> {
label: string;
items: React.ReactNode;
style?: ComponentStyle;
disclosure?: boolean;
placement?: MenuInitialState['placement'];
modal?: boolean;
visible?: boolean;
rtl?: boolean;
onVisibleSwitch?: (visible: boolean) => void;
}
export const Menu = observer<IMenuProps, React.ButtonHTMLAttributes<any>>(function Menu({
label,
items,
disclosure,
children,
style,
placement,
visible,
onVisibleSwitch,
modal,
rtl,
...props
}, ref) {
const propsRef = useObjectRef({ onVisibleSwitch, visible });
const menu = useMenuState({ modal, placement, visible, rtl });
const styles = useStyles(menuPanelStyles, style);
useEffect(() => {
propsRef.onVisibleSwitch?.(menu.visible);
}, [menu.visible]);
if (React.isValidElement(children) && disclosure) {
return styled(styles)(
<MenuStateContext.Provider value={menu}>
<MenuButton ref={ref} {...menu} {...props} {...children.props}>
{disclosureProps => React.cloneElement(children, { ...disclosureProps, ...children.props })}
</MenuButton>
<MenuPanel
label={label}
menu={menu}
style={style}
rtl={rtl}
>
{items}
</MenuPanel>
</MenuStateContext.Provider>
);
}
return styled(styles)(
<MenuStateContext.Provider value={menu}>
<MenuButton {...menu} {...props}>
<box>{children}</box>
</MenuButton>
<MenuPanel
label={label}
menu={menu}
style={style}
rtl={rtl}
>
{items}
</MenuPanel>
</MenuStateContext.Provider>
);
}, { forwardRef: true });
@@ -0,0 +1,41 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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-lite';
import styled from 'reshadow';
import { useTranslate } from '@cloudbeaver/core-localization';
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
import { MenuItem } from './MenuItem';
import { MenuItemElement } from './MenuItemElement';
import { menuPanelStyles } from './menuPanelStyles';
interface Props extends Omit<React.ButtonHTMLAttributes<any>, 'style'> {
style?: ComponentStyle;
}
export const MenuEmptyItem = observer<Props>(function MenuEmptyItem({
style,
}) {
const styles = useStyles(menuPanelStyles, style);
const translate = useTranslate();
const label = translate('core_ui_empty');
return styled(styles)(
<MenuItem
id='empty'
label={label}
disabled
>
<MenuItemElement label={label} style={style} />
</MenuItem>
);
});
@@ -0,0 +1,63 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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-lite';
import React, { useContext } from 'react';
import { MenuItem as ReakitMenuItem } from 'reakit/Menu';
import styled, { use } from 'reshadow';
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
import { useCombinedHandler } from '../useCombinedHandler';
import { menuPanelStyles } from './menuPanelStyles';
import { MenuStateContext } from './MenuStateContext';
export interface IMenuItemProps extends Omit<React.ButtonHTMLAttributes<any>, 'style'> {
label: string;
hidden?: boolean;
selected?: boolean;
close?: boolean;
style?: ComponentStyle;
}
export const MenuItem = observer<IMenuItemProps>(function MenuItem({
label,
children,
hidden,
selected,
close,
style,
onClick,
...rest
}) {
const menu = useContext(MenuStateContext);
const styles = useStyles(menuPanelStyles, style);
const handleClick = useCombinedHandler<[React.MouseEvent<HTMLButtonElement>]>(onClick, function handleClick() {
if (close) {
menu?.hide();
}
});
const MenuItem = ReakitMenuItem;
return styled(styles)(
<MenuItem
{...menu}
{...use({ hidden: hidden })}
aria-label={label}
aria-selected={selected}
{...rest}
disabled={selected || rest.disabled}
onClick={handleClick}
>
{children}
</MenuItem>
);
});
@@ -0,0 +1,60 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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-lite';
import styled from 'reshadow';
import { Icon, IconOrImage, Loader, useStateDelay } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles, ComponentStyle } from '@cloudbeaver/core-theming';
import { menuPanelStyles } from './menuPanelStyles';
interface IMenuItemElementProps {
label: string;
tooltip?: string;
binding?: string;
icon?: string | React.ReactNode;
menu?: boolean;
loading?: boolean;
style?: ComponentStyle;
}
export const MenuItemElement = observer<IMenuItemElementProps>(function MenuItemElement({
label,
tooltip,
binding,
icon,
menu,
loading = false,
style = [],
}) {
const translate = useTranslate();
const title = translate(label);
loading = useStateDelay(loading, 100);
return styled(useStyles(menuPanelStyles, style))(
<menu-panel-item title={tooltip ? translate(tooltip) : title}>
<menu-item-content>
{typeof icon === 'string' ? <IconOrImage icon={icon} /> : icon}
</menu-item-content>
<menu-item-text title={title}>
{title}
</menu-item-text>
<menu-item-binding title={binding}>
{binding}
</menu-item-binding>
<menu-item-content>
{loading && <Loader small fullSize />}
{menu && !loading && <Icon name="arrow" viewBox="0 0 16 16" />}
</menu-item-content>
</menu-panel-item>
);
});
@@ -0,0 +1,55 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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-lite';
import { Children } from 'react';
import { Menu, MenuStateReturn } from 'reakit/Menu';
import styled, { use } from 'reshadow';
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
import { MenuEmptyItem } from './MenuEmptyItem';
import { menuPanelStyles } from './menuPanelStyles';
export interface IMenuPanelProps {
label: string;
menu: MenuStateReturn; // from reakit useMenuState
panelAvailable?: boolean;
hasBindings?: boolean;
rtl?: boolean;
style?: ComponentStyle;
}
export const MenuPanel = observer<IMenuPanelProps>(function MenuPanel({
label,
menu,
panelAvailable = true,
hasBindings,
rtl,
children,
style,
}) {
const styles = useStyles(menuPanelStyles, style);
const visible = menu.visible;
if (!visible) {
return null;
}
return styled(styles)(
<Menu {...menu} aria-label={label} visible={panelAvailable}>
<menu-box dir={rtl ? 'rtl' : undefined} {...use({ hasBindings })}>
{Children.count(children) === 0 && (
<MenuEmptyItem style={style} />
)}
{children}
</menu-box>
</Menu>
);
});
@@ -0,0 +1,12 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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 type { MenuStateReturn } from 'reakit/ts';
export const MenuStateContext = createContext<MenuStateReturn | null>(null);
@@ -0,0 +1,143 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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';
export const menuPanelStyles = css`
menu-box {
composes: theme-background-surface theme-text-on-surface from global;
}
MenuItem,
MenuItemCheckbox,
MenuItemRadio,
MenuButton,
MenuItemElement {
composes: theme-ripple from global;
}
menu-panel-item, MenuSeparator {
composes: theme-border-color-background from global;
}
MenuButton {
background: none;
border: none;
outline: none !important;
color: inherit;
cursor: pointer;
& box {
display: flex;
align-items: center;
flex: 1;
height: inherit;
}
}
Menu {
outline: none;
z-index: 999;
}
menu-box {
composes: theme-typography--body2 theme-elevation-z5 from global;
min-width: 140px;
padding: 12px 0;
& menu-box {
margin-top: -12px;
}
}
menu-box {
display: flex;
flex-direction: column;
&:not([|hasBindings]) menu-item-binding {
width: 0;
}
}
MenuItem, MenuItemCheckbox, MenuItemRadio {
display: flex;
align-items: center;
border: none;
padding: 0;
background: none;
text-align: left;
outline: none;
color: inherit;
cursor: pointer;
white-space: nowrap;
&[use|hidden] {
display: none;
}
&:global([aria-selected="true"]) {
opacity: 0.7;
}
&:hover, &:global([aria-expanded="true"]) {
& Icon {
opacity: 1;
}
}
}
MenuSeparator {
flex: 1;
width: 100%;
display: flex;
margin: 4px 0;
border: none !important;
border-bottom: 1px solid !important;
}
menu-item-binding {
composes: theme-typography--caption from global;
}
menu-panel-item {
flex: 1;
display: flex;
position: relative;
align-items: center;
height: 30px;
padding: 0 4px;
& menu-item-text, & menu-item-binding {
display: block;
padding: 0 4px;
}
& menu-item-binding {
flex: 1;
text-align: right;
padding-left: 32px;
width: 80px;
overflow: hidden;
text-overflow: ellipsis;
}
& menu-item-content {
width: 24px;
height: 24px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
& Icon {
width: 16px;
height: 16px;
opacity: 0.5;
transform: rotate(-90deg);
}
& IconOrImage {
width: 16px;
height: 16px;
object-fit: contain;
}
& Loader {
width: 16px;
}
}
`;
@@ -0,0 +1,77 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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 type React from 'react';
import styled, { css } from 'reshadow';
import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming';
import { Icon } from './Icon';
const style = css`
timer {
position: relative;
width: 28px;
height: 24px;
& > Icon {
width: 24px;
height: 24px;
}
}
state, interval {
composes: theme-background-secondary from global;
border-radius: 50%;
width: 12px;
height: 12px;
}
state {
position: absolute;
right: 0;
bottom: 0;
display: flex;
& > Icon {
width: 12px;
height: 12px;
}
}
interval {
position: absolute;
right: 0;
top: 0;
display: flex;
font-size: 8px;
font-weight: bold;
line-height: normal;
justify-content: center;
align-items: center;
}
`;
interface Props {
state: 'play' | 'stop';
interval: number;
styles?: ComponentStyle;
}
export const TimerIcon: React.FC<Props & React.ButtonHTMLAttributes<HTMLDivElement>> = function TimerIcon({
state,
interval,
styles,
...rest
}) {
return styled(style, useStyles(styles))(
<timer {...rest}>
<Icon name='/icons/timer_m.svg#root' viewBox="0 0 24 24" />
<state><Icon name={`/icons/timer-${state}_m.svg#root`} viewBox="0 0 12 12" /></state>
<interval>{interval}</interval>
</timer>
);
};
+12
View File
@@ -15,8 +15,19 @@ export { ITEM_LIST_STYLES } from './ItemList/styles';
export * from './layout/AppLogo';
export * from './layout/BASE_LAYOUT_GRID_STYLES';
export * from './layout/TopAppBar';
export * from './Loader/Loader';
export * from './Marks/ConnectionMark';
export * from './Menu/Menu';
export * from './Menu/MenuEmptyItem';
export * from './Menu/MenuItem';
export * from './Menu/MenuItemElement';
export * from './Menu/MenuPanel';
export * from './Menu/menuPanelStyles';
export * from './Menu/MenuStateContext';
export * from './ObjectPropertyInfo/ObjectPropertyInfoForm/ObjectPropertyInfoForm';
export * from './ObjectPropertyInfo/useObjectPropertyCategories';
@@ -136,6 +147,7 @@ export * from './IconButton';
export * from './IconOrImage';
export * from './StaticImage';
export * from './TextPlaceholder';
export * from './TimerIcon';
export * from './InfoItem';
export * from './Iframe';
export * from './Code';
@@ -8,13 +8,15 @@
import { useObjectRef } from './useObjectRef';
export function useCombinedHandler<T extends any[]>(...handlers: Array<(...args: T) => any>): (...args: T) => void {
export function useCombinedHandler<T extends any[]>(
...handlers: Array<((...args: T) => any) | undefined>
): (...args: T) => void {
const optionsRef = useObjectRef({ handlers });
const state = useObjectRef(
() => ({
handler(...args: T) {
for (const handler of optionsRef.handlers) {
handler(...args);
handler?.(...args);
}
},
}),
@@ -15,6 +15,14 @@ export const defaultENLocale = [
['ui_processing_create', 'Create'],
['ui_processing_save', 'Save'],
['ui_processing_saved', 'Saved'],
['ui_processing_stop', 'Stop'],
['ui_second_first_form', '{args.interval} second'],
['ui_second_second_form', '{args.interval} seconds'],
['ui_second_third_form', '{args.interval} seconds'],
['ui_minute_first_form', '{args.interval} minute'],
['ui_minute_second_form', '{args.interval} minutes'],
['ui_minute_third_form', '{args.interval} minutes'],
['ui_custom', 'Custom'],
['ui_configure', 'Configure'],
['ui_apply', 'Apply'],
['ui_error', 'Error'],
@@ -44,6 +52,7 @@ export const defaultENLocale = [
['ui_open_in_tab', 'Open in tab'],
['ui_rename', 'Rename'],
['ui_rename_processing', 'Renaming...'],
['ui_interval', 'Interval'],
['ui_name', 'Name'],
['ui_cant_delete_item', "This item can't be deleted"],
['ui_no_items_placeholder', 'There are no items yet.'],
@@ -15,6 +15,14 @@ export const defaultITLocale = [
['ui_processing_create', 'Crea'],
['ui_processing_save', 'Salva'],
['ui_processing_saved', 'Saved'],
['ui_processing_stop', 'Stop'],
['ui_second_first_form', '{args.interval} second'],
['ui_second_second_form', '{args.interval} seconds'],
['ui_second_third_form', '{args.interval} seconds'],
['ui_minute_first_form', '{args.interval} minute'],
['ui_minute_second_form', '{args.interval} minutes'],
['ui_minute_third_form', '{args.interval} minutes'],
['ui_custom', 'Custom'],
['ui_configure', 'Configure'],
['ui_apply', 'Applica'],
['ui_error', 'Errore'],
@@ -13,6 +13,14 @@ export const defaultRULocale = [
['ui_processing_create', 'Создать'],
['ui_processing_save', 'Сохранить'],
['ui_processing_saved', 'Сохранено'],
['ui_processing_stop', 'Остановить'],
['ui_second_first_form', '{args.interval} секунда'],
['ui_second_second_form', '{args.interval} секунды'],
['ui_second_third_form', '{args.interval} секунд'],
['ui_minute_first_form', '{args.interval} минута'],
['ui_minute_second_form', '{args.interval} минуты'],
['ui_minute_third_form', '{args.interval} минут'],
['ui_custom', 'Настроить'],
['ui_configure', 'Настроить'],
['ui_apply', 'Применить'],
['ui_error', 'Ошибка'],
@@ -43,6 +51,7 @@ export const defaultRULocale = [
['ui_rename', 'Переименовать'],
['ui_rename_processing', 'Переименование...'],
['ui_name', 'Название'],
['ui_interval', 'Интервал'],
['ui_cant_delete_item', 'Этот элемент нельзя удалить'],
['ui_no_items_placeholder', 'Вы еще ничего не добавили.'],
['ui_search_no_result_placeholder', 'По вашему запросу ничего не найдено.'],
@@ -15,6 +15,14 @@ export const defaultZHLocale = [
['ui_processing_create', '创建'],
['ui_processing_save', '保存'],
['ui_processing_saved', 'Saved'],
['ui_processing_stop', 'Stop'],
['ui_second_first_form', '{args.interval} second'],
['ui_second_second_form', '{args.interval} seconds'],
['ui_second_third_form', '{args.interval} seconds'],
['ui_minute_first_form', '{args.interval} minute'],
['ui_minute_second_form', '{args.interval} minutes'],
['ui_minute_third_form', '{args.interval} minutes'],
['ui_custom', 'Custom'],
['ui_configure', '配置'],
['ui_apply', '应用'],
['ui_error', '错误'],
@@ -45,6 +53,7 @@ export const defaultZHLocale = [
['ui_rename', '重命名'],
['ui_rename_processing', '重命名中...'],
['ui_name', '名称'],
['ui_interval', 'Interval'],
['ui_cant_delete_item', '此项目不能删除'],
['ui_no_items_placeholder', '还没有项目'],
['ui_search_no_result_placeholder', '没有找到结果'],
@@ -0,0 +1,117 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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-lite';
import styled, { css } from 'reshadow';
import { Icon, Menu, MenuItem, MenuItemElement, TimerIcon } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { declensionOfNumber } from '@cloudbeaver/core-utils';
import type { IDatabaseDataModel } from '../../../DatabaseDataModel/IDatabaseDataModel';
import { useAutoRefresh } from './useAutoRefresh';
const styles = css`
auto-reload {
composes: theme-text-primary theme-ripple from global;
height: 100%;
display: flex;
cursor: pointer;
align-items: center;
padding: 0 16px;
& IconOrImage {
width: 24px;
height: 24px;
flex-grow: 0;
flex-shrink: 0;
}
& > Icon {
& :global(use) {
fill: var(--theme-primary) !important;
}
width: 16px;
height: 16px;
flex-grow: 0;
flex-shrink: 0;
}
}
`;
interface Props {
model: IDatabaseDataModel<any, any>;
disabled?: boolean;
}
const intervals = [5, 10, 15, 30, 60];
export const AutoRefreshButton = observer<Props>(function AutoRefreshButton({
model,
disabled,
}) {
const translate = useTranslate();
const autoRefresh = useAutoRefresh(model);
const interval = autoRefresh.settings.interval;
const intervals_messages: string[] = [];
for (let interval of intervals) {
let message = ['ui_second_first_form', 'ui_second_second_form', 'ui_second_third_form'];
if (interval >= 60) {
message = ['ui_minute_first_form', 'ui_minute_second_form', 'ui_minute_third_form'];
interval = Math.round(interval / 60);
}
intervals_messages.push(
translate(declensionOfNumber(interval, message), undefined, { interval })
);
}
return styled(styles)(
<Menu
label='Auto refresh'
items={(
<>
<MenuItem label={translate('ui_custom')} close onClick={autoRefresh.configure}>
<MenuItemElement label={translate('ui_custom')} />
</MenuItem>
{intervals.map((inte, i) => (
<MenuItem
key={inte}
label={intervals_messages[i]}
selected={interval === inte}
close
onClick={() => autoRefresh.setInterval(inte)}
>
<MenuItemElement label={intervals_messages[i]} />
</MenuItem>
))}
<MenuItem
label={translate('ui_processing_stop')}
selected={interval === null}
close
onClick={autoRefresh.stop}
>
<MenuItemElement label={translate('ui_processing_stop')} />
</MenuItem>
</>
)}
disabled={disabled}
disclosure
>
<auto-reload aria-disabled={disabled}>
<TimerIcon
state={interval === null ? 'play' : 'stop'}
interval={interval ?? 5}
/>
<Icon name="arrow" viewBox="0 0 16 16" />
</auto-reload>
</Menu>
);
});
@@ -0,0 +1,111 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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-lite';
import { useRef } from 'react';
import styled, { css } from 'reshadow';
import { BASE_CONTAINERS_STYLES, Button, Container, FieldCheckbox, Group, InputField, SubmittingForm } from '@cloudbeaver/core-blocks';
import { CommonDialogWrapper, DialogComponentProps } from '@cloudbeaver/core-dialogs';
import { useTranslate } from '@cloudbeaver/core-localization';
import type { IAutoRefreshSettings } from './IAutoRefreshSettings';
const styles = css`
footer-container {
display: flex;
width: min-content;
flex: 1;
align-items: center;
justify-content: flex-end;
gap: 24px;
}
buttons {
flex: 1;
display: flex;
gap: 24px;
}
wrapper {
display: flex;
height: 100%;
width: 100%;
overflow: auto;
}
fill {
flex: 1;
}
`;
interface Payload {
settings: IAutoRefreshSettings;
}
export const AutoRefreshSettingsDialog = observer<DialogComponentProps<Payload>>(function AutoRefreshSettingsDialog({
rejectDialog,
resolveDialog,
payload,
}) {
const translate = useTranslate();
const formRef = useRef<HTMLFormElement>(null);
function resolve() {
formRef.current?.focus();
const valid = formRef.current?.checkValidity();
formRef.current?.reportValidity();
if (valid) {
resolveDialog();
}
}
return styled(styles, BASE_CONTAINERS_STYLES)(
<CommonDialogWrapper
size='small'
title='data_viewer_auto_refresh_settings'
icon='/icons/settings_cog_m.svg'
footer={(
<footer-container>
<buttons>
<Button mod={['outlined']} onClick={() => rejectDialog()}>{translate('ui_processing_cancel')}</Button>
<fill />
<Button mod={['unelevated']} onClick={() => resolve()}>{translate('ui_processing_ok')}</Button>
</buttons>
</footer-container>
)}
noBodyPadding
noOverflow
onReject={rejectDialog}
>
<wrapper>
<SubmittingForm ref={formRef} onSubmit={() => resolve()}>
<Container>
<Group form gap>
<InputField
name='interval'
state={payload.settings}
type='number'
min={5}
max={3600}
>
{translate('ui_interval')}
</InputField>
<FieldCheckbox
id="dataViewer.tableViewer.autoRefresh.stopOnError"
name="stopOnError"
state={payload.settings}
>
{translate('data_viewer_auto_refresh_settings_stop_on_error')}
</FieldCheckbox>
</Group>
</Container>
</SubmittingForm>
</wrapper>
</CommonDialogWrapper>
);
});
@@ -0,0 +1,12 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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 IAutoRefreshSettings {
interval: number | null;
stopOnError: boolean;
}
@@ -0,0 +1,71 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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 } from 'mobx';
import { useInterval, useObjectRef, useObservableRef } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import type { IDatabaseDataModel } from '../../../DatabaseDataModel/IDatabaseDataModel';
import { AutoRefreshSettingsDialog } from './AutoRefreshSettingsDialog';
import type { IAutoRefreshSettings } from './IAutoRefreshSettings';
interface IAutoRefresh {
settings: IAutoRefreshSettings;
configure: () => Promise<void>;
setInterval: (interval: number) => void;
stop: () => void;
}
export function useAutoRefresh(model: IDatabaseDataModel<any, any>) {
const commonDialogService = useService(CommonDialogService);
const settings = useObservableRef<IAutoRefreshSettings>(() => ({
interval: null,
stopOnError: true,
}), {
interval: observable,
stopOnError: observable,
}, false);
useInterval(async () => {
await model.refresh();
if (model.source.error && settings.stopOnError) {
settings.interval = null;
}
}, settings.interval !== null ? settings.interval * 1000 : null);
return useObjectRef<IAutoRefresh>(() => ({
async configure() {
const settings: IAutoRefreshSettings = observable({ ...this.settings });
const result = await commonDialogService.open(AutoRefreshSettingsDialog, { settings });
if (result === DialogueStateResult.Resolved) {
let interval = settings.interval;
if (typeof interval === 'string') {
interval = Number.parseInt(interval);
}
if (!Number.isInteger(interval)) {
interval = null;
}
Object.assign(this.settings, { ...settings, interval });
}
},
setInterval(interval: number) {
this.settings.interval = interval;
},
stop() {
this.settings.interval = null;
},
}), { settings }, ['configure', 'setInterval', 'stop']);
}
@@ -15,6 +15,7 @@ import { useStyles } from '@cloudbeaver/core-theming';
import type { IDatabaseDataModel } from '../../DatabaseDataModel/IDatabaseDataModel';
import { getDefaultRowsCount } from '../../getDefaultRowsCount';
import { AutoRefreshButton } from './AutoRefresh/AutoRefreshButton';
import { TableFooterMenu } from './TableFooterMenu/TableFooterMenu';
const tableFooterStyles = css`
@@ -41,6 +42,9 @@ const tableFooterStyles = css`
padding: 0 16px;
& IconOrImage {
& :global(use) {
fill: var(--theme-primary) !important;
}
width: 24px;
height: 24px;
}
@@ -106,6 +110,7 @@ export const TableFooter = observer<Props>(function TableFooter({
<reload aria-disabled={disabled} onClick={() => model.refresh()}>
<IconOrImage icon='reload' viewBox="0 0 16 16" />
</reload>
<AutoRefreshButton model={model} disabled={disabled} />
<count>
<SubmittingForm onSubmit={handleChange}>
<input
@@ -17,6 +17,8 @@ export default [
['data_viewer_result_edited_title', 'Save changes'],
['data_viewer_result_edited_message', 'Result set was edited. Do you want to save changes in database?'],
['data_viewer_data_save_error_title', 'Error occurred while saving changes'],
['data_viewer_auto_refresh_settings', 'Auto refresh Settings'],
['data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'],
['data_viewer_presentation_value_title', 'Value'],
['data_viewer_presentation_value_text_title', 'Text'],
['data_viewer_presentation_value_text_plain_title', 'Text'],
@@ -13,6 +13,8 @@ export default [
['data_viewer_result_edited_title', 'Save changes'],
['data_viewer_result_edited_message', 'Result set was edited. Do you want to save changes in database?'],
['data_viewer_data_save_error_title', 'Error occurred while saving changes'],
['data_viewer_auto_refresh_settings', 'Auto refresh Settings'],
['data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'],
['data_viewer_presentation_value_title', 'Valore'],
['data_viewer_presentation_value_text_title', 'Testo'],
['data_viewer_presentation_value_text_plain_title', 'Testo'],
@@ -17,6 +17,8 @@ export default [
['data_viewer_result_edited_title', 'Сохранить изменения'],
['data_viewer_result_edited_message', 'Данные результата запроса были изменены. Вы хотите сохранить изменения?'],
['data_viewer_data_save_error_title', 'Ошибка сохранения изменений'],
['data_viewer_auto_refresh_settings', 'Параметры автообновления'],
['data_viewer_auto_refresh_settings_stop_on_error', 'Остановить при ошибке'],
['data_viewer_presentation_value_title', 'Значение'],
['data_viewer_presentation_value_text_title', 'Текст'],
['data_viewer_presentation_value_image_title', 'Изображение'],
@@ -17,6 +17,8 @@ export default [
['data_viewer_result_edited_title', '保存更改'],
['data_viewer_result_edited_message', '结果集已编辑。是否将更改保存到数据库?'],
['data_viewer_data_save_error_title', 'Error occurred while saving changes'],
['data_viewer_auto_refresh_settings', 'Auto refresh Settings'],
['data_viewer_auto_refresh_settings_stop_on_error', 'Stop on error'],
['data_viewer_presentation_value_title', '值'],
['data_viewer_presentation_value_text_title', '文本'],
['data_viewer_presentation_value_text_plain_title', '文本'],