mirror of
https://github.com/grafana/grafana.git
synced 2026-07-30 00:08:10 -05:00
InlineForms: Changes to make inline forms more flexible for query editors (#29782)
* Chore: Remove internal import for QueryEditorRow * WIP inline forms fixes * Added segment group component * Added component status doc tag * Chore: add as prop to InlineLabel to control rendered element (#29876) * Use div for InlineLabel & fix iconbuttons positioning Co-authored-by: Elfo404 <gio.ricci@grafana.com>
This commit is contained in:
co-authored by
Elfo404
parent
fac34f65c9
commit
1506ec7bfc
@@ -59,19 +59,11 @@ const getStyles = (theme: GrafanaTheme, grow?: boolean) => {
|
||||
container: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
flex: ${grow ? 1 : 0} 0 auto;
|
||||
margin: 0 ${theme.spacing.xs} ${theme.spacing.xs} 0;
|
||||
`,
|
||||
wrapper: css`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
`,
|
||||
|
||||
fillContainer: css`
|
||||
flex-grow: 1;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { FC, HTMLProps, ReactNode } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import { useStyles } from '../../themes';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
|
||||
export interface Props extends Omit<HTMLProps<HTMLDivElement>, 'css'> {
|
||||
children: ReactNode | ReactNode[];
|
||||
@@ -15,7 +16,7 @@ export const InlineFieldRow: FC<Props> = ({ children, className, ...htmlProps })
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = () => {
|
||||
const getStyles = (theme: GrafanaTheme) => {
|
||||
return {
|
||||
container: css`
|
||||
label: InlineFieldRow;
|
||||
|
||||
@@ -18,21 +18,31 @@ export interface Props extends Omit<LabelProps, 'css' | 'description' | 'categor
|
||||
/** @deprecated */
|
||||
/** This prop is deprecated and is not used anymore */
|
||||
isInvalid?: boolean;
|
||||
/** @beta */
|
||||
/** Controls which element the InlineLabel should be rendered into */
|
||||
as?: React.ElementType;
|
||||
}
|
||||
|
||||
export const InlineLabel: FunctionComponent<Props> = ({ children, className, tooltip, width, ...rest }) => {
|
||||
export const InlineLabel: FunctionComponent<Props> = ({
|
||||
children,
|
||||
className,
|
||||
tooltip,
|
||||
width,
|
||||
as: Component = 'label',
|
||||
...rest
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const styles = getInlineLabelStyles(theme, width);
|
||||
|
||||
return (
|
||||
<label className={cx(styles.label, className)} {...rest}>
|
||||
<Component className={cx(styles.label, className)} {...rest}>
|
||||
{children}
|
||||
{tooltip && (
|
||||
<Tooltip placement="top" content={tooltip} theme="info">
|
||||
<Icon name="info-circle" size="sm" className={styles.icon} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</label>
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, { FC } from 'react';
|
||||
import { cx, css } from 'emotion';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { useTheme } from '../../themes';
|
||||
|
||||
export interface Props {
|
||||
grow?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** @beta */
|
||||
export const InlineSegmentGroup: FC<Props> = ({ children, className, grow, ...htmlProps }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme, grow);
|
||||
|
||||
return (
|
||||
<div className={cx(styles.container, className)} {...htmlProps}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
InlineSegmentGroup.displayName = 'InlineSegmentGroup';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme, grow?: boolean) => {
|
||||
return {
|
||||
container: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
flex: ${grow ? 1 : 0} 0 auto;
|
||||
margin-bottom: ${theme.spacing.xs};
|
||||
`,
|
||||
};
|
||||
};
|
||||
@@ -152,6 +152,7 @@ export { Legend } from './Forms/Legend';
|
||||
export { FieldSet } from './Forms/FieldSet';
|
||||
export { FieldValidationMessage } from './Forms/FieldValidationMessage';
|
||||
export { InlineField } from './Forms/InlineField';
|
||||
export { InlineSegmentGroup } from './Forms/InlineSegmentGroup';
|
||||
export { InlineLabel } from './Forms/InlineLabel';
|
||||
export { InlineFieldRow } from './Forms/InlineFieldRow';
|
||||
export { FieldArray } from './Forms/FieldArray';
|
||||
|
||||
+17
-15
@@ -1,5 +1,5 @@
|
||||
import { MetricFindValue, SelectableValue } from '@grafana/data';
|
||||
import { Segment, SegmentAsync } from '@grafana/ui';
|
||||
import { InlineSegmentGroup, Segment, SegmentAsync } from '@grafana/ui';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { useDispatch } from '../../../hooks/useStatelessReducer';
|
||||
import { useDatasource } from '../ElasticsearchQueryContext';
|
||||
@@ -53,22 +53,24 @@ export const BucketAggregationEditor: FunctionComponent<QueryMetricEditorProps>
|
||||
|
||||
return (
|
||||
<>
|
||||
<Segment
|
||||
className={segmentStyles}
|
||||
options={bucketAggOptions}
|
||||
onChange={e => dispatch(changeBucketAggregationType(value.id, e.value!))}
|
||||
value={toOption(value)}
|
||||
/>
|
||||
|
||||
{isBucketAggregationWithField(value) && (
|
||||
<SegmentAsync
|
||||
<InlineSegmentGroup>
|
||||
<Segment
|
||||
className={segmentStyles}
|
||||
loadOptions={getFields}
|
||||
onChange={e => dispatch(changeBucketAggregationField(value.id, e.value))}
|
||||
placeholder="Select Field"
|
||||
value={value.field}
|
||||
options={bucketAggOptions}
|
||||
onChange={e => dispatch(changeBucketAggregationType(value.id, e.value!))}
|
||||
value={toOption(value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isBucketAggregationWithField(value) && (
|
||||
<SegmentAsync
|
||||
className={segmentStyles}
|
||||
loadOptions={getFields}
|
||||
onChange={e => dispatch(changeBucketAggregationField(value.id, e.value))}
|
||||
placeholder="Select Field"
|
||||
value={value.field}
|
||||
/>
|
||||
)}
|
||||
</InlineSegmentGroup>
|
||||
|
||||
<SettingsEditor bucketAgg={value} />
|
||||
</>
|
||||
|
||||
+25
-23
@@ -1,5 +1,5 @@
|
||||
import { MetricFindValue, SelectableValue } from '@grafana/data';
|
||||
import { Segment, SegmentAsync, useTheme } from '@grafana/ui';
|
||||
import { InlineSegmentGroup, Segment, SegmentAsync, useTheme } from '@grafana/ui';
|
||||
import { cx } from 'emotion';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { useDatasource, useQuery } from '../ElasticsearchQueryContext';
|
||||
@@ -88,31 +88,33 @@ export const MetricEditor: FunctionComponent<Props> = ({ value }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Segment
|
||||
className={cx(styles.color, segmentStyles)}
|
||||
options={getTypeOptions(previousMetrics, datasource.esVersion)}
|
||||
onChange={e => dispatch(changeMetricType(value.id, e.value!))}
|
||||
value={toOption(value)}
|
||||
/>
|
||||
|
||||
{isMetricAggregationWithField(value) && !isPipelineAggregation(value) && (
|
||||
<SegmentAsync
|
||||
<InlineSegmentGroup>
|
||||
<Segment
|
||||
className={cx(styles.color, segmentStyles)}
|
||||
loadOptions={getFields}
|
||||
onChange={e => dispatch(changeMetricField(value.id, e.value!))}
|
||||
placeholder="Select Field"
|
||||
value={value.field}
|
||||
options={getTypeOptions(previousMetrics, datasource.esVersion)}
|
||||
onChange={e => dispatch(changeMetricType(value.id, e.value!))}
|
||||
value={toOption(value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isPipelineAggregation(value) && !isPipelineAggregationWithMultipleBucketPaths(value) && (
|
||||
<MetricPicker
|
||||
className={cx(styles.color, segmentStyles)}
|
||||
onChange={e => dispatch(changeMetricField(value.id, e.value?.id!))}
|
||||
options={previousMetrics}
|
||||
value={value.field}
|
||||
/>
|
||||
)}
|
||||
{isMetricAggregationWithField(value) && !isPipelineAggregation(value) && (
|
||||
<SegmentAsync
|
||||
className={cx(styles.color, segmentStyles)}
|
||||
loadOptions={getFields}
|
||||
onChange={e => dispatch(changeMetricField(value.id, e.value!))}
|
||||
placeholder="Select Field"
|
||||
value={value.field}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isPipelineAggregation(value) && !isPipelineAggregationWithMultipleBucketPaths(value) && (
|
||||
<MetricPicker
|
||||
className={cx(styles.color, segmentStyles)}
|
||||
onChange={e => dispatch(changeMetricField(value.id, e.value?.id!))}
|
||||
options={previousMetrics}
|
||||
value={value.field}
|
||||
/>
|
||||
)}
|
||||
</InlineSegmentGroup>
|
||||
|
||||
{isMetricAggregationWithSettings(value) && <SettingsEditor metric={value} previousMetrics={previousMetrics} />}
|
||||
</>
|
||||
|
||||
+31
-33
@@ -1,6 +1,5 @@
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { IconButton, stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { getInlineLabelStyles } from '@grafana/ui/src/components/Forms/InlineLabel';
|
||||
import { IconButton, InlineFieldRow, InlineLabel, InlineSegmentGroup, stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { css } from 'emotion';
|
||||
import { noop } from 'lodash';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
@@ -23,44 +22,43 @@ export const QueryEditorRow: FunctionComponent<Props> = ({
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<fieldset className={styles.root}>
|
||||
<div className={getInlineLabelStyles(theme, 17).label}>
|
||||
<legend className={styles.label}>{label}</legend>
|
||||
{onHideClick && (
|
||||
<IconButton
|
||||
name={hidden ? 'eye-slash' : 'eye'}
|
||||
onClick={onHideClick}
|
||||
surface="header"
|
||||
size="sm"
|
||||
aria-pressed={hidden}
|
||||
aria-label="hide metric"
|
||||
className={styles.icon}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
name="trash-alt"
|
||||
surface="header"
|
||||
size="sm"
|
||||
className={styles.icon}
|
||||
onClick={onRemoveClick || noop}
|
||||
disabled={!onRemoveClick}
|
||||
aria-label="remove metric"
|
||||
/>
|
||||
</div>
|
||||
<InlineFieldRow>
|
||||
<InlineSegmentGroup>
|
||||
<InlineLabel width={17} as="div">
|
||||
<span>{label}</span>
|
||||
<span className={styles.iconWrapper}>
|
||||
{onHideClick && (
|
||||
<IconButton
|
||||
name={hidden ? 'eye-slash' : 'eye'}
|
||||
onClick={onHideClick}
|
||||
surface="header"
|
||||
size="sm"
|
||||
aria-pressed={hidden}
|
||||
aria-label="hide metric"
|
||||
className={styles.icon}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
name="trash-alt"
|
||||
surface="header"
|
||||
size="sm"
|
||||
className={styles.icon}
|
||||
onClick={onRemoveClick || noop}
|
||||
disabled={!onRemoveClick}
|
||||
aria-label="remove metric"
|
||||
/>
|
||||
</span>
|
||||
</InlineLabel>
|
||||
</InlineSegmentGroup>
|
||||
{children}
|
||||
</fieldset>
|
||||
</InlineFieldRow>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
root: css`
|
||||
iconWrapper: css`
|
||||
display: flex;
|
||||
margin-bottom: ${theme.spacing.xs};
|
||||
`,
|
||||
label: css`
|
||||
font-size: ${theme.typography.size.sm};
|
||||
margin: 0;
|
||||
`,
|
||||
icon: css`
|
||||
color: ${theme.colors.textWeak};
|
||||
|
||||
+14
-13
@@ -1,5 +1,5 @@
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { Icon, stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { Icon, InlineSegmentGroup, stylesFactory, useTheme } from '@grafana/ui';
|
||||
import { css, cx } from 'emotion';
|
||||
import React, { FunctionComponent, useState } from 'react';
|
||||
import { segmentStyles } from './styles';
|
||||
@@ -32,21 +32,22 @@ interface Props {
|
||||
|
||||
export const SettingsEditorContainer: FunctionComponent<Props> = ({ label, children, hidden = false }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const styles = getStyles(useTheme(), hidden);
|
||||
|
||||
return (
|
||||
<div className={cx(styles.wrapper)}>
|
||||
<button
|
||||
className={cx('gf-form-label query-part', styles.button, segmentStyles)}
|
||||
onClick={() => setOpen(!open)}
|
||||
aria-expanded={open}
|
||||
>
|
||||
<Icon name={open ? 'angle-down' : 'angle-right'} aria-hidden="true" className={styles.icon} />
|
||||
{label}
|
||||
</button>
|
||||
<InlineSegmentGroup>
|
||||
<div className={cx(styles.wrapper)}>
|
||||
<button
|
||||
className={cx('gf-form-label query-part', styles.button, segmentStyles)}
|
||||
onClick={() => setOpen(!open)}
|
||||
aria-expanded={open}
|
||||
>
|
||||
<Icon name={open ? 'angle-down' : 'angle-right'} aria-hidden="true" className={styles.icon} />
|
||||
{label}
|
||||
</button>
|
||||
|
||||
{open && <div className={styles.settingsWrapper}>{children}</div>}
|
||||
</div>
|
||||
{open && <div className={styles.settingsWrapper}>{children}</div>}
|
||||
</div>
|
||||
</InlineSegmentGroup>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user