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

mendersoftware / gui / 947088195

pending completion
947088195

Pull #2661

gitlab-ci

mzedel
chore: improved device filter scrolling behaviour

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #2661: chore: added lint rules for hooks usage

4411 of 6415 branches covered (68.76%)

297 of 440 new or added lines in 62 files covered. (67.5%)

1617 existing lines in 163 files now uncovered.

8311 of 10087 relevant lines covered (82.39%)

192.12 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, { useCallback, 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 } from '../../helpers';
29
import {
30
  getDeploymentsSelectionState,
31
  getDevicesById,
32
  getGroupNames,
33
  getIdAttribute,
34
  getMappedDeploymentSelection,
35
  getOnboardingState,
36
  getUserCapabilities
37
} from '../../selectors';
38
import { useDebounce } from '../../utils/debouncehook';
39
import { getOnboardingComponentFor } from '../../utils/onboardingmanager';
40
import useWindowSize from '../../utils/resizehook';
41
import { clearAllRetryTimers, clearRetryTimer, setRetryTimer } from '../../utils/retrytimer';
42
import TimeframePicker from '../common/timeframe-picker';
43
import TimerangePicker from '../common/timerange-picker';
44
import { DeploymentSize, DeploymentStatus } from './deploymentitem';
45
import { defaultRefreshDeploymentsLength as refreshDeploymentsLength } from './deployments';
46
import DeploymentsList, { defaultHeaders } from './deploymentslist';
47

48
const headers = [
7✔
49
  ...defaultHeaders.slice(0, defaultHeaders.length - 1),
50
  { title: 'Status', renderer: DeploymentStatus },
51
  { title: 'Data downloaded', renderer: DeploymentSize }
52
];
53

54
const type = DEPLOYMENT_STATES.finished;
7✔
55

56
const useStyles = makeStyles()(theme => ({
7✔
57
  datepickerContainer: {
58
    backgroundColor: theme.palette.background.lightgrey
59
  }
60
}));
61

