mirror of
https://github.com/grafana/grafana.git
synced 2025-02-15 01:53:33 -06:00
123 lines
3.7 KiB
TypeScript
123 lines
3.7 KiB
TypeScript
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<void>;
|
|
}
|
|
|
|
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<string>();
|
|
|
|
// sync local value with prop value
|
|
useEffect(() => {
|
|
setLocalValue(value);
|
|
}, [value]);
|
|
|
|
const onCommitChange = useCallback(
|
|
async (event: React.FormEvent<HTMLInputElement>) => {
|
|
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 ? (
|
|
<div className={styles.textContainer}>
|
|
<div className={styles.textWrapper}>
|
|
{/*
|
|
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
|
|
*/}
|
|
<Text element="h1" truncate>
|
|
{localValue}
|
|
</Text>
|
|
<IconButton name="pen" size="lg" tooltip="Edit title" onClick={() => setIsEditing(true)} />
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className={styles.inputContainer}>
|
|
<Field className={styles.field} loading={isLoading} invalid={!!errorMessage} error={errorMessage}>
|
|
<Input
|
|
className={styles.input}
|
|
defaultValue={localValue}
|
|
onKeyDown={(event) => {
|
|
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)}
|
|
/>
|
|
</Field>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
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),
|
|
}),
|
|
};
|
|
};
|