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

mendersoftware / gui / 901187442

pending completion
901187442

Pull #3795

gitlab-ci

mzedel
feat: increased chances of adopting our intended navigation patterns instead of unsupported browser navigation

Ticket: None
Changelog: None
Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #3795: feat: increased chances of adopting our intended navigation patterns instead of unsupported browser navigation

4389 of 6365 branches covered (68.96%)

5 of 5 new or added lines in 1 file covered. (100.0%)

1729 existing lines in 165 files now uncovered.

8274 of 10019 relevant lines covered (82.58%)

144.86 hits per line

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

60.78
/src/js/components/deployments/scheduleddeployments.js
1
// Copyright 2020 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 { Calendar, momentLocalizer } from 'react-big-calendar';
16
import 'react-big-calendar/lib/css/react-big-calendar.css';
17
import { useDispatch, useSelector } from 'react-redux';
18

19
import { CalendarToday as CalendarTodayIcon, List as ListIcon, Refresh as RefreshIcon } from '@mui/icons-material';
20
import { Button } from '@mui/material';
21
import { makeStyles } from 'tss-react/mui';
22

23
import moment from 'moment';
24

25
import { setSnackbar } from '../../actions/appActions';
26
import { getDeploymentsByStatus, setDeploymentsState } from '../../actions/deploymentActions';
27
import { DEPLOYMENT_STATES } from '../../constants/deploymentConstants';
28
import { tryMapDeployments } from '../../helpers';
29
import { getIdAttribute, getIsEnterprise, getUserCapabilities } from '../../selectors';
30
import { clearAllRetryTimers, clearRetryTimer, setRetryTimer } from '../../utils/retrytimer';
31
import EnterpriseNotification from '../common/enterpriseNotification';
32
import { DeploymentDeviceCount, DeploymentEndTime, DeploymentPhases, DeploymentStartTime } from './deploymentitem';
33
import { defaultRefreshDeploymentsLength as refreshDeploymentsLength } from './deployments';
34
import DeploymentsList, { defaultHeaders } from './deploymentslist';
35

36
const useStyles = makeStyles()(theme => ({
7✔
37
  inactive: { color: theme.palette.text.disabled },
38
  refreshIcon: { fill: theme.palette.grey[400], width: 111, height: 111 },
39
  tabSelect: { textTransform: 'none' }
40
}));
41

42
const localizer = momentLocalizer(moment);
7✔
43

44
const headers = [
7✔
45
  ...defaultHeaders.slice(0, 2),
46
  { title: 'Start time', renderer: DeploymentStartTime, props: { direction: 'up' } },
47
  { title: `End time`, renderer: DeploymentEndTime },
48
  { title: '# devices', class: 'align-right column-defined', renderer: DeploymentDeviceCount },
49
  { title: 'Phases', renderer: DeploymentPhases }
50
];
51

52
const tabs = {
7✔
53
  list: {
54
    icon: <ListIcon />,
55
    index: 'list',
56
    title: 'List view'
57
  },
58
  calendar: {
59
    icon: <CalendarTodayIcon />,
60
    index: 'calendar',
61
    title: 'Calendar'
62
  }
63
};
64

65
const type = DEPLOYMENT_STATES.scheduled;
7✔
66

