• 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

92.0
/frontend/src/js/components/devices/group-management/GroupDefinition.tsx
1
// Copyright 2020 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 { useState } from 'react';
2✔
15

2✔
16
import { Autocomplete, FormHelperText, TextField } from '@mui/material';
2✔
17
import { createFilterOptions } from '@mui/material/useAutocomplete';
2✔
18

2✔
19
import DocsLink from '@northern.tech/common-ui/DocsLink';
2✔
20
import InfoText from '@northern.tech/common-ui/InfoText';
2✔
21
import { UNGROUPED_GROUP } from '@northern.tech/store/constants';
2✔
22
import { fullyDecodeURI } from '@northern.tech/utils/helpers';
2✔
23
import validator from 'validator';
2✔
24

2✔
25
const filter = createFilterOptions();
8✔
26

2✔
27
export const validateGroupName = (encodedName: string, groups = [], selectedDevices = [], isCreationDynamic) => {
8✔
28
  const name = fullyDecodeURI(encodedName);
7✔
29
  let invalid = false;
7✔
30
  let errortext = null;
7✔
31
  const isModification = name.length && groups.some(group => decodeURIComponent(group) === name);
7✔
32
  if (!name && !isModification) {
7✔
33
    invalid = true;
3✔
34
  } else if (!validator.isWhitelisted(name, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-.')) {
6✔
35
    invalid = true;
3✔
36
    errortext = 'Valid characters are a-z, A-Z, 0-9, ., _ and -';
3✔
37
  } else if (selectedDevices.length && selectedDevices.every(({ group }) => group === name)) {
5✔
38
    invalid = true;
3✔
39
    errortext = `${name} is the same group the selected devices are already in`;
3✔
40
  } else if (isModification && isCreationDynamic) {
4✔
41
    invalid = true;
3✔
42
    errortext = 'A group with the same name already exists';
3✔
43
  } else if (name === UNGROUPED_GROUP.name) {
3!
44
    invalid = true;
2✔
45
    errortext = `A group with the name ${name} is created automatically`;
2✔
46
  }
2✔
47
  return { errortext, invalid, isModification, name };
7✔
48
};
2✔
49

2✔
50
const GroupOption = (props, option) => <li {...props}>{option.title}</li>;
8✔
51

2✔
52
export const GroupDefinition = ({ isCreationDynamic, groups, newGroup, onInputChange, selectedDevices, selectedGroup }) => {
8✔
53
  const [errortext, setErrorText] = useState('');
4✔
54

2✔
55
  const validateName = (encodedName: string) => {
4✔
56
    const { errortext: error, invalid, isModification, name } = validateGroupName(encodedName, groups, selectedDevices, isCreationDynamic);
2✔
57
    setErrorText(error);
2✔
58
    onInputChange(invalid, name, isModification);
2✔
59
  };
2✔
60

2✔
61
  const filteredGroups = groups
4✔
62
    .filter(group => group !== selectedGroup)
3✔
63
    .map(group => ({
3✔
64
      value: group,
2✔
65
      title: group
2✔
66
    }));
2✔
67
  return (
4✔
68
    <>
2✔
69
      <Autocomplete
2✔
70
        id="group-creation-selection"
2✔
71
        className="margin-top-x-small"
2✔
72
        autoSelect
2✔
73
        freeSolo
2✔
74
        filterSelectedOptions
2✔
75
        filterOptions={(options, params) => {
2✔
76
          const filtered = filter(options, params);
2✔
77
          if (
2!
78
            params.inputValue !== '' &&
2!
79
            !groups.some(group => decodeURIComponent(group) === params.inputValue) &&
2✔
80
            (filtered.length !== 1 || (filtered.length === 1 && filtered[0].title !== params.inputValue))
2✔
81
          ) {
2✔
82
            filtered.push({
2✔
83
              inputValue: params.inputValue,
2✔
84
              title: `Create "${params.inputValue}" group`
2✔
85
            });
2✔
86
          }
2✔
87
          return filtered;
2✔
88
        }}
2✔
89
        getOptionLabel={option => {
2✔
90
          if (typeof option === 'string') {
2!
91
            return option;
2✔
92
          }
2✔
93
          if (option.inputValue) {
2!
94
            return option.inputValue;
2✔
95
          }
2✔
96
          return option.title;
2✔
97
        }}
2✔
98
        handleHomeEndKeys
2✔
99
        inputValue={newGroup}
2✔
100
        options={filteredGroups}
2✔
UNCOV
101
        onInputChange={(e, newValue) => validateName(newValue)}
2✔
102
        renderInput={params => <TextField {...params} label="Select a group, or type to create new" InputProps={{ ...params.InputProps }} />}
6✔
103
        renderOption={GroupOption}
2✔
104
      />
2✔
105
      <FormHelperText>{errortext}</FormHelperText>
2✔
106
      {isCreationDynamic && (
2✔
107
        <InfoText>
2✔
108
          Note: individual devices can&apos;t be added to dynamic groups.
2✔
109
          <br />
2✔
110
          <DocsLink path="overview/device-group" title="Learn more about static vs. dynamic groups" />
2✔
111
        </InfoText>
2✔
112
      )}
2✔
113
    </>
2✔
114
  );
2✔
115
};
2✔
116

2✔
117
export default GroupDefinition;
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