diff --git a/portal-ui/src/screens/Console/Policies/PolicySelectors.tsx b/portal-ui/src/screens/Console/Policies/PolicySelectors.tsx index 27658bf22..d8ee0ac79 100644 --- a/portal-ui/src/screens/Console/Policies/PolicySelectors.tsx +++ b/portal-ui/src/screens/Console/Policies/PolicySelectors.tsx @@ -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) => diff --git a/portal-ui/src/screens/Console/Policies/SetPolicy.tsx b/portal-ui/src/screens/Console/Policies/SetPolicy.tsx index 04b54e1e6..d066d0f6f 100644 --- a/portal-ui/src/screens/Console/Policies/SetPolicy.tsx +++ b/portal-ui/src/screens/Console/Policies/SetPolicy.tsx @@ -173,7 +173,6 @@ const SetPolicy = ({
diff --git a/portal-ui/src/screens/Console/Users/AddUserScreen.tsx b/portal-ui/src/screens/Console/Users/AddUserScreen.tsx index 831c06d18..0744446aa 100644 --- a/portal-ui/src/screens/Console/Users/AddUserScreen.tsx +++ b/portal-ui/src/screens/Console/Users/AddUserScreen.tsx @@ -14,13 +14,15 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -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(false); - const [accessKey, setAccessKey] = useState(""); - const [secretKey, setSecretKey] = useState(""); - const [selectedGroups, setSelectedGroups] = useState([]); - const [selectedPolicies, setSelectedPolicies] = useState([]); - const [showPassword, setShowPassword] = useState(false); + return ( + + + } /> + + } + helpbox={} + > +
) => { + saveRecord(e); + }} + > + +
+ +
+
+ ) => { + dispatch(setSecretKey(e.target.value)); + }} + autoComplete="current-password" + overlayIcon={ + showPassword ? ( + + ) : ( + + ) + } + overlayAction={() => dispatch(setShowPassword(!showPassword))} + /> +
+ + + + + + { + dispatch(setSelectedGroups(elements)); + }} + /> + + + {addLoading && ( + + + + )} +
+ + - 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 ( - - - } /> - - } - helpbox={} - > - ) => { - saveRecord(e); - }} - > - -
- ) => { - setAccessKey(e.target.value); - }} - /> -
-
- ) => { - setSecretKey(e.target.value); - }} - autoComplete="current-password" - overlayIcon={ - showPassword ? ( - - ) : ( - - ) - } - overlayAction={() => setShowPassword(!showPassword)} - /> -
- - - - - - { - setSelectedGroups(elements); - }} - /> - - - {addLoading && ( - - - - )} -
- - - - - - -
-
-
-
- ); + +
+ +
+
+
+
+ ); }; -export default withStyles(styles)(AddUser); +export default withStyles(styles)(AddUser); \ No newline at end of file diff --git a/portal-ui/src/screens/Console/Users/AddUsersSlice.tsx b/portal-ui/src/screens/Console/Users/AddUsersSlice.tsx new file mode 100644 index 000000000..4f24c6ee9 --- /dev/null +++ b/portal-ui/src/screens/Console/Users/AddUsersSlice.tsx @@ -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 . +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) => { + state.addLoading = action.payload; + }, + setUserName: (state, action: PayloadAction) => { + state.userName = action.payload; + }, + setSelectedGroups: (state, action: PayloadAction) => { + state.selectedGroups = action.payload; + }, + setSecretKey: (state, action: PayloadAction) => { + state.secretKey = action.payload; + }, + setSelectedPolicies: (state, action: PayloadAction) => { + state.selectedPolicies = action.payload; + }, + setShowPassword: (state, action: PayloadAction) => { + state.showPassword = action.payload; + }, + setSendEnabled: (state) => { + state.sendEnabled = state.userName.trim() !== ""; + }, + setApinoerror: (state, action: PayloadAction) => { + 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; diff --git a/portal-ui/src/screens/Console/Users/SetUserPolicies.tsx b/portal-ui/src/screens/Console/Users/SetUserPolicies.tsx index 8ceb6929a..1a1f55783 100644 --- a/portal-ui/src/screens/Console/Users/SetUserPolicies.tsx +++ b/portal-ui/src/screens/Console/Users/SetUserPolicies.tsx @@ -110,7 +110,6 @@ const SetUserPolicies = ({ diff --git a/portal-ui/src/screens/Console/Users/UserSelector.tsx b/portal-ui/src/screens/Console/Users/UserSelector.tsx new file mode 100644 index 000000000..dff953e2e --- /dev/null +++ b/portal-ui/src/screens/Console/Users/UserSelector.tsx @@ -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 . + +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 ( + + ) => { + dispatch(setUserName(e.target.value)); + }} + /> + + ); +}; +export default UserSelector; diff --git a/portal-ui/src/screens/Console/Users/thunk/AddUsersThunk.tsx b/portal-ui/src/screens/Console/Users/thunk/AddUsersThunk.tsx new file mode 100644 index 000000000..ee57836cb --- /dev/null +++ b/portal-ui/src/screens/Console/Users/thunk/AddUsersThunk.tsx @@ -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 . + +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)); + }); + } +); diff --git a/portal-ui/src/store.ts b/portal-ui/src/store.ts index 3e6d1ebd2..4081a1f08 100644 --- a/portal-ui/src/store.ts +++ b/portal-ui/src/store.ts @@ -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, });