Enable Bucket Quota on Bucket Details (#776)

* Enable Bucket Quota on Bucket Details

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>

* warnings

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>

* remove uselss br

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
Daniel Valdivia
2021-05-28 17:21:45 -07:00
committed by GitHub
parent 16647b88e9
commit c95bc64dbe
10 changed files with 550 additions and 12 deletions

120
models/bucket_quota.go Normal file
View File

@@ -0,0 +1,120 @@
// Code generated by go-swagger; DO NOT EDIT.
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"encoding/json"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// BucketQuota bucket quota
//
// swagger:model bucketQuota
type BucketQuota struct {
// quota
Quota int64 `json:"quota,omitempty"`
// type
// Enum: [hard fifo]
Type string `json:"type,omitempty"`
}
// Validate validates this bucket quota
func (m *BucketQuota) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateType(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
var bucketQuotaTypeTypePropEnum []interface{}
func init() {
var res []string
if err := json.Unmarshal([]byte(`["hard","fifo"]`), &res); err != nil {
panic(err)
}
for _, v := range res {
bucketQuotaTypeTypePropEnum = append(bucketQuotaTypeTypePropEnum, v)
}
}
const (
// BucketQuotaTypeHard captures enum value "hard"
BucketQuotaTypeHard string = "hard"
// BucketQuotaTypeFifo captures enum value "fifo"
BucketQuotaTypeFifo string = "fifo"
)
// prop value enum
func (m *BucketQuota) validateTypeEnum(path, location string, value string) error {
if err := validate.EnumCase(path, location, value, bucketQuotaTypeTypePropEnum, true); err != nil {
return err
}
return nil
}
func (m *BucketQuota) validateType(formats strfmt.Registry) error {
if swag.IsZero(m.Type) { // not required
return nil
}
// value enum
if err := m.validateTypeEnum("type", "body", m.Type); err != nil {
return err
}
return nil
}
// MarshalBinary interface implementation
func (m *BucketQuota) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *BucketQuota) UnmarshalBinary(b []byte) error {
var res BucketQuota
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -28,6 +28,7 @@ import {
BucketEncryptionInfo,
BucketInfo,
BucketObjectLocking,
BucketQuota,
BucketReplication,
BucketVersioning,
} from "../types";
@@ -43,6 +44,8 @@ import SetRetentionConfig from "./SetRetentionConfig";
import EnableBucketEncryption from "./EnableBucketEncryption";
import EnableVersioningModal from "./EnableVersioningModal";
import UsageIcon from "../../../../icons/UsageIcon";
import GavelIcon from "@material-ui/icons/Gavel";
import EnableQuota from "./EnableQuota";
interface IBucketSummaryProps {
classes: any;
@@ -71,6 +74,12 @@ const styles = (theme: Theme) =>
reportedUsage: {
padding: "15px",
},
dualCardLeft: {
paddingRight: "5px",
},
dualCardRight: {
paddingLeft: "5px",
},
...hrClass,
...buttonsStyles,
});
@@ -93,13 +102,18 @@ const BucketSummary = ({
const [loadingBucket, setLoadingBucket] = useState<boolean>(true);
const [loadingEncryption, setLoadingEncryption] = useState<boolean>(true);
const [loadingVersioning, setLoadingVersioning] = useState<boolean>(true);
const [loadingQuota, setLoadingQuota] = useState<boolean>(true);
const [loadingReplication, setLoadingReplication] = useState<boolean>(true);
const [isVersioned, setIsVersioned] = useState<boolean>(false);
const [quotaEnabled, setQuotaEnabled] = useState<boolean>(false);
const [quota, setQuota] = useState<BucketQuota | null>(null);
const [encryptionEnabled, setEncryptionEnabled] = useState<boolean>(false);
const [retentionConfigOpen, setRetentionConfigOpen] =
useState<boolean>(false);
const [enableEncryptionScreenOpen, setEnableEncryptionScreenOpen] =
useState<boolean>(false);
const [enableQuotaScreenOpen, setEnableQuotaScreenOpen] =
useState<boolean>(false);
const [enableVersioningOpen, setEnableVersioningOpen] =
useState<boolean>(false);
@@ -166,6 +180,27 @@ const BucketSummary = ({
}
}, [loadingVersioning, setErrorSnackMessage, bucketName]);
useEffect(() => {
if (loadingQuota) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/quota`)
.then((res: BucketQuota) => {
setQuota(res);
if (res.quota) {
setQuotaEnabled(true);
} else {
setQuotaEnabled(false);
}
setLoadingQuota(false);
})
.catch((err: any) => {
setErrorSnackMessage(err);
setQuotaEnabled(false);
setLoadingVersioning(false);
});
}
}, [loadingQuota, setLoadingVersioning, setErrorSnackMessage, bucketName]);
useEffect(() => {
if (loadingVersioning) {
api
@@ -234,10 +269,17 @@ const BucketSummary = ({
const setBucketVersioning = () => {
setEnableVersioningOpen(true);
};
const setBucketQuota = () => {
setEnableQuotaScreenOpen(true);
};
const closeEnableBucketEncryption = () => {
setEnableEncryptionScreenOpen(false);
loadAllBucketData();
setLoadingEncryption(true);
};
const closeEnableBucketQuota = () => {
setEnableQuotaScreenOpen(false);
setLoadingQuota(true);
};
const closeSetAccessPolicy = () => {
@@ -257,6 +299,13 @@ const BucketSummary = ({
}
};
const cap = (str: string) => {
if (!str) {
return null;
}
return str[0].toUpperCase() + str.slice(1);
};
return (
<Fragment>
{enableEncryptionScreenOpen && (
@@ -268,6 +317,15 @@ const BucketSummary = ({
closeModalAndRefresh={closeEnableBucketEncryption}
/>
)}
{enableQuotaScreenOpen && (
<EnableQuota
open={enableQuotaScreenOpen}
selectedBucket={bucketName}
enabled={quotaEnabled}
cfg={quota}
closeModalAndRefresh={closeEnableBucketQuota}
/>
)}
{accessPolicyScreenOpen && (
<SetAccessPolicy
bucketName={bucketName}
@@ -381,10 +439,10 @@ const BucketSummary = ({
<br />
<Paper className={classes.paperContainer}>
<Grid container>
<Grid item xs={12}>
<Grid item xs={quotaEnabled ? 9 : 12}>
<h2>Versioning</h2>
<hr className={classes.hrClass} />
<table>
<table width={"100%"}>
<tbody>
<tr>
<td className={classes.titleCol}>Versioning:</td>
@@ -407,10 +465,47 @@ const BucketSummary = ({
</Fragment>
)}
</td>
<td className={classes.titleCol}>Quota:</td>
<td>
{loadingQuota ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
<Fragment>
<Button
color="primary"
className={classes.anchorButton}
onClick={setBucketQuota}
>
{quotaEnabled ? "Enabled" : "Disabled"}
</Button>
</Fragment>
)}
</td>
</tr>
</tbody>
</table>
</Grid>
{quotaEnabled && quota && (
<Grid item xs={3} className={classes.reportedUsage}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon} xs={2}>
<GavelIcon />
</Grid>
<Grid item xs={10}>
<Typography className={classes.elementTitle}>
{cap(quota?.type)} Quota
</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>
{niceBytes(`${quota?.quota}`)}
</Typography>
</Grid>
)}
</Grid>
</Paper>
<br />

View File

@@ -0,0 +1,237 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import Grid from "@material-ui/core/Grid";
import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import FormSwitchWrapper from "../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import { factorForDropdown, getBytes, units } from "../../../../common/utils";
import { BucketQuota } from "../types";
import { setModalErrorSnackMessage } from "../../../../actions";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import api from "../../../../common/api";
const styles = (theme: Theme) =>
createStyles({
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold",
},
},
},
buttonContainer: {
textAlign: "right",
},
multiContainer: {
display: "flex",
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
...modalBasic,
});
interface IEnableQuotaProps {
classes: any;
open: boolean;
enabled: boolean;
cfg: BucketQuota | null;
selectedBucket: string;
closeModalAndRefresh: () => void;
setModalErrorSnackMessage: typeof setModalErrorSnackMessage;
}
const EnableQuota = ({
classes,
open,
enabled,
cfg,
selectedBucket,
closeModalAndRefresh,
setModalErrorSnackMessage,
}: IEnableQuotaProps) => {
const [loading, setLoading] = useState<boolean>(false);
const [quotaEnabled, setQuotaEnabled] = useState<boolean>(false);
const [quotaType, setQuotaType] = useState<string>("hard");
const [quotaSize, setQuotaSize] = useState<string>("1");
const [quotaUnit, setQuotaUnit] = useState<string>("TiB");
useEffect(() => {
if (enabled) {
setQuotaEnabled(true);
if (cfg) {
setQuotaType(cfg.type);
setQuotaSize(`${cfg.quota}`);
setQuotaUnit(`B`);
let maxUnit = "B";
let maxQuota = cfg.quota;
for (let i = 0; i < units.length; i++) {
if (cfg.quota % Math.pow(1024, i) === 0) {
maxQuota = cfg.quota / Math.pow(1024, i);
maxUnit = units[i];
} else {
break;
}
}
setQuotaSize(`${maxQuota}`);
setQuotaUnit(maxUnit);
}
}
}, [enabled, cfg]);
const enableBucketEncryption = (event: React.FormEvent) => {
event.preventDefault();
if (loading) {
return;
}
let req = {
enabled: quotaEnabled,
amount: parseInt(getBytes(quotaSize, quotaUnit, false)),
quota_type: quotaType,
};
api
.invoke("PUT", `/api/v1/buckets/${selectedBucket}/quota`, req)
.then(() => {
setLoading(false);
closeModalAndRefresh();
})
.catch((err: any) => {
setLoading(false);
setModalErrorSnackMessage(err);
});
};
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModalAndRefresh();
}}
title="Enable Bucket Quota"
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
enableBucketEncryption(e);
}}
>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
<Grid item xs={12}>
<FormSwitchWrapper
value="bucket_quota"
id="bucket_quota"
name="bucket_quota"
checked={quotaEnabled}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setQuotaEnabled(event.target.checked);
}}
label={"Quota"}
indicatorLabels={["On", "Off"]}
/>
</Grid>
{quotaEnabled && (
<React.Fragment>
<Grid item xs={12}>
<RadioGroupSelector
currentSelection={quotaType}
id="quota_type"
name="quota_type"
label="Quota Type"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setQuotaType(e.target.value as string);
}}
selectorOptions={[
{ value: "hard", label: "Hard" },
{ value: "fifo", label: "FIFO" },
]}
/>
</Grid>
<Grid item xs={12}>
<div className={classes.multiContainer}>
<div className={classes.quotaSizeContainer}>
<InputBoxWrapper
type="number"
id="quota_size"
name="quota_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setQuotaSize(e.target.value);
}}
label="Quota"
value={quotaSize}
required
min="1"
/>
</div>
<div className={classes.sizeFactorContainer}>
<SelectWrapper
label="&nbsp;"
id="quota_unit"
name="quota_unit"
value={quotaUnit}
onChange={(
e: React.ChangeEvent<{ value: unknown }>
) => {
setQuotaUnit(e.target.value as string);
}}
options={factorForDropdown()}
/>
</div>
</div>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<br />
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={loading}
>
Save
</Button>
</Grid>
{loading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
};
const connector = connect(null, {
setModalErrorSnackMessage,
});
export default withStyles(styles)(connector(EnableQuota));

View File

@@ -86,6 +86,11 @@ export interface BucketReplication {
rules: BucketReplicationRule[];
}
export interface BucketQuota {
quota: number;
type: string;
}
export interface QuotaRequest {
enabled: boolean;
quota_type: string;

View File

@@ -206,9 +206,6 @@ const ListNotificationEndpoints = ({
Add Notification Target
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={[]}

View File

@@ -311,6 +311,10 @@ func (ac adminClient) setBucketQuota(ctx context.Context, bucket string, quota *
return ac.client.SetBucketQuota(ctx, bucket, quota)
}
func (ac adminClient) getBucketQuota(ctx context.Context, bucket string) (madmin.BucketQuota, error) {
return ac.client.GetBucketQuota(ctx, bucket)
}
// serverHealthInfo implements mc.ServerHealthInfo - Connect to a minio server and call Health Info Management API
func (ac adminClient) serverHealthInfo(ctx context.Context, healthDataTypes []madmin.HealthDataType, deadline time.Duration) <-chan madmin.HealthInfo {
return ac.client.ServerHealthInfo(ctx, healthDataTypes, deadline)

View File

@@ -1586,7 +1586,7 @@ func init() {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/listObjectsResponse"
"$ref": "#/definitions/bucketQuota"
}
},
"default": {
@@ -4264,6 +4264,21 @@ func init() {
}
}
},
"bucketQuota": {
"type": "object",
"properties": {
"quota": {
"type": "integer"
},
"type": {
"type": "string",
"enum": [
"hard",
"fifo"
]
}
}
},
"bucketReplicationDestination": {
"type": "object",
"properties": {
@@ -8714,7 +8729,7 @@ func init() {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/listObjectsResponse"
"$ref": "#/definitions/bucketQuota"
}
},
"default": {
@@ -12032,6 +12047,21 @@ func init() {
}
}
},
"bucketQuota": {
"type": "object",
"properties": {
"quota": {
"type": "integer"
},
"type": {
"type": "string",
"enum": [
"hard",
"fifo"
]
}
}
},
"bucketReplicationDestination": {
"type": "object",
"properties": {

View File

@@ -42,7 +42,7 @@ type GetBucketQuotaOK struct {
/*
In: Body
*/
Payload *models.ListObjectsResponse `json:"body,omitempty"`
Payload *models.BucketQuota `json:"body,omitempty"`
}
// NewGetBucketQuotaOK creates GetBucketQuotaOK with default headers values
@@ -52,13 +52,13 @@ func NewGetBucketQuotaOK() *GetBucketQuotaOK {
}
// WithPayload adds the payload to the get bucket quota o k response
func (o *GetBucketQuotaOK) WithPayload(payload *models.ListObjectsResponse) *GetBucketQuotaOK {
func (o *GetBucketQuotaOK) WithPayload(payload *models.BucketQuota) *GetBucketQuotaOK {
o.Payload = payload
return o
}
// SetPayload sets the payload to the get bucket quota o k response
func (o *GetBucketQuotaOK) SetPayload(payload *models.ListObjectsResponse) {
func (o *GetBucketQuotaOK) SetPayload(payload *models.BucketQuota) {
o.Payload = payload
}

View File

@@ -40,6 +40,15 @@ func registerBucketQuotaHandlers(api *operations.ConsoleAPI) {
}
return user_api.NewSetBucketQuotaOK()
})
// get bucket quota
api.UserAPIGetBucketQuotaHandler = user_api.GetBucketQuotaHandlerFunc(func(params user_api.GetBucketQuotaParams, session *models.Principal) middleware.Responder {
resp, err := getBucketQuotaResponse(session, params)
if err != nil {
return user_api.NewGetBucketQuotaDefault(int(err.Code)).WithPayload(err)
}
return user_api.NewGetBucketQuotaOK().WithPayload(resp)
})
}
func setBucketQuotaResponse(session *models.Principal, params user_api.SetBucketQuotaParams) *models.Error {
@@ -85,3 +94,34 @@ func setBucketQuota(ctx context.Context, ac *adminClient, bucket *string, bucket
}
return nil
}
func getBucketQuotaResponse(session *models.Principal, params user_api.GetBucketQuotaParams) (*models.BucketQuota, *models.Error) {
mAdmin, err := newMAdminClient(session)
if err != nil {
return nil, prepareError(err)
}
// create a minioClient interface implementation
// defining the client to be used
adminClient := adminClient{client: mAdmin}
quota, err := getBucketQuota(params.HTTPRequest.Context(), &adminClient, &params.Name)
if err != nil {
return nil, &models.Error{
Code: 500,
Message: swag.String(err.Error()),
}
}
return quota, nil
}
func getBucketQuota(ctx context.Context, ac *adminClient, bucket *string) (*models.BucketQuota, error) {
quota, err := ac.getBucketQuota(ctx, *bucket)
if err != nil {
return nil, err
}
return &models.BucketQuota{
Quota: int64(quota.Quota),
Type: string(quota.Type),
}, nil
}

View File

@@ -597,7 +597,7 @@ paths:
200:
description: A successful response.
schema:
$ref: "#/definitions/listObjectsResponse"
$ref: "#/definitions/bucketQuota"
default:
description: Generic error response.
schema:
@@ -3192,6 +3192,16 @@ definitions:
properties:
access:
$ref: "#/definitions/bucketAccess"
bucketQuota:
type: object
properties:
quota:
type: integer
type:
type: string
enum:
- hard
- fifo
setBucketQuota:
type: object
required: