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

mendersoftware / gui / 897326496

pending completion
897326496

Pull #3752

gitlab-ci

mzedel
chore(e2e): made use of shared timeout & login checking values to remove code duplication

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3752: chore(e2e-tests): slightly simplified log in test + separated log out test

4395 of 6392 branches covered (68.76%)

8060 of 9780 relevant lines covered (82.41%)

126.17 hits per line

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

67.12
/src/js/components/settings/integrations.js
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, useMemo, useState } from 'react';
15
import { connect } from 'react-redux';
16

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

20
import { changeIntegration, createIntegration, deleteIntegration, getIntegrations } from '../../actions/organizationActions';
21
import { TIMEOUTS } from '../../constants/appConstants';
22
import { EXTERNAL_PROVIDER } from '../../constants/deviceConstants';
23
import { customSort, versionCompare } from '../../helpers';
24
import { useDebounce } from '../../utils/debouncehook';
25
import Confirm from '../common/confirm';
26
import InfoHint from '../common/info-hint';
27
import { WebhookCreation } from './webhooks/configuration';
28
import Webhooks from './webhooks/webhooks';
29

30
const maxWidth = 750;
5✔
31

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

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

47
  const debouncedId = useDebounce(keyId, TIMEOUTS.debounceDefault);
1✔
48
  const debouncedSecret = useDebounce(keySecret, TIMEOUTS.debounceDefault);
1✔
49
  const debouncedRegion = useDebounce(awsRegion, TIMEOUTS.debounceDefault);
1✔
50
  const debounced = useDebounce(policy, TIMEOUTS.debounceDefault);
1✔
51

52
  const { classes } = useStyles();
1✔
53

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

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

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

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

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

92
  const { classes } = useStyles();
2✔
93

94
  useEffect(() => {
2✔
95
    setConnectionConfig({ connection_string: debouncedValue });
2✔
96
  }, [debouncedValue]);
97

98
  useEffect(() => {
2✔
99
    setValue(connectionConfig.connection_string);
2✔
100
  }, [connectionConfig.connection_string]);
101

102
  const updateConnectionConfig = ({ target: { value = '' } }) => setValue(value);
2!
103

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

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

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

129
  const { classes } = useStyles();
6✔
130

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

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

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

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

199
export const Integrations = ({ changeIntegration, createIntegration, deleteIntegration, getIntegrations, integrations = [], isPreRelease }) => {
5!
200
  const [availableIntegrations, setAvailableIntegrations] = useState([]);
2✔
201
  const [configuredIntegrations, setConfiguredIntegrations] = useState([]);
2✔
202
  const [isConfiguringWebhook, setIsConfiguringWebhook] = useState(false);
2✔
203

204
  const { classes } = useStyles();
2✔
205

206
  useEffect(() => {
2✔
207
    const available = determineAvailableIntegrations(integrations, isPreRelease);
1✔
208
    setAvailableIntegrations(integrations.length ? [] : available);
1!
209
    setConfiguredIntegrations(integrations.filter(integration => integration.provider !== EXTERNAL_PROVIDER.webhook.provider));
2✔
210
  }, [integrations, isPreRelease]);
211

212
  useEffect(() => {
2✔
213
    getIntegrations();
1✔
214
  }, []);
215

216
  const onConfigureIntegration = ({ target: { value: provider = '' } }) => {
2!
217
    if (provider === EXTERNAL_PROVIDER.webhook.provider) {
×
218
      return setIsConfiguringWebhook(true);
×
219
    }
220
    setConfiguredIntegrations([...configuredIntegrations, { id: 'new', provider }]);
×
221
    setAvailableIntegrations(integrations => integrations.filter(integration => integration.provider !== provider));
×
222
  };
223

224
  const onCancelClick = ({ id, provider }) => {
2✔
225
    if (id === 'new') {
×
226
      setAvailableIntegrations(current => [...current, EXTERNAL_PROVIDER[provider]].sort(customSort(true, 'provider')));
×
227
      setConfiguredIntegrations(current =>
×
228
        current.filter(
×
229
          integration => !(integration.id === id && integration.provider === provider && integration.provider !== EXTERNAL_PROVIDER.webhook.provider)
×
230
        )
231
      );
232
    }
233
    setIsConfiguringWebhook(false);
×
234
  };
235

236
  const onSaveClick = integration => {
2✔
237
    if (integration.id === 'new') {
×
238
      setIsConfiguringWebhook(false);
×
239
      return createIntegration(integration);
×
240
    }
241
    changeIntegration(integration);
×
242
  };
243

244
  const configuredWebhook = useMemo(() => integrations.find(integration => integration.provider === EXTERNAL_PROVIDER.webhook.provider), [integrations]);
2✔
245
  return (
2✔
246
    <div>
247
      <h2 className="margin-top-small">Integrations</h2>
248
      {configuredIntegrations.map((integration, index) => (
249
        <IntegrationConfiguration
2✔
250
          key={integration.provider}
251
          integration={integration}
252
          isLast={configuredIntegrations.length === index + 1}
253
          onCancel={onCancelClick}
254
          onDelete={deleteIntegration}
255
          onSave={onSaveClick}
256
        />
257
      ))}
258
      {!configuredWebhook && !!availableIntegrations.length && (
4!
259
        <Select className={classes.select} displayEmpty onChange={onConfigureIntegration} value="">
260
          <MenuItem value="">Add new integration</MenuItem>
261
          {availableIntegrations.map(item => (
262
            <MenuItem key={item.provider} value={item.provider}>
×
263
              {item.title}
264
            </MenuItem>
265
          ))}
266
          <MenuItem value="webhook">Webhooks</MenuItem>
267
        </Select>
268
      )}
269
      {!!configuredWebhook && <Webhooks webhook={configuredWebhook} />}
2!
270
      <WebhookCreation adding={isConfiguringWebhook} onCancel={onCancelClick} onSubmit={onSaveClick} />
271
    </div>
272
  );
273
};
274

275
const actionCreators = { changeIntegration, createIntegration, deleteIntegration, getIntegrations };
5✔
276

277
const mapStateToProps = state => {
5✔
278
  return {
×
279
    integrations: state.organization.externalDeviceIntegrations,
280
    isPreRelease: versionCompare(state.app.versionInformation.Integration, 'next') > -1
281
  };
282
};
283

284
export default connect(mapStateToProps, actionCreators)(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