• 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

82.48
/src/js/components/deployments/pastdeployments.js
1
// Copyright 2015 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, useRef, useState } from 'react';
15
import { connect } from 'react-redux';
16

17
// material ui
18
import { Autocomplete, TextField } from '@mui/material';
19
import { makeStyles } from 'tss-react/mui';
20

21
import historyImage from '../../../assets/img/history.png';
22
import { setSnackbar } from '../../actions/appActions';
23
import { getDeploymentsByStatus, setDeploymentsState } from '../../actions/deploymentActions';
24
import { advanceOnboarding } from '../../actions/onboardingActions';
25
import { BEGINNING_OF_TIME, SORTING_OPTIONS, TIMEOUTS } from '../../constants/appConstants';
26
import { DEPLOYMENT_STATES, DEPLOYMENT_TYPES } from '../../constants/deploymentConstants';
27
import { ALL_DEVICES, UNGROUPED_GROUP } from '../../constants/deviceConstants';
28
import { onboardingSteps } from '../../constants/onboardingConstants';
29
import { getISOStringBoundaries, tryMapDeployments } from '../../helpers';
30
import { getIdAttribute, getOnboardingState, getUserCapabilities } from '../../selectors';
31
import { useDebounce } from '../../utils/debouncehook';
32
import { getOnboardingComponentFor } from '../../utils/onboardingmanager';
33
import useWindowSize from '../../utils/resizehook';
34
import { clearAllRetryTimers, clearRetryTimer, setRetryTimer } from '../../utils/retrytimer';
35
import Loader from '../common/loader';
36
import TimeframePicker from '../common/timeframe-picker';
37
import TimerangePicker from '../common/timerange-picker';
38
import { DeploymentSize, DeploymentStatus } from './deploymentitem';
39
import { defaultRefreshDeploymentsLength as refreshDeploymentsLength } from './deployments';
40
import DeploymentsList, { defaultHeaders } from './deploymentslist';
41

42
const headers = [
7✔
43
  ...defaultHeaders.slice(0, defaultHeaders.length - 1),
44
  { title: 'Status', renderer: DeploymentStatus },
45
  { title: 'Data downloaded', renderer: DeploymentSize }
46
];
47

48
const type = DEPLOYMENT_STATES.finished;
7✔
49

50
const useStyles = makeStyles()(theme => ({
7✔
51
  datepickerContainer: {
52
    backgroundColor: theme.palette.background.lightgrey
53
  }
54
}));
55

