• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / mender-server / 10423

11 Nov 2025 04:53PM UTC coverage: 74.435% (-0.1%) from 74.562%
10423

push

gitlab-ci

web-flow
Merge pull request #1071 from mendersoftware/dependabot/npm_and_yarn/frontend/main/development-dependencies-92732187be

3868 of 5393 branches covered (71.72%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 2 files covered. (100.0%)

176 existing lines in 95 files now uncovered.

64605 of 86597 relevant lines covered (74.6%)

7.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.67
/frontend/src/js/common-ui/forms/KeyValueEditor.tsx
1
// Copyright 2021 Northern.tech AS
2✔
2
//
2✔
3
//    Licensed under the Apache License, Version 2.0 (the "License");
2✔
4
//    you may not use this file except in compliance with the License.
2✔
5
//    You may obtain a copy of the License at
2✔
6
//
2✔
7
//        http://www.apache.org/licenses/LICENSE-2.0
2✔
8
//
2✔
9
//    Unless required by applicable law or agreed to in writing, software
2✔
10
//    distributed under the License is distributed on an "AS IS" BASIS,
2✔
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2✔
12
//    See the License for the specific language governing permissions and
2✔
13
//    limitations under the License.
2✔
14
import { CSSProperties, ComponentType, useEffect, useState } from 'react';
2✔
15
import { useFieldArray, useFormContext } from 'react-hook-form';
2✔
16

2✔
17
import { Clear as ClearIcon, Add as ContentAddIcon } from '@mui/icons-material';
2✔
18
import { Fab, FormControl, FormHelperText, IconButton, OutlinedInput } from '@mui/material';
2✔
19
import { makeStyles } from 'tss-react/mui';
2✔
20

2✔
21
import Form from './Form';
2✔
22

2✔
23
type HelptipProps = {
2✔
24
  [key: string]: any;
2✔
25
  style?: CSSProperties;
2✔
26
};
2✔
27

2✔
28
type InputHelptip = {
2✔
29
  component: ComponentType<HelptipProps>;
2✔
30
  position?: string;
2✔
31
  props?: HelptipProps;
2✔
32
};
2✔
33

2✔
34
type InputLineItem = {
2✔
35
  helptip: InputHelptip | null;
2✔
36
  key: string;
2✔
37
  value: string;
2✔
38
};
2✔
39

2✔
40
const emptyInput: InputLineItem = { helptip: null, key: '', value: '' };
19✔
41

2✔
42
const reducePairs = (pairs: InputLineItem[]) => (pairs || []).reduce((accu, item) => ({ ...accu, ...(item.value ? { [item.key]: item.value } : {}) }), {});
187!
43

2✔
44
const useStyles = makeStyles()(theme => ({
19✔
45
  spacer: { minWidth: theme.spacing(30) },
2✔
46
  helptip: { left: -35, top: 15, position: 'absolute' },
2✔
47
  keyValueContainer: {
2✔
48
    display: 'grid',
2✔
49
    gridTemplateColumns: 'min-content min-content max-content',
2✔
50
    columnGap: theme.spacing(2),
2✔
51
    alignItems: 'baseline',
2✔
52
    justifyItems: 'baseline',
2✔
53
    '> div': {
2✔
54
      marginTop: 10
2✔
55
    }
2✔
56
  }
2✔
57
}));
2✔
58

2✔
59
interface KeyValueFieldsProps {
2✔
60
  disabled?: boolean;
2✔
61
  errortext?: string;
2✔
62
  inputHelpTipsMap: Record<string, { component: React.ComponentType<any>; props: any }>;
2✔
63
  onInputChange: (value: Record<string, string>) => void;
2✔
64
}
2✔
65

2✔
66
const KeyValueFields = ({ disabled, errortext, inputHelpTipsMap, onInputChange }: KeyValueFieldsProps) => {
19✔
67
  const { classes } = useStyles();
272✔
68
  const {
2✔
69
    control,
2✔
70
    watch,
2✔
71
    setValue,
2✔
72
    formState: { errors },
2✔
73
    trigger
2✔
74
  } = useFormContext();
272✔
75

2✔
76
  const { fields, append, remove, replace } = useFieldArray<{ inputs: InputLineItem[] }>({
272✔
77
    control,
2✔
78
    name: 'inputs',
2✔
79
    rules: {
2✔
80
      validate: {
2✔
81
        noDuplicates: (inputs?: InputLineItem[]) => {
2✔
82
          const keys = (inputs || []).map(item => item.key).filter(Boolean);
211!
83
          return new Set(keys).size === keys.length || 'Duplicate keys exist, only the last set value will be submitted';
177✔
84
        }
2✔
85
      }
2✔
86
    }
2✔
87
  });
2✔
88

2✔
89
  const inputs = watch('inputs') as InputLineItem[];
272✔
90

2✔
91
  useEffect(() => {
272✔
92
    const inputObject = reducePairs(inputs);
156✔
93
    onInputChange(inputObject);
156✔
94
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
95
  }, [JSON.stringify(inputs), onInputChange]);
2✔
96

2✔
97
  const onClearClick = () => replace([{ ...emptyInput }]);
272✔
98

2✔
99
  const addKeyValue = () => append({ ...emptyInput });
272✔
100

2✔
101
  const updateField = (index: number, field: 'key' | 'value', value: string) => {
272✔
102
    setValue(`inputs.${index}.${field}`, value);
144✔
103
    if (field === 'key') {
144✔
104
      const normalizedKey = value.toLowerCase();
59✔
105
      setValue(`inputs.${index}.helptip`, inputHelpTipsMap[normalizedKey]);
59✔
106
    }
2✔
107
    trigger();
144✔
108
  };
2✔
109

2✔
110
  return (
272✔
111
    <div>
2✔
112
      {fields.map((field, index) => {
2✔
113
        const hasError = Boolean(index === fields.length - 1 && (errortext || errors?.inputs?.root?.message));
320✔
114
        const hasRemovalDisabled = !(inputs?.[index]?.key && inputs?.[index]?.value);
320✔
115
        const { component: Helptip = null, props: helptipProps = {} } = (inputs[index].helptip ?? {}) as InputHelptip;
320✔
116
        return (
320✔
117
          <div className={`${classes.keyValueContainer} relative`} key={field.id}>
2✔
118
            <FormControl>
2✔
119
              <OutlinedInput
2✔
120
                disabled={disabled}
2✔
121
                value={inputs?.[index]?.key || ''}
2✔
122
                placeholder="Key"
2✔
123
                onChange={e => updateField(index, 'key', e.target.value)}
59✔
124
                type="text"
2✔
125
              />
2✔
126
              {hasError && <FormHelperText>{errortext || errors?.inputs?.root?.message}</FormHelperText>}
2✔
127
            </FormControl>
2✔
128
            <FormControl>
2✔
129
              <OutlinedInput
2✔
130
                disabled={disabled}
2✔
131
                value={inputs?.[index]?.value || ''}
2✔
132
                placeholder="Value"
2✔
133
                onChange={e => updateField(index, 'value', e.target.value)}
87✔
134
                type="text"
2✔
135
              />
2✔
136
            </FormControl>
2✔
137
            {fields.length > 1 && !hasRemovalDisabled ? (
2✔
UNCOV
138
              <IconButton disabled={disabled} onClick={() => remove(index)} size="large">
2✔
139
                <ClearIcon fontSize="small" />
2✔
140
              </IconButton>
2✔
141
            ) : (
2✔
142
              <span />
2✔
143
            )}
2✔
144
            {Helptip && <Helptip className={classes.helptip} {...helptipProps} />}
2✔
145
          </div>
2✔
146
        );
2✔
147
      })}
2✔
148
      <div className={classes.keyValueContainer}>
2✔
149
        <div className={classes.spacer}>
2✔
150
          <Fab
2✔
151
            disabled={disabled || !inputs?.[fields.length - 1]?.key || !inputs?.[fields.length - 1]?.value}
2✔
152
            style={{ marginBottom: 10 }}
2✔
153
            size="small"
2✔
154
            onClick={addKeyValue}
2✔
155
          >
2✔
156
            <ContentAddIcon />
2✔
157
          </Fab>
2✔
158
        </div>
2✔
159
        <div className={classes.spacer} />
2✔
160
        {inputs.length > 1 ? <a onClick={onClearClick}>clear all</a> : <div />}
2✔
161
      </div>
2✔
162
    </div>
2✔
163
  );
2✔
164
};
2✔
165

2✔
166
export const KeyValueEditor = ({ disabled, errortext, initialInput = {}, inputHelpTipsMap = {}, onInputChange }) => {
19✔
167
  const defaultValues = {
91✔
168
    inputs: Object.keys(initialInput).length
2!
169
      ? Object.entries(initialInput).map(([key, value]) => ({ helptip: inputHelpTipsMap[key.toLowerCase()], key, value }))
2✔
170
      : [{ ...emptyInput }]
2✔
171
  };
2✔
172
  const [initialValues] = useState(defaultValues);
91✔
173

2✔
174
  const onFormSubmit = data => onInputChange(reducePairs(data.inputs));
91✔
175

2✔
176
  return (
91✔
177
    <Form autocomplete="off" defaultValues={defaultValues} id="key-value-editor" initialValues={initialValues} onSubmit={onFormSubmit}>
2✔
178
      <KeyValueFields disabled={disabled} errortext={errortext} inputHelpTipsMap={inputHelpTipsMap} onInputChange={onInputChange} />
2✔
179
    </Form>
2✔
180
  );
2✔
181
};
2✔
182

2✔
183
export default KeyValueEditor;
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc