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

View File

@ -12,7 +12,9 @@ import { DataSourcePicker } from '@grafana/runtime';
import { Button, InlineField, InlineFieldRow, Input, useStyles2 } from '@grafana/ui';
import { ConfigDescriptionLink } from '../ConfigDescriptionLink';
import { IntervalInput } from '../IntervalInput/IntervalInput';
import { TagMappingInput } from '../TraceToLogs/TagMappingInput';
import { getTimeShiftLabel, getTimeShiftTooltip, invalidTimeShiftError } from '../TraceToLogs/TraceToLogsSettings';
export interface TraceToMetricsOptions {
datasourceUid?: string;
@ -76,49 +78,31 @@ export function TraceToMetricsSettings({ options, onOptionsChange }: Props) {
) : null}
</InlineFieldRow>
<InlineFieldRow>
<InlineField
label="Span start time shift"
labelWidth={26}
grow
tooltip="Shifts the start 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={(v) =>
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
...options.jsonData.tracesToMetrics,
spanStartTimeShift: v.currentTarget.value,
})
}
value={options.jsonData.tracesToMetrics?.spanStartTimeShift || ''}
/>
</InlineField>
</InlineFieldRow>
<IntervalInput
label={getTimeShiftLabel('start')}
tooltip={getTimeShiftTooltip('start')}
value={options.jsonData.tracesToMetrics?.spanStartTimeShift || ''}
onChange={(val) => {
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
...options.jsonData.tracesToMetrics,
spanStartTimeShift: val,
});
}}
isInvalidError={invalidTimeShiftError}
/>
<InlineFieldRow>
<InlineField
label="Span end time shift"
labelWidth={26}
grow
tooltip="Shifts the end 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={(v) =>
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
...options.jsonData.tracesToMetrics,
spanEndTimeShift: v.currentTarget.value,
})
}
value={options.jsonData.tracesToMetrics?.spanEndTimeShift || ''}
/>
</InlineField>
</InlineFieldRow>
<IntervalInput
label={getTimeShiftLabel('end')}
tooltip={getTimeShiftTooltip('end')}
value={options.jsonData.tracesToMetrics?.spanEndTimeShift || ''}
onChange={(val) => {
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
...options.jsonData.tracesToMetrics,
spanEndTimeShift: val,
});
}}
isInvalidError={invalidTimeShiftError}
/>
<InlineFieldRow>
<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 { 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';
@ -11,6 +13,14 @@ interface Props extends DataSourcePluginOptionsEditorProps<TempoJsonData> {}
export function QuerySettings({ options, onOptionsChange }: Props) {
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 (
<div className={styles.container}>
<InlineField
@ -29,50 +39,34 @@ export function QuerySettings({ options, onOptionsChange }: Props) {
}}
/>
</InlineField>
<InlineFieldRow>
<InlineField
label="Time shift for start of search"
labelWidth={26}
disabled={!options.jsonData.traceQuery?.timeShiftEnabled}
grow
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)"
>
<Input
type="text"
placeholder="30m"
width={40}
onChange={(v) =>
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'traceQuery', {
...options.jsonData.traceQuery,
spanStartTimeShift: v.currentTarget.value,
})
}
value={options.jsonData.traceQuery?.spanStartTimeShift || ''}
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField
label="Time shift for end of search"
labelWidth={26}
disabled={!options.jsonData.traceQuery?.timeShiftEnabled}
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>
<IntervalInput
label={getLabel('start')}
tooltip={getTooltip('start')}
value={options.jsonData.traceQuery?.spanStartTimeShift || ''}
disabled={!options.jsonData.traceQuery?.timeShiftEnabled}
onChange={(val) => {
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'traceQuery', {
...options.jsonData.traceQuery,
spanStartTimeShift: val,
});
}}
isInvalidError={invalidTimeShiftError}
/>
<IntervalInput
label={getLabel('end')}
tooltip={getTooltip('end')}
value={options.jsonData.traceQuery?.spanEndTimeShift || ''}
disabled={!options.jsonData.traceQuery?.timeShiftEnabled}
onChange={(val) => {
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'traceQuery', {
...options.jsonData.traceQuery,
spanEndTimeShift: val,
});
}}
isInvalidError={invalidTimeShiftError}
/>
</div>
);
}