diff --git a/compendium-frontend/.eslintrc.json b/compendium-frontend/.eslintrc.json index 5378a5f95373ba29c74ce7f948a0857966e5532d..32eef4da5b24f272bf177836d92c30c20cbca666 100644 --- a/compendium-frontend/.eslintrc.json +++ b/compendium-frontend/.eslintrc.json @@ -19,7 +19,8 @@ "react/prop-types": "off", "@typescript-eslint/no-unused-vars": [ "warn", { "argsIgnorePattern": "^_" } - ] + ], + "@typescript-eslint/no-empty-function": "off" }, "settings": { "react": { diff --git a/compendium-frontend/src/App.tsx b/compendium-frontend/src/App.tsx index 246535e553e3983bc85276f1ebb09281a7500ba8..8c8fbe33d95e382fe5b1e8ad93b7967553e0f201 100644 --- a/compendium-frontend/src/App.tsx +++ b/compendium-frontend/src/App.tsx @@ -12,7 +12,7 @@ import { FilterSelection } from "./Schema"; import SubOrganisation from "./pages/SubOrganisation"; import ParentOrganisation from "./pages/ParentOrganisation"; import ECProjects from "./pages/ECProjects"; -import SidebarProvider from "./helpers/SidebarProvider"; +import Providers from "./Providers"; import PolicyPage from "./pages/Policy"; @@ -25,8 +25,8 @@ function App(): ReactElement { return ( <div className="app"> <Router> - <ExternalPageNavBar /> - <SidebarProvider> + <Providers> + <ExternalPageNavBar /> <Routes> <Route path="/budget" element={<BudgetPage filterSelection={filterSelection} setFilterSelection={setFilterSelection} />} /> <Route path="/funding" element={<FundingSourcePage filterSelection={filterSelection} setFilterSelection={setFilterSelection} />} /> @@ -40,7 +40,8 @@ function App(): ReactElement { <Route path="/data" element={<CompendiumData />} /> <Route path="*" element={<Landing />} /> </Routes> - </SidebarProvider> + </Providers> + <GeantFooter /> </Router> diff --git a/compendium-frontend/src/Providers.tsx b/compendium-frontend/src/Providers.tsx new file mode 100644 index 0000000000000000000000000000000000000000..072f5cededfd9728fb7af0b97ea60b30d59e5bc8 --- /dev/null +++ b/compendium-frontend/src/Providers.tsx @@ -0,0 +1,17 @@ +import React, { ReactElement } from "react"; + +import SidebarProvider from "./helpers/SidebarProvider"; +import UserProvider from "./shared/UserProvider"; + + +function Providers({ children }): ReactElement { + return ( + <SidebarProvider> + <UserProvider> + {children} + </UserProvider> + </SidebarProvider> + ); +} + +export default Providers; diff --git a/compendium-frontend/src/components/global/ExternalPageNavBar.tsx b/compendium-frontend/src/components/global/ExternalPageNavBar.tsx index 701d7f10813f0aa289c6eaf9a9f295c32a64c73f..bb2ac21e3fd13657665c3016ff103f321c8f649a 100644 --- a/compendium-frontend/src/components/global/ExternalPageNavBar.tsx +++ b/compendium-frontend/src/components/global/ExternalPageNavBar.tsx @@ -1,5 +1,6 @@ import React, { ReactElement } from "react"; -import { Container, Row } from "react-bootstrap"; +import { Col, Container, Row } from "react-bootstrap"; +import Login from "../../shared/Login"; import GeantLogo from "../../images/geant_logo_f2020_new.svg"; /** @@ -14,26 +15,36 @@ function ExternalPageNavBar(): ReactElement { <Container> <Row> - <nav> - <a href="https://geant.org/"><img src={GeantLogo} /></a> + <Col xs={10}> + <div className="nav-wrapper"> + <nav className="header-nav"> + <a href="https://geant.org/"><img src={GeantLogo} /></a> - <ul> - <li><a href="https://network.geant.org/">NETWORK</a></li> - <li><a href="https://geant.org/services/">SERVICES</a></li> - <li><a href="https://community.geant.org/">COMMUNITY</a></li> - <li><a href="https://tnc23.geant.org/">TNC</a></li> - <li><a href="https://geant.org/projects/">PROJECTS</a></li> - <li><a href="https://connect.geant.org/">CONNECT</a></li> - <li><a href="https://impact.geant.org/">IMPACT</a></li> - <li><a href="https://careers.geant.org/">CAREERS</a></li> - <li><a href="https://about.geant.org/">ABOUT</a></li> - <li><a href="https://connect.geant.org/community-news">NEWS</a></li> - <li><a href="https://resources.geant.org/">RESOURCES</a></li> - </ul> - </nav> + <ul> + <li><a className="nav-link-entry" href="https://network.geant.org/">NETWORK</a></li> + <li><a className="nav-link-entry" href="https://geant.org/services/">SERVICES</a></li> + <li><a className="nav-link-entry" href="https://community.geant.org/">COMMUNITY</a></li> + <li><a className="nav-link-entry" href="https://tnc23.geant.org/">TNC</a></li> + <li><a className="nav-link-entry" href="https://geant.org/projects/">PROJECTS</a></li> + <li><a className="nav-link-entry" href="https://connect.geant.org/">CONNECT</a></li> + <li><a className="nav-link-entry" href="https://impact.geant.org/">IMPACT</a></li> + <li><a className="nav-link-entry" href="https://careers.geant.org/">CAREERS</a></li> + <li><a className="nav-link-entry" href="https://about.geant.org/">ABOUT</a></li> + <li><a className="nav-link-entry" href="https://connect.geant.org/community-news">NEWS</a></li> + <li><a className="nav-link-entry" href="https://resources.geant.org/">RESOURCES</a></li> + </ul> + + </nav> + </div> + + </Col> + + <Col align="right"> + <Login /> + </Col> </Row> </Container> - </div> + </div > ); } diff --git a/compendium-frontend/src/helpers/SidebarProvider.tsx b/compendium-frontend/src/helpers/SidebarProvider.tsx index fdf631d109931be26d3083a274ef4a33928aa6d0..02302f16b61cd9d7cbfef9699da0ae03e9832b49 100644 --- a/compendium-frontend/src/helpers/SidebarProvider.tsx +++ b/compendium-frontend/src/helpers/SidebarProvider.tsx @@ -6,7 +6,7 @@ interface Props { const sidebarContext = createContext<{ show: boolean; - toggle: React.Dispatch<any>; + toggle: () => void; }>({ show: false, toggle: () => { } diff --git a/compendium-frontend/src/main.scss b/compendium-frontend/src/main.scss index 7df1e65b66bbd1cb494a9a59e1612a6e5916f477..dff9a81ce01d7523498dd0c2d67f4e2ba283feeb 100644 --- a/compendium-frontend/src/main.scss +++ b/compendium-frontend/src/main.scss @@ -2,13 +2,49 @@ @import 'scss/base/text'; @import 'scss/layout/components'; -.external-page-nav-bar { - background-color: #003753; +.nav-link-entry { + border-radius: 2px; + font-family: "Open Sans", sans-serif; + font-size: 0.9rem; + font-weight: 600; + text-decoration: none; color: #b0cde1; + padding: 10px; +} + +.nav-link { + display: flex; + -webkit-box-align: center; + align-items: center; height: 60px; + + .nav-link-entry:hover { + color: #003753; + background-color: #b0cde1; + } + + ul { + line-height: 1.3; + text-transform: uppercase; + list-style: none; + + li { + float: left; + + + } + } +} + +.nav-wrapper { display: flex; -webkit-box-align: center; align-items: center; + height: 60px; +} + +.header-nav { + width: 100%; img { float: left; @@ -42,8 +78,14 @@ } } +.external-page-nav-bar { + background-color: #003753; + color: #b0cde1; + height: 60px; +} + .app { - display: flex; + // display: flex; flex-direction: column; min-height: 100vh; -} +} \ No newline at end of file diff --git a/compendium-frontend/src/scss/layout/Login.scss b/compendium-frontend/src/scss/layout/Login.scss new file mode 100644 index 0000000000000000000000000000000000000000..0d2dcb0c77a94003bb79d97aa8dd155276a6b238 --- /dev/null +++ b/compendium-frontend/src/scss/layout/Login.scss @@ -0,0 +1,24 @@ +.btn-login { + --bs-btn-color: #fff; + --bs-btn-border-color: #6c757d; + --bs-btn-border-radius: none; + + // active + --bs-btn-active-color: #fff; + // see https://stackoverflow.com/a/76213205 for why this syntax is necessary + --bs-btn-active-bg: #{$yellow-orange}; + --bs-btn-active-border-color: #{$yellow-orange}; + + // hover + --bs-btn-hover-color: rgb(0, 63, 95); + --bs-btn-hover-bg: #{$yellow-orange}; + --bs-btn-hover-border-color: #{$yellow-orange}; + + + // disabled + --bs-btn-disabled-color: #6c757d; + --bs-btn-disabled-bg: transparent; + // --bs-btn-disabled-border-color: #6c757d; + + border: 2px solid $yellow-orange; +} \ No newline at end of file diff --git a/compendium-frontend/src/scss/layout/_components.scss b/compendium-frontend/src/scss/layout/_components.scss index 9c3178d2f4ba37d2e411c0abc6d7c73386117c2e..cfd1ad3f56ed828de6bbe6c84c9389492cc46e5d 100644 --- a/compendium-frontend/src/scss/layout/_components.scss +++ b/compendium-frontend/src/scss/layout/_components.scss @@ -3,12 +3,17 @@ @import '../abstracts/variables'; @import '../layout/Sidebar'; @import '../layout/SectionNavigation'; +@import '../layout/Login'; .rounded-border { border-radius: 25px; border: 1px solid $light-ash-grey } +.card { + --bs-card-border-color: ""; +} + .grow { display: flex; flex-direction: column; @@ -288,4 +293,5 @@ $funding-source-colors: ( .color-of-badge-blank { background-color: rgb(0, 0, 0, 0); -} \ No newline at end of file +} + diff --git a/compendium-frontend/src/shared/Login.tsx b/compendium-frontend/src/shared/Login.tsx new file mode 100644 index 0000000000000000000000000000000000000000..baa752002a442e9d3aad8159e67e6ebec3449a57 --- /dev/null +++ b/compendium-frontend/src/shared/Login.tsx @@ -0,0 +1,23 @@ +import React, { useContext, useEffect } from 'react'; +import { Button } from 'react-bootstrap'; +import { userContext } from './UserProvider'; + + +const Login = () => { + const { user, logout } = useContext(userContext); + + if (!user.name) { + return ( + <a href='/login'><Button variant='login' active={false} style={{ maxWidth: '6rem' }}><span>Login</span></Button></a> + ) + } + + if (user.permissions.active) { + return <div className='nav-link' style={{ float: 'right' }}><a className='nav-link-entry' href="/survey">Go to Survey</a></div> + } + return <div className='nav-link' style={{ float: 'right' }}> + <span>{'<'}{user.name}{'>'}</span> + </div> +} + +export default Login \ No newline at end of file diff --git a/compendium-frontend/src/shared/UserProvider.tsx b/compendium-frontend/src/shared/UserProvider.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ff3a6e64c1c70df433d5b917c1f8bf865690e02a --- /dev/null +++ b/compendium-frontend/src/shared/UserProvider.tsx @@ -0,0 +1,55 @@ +import React, { createContext, useState, useEffect } from 'react'; + +interface Props { + children: React.ReactNode; +} + +interface User { + name: string, + email?: string, + permissions: { + admin: boolean, + active: boolean, + } +} + +async function fetchUser(): Promise<User> { + const response = await fetch('/api/user'); + const user = await response.json(); + return user +} + +const anonymousUser: User = { 'name': '', email: '', permissions: { admin: false, active: false } }; + +const userContext = createContext<{ + user: User; + logout: () => void; +}>({ + user: anonymousUser, + logout: () => { } +}); + + +const UserProvider: React.FC<Props> = ({ children }) => { + const [user, setUser] = useState<User>(anonymousUser); + + async function logoutUser() { + await fetch('/logout'); + setUser(anonymousUser); + } + + useEffect(() => { + fetchUser().then(user => { + setUser(user) + }); + }, []); + + return ( + <userContext.Provider value={{ user, logout: logoutUser }}> + {children} + </userContext.Provider> + ); +}; + +export { userContext }; +export default UserProvider; \ No newline at end of file diff --git a/compendium-frontend/webpack.config.ts b/compendium-frontend/webpack.config.ts index 4658af21429cbfd95b202692531d8336ae075405..73cfd59a07fc374ff23f10dafdd3924d1fffdb9a 100644 --- a/compendium-frontend/webpack.config.ts +++ b/compendium-frontend/webpack.config.ts @@ -85,6 +85,10 @@ const config: Configuration = { historyApiFallback: true, proxy: { "/api": "http://127.0.0.1:5000", + "/login": "http://127.0.0.1:5000", + "/logout": "http://127.0.0.1:5000", + "/authorize": "http://127.0.0.1:5000", + "/survey/*": "http://127.0.0.1:4001" }, }, plugins: [ diff --git a/compendium_v2/routes/authentication.py b/compendium_v2/routes/authentication.py index 1c50f619c5af2f73436d8a7d3f33829b04831231..f66d26bc428b266048032efbbc0d8acfc294911e 100644 --- a/compendium_v2/routes/authentication.py +++ b/compendium_v2/routes/authentication.py @@ -32,8 +32,8 @@ def authorize(): user = create_user(profile['email'], profile['name'], profile['sub']) login_user(user) - # redirect to /survey since we only require login for this part of the app - return redirect(url_for('compendium-v2-default.survey_index')) + # redirect to / + return redirect(url_for('compendium-v2-default.index')) @routes.route("/logout") diff --git a/compendium_v2/routes/user.py b/compendium_v2/routes/user.py index 00e0886acfe2f855dbe2dc8a2f1dd46726864a55..96dca333afa6fb0b873a1b4a4ca4fabf51dd5c38 100644 --- a/compendium_v2/routes/user.py +++ b/compendium_v2/routes/user.py @@ -6,7 +6,7 @@ from flask_login import current_user, AnonymousUserMixin # type: ignore from sqlalchemy import select from compendium_v2.db import db -from compendium_v2.db.auth_model import User +from compendium_v2.db.auth_model import User, ROLES from compendium_v2.routes import common @@ -17,13 +17,23 @@ USER_RESPONSE_SCHEMA = { '$schema': 'http://json-schema.org/draft-07/schema#', 'definitions': { + 'permissions': { + 'type': 'object', + 'properties': { + 'admin': {'type': 'boolean'}, + 'active': {'type': 'boolean'}, + }, + 'required': ['admin', 'active'], + 'additionalProperties': False + }, 'user': { 'type': 'object', 'properties': { 'name': {'type': 'string'}, 'email': {'type': ['string', 'null']}, + 'permissions': {'$ref': '#/definitions/permissions'}, }, - 'required': ['name'], + 'required': ['name', 'email', 'permissions'], 'additionalProperties': False } }, @@ -50,10 +60,20 @@ def current_user_view() -> Any: def _extract_data(entry: Union[User, AnonymousUserMixin]): if isinstance(entry, AnonymousUserMixin): return { - 'name': 'Anonymous User', + 'name': '', + 'email': None, + 'permissions': { + 'admin': False, + 'active': False, + } } return { 'name': entry.fullname, + 'email': entry.email, + 'permissions': { + 'admin': entry.roles == ROLES.admin, + 'active': entry.active, + } } return jsonify(_extract_data(current_user)) diff --git a/survey-frontend/.eslintrc.json b/survey-frontend/.eslintrc.json index 5378a5f95373ba29c74ce7f948a0857966e5532d..32eef4da5b24f272bf177836d92c30c20cbca666 100644 --- a/survey-frontend/.eslintrc.json +++ b/survey-frontend/.eslintrc.json @@ -19,7 +19,8 @@ "react/prop-types": "off", "@typescript-eslint/no-unused-vars": [ "warn", { "argsIgnorePattern": "^_" } - ] + ], + "@typescript-eslint/no-empty-function": "off" }, "settings": { "react": { diff --git a/survey-frontend/tsconfig.json b/survey-frontend/tsconfig.json index fdc02371afd46c55f86912c148f4b0dab8826f53..ff50d2d9d9d82dd8bb59fbd9f0115816f4d25775 100644 --- a/survey-frontend/tsconfig.json +++ b/survey-frontend/tsconfig.json @@ -16,6 +16,9 @@ "declaration": true, "declarationDir": "dist/types", "noImplicitAny": false, + "paths": { + "shared/*": ["../compendium-frontend/src/shared/*"] + } }, "include": ["src"] } diff --git a/survey-frontend/webpack.config.ts b/survey-frontend/webpack.config.ts index a00d00c923f200cc274791dfb08632935032d66f..c0a0f579118fc4c8370e4ffd2f61ef3d431f2e31 100644 --- a/survey-frontend/webpack.config.ts +++ b/survey-frontend/webpack.config.ts @@ -72,6 +72,9 @@ const config: Configuration = { }, resolve: { extensions: [".tsx", ".ts", ".js", ".html"], + alias: { + shared: path.resolve(__dirname, '../compendium-frontend/src/shared/'), + }, }, output: { path: path.resolve(__dirname, "..", "compendium_v2", "static"),