Refactor session to avoid duplicate calls to apis (#2868)

Co-authored-by: cesnietor <>
This commit is contained in:
Cesar N
2023-06-13 14:27:48 -07:00
committed by GitHub
parent 08a3ff65c7
commit 253053cc23
9 changed files with 177 additions and 84 deletions

View File

@@ -14,25 +14,18 @@
// 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 { useEffect, useState } from "react";
import { Navigate, useLocation } from "react-router-dom";
import useApi from "./screens/Console/Common/Hooks/useApi";
import { ErrorResponseHandler } from "./common/types";
import { ReplicationSite } from "./screens/Console/Configurations/SiteReplication/SiteReplication";
import { useSelector } from "react-redux";
import {
globalSetDistributedSetup,
setAnonymousMode,
setOverrideStyles,
setSiteReplicationInfo,
userLogged,
} from "./systemSlice";
import { SRInfoStateType } from "./types";
import { AppState, useAppDispatch } from "./store";
import { saveSessionResponse } from "./screens/Console/consoleSlice";
import { getOverrideColorVariants } from "./utils/stylesUtils";
import LoadingComponent from "./common/LoadingComponent";
import { api } from "api";
import { fetchSession } from "./screens/LoginPage/sessionThunk";
import { setSiteReplicationInfo, setLocationPath } from "./systemSlice";
import { SessionCallStates } from "./screens/Console/consoleSlice.types";
interface ProtectedRouteProps {
Component: any;
@@ -41,8 +34,11 @@ interface ProtectedRouteProps {
const ProtectedRoute = ({ Component }: ProtectedRouteProps) => {
const dispatch = useAppDispatch();
const [sessionLoading, setSessionLoading] = useState<boolean>(true);
const userLoggedIn = useSelector((state: AppState) => state.system.loggedIn);
const [componentLoading, setComponentLoading] = useState<boolean>(true);
const sessionLoadingState = useSelector(
(state: AppState) => state.console.sessionLoadingState
);
const anonymousMode = useSelector(
(state: AppState) => state.system.anonymousMode
);
@@ -53,56 +49,19 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => {
return <Navigate to={{ pathname: `login` }} />;
};
const pathnameParts = pathname.split("/");
const screen = pathnameParts.length > 2 ? pathnameParts[1] : "";
useEffect(() => {
dispatch(setLocationPath(pathname));
}, [dispatch, pathname]);
useEffect(() => {
api.session
.sessionCheck()
.then((res) => {
dispatch(saveSessionResponse(res.data));
dispatch(userLogged(true));
setSessionLoading(false);
dispatch(globalSetDistributedSetup(res.data?.distributedMode || false));
dispatch(fetchSession());
}, [dispatch]);
if (res.data.customStyles && res.data.customStyles !== "") {
const overrideColorVariants = getOverrideColorVariants(
res.data.customStyles
);
if (overrideColorVariants !== false) {
dispatch(setOverrideStyles(overrideColorVariants));
}
}
})
.catch(() => {
// if we are trying to browse, probe access to the requested prefix
if (screen === "browser") {
const bucket = pathnameParts.length >= 3 ? pathnameParts[2] : "";
// no bucket, no business
if (bucket === "") {
setSessionLoading(false);
return;
}
// before marking the session as done, let's check if the bucket is publicly accessible
api.buckets
.listObjects(
bucket,
{ limit: 1 },
{ headers: { "X-Anonymous": "1" } }
)
.then(() => {
dispatch(setAnonymousMode());
setSessionLoading(false);
})
.catch(() => {
setSessionLoading(false);
});
} else {
setSessionLoading(false);
}
});
}, [dispatch, screen, pathnameParts]);
useEffect(() => {
if (sessionLoadingState === SessionCallStates.Done) {
setComponentLoading(false);
}
}, [dispatch, sessionLoadingState]);
const [, invokeSRInfoApi] = useApi(
(res: any) => {
@@ -132,16 +91,17 @@ const ProtectedRoute = ({ Component }: ProtectedRouteProps) => {
);
useEffect(() => {
if (userLoggedIn && !sessionLoading && !anonymousMode) {
if (userLoggedIn && !componentLoading && !anonymousMode) {
invokeSRInfoApi("GET", `api/v1/admin/site-replication`);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userLoggedIn, sessionLoading]);
}, [userLoggedIn, componentLoading]);
// if we're still trying to retrieve user session render nothing
if (sessionLoading) {
if (componentLoading) {
return <LoadingComponent />;
}
// redirect user to the right page based on session status
return userLoggedIn ? <Component /> : <StorePathAndRedirect />;
};

View File

@@ -152,9 +152,9 @@ const CommandBar = () => {
}, []);
const initialActions: Action[] = routesAsKbarActions(
features,
buckets,
navigate
navigate,
features
);
useRegisterActions(initialActions, [buckets, features]);

View File

@@ -15,30 +15,31 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { SessionResponse } from "../../api/consoleApi";
import { AppState } from "../../store";
import { SessionResponse } from "api/consoleApi";
import { fetchSession } from "../../screens/LoginPage/sessionThunk";
import { SessionCallStates } from "./consoleSlice.types";
export interface ConsoleState {
session: SessionResponse;
sessionLoadingState: SessionCallStates;
}
const initialState: ConsoleState = {
session: {
status: undefined,
features: [],
distributedMode: false,
permissions: {},
allowResources: undefined,
customStyles: undefined,
envConstants: undefined,
serverEndPoint: "",
},
session: {},
sessionLoadingState: SessionCallStates.Initial,
};
export const consoleSlice = createSlice({
name: "console",
initialState,
reducers: {
setSessionLoadingState: (
state,
action: PayloadAction<SessionCallStates>
) => {
state.sessionLoadingState = action.payload;
},
saveSessionResponse: (state, action: PayloadAction<SessionResponse>) => {
state.session = action.payload;
},
@@ -46,9 +47,19 @@ export const consoleSlice = createSlice({
state.session = initialState.session;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchSession.pending, (state, action) => {
state.sessionLoadingState = SessionCallStates.Loading;
})
.addCase(fetchSession.fulfilled, (state, action) => {
state.sessionLoadingState = SessionCallStates.Done;
});
},
});
export const { saveSessionResponse, resetSession } = consoleSlice.actions;
export const { saveSessionResponse, resetSession, setSessionLoadingState } =
consoleSlice.actions;
export const selSession = (state: AppState) => state.console.session;
export const selFeatures = (state: AppState) =>
state.console.session ? state.console.session.features : [];

View File

@@ -0,0 +1,21 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 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/>.
export enum SessionCallStates {
Initial = "initial",
Loading = "loading",
Done = "done",
}

View File

@@ -21,9 +21,9 @@ import { IAM_PAGES } from "../../common/SecureComponent/permissions";
import { Bucket } from "../../api/consoleApi";
export const routesAsKbarActions = (
features: string[] | undefined,
buckets: Bucket[],
navigate: (url: string) => void
navigate: (url: string) => void,
features?: string[]
) => {
const initialActions: Action[] = [];
const allowedMenuItems = validRoutes(features);

View File

@@ -36,7 +36,7 @@ export interface LoginStrategyPayload {
}
export const getTargetPath = () => {
let targetPath = "/";
let targetPath = "/browser";
if (
localStorage.getItem("redirect-path") &&
localStorage.getItem("redirect-path") !== ""

View File

@@ -0,0 +1,98 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 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 { setErrorSnackMessage } from "../../systemSlice";
import { api } from "api";
import {
Error,
HttpResponse,
SessionResponse,
ListObjectsResponse,
} from "api/consoleApi";
import { errorToHandler } from "api/errors";
import {
saveSessionResponse,
setSessionLoadingState,
} from "../Console/consoleSlice";
import { SessionCallStates } from "../Console/consoleSlice.types";
import {
globalSetDistributedSetup,
setOverrideStyles,
setAnonymousMode,
} from "../../../src/systemSlice";
import { getOverrideColorVariants } from "../../utils/stylesUtils";
import { userLogged } from "../../../src/systemSlice";
import { AppState } from "../../store";
export const fetchSession = createAsyncThunk(
"session/fetchSession",
async (_, { getState, dispatch, rejectWithValue }) => {
const state = getState() as AppState;
const pathnameParts = state.system.locationPath.split("/");
const screen = pathnameParts.length > 2 ? pathnameParts[1] : "";
return api.session
.sessionCheck()
.then((res: HttpResponse<SessionResponse, Error>) => {
dispatch(userLogged(true));
dispatch(saveSessionResponse(res.data));
dispatch(globalSetDistributedSetup(res.data.distributedMode || false));
if (res.data.customStyles && res.data.customStyles !== "") {
const overrideColorVariants = getOverrideColorVariants(
res.data.customStyles
);
if (overrideColorVariants !== false) {
dispatch(setOverrideStyles(overrideColorVariants));
}
}
})
.catch(async (res: HttpResponse<SessionResponse, Error>) => {
if (screen === "browser") {
const bucket = pathnameParts.length >= 3 ? pathnameParts[2] : "";
// no bucket, no business
if (bucket === "") {
return;
}
// before marking the session as done, let's check if the bucket is publicly accessible (anonymous)
api.buckets
.listObjects(
bucket,
{ limit: 1 },
{ headers: { "X-Anonymous": "1" } }
)
.then(() => {
dispatch(setAnonymousMode());
})
.catch((res: HttpResponse<ListObjectsResponse, Error>) => {
dispatch(setErrorSnackMessage(errorToHandler(res.error)));
})
.finally(() => {
// TODO: we probably need a thunk for this api since setting the state here is hacky,
// we can use a state to let the ProtectedRoutes know when to render the elements
dispatch(setSessionLoadingState(SessionCallStates.Done));
});
} else {
dispatch(setSessionLoadingState(SessionCallStates.Done));
dispatch(setErrorSnackMessage(errorToHandler(res.error)));
}
return rejectWithValue(res.error);
});
}
);

View File

@@ -23,7 +23,7 @@ import logReducer from "./screens/Console/Logs/logsSlice";
import healthInfoReducer from "./screens/Console/HealthInfo/healthInfoSlice";
import watchReducer from "./screens/Console/Watch/watchSlice";
import consoleReducer from "./screens/Console/consoleSlice";
import bucketsReducer from "./screens/Console/Buckets/ListBuckets/AddBucket/addBucketsSlice";
import addBucketsReducer from "./screens/Console/Buckets/ListBuckets/AddBucket/addBucketsSlice";
import bucketDetailsReducer from "./screens/Console/Buckets/BucketDetails/bucketDetailsSlice";
import objectBrowserReducer from "./screens/Console/ObjectBrowser/objectBrowserSlice";
import dashboardReducer from "./screens/Console/Dashboard/dashboardSlice";
@@ -39,7 +39,7 @@ const rootReducer = combineReducers({
logs: logReducer,
watch: watchReducer,
console: consoleReducer,
addBucket: bucketsReducer,
addBucket: addBucketsReducer,
bucketDetails: bucketDetailsReducer,
objectBrowser: objectBrowserReducer,
healthInfo: healthInfoReducer,

View File

@@ -29,7 +29,6 @@ export interface SystemState {
loggedIn: boolean;
showMarketplace: boolean;
sidebarOpen: boolean;
session: string;
userName: string;
serverNeedsRestart: boolean;
serverIsLoading: boolean;
@@ -45,13 +44,13 @@ export interface SystemState {
anonymousMode: boolean;
helpName: string;
helpTabName: string;
locationPath: string;
}
const initialState: SystemState = {
value: 0,
loggedIn: false,
showMarketplace: false,
session: "",
userName: "",
sidebarOpen: initSideBarOpen,
siteReplicationInfo: { siteName: "", curSite: false, enabled: false },
@@ -76,6 +75,7 @@ const initialState: SystemState = {
anonymousMode: false,
helpName: "help",
helpTabName: "docs",
locationPath: "",
};
export const systemSlice = createSlice({
@@ -155,7 +155,6 @@ export const systemSlice = createSlice({
state.licenseInfo = action.payload;
},
setHelpName: (state, action: PayloadAction<string>) => {
console.log("setting helpName: ", action.payload);
state.helpName = action.payload;
},
setHelpTabName: (state, action: PayloadAction<string>) => {
@@ -172,6 +171,9 @@ export const systemSlice = createSlice({
state.anonymousMode = true;
state.loggedIn = true;
},
setLocationPath: (state, action: PayloadAction<string>) => {
state.locationPath = action.payload;
},
resetSystem: () => {
return initialState;
},
@@ -200,6 +202,7 @@ export const {
configurationIsLoading,
setHelpName,
setHelpTabName,
setLocationPath,
} = systemSlice.actions;
export const selDistSet = (state: AppState) => state.system.distributedSetup;