mirror of
https://github.com/grafana/grafana.git
synced 2025-02-13 00:55:47 -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
|
||||
const req = require.context('../src/components', true, /.story.tsx$/);
|
||||
const req = require.context('../src', true, /.story.tsx$/);
|
||||
|
||||
addDecorator(withKnobs);
|
||||
addDecorator(withPaddedStory);
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
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 { ErrorBoundary, ErrorBoundaryAlert } from './ErrorBoundary/ErrorBoundary';
|
||||
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;
|
||||
autoFocus?: boolean;
|
||||
openMenuOnFocus?: boolean;
|
||||
showLoading?: boolean;
|
||||
}
|
||||
|
||||
export class DataSourcePicker extends PureComponent<Props> {
|
||||
@ -35,7 +36,7 @@ export class DataSourcePicker extends PureComponent<Props> {
|
||||
};
|
||||
|
||||
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<Props> {
|
||||
label: current.name,
|
||||
value: current.name,
|
||||
imgUrl: current.meta.info.logos.small,
|
||||
loading: showLoading,
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -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<ExploreProps> {
|
||||
StartPage,
|
||||
datasourceInstance,
|
||||
datasourceError,
|
||||
datasourceLoading,
|
||||
datasourceMissing,
|
||||
exploreId,
|
||||
showingStartPage,
|
||||
@ -272,7 +270,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
||||
return (
|
||||
<div className={exploreClass} ref={this.getRef}>
|
||||
<ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} />
|
||||
{datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
|
||||
{datasourceMissing ? this.renderEmptyState() : null}
|
||||
|
||||
<FadeIn duration={datasourceError ? 150 : 5} in={datasourceError ? true : false}>
|
||||
@ -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,
|
||||
|
@ -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<Props, {}> {
|
||||
isLive,
|
||||
isPaused,
|
||||
originPanelId,
|
||||
datasourceLoading,
|
||||
} = this.props;
|
||||
|
||||
const styles = getStyles();
|
||||
@ -193,6 +195,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
||||
onChange={this.onChangeDatasource}
|
||||
datasources={exploreDatasources}
|
||||
current={selectedDatasource}
|
||||
showLoading={datasourceLoading}
|
||||
/>
|
||||
</div>
|
||||
{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,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -318,6 +318,9 @@ Array [
|
||||
<div
|
||||
className="gf-form-select-box__img-value"
|
||||
>
|
||||
<div
|
||||
className="css-zyq2zu"
|
||||
/>
|
||||
stackdriver auto
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user