From 16b041608d2fd4b3c4e4a8a54332d3ef881dfd7c Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Wed, 2 Oct 2019 10:15:06 +0200 Subject: [PATCH] Explore: Move data source loader into the select (#19465) --- packages/grafana-ui/.storybook/config.ts | 2 +- .../src/components/Select/Select.tsx | 3 +- .../src/components/Select/SelectOption.tsx | 14 ----- .../src/components/Select/SingleValue.tsx | 61 +++++++++++++++++++ .../src/components/Spinner/Spinner.story.tsx | 15 +++++ .../src/components/Spinner/Spinner.tsx | 33 ++++++++++ packages/grafana-ui/src/components/index.ts | 2 + .../components/transitions/FadeTransition.tsx | 43 +++++++++++++ .../src/utils/useDelayedSwitch.story.tsx | 26 ++++++++ .../grafana-ui/src/utils/useDelayedSwitch.ts | 55 +++++++++++++++++ .../components/Select/DataSourcePicker.tsx | 4 +- public/app/features/explore/Explore.tsx | 5 -- .../app/features/explore/ExploreToolbar.tsx | 5 ++ .../__snapshots__/QueryEditor.test.tsx.snap | 3 + 14 files changed, 249 insertions(+), 22 deletions(-) create mode 100644 packages/grafana-ui/src/components/Select/SingleValue.tsx create mode 100644 packages/grafana-ui/src/components/Spinner/Spinner.story.tsx create mode 100644 packages/grafana-ui/src/components/Spinner/Spinner.tsx create mode 100644 packages/grafana-ui/src/components/transitions/FadeTransition.tsx create mode 100644 packages/grafana-ui/src/utils/useDelayedSwitch.story.tsx create mode 100644 packages/grafana-ui/src/utils/useDelayedSwitch.ts diff --git a/packages/grafana-ui/.storybook/config.ts b/packages/grafana-ui/.storybook/config.ts index 98a4ee8bd9e..9c32f3c7e34 100644 --- a/packages/grafana-ui/.storybook/config.ts +++ b/packages/grafana-ui/.storybook/config.ts @@ -28,7 +28,7 @@ const handleThemeChange = (theme: string) => { } }; // automatically import all files ending in *.stories.tsx -const req = require.context('../src/components', true, /.story.tsx$/); +const req = require.context('../src', true, /.story.tsx$/); addDecorator(withKnobs); addDecorator(withPaddedStory); diff --git a/packages/grafana-ui/src/components/Select/Select.tsx b/packages/grafana-ui/src/components/Select/Select.tsx index 929d369c951..3721ae4ef37 100644 --- a/packages/grafana-ui/src/components/Select/Select.tsx +++ b/packages/grafana-ui/src/components/Select/Select.tsx @@ -11,7 +11,8 @@ import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async'; import { components } from '@torkelo/react-select'; // Components -import { SelectOption, SingleValue } from './SelectOption'; +import { SelectOption } from './SelectOption'; +import { SingleValue } from './SingleValue'; import SelectOptionGroup from './SelectOptionGroup'; import IndicatorsContainer from './IndicatorsContainer'; import NoOptionsMessage from './NoOptionsMessage'; diff --git a/packages/grafana-ui/src/components/Select/SelectOption.tsx b/packages/grafana-ui/src/components/Select/SelectOption.tsx index 624487940c1..249183eba0c 100644 --- a/packages/grafana-ui/src/components/Select/SelectOption.tsx +++ b/packages/grafana-ui/src/components/Select/SelectOption.tsx @@ -30,18 +30,4 @@ export const SelectOption = (props: ExtendedOptionProps) => { ); }; -// was not able to type this without typescript error -export const SingleValue = (props: any) => { - const { children, data } = props; - - return ( - -
- {data.imgUrl && } - {children} -
-
- ); -}; - export default SelectOption; diff --git a/packages/grafana-ui/src/components/Select/SingleValue.tsx b/packages/grafana-ui/src/components/Select/SingleValue.tsx new file mode 100644 index 00000000000..9346537cbd6 --- /dev/null +++ b/packages/grafana-ui/src/components/Select/SingleValue.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { css, cx } from 'emotion'; + +// Ignoring because I couldn't get @types/react-select work wih Torkel's fork +// @ts-ignore +import { components } from '@torkelo/react-select'; +import { FadeTransition, Spinner } from '..'; +import { useDelayedSwitch } from '../../utils/useDelayedSwitch'; +import { stylesFactory } from '../../themes'; + +const getStyles = stylesFactory(() => { + const container = css` + width: 16px; + height: 16px; + display: inline-block; + margin-right: 10px; + position: relative; + vertical-align: middle; + `; + + const item = css` + width: 100%; + height: 100%; + position: absolute; + `; + + return { container, item }; +}); + +type Props = { + children: React.ReactNode; + data: { + imgUrl?: string; + loading?: boolean; + }; +}; + +export const SingleValue = (props: Props) => { + const { children, data } = props; + const styles = getStyles(); + + const loading = useDelayedSwitch(data.loading || false, { delay: 250, duration: 750 }); + + return ( + +
+
+ + + + {data.imgUrl && ( + + + + )} +
+ {children} +
+
+ ); +}; diff --git a/packages/grafana-ui/src/components/Spinner/Spinner.story.tsx b/packages/grafana-ui/src/components/Spinner/Spinner.story.tsx new file mode 100644 index 00000000000..90ecc20e748 --- /dev/null +++ b/packages/grafana-ui/src/components/Spinner/Spinner.story.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import { storiesOf } from '@storybook/react'; +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; +import { Spinner } from './Spinner'; + +const story = storiesOf('UI/Spinner', module); +story.addDecorator(withCenteredStory); +story.add('spinner', () => { + return ( +
+ +
+ ); +}); diff --git a/packages/grafana-ui/src/components/Spinner/Spinner.tsx b/packages/grafana-ui/src/components/Spinner/Spinner.tsx new file mode 100644 index 00000000000..2642c019f9b --- /dev/null +++ b/packages/grafana-ui/src/components/Spinner/Spinner.tsx @@ -0,0 +1,33 @@ +import React, { FC } from 'react'; +import { cx, css } from 'emotion'; +import { stylesFactory } from '../../themes'; + +const getStyles = stylesFactory((size: number, inline: boolean) => { + return { + wrapper: css` + font-size: ${size}px; + ${inline + ? css` + display: inline-block; + ` + : ''} + `, + }; +}); + +type Props = { + className?: string; + style?: React.CSSProperties; + iconClassName?: string; + inline?: boolean; + size?: number; +}; +export const Spinner: FC = (props: Props) => { + const { className, inline = false, iconClassName, style, size = 16 } = props; + const styles = getStyles(size, inline); + return ( +
+ +
+ ); +}; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index a55f3f91d81..4e6505f1b8e 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -86,3 +86,5 @@ export { JSONFormatter } from './JSONFormatter/JSONFormatter'; export { JsonExplorer } from './JSONFormatter/json_explorer/json_explorer'; export { ErrorBoundary, ErrorBoundaryAlert } from './ErrorBoundary/ErrorBoundary'; export { AlphaNotice } from './AlphaNotice/AlphaNotice'; +export { Spinner } from './Spinner/Spinner'; +export { FadeTransition } from './transitions/FadeTransition'; diff --git a/packages/grafana-ui/src/components/transitions/FadeTransition.tsx b/packages/grafana-ui/src/components/transitions/FadeTransition.tsx new file mode 100644 index 00000000000..9b3159a7781 --- /dev/null +++ b/packages/grafana-ui/src/components/transitions/FadeTransition.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { css } from 'emotion'; +import { CSSTransition } from 'react-transition-group'; +import { stylesFactory } from '../../themes'; + +const getStyles = stylesFactory((duration: number) => { + return { + enter: css` + label: enter; + opacity: 0; + `, + enterActive: css` + label: enterActive; + opacity: 1; + transition: opacity ${duration}ms ease-out; + `, + exit: css` + label: exit; + opacity: 1; + `, + exitActive: css` + label: exitActive; + opacity: 0; + transition: opacity ${duration}ms ease-out; + `, + }; +}); + +type Props = { + children: React.ReactNode; + visible: boolean; + duration?: number; +}; + +export function FadeTransition(props: Props) { + const { visible, children, duration = 250 } = props; + const styles = getStyles(duration); + return ( + + {children} + + ); +} diff --git a/packages/grafana-ui/src/utils/useDelayedSwitch.story.tsx b/packages/grafana-ui/src/utils/useDelayedSwitch.story.tsx new file mode 100644 index 00000000000..d62a9132968 --- /dev/null +++ b/packages/grafana-ui/src/utils/useDelayedSwitch.story.tsx @@ -0,0 +1,26 @@ +import React from 'react'; + +import { storiesOf } from '@storybook/react'; +import { withCenteredStory } from './storybook/withCenteredStory'; +import { useDelayedSwitch } from './useDelayedSwitch'; +import { boolean, number } from '@storybook/addon-knobs'; + +const getKnobs = () => { + return { + value: boolean('Value', false), + duration: number('Duration to stay on', 2000), + delay: number('Delay before switching on', 2000), + }; +}; + +function StoryWrapper() { + const { value, delay = 0, duration = 0 } = getKnobs(); + const valueDelayed = useDelayedSwitch(value, { delay, duration }); + return
{valueDelayed ? 'ON' : 'OFF'}
; +} + +const story = storiesOf('Utils/useDelayedSwitch', module); +story.addDecorator(withCenteredStory); +story.add('useDelayedSwitch', () => { + return ; +}); diff --git a/packages/grafana-ui/src/utils/useDelayedSwitch.ts b/packages/grafana-ui/src/utils/useDelayedSwitch.ts new file mode 100644 index 00000000000..99f0bdb9a6b --- /dev/null +++ b/packages/grafana-ui/src/utils/useDelayedSwitch.ts @@ -0,0 +1,55 @@ +import { useEffect, useRef, useState } from 'react'; + +type DelayOptions = { + // Minimal amount of time the switch will be on. + duration?: number; + // Delay after which switch will turn on. + delay?: number; +}; + +/** + * Hook that delays changing of boolean switch to prevent too much time spent in "on" state. It is kind of a throttle + * but you can specify different time for on and off throttling so this only allows a boolean values and also prefers + * to stay "off" so turning "on" is always delayed while turning "off" is throttled. + * + * This is useful for showing loading elements to prevent it flashing too much in case of quick loading time or + * prevent it flash if loaded state comes right after switch to loading. + */ +export function useDelayedSwitch(value: boolean, options: DelayOptions = {}): boolean { + const { duration = 250, delay = 250 } = options; + + const [delayedValue, setDelayedValue] = useState(value); + const onStartTime = useRef(); + + useEffect(() => { + let timeout: number | undefined; + if (value) { + // If toggling to "on" state we always setTimout no matter how long we have been "off". + timeout = setTimeout(() => { + onStartTime.current = new Date(); + setDelayedValue(value); + }, delay) as any; + } else { + // If toggling to "off" state we check how much time we were already "on". + const timeSpent = onStartTime.current ? Date.now() - onStartTime.current.valueOf() : 0; + const turnOff = () => { + onStartTime.current = undefined; + setDelayedValue(value); + }; + if (timeSpent >= duration) { + // We already spent enough time "on" so change right away. + turnOff(); + } else { + timeout = setTimeout(turnOff, duration - timeSpent) as any; + } + } + return () => { + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + }; + }, [value, duration, delay]); + + return delayedValue; +} diff --git a/public/app/core/components/Select/DataSourcePicker.tsx b/public/app/core/components/Select/DataSourcePicker.tsx index 6edd54d0b44..984c7d062e5 100644 --- a/public/app/core/components/Select/DataSourcePicker.tsx +++ b/public/app/core/components/Select/DataSourcePicker.tsx @@ -15,6 +15,7 @@ export interface Props { onBlur?: () => void; autoFocus?: boolean; openMenuOnFocus?: boolean; + showLoading?: boolean; } export class DataSourcePicker extends PureComponent { @@ -35,7 +36,7 @@ export class DataSourcePicker extends PureComponent { }; render() { - const { datasources, current, autoFocus, onBlur, openMenuOnFocus } = this.props; + const { datasources, current, autoFocus, onBlur, openMenuOnFocus, showLoading } = this.props; const options = datasources.map(ds => ({ value: ds.name, @@ -47,6 +48,7 @@ export class DataSourcePicker extends PureComponent { label: current.name, value: current.name, imgUrl: current.meta.info.logos.small, + loading: showLoading, }; return ( diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 45503d50e7b..16028807666 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -67,7 +67,6 @@ interface ExploreProps { changeSize: typeof changeSize; datasourceError: string; datasourceInstance: DataSourceApi; - datasourceLoading: boolean | null; datasourceMissing: boolean; exploreId: ExploreId; initializeExplore: typeof initializeExplore; @@ -251,7 +250,6 @@ export class Explore extends React.PureComponent { StartPage, datasourceInstance, datasourceError, - datasourceLoading, datasourceMissing, exploreId, showingStartPage, @@ -272,7 +270,6 @@ export class Explore extends React.PureComponent { return (
- {datasourceLoading ?
Loading datasource...
: null} {datasourceMissing ? this.renderEmptyState() : null} @@ -360,7 +357,6 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia StartPage, datasourceError, datasourceInstance, - datasourceLoading, datasourceMissing, initialized, showingStartPage, @@ -406,7 +402,6 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia StartPage, datasourceError, datasourceInstance, - datasourceLoading, datasourceMissing, initialized, showingStartPage, diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 26469eac606..0d98dc8a0a6 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -68,6 +68,7 @@ interface StateProps { isPaused: boolean; originPanelId: number; queries: DataQuery[]; + datasourceLoading: boolean | null; } interface DispatchProps { @@ -156,6 +157,7 @@ export class UnConnectedExploreToolbar extends PureComponent { isLive, isPaused, originPanelId, + datasourceLoading, } = this.props; const styles = getStyles(); @@ -193,6 +195,7 @@ export class UnConnectedExploreToolbar extends PureComponent { onChange={this.onChangeDatasource} datasources={exploreDatasources} current={selectedDatasource} + showLoading={datasourceLoading} />
{supportedModes.length > 1 ? ( @@ -316,6 +319,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps isPaused, originPanelId, queries, + datasourceLoading, } = exploreItem; const selectedDatasource = datasourceInstance ? exploreDatasources.find(datasource => datasource.name === datasourceInstance.name) @@ -339,6 +343,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps isPaused, originPanelId, queries, + datasourceLoading, }; }; diff --git a/public/app/plugins/datasource/stackdriver/components/__snapshots__/QueryEditor.test.tsx.snap b/public/app/plugins/datasource/stackdriver/components/__snapshots__/QueryEditor.test.tsx.snap index 1b2b4147e44..8b53b129511 100644 --- a/public/app/plugins/datasource/stackdriver/components/__snapshots__/QueryEditor.test.tsx.snap +++ b/public/app/plugins/datasource/stackdriver/components/__snapshots__/QueryEditor.test.tsx.snap @@ -318,6 +318,9 @@ Array [
+
stackdriver auto