mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
20b6ae96a3
commit
f1338cee60
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
53
public/app/core/components/IntervalInput/IntervalInput.tsx
Normal file
53
public/app/core/components/IntervalInput/IntervalInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
28
public/app/core/components/IntervalInput/validation.test.ts
Normal file
28
public/app/core/components/IntervalInput/validation.test.ts
Normal 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();
|
||||
});
|
||||
});
|
5
public/app/core/components/IntervalInput/validation.ts
Normal file
5
public/app/core/components/IntervalInput/validation.ts
Normal 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;
|
||||
};
|
@ -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 (
|
||||
|
@ -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}>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user