Skip to content

Commit

Permalink
feat(gui): added feedback on file size limits to artifact upload dialog
Browse files Browse the repository at this point in the history
Ticket: MEN-7858
Changelog: Title
Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
  • Loading branch information
mzedel committed Jan 9, 2025
1 parent 0435e0c commit d612334
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 10 deletions.
31 changes: 31 additions & 0 deletions frontend/src/js/common-ui/InputErrorNotification.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2025 Northern.tech AS
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';

import { TIMEOUTS } from '@northern.tech/store/commonConstants';
import { screen, waitFor } from '@testing-library/react';

import { render } from '../../../tests/setupTests';
import { InputErrorNotification } from './InputErrorNotification';

describe('InfoHint Component', () => {
it('renders correctly', async () => {
const ui = <InputErrorNotification content="test" className="some-class" />;
const { rerender } = render(ui);
expect(screen.getByText('test')).toBeVisible();
await jest.advanceTimersByTimeAsync(TIMEOUTS.fiveSeconds);
await waitFor(() => rerender(ui));
expect(screen.getByText('test')).toHaveClass('fadeOut');
});
});
32 changes: 32 additions & 0 deletions frontend/src/js/common-ui/InputErrorNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2025 Northern.tech AS
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React, { useEffect, useRef, useState } from 'react';

import { TIMEOUTS } from '@northern.tech/store/commonConstants';

export const InputErrorNotification = ({ className, content }: { className: string; content: string }) => {
const [isVisible, setIsVisible] = useState(false);
const timer = useRef<NodeJS.Timeout | undefined>();

useEffect(() => {
if (!content) {
return;
}
setIsVisible(true);
timer.current = setTimeout(() => setIsVisible(false), TIMEOUTS.fiveSeconds);
return () => clearTimeout(timer.current);
}, [content]);

return <p className={`warning ${isVisible ? 'fadeInSlow' : 'fadeOut'} ${className}`}>{content}</p>;
};
63 changes: 53 additions & 10 deletions frontend/src/js/components/releases/dialogs/AddArtifact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import Dropzone from 'react-dropzone';
import { useDispatch, useSelector } from 'react-redux';

import { CloudUpload } from '@mui/icons-material';
import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
import { makeStyles } from 'tss-react/mui';

import { InputErrorNotification } from '@northern.tech/common-ui/InputErrorNotification';
import storeActions from '@northern.tech/store/actions';
import { getDeviceTypes } from '@northern.tech/store/selectors';
import { createArtifact, uploadArtifact } from '@northern.tech/store/thunks';
Expand All @@ -31,7 +32,29 @@ import ArtifactUploadConfirmation from './ArtifactUpload';

const { setSnackbar } = storeActions;

const reFilename = new RegExp(/^[a-z0-9.,_-]+$/i);
type SupportedUploadTypes = 'mender' | 'singleFile';

type Update = {
customDeviceTypes?: string;
destination?: string;
file?: File;
fileSystem?: string;
finalStep: boolean;
isValid: boolean;
isValidDestination?: boolean;
name: string;
selectedDeviceTypes?: string[];
softwareName?: string;
softwareVersion?: string;
type: SupportedUploadTypes;
};

type UploadType = {
key: SupportedUploadTypes;
component: ReactNode;
};

type UploadTypes = Record<string, UploadType>;

const useStyles = makeStyles()(theme => ({
dropzone: { ['&.dropzone']: { padding: theme.spacing(4) } },
Expand All @@ -46,7 +69,7 @@ const useStyles = makeStyles()(theme => ({
fileSizeWrapper: { marginTop: 5 }
}));

const uploadTypes = {
const uploadTypes: UploadTypes = {
mender: {
key: 'mender',
component: ArtifactUploadConfirmation
Expand All @@ -67,29 +90,48 @@ const shortenFileName = name => {
return name;
};

export const ArtifactUpload = ({ setSnackbar, updateCreation }) => {
const singleFileLimit = 512 * 1024 ** 2; //512MiB
const menderFileLimit = 10 * 1024 ** 3; //10GiB
const reFilename = new RegExp(/^[a-z0-9.,_-]+$/i);

const isMenderArtifact = (name: string): boolean => name.endsWith('.mender');

const validateFile = ({ name, size }: File): string => {
if (!reFilename.test(name)) {
return 'Only letters, digits and characters in the set ".,_-" are allowed in the filename.';
} else if (isMenderArtifact(name) && size > menderFileLimit) {
return 'Only artifacts smaller than 10GiB are supported.';
} else if (!isMenderArtifact(name) && size > singleFileLimit) {
return 'Artifact generation is only supported for files smaller than 512MiB.';
}
return '';
};

export const ArtifactUpload = ({ updateCreation }: { updateCreation: (some: Partial<Update>) => void }) => {
const onboardingAnchor = useRef();
const { classes } = useStyles();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const size = useWindowSize();
const [errorMessage, setErrorMessage] = useState<string>('');

const onDrop = acceptedFiles => {
const emptyFileInfo = { file: undefined, name: '', type: uploadTypes.mender.key };
if (acceptedFiles.length === 1) {
if (!reFilename.test(acceptedFiles[0].name)) {
const validationError = validateFile(acceptedFiles[0]);
if (validationError) {
updateCreation(emptyFileInfo);
setSnackbar('Only letters, digits and characters in the set ".,_-" are allowed in the filename.', null);
setErrorMessage(validationError);
} else {
const { name } = acceptedFiles[0];
updateCreation({
file: acceptedFiles[0],
name: shortenFileName(name),
type: name.endsWith('.mender') ? uploadTypes.mender.key : uploadTypes.singleFile.key
type: isMenderArtifact(name) ? uploadTypes.mender.key : uploadTypes.singleFile.key
});
}
} else {
updateCreation(emptyFileInfo);
setSnackbar('The selected file is not supported.', null);
setErrorMessage('The selected file is not supported.');
}
};

Expand All @@ -111,13 +153,14 @@ export const ArtifactUpload = ({ setSnackbar, updateCreation }) => {
</div>
)}
</Dropzone>
<InputErrorNotification className="flexbox centered" content={errorMessage} />
</>
);
};

export const AddArtifactDialog = ({ onCancel, onUploadStarted, releases, selectedFile }) => {
const [activeStep, setActiveStep] = useState(0);
const [creation, setCreation] = useState({
const [creation, setCreation] = useState<Update>({
customDeviceTypes: '',
destination: '',
file: undefined,
Expand Down Expand Up @@ -190,7 +233,7 @@ export const AddArtifactDialog = ({ onCancel, onUploadStarted, releases, selecte
<DialogTitle>Upload an Artifact</DialogTitle>
<DialogContent className="dialog-content margin-top margin-left margin-right margin-bottom">
{!file ? (
<ArtifactUpload {...commonProps} />
<ArtifactUpload updateCreation={onUpdateCreation} />
) : (
<ComponentToShow {...commonProps} activeStep={activeStep} creation={creation} deviceTypes={deviceTypes} onRemove={onRemove} />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,9 @@ exports[`AddArtifact Component renders correctly 1`] = `
to upload
</div>
</div>
<p
class="warning fadeOut flexbox centered"
/>
</div>
<div
class="MuiDialogActions-root MuiDialogActions-spacing emotion-8"
Expand Down

0 comments on commit d612334

Please sign in to comment.