Reduce renders in User Name when adding a user (#2106)

This commit is contained in:
Cesar Celis Hernandez
2022-06-10 15:57:52 -04:00
committed by GitHub
parent e68bc08fed
commit 5d591b18d9
8 changed files with 396 additions and 190 deletions

View File

@@ -36,11 +36,12 @@ import TableWrapper from "../Common/TableWrapper/TableWrapper";
import SearchBox from "../Common/SearchBox";
import { setModalErrorSnackMessage } from "../../../systemSlice";
import { useAppDispatch } from "../../../store";
import { setSelectedPolicies } from "../Users/AddUsersSlice";
interface ISelectPolicyProps {
classes: any;
selectedPolicy?: string[];
setSelectedPolicy: any;
}
const styles = (theme: Theme) =>
@@ -77,7 +78,6 @@ const styles = (theme: Theme) =>
const PolicySelectors = ({
classes,
selectedPolicy = [],
setSelectedPolicy,
}: ISelectPolicyProps) => {
const dispatch = useAppDispatch();
// Local State
@@ -129,7 +129,7 @@ const PolicySelectors = ({
// remove empty values
elements = elements.filter((element) => element !== "");
setSelectedPolicy(elements);
dispatch(setSelectedPolicies(elements));
};
const filteredRecords = records.filter((elementItem) =>

View File

@@ -173,7 +173,6 @@ const SetPolicy = ({
<div className={classes.tableBlock}>
<PolicySelectors
selectedPolicy={selectedPolicy}
setSelectedPolicy={setSelectedPolicy}
/>
</div>
</Grid>

View File

@@ -14,13 +14,15 @@
// 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, { Fragment, useState } from "react";
import UserSelector from "./UserSelector";
import React, { Fragment } from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import {createUserAsync, resetFormAsync} from "./thunk/AddUsersThunk";
import {
formFieldStyles,
modalStyleUtils,
formFieldStyles,
modalStyleUtils,
} from "../Common/FormComponents/common/styleLibrary";
import Grid from "@mui/material/Grid";
import { Button, LinearProgress } from "@mui/material";
@@ -37,200 +39,180 @@ import GroupsSelectors from "./GroupsSelectors";
import RemoveRedEyeIcon from "@mui/icons-material/RemoveRedEye";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
import { ErrorResponseHandler } from "../../../../src/common/types";
import api from "../../../../src/common/api";
import { useNavigate } from "react-router-dom";
import FormLayout from "../Common/FormLayout";
import AddUserHelpBox from "./AddUserHelpBox";
import { setErrorSnackMessage } from "../../../systemSlice";
import { useNavigate } from "react-router-dom";
import { useAppDispatch } from "../../../store";
import { useSelector} from "react-redux";
import {AppState} from "../../../store";
import {
setSelectedGroups,
setAddLoading,
setShowPassword,
setSecretKey,
setSendEnabled,
} from "./AddUsersSlice";
interface IAddUserProps {
classes: any;
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
bottomContainer: {
display: "flex",
flexGrow: 1,
alignItems: "center",
margin: "auto",
justifyContent: "center",
"& div": {
width: 150,
"@media (max-width: 900px)": {
flexFlow: "column",
createStyles({
bottomContainer: {
display: "flex",
flexGrow: 1,
alignItems: "center",
margin: "auto",
justifyContent: "center",
"& div": {
width: 150,
"@media (max-width: 900px)": {
flexFlow: "column",
},
},
},
},
},
...formFieldStyles,
...modalStyleUtils,
});
...formFieldStyles,
...modalStyleUtils,
});
const AddUser = ({ classes }: IAddUserProps) => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const dispatch = useAppDispatch();
const showPassword = useSelector(
(state: AppState) => state.createUser.showPassword
)
const selectedPolicies = useSelector(
(state: AppState) => state.createUser.selectedPolicies
)
const selectedGroups = useSelector(
(state: AppState) => state.createUser.selectedGroups
)
const secretKey = useSelector(
(state: AppState) => state.createUser.secretKey
)
const addLoading = useSelector(
(state: AppState) => state.createUser.addLoading
)
const sendEnabled = useSelector(
(state: AppState) => state.createUser.sendEnabled
)
const navigate = useNavigate();
dispatch(setSendEnabled());
const saveRecord = (event: React.FormEvent) => {
event.preventDefault();
if (secretKey.length < 8) {
dispatch(
setErrorSnackMessage({
errorMessage: "Passwords must be at least 8 characters long",
detailedError: "",
})
);
dispatch(setAddLoading(false));
return;
}
if (addLoading) {
return;
}
dispatch(setAddLoading(true));
dispatch(createUserAsync())
.unwrap() // <-- async Thunk returns a promise, that can be 'unwrapped')
.then(() => navigate(`${IAM_PAGES.USERS}`))
};
const [addLoading, setAddLoading] = useState<boolean>(false);
const [accessKey, setAccessKey] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
const [selectedPolicies, setSelectedPolicies] = useState<string[]>([]);
const [showPassword, setShowPassword] = useState<boolean>(false);
return (
<Fragment>
<Grid item xs={12}>
<PageHeader label={<BackLink to={IAM_PAGES.USERS} label={"Users"} />} />
<PageLayout>
<FormLayout
title={"Create User"}
icon={<CreateUserIcon />}
helpbox={<AddUserHelpBox />}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
saveRecord(e);
}}
>
<Grid item xs={12}>
<div className={classes.formFieldRow}>
<UserSelector classes={classes} />
</div>
<div className={classes.formFieldRow}>
<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))}
/>
</div>
<Grid container item spacing="20">
<Grid item xs={12}>
<PolicySelectors
selectedPolicy={selectedPolicies}
/>
</Grid>
<Grid item xs={12}>
<GroupsSelectors
selectedGroups={selectedGroups}
setSelectedGroups={(elements: string[]) => {
dispatch(setSelectedGroups(elements));
}}
/>
</Grid>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
<Grid item xs={12} className={classes.modalButtonBar}>
<Button
type="button"
variant="outlined"
color="primary"
onClick={(e) => {
dispatch(resetFormAsync());
}}
>
Clear
</Button>
const sendEnabled = accessKey.trim() !== "";
const saveRecord = (event: React.FormEvent) => {
event.preventDefault();
if (secretKey.length < 8) {
dispatch(
setErrorSnackMessage({
errorMessage: "Passwords must be at least 8 characters long",
detailedError: "",
})
);
setAddLoading(false);
return;
}
if (addLoading) {
return;
}
setAddLoading(true);
api
.invoke("POST", "/api/v1/users", {
accessKey,
secretKey,
groups: selectedGroups,
policies: selectedPolicies,
})
.then((res) => {
setAddLoading(false);
navigate(`${IAM_PAGES.USERS}`);
})
.catch((err: ErrorResponseHandler) => {
setAddLoading(false);
dispatch(setErrorSnackMessage(err));
});
};
const resetForm = () => {
setSelectedGroups([]);
setAccessKey("");
setSecretKey("");
setSelectedPolicies([]);
setShowPassword(false);
};
return (
<Fragment>
<Grid item xs={12}>
<PageHeader label={<BackLink to={IAM_PAGES.USERS} label={"Users"} />} />
<PageLayout>
<FormLayout
title={"Create User"}
icon={<CreateUserIcon />}
helpbox={<AddUserHelpBox />}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
saveRecord(e);
}}
>
<Grid item xs={12}>
<div className={classes.formFieldRow}>
<InputBoxWrapper
className={classes.spacerBottom}
classes={{
inputLabel: classes.sizedLabel,
}}
id="accesskey-input"
name="accesskey-input"
label="User Name"
value={accessKey}
autoFocus={true}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAccessKey(e.target.value);
}}
/>
</div>
<div className={classes.formFieldRow}>
<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>) => {
setSecretKey(e.target.value);
}}
autoComplete="current-password"
overlayIcon={
showPassword ? (
<VisibilityOffIcon />
) : (
<RemoveRedEyeIcon />
)
}
overlayAction={() => setShowPassword(!showPassword)}
/>
</div>
<Grid container item spacing="20">
<Grid item xs={12}>
<PolicySelectors
selectedPolicy={selectedPolicies}
setSelectedPolicy={setSelectedPolicies}
/>
</Grid>
<Grid item xs={12}>
<GroupsSelectors
selectedGroups={selectedGroups}
setSelectedGroups={(elements: string[]) => {
setSelectedGroups(elements);
}}
/>
</Grid>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
<Grid item xs={12} className={classes.modalButtonBar}>
<Button
type="button"
variant="outlined"
color="primary"
onClick={resetForm}
>
Clear
</Button>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading || !sendEnabled}
>
Save
</Button>
</Grid>
</form>
</FormLayout>
</PageLayout>
</Grid>
</Fragment>
);
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading || !sendEnabled}
>
Save
</Button>
</Grid>
</form>
</FormLayout>
</PageLayout>
</Grid>
</Fragment>
);
};
export default withStyles(styles)(AddUser);
export default withStyles(styles)(AddUser);

