FieldToConfigMappingEditor.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import { css } from '@emotion/css';
  2. import { capitalize } from 'lodash';
  3. import React from 'react';
  4. import { DataFrame, getFieldDisplayName, GrafanaTheme2, ReducerID, SelectableValue } from '@grafana/data';
  5. import { Select, StatsPicker, useStyles2 } from '@grafana/ui';
  6. import {
  7. configMapHandlers,
  8. evaluteFieldMappings,
  9. FieldToConfigMapHandler,
  10. FieldToConfigMapping,
  11. lookUpConfigHandler as findConfigHandlerFor,
  12. } from '../fieldToConfigMapping/fieldToConfigMapping';
  13. export interface Props {
  14. frame: DataFrame;
  15. mappings: FieldToConfigMapping[];
  16. onChange: (mappings: FieldToConfigMapping[]) => void;
  17. withReducers?: boolean;
  18. withNameAndValue?: boolean;
  19. }
  20. export function FieldToConfigMappingEditor({ frame, mappings, onChange, withReducers, withNameAndValue }: Props) {
  21. const styles = useStyles2(getStyles);
  22. const rows = getViewModelRows(frame, mappings, withNameAndValue);
  23. const configProps = configMapHandlers.map((def) => configHandlerToSelectOption(def, false)) as Array<
  24. SelectableValue<string>
  25. >;
  26. const onChangeConfigProperty = (row: FieldToConfigRowViewModel, value: SelectableValue<string | null>) => {
  27. const existingIdx = mappings.findIndex((x) => x.fieldName === row.fieldName);
  28. if (value) {
  29. if (existingIdx !== -1) {
  30. const update = [...mappings];
  31. update.splice(existingIdx, 1, { ...mappings[existingIdx], handlerKey: value.value! });
  32. onChange(update);
  33. } else {
  34. onChange([...mappings, { fieldName: row.fieldName, handlerKey: value.value! }]);
  35. }
  36. } else {
  37. if (existingIdx !== -1) {
  38. onChange(mappings.filter((x, index) => index !== existingIdx));
  39. } else {
  40. onChange([...mappings, { fieldName: row.fieldName, handlerKey: '__ignore' }]);
  41. }
  42. }
  43. };
  44. const onChangeReducer = (row: FieldToConfigRowViewModel, reducerId: ReducerID) => {
  45. const existingIdx = mappings.findIndex((x) => x.fieldName === row.fieldName);
  46. if (existingIdx !== -1) {
  47. const update = [...mappings];
  48. update.splice(existingIdx, 1, { ...mappings[existingIdx], reducerId });
  49. onChange(update);
  50. } else {
  51. onChange([...mappings, { fieldName: row.fieldName, handlerKey: row.handlerKey, reducerId }]);
  52. }
  53. };
  54. return (
  55. <table className={styles.table}>
  56. <thead>
  57. <tr>
  58. <th>Field</th>
  59. <th>Use as</th>
  60. {withReducers && <th>Select</th>}
  61. </tr>
  62. </thead>
  63. <tbody>
  64. {rows.map((row) => (
  65. <tr key={row.fieldName}>
  66. <td className={styles.labelCell}>{row.fieldName}</td>
  67. <td className={styles.selectCell} data-testid={`${row.fieldName}-config-key`}>
  68. <Select
  69. options={configProps}
  70. value={row.configOption}
  71. placeholder={row.placeholder}
  72. isClearable={true}
  73. onChange={(value) => onChangeConfigProperty(row, value)}
  74. />
  75. </td>
  76. {withReducers && (
  77. <td data-testid={`${row.fieldName}-reducer`} className={styles.selectCell}>
  78. <StatsPicker
  79. stats={[row.reducerId]}
  80. defaultStat={row.reducerId}
  81. onChange={(stats: string[]) => onChangeReducer(row, stats[0] as ReducerID)}
  82. />
  83. </td>
  84. )}
  85. </tr>
  86. ))}
  87. </tbody>
  88. </table>
  89. );
  90. }
  91. interface FieldToConfigRowViewModel {
  92. handlerKey: string | null;
  93. fieldName: string;
  94. configOption: SelectableValue<string | null> | null;
  95. placeholder?: string;
  96. missingInFrame?: boolean;
  97. reducerId: string;
  98. }
  99. function getViewModelRows(
  100. frame: DataFrame,
  101. mappings: FieldToConfigMapping[],
  102. withNameAndValue?: boolean
  103. ): FieldToConfigRowViewModel[] {
  104. const rows: FieldToConfigRowViewModel[] = [];
  105. const mappingResult = evaluteFieldMappings(frame, mappings ?? [], withNameAndValue);
  106. for (const field of frame.fields) {
  107. const fieldName = getFieldDisplayName(field, frame);
  108. const mapping = mappingResult.index[fieldName];
  109. const option = configHandlerToSelectOption(mapping.handler, mapping.automatic);
  110. rows.push({
  111. fieldName,
  112. configOption: mapping.automatic ? null : option,
  113. placeholder: mapping.automatic ? option?.label : 'Choose',
  114. handlerKey: mapping.handler?.key ?? null,
  115. reducerId: mapping.reducerId,
  116. });
  117. }
  118. // Add rows for mappings that have no matching field
  119. for (const mapping of mappings) {
  120. if (!rows.find((x) => x.fieldName === mapping.fieldName)) {
  121. const handler = findConfigHandlerFor(mapping.handlerKey);
  122. rows.push({
  123. fieldName: mapping.fieldName,
  124. handlerKey: mapping.handlerKey,
  125. configOption: configHandlerToSelectOption(handler, false),
  126. missingInFrame: true,
  127. reducerId: mapping.reducerId ?? ReducerID.lastNotNull,
  128. });
  129. }
  130. }
  131. return Object.values(rows);
  132. }
  133. function configHandlerToSelectOption(
  134. def: FieldToConfigMapHandler | null,
  135. isAutomatic: boolean
  136. ): SelectableValue<string> | null {
  137. if (!def) {
  138. return null;
  139. }
  140. let name = def.name ?? capitalize(def.key);
  141. if (isAutomatic) {
  142. name = `${name} (auto)`;
  143. }
  144. return {
  145. label: name,
  146. value: def.key,
  147. };
  148. }
  149. const getStyles = (theme: GrafanaTheme2) => ({
  150. table: css`
  151. margin-top: ${theme.spacing(1)};
  152. td,
  153. th {
  154. border-right: 4px solid ${theme.colors.background.primary};
  155. border-bottom: 4px solid ${theme.colors.background.primary};
  156. white-space: nowrap;
  157. }
  158. th {
  159. font-size: ${theme.typography.bodySmall.fontSize};
  160. line-height: ${theme.spacing(4)};
  161. padding: ${theme.spacing(0, 1)};
  162. }
  163. `,
  164. labelCell: css`
  165. font-size: ${theme.typography.bodySmall.fontSize};
  166. background: ${theme.colors.background.secondary};
  167. padding: ${theme.spacing(0, 1)};
  168. max-width: 400px;
  169. overflow: hidden;
  170. text-overflow: ellipsis;
  171. min-width: 140px;
  172. `,
  173. selectCell: css`
  174. padding: 0;
  175. min-width: 161px;
  176. `,
  177. });