From 69342f80b7ffdc41f564e361980e392959ff319a Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 18 May 2022 19:57:43 +0300 Subject: [PATCH] feat(plugin-data-viewer): CB-1657 auto refresh --- .../src/TopNavBar/shared/TopMenuItem.tsx | 1 - .../core-blocks/public/icons/timer-play.svg | 5 + .../core-blocks/public/icons/timer-play_m.svg | 5 + .../public/icons/timer-play_sm.svg | 5 + .../core-blocks/public/icons/timer-stop_m.svg | 5 + .../public/icons/timer-stop_sm.svg | 5 + .../core-blocks/public/icons/timer.svg | 14 ++ .../core-blocks/public/icons/timer_m.svg | 7 + .../core-blocks/public/icons/timer_sm.svg | 14 ++ .../core-blocks/public/icons/timer_stop.svg | 5 + webapp/packages/core-blocks/src/Menu/Menu.tsx | 88 +++++++++++ .../core-blocks/src/Menu/MenuEmptyItem.tsx | 41 +++++ .../core-blocks/src/Menu/MenuItem.tsx | 63 ++++++++ .../core-blocks/src/Menu/MenuItemElement.tsx | 60 ++++++++ .../core-blocks/src/Menu/MenuPanel.tsx | 55 +++++++ .../core-blocks/src/Menu/MenuStateContext.ts | 12 ++ .../core-blocks/src/Menu/menuPanelStyles.ts | 143 ++++++++++++++++++ webapp/packages/core-blocks/src/TimerIcon.tsx | 77 ++++++++++ webapp/packages/core-blocks/src/index.ts | 12 ++ .../core-blocks/src/useCombinedHandler.ts | 6 +- .../core-localization/src/locales/en.ts | 9 ++ .../core-localization/src/locales/it.ts | 8 + .../core-localization/src/locales/ru.ts | 9 ++ .../core-localization/src/locales/zh.ts | 9 ++ .../AutoRefresh/AutoRefreshButton.tsx | 117 ++++++++++++++ .../AutoRefresh/AutoRefreshSettingsDialog.tsx | 111 ++++++++++++++ .../AutoRefresh/IAutoRefreshSettings.ts | 12 ++ .../TableFooter/AutoRefresh/useAutoRefresh.ts | 71 +++++++++ .../TableViewer/TableFooter/TableFooter.tsx | 5 + .../plugin-data-viewer/src/locales/en.ts | 2 + .../plugin-data-viewer/src/locales/it.ts | 2 + .../plugin-data-viewer/src/locales/ru.ts | 2 + .../plugin-data-viewer/src/locales/zh.ts | 2 + 33 files changed, 979 insertions(+), 3 deletions(-) create mode 100644 webapp/packages/core-blocks/public/icons/timer-play.svg create mode 100644 webapp/packages/core-blocks/public/icons/timer-play_m.svg create mode 100644 webapp/packages/core-blocks/public/icons/timer-play_sm.svg create mode 100644 webapp/packages/core-blocks/public/icons/timer-stop_m.svg create mode 100644 webapp/packages/core-blocks/public/icons/timer-stop_sm.svg create mode 100644 webapp/packages/core-blocks/public/icons/timer.svg create mode 100644 webapp/packages/core-blocks/public/icons/timer_m.svg create mode 100644 webapp/packages/core-blocks/public/icons/timer_sm.svg create mode 100644 webapp/packages/core-blocks/public/icons/timer_stop.svg create mode 100644 webapp/packages/core-blocks/src/Menu/Menu.tsx create mode 100644 webapp/packages/core-blocks/src/Menu/MenuEmptyItem.tsx create mode 100644 webapp/packages/core-blocks/src/Menu/MenuItem.tsx create mode 100644 webapp/packages/core-blocks/src/Menu/MenuItemElement.tsx create mode 100644 webapp/packages/core-blocks/src/Menu/MenuPanel.tsx create mode 100644 webapp/packages/core-blocks/src/Menu/MenuStateContext.ts create mode 100644 webapp/packages/core-blocks/src/Menu/menuPanelStyles.ts create mode 100644 webapp/packages/core-blocks/src/TimerIcon.tsx create mode 100644 webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshButton.tsx create mode 100644 webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshSettingsDialog.tsx create mode 100644 webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/IAutoRefreshSettings.ts create mode 100644 webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/useAutoRefresh.ts diff --git a/webapp/packages/core-app/src/TopNavBar/shared/TopMenuItem.tsx b/webapp/packages/core-app/src/TopNavBar/shared/TopMenuItem.tsx index 2a3de07d60..4b443c6c3d 100644 --- a/webapp/packages/core-app/src/TopNavBar/shared/TopMenuItem.tsx +++ b/webapp/packages/core-app/src/TopNavBar/shared/TopMenuItem.tsx @@ -50,7 +50,6 @@ export const TopMenuItem = observer(function TopMenuItem({ menuItem, sty disclosure onClick={() => menuItem.onClick?.()} > - + + + + diff --git a/webapp/packages/core-blocks/public/icons/timer-play_m.svg b/webapp/packages/core-blocks/public/icons/timer-play_m.svg new file mode 100644 index 0000000000..2d3ca8822e --- /dev/null +++ b/webapp/packages/core-blocks/public/icons/timer-play_m.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/webapp/packages/core-blocks/public/icons/timer-play_sm.svg b/webapp/packages/core-blocks/public/icons/timer-play_sm.svg new file mode 100644 index 0000000000..843b9d196b --- /dev/null +++ b/webapp/packages/core-blocks/public/icons/timer-play_sm.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/webapp/packages/core-blocks/public/icons/timer-stop_m.svg b/webapp/packages/core-blocks/public/icons/timer-stop_m.svg new file mode 100644 index 0000000000..1c65da35d8 --- /dev/null +++ b/webapp/packages/core-blocks/public/icons/timer-stop_m.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/webapp/packages/core-blocks/public/icons/timer-stop_sm.svg b/webapp/packages/core-blocks/public/icons/timer-stop_sm.svg new file mode 100644 index 0000000000..645cc1ca28 --- /dev/null +++ b/webapp/packages/core-blocks/public/icons/timer-stop_sm.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/webapp/packages/core-blocks/public/icons/timer.svg b/webapp/packages/core-blocks/public/icons/timer.svg new file mode 100644 index 0000000000..15b9e5a3af --- /dev/null +++ b/webapp/packages/core-blocks/public/icons/timer.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/webapp/packages/core-blocks/public/icons/timer_m.svg b/webapp/packages/core-blocks/public/icons/timer_m.svg new file mode 100644 index 0000000000..6348c085ee --- /dev/null +++ b/webapp/packages/core-blocks/public/icons/timer_m.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/webapp/packages/core-blocks/public/icons/timer_sm.svg b/webapp/packages/core-blocks/public/icons/timer_sm.svg new file mode 100644 index 0000000000..a5fd13b4c2 --- /dev/null +++ b/webapp/packages/core-blocks/public/icons/timer_sm.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/webapp/packages/core-blocks/public/icons/timer_stop.svg b/webapp/packages/core-blocks/public/icons/timer_stop.svg new file mode 100644 index 0000000000..1c65da35d8 --- /dev/null +++ b/webapp/packages/core-blocks/public/icons/timer_stop.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/webapp/packages/core-blocks/src/Menu/Menu.tsx b/webapp/packages/core-blocks/src/Menu/Menu.tsx new file mode 100644 index 0000000000..b30df26ff0 --- /dev/null +++ b/webapp/packages/core-blocks/src/Menu/Menu.tsx @@ -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, '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>(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)( + + + {disclosureProps => React.cloneElement(children, { ...disclosureProps, ...children.props })} + + + {items} + + + ); + } + + return styled(styles)( + + + {children} + + + {items} + + + ); +}, { forwardRef: true }); \ No newline at end of file diff --git a/webapp/packages/core-blocks/src/Menu/MenuEmptyItem.tsx b/webapp/packages/core-blocks/src/Menu/MenuEmptyItem.tsx new file mode 100644 index 0000000000..7758f1736b --- /dev/null +++ b/webapp/packages/core-blocks/src/Menu/MenuEmptyItem.tsx @@ -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, 'style'> { + style?: ComponentStyle; +} + +export const MenuEmptyItem = observer(function MenuEmptyItem({ + style, +}) { + const styles = useStyles(menuPanelStyles, style); + const translate = useTranslate(); + + const label = translate('core_ui_empty'); + + return styled(styles)( + + + + ); +}); \ No newline at end of file diff --git a/webapp/packages/core-blocks/src/Menu/MenuItem.tsx b/webapp/packages/core-blocks/src/Menu/MenuItem.tsx new file mode 100644 index 0000000000..0a8b650515 --- /dev/null +++ b/webapp/packages/core-blocks/src/Menu/MenuItem.tsx @@ -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, 'style'> { + label: string; + hidden?: boolean; + selected?: boolean; + close?: boolean; + style?: ComponentStyle; +} + +export const MenuItem = observer(function MenuItem({ + label, + children, + hidden, + selected, + close, + style, + onClick, + ...rest +}) { + const menu = useContext(MenuStateContext); + const styles = useStyles(menuPanelStyles, style); + + const handleClick = useCombinedHandler<[React.MouseEvent]>(onClick, function handleClick() { + if (close) { + menu?.hide(); + } + }); + + const MenuItem = ReakitMenuItem; + + return styled(styles)( + + ); +}); \ No newline at end of file diff --git a/webapp/packages/core-blocks/src/Menu/MenuItemElement.tsx b/webapp/packages/core-blocks/src/Menu/MenuItemElement.tsx new file mode 100644 index 0000000000..d8edbe2b75 --- /dev/null +++ b/webapp/packages/core-blocks/src/Menu/MenuItemElement.tsx @@ -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(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))( + + + {typeof icon === 'string' ? : icon} + + + {title} + + + {binding} + + + {loading && } + {menu && !loading && } + + + + ); +}); diff --git a/webapp/packages/core-blocks/src/Menu/MenuPanel.tsx b/webapp/packages/core-blocks/src/Menu/MenuPanel.tsx new file mode 100644 index 0000000000..8baf16a576 --- /dev/null +++ b/webapp/packages/core-blocks/src/Menu/MenuPanel.tsx @@ -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(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)( + + + {Children.count(children) === 0 && ( + + )} + {children} + + + ); +}); \ No newline at end of file diff --git a/webapp/packages/core-blocks/src/Menu/MenuStateContext.ts b/webapp/packages/core-blocks/src/Menu/MenuStateContext.ts new file mode 100644 index 0000000000..8401fccdb0 --- /dev/null +++ b/webapp/packages/core-blocks/src/Menu/MenuStateContext.ts @@ -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(null); \ No newline at end of file diff --git a/webapp/packages/core-blocks/src/Menu/menuPanelStyles.ts b/webapp/packages/core-blocks/src/Menu/menuPanelStyles.ts new file mode 100644 index 0000000000..9622ce5669 --- /dev/null +++ b/webapp/packages/core-blocks/src/Menu/menuPanelStyles.ts @@ -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; + } + } + `; diff --git a/webapp/packages/core-blocks/src/TimerIcon.tsx b/webapp/packages/core-blocks/src/TimerIcon.tsx new file mode 100644 index 0000000000..5a76fb35ad --- /dev/null +++ b/webapp/packages/core-blocks/src/TimerIcon.tsx @@ -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> = function TimerIcon({ + state, + interval, + styles, + ...rest +}) { + return styled(style, useStyles(styles))( + + + + {interval} + + ); +}; \ No newline at end of file diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index 37f9b85980..a2a71547a0 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -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'; diff --git a/webapp/packages/core-blocks/src/useCombinedHandler.ts b/webapp/packages/core-blocks/src/useCombinedHandler.ts index d429b5b28e..0be4d3e52c 100644 --- a/webapp/packages/core-blocks/src/useCombinedHandler.ts +++ b/webapp/packages/core-blocks/src/useCombinedHandler.ts @@ -8,13 +8,15 @@ import { useObjectRef } from './useObjectRef'; -export function useCombinedHandler(...handlers: Array<(...args: T) => any>): (...args: T) => void { +export function useCombinedHandler( + ...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); } }, }), diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index b8d7ac0122..4e139677a3 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -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.'], diff --git a/webapp/packages/core-localization/src/locales/it.ts b/webapp/packages/core-localization/src/locales/it.ts index e7d451fee1..04de48ace5 100644 --- a/webapp/packages/core-localization/src/locales/it.ts +++ b/webapp/packages/core-localization/src/locales/it.ts @@ -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'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index 62c68de613..9e383d6071 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -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', 'По вашему запросу ничего не найдено.'], diff --git a/webapp/packages/core-localization/src/locales/zh.ts b/webapp/packages/core-localization/src/locales/zh.ts index d4946ac865..78ed2f75b9 100644 --- a/webapp/packages/core-localization/src/locales/zh.ts +++ b/webapp/packages/core-localization/src/locales/zh.ts @@ -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', '没有找到结果'], diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshButton.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshButton.tsx new file mode 100644 index 0000000000..f53dbbecae --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshButton.tsx @@ -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; + disabled?: boolean; +} + +const intervals = [5, 10, 15, 30, 60]; + +export const AutoRefreshButton = observer(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)( + + + + + {intervals.map((inte, i) => ( + autoRefresh.setInterval(inte)} + > + + + ))} + + + + + )} + disabled={disabled} + disclosure + > + + + + + + ); +}); \ No newline at end of file diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshSettingsDialog.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshSettingsDialog.tsx new file mode 100644 index 0000000000..4386751a6d --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/AutoRefreshSettingsDialog.tsx @@ -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>(function AutoRefreshSettingsDialog({ + rejectDialog, + resolveDialog, + payload, +}) { + const translate = useTranslate(); + const formRef = useRef(null); + + function resolve() { + formRef.current?.focus(); + const valid = formRef.current?.checkValidity(); + formRef.current?.reportValidity(); + + if (valid) { + resolveDialog(); + } + } + + return styled(styles, BASE_CONTAINERS_STYLES)( + + + + + + + + )} + noBodyPadding + noOverflow + onReject={rejectDialog} + > + + resolve()}> + + + + {translate('ui_interval')} + + + + {translate('data_viewer_auto_refresh_settings_stop_on_error')} + + + + + + + ); +}); \ No newline at end of file diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/IAutoRefreshSettings.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/IAutoRefreshSettings.ts new file mode 100644 index 0000000000..b10e724609 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/IAutoRefreshSettings.ts @@ -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; +} \ No newline at end of file diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/useAutoRefresh.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/useAutoRefresh.ts new file mode 100644 index 0000000000..e641822a11 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/AutoRefresh/useAutoRefresh.ts @@ -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; + setInterval: (interval: number) => void; + stop: () => void; +} + +export function useAutoRefresh(model: IDatabaseDataModel) { + const commonDialogService = useService(CommonDialogService); + const settings = useObservableRef(() => ({ + 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(() => ({ + 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']); +} \ No newline at end of file diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx index 7c5554c3d3..5f3cfc3e14 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx @@ -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(function TableFooter({ model.refresh()}> +