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

mendersoftware / gui / 913068613

pending completion
913068613

Pull #3803

gitlab-ci

web-flow
Merge pull request #3801 from mzedel/men-6383

MEN-6383 - device check in time
Pull Request #3803: staging alignment

4418 of 6435 branches covered (68.66%)

178 of 246 new or added lines in 27 files covered. (72.36%)

8352 of 10138 relevant lines covered (82.38%)

160.95 hits per line

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

83.22
/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 { useDispatch, useSelector } 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 { onboardingSteps } from '../../constants/onboardingConstants';
28
import { getISOStringBoundaries, tryMapDeployments } from '../../helpers';
29
import { getGroupNames, getIdAttribute, getOnboardingState, getUserCapabilities } from '../../selectors';
30
import { useDebounce } from '../../utils/debouncehook';
31
import { getOnboardingComponentFor } from '../../utils/onboardingmanager';
32
import useWindowSize from '../../utils/resizehook';
33
import { clearAllRetryTimers, clearRetryTimer, setRetryTimer } from '../../utils/retrytimer';
34
import TimeframePicker from '../common/timeframe-picker';
35
import TimerangePicker from '../common/timerange-picker';
36
import { DeploymentSize, DeploymentStatus } from './deploymentitem';
37
import { defaultRefreshDeploymentsLength as refreshDeploymentsLength } from './deployments';
38
import DeploymentsList, { defaultHeaders } from './deploymentslist';
39

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

46
const type = DEPLOYMENT_STATES.finished;
7✔
47

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

54
export const Past = props => {
7✔
55
  const { createClick, isShowingDetails } = props;
69✔
56
  // eslint-disable-next-line no-unused-vars
57
  const size = useWindowSize();
68✔
58
  const [tonight] = useState(getISOStringBoundaries(new Date()).end);
68✔
59
  const [loading, setLoading] = useState(false);
68✔
60
  const deploymentsRef = useRef();
68✔
61
  const timer = useRef();
68✔
62
  const [searchValue, setSearchValue] = useState('');
68✔
63
  const [typeValue, setTypeValue] = useState('');
68✔
64
  const { classes } = useStyles();
68✔
65

66
  const dispatch = useDispatch();
68✔
67
  const dispatchedSetSnackbar = (...args) => dispatch(setSnackbar(...args));
68✔
68

69
  const past = useSelector(state => state.deployments.selectionState.finished.selection.reduce(tryMapDeployments, { state, deployments: [] }).deployments);
121✔
70
  const { canConfigure, canDeploy } = useSelector(getUserCapabilities);
68✔
71
  const { attribute: idAttribute } = useSelector(getIdAttribute);
68✔
72
  const onboardingState = useSelector(getOnboardingState);
68✔
73
  const pastSelectionState = useSelector(state => state.deployments.selectionState.finished);
121✔
74
  const devices = useSelector(state => state.devices.byId);
121✔
75
  const groupNames = useSelector(getGroupNames);
68✔
76

77
  const debouncedSearch = useDebounce(searchValue, TIMEOUTS.debounceDefault);
68✔
78
  const debouncedType = useDebounce(typeValue, TIMEOUTS.debounceDefault);
68✔
79

80
  const { endDate, page, perPage, search: deviceGroup, startDate, total: count, type: deploymentType } = pastSelectionState;
68✔
81

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

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

110
  useEffect(() => {
68✔
111
    if (!past.length || onboardingState.complete) {
4✔
112
      return;
2✔
113
    }
114
    const pastDeploymentsFailed = past.reduce(
2✔
115
      (accu, item) =>
116
        item.status === 'failed' ||
3✔
117
        (item.statistics?.status &&
118
          item.statistics.status.noartifact + item.statistics.status.failure + item.statistics.status['already-installed'] + item.statistics.status.aborted >
119
            0) ||
120
        accu,
121
      false
122
    );
123
    let onboardingStep = onboardingSteps.DEPLOYMENTS_PAST_COMPLETED_NOTIFICATION;
2✔
124
    if (pastDeploymentsFailed) {
2!
125
      onboardingStep = onboardingSteps.DEPLOYMENTS_PAST_COMPLETED_FAILURE;
×
126
    }
127
    dispatch(advanceOnboarding(onboardingStep));
2✔
128
    setTimeout(() => {
2✔
129
      let notification = getOnboardingComponentFor(onboardingSteps.DEPLOYMENTS_PAST_COMPLETED_NOTIFICATION, onboardingState, {
2✔
130
        setSnackbar: dispatchedSetSnackbar
131
      });
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: dispatchedSetSnackbar }, notification)
137
          : notification;
138
      !!notification && dispatch(setSnackbar('open', TIMEOUTS.refreshDefault, '', notification, () => {}, true));
2!
139
    }, TIMEOUTS.debounceDefault);
140
  }, [past.length, onboardingState.complete]);
141

142
  useEffect(() => {
68✔
143
    dispatch(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 = (
68✔
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
    setLoading(true);
4✔
161
    return dispatch(getDeploymentsByStatus(type, currentPage, currentPerPage, roundedStartDate, roundedEndDate, currentDeviceGroup, currentType))
4✔
162
      .then(deploymentsAction => {
163
        setLoading(false);
3✔
164
        clearRetryTimer(type, dispatchedSetSnackbar);
3✔
165
        const { total, deploymentIds } = deploymentsAction[deploymentsAction.length - 1];
3✔
166
        if (total && !deploymentIds.length) {
3!
167
          return refreshPast(currentPage, currentPerPage, currentStartDate, currentEndDate, currentDeviceGroup);
×
168
        }
169
      })
170
      .catch(err => setRetryTimer(err, 'deployments', `Couldn't load deployments.`, refreshDeploymentsLength, dispatchedSetSnackbar));
×
171
  };
172

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

193
  const onGroupFilterChange = (e, value) => {
68✔
194
    if (!e) {
×
195
      return;
×
196
    }
197
    setSearchValue(value);
×
198
  };
199

200
  const onTypeFilterChange = (e, value) => {
68✔
201
    if (!e) {
×
202
      return;
×
203
    }
204
    setTypeValue(value);
×
205
  };
206

207
  const onTimeFilterChange = (startDate, endDate) => dispatch(setDeploymentsState({ [DEPLOYMENT_STATES.finished]: { page: 1, startDate, endDate } }));
68✔
208

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

281
export default 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