• 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.33
/frontend/src/js/components/releases/DeltaGenerationDetailsDrawer.tsx
1
// Copyright 2024 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, useMemo, useRef, useState } from 'react';
2✔
15
import { useSelector } from 'react-redux';
2✔
16
import { Link } from 'react-router-dom';
2✔
17

2✔
18
import { Launch as LaunchIcon } from '@mui/icons-material';
2✔
19
import { Alert, Divider, Drawer, LinearProgress, tableCellClasses } from '@mui/material';
2✔
20
import { makeStyles } from 'tss-react/mui';
2✔
21

2✔
22
import { TwoColumns } from '@northern.tech/common-ui/ConfigurationObject';
2✔
23
import { Code } from '@northern.tech/common-ui/CopyCode';
2✔
24
import DetailsTable from '@northern.tech/common-ui/DetailsTable';
2✔
25
import { DrawerTitle } from '@northern.tech/common-ui/DrawerTitle';
2✔
26
import FileSize from '@northern.tech/common-ui/FileSize';
2✔
27
import LinedHeader from '@northern.tech/common-ui/LinedHeader';
2✔
28
import Loader from '@northern.tech/common-ui/Loader';
2✔
29
import { MaybeTime } from '@northern.tech/common-ui/Time';
2✔
30
import storeActions from '@northern.tech/store/actions';
2✔
31
import { TIMEOUTS } from '@northern.tech/store/constants';
2✔
32
import { formatReleases, generateReleasesPath } from '@northern.tech/store/locationutils';
2✔
33
import { getDeltaJobById } from '@northern.tech/store/selectors';
2✔
34
import { useAppDispatch } from '@northern.tech/store/store';
2✔
35
import { getDeltaGenerationJobDetails, getDeltaGenerationJobs } from '@northern.tech/store/thunks';
2✔
36
import { DeltaJobDetailsItem, DeltaJobsListItem } from '@northern.tech/types/MenderTypes';
2✔
37
import { formatTime } from '@northern.tech/utils/helpers';
2✔
38
import copy from 'copy-to-clipboard';
2✔
39
import dayjs from 'dayjs';
2✔
40
import durationDayJs from 'dayjs/plugin/duration';
2✔
41

2✔
42
dayjs.extend(durationDayJs);
7✔
43

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

2✔
46
const useStyles = makeStyles()(theme => ({
7✔
47
  detailsContainer: {
2✔
48
    padding: theme.spacing(2),
2✔
49
    minWidth: '60vw'
2✔
50
  },
2✔
51
  table: {
2✔
52
    [`.${tableCellClasses.body}, .${tableCellClasses.head}`]: {
2✔
53
      paddingLeft: 0
2✔
54
    }
2✔
55
  }
2✔
56
}));
2✔
57

2✔
58
const deltaStateTitleMap = {
7✔
59
  artifact_uploaded: 'Success'
2✔
60
};
2✔
61

2✔
62
const deltaProgressMap = {
7✔
63
  success: 100,
2✔
64
  failed: 100,
2✔
65
  artifact_uploaded: 100
2✔
66
};
2✔
67

2✔
68
const deltaStatusColorMap = {
7✔
69
  artifact_uploaded: 'success',
2✔
70
  failed: 'secondary',
2✔
71
  pending: 'primary',
2✔
72
  success: 'success'
2✔
73
};
2✔
74

2✔
75
export const StatusIndicator = ({ status }) => {
7✔
76
  const statusKey = status?.toLowerCase();
24✔
77
  const statusTitle = deltaStateTitleMap[statusKey] ?? status;
24✔
78
  const progressColor = deltaStatusColorMap[statusKey] ?? deltaStatusColorMap.pending;
24!
79
  const progressValue = deltaProgressMap[statusKey];
24✔
80
  return (
24✔
81
    <>
2✔
82
      <div className="capitalized-start">{statusTitle}</div>
2✔
83
      <LinearProgress
2✔
84
        className="absolute full-width"
2✔
85
        color={progressColor}
2✔
86
        style={{ bottom: 0 }}
2✔
87
        value={progressValue}
2✔
88
        variant={progressValue ? 'determinate' : 'indeterminate'}
2!
89
      />
2✔
90
    </>
2✔
91
  );
2✔
92
};
2✔
93