View File

@@ -0,0 +1,105 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 { createSlice, PayloadAction } from "@reduxjs/toolkit";
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;
}
const initialState: ICreateUser = {
addLoading: false,
showPassword: false,
sendEnabled: false,
apinoerror: false,
userName: "",
secretKey: "",
selectedGroups: [],
selectedPolicies: [],
};
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;
},
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,
} = createUserSlice.actions;
export default createUserSlice.reducer;

View File

@@ -110,7 +110,6 @@ const SetUserPolicies = ({
<Grid item xs={12}>
<PolicySelectors
selectedPolicy={selectedPolicy}
setSelectedPolicy={setSelectedPolicy}
/>
</Grid>
</Grid>

View File

@@ -0,0 +1,51 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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, { Fragment } from "react";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { setUserName } from "./AddUsersSlice";
import { useDispatch, useSelector } from "react-redux";
import {AppState} from "../../../store";
interface IAddUserProps2 {
classes: any;
}
const UserSelector = ({ classes }: IAddUserProps2 ) => {
const dispatch = useDispatch();
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

@@ -0,0 +1,68 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 { createAsyncThunk } from "@reduxjs/toolkit";
import {
setSelectedGroups,
setUserName,
setSecretKey,
setSelectedPolicies,
setShowPassword,
setAddLoading,
} from "../AddUsersSlice";
import {AppState} from "../../../../store";
import api from "../../../../common/api";
import {ErrorResponseHandler} from "../../../../common/types";
import {setErrorSnackMessage} from "../../../../systemSlice";
import history from "../../../../history";
import {IAM_PAGES} from "../../../../common/SecureComponent/permissions";
export const resetFormAsync = createAsyncThunk(
"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));
history.push(`${IAM_PAGES.USERS}`);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setAddLoading(false));
dispatch(setErrorSnackMessage(err));
});
}
);

View File

@@ -28,6 +28,7 @@ import objectBrowserReducer from "./screens/Console/ObjectBrowser/objectBrowserS
import tenantsReducer from "./screens/Console/Tenants/tenantsSlice";
import dashboardReducer from "./screens/Console/Dashboard/dashboardSlice";
import createTenantReducer from "./screens/Console/Tenants/AddTenant/createTenantSlice";
import createUserReducer from "./screens/Console/Users/AddUsersSlice";
import addPoolReducer from "./screens/Console/Tenants/TenantDetails/Pools/AddPool/addPoolSlice";
import editPoolReducer from "./screens/Console/Tenants/TenantDetails/Pools/EditPool/editPoolSlice";
@@ -45,6 +46,7 @@ const rootReducer = combineReducers({
// Operator Reducers
tenants: tenantsReducer,
createTenant: createTenantReducer,
createUser: createUserReducer,
addPool: addPoolReducer,
editPool: editPoolReducer,
});