/usr/share/grafana/public/app/features/query/components
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', }), }; };
.
Edit
..
Edit
QueryActionComponent.ts
Edit
QueryEditorRow.test.tsx
Edit
QueryEditorRow.tsx
Edit
QueryEditorRowHeader.test.tsx
Edit
QueryEditorRowHeader.tsx
Edit
QueryEditorRows.test.tsx
Edit
QueryEditorRows.tsx
Edit
QueryErrorAlert.tsx
Edit
QueryGroup.test.tsx
Edit
QueryGroup.tsx
Edit
QueryGroupOptions.tsx
Edit
QueryLibraryEditingContainer.tsx
Edit