grafana/public/app/features/explore/CorrelationHelper.tsx
Alex Khomenko 99f7110e39
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>
2024-01-05 12:41:49 +02:00

280 lines
9.6 KiB
TypeScript

import { css } from '@emotion/css';
import React, { useState, useEffect, useId } from 'react';
import { useForm } from 'react-hook-form';
import { useAsync } from 'react-use';
import { DataLinkTransformationConfig, ExploreCorrelationHelperData, GrafanaTheme2 } from '@grafana/data';
import {
Collapse,
Alert,
Field,
Input,
Button,
Card,
IconButton,
useStyles2,
DeleteButton,
Tooltip,
Icon,
Stack,
} from '@grafana/ui';
import { useDispatch, useSelector } from 'app/types';
import { getTransformationVars } from '../correlations/transformations';
import { generateDefaultLabel } from '../correlations/utils';
import { CorrelationTransformationAddModal } from './CorrelationTransformationAddModal';
import { changeCorrelationHelperData } from './state/explorePane';
import { changeCorrelationEditorDetails } from './state/main';
import { selectCorrelationDetails, selectPanes } from './state/selectors';
interface Props {
exploreId: string;
correlations: ExploreCorrelationHelperData;
}
interface FormValues {
label: string;
description: string;
}
export const CorrelationHelper = ({ exploreId, correlations }: Props) => {
const dispatch = useDispatch();
const styles = useStyles2(getStyles);
const panes = useSelector(selectPanes);
const panesVals = Object.values(panes);
const { value: defaultLabel, loading: loadingLabel } = useAsync(
async () => await generateDefaultLabel(panesVals[0]!, panesVals[1]!),
[
panesVals[0]?.datasourceInstance,
panesVals[0]?.queries[0].datasource,
panesVals[1]?.datasourceInstance,
panesVals[1]?.queries[0].datasource,
]
);
const { register, watch, getValues, setValue } = useForm<FormValues>();
const [isLabelDescOpen, setIsLabelDescOpen] = useState(false);
const [isTransformOpen, setIsTransformOpen] = useState(false);
const [showTransformationAddModal, setShowTransformationAddModal] = useState(false);
const [transformations, setTransformations] = useState<DataLinkTransformationConfig[]>([]);
const [transformationIdxToEdit, setTransformationIdxToEdit] = useState<number | undefined>(undefined);
const correlationDetails = useSelector(selectCorrelationDetails);
const id = useId();
// only fire once on mount to allow save button to enable / disable when unmounted
useEffect(() => {
dispatch(changeCorrelationEditorDetails({ canSave: true }));
return () => {
dispatch(changeCorrelationEditorDetails({ canSave: false }));
};
}, [dispatch]);
useEffect(() => {
if (
!loadingLabel &&
defaultLabel !== undefined &&
!correlationDetails?.correlationDirty &&
getValues('label') !== ''
) {
setValue('label', defaultLabel);
}
}, [correlationDetails?.correlationDirty, defaultLabel, getValues, loadingLabel, setValue]);
useEffect(() => {
const subscription = watch((value) => {
let dirty = correlationDetails?.correlationDirty || false;
let description = value.description || '';
if (!dirty && (value.label !== defaultLabel || description !== '')) {
dirty = true;
} else if (dirty && value.label === defaultLabel && description.trim() === '') {
dirty = false;
}
dispatch(
changeCorrelationEditorDetails({ label: value.label, description: value.description, correlationDirty: dirty })
);
});
return () => subscription.unsubscribe();
}, [correlationDetails?.correlationDirty, defaultLabel, dispatch, watch]);
useEffect(() => {
const dirty =
!correlationDetails?.correlationDirty && transformations.length > 0 ? true : correlationDetails?.correlationDirty;
dispatch(changeCorrelationEditorDetails({ transformations: transformations, correlationDirty: dirty }));
let transVarRecords: Record<string, string> = {};
transformations.forEach((transformation) => {
const transformationVars = getTransformationVars(
{
type: transformation.type,
expression: transformation.expression,
mapValue: transformation.mapValue,
},
correlations.vars[transformation.field!],
transformation.field!
);
Object.keys(transformationVars).forEach((key) => {
transVarRecords[key] = transformationVars[key]?.value;
});
});
dispatch(
changeCorrelationHelperData({
exploreId: exploreId,
correlationEditorHelperData: {
resultField: correlations.resultField,
origVars: correlations.origVars,
vars: { ...correlations.origVars, ...transVarRecords },
},
})
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch, transformations]);
return (
<>
{showTransformationAddModal && (
<CorrelationTransformationAddModal
onCancel={() => {
setTransformationIdxToEdit(undefined);
setShowTransformationAddModal(false);
}}
onSave={(transformation: DataLinkTransformationConfig) => {
if (transformationIdxToEdit !== undefined) {
const editTransformations = [...transformations];
editTransformations[transformationIdxToEdit] = transformation;
setTransformations(editTransformations);
setTransformationIdxToEdit(undefined);
} else {
setTransformations([...transformations, transformation]);
}
setShowTransformationAddModal(false);
}}
fieldList={correlations.origVars}
transformationToEdit={
transformationIdxToEdit !== undefined ? transformations[transformationIdxToEdit] : undefined
}
/>
)}
<Alert title="Correlation details" severity="info">
The correlation link will appear by the <code>{correlations.resultField}</code> field. You can use the following
variables to set up your correlations:
<pre>
{Object.entries(correlations.vars).map((entry) => {
return `\$\{${entry[0]}\} = ${entry[1]}\n`;
})}
</pre>
<Collapse
collapsible
isOpen={isLabelDescOpen}
onToggle={() => {
setIsLabelDescOpen(!isLabelDescOpen);
}}
label={
<Stack gap={1} direction="row" wrap="wrap" alignItems="center">
Label / Description
{!isLabelDescOpen && !loadingLabel && (
<span className={styles.labelCollapseDetails}>{`Label: ${getValues('label') || defaultLabel}`}</span>
)}
</Stack>
}
>
<Field label="Label" htmlFor={`${id}-label`}>
<Input
{...register('label')}
id={`${id}-label`}
onBlur={() => {
if (getValues('label') === '' && defaultLabel !== undefined) {
setValue('label', defaultLabel);
}
}}
/>
</Field>
<Field label="Description" htmlFor={`${id}-description`}>
<Input {...register('description')} id={`${id}-description`} />
</Field>
</Collapse>
<Collapse
collapsible
isOpen={isTransformOpen}
onToggle={() => {
setIsTransformOpen(!isTransformOpen);
}}
label={
<Stack gap={1} direction="row" wrap="wrap" alignItems="center">
Transformations
<Tooltip content="A transformation extracts one or more variables out of a single field.">
<Icon name="info-circle" size="sm" />
</Tooltip>
</Stack>
}
>
<Button
variant="secondary"
fill="outline"
onClick={() => {
setShowTransformationAddModal(true);
}}
className={styles.transformationAction}
>
Add transformation
</Button>
{transformations.map((transformation, i) => {
const { type, field, expression, mapValue } = transformation;
const detailsString = [
(mapValue ?? '').length > 0 ? `Variable name: ${mapValue}` : undefined,
(expression ?? '').length > 0 ? (
<>
Expression: <code>{expression}</code>
</>
) : undefined,
].filter((val) => val);
return (
<Card key={`trans-${i}`}>
<Card.Heading>
{field}: {type}
</Card.Heading>
{detailsString.length > 0 && (
<Card.Meta className={styles.transformationMeta}>{detailsString}</Card.Meta>
)}
<Card.SecondaryActions>
<IconButton
key="edit"
name="edit"
aria-label="edit transformation"
onClick={() => {
setTransformationIdxToEdit(i);
setShowTransformationAddModal(true);
}}
/>
<DeleteButton
aria-label="delete transformation"
onConfirm={() => setTransformations(transformations.filter((_, idx) => i !== idx))}
closeOnConfirm
/>
</Card.SecondaryActions>
</Card>
);
})}
</Collapse>
</Alert>
</>
);
};
const getStyles = (theme: GrafanaTheme2) => {
return {
labelCollapseDetails: css({
marginLeft: theme.spacing(2),
...theme.typography['bodySmall'],
fontStyle: 'italic',
}),
transformationAction: css({
marginBottom: theme.spacing(2),
}),
transformationMeta: css({
alignItems: 'baseline',
}),
};
};