feat: Track storage usage

This commit is contained in:
Maksim Eltyshev
2025-08-23 00:03:20 +02:00
parent 2f4bcb0583
commit 4d77a1f596
89 changed files with 1052 additions and 304 deletions

View File

@@ -80,7 +80,7 @@ const Item = React.memo(({ id, isVisible }) => {
break;
default:
if (attachment.data.encoding === Encodings.UTF8) {
if (attachment.data.sizeInBytes <= Config.MAX_SIZE_IN_BYTES_TO_DISPLAY_CONTENT) {
if (attachment.data.size <= Config.MAX_SIZE_TO_DISPLAY_CONTENT) {
content = (
<ContentViewer
src={attachment.data.url}

View File

@@ -0,0 +1,21 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, Message } from 'semantic-ui-react';
const FileIsTooBig = React.memo(() => {
const [t] = useTranslation();
return (
<Message visible negative size="tiny">
<Icon name="file" />
{t('common.uploadFailedFileIsTooBig')}
</Message>
);
});
export default FileIsTooBig;

View File

@@ -0,0 +1,21 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, Message } from 'semantic-ui-react';
const NotEnoughStorage = React.memo(() => {
const [t] = useTranslation();
return (
<Message visible negative size="tiny">
<Icon name="hdd" />
{t('common.uploadFailedNotEnoughStorageSpace')}
</Message>
);
});
export default NotEnoughStorage;

View File

@@ -7,9 +7,13 @@ import React from 'react';
import { Toaster as HotToaster, ToastBar as HotToastBar } from 'react-hot-toast';
import ToastTypes from '../../../constants/ToastTypes';
import FileIsTooBig from './FileIsTooBig';
import NotEnoughStorage from './NotEnoughStorage';
import EmptyTrashToast from './EmptyTrashToast';
const TOAST_BY_TYPE = {
[ToastTypes.FILE_IS_TOO_BIG]: FileIsTooBig,
[ToastTypes.NOT_ENOUGH_STORAGE]: NotEnoughStorage,
[ToastTypes.EMPTY_TRASH]: EmptyTrashToast,
};

View File

@@ -15,7 +15,7 @@ const CARDS_LIMIT = 50;
const COMMENTS_LIMIT = 50;
const ACTIVITIES_LIMIT = 50;
const MAX_SIZE_IN_BYTES_TO_DISPLAY_CONTENT = 256 * 1024;
const MAX_SIZE_TO_DISPLAY_CONTENT = 256 * 1024;
const IS_MAC = navigator.platform.startsWith('Mac');
@@ -28,6 +28,6 @@ export default {
CARDS_LIMIT,
COMMENTS_LIMIT,
ACTIVITIES_LIMIT,
MAX_SIZE_IN_BYTES_TO_DISPLAY_CONTENT,
MAX_SIZE_TO_DISPLAY_CONTENT,
IS_MAC,
};

View File

@@ -3,8 +3,12 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
const FILE_IS_TOO_BIG = 'FILE_IS_TOO_BIG';
const NOT_ENOUGH_STORAGE = 'NOT_ENOUGH_STORAGE';
const EMPTY_TRASH = 'EMPTY_TRASH';
export default {
FILE_IS_TOO_BIG,
NOT_ENOUGH_STORAGE,
EMPTY_TRASH,
};

View File

@@ -280,6 +280,8 @@ export default {
typeNameToConfirm: null,
typeTitleToConfirm: null,
unsavedChanges: null,
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
userActions_title: 'إجراءات المستخدم',

View File

@@ -284,6 +284,8 @@ export default {
typeNameToConfirm: null,
typeTitleToConfirm: null,
unsavedChanges: null,
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
userActions_title: 'Потребителски действия',

View File

@@ -294,6 +294,8 @@ export default {
typeNameToConfirm: 'Zadejte název pro potvrzení.',
typeTitleToConfirm: 'Zadejte titulek pro potvrzení.',
unsavedChanges: 'Neuložené změny',
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Nahrané obrázky',
url: null,
userActions_title: 'Akce uživatele',

View File

@@ -301,6 +301,8 @@ export default {
typeNameToConfirm: 'Skriv navnet for at bekræfte.',
typeTitleToConfirm: 'Skriv overskriften for at bekræfte.',
unsavedChanges: 'Ikke-gemte ændringer',
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Uploadede billeder',
url: null,
userActions_title: 'Brugerhandlinger',

View File

@@ -309,6 +309,8 @@ export default {
typeNameToConfirm: 'Namen zur Bestätigung eingeben.',
typeTitleToConfirm: 'Titel zur Bestätigung eingeben.',
unsavedChanges: 'Ungespeicherte Änderungen',
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Hochgeladene Bilder',
url: null,
userActions_title: 'Benutzeraktionen',

View File

@@ -311,6 +311,8 @@ export default {
typeNameToConfirm: 'Πληκτρολογήστε το όνομα για επιβεβαίωση.',
typeTitleToConfirm: 'Πληκτρολογήστε τον τίτλο για επιβεβαίωση.',
unsavedChanges: 'Μη αποθηκευμένες αλλαγές',
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Μεταφορτωμένες εικόνες',
url: null,
userActions_title: 'Ενέργειες χρήστη',

View File

@@ -301,6 +301,8 @@ export default {
typeNameToConfirm: 'Type the name to confirm.',
typeTitleToConfirm: 'Type the title to confirm.',
unsavedChanges: 'Unsaved changes',
uploadFailedFileIsTooBig: 'Upload failed: File is too big.',
uploadFailedNotEnoughStorageSpace: 'Upload failed: Not enough storage space.',
uploadedImages: 'Uploaded images',
url: 'URL',
userActions_title: 'User Actions',

View File

@@ -296,6 +296,8 @@ export default {
typeNameToConfirm: 'Type the name to confirm.',
typeTitleToConfirm: 'Type the title to confirm.',
unsavedChanges: 'Unsaved changes',
uploadFailedFileIsTooBig: 'Upload failed: File is too big.',
uploadFailedNotEnoughStorageSpace: 'Upload failed: Not enough storage space.',
uploadedImages: 'Uploaded images',
url: 'URL',
userActions_title: 'User Actions',

View File

@@ -301,6 +301,8 @@ export default {
typeNameToConfirm: 'Escribe el nombre para confirmar',
typeTitleToConfirm: 'Escribe el título para confirmar',
unsavedChanges: 'Cambios no guardados',
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Imágenes subidas',
url: null,
userActions_title: 'Acciones de Usuario',

View File

@@ -299,6 +299,8 @@ export default {
typeNameToConfirm: 'Sisestage nimi, et kinnitada.',
typeTitleToConfirm: 'Sisestage pealkiri, et kinnitada.',
unsavedChanges: 'Muudetud andmed',
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Laaditud pildid',
url: null,
userActions_title: 'Kasutaja tegevused',

View File

@@ -281,6 +281,8 @@ export default {
typeNameToConfirm: null,
typeTitleToConfirm: null,
unsavedChanges: null,
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
userActions_title: 'اقدامات کاربر',

View File

@@ -295,6 +295,8 @@ export default {
typeNameToConfirm: 'Kirjoita nimi vahvistaaksesi.',
typeTitleToConfirm: 'Kirjoita otsikko vahvistaaksesi.',
unsavedChanges: 'Tallentamattomat muutokset',
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Ladatut kuvat',
url: null,
userActions_title: 'Käyttäjän toiminnot',

View File

@@ -304,6 +304,8 @@ export default {
typeNameToConfirm: 'Saissir le nom pour confirmer.',
typeTitleToConfirm: 'Saisir le titre pour confirmer.',
unsavedChanges: 'Modifications non enregistrées',
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Images téléchargées',
url: 'URL',
userActions_title: "Actions de l'utilisateur",

View File

@@ -293,6 +293,8 @@ export default {
typeNameToConfirm: 'Írja be a nevet a megerősítéshez.',
typeTitleToConfirm: 'Írja be a címet a megerősítéshez.',
unsavedChanges: 'Mentetlen változtatások',
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Feltöltött képek',
url: 'URL',
userActions_title: 'Felhasználói műveletek',

View File

@@ -284,6 +284,8 @@ export default {
typeNameToConfirm: null,
typeTitleToConfirm: null,
unsavedChanges: null,
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
userActions_title: 'Aksi Pengguna',

View File

@@ -302,6 +302,8 @@ export default {
typeNameToConfirm: 'Digita il nome per confermare',
typeTitleToConfirm: 'Digita il titolo per confermare',
unsavedChanges: 'Modifiche non salvate',
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Immagini caricate',
url: 'URL',
userActions_title: 'Azioni utente',

View File

@@ -283,6 +283,8 @@ export default {
typeNameToConfirm: null,
typeTitleToConfirm: null,
unsavedChanges: null,
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
userActions_title: 'ユーザーのアクション',

View File

@@ -282,6 +282,8 @@ export default {
typeNameToConfirm: null,
typeTitleToConfirm: null,
unsavedChanges: null,
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
userActions_title: '사용자 작업',

View File

@@ -284,6 +284,8 @@ export default {
typeNameToConfirm: null,
typeTitleToConfirm: null,
unsavedChanges: null,
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
userActions_title: 'Gebruikersacties',

View File

@@ -291,6 +291,8 @@ export default {
typeNameToConfirm: 'Wpisz nazwę aby potwierdzić.',
typeTitleToConfirm: 'Wpisz tytuł aby potwierdzić.',
unsavedChanges: 'Niezapisane zmiany',
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Wgrane obrazy',
url: null,
userActions_title: 'Akcje Użytkownika',

View File

@@ -303,6 +303,8 @@ export default {
typeNameToConfirm: 'Digite o nome para confirmar.',
typeTitleToConfirm: 'Digite o título para confirmar.',
unsavedChanges: 'Alterações não salvas',
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Imagens enviadas',
url: 'URL',
userActions_title: 'Ações do Usuário',

View File

@@ -285,6 +285,8 @@ export default {
typeNameToConfirm: null,
typeTitleToConfirm: null,
unsavedChanges: null,
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
userActions_title: 'Ações do Utilizador',

View File

@@ -284,6 +284,8 @@ export default {
typeNameToConfirm: null,
typeTitleToConfirm: null,
unsavedChanges: null,
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
userActions_title: 'Acțiunile utilizatorului',

View File

@@ -298,6 +298,8 @@ export default {
typeNameToConfirm: 'Введите имя для подтверждения',
typeTitleToConfirm: 'Введите название для подтверждения',
unsavedChanges: 'Несохранённые изменения',
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Загруженные изображения',
url: null,
userActions_title: 'Действия с пользователем',

View File

@@ -283,6 +283,8 @@ export default {
typeNameToConfirm: null,
typeTitleToConfirm: null,
unsavedChanges: null,
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
userActions_title: 'Akcie na používateľovi',

View File

@@ -283,6 +283,8 @@ export default {
typeNameToConfirm: null,
typeTitleToConfirm: null,
unsavedChanges: null,
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
userActions_title: 'Корисничке радње',

View File

@@ -280,6 +280,8 @@ export default {
typeNameToConfirm: null,
typeTitleToConfirm: null,
unsavedChanges: null,
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
userActions_title: 'Korisničke radnje',

View File

@@ -282,6 +282,8 @@ export default {
typeNameToConfirm: null,
typeTitleToConfirm: null,
unsavedChanges: null,
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
userActions_title: 'Användaråtgärder',

View File

@@ -280,6 +280,8 @@ export default {
typeNameToConfirm: null,
typeTitleToConfirm: null,
unsavedChanges: null,
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
userActions_title: 'Kullanıcı İşlemleri',

View File

@@ -297,6 +297,8 @@ export default {
typeNameToConfirm: "Введіть ім'я для підтвердження.",
typeTitleToConfirm: 'Введіть назву, щоб підтвердити.',
unsavedChanges: 'Незбережені зміни',
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: 'Завантажені зображення',
url: 'Посилання',
userActions_title: 'Дії користувача',

View File

@@ -279,6 +279,8 @@ export default {
typeNameToConfirm: null,
typeTitleToConfirm: null,
unsavedChanges: null,
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
userActions_title: 'Foydalanuvchi Amallari',

View File

@@ -281,6 +281,8 @@ export default {
typeNameToConfirm: '输入名称以确认',
typeTitleToConfirm: '输入标题以确认',
unsavedChanges: '未保存的更改',
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: '已上传图片',
url: '网址',
userActions_title: '用户操作',

View File

@@ -277,6 +277,8 @@ export default {
typeNameToConfirm: null,
typeTitleToConfirm: null,
unsavedChanges: null,
uploadFailedFileIsTooBig: null,
uploadFailedNotEnoughStorageSpace: null,
uploadedImages: null,
url: null,
userActions_title: '使用者操作',

View File

@@ -6,6 +6,7 @@
import omit from 'lodash/omit';
import truncate from 'lodash/truncate';
import { call, put, select } from 'redux-saga/effects';
import toast from 'react-hot-toast';
import request from '../request';
import selectors from '../../../selectors';
@@ -13,6 +14,7 @@ import actions from '../../../actions';
import api from '../../../api';
import { createLocalId } from '../../../utils/local-id';
import { AttachmentTypes } from '../../../constants/Enums';
import ToastTypes from '../../../constants/ToastTypes';
export function* createAttachment(cardId, data) {
const localId = yield call(createLocalId);
@@ -41,6 +43,22 @@ export function* createAttachment(cardId, data) {
: call(request, api.createAttachment, cardId, nextData));
} catch (error) {
yield put(actions.createAttachment.failure(localId, error));
if (error.code === 'E_UNPROCESSABLE_ENTITY') {
let toastType;
if (error.message.startsWith('Upload limit')) {
toastType = ToastTypes.FILE_IS_TOO_BIG;
} else if (error.message === 'Storage limit reached') {
toastType = ToastTypes.NOT_ENOUGH_STORAGE;
}
if (toastType) {
yield call(toast, {
type: toastType,
});
}
}
return;
}