Skip to content

Commit

Permalink
React Context and Project Nested Routing (#508)
Browse files Browse the repository at this point in the history
* autoupdate pre-commit

* Frontend refactor for context and routing

Frontend updates for admin portal-edit and portal-list are working

React context used to track active dashboard and project, instead of
browser local storage. Dashboard, IbutsuHeader, IbutsuPage rewritten for
using the context.

Static routes set for project dashboard/runs/results/reportbuilder using
UUID. Page component state needs to be updated in some areas when
navigation orginates from the URL with params set and not component
selection.

Project selection and dashboard selection are hooked up to the routing
with ugly event handler prop passing. Moving these to functional
components will allow using useEffect with dependencies instead to
control interaction between these components

sidebar moved into new functional component, project views need to be
accounted for in component state or in the context itself.

Now when a project is not selected, there is no page rendered (no
sidebar). This will need an empty state page to look good, something
easy just saying to select a project or portal.

Class component callbacks on setState don't pick up modified context, so
I'm having to hack a function parameter into the callback functions

Split Admin and Profile pages into separate component for sane routing
Both now use Page instead of IbutsuPage, keeping IbutsuHeader

Use an outlet in the admin and profile pages for easy routing

run and result updates for routing change

path relative links for react-router so that runs and results are nested
under `/project/:id`

Page refreshes now set both the project and dashboard selection from
URL params!

* remove utility functions, update Links

use path relative links from components where the route relative paths
don't compose correctly.

Update widget-config-controller to allow view type widgets to not have
any project set

needs testing

Updating FileUpload to remove params

Updating unit test to provide the necessary context for the FileUpload
component

* Updates for theme handling and Upload tooltip

use the HTML class to control theme instead of applying to every element

Update upload button to use ariaDisabled, add tooltip

* Remove FileUpload onClick prop

It's not necessary for the usecase and the cypress test was failing to
detect the mocked method being called.

* Add empty state for no project selection
  • Loading branch information
mshriver authored Sep 24, 2024
1 parent 5f06b87 commit 11a4362
Show file tree
Hide file tree
Showing 40 changed files with 909 additions and 508 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ repos:
- id: debug-statements

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.8
rev: v0.5.5
hooks:
- id: ruff
args:
Expand All @@ -31,7 +31,7 @@ repos:

## ES
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v9.4.0
rev: v9.8.0
hooks:
- id: eslint
additional_dependencies:
Expand Down
3 changes: 3 additions & 0 deletions backend/ibutsu_server/controllers/widget_config_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from ibutsu_server.util.query import get_offset
from ibutsu_server.util.uuid import validate_uuid

# TODO: pydantic validation of request data structure


def add_widget_config(widget_config=None, token_info=None, user=None):
"""Create a new widget config
Expand All @@ -25,6 +27,7 @@ def add_widget_config(widget_config=None, token_info=None, user=None):
data = connexion.request.json
if data["widget"] not in WIDGET_TYPES.keys():
return "Bad request, widget type does not exist", HTTPStatus.BAD_REQUEST

# add default weight of 10
if not data.get("weight"):
data["weight"] = 10
Expand Down
8 changes: 8 additions & 0 deletions frontend/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// eslint.config.js
export default [
{
rules: {
"no-unused-vars": "warn", // this isn't actually working through pre-commit or webpack
}
}
];
5 changes: 5 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
"@babel/core": "^7.24.7",
"@babel/eslint-parser": "^7.24.7",
"@babel/helper-call-delegate": "^7.12.13",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/plugin-syntax-jsx": "^7.24.7",
"@babel/plugin-transform-class-properties": "^7.24.7",
"@babel/plugin-transform-private-methods": "^7.24.7",
"@babel/plugin-transform-private-property-in-object": "^7.24.7",
"@babel/preset-flow": "^7.24.7",
"@babel/preset-react": "^7.24.7",
"@greatsumini/react-facebook-login": "^3.3.3",
Expand Down Expand Up @@ -44,6 +46,9 @@
"typescript": "^4.9.5",
"wolfy87-eventemitter": "^5.2.9"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11"
},
"scripts": {
"start": "serve -s build -l tcp://0.0.0.0:8080",
"build": "./bin/write-version-file.js && react-scripts build",
Expand Down
52 changes: 17 additions & 35 deletions frontend/src/admin.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import React from 'react';

import {
Nav,
NavList
} from '@patternfly/react-core';

import { NavLink, Route, Routes } from 'react-router-dom';
import { Navigate, Route, Routes } from 'react-router-dom';
import EventEmitter from 'wolfy87-eventemitter';
import ElementWrapper from './components/elementWrapper';

import { IbutsuPage } from './components';
import { AdminHome } from './pages/admin/home';
import AdminHome from './pages/admin/home';
import { UserList } from './pages/admin/user-list';
import { UserEdit } from './pages/admin/user-edit';
import { ProjectList } from './pages/admin/project-list';
import { ProjectEdit } from './pages/admin/project-edit';
import { AuthService } from './services/auth';

import './app.css';
import AdminPage from './components/admin-page';


export class Admin extends React.Component {
Expand All @@ -34,34 +30,20 @@ export class Admin extends React.Component {
}

render() {
const navigation = (
<Nav onSelect={this.onNavSelect} theme="dark" aria-label="Nav">
<NavList>
<li className="pf-v5-c-nav__item">
<NavLink to="/admin" className="pf-v5-c-nav__link">Admin Home</NavLink>
</li>
<li className="pf-v5-c-nav__item">
<NavLink to="/admin/users" className="pf-v5-c-nav__link">Users</NavLink>
</li>
<li className="pf-v5-c-nav__item">
<NavLink to="/admin/projects" className="pf-v5-c-nav__link">Projects</NavLink>
</li>
</NavList>
</Nav>
);

return (
<React.Fragment>
<IbutsuPage eventEmitter={this.eventEmitter} navigation={navigation} title="Administration | Ibutsu">
<Routes>
<Route path="*" element={<AdminHome />}/>
<Route path="/users" element={<ElementWrapper routeElement={UserList} />} />
<Route path="/users/:id" element={<ElementWrapper routeElement={UserEdit} />} />
<Route path="/projects" element={<ElementWrapper routeElement={ProjectList} />} />
<Route path="/projects/:id" element={<ElementWrapper routeElement={ProjectEdit} />} />
</Routes>
</IbutsuPage>
</React.Fragment>
<Routes>
<Route
path=""
element={<AdminPage eventEmitter={this.eventEmitter} />}
>
<Route path="home" element={<AdminHome/>} />
<Route path="users" element={<ElementWrapper routeElement={UserList} emitter={this.eventEmitter} />} />
<Route path="users/:id" element={<ElementWrapper routeElement={UserEdit} emitter={this.eventEmitter} />} />
<Route path="projects" element={<ElementWrapper routeElement={ProjectList} emitter={this.eventEmitter} />} />
<Route path="projects/:id" element={<ElementWrapper routeElement={ProjectEdit} emitter={this.eventEmitter} />} />
</Route>
<Route path="*" element={<Navigate to="" replace />}/>
</Routes>
);
}
}
136 changes: 59 additions & 77 deletions frontend/src/app.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
import React from 'react';

import {
Nav,
NavList
} from '@patternfly/react-core';
import EventEmitter from 'wolfy87-eventemitter';
import ElementWrapper from './components/elementWrapper';

import { NavLink, Route, Routes } from 'react-router-dom';
import { Route, Routes } from 'react-router-dom';

import { Dashboard } from './dashboard';
import { ReportBuilder } from './report-builder';
import { RunList } from './run-list';
import { Run } from './run';
import { ResultList } from './result-list';
import { Result } from './result';
import { Settings } from './settings';
import { View, IbutsuPage } from './components';
import { HttpClient } from './services/http';
import { getActiveProject } from './utilities';
import './app.css';
import { IbutsuContext } from './services/context';

import './app.css';

export class App extends React.Component {
static contextType = IbutsuContext;
constructor(props) {
super(props);
this.eventEmitter = new EventEmitter();
Expand All @@ -33,79 +28,66 @@ export class App extends React.Component {
searchValue: '',
views: []
};
this.eventEmitter.on('projectChange', () => {
this.getViews();
});
}

getViews() {
let params = {'filter': ['type=view', 'navigable=true']};
let project = getActiveProject();
if (project) {
params['filter'].push('project_id=' + project.id);
}
HttpClient.get([Settings.serverUrl, 'widget-config'], params)
.then(response => HttpClient.handleResponse(response))
.then(data => {
data.widgets.forEach(widget => {
if (project) {
widget.params['project'] = project.id;
}
else {
delete widget.params['project'];
}
});
this.setState({views: data.widgets});
});
}

componentDidMount() {
this.getViews();
}

render() {
document.title = 'Ibutsu';
const { views } = this.state;
const navigation = (
<Nav onSelect={this.onNavSelect} theme="dark" aria-label="Nav">
<NavList>
<li className="pf-v5-c-nav__item">
<NavLink to="/" className="pf-v5-c-nav__link">Dashboard</NavLink>
</li>
<li className="pf-v5-c-nav__item">
<NavLink to="/runs" className="pf-v5-c-nav__link">Runs</NavLink>
</li>
<li className="pf-v5-c-nav__item">
<NavLink to="/results" className="pf-v5-c-nav__link">Test Results</NavLink>
</li>
<li className="pf-v5-c-nav__item">
<NavLink to="/reports" className="pf-v5-c-nav__link">Report Builder</NavLink>
</li>
{views && views.map(view => (
view.widget !== "jenkins-analysis-view" && (
<li className="pf-v5-c-nav__item" key={view.id}>
<NavLink to={`/view/${view.id}`} className="pf-v5-c-nav__link">{view.title}</NavLink>
</li>
)
))}
</NavList>
</Nav>
);

return (
<React.Fragment>
<IbutsuPage eventEmitter={this.eventEmitter} navigation={navigation}>
<Routes>
<Route path="*" element={<Dashboard eventEmitter={this.eventEmitter} />} />
<Route path="/runs" element={<ElementWrapper routeElement={RunList} eventEmitter={this.eventEmitter} />} />
<Route path="/results" element={<ElementWrapper routeElement={ResultList} eventEmitter={this.eventEmitter} />} />
<Route path="/reports" element={<ElementWrapper routeElement={ReportBuilder} eventEmitter={this.eventEmitter} />} />
<Route path="/runs/:id" element={<ElementWrapper routeElement={Run} />} />
<Route path="/results/:id" element={<ElementWrapper routeElement={Result} />} />
<Route path="/view/:id" element={<ElementWrapper routeElement={View} />} />
</Routes>
</IbutsuPage>
</React.Fragment>
<Routes>
<Route
path=""
element={<ElementWrapper routeElement={IbutsuPage} eventEmitter={this.eventEmitter} />}
/>
<Route
path=":project_id/*"
element={<ElementWrapper routeElement={IbutsuPage} eventEmitter={this.eventEmitter} />}
>

{/* Nested project routes */}
<Route
path="dashboard/:dashboard_id"
element={
<ElementWrapper routeElement={Dashboard} eventEmitter={this.eventEmitter} />
}
/>
<Route
path="dashboard/*"
element={<ElementWrapper routeElement={Dashboard} eventEmitter={this.eventEmitter} />}
/>


<Route
path="runs"
element={
<ElementWrapper routeElement={RunList} eventEmitter={this.eventEmitter} />
}
/>
<Route
path="runs/:run_id"
element={<ElementWrapper routeElement={Run} />}
/>

<Route
path="results"
element={<ElementWrapper routeElement={ResultList} eventEmitter={this.eventEmitter} />}
/>
<Route
path="results/:result_id"
element={<ElementWrapper routeElement={Result} />}
/>

<Route
path="reports"
element={<ElementWrapper routeElement={ReportBuilder} eventEmitter={this.eventEmitter} />}
/>

<Route
path="view/:view_id"
element={<ElementWrapper routeElement={View} />}
/>
</Route>

</Routes>
);
}
}
46 changes: 25 additions & 21 deletions frontend/src/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,39 @@ import React from 'react';
import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
import { App } from './app';
import { Admin } from './admin';
import { Profile } from './profile';
import Profile from './profile';
import { Login } from './login';
import { SignUp } from './sign-up';
import { ForgotPassword } from './forgot-password';
import { ResetPassword } from './reset-password';
import { AuthService } from './services/auth';
import ElementWrapper from './components/elementWrapper';
import { IbutsuContextProvider } from './services/context';

export const Base = () => {
return (
<Router>
<Routes>
<Route path="/login" element={<ElementWrapper routeElement={Login} />} />
<Route path="/sign-up" element={<ElementWrapper routeElement={SignUp} />} />
<Route path="/forgot-password" element={<ElementWrapper routeElement={ForgotPassword} />} />
<Route path="/reset-password/:activationCode" element={<ElementWrapper routeElement={ResetPassword} />} />
<Route
path="/profile/*"
element={AuthService.isLoggedIn() ? <Profile /> : <Navigate to="/login" />}
/>
<Route
path="/admin/*"
element={AuthService.isLoggedIn() ? <Admin /> : <Navigate to="/" />}
/>
<Route
path="*"
element={AuthService.isLoggedIn() ? <App /> : <Navigate to="/login" />}
/>
</Routes>
</Router>
<IbutsuContextProvider>
<Router>
<Routes>
<Route path="login" element={<ElementWrapper routeElement={Login} />} />
<Route path="sign-up" element={<ElementWrapper routeElement={SignUp} />} />
<Route path="forgot-password" element={<ElementWrapper routeElement={ForgotPassword} />} />
<Route path="reset-password/:activationCode" element={<ElementWrapper routeElement={ResetPassword} />} />
<Route
path="profile/*"
element={AuthService.isLoggedIn() ? <Profile /> : <Navigate to="/login" />}
/>
<Route
path="admin/*"
element={AuthService.isLoggedIn() && AuthService.isSuperAdmin() ? <Admin /> : <Navigate to="/login" />}
/>
<Route
path="project/*"
element={AuthService.isLoggedIn() ? <App /> : <Navigate to="/login" />}
/>
<Route path="*" element={<Navigate to="project" replace />} />
</Routes>
</Router>
</IbutsuContextProvider>
);
};
Loading

0 comments on commit 11a4362

Please sign in to comment.