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

mendersoftware / gui / 1081664682

22 Nov 2023 02:11PM UTC coverage: 82.798% (-17.2%) from 99.964%
1081664682

Pull #4214

gitlab-ci

tranchitella
fix: Fixed the infinite page redirects when the back button is pressed

Remove the location and navigate from the useLocationParams.setValue callback
dependencies as they change the set function that is presented in other
useEffect dependencies. This happens when the back button is clicked, which
leads to the location changing infinitely.

Changelog: Title
Ticket: MEN-6847
Ticket: MEN-6796

Signed-off-by: Ihor Aleksandrychiev <ihor.aleksandrychiev@northern.tech>
Signed-off-by: Fabio Tranchitella <fabio.tranchitella@northern.tech>
Pull Request #4214: fix: Fixed the infinite page redirects when the back button is pressed

4319 of 6292 branches covered (0.0%)

8332 of 10063 relevant lines covered (82.8%)

191.0 hits per line

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

78.85
/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 { useDispatch, useSelector } 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 } from '../../helpers';
24
import { getExternalIntegrations, getIsPreview } from '../../selectors';
25
import { useDebounce } from '../../utils/debouncehook';
26
import Confirm from '../common/confirm';
27
import InfoHint from '../common/info-hint';
28
import { WebhookCreation } from './webhooks/configuration';
29
import Webhooks from './webhooks/webhooks';
30

31
const maxWidth = 750;
4✔
32

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

208
  const { classes } = useStyles();
2✔
209

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

216
  useEffect(() => {
2✔
217
    dispatch(getIntegrations());
1✔
218
  }, [dispatch]);
219

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

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

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

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

279
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