Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
7 changes: 7 additions & 0 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { WSOToken } from "../lib/types";
// More component imports
const Scheduler = lazy(() => import("./views/CourseScheduler/Scheduler"));
const About = lazy(() => import("./views/Misc/About"));
const Dashboard = lazy(() => import("./views/AdminTools/dashboard"));

const FAQ = lazy(() => import("./views/Misc/FAQ"));
const Team = lazy(() => import("./views/Misc/Team"));
const MobilePrivacyPolicy = lazy(
Expand Down Expand Up @@ -194,15 +196,20 @@ const App = () => {
</RequireScope>
}
/>

<Route path="schedulecourses" element={<Scheduler />} />
{/* Static Content Pages */}

<Route path="about" element={<About />} />
<Route path="faq" element={<FAQ />} />
<Route path="team" element={<Team />} />
<Route path="admin" element={<Dashboard />} />

<Route
path="mobile-privacy-policy"
element={<MobilePrivacyPolicy />}
/>

<Route path="login" element={<Login />} />
{/* Error-handling Pages */}
<Route path="403" element={<Error403 />} />
Expand Down
64 changes: 56 additions & 8 deletions src/components/Nav.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// React Imports
import React, { useEffect, useState } from "react";

import "./stylesheets/Nav.css";

// Redux imports
import { getWSO, getCurrUser } from "../lib/authSlice";
import { removeCredentials } from "../lib/authSlice";
Expand All @@ -11,6 +13,30 @@ import { Link } from "react-router-dom";
import history from "../lib/history";
import { userTypeStudent } from "../constants/general";

// Feature flag imports
import { useSelector } from "react-redux";
import { RootState } from "../reducers/index";

interface FeatureFlagElementProps {
element: React.ReactElement;
flag: keyof RootState["featureFlagState"];
}

const FeatureFlagElement: React.FC<FeatureFlagElementProps> = ({
element,
flag,
}) => {
const enabled = useSelector(
(state: RootState) => state.featureFlagState[flag]
);

return (
<div className="nav_feature_div">
{enabled ? <li className="nav_feature_item">{element}</li> : null}
</div>
);
};

const Nav = () => {
const dispatch = useAppDispatch();
const currUser = useAppSelector(getCurrUser);
Expand Down Expand Up @@ -111,17 +137,39 @@ const Nav = () => {
</>
)}

<FeatureFlagElement
element={<Link to="faq">FAQ</Link>}
flag="enableFAQ"
/>
<FeatureFlagElement
element={<a href="/wiki/">Wiki</a>}
flag="enableWiki"
/>
<FeatureFlagElement
element={<Link to="about">About</Link>}
flag="enableAbout"
/>

{/* {featureFlagElement(
<a href="/wiki/">Wiki</a>,
"enableWiki",


// getFeatureFlag(currentState, "enableWiki")
)}

{featureFlagElement(
<Link to="about">About</Link>,
"enableAbout",


// getFeatureFlag(currentState, "enableAbout")
)} */}
<li>
<Link to="faq">FAQ</Link>
</li>
<li>
<a href="/wiki/">Wiki</a>
</li>
<li>
<Link to="about">About</Link>
<Link to="schedulecourses">Course Scheduler</Link>
</li>
<li>
<Link to="schedulecourses">Course Scheduler</Link>
<Link to="admin">Admin Dashboard</Link>
</li>
{ephmatchVisibility > 0 && (
<li>
Expand Down
7 changes: 7 additions & 0 deletions src/components/stylesheets/Nav.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.nav_feature_div {
display: inline-block;
}

.nav_feature_div:not(:empty) {
padding-right: 2em;
}
38 changes: 38 additions & 0 deletions src/components/views/AdminTools/dashboard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// React imports
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { toggleFeatureFlag } from "../../../lib/featureFlagSlice";

const Dashboard = () => {
const dispatch = useDispatch();
const featureFlagState = useSelector((state) => state.featureFlagState);

const handleToggle = (flag) => {
dispatch(toggleFeatureFlag(flag));
};

return (
<div>
<p>dashboard</p>
{Object.entries(featureFlagState).map(([key, value]) =>
key[0] === "_" ? null : (
<button key={key} onClick={() => handleToggle(key)}>
{value
? "Toggle " + key.slice(6, key.length) + " Off"
: "Toggle " + key.slice(6, key.length) + " On"}
</button>
)
)}
</div>
);
};

// return (
// <div>
// <p>Enable About: {String(enableAbout)}</p>
// <button onClick={handleToggleAbout}>Toggle Enable About</button>
// </div>
// );
// };

export default Dashboard;
14 changes: 13 additions & 1 deletion src/components/views/Misc/About.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
import React from "react";
import DoughtyBanner from "../../../assets/images/banners/Doughty.jpg";
import { Link } from "react-router-dom";
import { useSelector } from "react-redux";

const About = () => {
const AboutPreRelease = () => {
return (
<div className="article">
<div className="about-banner">
Expand Down Expand Up @@ -118,4 +119,15 @@ const About = () => {
);
};

const About = () => {
const enableAbout = useSelector(
(state) => state.featureFlagState["enableAbout"]
);

return (
<div>
{enableAbout ? <AboutPreRelease /> : <p>Feature not available</p>}
</div>
);
};
export default About;
11 changes: 10 additions & 1 deletion src/components/views/Misc/FAQ.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// React imports
import React from "react";
import { useSelector } from "react-redux";

const FAQ = () => {
const FAQPreRelease = () => {
return (
<div className="article">
<section>
Expand Down Expand Up @@ -127,4 +128,12 @@ const FAQ = () => {
);
};

const FAQ = () => {
const enableFAQ = useSelector((state) => state.featureFlagState["enableFAQ"]);

return (
<div>{enableFAQ ? <FAQPreRelease /> : <p>Feature not available</p>}</div>
);
};

export default FAQ;
41 changes: 41 additions & 0 deletions src/lib/featureFlagSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";

// const API_ADDRESS = "http://localhost:8080";

interface FeatureFlagsState {
enableAbout: boolean;
enableWiki: boolean;
enableFAQ: boolean;

// Add more feature flags as needed
}

export const initialState: FeatureFlagsState = {
enableAbout: false,
enableWiki: false,
enableFAQ: false,

// Initialize other feature flags
};

const featureFlagSlice = createSlice({
name: "featureFlags",
initialState,
reducers: {
toggleFeatureFlag: (
state,
action: PayloadAction<keyof FeatureFlagsState>
) => {
const flagKey = action.payload;
if (flagKey in state) {
state[flagKey] = !state[flagKey];
}
},
Copy link
Member

Choose a reason for hiding this comment

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

I'm a little worried about toggling, because it is not idempotent (and also because redux is async).
Maybe the action should take in both flagKey and newState?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't understand the idea here, the values can only be "on" or "off", why does toggling not make sense?

},
});

export const { toggleFeatureFlag } = featureFlagSlice.actions;

// export const getFeatureFlag = (state: FeatureFlagsState, flag: keyof FeatureFlagsState) => featureFlagsState[flag];

export default featureFlagSlice.reducer;
13 changes: 12 additions & 1 deletion src/lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import {
} from "redux-persist";
import storage from "redux-persist/lib/storage";

import { courseReducer, schedulerUtilReducer, authReducer } from "../reducers";
import {
courseReducer,
schedulerUtilReducer,
authReducer,
featureFlagReducer,
} from "../reducers";

const persistedAuthReducer = persistReducer(
{ key: "auth", storage, whitelist: ["identityToken"] },
Expand All @@ -24,11 +29,17 @@ const persistedCourseSchedulerReducer = persistReducer(
schedulerUtilReducer
);

const persistedFeatureFlagReducer = persistReducer(
{ key: "featureFlags", storage },
featureFlagReducer
);

const store = configureStore({
reducer: {
courseState: courseReducer,
schedulerUtilState: persistedCourseSchedulerReducer,
authState: persistedAuthReducer,
featureFlagState: persistedFeatureFlagReducer,
},
// this is to disable React Toolkit's error message "A non-serializable value was detected in the state"
// TODO: stop using non-serializable object `authState.wso` and `schedulerUtilState.gapi`
Expand Down
19 changes: 18 additions & 1 deletion src/reducers/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
import courseReducer from "./course";
import schedulerUtilReducer from "./schedulerUtils";
import authReducer from "../lib/authSlice";
import featureFlagReducer from "../lib/featureFlagSlice";

import goodrichReducer from "./goodrich";

const rootReducer = {
courseState: courseReducer,
schedulerUtilState: schedulerUtilReducer,
authState: authReducer,
goodrichState: goodrichReducer,
featureFlagState: featureFlagReducer,
};

export interface RootState {
courseState: ReturnType<typeof courseReducer>;
schedulerUtilState: ReturnType<typeof schedulerUtilReducer>;
authState: ReturnType<typeof authReducer>;
goodrichState: ReturnType<typeof goodrichReducer>;
featureFlagState: ReturnType<typeof featureFlagReducer>;
}

export default rootReducer;
export { courseReducer, schedulerUtilReducer, authReducer, goodrichReducer };
export {
courseReducer,
schedulerUtilReducer,
authReducer,
goodrichReducer,
featureFlagReducer,
};