diff --git a/ui/App.tsx b/ui/App.tsx index a0950fcf4b..e8a7632513 100644 --- a/ui/App.tsx +++ b/ui/App.tsx @@ -14,6 +14,7 @@ import ErrorBoundary from "./components/ErrorBoundary"; import Layout from "./components/Layout"; import AppContextProvider from "./contexts/AppContext"; import AuthContextProvider, { AuthCheck } from "./contexts/AuthContext"; +import FeatureFlagsContextProvider from "./contexts/FeatureFlags"; import { Applications as appsClient, GitProvider, @@ -86,33 +87,13 @@ const App = () => ( ); export default function AppContainer() { - const [authFlag, setAuthFlag] = React.useState(null); - - const getAuthFlag = React.useCallback(() => { - fetch("/v1/featureflags") - .then((response) => response.json()) - .then((data) => - setAuthFlag(data.flags.WEAVE_GITOPS_AUTH_ENABLED === "true") - ) - .catch((err) => console.log(err)); - }, []); - - React.useEffect(() => { - getAuthFlag(); - }, [getAuthFlag]); - - // Loading... - if (authFlag === null) { - return null; - } - return ( - {authFlag ? ( + {/* does not use the base page so pull it up here */} @@ -125,9 +106,7 @@ export default function AppContainer() { - ) : ( - - )} + diff --git a/ui/components/Icon.tsx b/ui/components/Icon.tsx index 03b9a6677f..5f279f3cf2 100644 --- a/ui/components/Icon.tsx +++ b/ui/components/Icon.tsx @@ -12,6 +12,7 @@ import RemoveCircleIcon from "@material-ui/icons/RemoveCircle"; import SaveAltIcon from "@material-ui/icons/SaveAlt"; import SkipNextIcon from "@material-ui/icons/SkipNext"; import SkipPreviousIcon from "@material-ui/icons/SkipPrevious"; +import LogoutIcon from "@material-ui/icons/ExitToApp"; import * as React from "react"; import styled from "styled-components"; import { colors, spacing } from "../typedefs/styled"; @@ -34,6 +35,7 @@ export enum IconType { SkipNextIcon, SkipPreviousIcon, RemoveCircleIcon, + LogoutIcon, } type Props = { @@ -91,6 +93,9 @@ function getIcon(i: IconType) { case IconType.RemoveCircleIcon: return RemoveCircleIcon; + case IconType.LogoutIcon: + return LogoutIcon; + default: break; } diff --git a/ui/components/Layout.tsx b/ui/components/Layout.tsx index 5a1f07c824..344b332d92 100644 --- a/ui/components/Layout.tsx +++ b/ui/components/Layout.tsx @@ -5,9 +5,11 @@ import styled from "styled-components"; import useNavigation from "../hooks/navigation"; import { PageRoute } from "../lib/types"; import { formatURL, getNavValue } from "../lib/utils"; +import { FeatureFlags } from "../contexts/FeatureFlags"; import Flex from "./Flex"; import Link from "./Link"; import Logo from "./Logo"; +import UserSettings from "./UserSettings"; type Props = { className?: string; @@ -94,12 +96,8 @@ const TopToolBar = styled(Flex)` } `; -//style for account icon - disabled while no account functionality exists -// const UserAvatar = styled(Icon)` -// padding-right: ${(props) => props.theme.spacing.medium}; -// `; - function Layout({ className, children }: Props) { + const { authFlag } = React.useContext(FeatureFlags); const { currentPage } = useNavigation(); return ( @@ -107,7 +105,7 @@ function Layout({ className, children }: Props) { - {/* code for account icon - disabled while no account functionality exists */} + {authFlag ? : null}
diff --git a/ui/components/UserSettings.tsx b/ui/components/UserSettings.tsx new file mode 100644 index 0000000000..13f1f6bcd1 --- /dev/null +++ b/ui/components/UserSettings.tsx @@ -0,0 +1,68 @@ +import { + IconButton, + ListItemIcon, + Menu, + MenuItem, + Tooltip, +} from "@material-ui/core"; +import * as React from "react"; +import styled from "styled-components"; +import { Auth } from "../contexts/AuthContext"; +import Icon, { IconType } from "./Icon"; + +const UserAvatar = styled(Icon)` + padding-right: ${(props) => props.theme.spacing.medium}; +`; + +const SettingsMenu = styled(Menu)` + .MuiListItemIcon-root { + min-width: 25px; + color: ${(props) => props.theme.colors.black}; + } +`; + +function UserSettings() { + const [anchorEl, setAnchorEl] = React.useState(null); + const { userInfo, logOut } = React.useContext(Auth); + + const open = Boolean(anchorEl); + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + + return ( + <> + + + + + + + Hello, {userInfo?.email} + logOut()}> + + + + Logout + + + + ); +} + +export default styled(UserSettings)``; diff --git a/ui/contexts/AuthContext.tsx b/ui/contexts/AuthContext.tsx index f19f37403e..e06619df1c 100644 --- a/ui/contexts/AuthContext.tsx +++ b/ui/contexts/AuthContext.tsx @@ -2,12 +2,21 @@ import * as React from "react"; import { useHistory, Redirect } from "react-router-dom"; import Layout from "../components/Layout"; import LoadingPage from "../components/LoadingPage"; +import { FeatureFlags } from "../contexts/FeatureFlags"; const USER_INFO = "/oauth2/userinfo"; const SIGN_IN = "/oauth2/sign_in"; +const LOG_OUT = "/oauth2/logout"; const AUTH_PATH_SIGNIN = "/sign_in"; export const AuthCheck = ({ children }) => { + // If the auth flag is null go straight to rendering the children + const { authFlag } = React.useContext(FeatureFlags); + + if (!authFlag) { + return children; + } + const { loading, userInfo } = React.useContext(Auth); // Wait until userInfo is loaded before showing signin or app content @@ -36,15 +45,18 @@ export type AuthContext = { }; error: { status: number; statusText: string }; loading: boolean; + logOut: () => void; }; export const Auth = React.createContext(null); export default function AuthContextProvider({ children }) { - const [userInfo, setUserInfo] = React.useState<{ - email: string; - groups: string[]; - }>(null); + const { authFlag } = React.useContext(FeatureFlags); + const [userInfo, setUserInfo] = + React.useState<{ + email: string; + groups: string[]; + }>(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const history = useHistory(); @@ -80,21 +92,46 @@ export default function AuthContextProvider({ children }) { .finally(() => setLoading(false)); }, []); + const logOut = React.useCallback(() => { + setLoading(true); + fetch(LOG_OUT, { + method: "POST", + }) + .then((response) => { + if (response.status !== 200) { + setError(response); + return; + } + history.push("/sign_in"); + }) + .finally(() => setLoading(false)); + }, []); + React.useEffect(() => { + if (!authFlag) { + return null; + } getUserInfo(); return history.listen(getUserInfo); }, [getUserInfo, history]); return ( - - {children} - + <> + {authFlag ? ( + + {children} + + ) : ( + children + )} + ); } diff --git a/ui/contexts/FeatureFlags.tsx b/ui/contexts/FeatureFlags.tsx new file mode 100644 index 0000000000..c90506835e --- /dev/null +++ b/ui/contexts/FeatureFlags.tsx @@ -0,0 +1,40 @@ +import * as React from "react"; + +export type FeatureFlagsContext = { + authFlag: boolean | null; +}; + +export const FeatureFlags = + React.createContext(null); + +export default function FeatureFlagsContextProvider({ children }) { + const [authFlag, setAuthFlag] = React.useState(null); + + const getAuthFlag = React.useCallback(() => { + fetch("/v1/featureflags") + .then((response) => response.json()) + .then((data) => + setAuthFlag(data.flags.WEAVE_GITOPS_AUTH_ENABLED === "true") + ) + .catch((err) => console.log(err)); + }, []); + + React.useEffect(() => { + getAuthFlag(); + }, [getAuthFlag]); + + // Loading... + if (authFlag === null) { + return null; + } + + return ( + + {children} + + ); +} diff --git a/ui/index.ts b/ui/index.ts index f74da8a626..c06407697b 100644 --- a/ui/index.ts +++ b/ui/index.ts @@ -3,9 +3,13 @@ import Footer from "./components/Footer"; import GithubDeviceAuthModal from "./components/GithubDeviceAuthModal"; import LoadingPage from "./components/LoadingPage"; import RepoInputWithAuth from "./components/RepoInputWithAuth"; +import UserSettings from "./components/UserSettings"; import Icon, { IconType } from "./components/Icon"; import AppContextProvider from "./contexts/AppContext"; import AuthContextProvider from "./contexts/AuthContext"; +import FeatureFlagsContextProvider, { + FeatureFlags, +} from "./contexts/FeatureFlags"; import CallbackStateContextProvider from "./contexts/CallbackStateContext"; import useApplications from "./hooks/applications"; import { Applications as applicationsClient } from "./lib/api/applications/applications.pb"; @@ -21,6 +25,8 @@ import Applications from "./pages/Applications"; import OAuthCallback from "./pages/OAuthCallback"; export { + FeatureFlagsContextProvider, + FeatureFlags, AuthContextProvider, AppContextProvider, ApplicationAdd, @@ -42,4 +48,5 @@ export { Button, Icon, IconType, + UserSettings, };