Variables: Add confirmation modal when deleting variables (#56016)

This commit is contained in:
kay delaney 2022-09-30 16:32:14 +01:00 committed by GitHub
parent e2ef41be72
commit 81a39b7e5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 100 additions and 14 deletions

View File

@ -1,4 +1,4 @@
import { css } from '@emotion/css'; import { css, cx } from '@emotion/css';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
@ -25,6 +25,8 @@ export interface ConfirmModalProps {
dismissText?: string; dismissText?: string;
/** Icon for the modal header */ /** Icon for the modal header */
icon?: IconName; icon?: IconName;
/** Additional styling for modal container */
modalClass?: string;
/** Text user needs to fill in before confirming */ /** Text user needs to fill in before confirming */
confirmationText?: string; confirmationText?: string;
/** Text for alternative button */ /** Text for alternative button */
@ -46,6 +48,7 @@ export const ConfirmModal = ({
confirmationText, confirmationText,
dismissText = 'Cancel', dismissText = 'Cancel',
alternativeText, alternativeText,
modalClass,
icon = 'exclamation-triangle', icon = 'exclamation-triangle',
onConfirm, onConfirm,
onDismiss, onDismiss,
@ -66,7 +69,7 @@ export const ConfirmModal = ({
}, [isOpen]); }, [isOpen]);
return ( return (
<Modal className={styles.modal} title={title} icon={icon} isOpen={isOpen} onDismiss={onDismiss}> <Modal className={cx(styles.modal, modalClass)} title={title} icon={icon} isOpen={isOpen} onDismiss={onDismiss}>
<div className={styles.modalText}> <div className={styles.modalText}>
{body} {body}
{description ? <div className={styles.modalDescription}>{description}</div> : null} {description ? <div className={styles.modalDescription}>{description}</div> : null}

View File

@ -0,0 +1,34 @@
import { css } from '@emotion/css';
import React from 'react';
import { ConfirmModal } from '@grafana/ui';
interface Props {
varName: string;
isOpen: boolean;
onConfirm: () => void;
onDismiss: () => void;
}
export function ConfirmDeleteModal({ varName, isOpen = false, onConfirm, onDismiss }: Props) {
return (
<ConfirmModal
title="Delete variable"
isOpen={isOpen}
onConfirm={onConfirm}
onDismiss={onDismiss}
body={`
Are you sure you want to delete variable "${varName}"?
`}
modalClass={styles.modal}
confirmText="Delete"
/>
);
}
const styles = {
modal: css({
width: 'max-content',
maxWidth: '80vw',
}),
};

View File

@ -14,6 +14,7 @@ import { changeVariableOrder, duplicateVariable, removeVariable } from '../state
import { KeyedVariableIdentifier } from '../state/types'; import { KeyedVariableIdentifier } from '../state/types';
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils'; import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
import { ConfirmDeleteModal } from './ConfirmDeleteModal';
import { VariableEditorEditor } from './VariableEditorEditor'; import { VariableEditorEditor } from './VariableEditorEditor';
import { VariableEditorList } from './VariableEditorList'; import { VariableEditorList } from './VariableEditorList';
import { createNewVariable, initListMode } from './actions'; import { createNewVariable, initListMode } from './actions';
@ -60,7 +61,15 @@ const connector = connect(mapStateToProps, mapDispatchToProps);
type Props = OwnProps & ConnectedProps<typeof connector>; type Props = OwnProps & ConnectedProps<typeof connector>;
class VariableEditorContainerUnconnected extends PureComponent<Props> { interface State {
variableId?: KeyedVariableIdentifier;
}
class VariableEditorContainerUnconnected extends PureComponent<Props, State> {
state: State = {
variableId: undefined,
};
componentDidMount() { componentDidMount() {
this.props.initListMode(this.props.dashboard.uid); this.props.initListMode(this.props.dashboard.uid);
} }
@ -82,8 +91,17 @@ class VariableEditorContainerUnconnected extends PureComponent<Props> {
this.props.duplicateVariable(identifier); this.props.duplicateVariable(identifier);
}; };
onRemoveVariable = (identifier: KeyedVariableIdentifier) => { onModalOpen = (identifier: KeyedVariableIdentifier) => {
this.props.removeVariable(identifier); this.setState({ variableId: identifier });
};
onModalClose = () => {
this.setState({ variableId: undefined });
};
onRemoveVariable = () => {
this.props.removeVariable(this.state.variableId!);
this.onModalClose();
}; };
render() { render() {
@ -100,7 +118,7 @@ class VariableEditorContainerUnconnected extends PureComponent<Props> {
onEdit={this.onEditVariable} onEdit={this.onEditVariable}
onChangeOrder={this.onChangeVariableOrder} onChangeOrder={this.onChangeVariableOrder}
onDuplicate={this.onDuplicateVariable} onDuplicate={this.onDuplicateVariable}
onDelete={this.onRemoveVariable} onDelete={this.onModalOpen}
usages={this.props.usages} usages={this.props.usages}
usagesNetwork={this.props.usagesNetwork} usagesNetwork={this.props.usagesNetwork}
/> />
@ -109,6 +127,12 @@ class VariableEditorContainerUnconnected extends PureComponent<Props> {
<VariablesUnknownTable variables={this.props.variables} dashboard={this.props.dashboard} /> <VariablesUnknownTable variables={this.props.variables} dashboard={this.props.dashboard} />
)} )}
{variableToEdit && <VariableEditorEditor identifier={toKeyedVariableIdentifier(variableToEdit)} />} {variableToEdit && <VariableEditorEditor identifier={toKeyedVariableIdentifier(variableToEdit)} />}
<ConfirmDeleteModal
isOpen={this.state.variableId !== undefined}
varName={this.state.variableId?.id ?? ''}
onConfirm={this.onRemoveVariable}
onDismiss={this.onModalClose}
/>
</Page> </Page>
); );
} }

View File

@ -20,6 +20,7 @@ import { KeyedVariableIdentifier } from '../state/types';
import { VariableHide } from '../types'; import { VariableHide } from '../types';
import { toKeyedVariableIdentifier, toVariablePayload } from '../utils'; import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
import { ConfirmDeleteModal } from './ConfirmDeleteModal';
import { VariableHideSelect } from './VariableHideSelect'; import { VariableHideSelect } from './VariableHideSelect';
import { VariableSectionHeader } from './VariableSectionHeader'; import { VariableSectionHeader } from './VariableSectionHeader';
import { VariableTextField } from './VariableTextField'; import { VariableTextField } from './VariableTextField';
@ -61,7 +62,15 @@ export interface OwnProps {
type Props = OwnProps & ConnectedProps<typeof connector>; type Props = OwnProps & ConnectedProps<typeof connector>;
export class VariableEditorEditorUnConnected extends PureComponent<Props> { interface State {
showDeleteModal: boolean;
}
export class VariableEditorEditorUnConnected extends PureComponent<Props, State> {
state: State = {
showDeleteModal: false,
};
componentDidMount(): void { componentDidMount(): void {
this.props.variableEditorMount(this.props.identifier); this.props.variableEditorMount(this.props.identifier);
} }
@ -120,8 +129,17 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
this.props.updateOptions(toKeyedVariableIdentifier(this.props.variable)); this.props.updateOptions(toKeyedVariableIdentifier(this.props.variable));
}; };
onModalOpen = () => {
this.setState({ showDeleteModal: true });
};
onModalClose = () => {
this.setState({ showDeleteModal: false });
};
onDelete = () => { onDelete = () => {
this.props.removeVariable(this.props.identifier); this.props.removeVariable(this.props.identifier);
this.onModalClose();
locationService.partial({ editIndex: null }); locationService.partial({ editIndex: null });
}; };
@ -192,7 +210,7 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
{hasOptions(this.props.variable) ? <VariableValuesPreview variable={this.props.variable} /> : null} {hasOptions(this.props.variable) ? <VariableValuesPreview variable={this.props.variable} /> : null}
<HorizontalGroup spacing="md"> <HorizontalGroup spacing="md">
<Button variant="destructive" onClick={this.onDelete}> <Button variant="destructive" onClick={this.onModalOpen}>
Delete Delete
</Button> </Button>
<Button <Button
@ -212,6 +230,12 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
</HorizontalGroup> </HorizontalGroup>
</VerticalGroup> </VerticalGroup>
</form> </form>
<ConfirmDeleteModal
isOpen={this.state.showDeleteModal}
varName={this.props.editor.name}
onConfirm={this.onDelete}
onDismiss={this.onModalClose}
/>
</div> </div>
); );
} }