2✔
94
const statusColumns = [
7✔
95
  {
2✔
96
    key: 'started',
2✔
97
    title: 'Started',
2✔
98
    cellProps: { style: { width: '15%' } },
2✔
UNCOV
99
    render: ({ started }) => <MaybeTime value={formatTime(started)} />
2✔
100
  },
2✔
101
  {
2✔
102
    key: 'finished',
2✔
103
    title: 'Finished',
2✔
104
    cellProps: { style: { width: '15%' } },
2✔
UNCOV
105
    render: ({ finished }) => <MaybeTime value={formatTime(finished)} />
2✔
106
  },
2✔
107
  {
2✔
108
    key: 'totalTime',
2✔
109
    title: 'Total time',
2✔
110
    cellProps: { style: { width: '10%' } },
2✔
UNCOV
111
    render: ({ totalTime }) => totalTime
2✔
112
  },
2✔
113
  {
2✔
114
    key: 'toArtifactSize',
2✔
115
    title: 'Target Artifact size',
2✔
116
    cellProps: { style: { width: '12.5%' } },
2✔
UNCOV
117
    render: ({ target_size }) => (target_size ? <FileSize fileSize={target_size} /> : '-')
2!
118
  },
2✔
119
  {
2✔
120
    key: 'deltaArtifactSize',
2✔
121
    title: 'Delta Artifact size',
2✔
122
    cellProps: { style: { width: '12.5%' } },
2✔
UNCOV
123
    render: ({ delta_artifact_size }) => (delta_artifact_size ? <FileSize fileSize={delta_artifact_size} /> : '-')
2!
124
  },
2✔
125
  {
2✔
126
    key: 'dataSaved',
2✔
127
    title: 'Data saved',
2✔
128
    cellProps: { style: { width: '10%' } },
2✔
UNCOV
129
    render: ({ dataSaved }) => <FileSize fileSize={dataSaved} />
2✔
130
  },
2✔
131
  {
2✔
132
    key: 'status',
2✔
133
    title: 'Status',
2✔
134
    cellProps: { style: { width: '20%' } },
2✔
135
    render: StatusIndicator
2✔
136
  },
2✔
137
  {
2✔
138
    key: 'spacer',
2✔
139
    title: '',
2✔
140
    sortable: false,
2✔
141
    cellProps: { style: { width: '5%' } },
2✔
UNCOV
142
    render: () => ''
2✔
143
  }
2✔
144
];
2✔
145

2✔
146
const DELTA_GENERATION_TIMEOUT_MINUTES = 60;
7✔
147

2✔
148
const getTotalTime = (started?: string, finished?: string): string => {
7✔
149
  if (!started) {
4!
150
    return '-';
2✔
151
  }
2✔
152
  const startTime = dayjs(started);
4✔
153
  if (!finished) {
4!
154
    const duration = dayjs.duration(startTime.diff(dayjs()));
4✔
155
    if (duration.asMinutes() > DELTA_GENERATION_TIMEOUT_MINUTES) {
4!
156
      return `${DELTA_GENERATION_TIMEOUT_MINUTES}:00`;
4✔
157
    }
2✔
158
  }
2✔
159
  const endTime = dayjs(finished);
2✔
160
  const duration = dayjs.duration(startTime.diff(endTime));
2✔
161
  if (duration.minutes() !== Math.abs(duration.minutes())) {
2!
162
    // negative time calculated => something's off
2✔
163
    return '-';
2✔
164
  }
2✔
165
  return duration.format('HH:mm');
2✔
166
};
2✔
167

2✔
168
// Look for completion patterns in the log
2✔
169
const finishingPattern = /(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}).*?(:?completed|finished|done)/i;
7✔
170
const getFinishedTimeFromLog = (log?: string): string | undefined => {
7✔
171
  if (!log) {
4!
172
    return;
4✔
173
  }
2✔
174
  const lines = log.split('/n');
2✔
175
  for (const line of lines) {
2✔
176
    const match = line.match(finishingPattern);
2✔
177
    if (match) {
2!
178
      return match[1];
2✔
179
    }
2✔
180
  }
2✔
181
  return;
2✔
182
};
2✔
183

2✔
184
type EnhancedJobDetailsItem = DeltaJobDetailsItem &
2✔
185
  DeltaJobsListItem & {
2✔
186
    finished?: string;
2✔
187
    fromRelease: string;
2✔
188
    toRelease: string;
2✔
189
  };
2✔
190

2✔
191
const PageLink = ({ area, target }) =>
7✔
192
  target ? (
2!
193
    <Link className="flexbox center-aligned" to={`/${area}/${encodeURIComponent(target)}`} target="_blank">
2✔
194
      {target}
2✔
195
      <LaunchIcon className="margin-left-small link-color" fontSize="small" />
2✔
196
    </Link>
2✔
197
  ) : (
2✔
198
    '-'
2✔
199
  );
2✔
200

2✔
201
interface DeltaGenerationDetailsDrawerProps {
2✔
202
  jobId?: string;
2✔
203
  onClose: () => void;
2✔
204
  open: boolean;
2✔
205
}
2✔
206

2✔
207
export const DeltaGenerationDetailsDrawer = ({ jobId, onClose, open }: DeltaGenerationDetailsDrawerProps) => {
7✔
208
  const [isLoading, setIsLoading] = useState(false);
15✔
209
  const [error, setError] = useState<string | null>(null);
15✔
210
  const dispatch = useAppDispatch();
15✔
211
  const deltaJob: EnhancedJobDetailsItem = useSelector(state => getDeltaJobById(state, jobId));
22✔
212
  const { classes } = useStyles();
15✔
213
  const timer = useRef<ReturnType<typeof setInterval> | undefined>();
15✔
214

2✔
215
  const refreshJobDetails = useCallback(() => {
15✔
216
    setIsLoading(true);
4✔
217
    setError(null);
4✔
218
    // We need to get the list too to infer the completion time
2✔
219
    Promise.all([dispatch(getDeltaGenerationJobs()).unwrap(), dispatch(getDeltaGenerationJobDetails(jobId)).unwrap()])
4✔
UNCOV
220
      .catch(err => setError(err.message || 'Failed to load delta generation details'))
2!
221
      .finally(() => setIsLoading(false));
4✔
222
  }, [dispatch, jobId]);
2✔
223

2✔
224
  useEffect(() => {
15✔
225
    if (!jobId) {
9✔
226
      return;
7✔
227
    }
2✔
228
    clearInterval(timer.current);
4✔
229
    if ('failed' !== deltaJob?.status && 'success' !== deltaJob?.status) {
4!
230
      timer.current = setInterval(refreshJobDetails, TIMEOUTS.refreshDefault);
2✔
231
    }
2✔
232
    refreshJobDetails();
4✔
233
  }, [deltaJob?.status, jobId, refreshJobDetails]);
2✔
234

2✔
235
  const copyLinkToClipboard = () => {
15✔
236
    const location = window.location.href.substring(0, window.location.href.indexOf('/releases'));
2✔
237
    copy(`${location}${generateReleasesPath({ pageState: { selectedRelease: '' } })}?${formatReleases({ pageState: { tab: 'delta', id: jobId } })}`);
2✔
238
    dispatch(setSnackbar('Link copied to clipboard'));
2✔
239
  };
2✔
240

2✔
241
  const combinedData: EnhancedJobDetailsItem | undefined = useMemo(() => {
15✔
242
    if (!deltaJob) {
9✔
243
      return;
7✔
244
    }
2✔
245
    const { details, started, target_size, delta_artifact_size, to_release, to_version, from_release, from_version } = deltaJob;
4✔
246
    const finished = getFinishedTimeFromLog(details);
4✔
247
    const totalTime = getTotalTime(started, finished);
4✔
248
    const dataSaved = target_size && delta_artifact_size ? Math.max(0, target_size - delta_artifact_size) : 0;
4!
249

2✔
250
    return {
9✔
251
      ...deltaJob,
2✔
252
      toRelease: to_release || to_version || '-',
2!
253
      fromRelease: from_release || from_version || '-',
2!
254
      finished,
2✔
255
      totalTime,
2✔
256
      dataSaved
2✔
257
    };
2✔
258
  }, [deltaJob]);
2✔
259

2✔
260
  if (!combinedData) {
15✔
261
    return null;
11✔
262
  }
2✔
263

2✔
264
  const staticDetailsLeft = {
6✔
265
    'To Release': <PageLink area="releases" target={combinedData.toRelease} />,
2✔
266
    'From Release': <PageLink area="releases" target={combinedData.fromRelease} />,
2✔
267
    'Device types compatible': combinedData.devices_types_compatible?.join(', ') || '-'
2✔
268
  };
2✔
269
  const staticDetailsRight = {
15✔
270
    'From deployment': <PageLink area="deployments" target={combinedData.deployment_id} />
2✔
271
  };
2✔
272

2✔
273
  return (
15✔
274
    <Drawer anchor="right" open={open} onClose={onClose}>
2✔
275
      <DrawerTitle
2✔
276
        title={
2✔
277
          <>
2✔
278
            Delta Artifact information
2✔
279
            <div className="margin-left-small margin-right-small">ID: {jobId}</div>
2✔
280
          </>
2✔
281
        }
2✔
282
        onClose={onClose}
2✔
283
        onLinkCopy={copyLinkToClipboard}
2✔
284
      />
2✔
285
      <Divider />
2✔
286
      <div className={classes.detailsContainer}>
2✔
287
        {error && (
2!
288
          <Alert severity="error" className="margin-bottom">
2✔
289
            {error}
2✔
290
          </Alert>
2✔
291
        )}
2✔
292
        {isLoading ? (
2✔
293
          <Loader show={true} />
2✔
294
        ) : (
2✔
295
          <>
2✔
296
            <div className="two-columns">
2✔
297
              <TwoColumns items={staticDetailsLeft} />
2✔
298
              <TwoColumns items={staticDetailsRight} />
2✔
299
            </div>
2✔
300
            <LinedHeader className="margin-top-large" heading="Status" />
2✔
301
            <DetailsTable className={classes.table} columns={statusColumns} items={[combinedData]} />
2✔
302
            {combinedData.status === 'failed' && combinedData.log && <Code className="log">{combinedData.log}</Code>}
2!
303
          </>
2✔
304
        )}
2✔
305
      </div>
2✔
306
    </Drawer>
2✔
307
  );
2✔
308
};
2✔
309

2✔
310
export default DeltaGenerationDetailsDrawer;
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