mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Move data source loader into the select (#19465)
This commit is contained in:
parent
9a68236d8d
commit
16b041608d
@ -28,7 +28,7 @@ const handleThemeChange = (theme: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
// automatically import all files ending in *.stories.tsx
|
// 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(withKnobs);
|
||||||
addDecorator(withPaddedStory);
|
addDecorator(withPaddedStory);
|
||||||
|
@ -11,7 +11,8 @@ import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
|
|||||||
import { components } from '@torkelo/react-select';
|
import { components } from '@torkelo/react-select';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { SelectOption, SingleValue } from './SelectOption';
|
import { SelectOption } from './SelectOption';
|
||||||
|
import { SingleValue } from './SingleValue';
|
||||||
import SelectOptionGroup from './SelectOptionGroup';
|
import SelectOptionGroup from './SelectOptionGroup';
|
||||||
import IndicatorsContainer from './IndicatorsContainer';
|
import IndicatorsContainer from './IndicatorsContainer';
|
||||||
import NoOptionsMessage from './NoOptionsMessage';
|
import NoOptionsMessage from './NoOptionsMessage';
|
||||||
|
@ -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 (
|
|
||||||
<components.SingleValue {...props}>
|
|
||||||
<div className="gf-form-select-box__img-value">
|
|
||||||
{data.imgUrl && <img className="gf-form-select-box__desc-option__img" src={data.imgUrl} />}
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</components.SingleValue>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SelectOption;
|
export default SelectOption;
|
||||||
|
61
packages/grafana-ui/src/components/Select/SingleValue.tsx
Normal file
61
packages/grafana-ui/src/components/Select/SingleValue.tsx
Normal file
@ -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 (
|
||||||
|
<components.SingleValue {...props}>
|
||||||
|
<div className={cx('gf-form-select-box__img-value')}>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<FadeTransition duration={150} visible={loading}>
|
||||||
|
<Spinner className={styles.item} inline />
|
||||||
|
</FadeTransition>
|
||||||
|
{data.imgUrl && (
|
||||||
|
<FadeTransition duration={150} visible={!loading}>
|
||||||
|
<img className={styles.item} src={data.imgUrl} />
|
||||||
|
</FadeTransition>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</components.SingleValue>
|
||||||
|
);
|
||||||
|
};
|
15
packages/grafana-ui/src/components/Spinner/Spinner.story.tsx
Normal file
15
packages/grafana-ui/src/components/Spinner/Spinner.story.tsx
Normal file
@ -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 (
|
||||||
|
<div>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
33
packages/grafana-ui/src/components/Spinner/Spinner.tsx
Normal file
33
packages/grafana-ui/src/components/Spinner/Spinner.tsx
Normal file
@ -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: Props) => {
|
||||||
|
const { className, inline = false, iconClassName, style, size = 16 } = props;
|
||||||
|
const styles = getStyles(size, inline);
|
||||||
|
return (
|
||||||
|
<div style={style} className={cx(styles.wrapper, className)}>
|
||||||
|
<i className={cx('fa fa-spinner fa-spin', iconClassName)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -86,3 +86,5 @@ export { JSONFormatter } from './JSONFormatter/JSONFormatter';
|
|||||||
export { JsonExplorer } from './JSONFormatter/json_explorer/json_explorer';
|
export { JsonExplorer } from './JSONFormatter/json_explorer/json_explorer';
|
||||||
export { ErrorBoundary, ErrorBoundaryAlert } from './ErrorBoundary/ErrorBoundary';
|
export { ErrorBoundary, ErrorBoundaryAlert } from './ErrorBoundary/ErrorBoundary';
|
||||||
export { AlphaNotice } from './AlphaNotice/AlphaNotice';
|
export { AlphaNotice } from './AlphaNotice/AlphaNotice';
|
||||||
|
export { Spinner } from './Spinner/Spinner';
|
||||||
|
export { FadeTransition } from './transitions/FadeTransition';
|
||||||
|
@ -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 (
|
||||||
|
<CSSTransition in={visible} mountOnEnter={true} unmountOnExit={true} timeout={duration} classNames={styles}>
|
||||||
|
{children}
|
||||||
|
</CSSTransition>
|
||||||
|
);
|
||||||
|
}
|
26
packages/grafana-ui/src/utils/useDelayedSwitch.story.tsx
Normal file
26
packages/grafana-ui/src/utils/useDelayedSwitch.story.tsx
Normal file
@ -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 <div>{valueDelayed ? 'ON' : 'OFF'}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const story = storiesOf('Utils/useDelayedSwitch', module);
|
||||||
|
story.addDecorator(withCenteredStory);
|
||||||
|
story.add('useDelayedSwitch', () => {
|
||||||
|
return <StoryWrapper />;
|
||||||
|
});
|
55
packages/grafana-ui/src/utils/useDelayedSwitch.ts
Normal file
55
packages/grafana-ui/src/utils/useDelayedSwitch.ts
Normal file
@ -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<Date | undefined>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
@ -15,6 +15,7 @@ export interface Props {
|
|||||||
onBlur?: () => void;
|
onBlur?: () => void;
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
openMenuOnFocus?: boolean;
|
openMenuOnFocus?: boolean;
|
||||||
|
showLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DataSourcePicker extends PureComponent<Props> {
|
export class DataSourcePicker extends PureComponent<Props> {
|
||||||
@ -35,7 +36,7 @@ export class DataSourcePicker extends PureComponent<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { datasources, current, autoFocus, onBlur, openMenuOnFocus } = this.props;
|
const { datasources, current, autoFocus, onBlur, openMenuOnFocus, showLoading } = this.props;
|
||||||
|
|
||||||
const options = datasources.map(ds => ({
|
const options = datasources.map(ds => ({
|
||||||
value: ds.name,
|
value: ds.name,
|
||||||
@ -47,6 +48,7 @@ export class DataSourcePicker extends PureComponent<Props> {
|
|||||||
label: current.name,
|
label: current.name,
|
||||||
value: current.name,
|
value: current.name,
|
||||||
imgUrl: current.meta.info.logos.small,
|
imgUrl: current.meta.info.logos.small,
|
||||||
|
loading: showLoading,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -67,7 +67,6 @@ interface ExploreProps {
|
|||||||
changeSize: typeof changeSize;
|
changeSize: typeof changeSize;
|
||||||
datasourceError: string;
|
datasourceError: string;
|
||||||
datasourceInstance: DataSourceApi;
|
datasourceInstance: DataSourceApi;
|
||||||
datasourceLoading: boolean | null;
|
|
||||||
datasourceMissing: boolean;
|
datasourceMissing: boolean;
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
initializeExplore: typeof initializeExplore;
|
initializeExplore: typeof initializeExplore;
|
||||||
@ -251,7 +250,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
StartPage,
|
StartPage,
|
||||||
datasourceInstance,
|
datasourceInstance,
|
||||||
datasourceError,
|
datasourceError,
|
||||||
datasourceLoading,
|
|
||||||
datasourceMissing,
|
datasourceMissing,
|
||||||
exploreId,
|
exploreId,
|
||||||
showingStartPage,
|
showingStartPage,
|
||||||
@ -272,7 +270,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
return (
|
return (
|
||||||
<div className={exploreClass} ref={this.getRef}>
|
<div className={exploreClass} ref={this.getRef}>
|
||||||
<ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} />
|
<ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} />
|
||||||
{datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
|
|
||||||
{datasourceMissing ? this.renderEmptyState() : null}
|
{datasourceMissing ? this.renderEmptyState() : null}
|
||||||
|
|
||||||
<FadeIn duration={datasourceError ? 150 : 5} in={datasourceError ? true : false}>
|
<FadeIn duration={datasourceError ? 150 : 5} in={datasourceError ? true : false}>
|
||||||
@ -360,7 +357,6 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
|
|||||||
StartPage,
|
StartPage,
|
||||||
datasourceError,
|
datasourceError,
|
||||||
datasourceInstance,
|
datasourceInstance,
|
||||||
datasourceLoading,
|
|
||||||
datasourceMissing,
|
datasourceMissing,
|
||||||
initialized,
|
initialized,
|
||||||
showingStartPage,
|
showingStartPage,
|
||||||
@ -406,7 +402,6 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
|
|||||||
StartPage,
|
StartPage,
|
||||||
datasourceError,
|
datasourceError,
|
||||||
datasourceInstance,
|
datasourceInstance,
|
||||||
datasourceLoading,
|
|
||||||
datasourceMissing,
|
datasourceMissing,
|
||||||
initialized,
|
initialized,
|
||||||
showingStartPage,
|
showingStartPage,
|
||||||
|
@ -68,6 +68,7 @@ interface StateProps {
|
|||||||
isPaused: boolean;
|
isPaused: boolean;
|
||||||
originPanelId: number;
|
originPanelId: number;
|
||||||
queries: DataQuery[];
|
queries: DataQuery[];
|
||||||
|
datasourceLoading: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DispatchProps {
|
interface DispatchProps {
|
||||||
@ -156,6 +157,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
|||||||
isLive,
|
isLive,
|
||||||
isPaused,
|
isPaused,
|
||||||
originPanelId,
|
originPanelId,
|
||||||
|
datasourceLoading,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const styles = getStyles();
|
const styles = getStyles();
|
||||||
@ -193,6 +195,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
|||||||
onChange={this.onChangeDatasource}
|
onChange={this.onChangeDatasource}
|
||||||
datasources={exploreDatasources}
|
datasources={exploreDatasources}
|
||||||
current={selectedDatasource}
|
current={selectedDatasource}
|
||||||
|
showLoading={datasourceLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{supportedModes.length > 1 ? (
|
{supportedModes.length > 1 ? (
|
||||||
@ -316,6 +319,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
|
|||||||
isPaused,
|
isPaused,
|
||||||
originPanelId,
|
originPanelId,
|
||||||
queries,
|
queries,
|
||||||
|
datasourceLoading,
|
||||||
} = exploreItem;
|
} = exploreItem;
|
||||||
const selectedDatasource = datasourceInstance
|
const selectedDatasource = datasourceInstance
|
||||||
? exploreDatasources.find(datasource => datasource.name === datasourceInstance.name)
|
? exploreDatasources.find(datasource => datasource.name === datasourceInstance.name)
|
||||||
@ -339,6 +343,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
|
|||||||
isPaused,
|
isPaused,
|
||||||
originPanelId,
|
originPanelId,
|
||||||
queries,
|
queries,
|
||||||
|
datasourceLoading,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -318,6 +318,9 @@ Array [
|
|||||||
<div
|
<div
|
||||||
className="gf-form-select-box__img-value"
|
className="gf-form-select-box__img-value"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
className="css-zyq2zu"
|
||||||
|
/>
|
||||||
stackdriver auto
|
stackdriver auto
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user