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

mendersoftware / mender-server / 1648176235

30 Jan 2025 09:44AM UTC coverage: 77.589% (+1.0%) from 76.604%
1648176235

Pull #390

gitlab-ci

mzedel
fix(gui): made tenant creation form also work with unset SP device limits

Ticket: None
Changelog: None
Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #390: MEN-7961 - SP config fixes

4329 of 6285 branches covered (68.88%)

Branch coverage included in aggregate %.

26 of 34 new or added lines in 6 files covered. (76.47%)

2 existing lines in 1 file now uncovered.

42773 of 54422 relevant lines covered (78.6%)

21.58 hits per line

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

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

17
import { Button, Divider, FormControl, InputLabel, MenuItem, Select, TextField } from '@mui/material';
18
import { makeStyles } from 'tss-react/mui';
19

20
import Confirm from '@northern.tech/common-ui/Confirm';
21
import InfoHint from '@northern.tech/common-ui/InfoHint';
22
import { EXTERNAL_PROVIDER, TIMEOUTS } from '@northern.tech/store/constants';
23
import { getExternalIntegrations, getIsPreview, getWebhooks } from '@northern.tech/store/selectors';
24
import { changeIntegration, createIntegration, deleteIntegration, getIntegrations } from '@northern.tech/store/thunks';
25
import { useDebounce } from '@northern.tech/utils/debouncehook';
26
import { customSort } from '@northern.tech/utils/helpers';
27

28
import WebhookConfiguration from './webhooks/Configuration';
29
import Webhooks from './webhooks/Webhooks';
30

31
const maxWidth = 750;
5✔
32

33
const useStyles = makeStyles()(theme => ({
7✔
34
  leftButton: { marginRight: theme.spacing() },
35
  inputWrapper: { alignItems: 'flex-end' },
36
  select: { minWidth: 300 },
37
  formWrapper: { display: 'flex', flexDirection: 'column', gap: theme.spacing(2) },
38
  textInput: { minWidth: 500, wordBreak: 'break-all' },
39
  widthLimit: { maxWidth }
40
}));
41

42
const ConnectionDetailsInput = ({ connectionConfig, isEditing, setConnectionConfig }) => {
5✔
43
  const { access_key_id = '', secret_access_key = '', region = '', device_policy_name = '' } = connectionConfig.aws || {};
4✔
44
  const [keyId, setKeyId] = useState(access_key_id);
4✔
45
  const [keySecret, setKeySecret] = useState(secret_access_key);
4✔
46
  const [awsRegion, setRegion] = useState(region);
4✔
47
  const [policy, setPolicy] = useState(device_policy_name);
4✔
48

49
  const debouncedId = useDebounce(keyId, TIMEOUTS.debounceDefault);
4✔
50
  const debouncedSecret = useDebounce(keySecret, TIMEOUTS.debounceDefault);
4✔
51
  const debouncedRegion = useDebounce(awsRegion, TIMEOUTS.debounceDefault);
4✔
52
  const debounced = useDebounce(policy, TIMEOUTS.debounceDefault);
4✔
53

54
  const { classes } = useStyles();
4✔
55

56
  useEffect(() => {
4✔
57
    setConnectionConfig({
1✔
58
      aws: {
59
        access_key_id: debouncedId,
60
        secret_access_key: debouncedSecret,
61
        region: debouncedRegion,
62
        device_policy_name: debounced
63
      }
64
    });
65
  }, [debounced, debouncedRegion, debouncedId, debouncedSecret, setConnectionConfig]);
66

67
  useEffect(() => {
4✔
68
    setKeyId(access_key_id);
1✔
69
    setKeySecret(secret_access_key);
1✔
70
    setRegion(region);
1✔
71
    setPolicy(device_policy_name);
1✔
72
  }, [access_key_id, secret_access_key, region, device_policy_name]);
73

74
  const onKeyChange = ({ target: { value = '' } }) => setKeyId(value);
4!
75
  const onSecretChange = ({ target: { value = '' } }) => setKeySecret(value);
4!
76
  const onRegionChange = ({ target: { value = '' } }) => setRegion(value);
4!
77
  const onPolicyChange = ({ target: { value = '' } }) => setPolicy(value);
4!
78

79
  const commonProps = { className: classes.textInput, disabled: !isEditing, multiline: true };
4✔
80
  return (
4✔
81
    <div className={classes.formWrapper}>
82
      <TextField {...commonProps} label="Key ID" onChange={onKeyChange} value={keyId} />
83
      <TextField {...commonProps} label="Key Secret" onChange={onSecretChange} value={keySecret} />
84
      <TextField {...commonProps} label="Region" onChange={onRegionChange} value={awsRegion} />
85
      <TextField {...commonProps} label="Device Policy Name" onChange={onPolicyChange} value={policy} />
86
    </div>
87
  );
88
};
89

