Added decimal support to bucket quota selectors (#2126)

- Fixed an issue with calculateBytes function
- Fixed add bucket validation form

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2022-06-15 18:35:47 -05:00
committed by GitHub
parent a5c14790b3
commit 7ecc1022b2
14 changed files with 234 additions and 220 deletions

View File

@@ -596,7 +596,7 @@ export const calculateBytes = (
// Get unit for measure
const i = Math.floor(Math.log(bytes) / Math.log(k));
const fractionDigits = showDecimals ? 0 : 1;
const fractionDigits = showDecimals ? 1 : 0;
const bytesUnit = bytes / Math.pow(k, i);

View File

@@ -1,5 +1,3 @@
// check if we are using base path, if not this always is `/`
const baseLocation = new URL(document.baseURI);
export const baseUrl = baseLocation.pathname;

View File

@@ -70,21 +70,34 @@ const EnableQuota = ({
const [quotaEnabled, setQuotaEnabled] = useState<boolean>(false);
const [quotaSize, setQuotaSize] = useState<string>("1");
const [quotaUnit, setQuotaUnit] = useState<string>("Ti");
const [validInput, setValidInput] = useState<boolean>(false);
useEffect(() => {
if (enabled) {
setQuotaEnabled(true);
if (cfg) {
const unitCalc = calculateBytes(cfg.quota, false, false, true);
const unitCalc = calculateBytes(cfg.quota, true, false, true);
setQuotaSize(unitCalc.total.toString());
setQuotaUnit(unitCalc.unit);
setValidInput(true);
}
}
}, [enabled, cfg]);
useEffect(() => {
const valRegExp = /^\d*(?:\.\d{1,2})?$/;
if (!quotaEnabled) {
setValidInput(true);
return;
}
setValidInput(valRegExp.test(quotaSize));
}, [quotaEnabled, quotaSize]);
const enableBucketEncryption = () => {
if (loading) {
if (loading || !validInput) {
return;
}
let req = {
@@ -145,11 +158,13 @@ const EnableQuota = ({
id="quota_size"
name="quota_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.validity.valid) {
setQuotaSize(e.target.value);
setQuotaSize(e.target.value);
if (!e.target.validity.valid) {
setValidInput(false);
} else {
setValidInput(true);
}
}}
pattern={"[0-9]*"}
label="Quota"
value={quotaSize}
required
@@ -165,6 +180,7 @@ const EnableQuota = ({
disabled={false}
/>
}
error={!validInput ? "Please enter a valid quota" : ""}
/>
</Grid>
</Grid>
@@ -189,7 +205,7 @@ const EnableQuota = ({
type="submit"
variant="contained"
color="primary"
disabled={loading}
disabled={loading || !validInput}
>
Save
</Button>

View File

@@ -130,7 +130,7 @@ const AddBucket = ({ classes }: IsetProps) => {
(state: AppState) => state.addBucket.retentionValidity
);
const addLoading = useSelector((state: AppState) => state.addBucket.loading);
const valid = useSelector((state: AppState) => state.addBucket.valid);
const invalidFields = useSelector((state: AppState) => state.addBucket.invalidFields);
const lockingFieldDisabled = useSelector(
(state: AppState) => state.addBucket.lockingFieldDisabled
);
@@ -285,19 +285,16 @@ const AddBucket = ({ classes }: IsetProps) => {
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
type="number"
type="string"
id="quota_size"
name="quota_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.validity.valid) {
dispatch(setQuotaSize(e.target.value));
}
}}
label="Capacity"
value={quotaSize}
required
min="1"
pattern={"[0-9]*"}
overlayObject={
<InputUnitMenu
id={"quota_unit"}
@@ -309,6 +306,7 @@ const AddBucket = ({ classes }: IsetProps) => {
disabled={false}
/>
}
error={invalidFields.includes("quotaSize") ? "Please enter a valid quota" : ""}
/>
</Grid>
</React.Fragment>
@@ -387,7 +385,7 @@ const AddBucket = ({ classes }: IsetProps) => {
type="submit"
variant="contained"
color="primary"
disabled={addLoading || valid}
disabled={addLoading || invalidFields.length > 0}
>
Create Bucket
</Button>

View File

@@ -19,7 +19,7 @@ import { addBucketAsync } from "./addBucketThunks";
export interface AddBucketState {
loading: boolean;
valid: boolean;
invalidFields: string[];
name: string;
versioningEnabled: boolean;
lockingEnabled: boolean;
@@ -36,7 +36,7 @@ export interface AddBucketState {
const initialState: AddBucketState = {
loading: false,
valid: false,
invalidFields: ["name"],
name: "",
versioningEnabled: false,
lockingEnabled: false,
@@ -57,8 +57,11 @@ export const addBucketsSlice = createSlice({
reducers: {
setName: (state, action: PayloadAction<string>) => {
state.name = action.payload;
if (state.name.trim() === "") {
state.valid = false;
state.invalidFields = [...state.invalidFields, "name"];
} else {
state.invalidFields = state.invalidFields.filter((field) => field !== "name");
}
},
setVersioning: (state, action: PayloadAction<boolean>) => {
@@ -75,13 +78,22 @@ export const addBucketsSlice = createSlice({
},
setQuota: (state, action: PayloadAction<boolean>) => {
state.quotaEnabled = action.payload;
if(!action.payload) {
state.quotaSize = "1";
state.quotaUnit = "Ti";
state.invalidFields = state.invalidFields.filter((field) => field !== "quotaSize");
}
},
setQuotaSize: (state, action: PayloadAction<string>) => {
state.quotaSize = action.payload;
if (state.quotaEnabled && state.valid) {
if (state.quotaSize.trim() === "" || parseInt(state.quotaSize) === 0) {
state.valid = false;
if (state.quotaEnabled) {
if (state.quotaSize.trim() === "" || parseInt(state.quotaSize) === 0 || !(/^\d*(?:\.\d{1,2})?$/.test(state.quotaSize))) {
state.invalidFields = [...state.invalidFields, "quotaSize"];
} else {
state.invalidFields = state.invalidFields.filter((field) => field !== "quotaSize");
}
}
},
@@ -109,7 +121,9 @@ export const addBucketsSlice = createSlice({
state.retentionEnabled &&
(Number.isNaN(state.retentionValidity) || state.retentionValidity < 1)
) {
state.valid = false;
state.invalidFields = [...state.invalidFields, "retentionValidity"];
} else {
state.invalidFields = state.invalidFields.filter((field) => field !== "retentionValidity");
}
},
setRetentionMode: (state, action: PayloadAction<string>) => {
@@ -121,10 +135,12 @@ export const addBucketsSlice = createSlice({
setRetentionValidity: (state, action: PayloadAction<number>) => {
state.retentionValidity = action.payload;
if (
state.retentionEnabled &&
(Number.isNaN(state.retentionValidity) || state.retentionValidity < 1)
state.retentionEnabled &&
(Number.isNaN(state.retentionValidity) || state.retentionValidity < 1)
) {
state.valid = false;
state.invalidFields = [...state.invalidFields, "retentionValidity"];
} else {
state.invalidFields = state.invalidFields.filter((field) => field !== "retentionValidity");
}
},

View File

@@ -177,7 +177,7 @@ const BucketListItem = ({
const usageUnit = usage.split(" ")[1];
const quota = get(bucket, "details.quota.quota", "0");
const quotaForString = calculateBytes(quota);
const quotaForString = calculateBytes(quota, true, false);
const accessToStr = (bucket: Bucket): string => {
if (bucket.rw_access?.read && !bucket.rw_access?.write) {

View File

@@ -78,9 +78,7 @@ interface IAddNotificationEndpointProps {
classes: any;
}
const AddTierConfiguration = ({
classes,
}: IAddNotificationEndpointProps) => {
const AddTierConfiguration = ({ classes }: IAddNotificationEndpointProps) => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const params = useParams();

View File

@@ -85,11 +85,13 @@ const PolicySelectors = ({
const [loading, isLoading] = useState<boolean>(false);
const [filter, setFilter] = useState<string>("");
const currentPolicies = useSelector((state: AppState) => state.createUser.selectedPolicies);
const currentPolicies = useSelector(
(state: AppState) => state.createUser.selectedPolicies
);
const fetchPolicies = useCallback(() => {
isLoading(true);
api
.invoke("GET", `/api/v1/policies?limit=1000`)
.then((res: PolicyList) => {
@@ -115,11 +117,10 @@ const PolicySelectors = ({
}, [loading, fetchPolicies]);
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetD = e.target;
const value = targetD.value;
const checked = targetD.checked;
let elements: string[] = [...currentPolicies]; // We clone the checkedUsers array
if (checked) {
@@ -131,7 +132,7 @@ const PolicySelectors = ({
}
// remove empty values
elements = elements.filter((element) => element !== "");
dispatch(setSelectedPolicies(elements));
};

View File

@@ -48,8 +48,6 @@ interface ISetPolicyProps {
open: boolean;
}
const styles = (theme: Theme) =>
createStyles({
...modalBasic,
@@ -76,7 +74,9 @@ const SetPolicy = ({
const [loading, setLoading] = useState<boolean>(false);
const [actualPolicy, setActualPolicy] = useState<string[]>([]);
const [selectedPolicy, setSelectedPolicy] = useState<string[]>([]);
const currentPolicies = useSelector((state: AppState) => state.createUser.selectedPolicies);
const currentPolicies = useSelector(
(state: AppState) => state.createUser.selectedPolicies
);
const setPolicyAction = () => {
let users = null;
let groups = null;
@@ -175,9 +175,7 @@ const SetPolicy = ({
)}
<Grid item xs={12}>
<div className={classes.tableBlock}>
<PolicySelectors
selectedPolicy={selectedPolicy}
/>
<PolicySelectors selectedPolicy={selectedPolicy} />
</div>
</Grid>
</Grid>

View File

@@ -14,95 +14,92 @@
// 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 { createSlice, PayloadAction } from "@reduxjs/toolkit";
import {
createUserAsync,
resetFormAsync,
} from "./thunk/AddUsersThunk";
import { createUserAsync, resetFormAsync } from "./thunk/AddUsersThunk";
export interface ICreateUser {
userName: string;
secretKey: string;
selectedGroups: string[];
selectedPolicies: string[];
showPassword: boolean;
sendEnabled: boolean;
addLoading: boolean;
apinoerror: boolean;
secretKeylength: number;
userName: string;
secretKey: string;
selectedGroups: string[];
selectedPolicies: string[];
showPassword: boolean;
sendEnabled: boolean;
addLoading: boolean;
apinoerror: boolean;
secretKeylength: number;
}
const initialState: ICreateUser = {
addLoading: false,
showPassword: false,
sendEnabled: false,
apinoerror: false,
userName: "",
secretKey: "",
selectedGroups: [],
selectedPolicies: [],
secretKeylength: 0,
addLoading: false,
showPassword: false,
sendEnabled: false,
apinoerror: false,
userName: "",
secretKey: "",
selectedGroups: [],
selectedPolicies: [],
secretKeylength: 0,
};
export const createUserSlice = createSlice({
name: "createUser",
initialState,
reducers: {
setAddLoading: (state, action: PayloadAction<boolean>) => {
state.addLoading = action.payload;
},
setUserName: (state, action: PayloadAction<string>) => {
state.userName = action.payload;
},
setSelectedGroups: (state, action: PayloadAction<string[]>) => {
state.selectedGroups = action.payload;
},
setSecretKey: (state, action: PayloadAction<string>) => {
state.secretKey = action.payload;
state.secretKeylength = state.secretKey.length;
},
setSelectedPolicies: (state, action: PayloadAction<string[]>) => {
state.selectedPolicies = action.payload;
},
setShowPassword: (state, action: PayloadAction<boolean>) => {
state.showPassword = action.payload;
},
setSendEnabled: (state) => {
state.sendEnabled = state.userName.trim() !== "";
},
setApinoerror: (state, action: PayloadAction<boolean>) => {
state.apinoerror = action.payload;
},
name: "createUser",
initialState,
reducers: {
setAddLoading: (state, action: PayloadAction<boolean>) => {
state.addLoading = action.payload;
},
extraReducers: (builder) => {
builder
.addCase(resetFormAsync.fulfilled, (state, action) => {
state.userName = "";
state.selectedGroups = [];
state.secretKey = "";
state.selectedPolicies = [];
state.showPassword = false;
})
.addCase(createUserAsync.pending, (state, action) => {
state.addLoading = true;
})
.addCase(createUserAsync.rejected, (state, action) => {
state.addLoading = false;
})
.addCase(createUserAsync.fulfilled, (state, action) => {
state.apinoerror = true;
});
setUserName: (state, action: PayloadAction<string>) => {
state.userName = action.payload;
},
setSelectedGroups: (state, action: PayloadAction<string[]>) => {
state.selectedGroups = action.payload;
},
setSecretKey: (state, action: PayloadAction<string>) => {
state.secretKey = action.payload;
state.secretKeylength = state.secretKey.length;
},
setSelectedPolicies: (state, action: PayloadAction<string[]>) => {
state.selectedPolicies = action.payload;
},
setShowPassword: (state, action: PayloadAction<boolean>) => {
state.showPassword = action.payload;
},
setSendEnabled: (state) => {
state.sendEnabled = state.userName.trim() !== "";
},
setApinoerror: (state, action: PayloadAction<boolean>) => {
state.apinoerror = action.payload;
},
},
extraReducers: (builder) => {
builder
.addCase(resetFormAsync.fulfilled, (state, action) => {
state.userName = "";
state.selectedGroups = [];
state.secretKey = "";
state.selectedPolicies = [];
state.showPassword = false;
})
.addCase(createUserAsync.pending, (state, action) => {
state.addLoading = true;
})
.addCase(createUserAsync.rejected, (state, action) => {
state.addLoading = false;
})
.addCase(createUserAsync.fulfilled, (state, action) => {
state.apinoerror = true;
});
},
});
export const {
setUserName,
setSelectedGroups,
setSecretKey,
setSelectedPolicies,
setShowPassword,
setAddLoading,
setSendEnabled,
setApinoerror,
setUserName,
setSelectedGroups,
setSecretKey,
setSelectedPolicies,
setShowPassword,
setAddLoading,
setSendEnabled,
setApinoerror,
} = createUserSlice.actions;
export default createUserSlice.reducer;

View File

@@ -16,48 +16,42 @@
import React from "react";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import {setSecretKey, setShowPassword} from "./AddUsersSlice";
import { setSecretKey, setShowPassword } from "./AddUsersSlice";
import { useSelector } from "react-redux";
import {AppState, useAppDispatch} from "../../../store";
import { AppState, useAppDispatch } from "../../../store";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
import RemoveRedEyeIcon from "@mui/icons-material/RemoveRedEye";
interface IAddUserProps2 {
classes: any;
classes: any;
}
const PasswordSelector = ({ classes }: IAddUserProps2 ) => {
const dispatch = useAppDispatch();
const showPassword = useSelector(
(state: AppState) => state.createUser.showPassword
)
const secretKey = useSelector(
(state: AppState) => state.createUser.secretKey
)
return (
<InputBoxWrapper
className={classes.spacerBottom}
classes={{
inputLabel: classes.sizedLabel,
}}
id="standard-multiline-static"
name="standard-multiline-static"
label="Password"
type={showPassword ? "text" : "password"}
value={secretKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setSecretKey(e.target.value));
}}
autoComplete="current-password"
overlayIcon={
showPassword ? (
<VisibilityOffIcon />
) : (
<RemoveRedEyeIcon />
)
}
overlayAction={() => dispatch(setShowPassword(!showPassword))}
/>
);
const PasswordSelector = ({ classes }: IAddUserProps2) => {
const dispatch = useAppDispatch();
const showPassword = useSelector(
(state: AppState) => state.createUser.showPassword
);
const secretKey = useSelector(
(state: AppState) => state.createUser.secretKey
);
return (
<InputBoxWrapper
className={classes.spacerBottom}
classes={{
inputLabel: classes.sizedLabel,
}}
id="standard-multiline-static"
name="standard-multiline-static"
label="Password"
type={showPassword ? "text" : "password"}
value={secretKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setSecretKey(e.target.value));
}}
autoComplete="current-password"
overlayIcon={showPassword ? <VisibilityOffIcon /> : <RemoveRedEyeIcon />}
overlayAction={() => dispatch(setShowPassword(!showPassword))}
/>
);
};
export default PasswordSelector;

View File

@@ -61,7 +61,9 @@ const SetUserPolicies = ({
const [actualPolicy, setActualPolicy] = useState<string[]>([]);
const [selectedPolicy, setSelectedPolicy] = useState<string[]>([]);
const statePolicies = useSelector((state: AppState) => state.createUser.selectedPolicies);
const statePolicies = useSelector(
(state: AppState) => state.createUser.selectedPolicies
);
const SetUserPoliciesAction = () => {
let entity = "user";
@@ -111,9 +113,7 @@ const SetUserPolicies = ({
>
<Grid container>
<Grid item xs={12}>
<PolicySelectors
selectedPolicy={selectedPolicy}
/>
<PolicySelectors selectedPolicy={selectedPolicy} />
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>

View File

@@ -18,34 +18,32 @@ import React, { Fragment } from "react";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { setUserName } from "./AddUsersSlice";
import { useSelector } from "react-redux";
import {AppState, useAppDispatch} from "../../../store";
import { AppState, useAppDispatch } from "../../../store";
interface IAddUserProps2 {
classes: any;
classes: any;
}
const UserSelector = ({ classes }: IAddUserProps2 ) => {
const dispatch = useAppDispatch();
const userName = useSelector(
(state: AppState) => state.createUser.userName
)
return (
<Fragment>
<InputBoxWrapper
className={classes.spacerBottom}
classes={{
inputLabel: classes.sizedLabel,
}}
id="accesskey-input"
name="accesskey-input"
label="User Name"
value={userName}
autoFocus={true}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setUserName(e.target.value));
}}
/>
</Fragment>
);
const UserSelector = ({ classes }: IAddUserProps2) => {
const dispatch = useAppDispatch();
const userName = useSelector((state: AppState) => state.createUser.userName);
return (
<Fragment>
<InputBoxWrapper
className={classes.spacerBottom}
classes={{
inputLabel: classes.sizedLabel,
}}
id="accesskey-input"
name="accesskey-input"
label="User Name"
value={userName}
autoFocus={true}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setUserName(e.target.value));
}}
/>
</Fragment>
);
};
export default UserSelector;

View File

@@ -16,50 +16,50 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import {
setSelectedGroups,
setUserName,
setSecretKey,
setSelectedPolicies,
setShowPassword,
setAddLoading,
setSelectedGroups,
setUserName,
setSecretKey,
setSelectedPolicies,
setShowPassword,
setAddLoading,
} from "../AddUsersSlice";
import {AppState} from "../../../../store";
import { AppState } from "../../../../store";
import api from "../../../../common/api";
import {ErrorResponseHandler} from "../../../../common/types";
import {setErrorSnackMessage} from "../../../../systemSlice";
import { ErrorResponseHandler } from "../../../../common/types";
import { setErrorSnackMessage } from "../../../../systemSlice";
export const resetFormAsync = createAsyncThunk(
"resetForm/resetFormAsync",
async (_, { dispatch }) => {
dispatch(setSelectedGroups([]));
dispatch(setUserName(""));
dispatch(setSecretKey(""));
dispatch(setSelectedPolicies([]));
dispatch(setShowPassword(false));
}
"resetForm/resetFormAsync",
async (_, { dispatch }) => {
dispatch(setSelectedGroups([]));
dispatch(setUserName(""));
dispatch(setSecretKey(""));
dispatch(setSelectedPolicies([]));
dispatch(setShowPassword(false));
}
);
export const createUserAsync = createAsyncThunk(
"createTenant/createNamespaceAsync",
async (_, { getState, rejectWithValue, dispatch }) => {
const state = getState() as AppState;
const accessKey = state.createUser.userName
const secretKey = state.createUser.secretKey
const selectedGroups = state.createUser.selectedGroups
const selectedPolicies = state.createUser.selectedPolicies
return api
.invoke("POST", "/api/v1/users", {
accessKey,
secretKey,
groups: selectedGroups,
policies: selectedPolicies,
})
.then((res) => {
dispatch(setAddLoading(false));
})
.catch((err: ErrorResponseHandler) => {
dispatch(setAddLoading(false));
dispatch(setErrorSnackMessage(err));
});
}
"createTenant/createNamespaceAsync",
async (_, { getState, rejectWithValue, dispatch }) => {
const state = getState() as AppState;
const accessKey = state.createUser.userName;
const secretKey = state.createUser.secretKey;
const selectedGroups = state.createUser.selectedGroups;
const selectedPolicies = state.createUser.selectedPolicies;
return api
.invoke("POST", "/api/v1/users", {
accessKey,
secretKey,
groups: selectedGroups,
policies: selectedPolicies,
})
.then((res) => {
dispatch(setAddLoading(false));
})
.catch((err: ErrorResponseHandler) => {
dispatch(setAddLoading(false));
dispatch(setErrorSnackMessage(err));
});
}
);