View File

@ -40,7 +40,7 @@ export function VariableEditorList({
} }
reportInteraction('Variable drag and drop'); reportInteraction('Variable drag and drop');
const identifier = JSON.parse(result.draggableId); const identifier = JSON.parse(result.draggableId);
onChangeOrder(identifier, result.source.index, result.destination.index); onChangeOrder(identifier, variables[result.source.index].index, variables[result.destination.index].index);
}; };
return ( return (

View File

@ -90,7 +90,8 @@ export const createNewVariable =
(key: string | null | undefined, type: VariableType = 'query'): ThunkResult<void> => (key: string | null | undefined, type: VariableType = 'query'): ThunkResult<void> =>
(dispatch, getState) => { (dispatch, getState) => {
const rootStateKey = toStateKey(key); const rootStateKey = toStateKey(key);
const id = getNextAvailableId(type, getVariablesByKey(rootStateKey, getState())); const varsByKey = getVariablesByKey(rootStateKey, getState());
const id = getNextAvailableId(type, varsByKey);
const identifier: VariableIdentifier = { type, id }; const identifier: VariableIdentifier = { type, id };
const global = false; const global = false;
const index = getNewVariableIndex(rootStateKey, getState()); const index = getNewVariableIndex(rootStateKey, getState());
@ -102,7 +103,7 @@ export const createNewVariable =
toKeyedAction(rootStateKey, addVariable(toVariablePayload<AddVariable>(identifier, { global, model, index }))) toKeyedAction(rootStateKey, addVariable(toVariablePayload<AddVariable>(identifier, { global, model, index })))
); );
locationService.partial({ editIndex: index }); locationService.partial({ editIndex: varsByKey.length });
}; };
export const initListMode = export const initListMode =

View File

@ -67,9 +67,9 @@ const sharedReducerSlice = createSlice({
return; return;
} }
const variableStates = Object.values(state); const variableStates = Object.values(state).sort((a, b) => a.index - b.index);
for (let index = 0; index < variableStates.length; index++) { for (let i = 0; i < variableStates.length; i++) {
variableStates[index].index = index; variableStates[i].index = i;
} }
}, },
duplicateVariable: (state: VariablesState, action: PayloadAction<VariablePayload<{ newId: string }>>) => { duplicateVariable: (state: VariablesState, action: PayloadAction<VariablePayload<{ newId: string }>>) => {