90
const ConnectionStringInput = ({ connectionConfig, isEditing, setConnectionConfig, title }) => {
5✔
91
  const [value, setValue] = useState(connectionConfig.connection_string);
6✔
92
  const debouncedValue = useDebounce(value, TIMEOUTS.debounceDefault);
6✔
93

94
  const { classes } = useStyles();
6✔
95

96
  useEffect(() => {
6✔
97
    setConnectionConfig({ connection_string: debouncedValue });
2✔
98
  }, [debouncedValue, setConnectionConfig]);
99

100
  useEffect(() => {
6✔
101
    setValue(connectionConfig.connection_string);
3✔
102
  }, [connectionConfig.connection_string]);
103

104
  const updateConnectionConfig = ({ target: { value = '' } }) => setValue(value);
6!
105

106
  return (
6✔
107
    <TextField
108
      className={classes.textInput}
109
      disabled={!isEditing}
110
      label={`${title} connection string`}
111
      multiline
112
      onChange={updateConnectionConfig}
113
      value={value}
114
    />
115
  );
116
};
117

118
const providerConfigMap = {
5✔
119
  'iot-core': ConnectionDetailsInput,
120
  'iot-hub': ConnectionStringInput
121
};
122

123
export const IntegrationConfiguration = ({ integration, isLast, onCancel, onDelete, onSave }) => {
5✔
124
  const { credentials = {}, provider } = integration;
12✔
125
  const [connectionConfig, setConnectionConfig] = useState(credentials);
12✔
126
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
127
  const { type, ...otherProps } = credentials;
12✔
128
  const [isEditing, setIsEditing] = useState(!Object.values(otherProps).some(i => i));
12✔
129
  const [isDeleting, setIsDeleting] = useState(false);
12✔
130

131
  const { classes } = useStyles();
12✔
132

133
  useEffect(() => {
12✔
134
    const { credentials = {} } = integration;
5✔
135
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
136
    const { type, ...otherProps } = credentials;
5✔
137
    setConnectionConfig(credentials);
5✔
138
    setIsEditing(!Object.values(otherProps).some(i => i));
5✔
139
  }, [integration]);
140

141
  const onCancelClick = () => {
12✔
142
    setIsEditing(false);
×
143
    setConnectionConfig(credentials);
×
144
    onCancel(integration);
×
145
  };
146
  const onDeleteClick = () => setIsDeleting(true);
12✔
147
  const onDeleteConfirm = () => onDelete(integration);
12✔
148
  const onEditClick = () => setIsEditing(true);
12✔
149
  const onSaveClick = () =>
12✔
150
    onSave({
×
151
      ...integration,
152
      credentials: {
153
        type: EXTERNAL_PROVIDER[provider].credentialsType,
154
        ...connectionConfig
155
      }
156
    });
157

158
  const ConfigInput = providerConfigMap[provider];
12✔
159
  const { configHint, title } = EXTERNAL_PROVIDER[provider];
12✔
160
  return (
12✔
161
    <>
162
      <h3 className="margin-bottom-none">{title}</h3>
163
      <div className={`flexbox space-between padding-top-small relative ${classes.widthLimit} ${classes.inputWrapper}`}>
164
        <ConfigInput connectionConfig={connectionConfig} isEditing={isEditing} setConnectionConfig={setConnectionConfig} title={title} />
165
        <div className="flexbox margin-bottom-x-small">
166
          {isEditing ? (
12✔
167
            <>
168
              <Button className={classes.leftButton} onClick={onCancelClick}>
169
                Cancel
170
              </Button>
171
              <Button variant="contained" onClick={onSaveClick} disabled={credentials === connectionConfig}>
172
                Save
173
              </Button>
174
            </>
175
          ) : (
176
            <>
177
              <Button className={classes.leftButton} onClick={onEditClick}>
178
                Edit
179
              </Button>
180
              <Button onClick={onDeleteClick}>Delete</Button>
181
            </>
182
          )}
183
        </div>
184
        {isDeleting && <Confirm type="integrationRemoval" classes="confirmation-overlay" action={onDeleteConfirm} cancel={() => setIsDeleting(false)} />}
×
185
      </div>
186
      <InfoHint className={`margin-bottom ${classes.widthLimit}`} content={configHint} />
187
      {!isLast && <Divider className={`margin-bottom ${classes.widthLimit}`} />}
19✔
188
    </>
189
  );
190
};
191

