Commit 862f2e48 by Erik Sundell Committed by GitHub

React group by segment poc (#19436)

* Add simple group by component

* Make segment generic

* Refactoring segments. Add support for lazy loading

* Use base props

* Add example with grouped options

* Move examples to storybook

* Fixes according to pr feedback

* Cleanup

* added className

* Fixes according to feed back

* Add query string to api so that search can be imlemented in the future
parent c2749052
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { SelectableValue } from '@grafana/data';
import { Segment } from './';
import { UseState } from '../../utils/storybook/UseState';
const SegmentStories = storiesOf('UI/Segment/SegmentSync', module);
const AddButton = (
<a className="gf-form-label query-part">
<i className="fa fa-plus" />
</a>
);
const toOption = (value: any) => ({ label: value, value: value });
SegmentStories.add('Array Options', () => {
const options = ['Option1', 'Option2', 'OptionWithLooongLabel', 'Option4'].map(toOption);
return (
<UseState initialState={options[0].value}>
{(value, updateValue) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<Segment
value={value}
options={options}
onChange={(value: SelectableValue<string>) => {
updateValue(value);
action('Segment value changed')(value);
}}
/>
<Segment
Component={AddButton}
onChange={(value: SelectableValue<string>) => action('New value added')(value)}
options={options}
/>
</div>
</>
)}
</UseState>
);
});
const groupedOptions = [
{ label: 'Names', options: ['Jane', 'Tom', 'Lisa'].map(toOption) },
{ label: 'Prime', options: [2, 3, 5, 7, 11, 13].map(toOption) },
];
SegmentStories.add('Grouped Array Options', () => {
return (
<UseState initialState={groupedOptions[0].options[0].value}>
{(value, updateValue) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<Segment
value={value}
options={groupedOptions}
onChange={(value: SelectableValue<string>) => {
updateValue(value);
action('Segment value changed')(value);
}}
/>
<Segment
Component={AddButton}
onChange={value => action('New value added')(value)}
options={groupedOptions}
/>
</div>
</>
)}
</UseState>
);
});
const CustomLabelComponent = ({ value }: any) => <div className="gf-form-label">custom({value})</div>;
SegmentStories.add('Custom Label Field', () => {
return (
<UseState initialState={groupedOptions[0].options[0].value}>
{(value, setValue) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<Segment
Component={<CustomLabelComponent value={value} />}
options={groupedOptions}
onChange={(value: SelectableValue<string>) => {
setValue(value);
action('Segment value changed')(value);
}}
/>
<Segment
Component={AddButton}
onChange={value => action('New value added')(value)}
options={groupedOptions}
/>
</div>
</>
)}
</UseState>
);
});
import React from 'react';
import { cx } from 'emotion';
import { SelectableValue } from '@grafana/data';
import { SegmentSelect, useExpandableLabel, SegmentProps } from './';
export interface SegmentSyncProps<T> extends SegmentProps<T> {
options: Array<SelectableValue<T>>;
}
export function Segment<T>({
options,
value,
onChange,
Component,
className,
}: React.PropsWithChildren<SegmentSyncProps<T>>) {
const [Label, width, expanded, setExpanded] = useExpandableLabel(false);
if (!expanded) {
return <Label Component={Component || <a className={cx('gf-form-label', 'query-part', className)}>{value}</a>} />;
}
return (
<SegmentSelect
width={width}
options={options}
onClickOutside={() => setExpanded(false)}
onChange={value => {
setExpanded(false);
onChange(value);
}}
/>
);
}
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { SelectableValue } from '@grafana/data';
const SegmentStories = storiesOf('UI/Segment/SegmentAsync', module);
import { SegmentAsync } from './';
import { UseState } from '../../utils/storybook/UseState';
const AddButton = (
<a className="gf-form-label query-part">
<i className="fa fa-plus" />
</a>
);
const toOption = (value: any) => ({ label: value, value: value });
const loadOptions = (options: any): Promise<Array<SelectableValue<string>>> =>
new Promise(res => setTimeout(() => res(options), 2000));
SegmentStories.add('Array Options', () => {
const options = ['Option1', 'Option2', 'OptionWithLooongLabel', 'Option4'].map(toOption);
return (
<UseState initialState={options[0].value}>
{(value, updateValue) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<SegmentAsync
value={value}
loadOptions={() => loadOptions(options)}
onChange={value => {
updateValue(value);
action('Segment value changed')(value);
}}
/>
<SegmentAsync
Component={AddButton}
onChange={value => action('New value added')(value)}
loadOptions={() => loadOptions(options)}
/>
</div>
</>
)}
</UseState>
);
});
const groupedOptions = [
{ label: 'Names', options: ['Jane', 'Tom', 'Lisa'].map(toOption) },
{ label: 'Prime', options: [2, 3, 5, 7, 11, 13].map(toOption) },
];
SegmentStories.add('Grouped Array Options', () => {
return (
<UseState initialState={groupedOptions[0].options[0].value}>
{(value, updateValue) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<SegmentAsync
value={value}
loadOptions={() => loadOptions(groupedOptions)}
onChange={value => {
updateValue(value);
action('Segment value changed')(value);
}}
/>
<SegmentAsync
Component={AddButton}
onChange={value => action('New value added')(value)}
loadOptions={() => loadOptions(groupedOptions)}
/>
</div>
</>
)}
</UseState>
);
});
const CustomLabelComponent = ({ value }: any) => <div className="gf-form-label">custom({value})</div>;
SegmentStories.add('Custom Label Field', () => {
return (
<UseState initialState={groupedOptions[0].options[0].value}>
{(value, updateValue) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<SegmentAsync
Component={<CustomLabelComponent value={value} />}
loadOptions={() => loadOptions(groupedOptions)}
onChange={value => {
updateValue(value);
action('Segment value changed')(value);
}}
/>
<SegmentAsync
Component={AddButton}
onChange={value => action('New value added')(value)}
loadOptions={() => loadOptions(groupedOptions)}
/>
</div>
</>
)}
</UseState>
);
});
import React, { useState } from 'react';
import { cx } from 'emotion';
import { SegmentSelect } from './SegmentSelect';
import { SelectableValue } from '@grafana/data';
import { useExpandableLabel, SegmentProps } from '.';
export interface SegmentAsyncProps<T> extends SegmentProps<T> {
loadOptions: (query?: string) => Promise<Array<SelectableValue<T>>>;
}
export function SegmentAsync<T>({
value,
onChange,
loadOptions,
Component,
className,
}: React.PropsWithChildren<SegmentAsyncProps<T>>) {
const [selectPlaceholder, setSelectPlaceholder] = useState<string>('');
const [loadedOptions, setLoadedOptions] = useState<Array<SelectableValue<T>>>([]);
const [Label, width, expanded, setExpanded] = useExpandableLabel(false);
if (!expanded) {
return (
<Label
onClick={async () => {
setSelectPlaceholder('Loading options...');
const opts = await loadOptions();
setLoadedOptions(opts);
setSelectPlaceholder(opts.length ? '' : 'No options found');
}}
Component={Component || <a className={cx('gf-form-label', 'query-part', className)}>{value}</a>}
/>
);
}
return (
<SegmentSelect
width={width}
options={loadedOptions}
noOptionsMessage={selectPlaceholder}
onClickOutside={() => {
setSelectPlaceholder('');
setLoadedOptions([]);
setExpanded(false);
}}
onChange={value => {
setSelectPlaceholder('');
setLoadedOptions([]);
setExpanded(false);
onChange(value);
}}
/>
);
}
import React, { useRef } from 'react';
import { css, cx } from 'emotion';
import useClickAway from 'react-use/lib/useClickAway';
import { SelectableValue } from '@grafana/data';
import { Select } from '../Select/Select';
export interface Props<T> {
options: Array<SelectableValue<T>>;
onChange: (value: T) => void;
onClickOutside: () => void;
width: number;
noOptionsMessage?: string;
}
export function SegmentSelect<T>({
options = [],
onChange,
onClickOutside,
width,
noOptionsMessage = '',
}: React.PropsWithChildren<Props<T>>) {
const ref = useRef(null);
useClickAway(ref, () => {
onClickOutside();
});
return (
<div ref={ref}>
<Select
className={cx(
css`
width: ${width > 120 ? width : 120}px;
`
)}
noOptionsMessage={() => noOptionsMessage}
placeholder=""
autoFocus={true}
isOpen={true}
onChange={({ value }) => onChange(value!)}
options={options}
/>
</div>
);
}
export { Segment } from './Segment';
export { SegmentAsync } from './SegmentAsync';
export { SegmentSelect } from './SegmentSelect';
export { SegmentProps } from './types';
export { useExpandableLabel } from './useExpandableLabel';
import { ReactElement } from 'react';
export interface SegmentProps<T> {
onChange: (item: T) => void;
value?: T;
Component?: ReactElement;
className?: string;
}
import React, { useState, useRef, ReactElement } from 'react';
export const useExpandableLabel = (initialExpanded: boolean) => {
const ref = useRef<HTMLDivElement>(null);
const [expanded, setExpanded] = useState(initialExpanded);
const [width, setWidth] = useState();
const Label = ({ Component, onClick }: { Component: ReactElement; onClick: () => void }) => (
<div
className="gf-form"
ref={ref}
onClick={() => {
setExpanded(true);
if (ref && ref.current) {
setWidth(ref.current.clientWidth);
}
if (onClick) {
onClick();
}
}}
>
{Component}
</div>
);
return [Label, width, expanded, setExpanded];
};
......@@ -89,3 +89,6 @@ export { ErrorBoundary, ErrorBoundaryAlert } from './ErrorBoundary/ErrorBoundary
export { AlphaNotice } from './AlphaNotice/AlphaNotice';
export { Spinner } from './Spinner/Spinner';
export { FadeTransition } from './transitions/FadeTransition';
// Segment
export { Segment, SegmentAsync, SegmentSelect } from './Segment/';
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment