mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
React Hook Form: Update to v 7.49.2 (#79493)
* Update RHF to latest * Update Form types * Fix alerting types * Fix correlations types * Update tests * Fix tests * Update LabelsField.tsx to use InputControl * Update RuleEditorGrafanaRules.test.tsx * Update RuleEditorCloudRules.test.tsx * Only require one label * Update RuleEditorRecordingRule.test.tsx * Fix labels rules * Revert * Remove RHF from ignore rules * Revert * update form validation for overriding group timings * Fix changes to correlations * Fix auth type errors --------- Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com> Co-authored-by: Kristina Durivage <kristina.durivage@grafana.com>
This commit is contained in:
@@ -378,7 +378,7 @@
|
|||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-grid-layout": "1.4.2",
|
"react-grid-layout": "1.4.2",
|
||||||
"react-highlight-words": "0.20.0",
|
"react-highlight-words": "0.20.0",
|
||||||
"react-hook-form": "7.5.3",
|
"react-hook-form": "^7.49.2",
|
||||||
"react-i18next": "^12.0.0",
|
"react-i18next": "^12.0.0",
|
||||||
"react-inlinesvg": "3.0.2",
|
"react-inlinesvg": "3.0.2",
|
||||||
"react-loading-skeleton": "3.3.1",
|
"react-loading-skeleton": "3.3.1",
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
"react-custom-scrollbars-2": "4.5.0",
|
"react-custom-scrollbars-2": "4.5.0",
|
||||||
"react-dropzone": "14.2.3",
|
"react-dropzone": "14.2.3",
|
||||||
"react-highlight-words": "0.20.0",
|
"react-highlight-words": "0.20.0",
|
||||||
"react-hook-form": "7.5.3",
|
"react-hook-form": "^7.49.2",
|
||||||
"react-i18next": "^12.0.0",
|
"react-i18next": "^12.0.0",
|
||||||
"react-inlinesvg": "3.0.2",
|
"react-inlinesvg": "3.0.2",
|
||||||
"react-loading-skeleton": "3.3.1",
|
"react-loading-skeleton": "3.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { HTMLProps, useEffect } from 'react';
|
import React, { HTMLProps, useEffect } from 'react';
|
||||||
import { useForm, Mode, DeepPartial, UnpackNestedValue, SubmitHandler, FieldValues } from 'react-hook-form';
|
import { useForm, Mode, DefaultValues, SubmitHandler, FieldValues } from 'react-hook-form';
|
||||||
|
|
||||||
import { FormAPI } from '../../types';
|
import { FormAPI } from '../../types';
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ interface FormProps<T extends FieldValues> extends Omit<HTMLProps<HTMLFormElemen
|
|||||||
validateOn?: Mode;
|
validateOn?: Mode;
|
||||||
validateOnMount?: boolean;
|
validateOnMount?: boolean;
|
||||||
validateFieldsOnMount?: string | string[];
|
validateFieldsOnMount?: string | string[];
|
||||||
defaultValues?: UnpackNestedValue<DeepPartial<T>>;
|
defaultValues?: DefaultValues<T>;
|
||||||
onSubmit: SubmitHandler<T>;
|
onSubmit: SubmitHandler<T>;
|
||||||
children: (api: FormAPI<T>) => React.ReactNode;
|
children: (api: FormAPI<T>) => React.ReactNode;
|
||||||
/** Sets max-width for container. Use it instead of setting individual widths on inputs.*/
|
/** Sets max-width for container. Use it instead of setting individual widths on inputs.*/
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const NotificationChannelOptions = ({
|
|||||||
label={option.label}
|
label={option.label}
|
||||||
description={option.description}
|
description={option.description}
|
||||||
invalid={errors.settings && !!errors.settings[option.propertyName]}
|
invalid={errors.settings && !!errors.settings[option.propertyName]}
|
||||||
error={errors.settings && errors.settings[option.propertyName]?.message}
|
error={errors.settings && String(errors.settings[option.propertyName]?.message || '')}
|
||||||
>
|
>
|
||||||
{secureFields && secureFields[option.propertyName] ? (
|
{secureFields && secureFields[option.propertyName] ? (
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ import { TimezoneSelect } from './timezones';
|
|||||||
|
|
||||||
export const MuteTimingTimeInterval = () => {
|
export const MuteTimingTimeInterval = () => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const { formState, register, setValue } = useFormContext();
|
const { formState, register, setValue } = useFormContext<MuteTimingFields>();
|
||||||
const {
|
const {
|
||||||
fields: timeIntervals,
|
fields: timeIntervals,
|
||||||
append: addTimeInterval,
|
append: addTimeInterval,
|
||||||
remove: removeTimeInterval,
|
remove: removeTimeInterval,
|
||||||
} = useFieldArray<MuteTimingFields>({
|
} = useFieldArray({
|
||||||
name: 'time_intervals',
|
name: 'time_intervals',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -43,7 +43,11 @@ export const MuteTimingTimeInterval = () => {
|
|||||||
return (
|
return (
|
||||||
<div key={timeInterval.id} className={styles.timeIntervalSection}>
|
<div key={timeInterval.id} className={styles.timeIntervalSection}>
|
||||||
<MuteTimingTimeRange intervalIndex={timeIntervalIndex} />
|
<MuteTimingTimeRange intervalIndex={timeIntervalIndex} />
|
||||||
<Field label="Location" invalid={Boolean(errors.location)} error={errors.location?.message}>
|
<Field
|
||||||
|
label="Location"
|
||||||
|
invalid={Boolean(errors.time_intervals?.[timeIntervalIndex]?.location)}
|
||||||
|
error={errors.time_intervals?.[timeIntervalIndex]?.location?.message}
|
||||||
|
>
|
||||||
<TimezoneSelect
|
<TimezoneSelect
|
||||||
prefix={<Icon name="map-marker" />}
|
prefix={<Icon name="map-marker" />}
|
||||||
width={50}
|
width={50}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const MuteTimingTimeRange = ({ intervalIndex }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const formErrors = formState.errors.time_intervals?.[intervalIndex];
|
const formErrors = formState.errors.time_intervals?.[intervalIndex];
|
||||||
const timeRangeInvalid = formErrors?.times?.some((value) => value?.start_time || value?.end_time) ?? false;
|
const timeRangeInvalid = formErrors?.times?.some?.((value) => value?.start_time || value?.end_time) ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ export const AmRoutesExpandedForm = ({
|
|||||||
>
|
>
|
||||||
<PromDurationInput
|
<PromDurationInput
|
||||||
{...register('repeatIntervalValue', {
|
{...register('repeatIntervalValue', {
|
||||||
validate: (value: string) => {
|
validate: (value = '') => {
|
||||||
const groupInterval = getValues('groupIntervalValue');
|
const groupInterval = getValues('groupIntervalValue');
|
||||||
return repeatIntervalValidator(value, groupInterval);
|
return repeatIntervalValidator(value, groupInterval);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export const TemplateForm = ({ existing, alertManagerSourceName, config, provena
|
|||||||
watch,
|
watch,
|
||||||
} = formApi;
|
} = formApi;
|
||||||
|
|
||||||
const validateNameIsUnique: Validate<string> = (name: string) => {
|
const validateNameIsUnique: Validate<string, TemplateFormValues> = (name: string) => {
|
||||||
return !config.template_files[name] || existing?.name === name
|
return !config.template_files[name] || existing?.name === name
|
||||||
? true
|
? true
|
||||||
: 'Another template with this name already exists.';
|
: 'Another template with this name already exists.';
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ export function ChannelSubForm<R extends ChannelValues>({
|
|||||||
// Prevent forgetting about initial values when switching the integration type and the oncall integration type
|
// Prevent forgetting about initial values when switching the integration type and the oncall integration type
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Restore values when switching back from a changed integration to the default one
|
// Restore values when switching back from a changed integration to the default one
|
||||||
const subscription = watch((_, { name, type, value }) => {
|
const subscription = watch((v, { name, type }) => {
|
||||||
|
const value = name ? v[name] : '';
|
||||||
if (initialValues && name === fieldName('type') && value === initialValues.type && type === 'change') {
|
if (initialValues && name === fieldName('type') && value === initialValues.type && type === 'change') {
|
||||||
setValue(fieldName('settings'), initialValues.settings);
|
setValue(fieldName('settings'), initialValues.settings);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export function ReceiverForm<R extends ChannelValues>({
|
|||||||
|
|
||||||
const { fields, append, remove } = useControlledFieldArray<R>({ name: 'items', formAPI, softDelete: true });
|
const { fields, append, remove } = useControlledFieldArray<R>({ name: 'items', formAPI, softDelete: true });
|
||||||
|
|
||||||
const validateNameIsAvailable: Validate<string> = useCallback(
|
const validateNameIsAvailable: Validate<string, ReceiverFormValues<R>> = useCallback(
|
||||||
(name: string) =>
|
(name: string) =>
|
||||||
takenReceiverNames.map((name) => name.trim().toLowerCase()).includes(name.trim().toLowerCase())
|
takenReceiverNames.map((name) => name.trim().toLowerCase()).includes(name.trim().toLowerCase())
|
||||||
? 'Another receiver with this name already exists.'
|
? 'Another receiver with this name already exists.'
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { FieldArrayMethodProps, useFieldArray, useFormContext } from 'react-hook-form';
|
import { useFieldArray, UseFieldArrayAppend, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
import { Button, Field, InlineLabel, Input, LoadingPlaceholder, Stack, Text, useStyles2 } from '@grafana/ui';
|
import {
|
||||||
|
Button,
|
||||||
|
Field,
|
||||||
|
InlineLabel,
|
||||||
|
Input,
|
||||||
|
InputControl,
|
||||||
|
LoadingPlaceholder,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
useStyles2,
|
||||||
|
} from '@grafana/ui';
|
||||||
import { useDispatch } from 'app/types';
|
import { useDispatch } from 'app/types';
|
||||||
|
|
||||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||||
@@ -85,10 +95,7 @@ const RemoveButton: FC<{
|
|||||||
);
|
);
|
||||||
|
|
||||||
const AddButton: FC<{
|
const AddButton: FC<{
|
||||||
append: (
|
append: UseFieldArrayAppend<RuleFormValues, 'labels'>;
|
||||||
value: Partial<{ key: string; value: string }> | Array<Partial<{ key: string; value: string }>>,
|
|
||||||
options?: FieldArrayMethodProps | undefined
|
|
||||||
) => void;
|
|
||||||
className: string;
|
className: string;
|
||||||
}> = ({ append, className }) => (
|
}> = ({ append, className }) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -107,11 +114,9 @@ const AddButton: FC<{
|
|||||||
const LabelsWithSuggestions: FC<{ dataSourceName: string }> = ({ dataSourceName }) => {
|
const LabelsWithSuggestions: FC<{ dataSourceName: string }> = ({ dataSourceName }) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const {
|
const {
|
||||||
register,
|
|
||||||
control,
|
control,
|
||||||
watch,
|
watch,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
setValue,
|
|
||||||
} = useFormContext<RuleFormValues>();
|
} = useFormContext<RuleFormValues>();
|
||||||
|
|
||||||
const labels = watch('labels');
|
const labels = watch('labels');
|
||||||
@@ -151,17 +156,24 @@ const LabelsWithSuggestions: FC<{ dataSourceName: string }> = ({ dataSourceName
|
|||||||
error={errors.labels?.[index]?.key?.message}
|
error={errors.labels?.[index]?.key?.message}
|
||||||
data-testid={`label-key-${index}`}
|
data-testid={`label-key-${index}`}
|
||||||
>
|
>
|
||||||
<AlertLabelDropdown
|
<InputControl
|
||||||
{...register(`labels.${index}.key`, {
|
name={`labels.${index}.key`}
|
||||||
required: { value: Boolean(labels[index]?.value), message: 'Required.' },
|
control={control}
|
||||||
})}
|
rules={{ required: Boolean(labels[index]?.value) ? 'Required.' : false }}
|
||||||
defaultValue={field.key ? { label: field.key, value: field.key } : undefined}
|
render={({ field: { onChange, ref, ...rest } }) => {
|
||||||
options={keys}
|
return (
|
||||||
onChange={(newValue: SelectableValue) => {
|
<AlertLabelDropdown
|
||||||
setValue(`labels.${index}.key`, newValue.value);
|
{...rest}
|
||||||
setSelectedKey(newValue.value);
|
defaultValue={field.key ? { label: field.key, value: field.key } : undefined}
|
||||||
|
options={keys}
|
||||||
|
onChange={(newValue: SelectableValue) => {
|
||||||
|
onChange(newValue.value);
|
||||||
|
setSelectedKey(newValue.value);
|
||||||
|
}}
|
||||||
|
type="key"
|
||||||
|
/>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
type="key"
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<InlineLabel className={styles.equalSign}>=</InlineLabel>
|
<InlineLabel className={styles.equalSign}>=</InlineLabel>
|
||||||
@@ -171,19 +183,26 @@ const LabelsWithSuggestions: FC<{ dataSourceName: string }> = ({ dataSourceName
|
|||||||
error={errors.labels?.[index]?.value?.message}
|
error={errors.labels?.[index]?.value?.message}
|
||||||
data-testid={`label-value-${index}`}
|
data-testid={`label-value-${index}`}
|
||||||
>
|
>
|
||||||
<AlertLabelDropdown
|
<InputControl
|
||||||
{...register(`labels.${index}.value`, {
|
control={control}
|
||||||
required: { value: Boolean(labels[index]?.key), message: 'Required.' },
|
name={`labels.${index}.value`}
|
||||||
})}
|
rules={{ required: Boolean(labels[index]?.value) ? 'Required.' : false }}
|
||||||
defaultValue={field.value ? { label: field.value, value: field.value } : undefined}
|
render={({ field: { onChange, ref, ...rest } }) => {
|
||||||
options={values}
|
return (
|
||||||
onChange={(newValue: SelectableValue) => {
|
<AlertLabelDropdown
|
||||||
setValue(`labels.${index}.value`, newValue.value);
|
{...rest}
|
||||||
|
defaultValue={field.value ? { label: field.value, value: field.value } : undefined}
|
||||||
|
options={values}
|
||||||
|
onChange={(newValue: SelectableValue) => {
|
||||||
|
onChange(newValue.value);
|
||||||
|
}}
|
||||||
|
onOpenMenu={() => {
|
||||||
|
setSelectedKey(labels[index].key);
|
||||||
|
}}
|
||||||
|
type="value"
|
||||||
|
/>
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
onOpenMenu={() => {
|
|
||||||
setSelectedKey(labels[index].key);
|
|
||||||
}}
|
|
||||||
type="value"
|
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
@@ -268,7 +287,7 @@ const LabelsField: FC<Props> = ({ dataSourceName }) => {
|
|||||||
Add labels to your rule to annotate your rules, ease searching, or route to a notification policy.
|
Add labels to your rule to annotate your rules, ease searching, or route to a notification policy.
|
||||||
</Text>
|
</Text>
|
||||||
<NeedHelpInfo
|
<NeedHelpInfo
|
||||||
contentText="The dropdown only displays labels that you have previously used for alerts.
|
contentText="The dropdown only displays labels that you have previously used for alerts.
|
||||||
Select a label from the options below or type in a new one."
|
Select a label from the options below or type in a new one."
|
||||||
title="Labels"
|
title="Labels"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { DeepMap, FieldError, FormProvider, useForm, UseFormWatch } from 'react-hook-form';
|
import { FormProvider, SubmitErrorHandler, useForm, UseFormWatch } from 'react-hook-form';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
@@ -144,7 +144,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onInvalid = (errors: DeepMap<RuleFormValues, FieldError>): void => {
|
const onInvalid: SubmitErrorHandler<RuleFormValues> = (errors): void => {
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
trackNewAlerRuleFormError({
|
trackNewAlerRuleFormError({
|
||||||
grafana_version: config.buildInfo.version,
|
grafana_version: config.buildInfo.version,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { CloudNotifierType, NotifierType } from 'app/types';
|
|||||||
import { ControlledField } from '../hooks/useControlledFieldArray';
|
import { ControlledField } from '../hooks/useControlledFieldArray';
|
||||||
|
|
||||||
export interface ChannelValues {
|
export interface ChannelValues {
|
||||||
__id: string; // used to correllate form values to original DTOs
|
__id: string; // used to correlate form values to original DTOs
|
||||||
type: string;
|
type: string;
|
||||||
settings: Record<string, any>;
|
settings: Record<string, any>;
|
||||||
secureSettings: Record<string, any>;
|
secureSettings: Record<string, any>;
|
||||||
|
|||||||
@@ -221,8 +221,8 @@ export const mapMultiSelectValueToStrings = (
|
|||||||
return selectableValuesToStrings(selectableValues);
|
return selectableValuesToStrings(selectableValues);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function promDurationValidator(duration: string) {
|
export function promDurationValidator(duration?: string) {
|
||||||
if (duration.length === 0) {
|
if (!duration || duration.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +237,7 @@ export const objectMatchersToString = (matchers: ObjectMatcher[]): string[] => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const repeatIntervalValidator = (repeatInterval: string, groupInterval: string) => {
|
export const repeatIntervalValidator = (repeatInterval: string, groupInterval = '') => {
|
||||||
if (repeatInterval.length === 0) {
|
if (repeatInterval.length === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { FieldData, SSOProvider } from './types';
|
import { FieldData, SSOProvider } from './types';
|
||||||
|
import { isSelectableValue } from './utils/guards';
|
||||||
|
|
||||||
/** Map providers to their settings */
|
/** Map providers to their settings */
|
||||||
export const fields: Record<SSOProvider['provider'], Array<keyof SSOProvider['settings']>> = {
|
export const fields: Record<SSOProvider['provider'], Array<keyof SSOProvider['settings']>> = {
|
||||||
@@ -46,7 +47,10 @@ export const fieldMap: Record<string, FieldData> = {
|
|||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
return isNumeric(value);
|
return isNumeric(value);
|
||||||
}
|
}
|
||||||
return value.every((v) => v?.value && isNumeric(v.value));
|
if (isSelectableValue(value)) {
|
||||||
|
return value.every((v) => v?.value && isNumeric(v.value));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
message: 'Team ID must be a number.',
|
message: 'Team ID must be a number.',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Validate } from 'react-hook-form';
|
||||||
|
|
||||||
import { IconName, SelectableValue } from '@grafana/data';
|
import { IconName, SelectableValue } from '@grafana/data';
|
||||||
import { Settings } from 'app/types';
|
import { Settings } from 'app/types';
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ export type SSOProviderSettingsBase = {
|
|||||||
emailAttributePath?: string;
|
emailAttributePath?: string;
|
||||||
emptyScopes?: boolean;
|
emptyScopes?: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
extra?: Record<string, unknown>;
|
extra?: Record<string, string>;
|
||||||
groupsAttributePath?: string;
|
groupsAttributePath?: string;
|
||||||
hostedDomain?: string;
|
hostedDomain?: string;
|
||||||
icon?: IconName;
|
icon?: IconName;
|
||||||
@@ -97,7 +99,7 @@ export type FieldData = {
|
|||||||
validation?: {
|
validation?: {
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
validate?: (value: string | Array<SelectableValue<string>>) => boolean | string | Promise<boolean | string>;
|
validate?: Validate<SSOProviderDTO[keyof SSOProviderDTO], SSOProviderDTO>;
|
||||||
};
|
};
|
||||||
multi?: boolean;
|
multi?: boolean;
|
||||||
allowCustomValue?: boolean;
|
allowCustomValue?: boolean;
|
||||||
|
|||||||
@@ -538,7 +538,7 @@ describe('CorrelationsPage', () => {
|
|||||||
|
|
||||||
// select Regex, be sure expression field is not disabled and contains the former expression
|
// select Regex, be sure expression field is not disabled and contains the former expression
|
||||||
openMenu(typeFilterSelect[0]);
|
openMenu(typeFilterSelect[0]);
|
||||||
await userEvent.click(screen.getByText('Regular expression', { selector: 'span' }));
|
await userEvent.click(screen.getByText('Regular expression'));
|
||||||
expressionInput = screen.queryByLabelText(/expression/i);
|
expressionInput = screen.queryByLabelText(/expression/i);
|
||||||
expect(expressionInput).toBeInTheDocument();
|
expect(expressionInput).toBeInTheDocument();
|
||||||
expect(expressionInput).toBeEnabled();
|
expect(expressionInput).toBeEnabled();
|
||||||
@@ -554,7 +554,8 @@ describe('CorrelationsPage', () => {
|
|||||||
await userEvent.click(screen.getByRole('button', { name: /add transformation/i }));
|
await userEvent.click(screen.getByRole('button', { name: /add transformation/i }));
|
||||||
typeFilterSelect = screen.getAllByLabelText('Type');
|
typeFilterSelect = screen.getAllByLabelText('Type');
|
||||||
openMenu(typeFilterSelect[0]);
|
openMenu(typeFilterSelect[0]);
|
||||||
await userEvent.click(screen.getByText('Regular expression'));
|
const menu = await screen.findByLabelText('Select options menu');
|
||||||
|
await userEvent.click(within(menu).getByText('Regular expression'));
|
||||||
expressionInput = screen.queryByLabelText(/expression/i);
|
expressionInput = screen.queryByLabelText(/expression/i);
|
||||||
expect(expressionInput).toBeInTheDocument();
|
expect(expressionInput).toBeInTheDocument();
|
||||||
expect(expressionInput).toBeEnabled();
|
expect(expressionInput).toBeEnabled();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { getVariableUsageInfo } from '../../explore/utils/links';
|
|||||||
|
|
||||||
import { TransformationsEditor } from './TransformationsEditor';
|
import { TransformationsEditor } from './TransformationsEditor';
|
||||||
import { useCorrelationsFormContext } from './correlationsFormContext';
|
import { useCorrelationsFormContext } from './correlationsFormContext';
|
||||||
|
import { FormDTO } from './types';
|
||||||
import { getInputId } from './utils';
|
import { getInputId } from './utils';
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
@@ -25,7 +26,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const ConfigureCorrelationSourceForm = () => {
|
export const ConfigureCorrelationSourceForm = () => {
|
||||||
const { control, formState, register, getValues } = useFormContext();
|
const { control, formState, register, getValues } = useFormContext<FormDTO>();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const withDsUID = (fn: Function) => (ds: DataSourceInstanceSettings) => fn(ds.uid);
|
const withDsUID = (fn: Function) => (ds: DataSourceInstanceSettings) => fn(ds.uid);
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import { DataSourcePicker } from 'app/features/datasources/components/picker/Dat
|
|||||||
|
|
||||||
import { QueryEditorField } from './QueryEditorField';
|
import { QueryEditorField } from './QueryEditorField';
|
||||||
import { useCorrelationsFormContext } from './correlationsFormContext';
|
import { useCorrelationsFormContext } from './correlationsFormContext';
|
||||||
|
import { FormDTO } from './types';
|
||||||
|
|
||||||
export const ConfigureCorrelationTargetForm = () => {
|
export const ConfigureCorrelationTargetForm = () => {
|
||||||
const { control, formState } = useFormContext();
|
const { control, formState } = useFormContext<FormDTO>();
|
||||||
const withDsUID = (fn: Function) => (ds: DataSourceInstanceSettings) => fn(ds.uid);
|
const withDsUID = (fn: Function) => (ds: DataSourceInstanceSettings) => fn(ds.uid);
|
||||||
const { correlation } = useCorrelationsFormContext();
|
const { correlation } = useCorrelationsFormContext();
|
||||||
const targetUID: string | undefined = useWatch({ name: 'targetUID' }) || correlation?.targetUID;
|
const targetUID: string | undefined = useWatch({ name: 'targetUID' }) || correlation?.targetUID;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useFormContext, useWatch } from 'react-hook-form';
|
|||||||
import { Field, Icon, IconButton, Input, Label, Select, Stack, Tooltip, useStyles2 } from '@grafana/ui';
|
import { Field, Icon, IconButton, Input, Label, Select, Stack, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
import { Trans, t } from 'app/core/internationalization';
|
import { Trans, t } from 'app/core/internationalization';
|
||||||
|
|
||||||
import { getSupportedTransTypeDetails, getTransformOptions } from './types';
|
import { FormDTO, getSupportedTransTypeDetails, getTransformOptions } from './types';
|
||||||
type Props = {
|
type Props = {
|
||||||
index: number;
|
index: number;
|
||||||
value: Record<string, string>;
|
value: Record<string, string>;
|
||||||
@@ -23,7 +23,7 @@ const getStyles = () => ({
|
|||||||
|
|
||||||
const TransformationEditorRow = (props: Props) => {
|
const TransformationEditorRow = (props: Props) => {
|
||||||
const { index, value: defaultValue, readOnly, remove } = props;
|
const { index, value: defaultValue, readOnly, remove } = props;
|
||||||
const { control, formState, register, setValue, watch, getValues } = useFormContext();
|
const { control, formState, register, setValue, watch, getValues } = useFormContext<FormDTO>();
|
||||||
|
|
||||||
const [keptVals, setKeptVals] = useState<{ expression?: string; mapValue?: string }>({});
|
const [keptVals, setKeptVals] = useState<{ expression?: string; mapValue?: string }>({});
|
||||||
|
|
||||||
@@ -63,34 +63,37 @@ const TransformationEditorRow = (props: Props) => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
}
|
}
|
||||||
invalid={!!formState.errors?.config?.transformations?.[index]?.type}
|
invalid={!!formState.errors?.config?.transformations?.[index]?.type}
|
||||||
error={formState.errors?.config?.transformations?.[index]?.type?.message}
|
error={formState.errors?.config?.transformations?.[index]?.message}
|
||||||
validationMessageHorizontalOverflow={true}
|
validationMessageHorizontalOverflow={true}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
value={typeValue}
|
value={typeValue}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
const currentValues = getValues().config.transformations[index];
|
const currentValues = getValues()?.config?.transformations?.[index];
|
||||||
setKeptVals({
|
if (currentValues) {
|
||||||
expression: currentValues.expression,
|
setKeptVals({
|
||||||
mapValue: currentValues.mapValue,
|
expression: currentValues.expression,
|
||||||
});
|
mapValue: currentValues.mapValue,
|
||||||
|
});
|
||||||
const newValueDetails = getSupportedTransTypeDetails(value.value);
|
|
||||||
|
|
||||||
if (newValueDetails.expressionDetails.show) {
|
|
||||||
setValue(`config.transformations.${index}.expression`, keptVals?.expression || '');
|
|
||||||
} else {
|
|
||||||
setValue(`config.transformations.${index}.expression`, '');
|
|
||||||
}
|
}
|
||||||
|
if (value.value) {
|
||||||
|
const newValueDetails = getSupportedTransTypeDetails(value.value);
|
||||||
|
|
||||||
if (newValueDetails.mapValueDetails.show) {
|
if (newValueDetails.expressionDetails.show) {
|
||||||
setValue(`config.transformations.${index}.mapValue`, keptVals?.mapValue || '');
|
setValue(`config.transformations.${index}.expression`, keptVals?.expression || '');
|
||||||
} else {
|
} else {
|
||||||
setValue(`config.transformations.${index}.mapValue`, '');
|
setValue(`config.transformations.${index}.expression`, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newValueDetails.mapValueDetails.show) {
|
||||||
|
setValue(`config.transformations.${index}.mapValue`, keptVals?.mapValue || '');
|
||||||
|
} else {
|
||||||
|
setValue(`config.transformations.${index}.mapValue`, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(`config.transformations.${index}.type`, value.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
setValue(`config.transformations.${index}.type`, value.value);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
options={transformOptions}
|
options={transformOptions}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ComponentType } from 'react';
|
import { ComponentType } from 'react';
|
||||||
import { DeepPartial, UnpackNestedValue } from 'react-hook-form';
|
import { DefaultValues } from 'react-hook-form';
|
||||||
|
|
||||||
export type WizardProps<T> = {
|
export type WizardProps<T> = {
|
||||||
/**
|
/**
|
||||||
* Initial values for the form
|
* Initial values for the form
|
||||||
*/
|
*/
|
||||||
defaultValues?: UnpackNestedValue<DeepPartial<T>>;
|
defaultValues?: DefaultValues<T>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of steps/pages in the wizard.
|
* List of steps/pages in the wizard.
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type CorrelationConfigType = 'query';
|
|||||||
|
|
||||||
export interface CorrelationConfig {
|
export interface CorrelationConfig {
|
||||||
field: string;
|
field: string;
|
||||||
target: object;
|
target: object; // this contains anything that would go in the query editor, so any extension off DataQuery a datasource would have, and needs to be generic
|
||||||
type: CorrelationConfigType;
|
type: CorrelationConfigType;
|
||||||
transformations?: DataLinkTransformationConfig[];
|
transformations?: DataLinkTransformationConfig[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,10 +84,10 @@ export const CorrelationHelper = ({ exploreId, correlations }: Props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = watch((value) => {
|
const subscription = watch((value) => {
|
||||||
let dirty = correlationDetails?.correlationDirty || false;
|
let dirty = correlationDetails?.correlationDirty || false;
|
||||||
|
let description = value.description || '';
|
||||||
if (!dirty && (value.label !== defaultLabel || value.description !== '')) {
|
if (!dirty && (value.label !== defaultLabel || description !== '')) {
|
||||||
dirty = true;
|
dirty = true;
|
||||||
} else if (dirty && value.label === defaultLabel && value.description.trim() === '') {
|
} else if (dirty && value.label === defaultLabel && description.trim() === '') {
|
||||||
dirty = false;
|
dirty = false;
|
||||||
}
|
}
|
||||||
dispatch(
|
dispatch(
|
||||||
|
|||||||
@@ -101,18 +101,21 @@ export const CorrelationTransformationAddModal = ({
|
|||||||
isExpressionValid = !formFieldsVis.expressionDetails.show;
|
isExpressionValid = !formFieldsVis.expressionDetails.show;
|
||||||
}
|
}
|
||||||
setIsExpValid(isExpressionValid);
|
setIsExpValid(isExpressionValid);
|
||||||
const transformationVars = getTransformationVars(
|
let transKeys = [];
|
||||||
{
|
if (formValues.type) {
|
||||||
type: formValues.type,
|
const transformationVars = getTransformationVars(
|
||||||
expression: isExpressionValid ? expression : '',
|
{
|
||||||
mapValue: formValues.mapValue,
|
type: formValues.type,
|
||||||
},
|
expression: isExpressionValid ? expression : '',
|
||||||
fieldList[formValues.field!] || '',
|
mapValue: formValues.mapValue,
|
||||||
formValues.field!
|
},
|
||||||
);
|
fieldList[formValues.field!] || '',
|
||||||
|
formValues.field!
|
||||||
|
);
|
||||||
|
|
||||||
const transKeys = Object.keys(transformationVars);
|
transKeys = Object.keys(transformationVars);
|
||||||
setTransformationVars(transKeys.length > 0 ? { ...transformationVars } : {});
|
setTransformationVars(transKeys.length > 0 ? { ...transformationVars } : {});
|
||||||
|
}
|
||||||
|
|
||||||
if (transKeys.length === 0 || !isExpressionValid) {
|
if (transKeys.length === 0 || !isExpressionValid) {
|
||||||
setValidToSave(false);
|
setValidToSave(false);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { partial } from 'lodash';
|
import { partial } from 'lodash';
|
||||||
import React, { type ReactElement, useEffect, useState } from 'react';
|
import React, { type ReactElement, useEffect, useState } from 'react';
|
||||||
import { DeepMap, FieldError, useForm } from 'react-hook-form';
|
import { DeepMap, FieldError, FieldErrors, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { locationUtil, SelectableValue } from '@grafana/data';
|
import { locationUtil, SelectableValue } from '@grafana/data';
|
||||||
import { config, locationService, reportInteraction } from '@grafana/runtime';
|
import { config, locationService, reportInteraction } from '@grafana/runtime';
|
||||||
@@ -34,7 +34,7 @@ interface SaveToExistingDashboard extends SaveTargetDTO {
|
|||||||
type FormDTO = SaveToNewDashboardDTO | SaveToExistingDashboard;
|
type FormDTO = SaveToNewDashboardDTO | SaveToExistingDashboard;
|
||||||
|
|
||||||
function assertIsSaveToExistingDashboardError(
|
function assertIsSaveToExistingDashboardError(
|
||||||
errors: DeepMap<FormDTO, FieldError>
|
errors: FieldErrors<FormDTO>
|
||||||
): asserts errors is DeepMap<SaveToExistingDashboard, FieldError> {
|
): asserts errors is DeepMap<SaveToExistingDashboard, FieldError> {
|
||||||
// the shape of the errors object is always compatible with the type above, but we need to
|
// the shape of the errors object is always compatible with the type above, but we need to
|
||||||
// explicitly assert its type so that TS can narrow down FormDTO to SaveToExistingDashboard
|
// explicitly assert its type so that TS can narrow down FormDTO to SaveToExistingDashboard
|
||||||
|
|||||||
@@ -299,8 +299,8 @@ const builtInVariables = [
|
|||||||
* @param query
|
* @param query
|
||||||
* @param scopedVars
|
* @param scopedVars
|
||||||
*/
|
*/
|
||||||
export function getVariableUsageInfo<T extends DataLink>(
|
export function getVariableUsageInfo(
|
||||||
query: T,
|
query: object,
|
||||||
scopedVars: ScopedVars
|
scopedVars: ScopedVars
|
||||||
): { variables: VariableInterpolation[]; allVariablesDefined: boolean } {
|
): { variables: VariableInterpolation[]; allVariablesDefined: boolean } {
|
||||||
let variables: VariableInterpolation[] = [];
|
let variables: VariableInterpolation[] = [];
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ jest.mock('app/core/core', () => ({
|
|||||||
hasPermission: () => true,
|
hasPermission: () => true,
|
||||||
hasPermissionInMetadata: () => true,
|
hasPermissionInMetadata: () => true,
|
||||||
user: { orgId: 1 },
|
user: { orgId: 1 },
|
||||||
|
fetchUserPermissions: () => Promise.resolve(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type FormModel = { folderName: string };
|
|||||||
interface Props {
|
interface Props {
|
||||||
onSubmit: SubmitHandler<FormModel>;
|
onSubmit: SubmitHandler<FormModel>;
|
||||||
onDismiss: () => void;
|
onDismiss: () => void;
|
||||||
validate: Validate<string>;
|
validate: Validate<string, FormModel>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialFormModel = { folderName: '' };
|
const initialFormModel = { folderName: '' };
|
||||||
|
|||||||
14
yarn.lock
14
yarn.lock
@@ -3551,7 +3551,7 @@ __metadata:
|
|||||||
react-dom: "npm:18.2.0"
|
react-dom: "npm:18.2.0"
|
||||||
react-dropzone: "npm:14.2.3"
|
react-dropzone: "npm:14.2.3"
|
||||||
react-highlight-words: "npm:0.20.0"
|
react-highlight-words: "npm:0.20.0"
|
||||||
react-hook-form: "npm:7.5.3"
|
react-hook-form: "npm:^7.49.2"
|
||||||
react-i18next: "npm:^12.0.0"
|
react-i18next: "npm:^12.0.0"
|
||||||
react-inlinesvg: "npm:3.0.2"
|
react-inlinesvg: "npm:3.0.2"
|
||||||
react-loading-skeleton: "npm:3.3.1"
|
react-loading-skeleton: "npm:3.3.1"
|
||||||
@@ -17664,7 +17664,7 @@ __metadata:
|
|||||||
react-dropzone: "npm:^14.2.3"
|
react-dropzone: "npm:^14.2.3"
|
||||||
react-grid-layout: "npm:1.4.2"
|
react-grid-layout: "npm:1.4.2"
|
||||||
react-highlight-words: "npm:0.20.0"
|
react-highlight-words: "npm:0.20.0"
|
||||||
react-hook-form: "npm:7.5.3"
|
react-hook-form: "npm:^7.49.2"
|
||||||
react-i18next: "npm:^12.0.0"
|
react-i18next: "npm:^12.0.0"
|
||||||
react-inlinesvg: "npm:3.0.2"
|
react-inlinesvg: "npm:3.0.2"
|
||||||
react-loading-skeleton: "npm:3.3.1"
|
react-loading-skeleton: "npm:3.3.1"
|
||||||
@@ -25683,12 +25683,12 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-hook-form@npm:7.5.3":
|
"react-hook-form@npm:^7.49.2":
|
||||||
version: 7.5.3
|
version: 7.49.2
|
||||||
resolution: "react-hook-form@npm:7.5.3"
|
resolution: "react-hook-form@npm:7.49.2"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17
|
react: ^16.8.0 || ^17 || ^18
|
||||||
checksum: ee359714c538ee8f328e147732aba629269849e159a88f96662c2e2d8ca175a6015e5364304bb8bada0249de0c335bafa64835abe09b5db895b702bce9159912
|
checksum: 7895d65b8458c42d46eb338803bb0fd1aab42fc69ecf80b47846eace9493a10cac5b05c9b744a5f9f1f7969a3e2703fc2118cdab97e49a7798a72d09f106383f
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user