mirror of https://github.com/grafana/grafana
Correlations: Add transformations to Explore Editor (#75930)
* Add transformation add modal and use it * Hook up saving * Add transformation vars to var list, show added transformations * Form validation * Remove type assertion, start building out transformation data in helper (WIP) * Style expression better, add delete logic * Add ability to edit, additional styling on transformation card in helper * simplify styling, conditionally run edit set up logic * Keep more field information in function, integrate it with new editor * Show default label on collapsed section, use deleteButton for confirmation of deleting transformations * Change transformation add calculations from function to hook, add label to collapsed header, add transformation tooltip * Make correlation and editor dirty state distinctive and integrate, WIP * Track action pane for more detailed messaging/actions * Better cancel modal logic * Remove changes to adminsitration transformation editor * Remove debugging line * Remove unneeded comment * Add in logic for closing editor mode * Add tests for modal logic * Use state to build vars list for helper * WIP * Fix labels and dirty state * Fix bad message and stop exiting mode if discard action is performed * Fix tests * Update to not use unstable component and tweak default labelpull/77573/head
parent
a2629f3dd3
commit
9e0ca0d113
@ -0,0 +1,240 @@ |
||||
import { css } from '@emotion/css'; |
||||
import React, { useId, useState, useMemo, useEffect } from 'react'; |
||||
import Highlighter from 'react-highlight-words'; |
||||
import { useForm } from 'react-hook-form'; |
||||
|
||||
import { DataLinkTransformationConfig, ScopedVars } from '@grafana/data'; |
||||
import { Button, Field, Icon, Input, InputControl, Label, Modal, Select, Tooltip, Stack } from '@grafana/ui'; |
||||
|
||||
import { |
||||
getSupportedTransTypeDetails, |
||||
getTransformOptions, |
||||
TransformationFieldDetails, |
||||
} from '../correlations/Forms/types'; |
||||
import { getTransformationVars } from '../correlations/transformations'; |
||||
|
||||
interface CorrelationTransformationAddModalProps { |
||||
onCancel: () => void; |
||||
onSave: (transformation: DataLinkTransformationConfig) => void; |
||||
fieldList: Record<string, string>; |
||||
transformationToEdit?: DataLinkTransformationConfig; |
||||
} |
||||
|
||||
interface ShowFormFields { |
||||
expressionDetails: TransformationFieldDetails; |
||||
mapValueDetails: TransformationFieldDetails; |
||||
} |
||||
|
||||
const LabelWithTooltip = ({ label, tooltipText }: { label: string; tooltipText: string }) => ( |
||||
<Stack gap={1} direction="row" wrap="wrap" alignItems="flex-start"> |
||||
<Label>{label}</Label> |
||||
<Tooltip content={tooltipText}> |
||||
<Icon name="info-circle" size="sm" /> |
||||
</Tooltip> |
||||
</Stack> |
||||
); |
||||
|
||||
export const CorrelationTransformationAddModal = ({ |
||||
onSave, |
||||
onCancel, |
||||
fieldList, |
||||
transformationToEdit, |
||||
}: CorrelationTransformationAddModalProps) => { |
||||
const [exampleValue, setExampleValue] = useState<string | undefined>(undefined); |
||||
const [transformationVars, setTransformationVars] = useState<ScopedVars>({}); |
||||
const [formFieldsVis, setFormFieldsVis] = useState<ShowFormFields>({ |
||||
mapValueDetails: { show: false }, |
||||
expressionDetails: { show: false }, |
||||
}); |
||||
const [isExpValid, setIsExpValid] = useState(false); // keep the highlighter from erroring on bad expressions
|
||||
const [validToSave, setValidToSave] = useState(false); |
||||
const { getValues, control, register, watch } = useForm<DataLinkTransformationConfig>({ |
||||
defaultValues: useMemo(() => { |
||||
if (transformationToEdit) { |
||||
const exampleVal = fieldList[transformationToEdit?.field!]; |
||||
setExampleValue(exampleVal); |
||||
if (transformationToEdit?.expression) { |
||||
setIsExpValid(true); |
||||
} |
||||
const transformationTypeDetails = getSupportedTransTypeDetails(transformationToEdit?.type!); |
||||
setFormFieldsVis({ |
||||
mapValueDetails: transformationTypeDetails.mapValueDetails, |
||||
expressionDetails: transformationTypeDetails.expressionDetails, |
||||
}); |
||||
|
||||
const transformationVars = getTransformationVars( |
||||
{ |
||||
type: transformationToEdit?.type!, |
||||
expression: transformationToEdit?.expression, |
||||
mapValue: transformationToEdit?.mapValue, |
||||
}, |
||||
exampleVal || '', |
||||
transformationToEdit?.field! |
||||
); |
||||
setTransformationVars({ ...transformationVars }); |
||||
setValidToSave(true); |
||||
return { |
||||
type: transformationToEdit?.type, |
||||
field: transformationToEdit?.field, |
||||
mapValue: transformationToEdit?.mapValue, |
||||
expression: transformationToEdit?.expression, |
||||
}; |
||||
} else { |
||||
return undefined; |
||||
} |
||||
}, [fieldList, transformationToEdit]), |
||||
}); |
||||
const id = useId(); |
||||
|
||||
useEffect(() => { |
||||
const subscription = watch((formValues) => { |
||||
const expression = formValues.expression; |
||||
let isExpressionValid = false; |
||||
if (expression !== undefined) { |
||||
isExpressionValid = true; |
||||
try { |
||||
new RegExp(expression); |
||||
} catch (e) { |
||||
isExpressionValid = false; |
||||
} |
||||
} else { |
||||
isExpressionValid = !formFieldsVis.expressionDetails.show; |
||||
} |
||||
setIsExpValid(isExpressionValid); |
||||
const transformationVars = getTransformationVars( |
||||
{ |
||||
type: formValues.type, |
||||
expression: isExpressionValid ? expression : '', |
||||
mapValue: formValues.mapValue, |
||||
}, |
||||
fieldList[formValues.field!] || '', |
||||
formValues.field! |
||||
); |
||||
|
||||
const transKeys = Object.keys(transformationVars); |
||||
setTransformationVars(transKeys.length > 0 ? { ...transformationVars } : {}); |
||||
|
||||
if (transKeys.length === 0 || !isExpressionValid) { |
||||
setValidToSave(false); |
||||
} else { |
||||
setValidToSave(true); |
||||
} |
||||
}); |
||||
return () => subscription.unsubscribe(); |
||||
}, [fieldList, formFieldsVis.expressionDetails.show, watch]); |
||||
|
||||
return ( |
||||
<Modal |
||||
isOpen={true} |
||||
title={`${transformationToEdit ? 'Edit' : 'Add'} transformation`} |
||||
onDismiss={onCancel} |
||||
className={css({ width: '700px' })} |
||||
> |
||||
<p> |
||||
A transformation extracts variables out of a single field. These variables will be available along with your |
||||
field variables. |
||||
</p> |
||||
<Field label="Field"> |
||||
<InputControl |
||||
control={control} |
||||
render={({ field: { onChange, ref, ...field } }) => ( |
||||
<Select |
||||
{...field} |
||||
onChange={(value) => { |
||||
if (value.value) { |
||||
onChange(value.value); |
||||
setExampleValue(fieldList[value.value]); |
||||
} |
||||
}} |
||||
options={Object.entries(fieldList).map((entry) => { |
||||
return { label: entry[0], value: entry[0] }; |
||||
})} |
||||
aria-label="field" |
||||
/> |
||||
)} |
||||
name={`field` as const} |
||||
/> |
||||
</Field> |
||||
|
||||
{exampleValue && ( |
||||
<> |
||||
<pre> |
||||
<Highlighter |
||||
textToHighlight={exampleValue} |
||||
searchWords={[isExpValid ? getValues('expression') ?? '' : '']} |
||||
autoEscape={false} |
||||
/> |
||||
</pre> |
||||
<Field label="Type"> |
||||
<InputControl |
||||
control={control} |
||||
render={({ field: { onChange, ref, ...field } }) => ( |
||||
<Select |
||||
{...field} |
||||
onChange={(value) => { |
||||
onChange(value.value); |
||||
const transformationTypeDetails = getSupportedTransTypeDetails(value.value!); |
||||
setFormFieldsVis({ |
||||
mapValueDetails: transformationTypeDetails.mapValueDetails, |
||||
expressionDetails: transformationTypeDetails.expressionDetails, |
||||
}); |
||||
}} |
||||
options={getTransformOptions()} |
||||
aria-label="type" |
||||
/> |
||||
)} |
||||
name={`type` as const} |
||||
/> |
||||
</Field> |
||||
{formFieldsVis.expressionDetails.show && ( |
||||
<Field |
||||
label={ |
||||
formFieldsVis.expressionDetails.helpText ? ( |
||||
<LabelWithTooltip label="Expression" tooltipText={formFieldsVis.expressionDetails.helpText} /> |
||||
) : ( |
||||
'Expression' |
||||
) |
||||
} |
||||
htmlFor={`${id}-expression`} |
||||
required={formFieldsVis.expressionDetails.required} |
||||
> |
||||
<Input {...register('expression')} id={`${id}-expression`} /> |
||||
</Field> |
||||
)} |
||||
{formFieldsVis.mapValueDetails.show && ( |
||||
<Field |
||||
label={ |
||||
formFieldsVis.mapValueDetails.helpText ? ( |
||||
<LabelWithTooltip label="Variable Name" tooltipText={formFieldsVis.mapValueDetails.helpText} /> |
||||
) : ( |
||||
'Variable Name' |
||||
) |
||||
} |
||||
htmlFor={`${id}-mapValue`} |
||||
> |
||||
<Input {...register('mapValue')} id={`${id}-mapValue`} /> |
||||
</Field> |
||||
)} |
||||
{Object.entries(transformationVars).length > 0 && ( |
||||
<> |
||||
This transformation will add the following variables: |
||||
<pre> |
||||
{Object.entries(transformationVars).map((entry) => { |
||||
return `\$\{${entry[0]}\} = ${entry[1]?.value}\n`; |
||||
})} |
||||
</pre> |
||||
</> |
||||
)} |
||||
</> |
||||
)} |
||||
<Modal.ButtonRow> |
||||
<Button variant="secondary" onClick={onCancel} fill="outline"> |
||||
Cancel |
||||
</Button> |
||||
<Button variant="primary" onClick={() => onSave(getValues())} disabled={!validToSave}> |
||||
{transformationToEdit ? 'Edit transformation' : 'Add transformation to correlation'} |
||||
</Button> |
||||
</Modal.ButtonRow> |
||||
</Modal> |
||||
); |
||||
}; |
@ -0,0 +1,39 @@ |
||||
import { CORRELATION_EDITOR_POST_CONFIRM_ACTION } from 'app/types'; |
||||
|
||||
import { showModalMessage } from './correlationEditLogic'; |
||||
|
||||
// note, closing the editor does not care if isLeft is true or not. Both are covered for regression purposes.
|
||||
describe('correlationEditLogic', function () { |
||||
it.each` |
||||
action | isLeft | dirCor | dirQuer | expected |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_PANE} | ${false} | ${false} | ${false} | ${undefined} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_PANE} | ${false} | ${true} | ${false} | ${'Closing the pane will cause the correlation in progress to be lost. Would you like to save before continuing?'} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_PANE} | ${false} | ${false} | ${true} | ${'Closing the pane will lose the changed query. Would you like to save before continuing?'} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_PANE} | ${false} | ${true} | ${true} | ${'Closing the pane will cause the correlation in progress to be lost. Would you like to save before continuing?'} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_PANE} | ${true} | ${false} | ${false} | ${undefined} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_PANE} | ${true} | ${true} | ${false} | ${'Closing the pane will cause the correlation in progress to be lost. Would you like to save before continuing?'} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_PANE} | ${true} | ${false} | ${true} | ${'Closing the pane will cause the query in the right pane to be re-ran and links added to that data. Would you like to save before continuing?'} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_PANE} | ${true} | ${true} | ${true} | ${'Closing the pane will cause the correlation in progress to be lost. Would you like to save before continuing?'} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE} | ${false} | ${false} | ${false} | ${undefined} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE} | ${false} | ${true} | ${false} | ${undefined} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE} | ${false} | ${false} | ${true} | ${'Changing the datasource will lose the changed query. Would you like to save before continuing?'} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE} | ${false} | ${true} | ${true} | ${'Changing the datasource will lose the changed query. Would you like to save before continuing?'} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE} | ${true} | ${false} | ${false} | ${undefined} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE} | ${true} | ${true} | ${false} | ${'Changing the datasource will cause the correlation in progress to be lost. Would you like to save before continuing?'} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE} | ${true} | ${false} | ${true} | ${undefined} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE} | ${true} | ${true} | ${true} | ${'Changing the datasource will cause the correlation in progress to be lost. Would you like to save before continuing?'} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_EDITOR} | ${false} | ${false} | ${false} | ${undefined} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_EDITOR} | ${false} | ${true} | ${false} | ${'Closing the editor will cause the correlation in progress to be lost. Would you like to save before continuing?'} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_EDITOR} | ${false} | ${false} | ${true} | ${'Closing the editor will remove the variables, and your changed query may no longer be valid. Would you like to save before continuing?'} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_EDITOR} | ${false} | ${true} | ${true} | ${'Closing the editor will cause the correlation in progress to be lost. Would you like to save before continuing?'} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_EDITOR} | ${true} | ${false} | ${false} | ${undefined} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_EDITOR} | ${true} | ${true} | ${false} | ${'Closing the editor will cause the correlation in progress to be lost. Would you like to save before continuing?'} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_EDITOR} | ${true} | ${false} | ${true} | ${'Closing the editor will remove the variables, and your changed query may no longer be valid. Would you like to save before continuing?'} |
||||
${CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_EDITOR} | ${true} | ${true} | ${true} | ${'Closing the editor will cause the correlation in progress to be lost. Would you like to save before continuing?'} |
||||
`(
|
||||
"Action $action, isLeft=$isLeft, dirtyCorrelation=$dirCor, dirtyQueryEditor=$dirQuer should return message '$expected'", |
||||
({ action, isLeft, dirCor, dirQuer, expected }) => { |
||||
expect(showModalMessage(action, isLeft, dirCor, dirQuer)).toEqual(expected); |
||||
} |
||||
); |
||||
}); |
@ -0,0 +1,74 @@ |
||||
import { template } from 'lodash'; |
||||
|
||||
import { CORRELATION_EDITOR_POST_CONFIRM_ACTION } from 'app/types'; |
||||
|
||||
enum CONSEQUENCES { |
||||
SOURCE_TARGET_CHANGE = 'cause the query in the right pane to be re-ran and links added to that data', |
||||
FULL_QUERY_LOSS = 'lose the changed query', |
||||
FULL_CORR_LOSS = 'cause the correlation in progress to be lost', |
||||
INVALID_VAR = 'remove the variables, and your changed query may no longer be valid', |
||||
} |
||||
|
||||
// returns a string if the modal should show, with what the message string should be
|
||||
// returns undefined if the modal shouldn't show
|
||||
export const showModalMessage = ( |
||||
action: CORRELATION_EDITOR_POST_CONFIRM_ACTION, |
||||
isActionLeft: boolean, |
||||
dirtyCorrelation: boolean, |
||||
dirtyQueryEditor: boolean |
||||
) => { |
||||
const messageTemplate = template( |
||||
'<%= actionStr %> will <%= consequenceStr %>. Would you like to save before continuing?' |
||||
); |
||||
let actionStr = ''; |
||||
let consequenceStr = ''; |
||||
|
||||
// dirty correlation message always takes priority over dirty query
|
||||
if (action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_PANE) { |
||||
actionStr = 'Closing the pane'; |
||||
if (isActionLeft) { |
||||
if (dirtyCorrelation) { |
||||
consequenceStr = CONSEQUENCES.FULL_CORR_LOSS; |
||||
} else if (dirtyQueryEditor) { |
||||
consequenceStr = CONSEQUENCES.SOURCE_TARGET_CHANGE; |
||||
} else { |
||||
return undefined; |
||||
} |
||||
} else { |
||||
// right pane close
|
||||
if (dirtyCorrelation) { |
||||
consequenceStr = CONSEQUENCES.FULL_CORR_LOSS; |
||||
} else if (dirtyQueryEditor) { |
||||
consequenceStr = CONSEQUENCES.FULL_QUERY_LOSS; |
||||
} else { |
||||
return undefined; |
||||
} |
||||
} |
||||
} else if (action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CHANGE_DATASOURCE) { |
||||
actionStr = 'Changing the datasource'; |
||||
if (isActionLeft) { |
||||
if (dirtyCorrelation) { |
||||
consequenceStr = CONSEQUENCES.FULL_CORR_LOSS; |
||||
} else { |
||||
return undefined; |
||||
} |
||||
} else { |
||||
// right datasource change
|
||||
if (dirtyQueryEditor) { |
||||
consequenceStr = CONSEQUENCES.FULL_QUERY_LOSS; |
||||
} else { |
||||
return undefined; |
||||
} |
||||
} |
||||
} else if (action === CORRELATION_EDITOR_POST_CONFIRM_ACTION.CLOSE_EDITOR) { |
||||
actionStr = 'Closing the editor'; |
||||
if (dirtyCorrelation) { |
||||
consequenceStr = CONSEQUENCES.FULL_CORR_LOSS; |
||||
} else if (dirtyQueryEditor) { |
||||
consequenceStr = CONSEQUENCES.INVALID_VAR; |
||||
} else { |
||||
return undefined; |
||||
} |
||||
} |
||||
return messageTemplate({ actionStr, consequenceStr }); |
||||
}; |
Loading…
Reference in new issue