Explore: Move data source loader into the select (#19465)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 />;
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user