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

mendersoftware / mender-server / 10423

11 Nov 2025 04:53PM UTC coverage: 74.435% (-0.1%) from 74.562%
10423

push

gitlab-ci

web-flow
Merge pull request #1071 from mendersoftware/dependabot/npm_and_yarn/frontend/main/development-dependencies-92732187be

3868 of 5393 branches covered (71.72%)

Branch coverage included in aggregate %.

5 of 5 new or added lines in 2 files covered. (100.0%)

176 existing lines in 95 files now uncovered.

64605 of 86597 relevant lines covered (74.6%)

7.74 hits per line

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

92.16
/frontend/src/js/components/deployments/ScheduledDeployments.tsx
1
// Copyright 2020 Northern.tech AS
2✔
2
//
2✔
3
//    Licensed under the Apache License, Version 2.0 (the "License");
2✔
4
//    you may not use this file except in compliance with the License.
2✔
5
//    You may obtain a copy of the License at
2✔
6
//
2✔
7
//        http://www.apache.org/licenses/LICENSE-2.0
2✔
8
//
2✔
9
//    Unless required by applicable law or agreed to in writing, software
2✔
10
//    distributed under the License is distributed on an "AS IS" BASIS,
2✔
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2✔
12
//    See the License for the specific language governing permissions and
2✔
13
//    limitations under the License.
2✔
14
import { useCallback, useEffect, useRef, useState } from 'react';
2✔
15
import { Calendar, dayjsLocalizer } from 'react-big-calendar';
2✔
16
import 'react-big-calendar/lib/css/react-big-calendar.css';
2✔
17
import { useDispatch, useSelector } from 'react-redux';
2✔
18

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

2✔
23
import { DefaultUpgradeNotification } from '@northern.tech/common-ui/EnterpriseNotification';
2✔
24
import storeActions from '@northern.tech/store/actions';
2✔
25
import { DEPLOYMENT_STATES } from '@northern.tech/store/constants';
2✔
26
import {
2✔
27
  getDeploymentsByStatus as getDeploymentsByStatusSelector,
2✔
28
  getDeploymentsSelectionState,
2✔
29
  getDevicesById,
2✔
30
  getIdAttribute,
2✔
31
  getMappedDeploymentSelection,
2✔
32
  getTenantCapabilities,
2✔
33
  getUserCapabilities
2✔
34
} from '@northern.tech/store/selectors';
2✔
35
import { getDeploymentsByStatus, setDeploymentsState } from '@northern.tech/store/thunks';
2✔
36
import { clearAllRetryTimers, clearRetryTimer, setRetryTimer } from '@northern.tech/utils/retrytimer';
2✔
37
import dayjs from 'dayjs';
2✔
38

2✔
39
import { DeploymentDeviceCount, DeploymentEndTime, DeploymentPhases, DeploymentStartTime } from './DeploymentItem';
2✔
40
import DeploymentsList, { defaultHeaders } from './DeploymentsList';
2✔
41
import { defaultRefreshDeploymentsLength as refreshDeploymentsLength } from './constants';
2✔
42

2✔
43
const { setSnackbar } = storeActions;
7✔
44

2✔
45
const useStyles = makeStyles()(theme => ({
7✔
46
  inactive: { color: theme.palette.text.disabled },
2✔
47
  refreshIcon: { fill: theme.palette.grey[400], width: 111, height: 111 },
2✔
48
  tabSelect: { textTransform: 'none' }
2✔
49
}));
2✔
50

2✔
51
const localizer = dayjsLocalizer(dayjs);
7✔
52

2✔
53
const headers = [
7✔
54
  ...defaultHeaders.slice(0, 2),
2✔
55
  { title: 'Start time', renderer: DeploymentStartTime, props: { direction: 'up' } },
2✔
56
  { title: `End time`, renderer: DeploymentEndTime },
2✔
57
  { title: '# devices', class: 'align-right column-defined', renderer: DeploymentDeviceCount },
2✔
58
  { title: 'Phases', renderer: DeploymentPhases }
2✔
59
];
2✔
60

2✔
61
const tabs = {
7✔
62
  list: {
2✔
63
    icon: <ListIcon />,
2✔
64
    index: 'list',
2✔
65
    title: 'List view'
2✔
66
  },
2✔
67
  calendar: {
2✔
68
    icon: <CalendarTodayIcon />,
2✔
69
    index: 'calendar',
2✔
70
    title: 'Calendar'
2✔
71
  }
2✔
72
};
2✔
73

2✔
74
const type = DEPLOYMENT_STATES.scheduled;
7✔
75

