Merge branch 'error-handling' into 'next'
Improve Error Handling See merge request soapbox-pub/soapbox-fe!1163
This commit is contained in:
commit
bb2729d44b
|
@ -0,0 +1,111 @@
|
||||||
|
import { InstanceRecord } from 'soapbox/normalizers';
|
||||||
|
import rootReducer from 'soapbox/reducers';
|
||||||
|
import { mockStore } from 'soapbox/test_helpers';
|
||||||
|
|
||||||
|
import { uploadCompose } from '../compose';
|
||||||
|
|
||||||
|
describe('uploadCompose()', () => {
|
||||||
|
describe('with images', () => {
|
||||||
|
let files, store;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const instance = InstanceRecord({
|
||||||
|
configuration: {
|
||||||
|
statuses: {
|
||||||
|
max_media_attachments: 4,
|
||||||
|
},
|
||||||
|
media_attachments: {
|
||||||
|
image_size_limit: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = rootReducer(undefined, {})
|
||||||
|
.set('me', '1234')
|
||||||
|
.set('instance', instance);
|
||||||
|
|
||||||
|
store = mockStore(state);
|
||||||
|
files = [{
|
||||||
|
uri: 'image.png',
|
||||||
|
name: 'Image',
|
||||||
|
size: 15,
|
||||||
|
type: 'image/png',
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates an alert if exceeds max size', async() => {
|
||||||
|
const mockIntl = {
|
||||||
|
formatMessage: jest.fn().mockReturnValue('Image exceeds the current file size limit (10 Bytes)'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedActions = [
|
||||||
|
{ type: 'COMPOSE_UPLOAD_REQUEST', skipLoading: true },
|
||||||
|
{
|
||||||
|
type: 'ALERT_SHOW',
|
||||||
|
message: 'Image exceeds the current file size limit (10 Bytes)',
|
||||||
|
actionLabel: undefined,
|
||||||
|
actionLink: undefined,
|
||||||
|
severity: 'error',
|
||||||
|
},
|
||||||
|
{ type: 'COMPOSE_UPLOAD_FAIL', error: true, skipLoading: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
await store.dispatch(uploadCompose(files, mockIntl));
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with videos', () => {
|
||||||
|
let files, store;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const instance = InstanceRecord({
|
||||||
|
configuration: {
|
||||||
|
statuses: {
|
||||||
|
max_media_attachments: 4,
|
||||||
|
},
|
||||||
|
media_attachments: {
|
||||||
|
video_size_limit: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = rootReducer(undefined, {})
|
||||||
|
.set('me', '1234')
|
||||||
|
.set('instance', instance);
|
||||||
|
|
||||||
|
store = mockStore(state);
|
||||||
|
files = [{
|
||||||
|
uri: 'video.mp4',
|
||||||
|
name: 'Video',
|
||||||
|
size: 15,
|
||||||
|
type: 'video/mp4',
|
||||||
|
}];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates an alert if exceeds max size', async() => {
|
||||||
|
const mockIntl = {
|
||||||
|
formatMessage: jest.fn().mockReturnValue('Video exceeds the current file size limit (10 Bytes)'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedActions = [
|
||||||
|
{ type: 'COMPOSE_UPLOAD_REQUEST', skipLoading: true },
|
||||||
|
{
|
||||||
|
type: 'ALERT_SHOW',
|
||||||
|
message: 'Video exceeds the current file size limit (10 Bytes)',
|
||||||
|
actionLabel: undefined,
|
||||||
|
actionLink: undefined,
|
||||||
|
severity: 'error',
|
||||||
|
},
|
||||||
|
{ type: 'COMPOSE_UPLOAD_FAIL', error: true, skipLoading: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
await store.dispatch(uploadCompose(files, mockIntl));
|
||||||
|
const actions = store.getActions();
|
||||||
|
|
||||||
|
expect(actions).toEqual(expectedActions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,5 +1,7 @@
|
||||||
import { defineMessages } from 'react-intl';
|
import { defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
import { httpErrorMessages } from 'soapbox/utils/errors';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
|
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
|
||||||
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
|
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
|
||||||
|
@ -34,7 +36,7 @@ export function showAlert(title = messages.unexpectedTitle, message = messages.u
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showAlertForError(error) {
|
export function showAlertForError(error) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, _getState) => {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
const { data, status, statusText } = error.response;
|
const { data, status, statusText } = error.response;
|
||||||
|
|
||||||
|
@ -48,13 +50,16 @@ export function showAlertForError(error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = statusText;
|
let message = statusText;
|
||||||
const title = `${status}`;
|
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
message = data.error;
|
message = data.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return dispatch(showAlert(title, message, 'error'));
|
if (!message) {
|
||||||
|
message = httpErrorMessages.find((httpError) => httpError.code === status)?.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dispatch(showAlert('', message, 'error'));
|
||||||
} else {
|
} else {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return dispatch(showAlert(undefined, undefined, 'error'));
|
return dispatch(showAlert(undefined, undefined, 'error'));
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { defineMessages } from 'react-intl';
|
||||||
import snackbar from 'soapbox/actions/snackbar';
|
import snackbar from 'soapbox/actions/snackbar';
|
||||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
import { formatBytes } from 'soapbox/utils/media';
|
||||||
|
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
|
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
|
||||||
|
@ -78,10 +79,12 @@ export const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS';
|
||||||
export const COMPOSE_REMOVE_FROM_MENTIONS = 'COMPOSE_REMOVE_FROM_MENTIONS';
|
export const COMPOSE_REMOVE_FROM_MENTIONS = 'COMPOSE_REMOVE_FROM_MENTIONS';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' },
|
||||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
exceededVideoSizeLimit: { id: 'upload_error.video_size_limit', defaultMessage: 'Video exceeds the current file size limit ({limit})' },
|
||||||
scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' },
|
scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' },
|
||||||
success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent' },
|
success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent' },
|
||||||
|
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||||
|
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||||
view: { id: 'snackbar.view', defaultMessage: 'View' },
|
view: { id: 'snackbar.view', defaultMessage: 'View' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -295,10 +298,12 @@ export function submitComposeFail(error) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uploadCompose(files) {
|
export function uploadCompose(files, intl) {
|
||||||
return function(dispatch, getState) {
|
return function(dispatch, getState) {
|
||||||
if (!isLoggedIn(getState)) return;
|
if (!isLoggedIn(getState)) return;
|
||||||
const attachmentLimit = getState().getIn(['instance', 'configuration', 'statuses', 'max_media_attachments']);
|
const attachmentLimit = getState().getIn(['instance', 'configuration', 'statuses', 'max_media_attachments']);
|
||||||
|
const maxImageSize = getState().getIn(['instance', 'configuration', 'media_attachments', 'image_size_limit']);
|
||||||
|
const maxVideoSize = getState().getIn(['instance', 'configuration', 'media_attachments', 'video_size_limit']);
|
||||||
|
|
||||||
const media = getState().getIn(['compose', 'media_attachments']);
|
const media = getState().getIn(['compose', 'media_attachments']);
|
||||||
const progress = new Array(files.length).fill(0);
|
const progress = new Array(files.length).fill(0);
|
||||||
|
@ -314,6 +319,22 @@ export function uploadCompose(files) {
|
||||||
Array.from(files).forEach((f, i) => {
|
Array.from(files).forEach((f, i) => {
|
||||||
if (media.size + i > attachmentLimit - 1) return;
|
if (media.size + i > attachmentLimit - 1) return;
|
||||||
|
|
||||||
|
const isImage = f.type.match(/image.*/);
|
||||||
|
const isVideo = f.type.match(/video.*/);
|
||||||
|
if (isImage && maxImageSize && (f.size > maxImageSize)) {
|
||||||
|
const limit = formatBytes(maxImageSize);
|
||||||
|
const message = intl.formatMessage(messages.exceededImageSizeLimit, { limit });
|
||||||
|
dispatch(snackbar.error(message));
|
||||||
|
dispatch(uploadComposeFail(true));
|
||||||
|
return;
|
||||||
|
} else if (isVideo && maxVideoSize && (f.size > maxVideoSize)) {
|
||||||
|
const limit = formatBytes(maxVideoSize);
|
||||||
|
const message = intl.formatMessage(messages.exceededVideoSizeLimit, { limit });
|
||||||
|
dispatch(snackbar.error(message));
|
||||||
|
dispatch(uploadComposeFail(true));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: Don't define function in loop
|
// FIXME: Don't define function in loop
|
||||||
/* eslint-disable no-loop-func */
|
/* eslint-disable no-loop-func */
|
||||||
resizeImage(f).then(file => {
|
resizeImage(f).then(file => {
|
||||||
|
|
|
@ -203,7 +203,7 @@ class ModalRoot extends React.PureComponent {
|
||||||
<div
|
<div
|
||||||
ref={this.setRef}
|
ref={this.setRef}
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'fixed top-0 left-0 z-1000 w-full h-full overflow-x-hidden overflow-y-auto': true,
|
'fixed top-0 left-0 z-[100] w-full h-full overflow-x-hidden overflow-y-auto': true,
|
||||||
'pointer-events-none': !visible,
|
'pointer-events-none': !visible,
|
||||||
})}
|
})}
|
||||||
style={{ opacity: revealed ? 1 : 0 }}
|
style={{ opacity: revealed ? 1 : 0 }}
|
||||||
|
|
|
@ -39,7 +39,7 @@ const mapStateToProps = state => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
|
||||||
onChange(text) {
|
onChange(text) {
|
||||||
dispatch(changeCompose(text));
|
dispatch(changeCompose(text));
|
||||||
|
@ -66,7 +66,7 @@ const mapDispatchToProps = (dispatch) => ({
|
||||||
},
|
},
|
||||||
|
|
||||||
onPaste(files) {
|
onPaste(files) {
|
||||||
dispatch(uploadCompose(files));
|
dispatch(uploadCompose(files, intl));
|
||||||
},
|
},
|
||||||
|
|
||||||
onPickEmoji(position, data, needsSpace) {
|
onPickEmoji(position, data, needsSpace) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { injectIntl } from 'react-intl';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { uploadCompose } from '../../../actions/compose';
|
import { uploadCompose } from '../../../actions/compose';
|
||||||
|
@ -8,12 +9,12 @@ const mapStateToProps = state => ({
|
||||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
|
||||||
onSelectFile(files) {
|
onSelectFile(files) {
|
||||||
dispatch(uploadCompose(files));
|
dispatch(uploadCompose(files, intl));
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(UploadButton);
|
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(UploadButton));
|
||||||
|
|
|
@ -426,7 +426,7 @@ class UI extends React.PureComponent {
|
||||||
this.dragTargets = [];
|
this.dragTargets = [];
|
||||||
|
|
||||||
if (e.dataTransfer && e.dataTransfer.files.length >= 1) {
|
if (e.dataTransfer && e.dataTransfer.files.length >= 1) {
|
||||||
this.props.dispatch(uploadCompose(e.dataTransfer.files));
|
this.props.dispatch(uploadCompose(e.dataTransfer.files, this.props.intl));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
import camelCase from 'lodash/camelCase';
|
|
||||||
import startCase from 'lodash/startCase';
|
|
||||||
|
|
||||||
const toSentence = (arr) => arr
|
|
||||||
.reduce(
|
|
||||||
(prev, curr, i) => prev + curr + (i === arr.length - 2 ? ' and ' : ', '),
|
|
||||||
'',
|
|
||||||
)
|
|
||||||
.slice(0, -2);
|
|
||||||
|
|
||||||
const buildErrorMessage = (errors) => {
|
|
||||||
const individualErrors = Object.keys(errors).map(
|
|
||||||
(attribute) => `${startCase(camelCase(attribute))} ${toSentence(
|
|
||||||
errors[attribute],
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return toSentence(individualErrors);
|
|
||||||
};
|
|
||||||
|
|
||||||
export { buildErrorMessage };
|
|
|
@ -0,0 +1,203 @@
|
||||||
|
import camelCase from 'lodash/camelCase';
|
||||||
|
import startCase from 'lodash/startCase';
|
||||||
|
|
||||||
|
const toSentence = (arr: string[]) => arr
|
||||||
|
.reduce(
|
||||||
|
(prev, curr, i) => prev + curr + (i === arr.length - 2 ? ' and ' : ', '),
|
||||||
|
'',
|
||||||
|
)
|
||||||
|
.slice(0, -2);
|
||||||
|
|
||||||
|
type Errors = {
|
||||||
|
[key: string]: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildErrorMessage = (errors: Errors) => {
|
||||||
|
const individualErrors = Object.keys(errors).map(
|
||||||
|
(attribute) => `${startCase(camelCase(attribute))} ${toSentence(
|
||||||
|
errors[attribute],
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return toSentence(individualErrors);
|
||||||
|
};
|
||||||
|
|
||||||
|
const httpErrorMessages: { code: number, name: string, description: string }[] = [
|
||||||
|
{
|
||||||
|
code: 100,
|
||||||
|
name: 'Continue',
|
||||||
|
description: 'The server has received the request headers, and the client should proceed to send the request body',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 101,
|
||||||
|
name: 'Switching Protocols',
|
||||||
|
description: 'The requester has asked the server to switch protocols',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 103,
|
||||||
|
name: 'Checkpoint',
|
||||||
|
description: 'Used in the resumable requests proposal to resume aborted PUT or POST requests',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 200,
|
||||||
|
name: 'OK',
|
||||||
|
description: 'The request is OK (this is the standard response for successful HTTP requests)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 201,
|
||||||
|
name: 'Created',
|
||||||
|
description: 'The request has been fulfilled',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 202,
|
||||||
|
name: 'Accepted',
|
||||||
|
description: 'The request has been accepted for processing',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 203,
|
||||||
|
name: 'Non-Authoritative Information',
|
||||||
|
description: 'The request has been successfully processed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 204,
|
||||||
|
name: 'No Content',
|
||||||
|
description: 'The request has been successfully processed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 205,
|
||||||
|
name: 'Reset Content',
|
||||||
|
description: 'The request has been successfully processed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 206,
|
||||||
|
name: 'Partial Content',
|
||||||
|
description: 'The server is delivering only part of the resource due to a range header sent by the client',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 400,
|
||||||
|
name: 'Bad Request',
|
||||||
|
description: 'The request cannot be fulfilled due to bad syntax',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 401,
|
||||||
|
name: 'Unauthorized',
|
||||||
|
description: 'The request was a legal request',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 402,
|
||||||
|
name: 'Payment Required',
|
||||||
|
description: 'Reserved for future use',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 403,
|
||||||
|
name: 'Forbidden',
|
||||||
|
description: 'The request was a legal request',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 404,
|
||||||
|
name: 'Not Found',
|
||||||
|
description: 'The requested page could not be found but may be available again in the future',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 405,
|
||||||
|
name: 'Method Not Allowed',
|
||||||
|
description: 'A request was made of a page using a request method not supported by that page',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 406,
|
||||||
|
name: 'Not Acceptable',
|
||||||
|
description: 'The server can only generate a response that is not accepted by the client',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 407,
|
||||||
|
name: 'Proxy Authentication Required',
|
||||||
|
description: 'The client must first authenticate itself with the proxy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 408,
|
||||||
|
name: 'Request',
|
||||||
|
description: ' Timeout\tThe server timed out waiting for the request',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 409,
|
||||||
|
name: 'Conflict',
|
||||||
|
description: 'The request could not be completed because of a conflict in the request',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 410,
|
||||||
|
name: 'Gone',
|
||||||
|
description: 'The requested page is no longer available',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 411,
|
||||||
|
name: 'Length Required',
|
||||||
|
description: 'The "Content-Length" is not defined. The server will not accept the request without it',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 412,
|
||||||
|
name: 'Precondition',
|
||||||
|
description: ' Failed. The precondition given in the request evaluated to false by the server',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 413,
|
||||||
|
name: 'Request Entity Too Large',
|
||||||
|
description: 'The server will not accept the request',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 414,
|
||||||
|
name: 'Request-URI Too Long',
|
||||||
|
description: 'The server will not accept the request',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 415,
|
||||||
|
name: 'Unsupported Media Type',
|
||||||
|
description: 'The server will not accept the request',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 416,
|
||||||
|
name: 'Requested Range Not Satisfiable',
|
||||||
|
description: 'The client has asked for a portion of the file',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 417,
|
||||||
|
name: 'Expectation Failed',
|
||||||
|
description: 'The server cannot meet the requirements of the Expect request-header field',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 500,
|
||||||
|
name: 'Internal Server Error',
|
||||||
|
description: 'A generic error message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 501,
|
||||||
|
name: 'Not Implemented',
|
||||||
|
description: 'The server either does not recognize the request method',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 502,
|
||||||
|
name: 'Bad Gateway',
|
||||||
|
description: 'The server was acting as a gateway or proxy and received an invalid response from the upstream server',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 503,
|
||||||
|
name: 'Service Unavailable',
|
||||||
|
description: 'The server is currently unavailable (overloaded or down)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 504,
|
||||||
|
name: 'Gateway Timeout',
|
||||||
|
description: 'The server was acting as a gateway or proxy and did not receive a timely response from the upstream server',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 505,
|
||||||
|
name: 'HTTP Version Not Supported',
|
||||||
|
description: 'The server does not support the HTTP protocol version used in the request',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 511,
|
||||||
|
name: 'Network Authentication Required',
|
||||||
|
description: 'The client needs to auth',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export { buildErrorMessage, httpErrorMessages };
|
|
@ -1,10 +0,0 @@
|
||||||
export const truncateFilename = (url, maxLength) => {
|
|
||||||
const filename = url.split('/').pop();
|
|
||||||
|
|
||||||
if (filename.length <= maxLength) return filename;
|
|
||||||
|
|
||||||
return [
|
|
||||||
filename.substr(0, maxLength/2),
|
|
||||||
filename.substr(filename.length - maxLength/2),
|
|
||||||
].join('…');
|
|
||||||
};
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
const truncateFilename = (url: string, maxLength: number) => {
|
||||||
|
const filename = url.split('/').pop();
|
||||||
|
|
||||||
|
if (!filename) {
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filename.length <= maxLength) return filename;
|
||||||
|
|
||||||
|
return [
|
||||||
|
filename.substr(0, maxLength/2),
|
||||||
|
filename.substr(filename.length - maxLength/2),
|
||||||
|
].join('…');
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatBytes = (bytes: number, decimals: number = 2) => {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
export { formatBytes, truncateFilename };
|
|
@ -1,5 +1,5 @@
|
||||||
.notification-list {
|
.notification-list {
|
||||||
@apply w-full flex flex-col items-center space-y-2 sm:items-end;
|
@apply w-full flex flex-col items-center space-y-2 sm:items-end z-[1001] relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-bar {
|
.notification-bar {
|
||||||
|
|
Loading…
Reference in New Issue