• 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

91.75
/frontend/src/js/components/devices/troubleshoot/FileTransfer.tsx
1
// Copyright 2021 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, useState } from 'react';
2✔
15
import { useDispatch } from 'react-redux';
2✔
16

2✔
17
import { FileCopy as CopyPasteIcon } from '@mui/icons-material';
2✔
18
import { Button, Divider, IconButton, InputAdornment, Tab, Tabs, TextField, Tooltip } from '@mui/material';
2✔
19
import { makeStyles } from 'tss-react/mui';
2✔
20

2✔
21
import FileUpload from '@northern.tech/common-ui/forms/FileUpload';
2✔
22
import { canAccess } from '@northern.tech/store/constants';
2✔
23
import { deviceFileUpload } from '@northern.tech/store/thunks';
2✔
24

2✔
25
const tabs = [
13✔
26
  { key: 'upload', canAccess: ({ userCapabilities: { canTroubleshoot, canWriteDevices } }) => canTroubleshoot && canWriteDevices },
6✔
27
  { key: 'download', canAccess }
2✔
28
];
2✔
29

2✔
30
const useStyles = makeStyles()(theme => ({
13✔
31
  column: { maxWidth: theme.spacing(80) },
2✔
32
  inputWrapper: { alignItems: 'end', gap: theme.spacing(2), display: 'grid', gridTemplateColumns: `${theme.spacing(60)} min-content` }
2✔
33
}));
2✔
34

2✔
35
const CopyPasteButton = ({ onClick }) => (
13✔
36
  <InputAdornment position="end">
8✔
37
    <Tooltip title="Paste" placement="top">
2✔
38
      <IconButton onClick={onClick}>
2✔
39
        <CopyPasteIcon />
2✔
40
      </IconButton>
2✔
41
    </Tooltip>
2✔
42
  </InputAdornment>
2✔
43
);
2✔
44