2✔
76
export const Scheduled = ({ abort, createClick, openReport, ...remainder }) => {
7✔
77
  const [calendarEvents, setCalendarEvents] = useState([]);
7✔
78
  const [tabIndex, setTabIndex] = useState(tabs.list.index);
7✔
79
  const timer = useRef();
7✔
80
  const { canConfigure, canDeploy } = useSelector(getUserCapabilities);
7✔
81
  const {
2✔
82
    scheduled: { total: count }
2✔
83
  } = useSelector(getDeploymentsByStatusSelector);
7✔
84
  const idAttribute = useSelector(getIdAttribute);
7✔
85
  const devices = useSelector(getDevicesById);
7✔
86
  // TODO: isEnterprise is misleading here, but is passed down to the DeploymentListItem, this should be renamed
2✔
87
  const { canDelta: isEnterprise } = useSelector(getTenantCapabilities);
7✔
88
  const { scheduled: scheduledState } = useSelector(getDeploymentsSelectionState);
7✔
89
  const items = useSelector(state => getMappedDeploymentSelection(state, type));
26✔
90
  const dispatch = useDispatch();
7✔
91
  const dispatchedSetSnackbar = useCallback((...args) => dispatch(setSnackbar(...args)), [dispatch]);
7✔
92
  const { classes } = useStyles();
7✔
93

2✔
94
  const { page, perPage } = scheduledState;
7✔
95

2✔
96
  const refreshDeployments = useCallback(
7✔
97
    () =>
2✔
98
      dispatch(getDeploymentsByStatus({ status: DEPLOYMENT_STATES.scheduled, page, perPage }))
5✔
99
        .then(({ payload }) => {
2✔
100
          clearRetryTimer(type, dispatchedSetSnackbar);
5✔
101
          const { total, deploymentIds } = payload[payload.length - 1];
5✔
102
          if (total && !deploymentIds.length) {
5!
103
            return refreshDeployments();
2✔
104
          }
2✔
105
        })
2✔
UNCOV
106
        .catch(err => setRetryTimer(err, 'deployments', `Couldn't load deployments.`, refreshDeploymentsLength, dispatchedSetSnackbar)),
2✔
107
    [dispatch, dispatchedSetSnackbar, page, perPage]
2✔
108
  );
2✔
109

2✔
110
  useEffect(() => {
7✔
111
    if (!isEnterprise) {
4!
112
      return;
2✔
113
    }
2✔
114
    refreshDeployments();
4✔
115
    return () => {
4✔
116
      clearAllRetryTimers(dispatchedSetSnackbar);
4✔
117
    };
2✔
118
  }, [dispatchedSetSnackbar, isEnterprise, refreshDeployments]);
2✔
119

2✔
120
  useEffect(() => {
7✔
121
    if (!isEnterprise) {
4!
122
      return;
2✔
123
    }
2✔
124
    clearInterval(timer.current);
4✔
125
    timer.current = setInterval(refreshDeployments, refreshDeploymentsLength);
4✔
126
    return () => {
4✔
127
      clearInterval(timer.current);
4✔
128
    };
2✔
129
  }, [isEnterprise, page, perPage, refreshDeployments]);
2✔
130

2✔
131
  useEffect(() => {
7✔
132
    if (tabIndex !== tabs.calendar.index) {
5!
133
      return;
5✔
134
    }
2✔
135
    const calendarEvents = items.map(deployment => {
2✔
136
      const start = new Date(deployment.start_ts || deployment.phases ? deployment.phases[0].start_ts : deployment.created);
2!
137
      let endDate = start;
2✔
138
      if (deployment.phases && deployment.phases.length && deployment.phases[deployment.phases.length - 1].end_ts) {
2!
139
        endDate = new Date(deployment.phases[deployment.phases.length - 1].end_ts);
2✔
140
      } else if (deployment.filter_id || deployment.filter) {
2!
141
        // calendar doesn't support never ending events so we arbitrarly set one year
2✔
142
        endDate = dayjs(start).add(1, 'year').toDate();
2✔
143
      }
2✔
144
      return {
2✔
145
        allDay: !(deployment.filter_id || deployment.filter),
2!
146
        id: deployment.id,
2✔
147
        title: `${deployment.name} ${deployment.artifact_name}`,
2✔
148
        start,
2✔
149
        end: endDate
2✔
150
      };
2✔
151
    });
2✔
152
    setCalendarEvents(calendarEvents);
2✔
153
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
154
  }, [JSON.stringify(items), tabIndex]);
2✔
155

2✔
156
  const abortDeployment = id => abort(id).then(refreshDeployments);
7✔
157

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

2✔
231
export default Scheduled;
2✔
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