import { css } from '@emotion/css'; import React, { useCallback, useEffect, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { isFetchError } from '@grafana/runtime'; import { Field, IconButton, Input, useStyles2, Text } from '@grafana/ui'; export interface Props { value: string; onEdit: (newValue: string) => Promise; } export const EditableTitle = ({ value, onEdit }: Props) => { const styles = useStyles2(getStyles); const [localValue, setLocalValue] = useState(value); const [isEditing, setIsEditing] = useState(false); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(); // sync local value with prop value useEffect(() => { setLocalValue(value); }, [value]); const onCommitChange = useCallback( async (event: React.FormEvent) => { const newValue = event.currentTarget.value; if (!newValue) { setErrorMessage('Please enter a title'); } else if (newValue === value) { // no need to bother saving if the value hasn't changed // just clear any previous error messages and exit edit mode setErrorMessage(undefined); setIsEditing(false); } else { setIsLoading(true); try { await onEdit(newValue); setErrorMessage(undefined); setIsEditing(false); } catch (error) { if (isFetchError(error)) { setErrorMessage(error.data.message); } else if (error instanceof Error) { setErrorMessage(error.message); } } setIsLoading(false); } }, [onEdit, value] ); return !isEditing ? (
{/* use localValue instead of value this is to prevent the title from flickering back to the old value after the user has edited caused by the delay between the save completing and the new value being refetched */} {localValue} setIsEditing(true)} />
) : (
{ if (event.key === 'Enter') { onCommitChange(event); } }} // perfectly reasonable to autofocus here since we've made a conscious choice by clicking the edit button // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus onBlur={onCommitChange} onChange={(event) => setLocalValue(event.currentTarget.value)} onFocus={() => setIsEditing(true)} />
); }; EditableTitle.displayName = 'EditableTitle'; const getStyles = (theme: GrafanaTheme2) => { return { textContainer: css({ minWidth: 0, }), field: css({ flex: 1, // magic number here to ensure the input text lines up exactly with the h1 text // input has a 1px border + theme.spacing(1) padding so we need to offset that left: `calc(-${theme.spacing(1)} - 1px)`, position: 'relative', marginBottom: 0, }), input: css({ input: { ...theme.typography.h1, }, }), inputContainer: css({ display: 'flex', flex: 1, }), textWrapper: css({ alignItems: 'center', display: 'flex', gap: theme.spacing(1), }), }; };