QueryEditorRowHeader.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import { css, cx } from '@emotion/css';
  2. import React, { ReactNode, useState } from 'react';
  3. import { DataQuery, DataSourceInstanceSettings, GrafanaTheme } from '@grafana/data';
  4. import { selectors } from '@grafana/e2e-selectors';
  5. import { DataSourcePicker } from '@grafana/runtime';
  6. import { Icon, Input, FieldValidationMessage, useStyles } from '@grafana/ui';
  7. export interface Props<TQuery extends DataQuery = DataQuery> {
  8. query: TQuery;
  9. queries: TQuery[];
  10. disabled?: boolean;
  11. dataSource: DataSourceInstanceSettings;
  12. renderExtras?: () => ReactNode;
  13. onChangeDataSource?: (settings: DataSourceInstanceSettings) => void;
  14. onChange: (query: TQuery) => void;
  15. onClick: (e: React.MouseEvent) => void;
  16. collapsedText: string | null;
  17. alerting?: boolean;
  18. }
  19. export const QueryEditorRowHeader = <TQuery extends DataQuery>(props: Props<TQuery>) => {
  20. const { query, queries, onClick, onChange, collapsedText, renderExtras, disabled } = props;
  21. const styles = useStyles(getStyles);
  22. const [isEditing, setIsEditing] = useState<boolean>(false);
  23. const [validationError, setValidationError] = useState<string | null>(null);
  24. const onEditQuery = (event: React.SyntheticEvent) => {
  25. setIsEditing(true);
  26. };
  27. const onEndEditName = (newName: string) => {
  28. setIsEditing(false);
  29. // Ignore change if invalid
  30. if (validationError) {
  31. setValidationError(null);
  32. return;
  33. }
  34. if (query.refId !== newName) {
  35. onChange({
  36. ...query,
  37. refId: newName,
  38. });
  39. }
  40. };
  41. const onInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
  42. const newName = event.currentTarget.value.trim();
  43. if (newName.length === 0) {
  44. setValidationError('An empty query name is not allowed');
  45. return;
  46. }
  47. for (const otherQuery of queries) {
  48. if (otherQuery !== query && newName === otherQuery.refId) {
  49. setValidationError('Query name already exists');
  50. return;
  51. }
  52. }
  53. if (validationError) {
  54. setValidationError(null);
  55. }
  56. };
  57. const onEditQueryBlur = (event: React.SyntheticEvent<HTMLInputElement>) => {
  58. onEndEditName(event.currentTarget.value.trim());
  59. };
  60. const onKeyDown = (event: React.KeyboardEvent) => {
  61. if (event.key === 'Enter') {
  62. onEndEditName((event.target as any).value);
  63. }
  64. };
  65. const onFocus = (event: React.FocusEvent<HTMLInputElement>) => {
  66. event.target.select();
  67. };
  68. return (
  69. <>
  70. <div className={styles.wrapper}>
  71. {!isEditing && (
  72. <button
  73. className={styles.queryNameWrapper}
  74. aria-label={selectors.components.QueryEditorRow.title(query.refId)}
  75. title="Edit query name"
  76. onClick={onEditQuery}
  77. data-testid="query-name-div"
  78. >
  79. <span className={styles.queryName}>{query.refId}</span>
  80. <Icon name="pen" className={styles.queryEditIcon} size="sm" />
  81. </button>
  82. )}
  83. {isEditing && (
  84. <>
  85. <Input
  86. type="text"
  87. defaultValue={query.refId}
  88. onBlur={onEditQueryBlur}
  89. autoFocus
  90. onKeyDown={onKeyDown}
  91. onFocus={onFocus}
  92. invalid={validationError !== null}
  93. onChange={onInputChange}
  94. className={styles.queryNameInput}
  95. data-testid="query-name-input"
  96. />
  97. {validationError && <FieldValidationMessage horizontal>{validationError}</FieldValidationMessage>}
  98. </>
  99. )}
  100. {renderDataSource(props, styles)}
  101. {renderExtras && <div className={styles.itemWrapper}>{renderExtras()}</div>}
  102. {disabled && <em className={styles.contextInfo}>Disabled</em>}
  103. </div>
  104. {collapsedText && (
  105. <div className={styles.collapsedText} onClick={onClick}>
  106. {collapsedText}
  107. </div>
  108. )}
  109. </>
  110. );
  111. };
  112. const renderDataSource = <TQuery extends DataQuery>(
  113. props: Props<TQuery>,
  114. styles: ReturnType<typeof getStyles>
  115. ): ReactNode => {
  116. const { alerting, dataSource, onChangeDataSource } = props;
  117. if (!onChangeDataSource) {
  118. return <em className={styles.contextInfo}>({dataSource.name})</em>;
  119. }
  120. return (
  121. <div className={styles.itemWrapper}>
  122. <DataSourcePicker variables={true} alerting={alerting} current={dataSource.name} onChange={onChangeDataSource} />
  123. </div>
  124. );
  125. };
  126. const getStyles = (theme: GrafanaTheme) => {
  127. return {
  128. wrapper: css`
  129. label: Wrapper;
  130. display: flex;
  131. align-items: center;
  132. margin-left: ${theme.spacing.xs};
  133. `,
  134. queryNameWrapper: css`
  135. display: flex;
  136. cursor: pointer;
  137. border: 1px solid transparent;
  138. border-radius: ${theme.border.radius.md};
  139. align-items: center;
  140. padding: 0 0 0 ${theme.spacing.xs};
  141. margin: 0;
  142. background: transparent;
  143. &:hover {
  144. background: ${theme.colors.bg3};
  145. border: 1px dashed ${theme.colors.border3};
  146. }
  147. &:focus {
  148. border: 2px solid ${theme.colors.formInputBorderActive};
  149. }
  150. &:hover,
  151. &:focus {
  152. .query-name-edit-icon {
  153. visibility: visible;
  154. }
  155. }
  156. `,
  157. queryName: css`
  158. font-weight: ${theme.typography.weight.semibold};
  159. color: ${theme.colors.textBlue};
  160. cursor: pointer;
  161. overflow: hidden;
  162. margin-left: ${theme.spacing.xs};
  163. `,
  164. queryEditIcon: cx(
  165. css`
  166. margin-left: ${theme.spacing.md};
  167. visibility: hidden;
  168. `,
  169. 'query-name-edit-icon'
  170. ),
  171. queryNameInput: css`
  172. max-width: 300px;
  173. margin: -4px 0;
  174. `,
  175. collapsedText: css`
  176. font-weight: ${theme.typography.weight.regular};
  177. font-size: ${theme.typography.size.sm};
  178. color: ${theme.colors.textWeak};
  179. padding-left: ${theme.spacing.sm};
  180. align-items: center;
  181. overflow: hidden;
  182. font-style: italic;
  183. white-space: nowrap;
  184. text-overflow: ellipsis;
  185. `,
  186. contextInfo: css`
  187. font-size: ${theme.typography.size.sm};
  188. font-style: italic;
  189. color: ${theme.colors.textWeak};
  190. padding-left: 10px;
  191. `,
  192. itemWrapper: css`
  193. display: flex;
  194. margin-left: 4px;
  195. `,
  196. };
  197. };