mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
c2749052d7
commit
862f2e4821
112
packages/grafana-ui/src/components/Segment/Segment.story.tsx
Normal file
112
packages/grafana-ui/src/components/Segment/Segment.story.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
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>
|
||||
);
|
||||
});
|
34
packages/grafana-ui/src/components/Segment/Segment.tsx
Normal file
34
packages/grafana-ui/src/components/Segment/Segment.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
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>
|
||||
);
|
||||
});
|
54
packages/grafana-ui/src/components/Segment/SegmentAsync.tsx
Normal file
54
packages/grafana-ui/src/components/Segment/SegmentAsync.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
45
packages/grafana-ui/src/components/Segment/SegmentSelect.tsx
Normal file
45
packages/grafana-ui/src/components/Segment/SegmentSelect.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
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>
|
||||
);
|
||||
}
|
5
packages/grafana-ui/src/components/Segment/index.ts
Normal file
5
packages/grafana-ui/src/components/Segment/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export { Segment } from './Segment';
|
||||
export { SegmentAsync } from './SegmentAsync';
|
||||
export { SegmentSelect } from './SegmentSelect';
|
||||
export { SegmentProps } from './types';
|
||||
export { useExpandableLabel } from './useExpandableLabel';
|
8
packages/grafana-ui/src/components/Segment/types.ts
Normal file
8
packages/grafana-ui/src/components/Segment/types.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
export interface SegmentProps<T> {
|
||||
onChange: (item: T) => void;
|
||||
value?: T;
|
||||
Component?: ReactElement;
|
||||
className?: string;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
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/';
|
||||
|
Loading…
Reference in New Issue
Block a user