• 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

100.0
/frontend/src/js/components/dashboard/widgets/ChartAddition.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 React, { useState } from 'react';
2✔
15
import { Controller, useFormContext } from 'react-hook-form';
2✔
16

2✔
17
import { Add as AddIcon } from '@mui/icons-material';
2✔
18
import {
2✔
19
  FormControl,
2✔
20
  IconButton,
2✔
21
  InputLabel,
2✔
22
  ListSubheader,
2✔
23
  MenuItem,
2✔
24
  Select,
2✔
25
  iconButtonClasses,
2✔
26
  selectClasses,
2✔
27
  svgIconClasses,
2✔
28
  useTheme
2✔
29
} from '@mui/material';
2✔
30
import { makeStyles } from 'tss-react/mui';
2✔
31

2✔
32
import Confirm from '@northern.tech/common-ui/Confirm';
2✔
33
import EnterpriseNotification from '@northern.tech/common-ui/EnterpriseNotification';
2✔
34
import { InfoHintContainer } from '@northern.tech/common-ui/InfoHint';
2✔
35
import Form from '@northern.tech/common-ui/forms/Form';
2✔
36
import { BENEFITS, chartTypes, emptyChartSelection } from '@northern.tech/store/constants';
2✔
37
import { toggle } from '@northern.tech/utils/helpers';
2✔
38

2✔
39
import { HELPTOOLTIPS } from '../../helptips/HelpTooltips';
2✔
40
import { MenderHelpTooltip } from '../../helptips/MenderTooltip';
2✔
41

2✔
42
const fontSize = 'smaller';
9✔
43

2✔
44
const useStyles = makeStyles()(theme => ({
32✔
45
  additionButton: { fontSize: '1rem', cursor: 'pointer' },
2✔
46
  button: { marginLeft: theme.spacing(2), padding: '6px 8px', fontSize },
2✔
47
  buttonWrapper: { display: 'flex', justifyContent: 'flex-end', alignContent: 'center' },
2✔
48
  header: { minHeight: 30, [`.${svgIconClasses.root}`]: { marginLeft: theme.spacing() } },
2✔
49
  iconButton: {
2✔
50
    [`&.${iconButtonClasses.root}`]: {
2✔
51
      borderRadius: 5,
2✔
52
      border: `1px solid ${theme.palette.primary.main}`,
2✔
53
      marginRight: theme.spacing(),
2✔
54
      '&.selected': {
2✔
55
        background: theme.palette.primary.main,
2✔
56
        color: theme.palette.background.paper
2✔
57
      }
2✔
58
    }
2✔
59
  },
2✔
60
  formWrapper: {
2✔
61
    alignItems: 'baseline',
2✔
62
    columnGap: theme.spacing(3),
2✔
63
    display: 'grid',
2✔
64
    fontSize,
2✔
65
    gridTemplateColumns: 'max-content 1fr',
2✔
66
    gridTemplateRows: 'auto',
2✔
67
    rowGap: theme.spacing(0.5),
2✔
68
    marginTop: theme.spacing(),
2✔
69
    [`.${selectClasses.select}`]: { paddingBottom: theme.spacing(0.5), paddingTop: 0, fontSize }
2✔
70
  }
2✔
71
}));
2✔
72

2✔
73
export const Header = ({ chartType }) => {
9✔
74
  const { classes } = useStyles();
11✔
75
  const { Icon } = chartTypes[chartType];
11✔
76
  return (
11✔
77
    <div className={`flexbox center-aligned ${classes.header}`}>
2✔
78
      Software distribution
2✔
79
      <Icon />
2✔
80
    </div>
2✔
81
  );
2✔
82
};
2✔
83

2✔
84
const GroupSelect = ({ groups, name }) => {
9✔
85
  const { control } = useFormContext();
5✔
86
  return (
5✔
87
    <FormControl className="margin-top-none">
2✔
88
      <InputLabel id="group-select-label" shrink>
2✔
89
        Device group
2✔
90
      </InputLabel>
2✔
91
      <Controller
2✔
92
        name={name}
2✔
93
        control={control}
2✔
94
        defaultValue=""
2✔
95
        render={({ field }) => (
2✔
96
          <Select labelId="group-select-label" displayEmpty label="Device group" {...field}>
6✔
97
            <MenuItem value="">
2✔
98
              <em>All Devices</em>
2✔
99
            </MenuItem>
2✔
100
            {Object.keys(groups).map(group => (
2✔
101
              <MenuItem key={group} value={group}>
10✔
102
                {group}
2✔
103
              </MenuItem>
2✔
104
            ))}
2✔
105
          </Select>
2✔
106
        )}
2✔
107
      />
2✔
108
    </FormControl>
2✔
109
  );
2✔
110
};
2✔
111

2✔
112
const getIndentation = (level, theme) => ({ paddingLeft: theme.spacing(2) + level * theme.spacing() });
11✔
113

