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:
Alex Khomenko
2024-01-05 12:41:49 +02:00
committed by GitHub
parent 3537c5440f
commit 99f7110e39
29 changed files with 147 additions and 107 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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.*/

View File

@@ -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

View File

@@ -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}

View File

@@ -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>

View File

@@ -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);
}, },

View File

@@ -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.';

View File

@@ -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);
} }

View File

@@ -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.'

View File

@@ -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"
/> />

View File

@@ -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,

View File

@@ -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>;

View File

@@ -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;
} }

View File

@@ -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.',
}, },

View File

@@ -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;

View File

@@ -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();

View File

@@ -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);

View File

@@ -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;

View File

@@ -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}

View File

@@ -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.

View File

@@ -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[];
} }

View File

@@ -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(

View File

@@ -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);

View File

@@ -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

View File

@@ -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[] = [];

View File

@@ -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(),
}, },
})); }));

View File

@@ -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: '' };

View File

@@ -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