1import React, { FC, useEffect, useState } from 'react'; 2import { GrafanaTheme2 } from '@grafana/data'; 3import { css } from '@emotion/css'; 4import { Button, Input, useStyles2 } from '@grafana/ui'; 5import { ActionIcon } from '../../../rules/ActionIcon'; 6 7interface Props { 8 value?: Record<string, string>; 9 readOnly?: boolean; 10 onChange: (value: Record<string, string>) => void; 11} 12 13export const KeyValueMapInput: FC<Props> = ({ value, onChange, readOnly = false }) => { 14 const styles = useStyles2(getStyles); 15 const [pairs, setPairs] = useState(recordToPairs(value)); 16 useEffect(() => setPairs(recordToPairs(value)), [value]); 17 18 const emitChange = (pairs: Array<[string, string]>) => { 19 onChange(pairsToRecord(pairs)); 20 }; 21 22 const deleteItem = (index: number) => { 23 const newPairs = pairs.slice(); 24 const removed = newPairs.splice(index, 1)[0]; 25 setPairs(newPairs); 26 if (removed[0]) { 27 emitChange(newPairs); 28 } 29 }; 30 31 const updatePair = (values: [string, string], index: number) => { 32 const old = pairs[index]; 33 const newPairs = pairs.map((pair, i) => (i === index ? values : pair)); 34 setPairs(newPairs); 35 if (values[0] || old[0]) { 36 emitChange(newPairs); 37 } 38 }; 39 40 return ( 41 <div> 42 {!!pairs.length && ( 43 <table className={styles.table}> 44 <thead> 45 <tr> 46 <th>Name</th> 47 <th>Value</th> 48 {!readOnly && <th></th>} 49 </tr> 50 </thead> 51 <tbody> 52 {pairs.map(([key, value], index) => ( 53 <tr key={index}> 54 <td> 55 <Input 56 readOnly={readOnly} 57 value={key} 58 onChange={(e) => updatePair([e.currentTarget.value, value], index)} 59 /> 60 </td> 61 <td> 62 <Input 63 readOnly={readOnly} 64 value={value} 65 onChange={(e) => updatePair([key, e.currentTarget.value], index)} 66 /> 67 </td> 68 {!readOnly && ( 69 <td> 70 <ActionIcon icon="trash-alt" tooltip="delete" onClick={() => deleteItem(index)} /> 71 </td> 72 )} 73 </tr> 74 ))} 75 </tbody> 76 </table> 77 )} 78 {!readOnly && ( 79 <Button 80 className={styles.addButton} 81 type="button" 82 variant="secondary" 83 icon="plus" 84 size="sm" 85 onClick={() => setPairs([...pairs, ['', '']])} 86 > 87 Add 88 </Button> 89 )} 90 </div> 91 ); 92}; 93 94const getStyles = (theme: GrafanaTheme2) => ({ 95 addButton: css` 96 margin-top: ${theme.spacing(1)}; 97 `, 98 table: css` 99 tbody td { 100 padding: 0 ${theme.spacing(1)} ${theme.spacing(1)} 0; 101 } 102 `, 103}); 104 105const pairsToRecord = (pairs: Array<[string, string]>): Record<string, string> => { 106 const record: Record<string, string> = {}; 107 for (const [key, value] of pairs) { 108 if (key) { 109 record[key] = value; 110 } 111 } 112 return record; 113}; 114 115const recordToPairs = (obj?: Record<string, string>): Array<[string, string]> => Object.entries(obj ?? {}); 116