2✔
114
const SoftwareSelect = ({ software, name }) => {
9✔
115
  const { control } = useFormContext();
5✔
116
  const theme = useTheme();
5✔
117
  return (
5✔
118
    <FormControl className="margin-top-none">
2✔
119
      <InputLabel shrink id="software-select-label">
2✔
120
        Software
2✔
121
      </InputLabel>
2✔
122
      <Controller
2✔
123
        name={name}
2✔
124
        control={control}
2✔
125
        defaultValue=""
2✔
126
        render={({ field }) => (
2✔
127
          <Select labelId="software-select-label" label="Software" displayEmpty {...field}>
5✔
128
            {software.map(({ subheader, title, value, nestingLevel }) =>
2✔
129
              subheader ? (
11✔
130
                <ListSubheader key={value} style={getIndentation(nestingLevel, theme)}>
2✔
131
                  {subheader}
2✔
132
                </ListSubheader>
2✔
133
              ) : (
2✔
134
                <MenuItem key={value} style={getIndentation(nestingLevel, theme)} value={value}>
2✔
135
                  {title}
2✔
136
                </MenuItem>
2✔
137
              )
2✔
138
            )}
2✔
139
          </Select>
2✔
140
        )}
2✔
141
      />
2✔
142
    </FormControl>
2✔
143
  );
2✔
144
};
2✔
145

2✔
146
const ChartSelect = ({ classes, name }) => {
9✔
147
  const { control, watch } = useFormContext();
5✔
148
  const selectedType = watch(name);
5✔
149
  return (
5✔
150
    <Controller
2✔
151
      name={name}
2✔
152
      control={control}
2✔
153
      defaultValue={Object.keys(chartTypes)[0]}
2✔
154
      render={({ field: { onChange } }) => (
2✔
155
        <div>
5✔
156
          {Object.values(chartTypes).map(type => {
2✔
157
            const { Icon, key } = type;
8✔
158
            return (
8✔
UNCOV
159
              <IconButton className={`${classes.iconButton} ${selectedType === key ? 'selected' : ''}`} key={key} size="small" onClick={() => onChange(key)}>
2✔
160
                <Icon fontSize="small" />
2✔
161
              </IconButton>
2✔
162
            );
2✔
163
          })}
2✔
164
        </div>
2✔
165
      )}
2✔
166
    />
2✔
167
  );
2✔
168
};
2✔
169

2✔
170
const chartOptions = [
9✔
171
  { key: 'software', title: 'Software', Selector: SoftwareSelect },
2✔
172
  { key: 'group', title: 'Device group', Selector: GroupSelect },
2✔
173
  { key: 'chartType', title: 'Display', Selector: ChartSelect }
2✔
174
];
2✔
175

2✔
176
export const ChartEditWidget = ({ groups, onSave, onCancel, selection: selectionProp = {}, software = [] }) => {
9✔
177
  const { classes } = useStyles();
3✔
178

2✔
179
  return (
3✔
180
    <Form
2✔
181
      defaultValues={emptyChartSelection}
2✔
182
      initialValues={selectionProp}
2✔
183
      onSubmit={onSave}
2✔
184
      handleCancel={onCancel}
2✔
185
      showButtons
2✔
186
      submitLabel="Save"
2✔
187
      className="widget chart-widget"
2✔
188
    >
2✔
189
      <Header chartType={emptyChartSelection.chartType} />
2✔
190
      <div className={classes.formWrapper}>
2✔
191
        {chartOptions.map(({ key, title, Selector }) => (
2✔
192
          <React.Fragment key={key}>
5✔
193
            <div>{title}</div>
2✔
194
            <Selector classes={classes} groups={groups} software={software} name={key} />
2✔
195
          </React.Fragment>
2✔
196
        ))}
2✔
197
      </div>
2✔
198
    </Form>
2✔
199
  );
2✔
200
};
2✔
201

2✔
202
export const RemovalWidget = ({ onCancel, onClick }) => (
9✔
203
  <div className="widget chart-widget">
2✔
204
    <Confirm classes="flexbox centered confirmation-overlay" cancel={onCancel} action={onClick} style={{ justifyContent: 'center' }} type="chartRemoval" />
2✔
205
  </div>
2✔
206
);
2✔
207

2✔
208
export const WidgetAdditionWidget = ({ onAdditionClick, ...remainder }) => {
9✔
209
  const [adding, setAdding] = useState(false);
59✔
210
  const { classes } = useStyles();
59✔
211

2✔
212
  const addCurrentSelection = selection => {
59✔
213
    onAdditionClick(selection);
3✔
214
    setAdding(false);
3✔
215
  };
2✔
216

2✔
217
  const onCancelClick = () => setAdding(toggle);
59✔
218

2✔
219
  return adding ? (
59✔
220
    <ChartEditWidget {...remainder} onSave={addCurrentSelection} onCancel={onCancelClick} />
2✔
221
  ) : (
2✔
222
    <div className="widget">
2✔
223
      <InfoHintContainer className="" style={{ alignItems: 'end' }}>
2✔
224
        <EnterpriseNotification id={BENEFITS.dashboard.id} />
2✔
225
        <MenderHelpTooltip id={HELPTOOLTIPS.dashboardWidget.id} />
2✔
226
      </InfoHintContainer>
2✔
227
      <div className={`flexbox centered muted ${classes.additionButton}`} onClick={() => setAdding(true)}>
3✔
228
        <AddIcon />
2✔
229
        <span className={classes.additionButton}>add a widget</span>
2✔
230
      </div>
2✔
231
    </div>
2✔
232
  );
2✔
233
};
2✔
234

2✔
235
export default WidgetAdditionWidget;
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