diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 914c946bd3e..d57d75a003f 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -19,6 +19,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - Interpolation during rendering is now more performance intensive, since the rendering approach was changed. Therefore, interpolation is disabled by default. On the flip side, the rendered quality is often higher than it used to be. [#6748](https://github.com/scalableminds/webknossos/pull/6748) - Updated the styling of the "welcome" screen for new users to be in line with the new branding. [#6904](https://github.com/scalableminds/webknossos/pull/6904) - Improved Terms-of-Service modal (e.g., allow to switch organization even when modal was blocking the remaining usage of WEBKNOSSOS). [#6930](https://github.com/scalableminds/webknossos/pull/6930) +- Uploads are now blocked when the organization’s storage quota is exceeded. [#6893](https://github.com/scalableminds/webknossos/pull/6893) ### Fixed - Fixed an issue with text hints not being visible on the logout page for dark mode users. [#6916](https://github.com/scalableminds/webknossos/pull/6916) diff --git a/app/controllers/WKRemoteDataStoreController.scala b/app/controllers/WKRemoteDataStoreController.scala index c51a95e8169..f259cdc78c7 100644 --- a/app/controllers/WKRemoteDataStoreController.scala +++ b/app/controllers/WKRemoteDataStoreController.scala @@ -60,6 +60,9 @@ class WKRemoteDataStoreController @Inject()( organization <- organizationDAO.findOneByName(uploadInfo.organization)(GlobalAccessContext) ?~> Messages( "organization.notFound", uploadInfo.organization) ~> NOT_FOUND + usedStorageBytes <- organizationDAO.getUsedStorage(organization._id) + _ <- Fox.runOptional(organization.includedStorageBytes)(includedStorage => + bool2Fox(usedStorageBytes <= includedStorage)) ?~> "dataSet.upload.storageExceeded" ~> FORBIDDEN _ <- bool2Fox(organization._id == user._organization) ?~> "notAllowed" ~> FORBIDDEN _ <- dataSetService.assertValidDataSetName(uploadInfo.name) _ <- dataSetService.assertNewDataSetName(uploadInfo.name, organization._id) ?~> "dataSet.name.alreadyTaken" diff --git a/conf/messages b/conf/messages index 5c84737d5f9..254bd2671ea 100644 --- a/conf/messages +++ b/conf/messages @@ -100,6 +100,7 @@ dataSet.upload.validation.failed=Failed to validate Dataset information for uplo dataSet.upload.linkRestricted=Can only link layers of datasets that are either public or allowed to be administrated by your account dataSet.upload.invalidLinkedLayers=Could not link all requested layers dataSet.upload.noFiles=Tried to finish upload with no files. May be a retry of a failed finish request, see previous errors. +dataSet.upload.storageExceeded=Cannot upload dataset because the storage quota of the organization is exceeded. dataSet.explore.failed.readFile=Failed to read remote file dataSet.explore.magDtypeMismatch=Element class must be the same for all mags of a layer. Got {0} diff --git a/frontend/javascripts/admin/dataset/dataset_upload_view.tsx b/frontend/javascripts/admin/dataset/dataset_upload_view.tsx index 1b136d63d2a..ae2fa94de2f 100644 --- a/frontend/javascripts/admin/dataset/dataset_upload_view.tsx +++ b/frontend/javascripts/admin/dataset/dataset_upload_view.tsx @@ -9,9 +9,15 @@ import classnames from "classnames"; import _ from "lodash"; import { useDropzone, FileWithPath } from "react-dropzone"; import ErrorHandling from "libs/error_handling"; -import type { RouteComponentProps } from "react-router-dom"; +import { Link, RouteComponentProps } from "react-router-dom"; import { withRouter } from "react-router-dom"; -import type { APITeam, APIDataStore, APIUser, APIDatasetId } from "types/api_flow_types"; +import type { + APITeam, + APIDataStore, + APIUser, + APIDatasetId, + APIOrganization, +} from "types/api_flow_types"; import type { OxalisState } from "oxalis/store"; import { reserveDatasetUpload, @@ -41,6 +47,8 @@ import { FormInstance } from "antd/lib/form"; import type { Vector3 } from "oxalis/constants"; import { FormItemWithInfo, confirmAsync } from "../../dashboard/dataset/helper_components"; import FolderSelection from "dashboard/folders/folder_selection"; +import { hasPricingPlanExceededStorage } from "admin/organization/pricing_plan_utils"; +import { enforceActiveOrganization } from "oxalis/model/accessors/organization_accessors"; const FormItem = Form.Item; const REPORT_THROTTLE_THRESHOLD = 1 * 60 * 1000; // 1 min @@ -56,6 +64,7 @@ type OwnProps = { }; type StateProps = { activeUser: APIUser | null | undefined; + organization: APIOrganization; }; type Props = OwnProps & StateProps; type PropsWithFormAndRouter = Props & { @@ -613,6 +622,23 @@ class DatasetUploadView extends React.Component { }} > + {hasPricingPlanExceededStorage(this.props.organization) ? ( + + Your organization has exceeded the available storage. Uploading new datasets is + disabled. Visit the{" "} + + organization page + {" "} + for details. + + } + style={{ marginBottom: 8 }} + /> + ) : null} +
{ size="large" type="primary" htmlType="submit" + disabled={hasPricingPlanExceededStorage(this.props.organization)} style={{ width: "100%", }} @@ -1043,6 +1070,7 @@ function FileUploadArea({ const mapStateToProps = (state: OxalisState): StateProps => ({ activeUser: state.activeUser, + organization: enforceActiveOrganization(state.activeOrganization), }); const connector = connect(mapStateToProps);