diff options
| author | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2022-04-06 15:35:09 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-04-06 15:35:09 +0200 |
| commit | 0d4f19003324e196fffae3f252786e30197dfa4c (patch) | |
| tree | 08ba4eb3b0e067ae933c636ca3967c45058fd69f /opendc-web/opendc-web-ui/src | |
| parent | af87540d49d58c465f0847c016814d58cfeb44fc (diff) | |
| parent | 9f0b7ddd0d62e4dc43a69ea8fafd06be1a663f9f (diff) | |
merge: Integrate UI into Quarkus web application (#69)
This pull request adds a Quarkus extension that integrates with the existing Quarkus web application to provide access to the OpenDC web UI. The benefit of this approach is that in this way, OpenDC can be distributed as a single JVM application to the user, which can host its own web UI directly.
Furthermore, this pull request also adds support for unauthenticated access to the API when accessing in dev mode, so that users do not have to setup Auth0 in order to use OpenDC locally.
## Implementation Notes :hammer_and_pick:
* Do not use next/image
* Migrate to next-global-css
* Update PatternFly to latest version
* Add Gradle integration with Next.js project
* Build web UI via Gradle
* Support building WebJar for OpenDC web UI
* Add extension for serving OpenDC web UI
* Include web UI in development mode
* Add workaround for Quarkus Gradle build issues
* Add support for unauthenticated user access
## External Dependencies :four_leaf_clover:
* [node-gradle](https://github.com/node-gradle/gradle-node-plugin)
Diffstat (limited to 'opendc-web/opendc-web-ui/src')
| -rw-r--r-- | opendc-web/opendc-web-ui/src/api/index.js | 17 | ||||
| -rw-r--r-- | opendc-web/opendc-web-ui/src/auth.js | 42 | ||||
| -rw-r--r-- | opendc-web/opendc-web-ui/src/components/AppHeader.js | 4 | ||||
| -rw-r--r-- | opendc-web/opendc-web-ui/src/components/AppHeaderTools.js | 16 | ||||
| -rw-r--r-- | opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineComponent.js | 10 | ||||
| -rw-r--r-- | opendc-web/opendc-web-ui/src/config.js | 41 | ||||
| -rw-r--r-- | opendc-web/opendc-web-ui/src/data/project.js | 2 | ||||
| -rw-r--r-- | opendc-web/opendc-web-ui/src/pages/_app.js | 18 | ||||
| -rw-r--r-- | opendc-web/opendc-web-ui/src/pages/projects/index.js | 17 | ||||
| -rw-r--r-- | opendc-web/opendc-web-ui/src/redux/index.js | 3 |
10 files changed, 111 insertions, 59 deletions
diff --git a/opendc-web/opendc-web-ui/src/api/index.js b/opendc-web/opendc-web-ui/src/api/index.js index 1a9877d0..75751658 100644 --- a/opendc-web/opendc-web-ui/src/api/index.js +++ b/opendc-web/opendc-web-ui/src/api/index.js @@ -20,7 +20,7 @@ * SOFTWARE. */ -const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL +import { apiUrl } from '../config' /** * Send the specified request to the OpenDC API. @@ -31,14 +31,19 @@ const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL * @param body The body of the request. */ export async function request(auth, path, method = 'GET', body) { + const headers = { + 'Content-Type': 'application/json', + } + const { getAccessTokenSilently } = auth - const token = await getAccessTokenSilently() + if (getAccessTokenSilently) { + const token = await getAccessTokenSilently() + headers['Authorization'] = `Bearer ${token}` + } + const response = await fetch(`${apiUrl}/${path}`, { method: method, - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, + headers: headers, body: body && JSON.stringify(body), }) const json = await response.json() diff --git a/opendc-web/opendc-web-ui/src/auth.js b/opendc-web/opendc-web-ui/src/auth.js index e670476c..3d6cf87c 100644 --- a/opendc-web/opendc-web-ui/src/auth.js +++ b/opendc-web/opendc-web-ui/src/auth.js @@ -23,15 +23,27 @@ import PropTypes from 'prop-types' import { Auth0Provider, useAuth0 } from '@auth0/auth0-react' import { useEffect } from 'react' +import { auth } from './config' /** - * Obtain the authentication context. + * Helper function to provide the authentication context in case Auth0 is not + * configured. */ -export function useAuth() { - return useAuth0() +function useAuthDev() { + return { + isAuthenticated: false, + isLoading: false, + logout: () => {}, + loginWithRedirect: () => {}, + } } /** + * Obtain the authentication context. + */ +export const useAuth = auth.domain ? useAuth0 : useAuthDev + +/** * Force the user to be authenticated or redirect to the homepage. */ export function useRequireAuth() { @@ -51,16 +63,20 @@ export function useRequireAuth() { * AuthProvider which provides an authentication context. */ export function AuthProvider({ children }) { - return ( - <Auth0Provider - domain={process.env.NEXT_PUBLIC_AUTH0_DOMAIN} - clientId={process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID} - redirectUri={global.window && global.window.location.origin} - audience={process.env.NEXT_PUBLIC_AUTH0_AUDIENCE} - > - {children} - </Auth0Provider> - ) + if (auth.domain) { + return ( + <Auth0Provider + domain={auth.domain} + clientId={auth.clientId} + redirectUri={auth.redirectUri} + audience={auth.audience} + > + {children} + </Auth0Provider> + ) + } + + return children } AuthProvider.propTypes = { diff --git a/opendc-web/opendc-web-ui/src/components/AppHeader.js b/opendc-web/opendc-web-ui/src/components/AppHeader.js index b33212c4..fd54b3ad 100644 --- a/opendc-web/opendc-web-ui/src/components/AppHeader.js +++ b/opendc-web/opendc-web-ui/src/components/AppHeader.js @@ -22,13 +22,13 @@ import { PageHeader } from '@patternfly/react-core' import React from 'react' -import Image from 'next/image' import AppHeaderTools from './AppHeaderTools' import { AppNavigation } from './AppNavigation' import AppLogo from './AppLogo' export function AppHeader() { - const logo = <Image src="/img/logo.png" layout="fixed" width={30} height={30} alt="OpenDC" /> + // eslint-disable-next-line @next/next/no-img-element + const logo = <img src="/img/logo.png" width={30} height={30} alt="OpenDC" /> return ( <PageHeader diff --git a/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js b/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js index 02e5d265..3e58b209 100644 --- a/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js +++ b/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js @@ -39,7 +39,9 @@ import { useAuth } from '../auth' import { GithubIcon, HelpIcon } from '@patternfly/react-icons' function AppHeaderTools() { - const auth = useAuth() + const { logout, user, isAuthenticated, isLoading } = useAuth() + const username = isAuthenticated || isLoading ? user?.name : 'Anonymous' + const avatar = isAuthenticated || isLoading ? user?.picture : '/img/avatar.svg' const [isKebabDropdownOpen, setKebabDropdownOpen] = useState(false) const kebabDropdownItems = [ @@ -56,7 +58,11 @@ function AppHeaderTools() { const [isDropdownOpen, setDropdownOpen] = useState(false) const userDropdownItems = [ <DropdownGroup key="group 2"> - <DropdownItem key="group 2 logout" onClick={() => auth.logout({ returnTo: window.location.origin })}> + <DropdownItem + key="group 2 logout" + isDisabled={!isAuthenticated} + onClick={() => logout({ returnTo: window.location.origin })} + > Logout </DropdownItem> </DropdownGroup>, @@ -105,7 +111,7 @@ function AppHeaderTools() { isOpen={isDropdownOpen} toggle={ <DropdownToggle onToggle={() => setDropdownOpen(!isDropdownOpen)}> - {auth?.user?.name ?? ( + {username ?? ( <Skeleton fontSize="xs" width="150px" @@ -119,8 +125,8 @@ function AppHeaderTools() { /> </PageHeaderToolsItem> </PageHeaderToolsGroup> - {auth?.user?.picture ? ( - <Avatar src={auth.user.picture} alt="Avatar image" /> + {avatar ? ( + <Avatar src={avatar} alt="Avatar image" /> ) : ( <Skeleton className="pf-c-avatar" shape="circle" width="2.25rem" screenreaderText="Loading avatar" /> )} diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineComponent.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineComponent.js index 921622d6..18c3db3c 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineComponent.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineComponent.js @@ -1,17 +1,11 @@ import PropTypes from 'prop-types' import React from 'react' -import Image from 'next/image' import { Flex, Label } from '@patternfly/react-core' import { Machine } from '../../../../shapes' const UnitIcon = ({ id, type }) => ( - <Image - src={'/img/topology/' + id + '-icon.png'} - alt={'Machine contains ' + type + ' units'} - layout="intrinsic" - height={24} - width={24} - /> + // eslint-disable-next-line @next/next/no-img-element + <img src={'/img/topology/' + id + '-icon.png'} alt={'Machine contains ' + type + ' units'} height={24} width={24} /> ) UnitIcon.propTypes = { diff --git a/opendc-web/opendc-web-ui/src/config.js b/opendc-web/opendc-web-ui/src/config.js new file mode 100644 index 00000000..52952d69 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/config.js @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * URL to OpenDC API. + */ +export const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL + +/** + * Authentication configuration. + */ +export const auth = { + domain: process.env.NEXT_PUBLIC_AUTH0_DOMAIN, + clientId: process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID, + audience: process.env.NEXT_PUBLIC_AUTH0_AUDIENCE, + redirectUri: global.window && global.window.location.origin, +} + +/** + * Sentry DSN for web frontend. + */ +export const sentryDsn = process.env.NEXT_PUBLIC_SENTRY_DSN diff --git a/opendc-web/opendc-web-ui/src/data/project.js b/opendc-web/opendc-web-ui/src/data/project.js index b1db3da5..60a8fab6 100644 --- a/opendc-web/opendc-web-ui/src/data/project.js +++ b/opendc-web/opendc-web-ui/src/data/project.js @@ -23,7 +23,7 @@ import { useQuery, useMutation } from 'react-query' import { addProject, deleteProject, fetchProject, fetchProjects } from '../api/projects' import { addPortfolio, deletePortfolio, fetchPortfolio, fetchPortfolios } from '../api/portfolios' -import { addScenario, deleteScenario, fetchScenario, fetchScenariosOfPortfolio } from '../api/scenarios' +import { addScenario, deleteScenario, fetchScenario } from '../api/scenarios' /** * Configure the query defaults for the project endpoints. diff --git a/opendc-web/opendc-web-ui/src/pages/_app.js b/opendc-web/opendc-web-ui/src/pages/_app.js index 4861f5c1..bac9a5af 100644 --- a/opendc-web/opendc-web-ui/src/pages/_app.js +++ b/opendc-web/opendc-web-ui/src/pages/_app.js @@ -30,6 +30,7 @@ import { AuthProvider, useRequireAuth } from '../auth' import * as Sentry from '@sentry/react' import { Integrations } from '@sentry/tracing' import { QueryClientProvider } from 'react-query' +import { sentryDsn } from '../config' import '@patternfly/react-core/dist/styles/base.css' import '@patternfly/react-styles/css/utilities/Alignment/alignment.css' @@ -67,17 +68,14 @@ Inner.propTypes = { }).isRequired, } -const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN // Initialize Sentry if the user has configured a DSN -if (process.browser && dsn) { - if (dsn) { - Sentry.init({ - environment: process.env.NODE_ENV, - dsn: dsn, - integrations: [new Integrations.BrowserTracing()], - tracesSampleRate: 0.1, - }) - } +if (process.browser && sentryDsn) { + Sentry.init({ + environment: process.env.NODE_ENV, + dsn: sentryDsn, + integrations: [new Integrations.BrowserTracing()], + tracesSampleRate: 0.1, + }) } export default function App(props) { diff --git a/opendc-web/opendc-web-ui/src/pages/projects/index.js b/opendc-web/opendc-web-ui/src/pages/projects/index.js index bb1fbd69..40792275 100644 --- a/opendc-web/opendc-web-ui/src/pages/projects/index.js +++ b/opendc-web/opendc-web-ui/src/pages/projects/index.js @@ -23,38 +23,29 @@ import React, { useMemo, useState } from 'react' import Head from 'next/head' import ProjectFilterPanel from '../../components/projects/FilterPanel' -import { useAuth } from '../../auth' import { AppPage } from '../../components/AppPage' import { PageSection, PageSectionVariants, Text, TextContent } from '@patternfly/react-core' import { useProjects, useDeleteProject } from '../../data/project' import ProjectTable from '../../components/projects/ProjectTable' import NewProject from '../../components/projects/NewProject' -const getVisibleProjects = (projects, filter, userId) => { +const getVisibleProjects = (projects, filter) => { switch (filter) { case 'SHOW_ALL': return projects case 'SHOW_OWN': - return projects.filter((project) => - project.authorizations.some((a) => a.userId === userId && a.level === 'OWN') - ) + return projects.filter((project) => project.role === 'OWNER') case 'SHOW_SHARED': - return projects.filter((project) => - project.authorizations.some((a) => a.userId === userId && a.level !== 'OWN') - ) + return projects.filter((project) => project.role !== 'OWNER') default: return projects } } function Projects() { - const { user } = useAuth() const { status, data: projects } = useProjects() const [filter, setFilter] = useState('SHOW_ALL') - const visibleProjects = useMemo( - () => getVisibleProjects(projects ?? [], filter, user?.sub), - [projects, filter, user?.sub] - ) + const visibleProjects = useMemo(() => getVisibleProjects(projects ?? [], filter), [projects, filter]) const { mutate: deleteProject } = useDeleteProject() diff --git a/opendc-web/opendc-web-ui/src/redux/index.js b/opendc-web/opendc-web-ui/src/redux/index.js index fa0c9d23..53cd9144 100644 --- a/opendc-web/opendc-web-ui/src/redux/index.js +++ b/opendc-web/opendc-web-ui/src/redux/index.js @@ -6,6 +6,7 @@ import thunk from 'redux-thunk' import rootReducer from './reducers' import rootSaga from './sagas' import { createReduxEnhancer } from '@sentry/react' +import { sentryDsn } from '../config' let store @@ -20,7 +21,7 @@ function initStore(initialState, ctx) { let middleware = applyMiddleware(...middlewares) - if (process.env.NEXT_PUBLIC_SENTRY_DSN) { + if (sentryDsn) { middleware = compose(middleware, createReduxEnhancer()) } |
