Skip to content
This repository was archived by the owner on Nov 30, 2022. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ The types of changes are:
* Add consent request api [#1387](https://github.com/ethyca/fidesops/pull/1387)
* Add authenticated route to get consent preferences [#1402](https://github.com/ethyca/fidesops/pull/1402)
* Access and erasure support for Braze [#1248](https://github.com/ethyca/fidesops/pull/1248)
* Admin UI: Persist Redux store to localStorage [#1401](https://github.com/ethyca/fidesops/pull/1409)

### Removed

Expand Down
27 changes: 21 additions & 6 deletions clients/ops/admin-ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions clients/ops/admin-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"react-dom": "^17.0.2",
"react-feature-flags": "^1.0.0",
"react-redux": "^7.2.6",
"redux-persist": "^6.0.0",
"whatwg-fetch": "^3.6.2",
"yup": "^0.32.11"
},
Expand Down
109 changes: 89 additions & 20 deletions clients/ops/admin-ui/src/app/store.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import { configureStore, StateFromReducersMapObject } from "@reduxjs/toolkit";
import {
AnyAction,
combineReducers,
configureStore,
StateFromReducersMapObject,
} from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/query/react";

import { STORED_CREDENTIALS_KEY } from "../constants";
import {
authApi,
AuthState,
credentialStorage,
reducer as authReducer,
} from "../features/auth";
FLUSH,
PAUSE,
PERSIST,
persistReducer,
persistStore,
PURGE,
REGISTER,
REHYDRATE,
} from "redux-persist";
import createWebStorage from "redux-persist/lib/storage/createWebStorage";

import { STORAGE_ROOT_KEY } from "../constants";
import { authApi, AuthState, reducer as authReducer } from "../features/auth";
import {
connectionTypeApi,
reducer as connectionTypeReducer,
Expand All @@ -25,27 +36,83 @@ import {
userApi,
} from "../features/user-management";

const reducer = {
[privacyRequestApi.reducerPath]: privacyRequestApi.reducer,
subjectRequests: privacyRequestsReducer,
[userApi.reducerPath]: userApi.reducer,
/**
* To prevent the "redux-perist failed to create sync storage. falling back to noop storage"
* console message within Next.js, the following snippet is required.
* {@https://mightycoders.xyz/redux-persist-failed-to-create-sync-storage-falling-back-to-noop-storage}
*/
const createNoopStorage = () => ({
getItem() {
return Promise.resolve(null);
},
setItem(_key: any, value: any) {
return Promise.resolve(value);
},
removeItem() {
return Promise.resolve();
},
});

const storage =
typeof window !== "undefined"
? createWebStorage("local")
: createNoopStorage();

const reducerMap = {
[authApi.reducerPath]: authApi.reducer,
userManagement: userManagementReducer,
[datastoreConnectionApi.reducerPath]: datastoreConnectionApi.reducer,
datastoreConnections: datastoreConnectionReducer,
auth: authReducer,
[connectionTypeApi.reducerPath]: connectionTypeApi.reducer,
connectionType: connectionTypeReducer,
[datastoreConnectionApi.reducerPath]: datastoreConnectionApi.reducer,
datastoreConnections: datastoreConnectionReducer,
[privacyRequestApi.reducerPath]: privacyRequestApi.reducer,
subjectRequests: privacyRequestsReducer,
[userApi.reducerPath]: userApi.reducer,
userManagement: userManagementReducer,
};

export type RootState = StateFromReducersMapObject<typeof reducer>;
const allReducers = combineReducers(reducerMap);

const rootReducer = (state: any, action: AnyAction) => {
let newState = { ...state };
if (action.type === "auth/logout") {
storage.removeItem(STORAGE_ROOT_KEY);
newState = undefined;
}
return allReducers(newState, action);
};

const persistConfig = {
key: "root",
storage,
/*
NOTE: It is also strongly recommended to blacklist any api(s) that you have configured with RTK Query.
If the api slice reducer is not blacklisted, the api cache will be automatically persisted
and restored which could leave you with phantom subscriptions from components that do not exist any more.
(https://redux-toolkit.js.org/usage/usage-guide#use-with-redux-persist)
*/
blacklist: [
authApi.reducerPath,
connectionTypeApi.reducerPath,
datastoreConnectionApi.reducerPath,
privacyRequestApi.reducerPath,
userApi.reducerPath,
],
};

const persistedReducer = persistReducer(persistConfig, rootReducer);

export type RootState = StateFromReducersMapObject<typeof reducerMap>;

export const makeStore = (preloadedState?: Partial<RootState>) =>
configureStore({
reducer,
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(
credentialStorage.middleware,
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}).concat(
privacyRequestApi.middleware,
userApi.middleware,
authApi.middleware,
Expand All @@ -58,7 +125,7 @@ export const makeStore = (preloadedState?: Partial<RootState>) =>

let storedAuthState: AuthState | undefined;
if (typeof window !== "undefined" && "localStorage" in window) {
const storedAuthStateString = localStorage.getItem(STORED_CREDENTIALS_KEY);
const storedAuthStateString = localStorage.getItem(STORAGE_ROOT_KEY);
if (storedAuthStateString) {
try {
storedAuthState = JSON.parse(storedAuthStateString);
Expand All @@ -74,6 +141,8 @@ const store = makeStore({
auth: storedAuthState,
});

export const persistor = persistStore(store);

setupListeners(store.dispatch);

export default store;
5 changes: 4 additions & 1 deletion clients/ops/admin-ui/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ const API_URL = process.env.NEXT_PUBLIC_FIDESOPS_API
: "";
export const BASE_URL = API_URL + BASE_API_URN;

export const STORED_CREDENTIALS_KEY = "auth.fidesops-admin-ui";
/**
* Redux-persist storage root key
*/
export const STORAGE_ROOT_KEY = "persist:root";

export const USER_PRIVILEGES: UserPrivileges[] = [
{
Expand Down

This file was deleted.

29 changes: 2 additions & 27 deletions clients/ops/admin-ui/src/features/auth/auth.slice.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import {
createListenerMiddleware,
createSlice,
PayloadAction,
} from "@reduxjs/toolkit";
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

import type { RootState } from "../../app/store";
import { BASE_URL, STORED_CREDENTIALS_KEY } from "../../constants";
import { BASE_URL } from "../../constants";
import { addCommonHeaders } from "../common/CommonHeaders";
import { utf8ToB64 } from "../common/utils";
import { User } from "../user-management/types";
Expand Down Expand Up @@ -56,27 +52,6 @@ export const selectToken = (state: RootState) => selectAuth(state).token;

export const { login, logout } = authSlice.actions;

export const credentialStorage = createListenerMiddleware();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the following event listeners given the redux-perist localStorage functionality. Redux-persist localStorage root key is persist-root.

credentialStorage.startListening({
actionCreator: login,
effect: (action, { getState }) => {
if (window && window.localStorage) {
localStorage.setItem(
STORED_CREDENTIALS_KEY,
JSON.stringify(selectAuth(getState() as RootState))
);
}
},
});
credentialStorage.startListening({
actionCreator: logout,
effect: () => {
if (window && window.localStorage) {
localStorage.removeItem(STORED_CREDENTIALS_KEY);
}
},
});

// Auth API
export const authApi = createApi({
reducerPath: "authApi",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,35 +18,11 @@ import { AddConnectionStep } from "./types";
const AddConnection: React.FC = () => {
const dispatch = useDispatch();
const router = useRouter();
const { connectorType, key, step: currentStep } = router.query;
const { connectorType, step: currentStep } = router.query;

const { connectionOption, step } = useAppSelector(selectConnectionTypeState);

/**
* NOTE: If the user reloads the web page via F5, the react redux store state is lost.
Copy link
Contributor Author

@chriscalhoun1974 chriscalhoun1974 Sep 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The following code snippet was no longer needed given the Redux store is persisted in localStorage.

* By default its persisted in internal memory. As a result, a runtime exception occurs
* which impedes the page rendering.
*
* @example
* The above error occurred in the <AddConnection> component
*
* For now, a temporary solution is to redirect the user
* to the "Choose your connection" step. This allows a better overall user experience.
* A permanent solution will be to persist the react redux store state to either local storage
* or session storage. Once completed, this method can be deleted.
*/
const reload = useCallback(() => {
if (
key &&
currentStep &&
(currentStep as unknown as number) !== step?.stepId
) {
window.location.href = STEPS[1].href;
}
}, [currentStep, key, step?.stepId]);

useEffect(() => {
reload();
if (connectorType) {
dispatch(setConnectionOption(JSON.parse(connectorType as string)));
}
Expand All @@ -55,19 +31,7 @@ const AddConnection: React.FC = () => {
dispatch(setStep(item || STEPS[1]));
}
return () => {};
}, [connectorType, currentStep, dispatch, reload, router.query.step]);

const getComponent = useCallback(() => {
switch (step.stepId) {
case 1:
return <ChooseConnection />;
case 2:
case 3:
return <ConfigureConnector />;
default:
return <ChooseConnection />;
}
}, [step.stepId]);
}, [connectorType, currentStep, dispatch, router.query.step]);

const getLabel = useCallback(
(s: AddConnectionStep): string => {
Expand Down Expand Up @@ -108,7 +72,17 @@ const AddConnection: React.FC = () => {
{!connectionOption && <Text>{getLabel(step)}</Text>}
</Box>
</Heading>
{getComponent()}
{(() => {
switch (step.stepId) {
case 1:
return <ChooseConnection />;
case 2:
case 3:
return <ConfigureConnector />;
default:
return <ChooseConnection />;
}
})()}
</>
);
};
Expand Down
Loading