Tracing: Add inline validation to time shift configuration fields (#70879)

* Inline validation

* Update invalidTimeShiftError after self review

* Renames and moved err msg

* Update validation

* Remove local state
This commit is contained in:
Joey 2023-07-04 10:49:21 +01:00 committed by GitHub
parent 20b6ae96a3
commit f1338cee60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 252 additions and 118 deletions

View File

@ -0,0 +1,76 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React, { useState } from 'react';
import { invalidTimeShiftError } from '../TraceToLogs/TraceToLogsSettings';
import { IntervalInput } from './IntervalInput';
describe('IntervalInput', () => {
const IntervalInputtWithProps = ({ val }: { val: string }) => {
const [value, setValue] = useState(val);
return (
<IntervalInput
label=""
tooltip=""
value={value}
disabled={false}
onChange={(v) => {
setValue(v);
}}
isInvalidError={invalidTimeShiftError}
/>
);
};
describe('validates time shift correctly', () => {
it('for previosuly saved invalid value', async () => {
render(<IntervalInputtWithProps val="77" />);
expect(screen.getByDisplayValue('77')).toBeInTheDocument();
expect(screen.getByText(invalidTimeShiftError)).toBeInTheDocument();
});
it('for previously saved empty value', async () => {
render(<IntervalInputtWithProps val="" />);
expect(screen.getByPlaceholderText('0')).toBeInTheDocument();
expect(screen.queryByText(invalidTimeShiftError)).not.toBeInTheDocument();
});
it('for empty (valid) value', async () => {
render(<IntervalInputtWithProps val="1ms" />);
await userEvent.clear(screen.getByDisplayValue('1ms'));
await waitFor(() => {
expect(screen.queryByText(invalidTimeShiftError)).not.toBeInTheDocument();
});
});
it('for valid value', async () => {
render(<IntervalInputtWithProps val="10ms" />);
expect(screen.queryByText(invalidTimeShiftError)).not.toBeInTheDocument();
const input = screen.getByDisplayValue('10ms');
await userEvent.clear(input);
await userEvent.type(input, '100s');
await waitFor(() => {
expect(screen.queryByText(invalidTimeShiftError)).not.toBeInTheDocument();
});
await userEvent.clear(input);
await userEvent.type(input, '-77ms');
await waitFor(() => {
expect(screen.queryByText(invalidTimeShiftError)).not.toBeInTheDocument();
});
});
it('for invalid value', async () => {
render(<IntervalInputtWithProps val="10ms" />);
const input = screen.getByDisplayValue('10ms');
await userEvent.clear(input);
await userEvent.type(input, 'abc');
await waitFor(() => {
expect(screen.queryByText(invalidTimeShiftError)).toBeInTheDocument();
});
});
});
});

View File

@ -0,0 +1,53 @@
import React, { useState } from 'react';
import { useDebounce } from 'react-use';
import { InlineField, InlineFieldRow, Input } from '@grafana/ui';
import { validateInterval } from './validation';
interface Props {
label: string;
tooltip: string;
value: string;
onChange: (val: string) => void;
isInvalidError: string;
disabled?: boolean;
}
export function IntervalInput(props: Props) {
const [intervalIsInvalid, setIntervalIsInvalid] = useState(() => {
return props.value ? validateInterval(props.value) : false;
});
useDebounce(
() => {
setIntervalIsInvalid(validateInterval(props.value));
},
500,
[props.value]
);
return (
<InlineFieldRow>
<InlineField
label={props.label}
labelWidth={26}
disabled={props.disabled ?? false}
grow
tooltip={props.tooltip}
invalid={intervalIsInvalid}
error={props.isInvalidError}
>
<Input
type="text"
placeholder="0"
width={40}
onChange={(e) => {
props.onChange(e.currentTarget.value);
}}
value={props.value}
/>
</InlineField>
</InlineFieldRow>
);
}

View File

@ -0,0 +1,28 @@
import { validateInterval } from './validation';
describe('Validation', () => {
it('should validate incorrect values correctly', () => {
expect(validateInterval('-')).toBeTruthy();
expect(validateInterval('1')).toBeTruthy();
expect(validateInterval('test')).toBeTruthy();
expect(validateInterval('1ds')).toBeTruthy();
expect(validateInterval('10Ms')).toBeTruthy();
expect(validateInterval('-9999999')).toBeTruthy();
});
it('should validate correct values correctly', () => {
expect(validateInterval('1y')).toBeFalsy();
expect(validateInterval('1M')).toBeFalsy();
expect(validateInterval('1w')).toBeFalsy();
expect(validateInterval('1d')).toBeFalsy();
expect(validateInterval('2h')).toBeFalsy();
expect(validateInterval('4m')).toBeFalsy();
expect(validateInterval('8s')).toBeFalsy();
expect(validateInterval('80ms')).toBeFalsy();
expect(validateInterval('-80ms')).toBeFalsy();
});
it('should not return error if no value provided', () => {
expect(validateInterval('')).toBeFalsy();
});
});

View File

@ -0,0 +1,5 @@
export const validateInterval = (val: string) => {
const intervalRegex = /^(-?\d+(?:\.\d+)?)(ms|[Mwdhmsy])$/;
const matches = val.match(intervalRegex);
return matches || !val ? false : true;
};

View File

@ -7,6 +7,8 @@ import { DataSourcePicker } from '@grafana/runtime';
import { InlineField, InlineFieldRow, Input, InlineSwitch } from '@grafana/ui'; import { InlineField, InlineFieldRow, Input, InlineSwitch } from '@grafana/ui';
import { ConfigDescriptionLink } from 'app/core/components/ConfigDescriptionLink'; import { ConfigDescriptionLink } from 'app/core/components/ConfigDescriptionLink';
import { IntervalInput } from '../IntervalInput/IntervalInput';
import { TagMappingInput } from './TagMappingInput'; import { TagMappingInput } from './TagMappingInput';
// @deprecated use getTraceToLogsOptions to get the v2 version of this config from jsonData // @deprecated use getTraceToLogsOptions to get the v2 version of this config from jsonData
@ -123,15 +125,23 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) {
</InlineField> </InlineField>
</InlineFieldRow> </InlineFieldRow>
<TimeRangeShift <IntervalInput
type={'start'} label={getTimeShiftLabel('start')}
tooltip={getTimeShiftTooltip('start')}
value={traceToLogs.spanStartTimeShift || ''} value={traceToLogs.spanStartTimeShift || ''}
onChange={(val) => updateTracesToLogs({ spanStartTimeShift: val })} onChange={(val) => {
updateTracesToLogs({ spanStartTimeShift: val });
}}
isInvalidError={invalidTimeShiftError}
/> />
<TimeRangeShift <IntervalInput
type={'end'} label={getTimeShiftLabel('end')}
tooltip={getTimeShiftTooltip('end')}
value={traceToLogs.spanEndTimeShift || ''} value={traceToLogs.spanEndTimeShift || ''}
onChange={(val) => updateTracesToLogs({ spanEndTimeShift: val })} onChange={(val) => {
updateTracesToLogs({ spanEndTimeShift: val });
}}
isInvalidError={invalidTimeShiftError}
/> />
<InlineFieldRow> <InlineFieldRow>
@ -222,31 +232,15 @@ function IdFilter(props: IdFilterProps) {
); );
} }
interface TimeRangeShiftProps { export const getTimeShiftLabel = (type: 'start' | 'end') => {
type: 'start' | 'end'; return `Span ${type} time shift`;
value: string; };
onChange: (val: string) => void;
} export const getTimeShiftTooltip = (type: 'start' | 'end') => {
function TimeRangeShift(props: TimeRangeShiftProps) { return `Shifts the ${type} time of the span. Default: 0 (Time units can be used here, for example: 5s, -1m, 3h)`;
return ( };
<InlineFieldRow>
<InlineField export const invalidTimeShiftError = 'Invalid time shift. See tooltip for examples.';
label={`Span ${props.type} time shift`}
labelWidth={26}
grow
tooltip={`Shifts the ${props.type} time of the span. Default: 0 (Time units can be used here, for example: 5s, -1m, 3h)`}
>
<Input
type="text"
placeholder="0"
width={40}
onChange={(e) => props.onChange(e.currentTarget.value)}
value={props.value}
/>
</InlineField>
</InlineFieldRow>
);
}
export const TraceToLogsSection = ({ options, onOptionsChange }: DataSourcePluginOptionsEditorProps) => { export const TraceToLogsSection = ({ options, onOptionsChange }: DataSourcePluginOptionsEditorProps) => {
return ( return (

View File

@ -12,7 +12,9 @@ import { DataSourcePicker } from '@grafana/runtime';
import { Button, InlineField, InlineFieldRow, Input, useStyles2 } from '@grafana/ui'; import { Button, InlineField, InlineFieldRow, Input, useStyles2 } from '@grafana/ui';
import { ConfigDescriptionLink } from '../ConfigDescriptionLink'; import { ConfigDescriptionLink } from '../ConfigDescriptionLink';
import { IntervalInput } from '../IntervalInput/IntervalInput';
import { TagMappingInput } from '../TraceToLogs/TagMappingInput'; import { TagMappingInput } from '../TraceToLogs/TagMappingInput';
import { getTimeShiftLabel, getTimeShiftTooltip, invalidTimeShiftError } from '../TraceToLogs/TraceToLogsSettings';
export interface TraceToMetricsOptions { export interface TraceToMetricsOptions {
datasourceUid?: string; datasourceUid?: string;
@ -76,49 +78,31 @@ export function TraceToMetricsSettings({ options, onOptionsChange }: Props) {
) : null} ) : null}
</InlineFieldRow> </InlineFieldRow>
<InlineFieldRow> <IntervalInput
<InlineField label={getTimeShiftLabel('start')}
label="Span start time shift" tooltip={getTimeShiftTooltip('start')}
labelWidth={26} value={options.jsonData.tracesToMetrics?.spanStartTimeShift || ''}
grow onChange={(val) => {
tooltip="Shifts the start time of the span. Default: 0 (Time units can be used here, for example: 5s, -1m, 3h)" updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
> ...options.jsonData.tracesToMetrics,
<Input spanStartTimeShift: val,
type="text" });
placeholder="0" }}
width={40} isInvalidError={invalidTimeShiftError}
onChange={(v) => />
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
...options.jsonData.tracesToMetrics,
spanStartTimeShift: v.currentTarget.value,
})
}
value={options.jsonData.tracesToMetrics?.spanStartTimeShift || ''}
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow> <IntervalInput
<InlineField label={getTimeShiftLabel('end')}
label="Span end time shift" tooltip={getTimeShiftTooltip('end')}
labelWidth={26} value={options.jsonData.tracesToMetrics?.spanEndTimeShift || ''}
grow onChange={(val) => {
tooltip="Shifts the end time of the span. Default: 0 (Time units can be used here, for example: 5s, -1m, 3h)" updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
> ...options.jsonData.tracesToMetrics,
<Input spanEndTimeShift: val,
type="text" });
placeholder="0" }}
width={40} isInvalidError={invalidTimeShiftError}
onChange={(v) => />
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
...options.jsonData.tracesToMetrics,
spanEndTimeShift: v.currentTarget.value,
})
}
value={options.jsonData.tracesToMetrics?.spanEndTimeShift || ''}
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow> <InlineFieldRow>
<InlineField tooltip="Tags that will be used in the metrics query" label="Tags" labelWidth={26}> <InlineField tooltip="Tags that will be used in the metrics query" label="Tags" labelWidth={26}>

View File

@ -2,7 +2,9 @@ import { css } from '@emotion/css';
import React from 'react'; import React from 'react';
import { DataSourcePluginOptionsEditorProps, GrafanaTheme2, updateDatasourcePluginJsonDataOption } from '@grafana/data'; import { DataSourcePluginOptionsEditorProps, GrafanaTheme2, updateDatasourcePluginJsonDataOption } from '@grafana/data';
import { InlineField, InlineFieldRow, InlineSwitch, Input, useStyles2 } from '@grafana/ui'; import { InlineField, InlineSwitch, useStyles2 } from '@grafana/ui';
import { IntervalInput } from 'app/core/components/IntervalInput/IntervalInput';
import { invalidTimeShiftError } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
import { TempoJsonData } from '../types'; import { TempoJsonData } from '../types';
@ -11,6 +13,14 @@ interface Props extends DataSourcePluginOptionsEditorProps<TempoJsonData> {}
export function QuerySettings({ options, onOptionsChange }: Props) { export function QuerySettings({ options, onOptionsChange }: Props) {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const getLabel = (type: 'start' | 'end') => {
return `Time shift for ${type} of search`;
};
const getTooltip = (type: 'start' | 'end') => {
return `Shifts the ${type} of the time range when searching by TraceID. Searching can return traces that do not fully fall into the search time range, so we recommend using higher time shifts for longer traces. Default: 30m (Time units can be used here, for example: 5s, 1m, 3h`;
};
return ( return (
<div className={styles.container}> <div className={styles.container}>
<InlineField <InlineField
@ -29,50 +39,34 @@ export function QuerySettings({ options, onOptionsChange }: Props) {
}} }}
/> />
</InlineField> </InlineField>
<InlineFieldRow>
<InlineField <IntervalInput
label="Time shift for start of search" label={getLabel('start')}
labelWidth={26} tooltip={getTooltip('start')}
disabled={!options.jsonData.traceQuery?.timeShiftEnabled} value={options.jsonData.traceQuery?.spanStartTimeShift || ''}
grow disabled={!options.jsonData.traceQuery?.timeShiftEnabled}
tooltip="Shifts the start of the time range when searching by TraceID. Searching can return traces that do not fully fall into the search time range, so we recommend using higher time shifts for longer traces. Default: 30m (Time units can be used here, for example: 5s, 1m, 3h)" onChange={(val) => {
> updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'traceQuery', {
<Input ...options.jsonData.traceQuery,
type="text" spanStartTimeShift: val,
placeholder="30m" });
width={40} }}
onChange={(v) => isInvalidError={invalidTimeShiftError}
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'traceQuery', { />
...options.jsonData.traceQuery,
spanStartTimeShift: v.currentTarget.value, <IntervalInput
}) label={getLabel('end')}
} tooltip={getTooltip('end')}
value={options.jsonData.traceQuery?.spanStartTimeShift || ''} value={options.jsonData.traceQuery?.spanEndTimeShift || ''}
/> disabled={!options.jsonData.traceQuery?.timeShiftEnabled}
</InlineField> onChange={(val) => {
</InlineFieldRow> updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'traceQuery', {
<InlineFieldRow> ...options.jsonData.traceQuery,
<InlineField spanEndTimeShift: val,
label="Time shift for end of search" });
labelWidth={26} }}
disabled={!options.jsonData.traceQuery?.timeShiftEnabled} isInvalidError={invalidTimeShiftError}
grow />
tooltip="Shifts the end of the time range when searching by TraceID. Searching can return traces that do not fully fall into the search time range, so we recommend using higher time shifts for longer traces. Default: 30m (Time units can be used here, for example: 5s, 1m, 3h)"
>
<Input
type="text"
placeholder="30m"
width={40}
onChange={(v) =>
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'traceQuery', {
...options.jsonData.traceQuery,
spanEndTimeShift: v.currentTarget.value,
})
}
value={options.jsonData.traceQuery?.spanEndTimeShift || ''}
/>
</InlineField>
</InlineFieldRow>
</div> </div>
); );
} }