192
const determineAvailableIntegrations = (integrations, isPreRelease) =>
5✔
193
  Object.values(EXTERNAL_PROVIDER).reduce((accu, provider) => {
2✔
194
    const hasIntegrationConfigured = integrations.some(integration => integration.provider == provider.provider);
10✔
195
    if (provider.title && (provider.enabled || isPreRelease) && !hasIntegrationConfigured) {
6!
196
      accu.push(provider);
×
197
    }
198
    return accu;
6✔
199
  }, []);
200

201
export const Integrations = () => {
5✔
202
  const [availableIntegrations, setAvailableIntegrations] = useState([]);
4✔
203
  const [configuredIntegrations, setConfiguredIntegrations] = useState([]);
4✔
204
  const [isConfiguringWebhook, setIsConfiguringWebhook] = useState(false);
4✔
205
  const integrations = useSelector(getExternalIntegrations);
4✔
206
  const webhooks = useSelector(getWebhooks);
4✔
207
  const isPreRelease = useSelector(getIsPreview);
4✔
208
  const dispatch = useDispatch();
4✔
209

210
  const { classes } = useStyles();
4✔
211

212
  useEffect(() => {
4✔
213
    const available = determineAvailableIntegrations(integrations, isPreRelease);
2✔
214
    setAvailableIntegrations(available);
2✔
215
    setConfiguredIntegrations(integrations.filter(integration => integration.provider !== EXTERNAL_PROVIDER.webhook.provider));
4✔
216
  }, [integrations, isPreRelease]);
217

218
  useEffect(() => {
4✔
219
    dispatch(getIntegrations());
1✔
220
  }, [dispatch]);
221

222
  const onConfigureIntegration = ({ target: { value: provider = '' } }) => {
4!
223
    if (provider === EXTERNAL_PROVIDER.webhook.provider) {
×
224
      return setIsConfiguringWebhook(true);
×
225
    }
226
    setConfiguredIntegrations([...configuredIntegrations, { id: 'new', provider }]);
×
227
    setAvailableIntegrations(integrations => integrations.filter(integration => integration.provider !== provider));
×
228
  };
229

230
  const onCancelClick = ({ id, provider }) => {
4✔
231
    if (id === 'new') {
×
232
      setAvailableIntegrations(current => [...current, EXTERNAL_PROVIDER[provider]].sort(customSort(true, 'provider')));
×
233
      setConfiguredIntegrations(current =>
×
234
        current.filter(
×
235
          integration => !(integration.id === id && integration.provider === provider && integration.provider !== EXTERNAL_PROVIDER.webhook.provider)
×
236
        )
237
      );
238
    }
239
    setIsConfiguringWebhook(false);
×
240
  };
241

242
  const onSaveClick = integration => {
4✔
243
    if (integration.id === 'new') {
×
244
      setIsConfiguringWebhook(false);
×
245
      return dispatch(createIntegration(integration));
×
246
    }
247
    dispatch(changeIntegration(integration));
×
248
  };
249

250
  return (
4✔
251
    <div>
252
      <h2 className="margin-top-small">Integrations</h2>
253
      {configuredIntegrations.map((integration, index) => (
254
        <IntegrationConfiguration
6✔
255
          key={integration.provider}
256
          integration={integration}
257
          isLast={configuredIntegrations.length === index + 1}
258
          onCancel={onCancelClick}
UNCOV
259
          onDelete={integration => dispatch(deleteIntegration(integration))}
×
260
          onSave={onSaveClick}
261
        />
262
      ))}
263
      <Webhooks />
264
      {!!availableIntegrations.length && (
4!
265
        <FormControl>
266
          <InputLabel id="integration-select-label">Add an integration</InputLabel>
267
          <Select className={classes.select} label="Add an integration" labelId="integration-select-label" onChange={onConfigureIntegration} value="">
268
            {availableIntegrations.map(item => (
UNCOV
269
              <MenuItem key={item.provider} value={item.provider}>
×
270
                {item.title}
271
              </MenuItem>
272
            ))}
273
            {!webhooks.length && <MenuItem value="webhook">Webhooks</MenuItem>}
×
274
          </Select>
275
        </FormControl>
276
      )}
277
      {isConfiguringWebhook && <WebhookConfiguration onCancel={onCancelClick} onSubmit={onSaveClick} />}
4!
278
    </div>
279
  );
280
};
281

282
export default Integrations;
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