62
export const Past = props => {
7✔
63
  const { createClick, isShowingDetails } = props;
77✔
64
  // eslint-disable-next-line no-unused-vars
65
  const size = useWindowSize();
76✔
66
  const [tonight] = useState(getISOStringBoundaries(new Date()).end);
76✔
67
  const [loading, setLoading] = useState(false);
76✔
68
  const deploymentsRef = useRef();
76✔
69
  const timer = useRef();
76✔
70
  const [searchValue, setSearchValue] = useState('');
76✔
71
  const [typeValue, setTypeValue] = useState('');
76✔
72
  const { classes } = useStyles();
76✔
73

74
  const dispatch = useDispatch();
76✔
75
  const dispatchedSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
76✔
76

77
  const { finished: pastSelectionState } = useSelector(getDeploymentsSelectionState);
76✔
78
  const past = useSelector(state => getMappedDeploymentSelection(state, type));
141✔
79
  const { canConfigure, canDeploy } = useSelector(getUserCapabilities);
76✔
80
  const { attribute: idAttribute } = useSelector(getIdAttribute);
76✔
81
  const onboardingState = useSelector(getOnboardingState);
76✔
82
  const devices = useSelector(getDevicesById);
76✔
83
  const groupNames = useSelector(getGroupNames);
76✔
84

85
  const debouncedSearch = useDebounce(searchValue, TIMEOUTS.debounceDefault);
76✔
86
  const debouncedType = useDebounce(typeValue, TIMEOUTS.debounceDefault);
76✔
87

88
  const { endDate, page, perPage, search: deviceGroup, startDate, total: count, type: deploymentType } = pastSelectionState;
76✔
89

90
  /*
91
  / refresh only finished deployments
92
  /
93
  */
94
  const refreshPast = useCallback(
76✔
95
    (
96
      currentPage = page,
4✔
97
      currentPerPage = perPage,
4✔
98
      currentStartDate = startDate,
4✔
99
      currentEndDate = endDate,
4✔
100
      currentDeviceGroup = deviceGroup,
4✔
101
      currentType = deploymentType
4✔
102
    ) => {
103
      const roundedStartDate = Math.round(Date.parse(currentStartDate) / 1000);
4✔
104
      const roundedEndDate = Math.round(Date.parse(currentEndDate) / 1000);
4✔
105
      setLoading(true);
4✔
106
      return dispatch(getDeploymentsByStatus(type, currentPage, currentPerPage, roundedStartDate, roundedEndDate, currentDeviceGroup, currentType))
4✔
107
        .then(deploymentsAction => {
108
          setLoading(false);
3✔
109
          clearRetryTimer(type, dispatchedSetSnackbar);
3✔
110
          const { total, deploymentIds } = deploymentsAction[deploymentsAction.length - 1];
3✔
111
          if (total && !deploymentIds.length) {
3!
NEW
112
            return refreshPast(currentPage, currentPerPage, currentStartDate, currentEndDate, currentDeviceGroup);
×
113
          }
114
        })
NEW
115
        .catch(err => setRetryTimer(err, 'deployments', `Couldn't load deployments.`, refreshDeploymentsLength, dispatchedSetSnackbar));
×
116
    },
117
    [deploymentType, deviceGroup, dispatch, dispatchedSetSnackbar, endDate, page, perPage, startDate]
118
  );
119

120
  useEffect(() => {
76✔
121
    const roundedStartDate = Math.round(Date.parse(startDate || BEGINNING_OF_TIME) / 1000);
4✔
122
    const roundedEndDate = Math.round(Date.parse(endDate) / 1000);
4✔
123
    setLoading(true);
4✔
124
    dispatch(getDeploymentsByStatus(type, page, perPage, roundedStartDate, roundedEndDate, deviceGroup, deploymentType, true, SORTING_OPTIONS.desc))
4✔
125
      .then(deploymentsAction => {
126
        const deploymentsList = deploymentsAction ? Object.values(deploymentsAction[0].deployments) : [];
3!
127
        if (deploymentsList.length) {
3!
128
          let newStartDate = new Date(deploymentsList[deploymentsList.length - 1].created);
3✔
129
          const { start: startDate } = getISOStringBoundaries(newStartDate);
3✔
130
          dispatch(setDeploymentsState({ [DEPLOYMENT_STATES.finished]: { startDate } }));
3✔
131
        }
132
      })
133
      .finally(() => setLoading(false));
3✔
134
    return () => {
4✔
135
      clearAllRetryTimers(dispatchedSetSnackbar);
4✔
136
    };
137
  }, [deploymentType, deviceGroup, dispatch, dispatchedSetSnackbar, endDate, page, perPage, startDate]);
138

139
  useEffect(() => {
76✔
140
    clearInterval(timer.current);
4✔
141
    timer.current = setInterval(refreshPast, refreshDeploymentsLength);
4✔
142
    refreshPast();
4✔
143
    return () => {
4✔
144
      clearInterval(timer.current);
4✔
145
    };
146
  }, [page, perPage, startDate, endDate, deviceGroup, deploymentType, refreshPast]);
147

148
  useEffect(() => {
76✔
149
    if (!past.length || onboardingState.complete) {
32✔
150
      return;
15✔
151
    }
152
    const pastDeploymentsFailed = past.reduce(
17✔
153
      (accu, item) =>
154
        item.status === 'failed' ||
30✔
155
        (item.statistics?.status &&
156
          item.statistics.status.noartifact + item.statistics.status.failure + item.statistics.status['already-installed'] + item.statistics.status.aborted >
157
            0) ||
158
        accu,
159
      false
160
    );
161
    let onboardingStep = onboardingSteps.DEPLOYMENTS_PAST_COMPLETED_NOTIFICATION;
17✔
162
    if (pastDeploymentsFailed) {
17!
UNCOV
163
      onboardingStep = onboardingSteps.DEPLOYMENTS_PAST_COMPLETED_FAILURE;
×
164
    }
165
    dispatch(advanceOnboarding(onboardingStep));
17✔
166
    setTimeout(() => {
17✔
167
      let notification = getOnboardingComponentFor(onboardingSteps.DEPLOYMENTS_PAST_COMPLETED_NOTIFICATION, onboardingState, {
15✔
168
        setSnackbar: dispatchedSetSnackbar
169
      });
170
      // the following extra check is needed since this component will still be mounted if a user returns to the initial tab after the first
171
      // onboarding deployment & thus the effects will still run, so only ever consider the notification for the second deployment
172
      notification =
15✔
173
        past.length > 1
15✔
174
          ? getOnboardingComponentFor(onboardingSteps.ONBOARDING_FINISHED_NOTIFICATION, onboardingState, { setSnackbar: dispatchedSetSnackbar }, notification)
175
          : notification;
176
      !!notification && dispatch(setSnackbar('open', TIMEOUTS.refreshDefault, '', notification, () => {}, true));
15!
177
    }, TIMEOUTS.debounceDefault);
178
  }, [past.length, onboardingState.complete, past, onboardingState, dispatch, dispatchedSetSnackbar]);
179

180
  useEffect(() => {
76✔
181
    dispatch(setDeploymentsState({ [DEPLOYMENT_STATES.finished]: { page: 1, search: debouncedSearch, type: debouncedType } }));
3✔
182
  }, [debouncedSearch, debouncedType, dispatch]);
183

184
  let onboardingComponent = null;
76✔
185
  if (deploymentsRef.current) {
76✔
186
    const detailsButtons = deploymentsRef.current.getElementsByClassName('MuiButton-contained');
42✔
187
    const left = detailsButtons.length
42!
188
      ? deploymentsRef.current.offsetLeft + detailsButtons[0].offsetLeft + detailsButtons[0].offsetWidth / 2 + 15
189
      : deploymentsRef.current.offsetWidth;
190
    let anchor = { left: deploymentsRef.current.offsetWidth / 2, top: deploymentsRef.current.offsetTop };
42✔
191
    onboardingComponent = getOnboardingComponentFor(onboardingSteps.DEPLOYMENTS_PAST_COMPLETED, onboardingState, {
42✔
192
      anchor,
193
      setSnackbar: dispatchedSetSnackbar
194
    });
195
    onboardingComponent = getOnboardingComponentFor(
42✔
196
      onboardingSteps.DEPLOYMENTS_PAST_COMPLETED_FAILURE,
197
      onboardingState,
198
      { anchor: { left, top: detailsButtons[0].parentElement.offsetTop + detailsButtons[0].parentElement.offsetHeight } },
199
      onboardingComponent
200
    );
201
    onboardingComponent = getOnboardingComponentFor(onboardingSteps.ONBOARDING_FINISHED, onboardingState, { anchor }, onboardingComponent);
42✔
202
  }
203

204
  const onGroupFilterChange = (e, value) => {
76✔
UNCOV
205
    if (!e) {
×
UNCOV
206
      return;
×
207
    }
UNCOV
208
    setSearchValue(value);
×
209
  };
210

211
  const onTypeFilterChange = (e, value) => {
76✔
UNCOV
212
    if (!e) {
×
UNCOV
213
      return;
×
214
    }
UNCOV
215
    setTypeValue(value);
×
216
  };
217

218
  const onTimeFilterChange = (startDate, endDate) => dispatch(setDeploymentsState({ [DEPLOYMENT_STATES.finished]: { page: 1, startDate, endDate } }));
76✔
219

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

292
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