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

mendersoftware / mender-server / 14628

27 Apr 2026 12:11PM UTC coverage: 48.242% (-29.4%) from 77.64%
14628

push

gitlab-ci

web-flow
Merge pull request #1724 from mineralsfree/upgrade/mui

Upgrade/mui

4089 of 5760 branches covered (70.99%)

Branch coverage included in aggregate %.

13 of 14 new or added lines in 8 files covered. (92.86%)

362 existing lines in 43 files now uncovered.

65310 of 138095 relevant lines covered (47.29%)

5.76 hits per line

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

98.78
/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 type { CSSProperties, ComponentType } from 'react';
2✔
15
import { useEffect, useState } from 'react';
2✔
16
import { useFieldArray, useFormContext } from 'react-hook-form';
2✔
17

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

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

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

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

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

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

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

187!
45
const useStyles = makeStyles()(theme => ({
2✔
46
  formReset: { justifyContent: 'end' },
17✔
47
  helptip: { left: -35, top: theme.spacing(), position: 'absolute !important' },
2✔
48
  keyValueContainer: {
2✔
49
    display: 'grid',
2✔
50
    gridTemplateColumns: 'minmax(200px, min-content) minmax(200px, min-content) max-content',
2✔
51
    columnGap: theme.spacing(2),
2✔
52
    alignItems: 'baseline',
2✔
53
    justifyItems: 'baseline',
2✔
54
    '> div': {
2✔
55
      marginTop: 10
2✔
56
    }
2✔
57
  }
2✔
58
}));
2✔
59

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

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

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

2✔
91
  const inputs = watch('inputs') as InputLineItem[];
2✔
92

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

2✔
99
  useEffect(() => {
2✔
100
    replace(initialValues);
272✔
101
    // eslint-disable-next-line react-hooks/exhaustive-deps
11✔
102
  }, [JSON.stringify(initialValues)]);
2✔
103

2✔
104
  const onClearClick = () => replace([{ ...emptyInput }]);
2✔
105

272✔
106
  const addKeyValue = () => append({ ...emptyInput });
2✔
107

272✔
108
  const updateField = (index: number, field: 'key' | 'value', value: string) => {
2✔
109
    setValue(`inputs.${index}.${field}`, value);
272✔
110
    if (field === 'key') {
144✔
111
      const normalizedKey = value.toLowerCase();
144✔
112
      setValue(`inputs.${index}.helptip`, inputHelpTipsMap[normalizedKey]);
59✔
113
    }
59✔
114
    trigger();
2✔
115
  };
144✔
116

2✔
117
  return (
2✔
118
    <div>
272✔
119
      {fields.map((field, index) => {
2✔
120
        const hasError = Boolean(index === fields.length - 1 && (errortext || errors?.inputs?.root?.message));
2✔
121
        const hasRemovalDisabled = !(inputs?.[index]?.key && inputs?.[index]?.value);
319✔
122
        const { component: Helptip = null, props: helptipProps = {} } = (inputs[index].helptip ?? {}) as InputHelptip;
319✔
123
        return (
319✔
124
          <div className={`${classes.keyValueContainer} relative`} key={field.id}>
319✔
125
            <FormControl>
2✔
126
              <OutlinedInput
2✔
127
                disabled={disabled}
2✔
128
                value={inputs?.[index]?.key || ''}
2✔
129
                placeholder="Key"
2✔
130
                onChange={e => updateField(index, 'key', e.target.value)}
2✔
131
                type="text"
59✔
132
              />
2✔
133
              {hasError && <FormHelperText>{errortext || errors?.inputs?.root?.message}</FormHelperText>}
2✔
134
            </FormControl>
2✔
135
            <FormControl>
2✔
136
              <OutlinedInput
2✔
137
                disabled={disabled}
2✔
138
                value={inputs?.[index]?.value || ''}
2✔
139
                placeholder="Value"
2✔
140
                onChange={e => updateField(index, 'value', e.target.value)}
2✔
141
                type="text"
87✔
142
              />
2✔
143
            </FormControl>
2✔
144
            {fields.length > 1 && !hasRemovalDisabled ? (
2✔
145
              <IconButton disabled={disabled} onClick={() => remove(index)} size="large">
2✔
UNCOV
146
                <ClearIcon fontSize="small" />
2✔
147
              </IconButton>
2✔
148
            ) : (
2✔
149
              <span />
2✔
150
            )}
2✔
151
            {Helptip && <Helptip className={classes.helptip} {...helptipProps} />}
2✔
152
          </div>
2✔
153
        );
2✔
154
      })}
2✔
155
      <div className={`margin-top-small ${classes.keyValueContainer}`}>
2✔
156
        <div className="margin-left-x-small">
2✔
157
          <Fab disabled={disabled || !inputs?.[fields.length - 1]?.key || !inputs?.[fields.length - 1]?.value} size="small" onClick={addKeyValue}>
2✔
158
            <ContentAddIcon />
2✔
159
          </Fab>
2✔
160
        </div>
2✔
161
        <div className={`flexbox align-items-center full-width ${classes.formReset}`}>
2✔
162
          {inputs.length > 1 ? (
2✔
163
            <Button className="align-self-end" variant="text" onClick={onClearClick}>
2✔
164
              Clear all
2✔
165
            </Button>
2✔
166
          ) : (
2✔
167
            <div />
2✔
168
          )}
2✔
169
        </div>
2✔
170
      </div>
2✔
171
    </div>
2✔
172
  );
2✔
173
};
2✔
174

2✔
175
export const KeyValueEditor = ({ disabled, errortext, initialInput = {}, inputHelpTipsMap = {}, onInputChange }) => {
2✔
176
  const defaultValues = {
17✔
177
    inputs: Object.keys(initialInput).length
91✔
178
      ? Object.entries(initialInput).map(([key, value]) => ({ helptip: inputHelpTipsMap[key.toLowerCase()], key, value }) as InputLineItem)
2!
UNCOV
179
      : [{ ...emptyInput }]
2✔
180
  };
2✔
181
  const [initialValues, setInitialValues] = useState(defaultValues);
2✔
182

91✔
183
  useEffect(() => {
2✔
184
    setInitialValues(defaultValues);
91✔
185
    // eslint-disable-next-line react-hooks/exhaustive-deps
11✔
186
  }, [JSON.stringify(initialInput)]);
2✔
187

2✔
188
  const onFormSubmit = data => onInputChange(reducePairs(data.inputs));
2✔
189

91✔
190
  return (
2✔
191
    <Form autocomplete="off" defaultValues={defaultValues} id="key-value-editor" initialValues={initialValues} onSubmit={onFormSubmit}>
91✔
192
      <KeyValueFields
2✔
193
        disabled={disabled}
2✔
194
        errortext={errortext}
2✔
195
        initialValues={defaultValues.inputs}
2✔
196
        inputHelpTipsMap={inputHelpTipsMap}
2✔
197
        onInputChange={onInputChange}
2✔
198
      />
2✔
199
    </Form>
2✔
200
  );
2✔
201
};
2✔
202

2✔
203
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

© 2026 Coveralls, Inc