2✔
45
export const FileTransfer = ({
13✔
46
  device: { id: deviceId },
2✔
47
  downloadPath,
2✔
48
  file,
2✔
49
  onDownload,
2✔
50
  setFile,
2✔
51
  setDownloadPath,
2✔
52
  setUploadPath,
2✔
53
  uploadPath,
2✔
54
  userCapabilities
2✔
55
}) => {
2✔
56
  const { classes } = useStyles();
8✔
57
  const [currentTab, setCurrentTab] = useState(tabs[0].key);
8✔
58
  const [isValidDestination, setIsValidDestination] = useState(true);
8✔
59
  const [availableTabs, setAvailableTabs] = useState(tabs);
8✔
60
  const dispatch = useDispatch();
8✔
61

2✔
62
  useEffect(() => {
8✔
63
    const destination = currentTab === 'download' ? downloadPath : uploadPath;
6!
64
    const isValid = destination.length ? /^(?:\/|[a-z]+:\/\/)/.test(destination) : true;
6!
65
    setIsValidDestination(isValid);
6✔
66
  }, [currentTab, downloadPath, uploadPath]);
2✔
67

2✔
68
  useEffect(() => {
8✔
69
    const availableTabs = tabs.reduce((accu, item) => {
6✔
70
      if (item.canAccess({ userCapabilities })) {
10!
71
        accu.push(item);
10✔
72
      }
2✔
73
      return accu;
10✔
74
    }, []);
2✔
75
    setAvailableTabs(availableTabs);
6✔
76
    setCurrentTab(availableTabs[0].key);
6✔
77
    // eslint-disable-next-line react-hooks/exhaustive-deps
2✔
78
  }, [JSON.stringify(userCapabilities)]);
2✔
79

2✔
80
  const onPasteDownloadClick = async () => {
8✔
81
    const path = await navigator.clipboard.readText();
2✔
82
    setDownloadPath(path);
2✔
83
  };
2✔
84

2✔
85
  const onPasteUploadClick = async () => {
8✔
86
    const path = await navigator.clipboard.readText();
2✔
87
    setUploadPath(path);
2✔
88
  };
2✔
89

2✔
90
  const onFileSelect = selectedFile => {
8✔
91
    let path;
2✔
92
    if (selectedFile) {
2!
93
      path = `${uploadPath}/${selectedFile.name}`;
2✔
94
    } else {
2✔
95
      path = file && uploadPath.includes(file.name) ? uploadPath.substring(0, uploadPath.lastIndexOf('/')) : uploadPath;
2!
96
    }
2✔
97
    setUploadPath(path);
2✔
98
    setFile(selectedFile);
2✔
99
  };
2✔
100

2✔
101
  const onUploadClick = useCallback(() => dispatch(deviceFileUpload({ deviceId, path: uploadPath, file })), [dispatch, deviceId, uploadPath, file]);
8✔
102

2✔
103
  const fileInputProps = {
8✔
104
    error: !isValidDestination,
2✔
105
    fullWidth: true,
2✔
106
    InputLabelProps: { shrink: true }
2✔
107
  };
2✔
108

2✔
109
  return (
8✔
110
    <div className={classes.column}>
2✔
UNCOV
111
      <Tabs onChange={(e, item) => setCurrentTab(item)} value={currentTab} visibleScrollbar>
2✔
112
        {availableTabs.map(({ key }) => (
2✔
113
          <Tab className="capitalized" key={key} label={key} value={key} />
14✔
114
        ))}
2✔
115
      </Tabs>
2✔
116
      <Divider />
2✔
117
      {currentTab === 'upload' ? (
2!
118
        <div className={classes.column}>
2✔
119
          <div>
2✔
120
            <p className="margin-top">
2✔
121
              <b>Upload a file to the device</b>
2✔
122
            </p>
2✔
123
            <FileUpload
2✔
124
              enableContentReading={false}
2✔
125
              fileNameSelection={file?.name}
2✔
UNCOV
126
              onFileChange={() => undefined}
2✔
127
              onFileSelect={onFileSelect}
2✔
128
              placeholder={
2✔
129
                <>
2✔
130
                  Drag here or <a>browse</a> to upload a file
2✔
131
                </>
2✔
132
              }
2✔
133
            />
2✔
134
          </div>
2✔
135
          <p className="margin-top margin-bottom-none">
2✔
136
            <b>Destination directory on the device where the file will be transferred</b>
2✔
137
          </p>
2✔
138
          <div className={classes.inputWrapper}>
2✔
139
            <TextField
2✔
140
              {...fileInputProps}
2✔
141
              helperText={!isValidDestination && <div className="warning">Destination has to be an absolute path</div>}
2!
UNCOV
142
              onChange={e => setUploadPath(e.target.value)}
2✔
143
              placeholder="Example: /opt/installed-by-single-file"
2✔
144
              value={uploadPath}
2✔
145
              InputProps={{ endAdornment: <CopyPasteButton onClick={onPasteUploadClick} /> }}
2✔
146
            />
2✔
147
            <Button variant="contained" disabled={!(file && uploadPath && isValidDestination)} onClick={onUploadClick}>
2!
148
              Upload
2✔
149
            </Button>
2✔
150
          </div>
2✔
151
        </div>
2✔
152
      ) : (
2✔
153
        <div>
2✔
154
          <p className="margin-top margin-bottom-none">
2✔
155
            <b>Path to the file on the device</b>
2✔
156
          </p>
2✔
157
          <div className={classes.inputWrapper}>
2✔
158
            <TextField
2✔
159
              {...fileInputProps}
2✔
160
              helperText={!isValidDestination && <div className="warning">Destination has to be an absolute path</div>}
2!
161
              onChange={e => setDownloadPath(e.target.value)}
2✔
162
              placeholder="Example: /home/mender/"
2✔
163
              value={downloadPath}
2✔
164
              InputProps={{ endAdornment: <CopyPasteButton onClick={onPasteDownloadClick} /> }}
2✔
165
            />
2✔
166
            <Button
2✔
167
              variant="contained"
2✔
168
              disabled={!(downloadPath && isValidDestination)}
2!
169
              onClick={() => onDownload(downloadPath)}
2✔
170
              style={{ alignSelf: 'flex-end' }}
2✔
171
            >
2✔
172
              Download
2✔
173
            </Button>
2✔
174
          </div>
2✔
175
        </div>
2✔
176
      )}
2✔
177
    </div>
2✔
178
  );
2✔
179
};
2✔
180

2✔
181
export default FileTransfer;
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