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