56
export const Past = props => {
7✔
57
  const {
58
    advanceOnboarding,
59
    createClick,
60
    getDeploymentsByStatus,
61
    groups,
62
    isShowingDetails,
63
    onboardingState,
64
    past,
65
    pastSelectionState,
66
    setDeploymentsState,
67
    setSnackbar
68
  } = props;
67✔
69
  // eslint-disable-next-line no-unused-vars
70
  const size = useWindowSize();
66✔
71
  const [tonight] = useState(getISOStringBoundaries(new Date()).end);
66✔
72
  const [loading, setLoading] = useState(false);
66✔
73
  const deploymentsRef = useRef();
66✔
74
  const timer = useRef();
66✔
75
  const [searchValue, setSearchValue] = useState('');
66✔
76
  const [typeValue, setTypeValue] = useState('');
66✔
77
  const { classes } = useStyles();
66✔
78

79
  const debouncedSearch = useDebounce(searchValue, TIMEOUTS.debounceDefault);
66✔
80
  const debouncedType = useDebounce(typeValue, TIMEOUTS.debounceDefault);
66✔
81

82
  const { endDate, page, perPage, search: deviceGroup, startDate, total: count, type: deploymentType } = pastSelectionState;
66✔
83

84
  useEffect(() => {
66✔
85
    const roundedStartDate = Math.round(Date.parse(startDate || BEGINNING_OF_TIME) / 1000);
3✔
86
    const roundedEndDate = Math.round(Date.parse(endDate) / 1000);
3✔
87
    setLoading(true);
3✔
88
    getDeploymentsByStatus(type, page, perPage, roundedStartDate, roundedEndDate, deviceGroup, deploymentType, true, SORTING_OPTIONS.desc)
3✔
89
      .then(deploymentsAction => {
90
        const deploymentsList = deploymentsAction ? Object.values(deploymentsAction[0].deployments) : [];
2!
91
        if (deploymentsList.length) {
2!
92
          let newStartDate = new Date(deploymentsList[deploymentsList.length - 1].created);
2✔
93
          const { start: startDate } = getISOStringBoundaries(newStartDate);
2✔
94
          setDeploymentsState({ [DEPLOYMENT_STATES.finished]: { startDate } });
2✔
95
        }
96
      })
97
      .finally(() => setLoading(false));
2✔
98
    return () => {
3✔
99
      clearAllRetryTimers(setSnackbar);
3✔
100
    };
101
  }, []);
102

103
  useEffect(() => {
66✔
104
    clearInterval(timer.current);
4✔
105
    timer.current = setInterval(refreshPast, refreshDeploymentsLength);
4✔
106
    refreshPast();
4✔
107
    return () => {
4✔
108
      clearInterval(timer.current);
4✔
109
    };
110
  }, [page, perPage, startDate, endDate, deviceGroup, deploymentType]);
111

112
  useEffect(() => {
66✔
113
    if (!past.length || onboardingState.complete) {
4✔
114
      return;
2✔
115
    }
116
    const pastDeploymentsFailed = past.reduce(
2✔
117
      (accu, item) =>
118
        item.status === 'failed' ||
3✔
119
        (item.statistics?.status &&
120
          item.statistics.status.noartifact + item.statistics.status.failure + item.statistics.status['already-installed'] + item.statistics.status.aborted >
121
            0) ||
122
        accu,
123
      false
124
    );
125
    if (pastDeploymentsFailed) {
2!
126
      advanceOnboarding(onboardingSteps.DEPLOYMENTS_PAST_COMPLETED_FAILURE);
×
127
    } else {
128
      advanceOnboarding(onboardingSteps.DEPLOYMENTS_PAST_COMPLETED_NOTIFICATION);
2✔
129
    }
130
    setTimeout(() => {
2✔
131
      let notification = getOnboardingComponentFor(onboardingSteps.DEPLOYMENTS_PAST_COMPLETED_NOTIFICATION, onboardingState, { setSnackbar });
2✔
132
      // the following extra check is needed since this component will still be mounted if a user returns to the initial tab after the first
133
      // onboarding deployment & thus the effects will still run, so only ever consider the notification for the second deployment
134
      notification =
2✔
135
        past.length > 1
2✔
136
          ? getOnboardingComponentFor(onboardingSteps.ONBOARDING_FINISHED_NOTIFICATION, onboardingState, { setSnackbar }, notification)
137
          : notification;
138
      !!notification && setSnackbar('open', TIMEOUTS.refreshDefault, '', notification, () => {}, true);
2!
139
    }, 400);
140
  }, [past.length, onboardingState.complete]);
141

142
  useEffect(() => {
66✔
143
    setDeploymentsState({ [DEPLOYMENT_STATES.finished]: { page: 1, search: debouncedSearch, type: debouncedType } });
3✔
144
  }, [debouncedSearch, debouncedType]);
145

146
  /*
147
  / refresh only finished deployments
148
  /
149
  */
150
  const refreshPast = (
66✔
151
    currentPage = page,
4✔
152
    currentPerPage = perPage,
4✔
153
    currentStartDate = startDate,
4✔
154
    currentEndDate = endDate,
4✔
155
    currentDeviceGroup = deviceGroup,
4✔
156
    currentType = deploymentType
4✔
157
  ) => {
158
    const roundedStartDate = Math.round(Date.parse(currentStartDate) / 1000);
4✔
159
    const roundedEndDate = Math.round(Date.parse(currentEndDate) / 1000);
4✔
160
    return getDeploymentsByStatus(type, currentPage, currentPerPage, roundedStartDate, roundedEndDate, currentDeviceGroup, currentType)
4✔
161
      .then(deploymentsAction => {
162
        clearRetryTimer(type, setSnackbar);
3✔
163
        const { total, deploymentIds } = deploymentsAction[deploymentsAction.length - 1];
3✔
164
        if (total && !deploymentIds.length) {
3!
165
          return refreshPast(currentPage, currentPerPage, currentStartDate, currentEndDate, currentDeviceGroup);
×
166
        }
167
      })
168
      .catch(err => setRetryTimer(err, 'deployments', `Couldn't load deployments.`, refreshDeploymentsLength, setSnackbar));
×
169
  };
170

171
  let onboardingComponent = null;
66✔
172
  if (deploymentsRef.current) {
66✔
173
    const detailsButtons = deploymentsRef.current.getElementsByClassName('MuiButton-contained');
33✔
174
    const left = detailsButtons.length
33!
175
      ? deploymentsRef.current.offsetLeft + detailsButtons[0].offsetLeft + detailsButtons[0].offsetWidth / 2 + 15
176
      : deploymentsRef.current.offsetWidth;
177
    let anchor = { left: deploymentsRef.current.offsetWidth / 2, top: deploymentsRef.current.offsetTop };
33✔
178
    onboardingComponent = getOnboardingComponentFor(onboardingSteps.DEPLOYMENTS_PAST_COMPLETED, onboardingState, { anchor, setSnackbar });
33✔
179
    onboardingComponent = getOnboardingComponentFor(
33✔
180
      onboardingSteps.DEPLOYMENTS_PAST_COMPLETED_FAILURE,
181
      onboardingState,
182
      { anchor: { left, top: detailsButtons[0].parentElement.offsetTop + detailsButtons[0].parentElement.offsetHeight } },
183
      onboardingComponent
184
    );
185
    onboardingComponent = getOnboardingComponentFor(onboardingSteps.ONBOARDING_FINISHED, onboardingState, { anchor }, onboardingComponent);
33✔
186
  }
187

188
  const onGroupFilterChange = (e, value) => {
66✔
189
    if (!e) {
×
190
      return;
×
191
    }
192
    setSearchValue(value);
×
193
  };
194

195
  const onTypeFilterChange = (e, value) => {
66✔
196
    if (!e) {
×
197
      return;
×
198
    }
199
    setTypeValue(value);
×
200
  };
201

202
  const onTimeFilterChange = (startDate, endDate) => setDeploymentsState({ [DEPLOYMENT_STATES.finished]: { page: 1, startDate, endDate } });
66✔
203

204
  return (
66✔
205
    <div className="fadeIn margin-left margin-top-large">
206
      <div className={`datepicker-container ${classes.datepickerContainer}`}>
207
        <TimerangePicker endDate={endDate} onChange={onTimeFilterChange} startDate={startDate} />
208
        <TimeframePicker onChange={onTimeFilterChange} endDate={endDate} startDate={startDate} tonight={tonight} />
209
        <Autocomplete
210
          id="device-group-selection"
211
          autoHighlight
212
          autoSelect
213
          filterSelectedOptions
214
          freeSolo
215
          handleHomeEndKeys
216
          inputValue={deviceGroup}
217
          options={groups}
218
          onInputChange={onGroupFilterChange}
219
          renderInput={params => (
220
            <TextField {...params} label="Filter by device group" placeholder="Select a group" InputProps={{ ...params.InputProps }} style={{ marginTop: 0 }} />
66✔
221
          )}
222
        />
223
        <Autocomplete
224
          id="deployment-type-selection"
225
          autoHighlight
226
          autoSelect
227
          filterSelectedOptions
228
          handleHomeEndKeys
229
          classes={{ input: deploymentType ? 'capitalized' : '', option: 'capitalized' }}
66!
230
          inputValue={deploymentType}
231
          onInputChange={onTypeFilterChange}
232
          options={Object.keys(DEPLOYMENT_TYPES)}
233
          renderInput={params => (
234
            <TextField {...params} label="Filter by type" placeholder="Select a type" InputProps={{ ...params.InputProps }} style={{ marginTop: 0 }} />
66✔
235
          )}
236
        />
237
      </div>
238
      <div className="deploy-table-contain">
239
        <Loader show={loading} />
240
        {/* TODO: fix status retrieval for past deployments to decide what to show here - */}
241
        {!loading && !!past.length && !!onboardingComponent && !isShowingDetails && onboardingComponent}
130!
242
        {!!past.length && (
101✔
243
          <DeploymentsList
244
            {...props}
245
            componentClass="margin-left-small"
246
            count={count}
247
            headers={headers}
248
            items={past}
249
            page={page}
250
            onChangeRowsPerPage={perPage => setDeploymentsState({ [DEPLOYMENT_STATES.finished]: { page: 1, perPage } })}
×
251
            onChangePage={page => setDeploymentsState({ [DEPLOYMENT_STATES.finished]: { page } })}
×
252
            pageSize={perPage}
253
            rootRef={deploymentsRef}
254
            showPagination
255
            type={type}
256
          />
257
        )}
258
        {!(loading || past.length) && (
167✔
259
          <div className="dashboard-placeholder">
260
            <p>No finished deployments were found.</p>
261
            <p>
262
              Try adjusting the filters, or <a onClick={createClick}>Create a new deployment</a> to get started
263
            </p>
264
            <img src={historyImage} alt="Past" />
265
          </div>
266
        )}
267
      </div>
268
    </div>
269
  );
270
};
271

272
const actionCreators = { advanceOnboarding, getDeploymentsByStatus, setDeploymentsState, setSnackbar };
7✔
273

274
const mapStateToProps = state => {
7✔
275
  const past = state.deployments.selectionState.finished.selection.reduce(tryMapDeployments, { state, deployments: [] }).deployments;
50✔
276
  // eslint-disable-next-line no-unused-vars
277
  const { [UNGROUPED_GROUP.id]: ungrouped, ...groups } = state.devices.groups.byId;
50✔
278
  const { canConfigure, canDeploy } = getUserCapabilities(state);
50✔
279
  return {
50✔
280
    canConfigure,
281
    canDeploy,
282
    devices: state.devices.byId,
283
    groups: [ALL_DEVICES, ...Object.keys(groups)],
284
    idAttribute: getIdAttribute(state).attribute,
285
    onboardingState: getOnboardingState(state),
286
    past,
287
    pastSelectionState: state.deployments.selectionState.finished
288
  };
289
};
290

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