Files
grafana/public/app/features/query/components/QueryEditorRowHeader.tsx
Hugo Häggmark 2b8c74de2e i18n: removes useTranslate hook (#106556)
* i18n: removes useTranslate hook

* chore: fix duplicate imports

* chore: fix import sorting and hook dependencies
2025-06-12 11:03:52 +02:00

234 lines
6.6 KiB
TypeScript

import { css, cx } from '@emotion/css';
import * as React from 'react';
import { ReactNode, useState } from 'react';
import { DataQuery, DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { FieldValidationMessage, Icon, Input, useStyles2 } from '@grafana/ui';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
export interface Props<TQuery extends DataQuery = DataQuery> {
query: TQuery;
queries: TQuery[];
hidden?: boolean;
dataSource: DataSourceInstanceSettings;
renderExtras?: () => ReactNode;
onChangeDataSource?: (settings: DataSourceInstanceSettings) => void;
onChange: (query: TQuery) => void;
collapsedText: string | null;
alerting?: boolean;
hideRefId?: boolean;
}
export const QueryEditorRowHeader = <TQuery extends DataQuery>(props: Props<TQuery>) => {
const { query, queries, onChange, collapsedText, renderExtras, hidden, hideRefId = false } = props;
const styles = useStyles2(getStyles);
const [isEditing, setIsEditing] = useState<boolean>(false);
const [validationError, setValidationError] = useState<string | null>(null);
const onEditQuery = (event: React.SyntheticEvent) => {
setIsEditing(true);
};
const onEndEditName = (newName: string) => {
setIsEditing(false);
// Ignore change if invalid
if (validationError) {
setValidationError(null);
return;
}
if (query.refId !== newName) {
onChange({
...query,
refId: newName,
});
}
};
const onInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
const newName = event.currentTarget.value.trim();
if (newName.length === 0) {
setValidationError('An empty query name is not allowed');
return;
}
for (const otherQuery of queries) {
if (otherQuery !== query && newName === otherQuery.refId) {
setValidationError('Query name already exists');
return;
}
}
if (validationError) {
setValidationError(null);
}
};
const onEditQueryBlur = (event: React.SyntheticEvent<HTMLInputElement>) => {
onEndEditName(event.currentTarget.value.trim());
};
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
onEndEditName(event.currentTarget.value);
}
};
const onFocus = (event: React.FocusEvent<HTMLInputElement>) => {
event.target.select();
};
return (
<>
<div className={styles.wrapper}>
{!hideRefId && !isEditing && (
<button
className={styles.queryNameWrapper}
aria-label={selectors.components.QueryEditorRow.title(query.refId)}
title={t('query.query-editor-row-header.query-name-div-title-edit-query-name', 'Edit query name')}
onClick={onEditQuery}
data-testid="query-name-div"
type="button"
>
<span className={styles.queryName}>{query.refId}</span>
<Icon name="pen" className={styles.queryEditIcon} size="sm" />
</button>
)}
{!hideRefId && isEditing && (
<>
<Input
type="text"
defaultValue={query.refId}
onBlur={onEditQueryBlur}
autoFocus
onKeyDown={onKeyDown}
onFocus={onFocus}
invalid={validationError !== null}
onChange={onInputChange}
className={styles.queryNameInput}
data-testid="query-name-input"
/>
{validationError && <FieldValidationMessage horizontal>{validationError}</FieldValidationMessage>}
</>
)}
{renderDataSource(props, styles)}
{renderExtras && <div className={styles.itemWrapper}>{renderExtras()}</div>}
{hidden && (
<em className={styles.contextInfo}>
<Trans i18nKey="query.query-editor-row-header.hidden">Hidden</Trans>
</em>
)}
</div>
{collapsedText && <div className={styles.collapsedText}>{collapsedText}</div>}
</>
);
};
const renderDataSource = <TQuery extends DataQuery>(
props: Props<TQuery>,
styles: ReturnType<typeof getStyles>
): ReactNode => {
const { alerting, dataSource, onChangeDataSource } = props;
if (!onChangeDataSource) {
return <em className={styles.contextInfo}>({dataSource.name})</em>;
}
return (
<div className={styles.itemWrapper}>
<DataSourcePicker
dashboard={true}
variables={true}
alerting={alerting}
current={dataSource.name}
onChange={onChangeDataSource}
/>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => {
return {
wrapper: css({
label: 'Wrapper',
display: 'flex',
alignItems: 'center',
marginLeft: theme.spacing(0.5),
overflow: 'hidden',
}),
queryNameWrapper: css({
display: 'flex',
cursor: 'pointer',
border: '1px solid transparent',
borderRadius: theme.shape.radius.default,
alignItems: 'center',
padding: theme.spacing(0, 0, 0, 0.5),
margin: 0,
background: 'transparent',
overflow: 'hidden',
'&:hover': {
background: theme.colors.action.hover,
border: `1px dashed ${theme.colors.border.strong}`,
},
'&:focus': {
border: `2px solid ${theme.colors.primary.border}`,
},
'&:hover, &:focus': {
'.query-name-edit-icon': {
visibility: 'visible',
},
},
}),
queryName: css({
fontWeight: theme.typography.fontWeightMedium,
color: theme.colors.primary.text,
cursor: 'pointer',
overflow: 'hidden',
marginLeft: theme.spacing(0.5),
}),
queryEditIcon: cx(
css({
marginLeft: theme.spacing(2),
visibility: 'hidden',
}),
'query-name-edit-icon'
),
queryNameInput: css({
maxWidth: '300px',
margin: '-4px 0',
}),
collapsedText: css({
fontWeight: theme.typography.fontWeightRegular,
fontSize: theme.typography.bodySmall.fontSize,
color: theme.colors.text.secondary,
paddingLeft: theme.spacing(1),
alignItems: 'center',
overflow: 'hidden',
fontStyle: 'italic',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}),
contextInfo: css({
fontSize: theme.typography.bodySmall.fontSize,
fontStyle: 'italic',
color: theme.colors.text.secondary,
paddingLeft: '10px',
paddingRight: '10px',
}),
itemWrapper: css({
display: 'flex',
marginLeft: '4px',
}),
};
};