67
export const Scheduled = ({ abort, createClick, openReport, ...remainder }) => {
7✔
68
  const [calendarEvents, setCalendarEvents] = useState([]);
3✔
69
  const [tabIndex, setTabIndex] = useState(tabs.list.index);
3✔
70
  const timer = useRef();
3✔
71
  const items = useSelector(state => state.deployments.selectionState.scheduled.selection.reduce(tryMapDeployments, { state, deployments: [] }).deployments);
6✔
72
  const { canConfigure, canDeploy } = useSelector(getUserCapabilities);
3✔
73
  const count = useSelector(state => state.deployments.byStatus.scheduled.total);
6✔
74
  const { attribute: idAttribute } = useSelector(getIdAttribute);
3✔
75
  const devices = useSelector(state => state.devices.byId);
6✔
76
  // TODO: isEnterprise is misleading here, but is passed down to the DeploymentListItem, this should be renamed
77
  const isEnterprise = useSelector(state => {
3✔
78
    const { plan = 'os' } = state.organization.organization;
6!
79
    return getIsEnterprise(state) || plan !== 'os';
6!
80
  });
81
  const scheduledState = useSelector(state => state.deployments.selectionState.scheduled);
6✔
82
  const dispatch = useDispatch();
3✔
83
  const dispatchedSetSnackbar = (...args) => dispatch(setSnackbar(...args));
3✔
84
  const { classes } = useStyles();
3✔
85

86
  const { page, perPage } = scheduledState;
3✔
87

88
  useEffect(() => {
3✔
89
    if (!isEnterprise) {
2!
UNCOV
90
      return;
×
91
    }
92
    refreshDeployments();
2✔
93
    return () => {
2✔
94
      clearAllRetryTimers(dispatchedSetSnackbar);
2✔
95
    };
96
  }, [isEnterprise]);
97

98
  useEffect(() => {
3✔
99
    if (!isEnterprise) {
2!
UNCOV
100
      return;
×
101
    }
102
    clearInterval(timer.current);
2✔
103
    timer.current = setInterval(refreshDeployments, refreshDeploymentsLength);
2✔
104
    return () => {
2✔
105
      clearInterval(timer.current);
2✔
106
    };
107
  }, [isEnterprise, page, perPage]);
108

109
  useEffect(() => {
3✔
110
    if (tabIndex !== tabs.calendar.index) {
2!
111
      return;
2✔
112
    }
UNCOV
113
    const calendarEvents = items.map(deployment => {
×
UNCOV
114
      const start = new Date(deployment.start_ts || deployment.phases ? deployment.phases[0].start_ts : deployment.created);
×
UNCOV
115
      let endDate = start;
×
UNCOV
116
      if (deployment.phases && deployment.phases.length && deployment.phases[deployment.phases.length - 1].end_ts) {
×
UNCOV
117
        endDate = new Date(deployment.phases[deployment.phases.length - 1].end_ts);
×
UNCOV
118
      } else if (deployment.filter_id || deployment.filter) {
×
119
        // calendar doesn't support never ending events so we arbitrarly set one year
UNCOV
120
        endDate = moment(start).add(1, 'year');
×
121
      }
UNCOV
122
      return {
×
123
        allDay: !(deployment.filter_id || deployment.filter),
×
124
        id: deployment.id,
125
        title: `${deployment.name} ${deployment.artifact_name}`,
126
        start,
127
        end: endDate
128
      };
129
    });
UNCOV
130
    setCalendarEvents(calendarEvents);
×
131
  }, [tabIndex]);
132

133
  const refreshDeployments = useCallback(() => {
3✔
134
    return dispatch(getDeploymentsByStatus(DEPLOYMENT_STATES.scheduled, page, perPage))
2✔
135
      .then(deploymentsAction => {
136
        clearRetryTimer(type, dispatchedSetSnackbar);
1✔
137
        const { total, deploymentIds } = deploymentsAction[deploymentsAction.length - 1];
1✔
138
        if (total && !deploymentIds.length) {
1!
UNCOV
139
          return refreshDeployments();
×
140
        }
141
      })
UNCOV
142
      .catch(err => setRetryTimer(err, 'deployments', `Couldn't load deployments.`, refreshDeploymentsLength, dispatchedSetSnackbar));
×
143
  }, [page, perPage]);
144

145
  const abortDeployment = id => abort(id).then(refreshDeployments);
3✔
146

147
  const props = {
3✔
148
    ...remainder,
149
    canDeploy,
150
    canConfigure,
151
    count,
152
    devices,
153
    idAttribute,
154
    isEnterprise,
155
    items,
156
    openReport,
157
    page
158
  };
159
  return (
3✔
160
    <div className="fadeIn margin-left">
161
      {items.length ? (
3✔
162
        <>
163
          <div className="margin-large margin-left-small">
164
            {Object.entries(tabs).map(([currentIndex, tab]) => (
165
              <Button
4✔
166
                className={`${classes.tabSelect} ${currentIndex !== tabIndex ? classes.inactive : ''}`}
4✔
167
                color="primary"
168
                key={currentIndex}
169
                startIcon={tab.icon}
UNCOV
170
                onClick={() => setTabIndex(currentIndex)}
×
171
              >
172
                {tab.title}
173
              </Button>
174
            ))}
175
          </div>
176
          {tabIndex === tabs.list.index && (
4✔
177
            <DeploymentsList
178
              {...props}
179
              abort={abortDeployment}
180
              headers={headers}
181
              type={type}
UNCOV
182
              onChangeRowsPerPage={perPage => dispatch(setDeploymentsState({ [DEPLOYMENT_STATES.scheduled]: { page: 1, perPage } }))}
×
UNCOV
183
              onChangePage={page => dispatch(setDeploymentsState({ [DEPLOYMENT_STATES.scheduled]: { page } }))}
×
184
            />
185
          )}
186
          {tabIndex === tabs.calendar.index && (
2!
187
            <Calendar
188
              localizer={localizer}
189
              className="margin-left margin-bottom"
190
              events={calendarEvents}
191
              startAccessor="start"
192
              endAccessor="end"
193
              style={{ height: 700 }}
UNCOV
194
              onSelectEvent={calendarEvent => openReport(type, calendarEvent.id)}
×
195
            />
196
          )}
197
        </>
198
      ) : (
199
        <div className="dashboard-placeholder margin-top">
200
          {isEnterprise ? (
1!
201
            <>
202
              <p>Scheduled deployments will appear here. </p>
203
              {canDeploy && (
2✔
204
                <p>
205
                  <a onClick={createClick}>Create a deployment</a> to get started
206
                </p>
207
              )}
208
            </>
209
          ) : (
210
            <div className="flexbox centered">
211
              <EnterpriseNotification isEnterprise={isEnterprise} benefit="scheduled deployments to steer the distribution of your updates." />
212
            </div>
213
          )}
214
          <RefreshIcon className={`flip-horizontal ${classes.refreshIcon}`} />
215
        </div>
216
      )}
217
    </div>
218
  );
219
};
220

221
export default Scheduled;
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