Grafana-UI: Editor UI components (#41136)

* Grafana-UI: Update theme.spacing to support string value when called with just one arugment

This allows theme.spacing("auto") to be valid

* Grafana-UI: Support width="auto" for Select component

This allows for inline Selects that are sized based on their content,
rather than occupying block-width

* Add toOption for creating Select options to @grafana/data

* Add test util
This commit is contained in:
Josh Hunt 2021-11-01 11:06:28 +00:00 committed by GitHub
parent 7b15cd0ed2
commit 419c465edf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 53 additions and 22 deletions

View File

@ -17,7 +17,7 @@ export type ThemeSpacingArgument = number | string;
* tslint:disable:unified-signatures */ * tslint:disable:unified-signatures */
export interface ThemeSpacing { export interface ThemeSpacing {
(): string; (): string;
(value: number): string; (value: ThemeSpacingArgument): string;
(topBottom: ThemeSpacingArgument, rightLeft: ThemeSpacingArgument): string; (topBottom: ThemeSpacingArgument, rightLeft: ThemeSpacingArgument): string;
(top: ThemeSpacingArgument, rightLeft: ThemeSpacingArgument, bottom: ThemeSpacingArgument): string; (top: ThemeSpacingArgument, rightLeft: ThemeSpacingArgument, bottom: ThemeSpacingArgument): string;
( (

View File

@ -12,6 +12,7 @@ export * from './namedColorsPalette';
export * from './series'; export * from './series';
export * from './binaryOperators'; export * from './binaryOperators';
export * from './nodeGraph'; export * from './nodeGraph';
export * from './selectUtils';
export { PanelOptionsEditorBuilder, FieldConfigEditorBuilder } from './OptionsUIBuilders'; export { PanelOptionsEditorBuilder, FieldConfigEditorBuilder } from './OptionsUIBuilders';
export { arrayUtils }; export { arrayUtils };
export { getFlotPairs, getFlotPairsConstant } from './flotPairs'; export { getFlotPairs, getFlotPairsConstant } from './flotPairs';

View File

@ -0,0 +1,3 @@
import { SelectableValue } from '../types';
export const toOption = (value: string): SelectableValue<string> => ({ label: value, value });

View File

@ -295,6 +295,29 @@ AutoMenuPlacement.args = {
menuPlacement: auto, menuPlacement: auto,
}; };
export const WidthAuto: Story = (args) => {
const [value, setValue] = useState<SelectableValue<string>>();
return (
<>
<div style={{ width: '100%' }}>
<Select
menuShouldPortal
options={generateOptions()}
value={value}
onChange={(v) => {
setValue(v);
action('onChange')(v);
}}
prefix={getPrefix(args.icon)}
{...args}
width="auto"
/>
</div>
</>
);
};
export const CustomValueCreation: Story = (args) => { export const CustomValueCreation: Story = (args) => {
const [value, setValue] = useState<SelectableValue<string>>(); const [value, setValue] = useState<SelectableValue<string>>();
const [customOptions, setCustomOptions] = useState<Array<SelectableValue<string>>>([]); const [customOptions, setCustomOptions] = useState<Array<SelectableValue<string>>>([]);

View File

@ -260,13 +260,18 @@ export function SelectBase<T>({
css` css`
display: inline-block; display: inline-block;
color: ${theme.colors.text.disabled}; color: ${theme.colors.text.disabled};
position: absolute;
top: 50%;
transform: translateY(-50%);
box-sizing: border-box; box-sizing: border-box;
line-height: 1; line-height: 1;
white-space: nowrap; white-space: nowrap;
` `,
// When width: auto, the placeholder must take up space in the Select otherwise the width collapses down
width !== 'auto' &&
css`
position: absolute;
top: 50%;
transform: translateY(-50%);
`
)} )}
> >
{props.children} {props.children}
@ -355,8 +360,8 @@ export function SelectBase<T>({
zIndex: theme.zIndex.dropdown, zIndex: theme.zIndex.dropdown,
}), }),
container: () => ({ container: () => ({
position: 'relative', width: width ? theme.spacing(width) : '100%',
width: width ? `${8 * width}px` : '100%', display: width === 'auto' ? 'inline-flex' : 'flex',
}), }),
option: (provided: any, state: any) => ({ option: (provided: any, state: any) => ({
...provided, ...provided,

View File

@ -6,16 +6,16 @@ import { css, cx } from '@emotion/css';
import { stylesFactory } from '../../themes'; import { stylesFactory } from '../../themes';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { focusCss } from '../../themes/mixins'; import { focusCss } from '../../themes/mixins';
import { components, ContainerProps, GroupTypeBase } from 'react-select'; import { components, ContainerProps as BaseContainerProps, GroupTypeBase } from 'react-select';
// isFocus prop is actually available, but its not in the types for the version we have. // isFocus prop is actually available, but its not in the types for the version we have.
interface CorrectContainerProps<Option, isMulti extends boolean, Group extends GroupTypeBase<Option>> export interface ContainerProps<Option, isMulti extends boolean, Group extends GroupTypeBase<Option>>
extends ContainerProps<Option, isMulti, Group> { extends BaseContainerProps<Option, isMulti, Group> {
isFocused: boolean; isFocused: boolean;
} }
export const SelectContainer = <Option, isMulti extends boolean, Group extends GroupTypeBase<Option>>( export const SelectContainer = <Option, isMulti extends boolean, Group extends GroupTypeBase<Option>>(
props: CorrectContainerProps<Option, isMulti, Group> props: ContainerProps<Option, isMulti, Group>
) => { ) => {
const { const {
isDisabled, isDisabled,
@ -50,7 +50,7 @@ const getSelectContainerStyles = stylesFactory(
css` css`
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
display: flex; /* The display property is set by the styles prop in SelectBase because it's dependant on the width prop */
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;

View File

@ -5,3 +5,7 @@ export const selectOptionInTest = async (
input: HTMLElement, input: HTMLElement,
optionOrOptions: string | RegExp | Array<string | RegExp> optionOrOptions: string | RegExp | Array<string | RegExp>
) => await select(input, optionOrOptions, { container: document.body }); ) => await select(input, optionOrOptions, { container: document.body });
// Finds the parent of the Select so you can assert if it has a value
export const getSelectParent = (input: HTMLElement) =>
input.parentElement?.parentElement?.parentElement?.parentElement?.parentElement;

View File

@ -69,7 +69,7 @@ export interface SelectCommonProps<T> {
tabSelectsValue?: boolean; tabSelectsValue?: boolean;
value?: SelectValue<T> | null; value?: SelectValue<T> | null;
/** Sets the width to a multiple of 8px. Should only be used with inline forms. Setting width of the container is preferred in other cases.*/ /** Sets the width to a multiple of 8px. Should only be used with inline forms. Setting width of the container is preferred in other cases.*/
width?: number; width?: number | 'auto';
isOptionDisabled?: () => boolean; isOptionDisabled?: () => boolean;
/** allowCustomValue must be enabled. Determines whether the "create new" option should be displayed based on the current input value, select value and options array. */ /** allowCustomValue must be enabled. Determines whether the "create new" option should be displayed based on the current input value, select value and options array. */
isValidNewOption?: ( isValidNewOption?: (

View File

@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import { LegacyForms } from '@grafana/ui'; import { LegacyForms } from '@grafana/ui';
import { TemplateSrv } from '@grafana/runtime'; import { TemplateSrv } from '@grafana/runtime';
import { SelectableValue } from '@grafana/data'; import { SelectableValue, toOption } from '@grafana/data';
import CloudMonitoringDatasource from '../datasource'; import CloudMonitoringDatasource from '../datasource';
import { AnnotationsHelp, LabelFilter, Metrics, Project, QueryEditorRow } from './'; import { AnnotationsHelp, LabelFilter, Metrics, Project, QueryEditorRow } from './';
import { toOption } from '../functions';
import { AnnotationTarget, EditorMode, MetricDescriptor, MetricKind } from '../types'; import { AnnotationTarget, EditorMode, MetricDescriptor, MetricKind } from '../types';
const { Input } = LegacyForms; const { Input } = LegacyForms;

View File

@ -1,10 +1,10 @@
import React, { FunctionComponent, useCallback, useMemo } from 'react'; import React, { FunctionComponent, useCallback, useMemo } from 'react';
import { flatten } from 'lodash'; import { flatten } from 'lodash';
import { SelectableValue } from '@grafana/data'; import { SelectableValue, toOption } from '@grafana/data';
import { CustomControlProps } from '@grafana/ui/src/components/Select/types'; import { CustomControlProps } from '@grafana/ui/src/components/Select/types';
import { Button, HorizontalGroup, Select, VerticalGroup } from '@grafana/ui'; import { Button, HorizontalGroup, Select, VerticalGroup } from '@grafana/ui';
import { labelsToGroupedOptions, stringArrayToFilters, toOption } from '../functions'; import { labelsToGroupedOptions, stringArrayToFilters } from '../functions';
import { Filter } from '../types'; import { Filter } from '../types';
import { SELECT_WIDTH } from '../constants'; import { SELECT_WIDTH } from '../constants';
import { QueryEditorRow } from '.'; import { QueryEditorRow } from '.';

View File

@ -1,13 +1,12 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { QueryEditorProps } from '@grafana/data'; import { QueryEditorProps, toOption } from '@grafana/data';
import { Button, Select } from '@grafana/ui'; import { Button, Select } from '@grafana/ui';
import { MetricQueryEditor, SLOQueryEditor, QueryEditorRow } from './'; import { MetricQueryEditor, SLOQueryEditor, QueryEditorRow } from './';
import { CloudMonitoringQuery, MetricQuery, QueryType, SLOQuery, EditorMode } from '../types'; import { CloudMonitoringQuery, MetricQuery, QueryType, SLOQuery, EditorMode } from '../types';
import { SELECT_WIDTH, QUERY_TYPES } from '../constants'; import { SELECT_WIDTH, QUERY_TYPES } from '../constants';
import { defaultQuery } from './MetricQueryEditor'; import { defaultQuery } from './MetricQueryEditor';
import { defaultQuery as defaultSLOQuery } from './SLO/SLOQueryEditor'; import { defaultQuery as defaultSLOQuery } from './SLO/SLOQueryEditor';
import { toOption } from '../functions';
import CloudMonitoringDatasource from '../datasource'; import CloudMonitoringDatasource from '../datasource';
export type Props = QueryEditorProps<CloudMonitoringDatasource, CloudMonitoringQuery>; export type Props = QueryEditorProps<CloudMonitoringDatasource, CloudMonitoringQuery>;

View File

@ -1,6 +1,5 @@
import { chunk, flatten, initial, startCase, uniqBy } from 'lodash'; import { chunk, flatten, initial, startCase, uniqBy } from 'lodash';
import { ALIGNMENTS, AGGREGATIONS, SYSTEM_LABELS } from './constants'; import { ALIGNMENTS, AGGREGATIONS, SYSTEM_LABELS } from './constants';
import { SelectableValue } from '@grafana/data';
import CloudMonitoringDatasource from './datasource'; import CloudMonitoringDatasource from './datasource';
import { TemplateSrv, getTemplateSrv } from '@grafana/runtime'; import { TemplateSrv, getTemplateSrv } from '@grafana/runtime';
import { MetricDescriptor, ValueTypes, MetricKind, AlignmentTypes, PreprocessorType, Filter } from './types'; import { MetricDescriptor, ValueTypes, MetricKind, AlignmentTypes, PreprocessorType, Filter } from './types';
@ -118,8 +117,6 @@ export const stringArrayToFilters = (filterArray: string[]) =>
condition, condition,
})); }));
export const toOption = (value: string) => ({ label: value, value } as SelectableValue<string>);
export const formatCloudMonitoringError = (error: any) => { export const formatCloudMonitoringError = (error: any) => {
let message = error.statusText ?? ''; let message = error.statusText ?? '';
if (error.data && error.data.error) { if (error.data && error.data.error) {