diff --git a/frontend/src/components/ibutsu-page.js b/frontend/src/components/ibutsu-page.js
index 3c74b984..38b3faf2 100644
--- a/frontend/src/components/ibutsu-page.js
+++ b/frontend/src/components/ibutsu-page.js
@@ -1,44 +1,55 @@
+{/* TODO: Consider renaming to projects-page, maybe updates for static routing? */}
+
import React from 'react';
import PropTypes from 'prop-types';
+import { Outlet } from 'react-router-dom';
+
import {
Alert,
- AlertActionLink,
AlertGroup,
AlertVariant,
+ EmptyState,
+ EmptyStateBody,
+ EmptyStateHeader,
+ EmptyStateIcon,
Page,
- PageSidebar,
- PageSidebarBody
} from '@patternfly/react-core';
import ElementWrapper from './elementWrapper';
-
import { IbutsuHeader } from '../components';
-import { ALERT_TIMEOUT, VERSION_CHECK_TIMEOUT } from '../constants';
-import { HttpClient } from '../services/http';
-import { getDateString, getTheme } from '../utilities';
-import packageJson from '../../package.json'
+import { ALERT_TIMEOUT } from '../constants';
+import { getDateString } from '../utilities';
+import { IbutsuContext } from '../services/context';
+import IbutsuSidebar from './sidebar';
+import { ArchiveIcon } from '@patternfly/react-icons';
export class IbutsuPage extends React.Component {
+ static contextType = IbutsuContext;
+
static propTypes = {
eventEmitter: PropTypes.object,
navigation: PropTypes.node,
+ location: PropTypes.object,
children: PropTypes.node,
- title: PropTypes.string
+ title: PropTypes.string,
+ params: PropTypes.object
}
constructor(props) {
super(props);
- this.versionCheckId = '';
this.state = {
notifications: [],
- version: packageJson.version
+ views: []
};
this.props.eventEmitter.on('showNotification', (type, title, message, action, timeout, key) => {
this.showNotification(type, title, message, action, timeout, key);
});
- this.props.eventEmitter.on('themeChange', this.setTheme);
+ this.props.eventEmitter.on('projectChange', () => {
+ });
+ // TODO: empty state props.children override
+
}
showNotification(type, title, message, action, timeout, key) {
@@ -71,42 +82,9 @@ export class IbutsuPage extends React.Component {
});
}
- setTheme() {
- const isDarkTheme = getTheme() === 'dark';
- if (isDarkTheme) {
- document.documentElement.classList.add('pf-v5-theme-dark');
- }
- else {
- document.documentElement.classList.remove('pf-v5-theme-dark');
- }
- }
-
- checkVersion() {
- const frontendUrl = window.location.origin;
- HttpClient.get([frontendUrl, 'version.json'], {'v': getDateString()})
- .then(response => HttpClient.handleResponse(response))
- .then((data) => {
- if (data && data.version && (data.version !== this.state.version)) {
- const action = { window.location.reload(); }}>Reload;
- this.showNotification('info', 'Ibutsu has been updated', 'A newer version of Ibutsu is available, click reload to get it.', action, true, 'check-version');
- }
- });
- }
-
- componentWillUnmount() {
- if (this.versionCheckId) {
- clearInterval(this.versionCheckId);
- }
- }
-
- componentDidMount() {
- this.setTheme();
- this.checkVersion();
- this.versionCheckId = setInterval(() => this.checkVersion(), VERSION_CHECK_TIMEOUT);
- }
-
render() {
document.title = this.props.title || 'Ibutsu';
+ const { primaryObject } = this.context;
return (
@@ -117,17 +95,21 @@ export class IbutsuPage extends React.Component {
))}
}
- sidebar={
-
-
- {this.props.navigation}
-
- }
+ header={}
+ sidebar={}
isManagedSidebar={true}
style={{position: "relative"}}
>
- {this.props.children}
+ {primaryObject ?
+ :
+
+ } headingLevel="h4" />
+
+ There is currently no project selected. Please select a project from the dropdown in
+ order to view the dashboard.
+
+
+ }
);
diff --git a/frontend/src/components/profile-page.js b/frontend/src/components/profile-page.js
new file mode 100644
index 00000000..de65a3b0
--- /dev/null
+++ b/frontend/src/components/profile-page.js
@@ -0,0 +1,59 @@
+import React from 'react';
+
+import {
+ Nav,
+ NavList,
+ Page,
+ PageSidebar,
+ PageSidebarBody
+} from '@patternfly/react-core';
+
+import { NavLink, Outlet} from 'react-router-dom';
+
+
+import ElementWrapper from './elementWrapper';
+import { IbutsuHeader } from './ibutsu-header';
+import PropTypes from 'prop-types';
+
+
+
+const ProfilePage = (props) => {
+ // TODO useEffect
+
+ const navigation = (
+ // TODO what is onNavSelect doing here ...
+
+
+
+
+
+ );
+
+ return (
+
+ }
+ sidebar={navigation}
+ isManagedSidebar={true}
+ style={{position: "relative"}}
+ >
+
+
+
+ );
+}
+
+ProfilePage.propTypes = {
+ eventEmitter: PropTypes.object,
+};
+
+export default ProfilePage
diff --git a/frontend/src/components/result.js b/frontend/src/components/result.js
index 7261297e..fb8ead95 100644
--- a/frontend/src/components/result.js
+++ b/frontend/src/components/result.js
@@ -239,7 +239,7 @@ export class ResultView extends React.Component {
resultIcon = getIconForResult(testResult.result);
startTime = new Date(testResult.start_time);
parameters = Object.keys(testResult.params).map((key) => {key} = {testResult.params[key]}
);
- runLink = {testResult.run_id};
+ runLink = {testResult.run_id};
}
const jsonViewTheme = {
scheme: 'monokai',
@@ -296,7 +296,7 @@ export class ResultView extends React.Component {
Component:,
- {testResult.component}
+ {testResult.component}
]}
/>
@@ -499,7 +499,7 @@ export class ResultView extends React.Component {
Source:,
- {testResult.source}
+ {testResult.source}
]}
/>
diff --git a/frontend/src/components/sidebar.js b/frontend/src/components/sidebar.js
new file mode 100644
index 00000000..c9ca133d
--- /dev/null
+++ b/frontend/src/components/sidebar.js
@@ -0,0 +1,91 @@
+
+import React, { useContext, useState } from "react";
+import PropTypes from 'prop-types';
+
+import { Link } from 'react-router-dom';
+import { IbutsuContext } from "../services/context";
+import { PageSidebar,
+ PageSidebarBody,
+ Nav,
+ NavList,
+ } from '@patternfly/react-core';
+import { HttpClient } from "../services/http";
+import { Settings } from "../settings";
+
+
+const IbutsuSidebar = (props) => {
+ const context = useContext(IbutsuContext);
+ const [views, setViews] = useState();
+ props.eventEmitter.on('projectChange', (project) => {
+ setProjectViews(project);
+ })
+ // TODO: useEffect for view reset on project change
+
+
+ function setProjectViews(project) {
+ const { primaryObject } = context;
+ const targetProject = project ?? primaryObject;
+ if (!targetProject) {return;}
+
+ let params = {'filter': ['type=view', 'navigable=true']};
+
+ // read selected project from location
+ params['filter'].push('project_id=' + targetProject.id);
+
+ HttpClient.get([Settings.serverUrl, 'widget-config'], params)
+ .then(response => HttpClient.handleResponse(response))
+ .then(data => {
+ //debugger; //eslint-disable-line no-debugger
+
+ data.widgets.forEach(widget => {
+ if (targetProject) {
+ widget.params['project'] = targetProject.id;
+ }
+ else {
+ delete widget.params['project'];
+ }
+ });
+ setViews(data.widgets)})
+ .catch(error => console.log(error));
+ }
+
+ const { primaryType, primaryObject } = context;
+ if ( primaryType == 'project' && primaryObject ) {
+ if (! views ) {setProjectViews(primaryObject);}
+ return (
+
+
+
+
+
+ );
+ }
+};
+
+IbutsuSidebar.propTypes = {
+ eventEmitter: PropTypes.object,
+};
+
+export default IbutsuSidebar;
diff --git a/frontend/src/components/test-history.js b/frontend/src/components/test-history.js
index 6379a16b..f4419fc2 100644
--- a/frontend/src/components/test-history.js
+++ b/frontend/src/components/test-history.js
@@ -214,7 +214,7 @@ export class TestHistoryTable extends React.Component {
.then(data => this.setState({
lastPassedDate:
-
+
{new Date(data.results[0].start_time).toLocaleString()}
diff --git a/frontend/src/components/user-dropdown.js b/frontend/src/components/user-dropdown.js
index e2fe3b29..243f0f0e 100644
--- a/frontend/src/components/user-dropdown.js
+++ b/frontend/src/components/user-dropdown.js
@@ -11,9 +11,10 @@ import { UserIcon } from '@patternfly/react-icons';
import { Link } from 'react-router-dom';
import { AuthService } from '../services/auth';
-import { clearActiveProject, clearActiveDashboard } from '../utilities';
+import { IbutsuContext } from '../services/context';
export class UserDropdown extends React.Component {
+ static contextType = IbutsuContext;
static propTypes = {
eventEmitter: PropTypes.object
}
@@ -48,8 +49,9 @@ export class UserDropdown extends React.Component {
};
logout = () => {
- clearActiveProject();
- clearActiveDashboard();
+ const { setPrimaryObject, setActiveDashboard } = this.context;
+ setPrimaryObject();
+ setActiveDashboard();
AuthService.logout();
window.location = "/";
}
@@ -80,11 +82,11 @@ export class UserDropdown extends React.Component {
>
- Profile
+ Profile
{!!this.state.isSuperAdmin &&
- Administration
+ Administration
}
diff --git a/frontend/src/components/view.js b/frontend/src/components/view.js
index e2b56b1e..0763cd97 100644
--- a/frontend/src/components/view.js
+++ b/frontend/src/components/view.js
@@ -21,6 +21,7 @@ const VIEW_MAP = {
};
export class View extends React.Component {
+ // TODO: convert to functional
static propTypes = {
location: PropTypes.object,
navigate: PropTypes.func,
@@ -30,7 +31,7 @@ export class View extends React.Component {
constructor(props) {
super(props);
this.state = {
- id: props.params.id,
+ id: props.params.view_id,
view: null,
};
}
@@ -43,7 +44,7 @@ export class View extends React.Component {
componentDidUpdate(prevProps){
if (prevProps !== this.props) {
- this.setState({id: this.props.params.id}, this.getView);
+ this.setState({id: this.props.params.view_id}, this.getView);
}
}
diff --git a/frontend/src/constants.js b/frontend/src/constants.js
index dd6d5877..2045c8b7 100644
--- a/frontend/src/constants.js
+++ b/frontend/src/constants.js
@@ -114,3 +114,5 @@ export const CLASSIFICATION = {
unknown: "Unknown"
};
export const HEATMAP_MAX_BUILDS = 40;
+
+export const THEME_KEY = 'theme';
diff --git a/frontend/src/dashboard.js b/frontend/src/dashboard.js
index 486eb49e..235342c1 100644
--- a/frontend/src/dashboard.js
+++ b/frontend/src/dashboard.js
@@ -26,7 +26,6 @@ import {
} from '@patternfly/react-core';
import {
- ArchiveIcon,
CubesIcon,
PlusCircleIcon,
TachometerAltIcon,
@@ -46,67 +45,85 @@ import {
ResultAggregatorWidget,
ResultSummaryWidget
} from './widgets';
-import { getActiveProject, getActiveDashboard } from './utilities.js';
+import { IbutsuContext } from './services/context.js';
export class Dashboard extends React.Component {
+ static contextType = IbutsuContext;
static propTypes = {
- eventEmitter: PropTypes.object
+ eventEmitter: PropTypes.object,
+ navigate: PropTypes.func,
+ params: PropTypes.object,
}
constructor(props) {
super(props);
- let dashboard = getActiveDashboard() || this.getDefaultDashboard();
this.state = {
widgets: [],
filteredDashboards: [],
dashboards: [],
- selectedDashboard: dashboard,
+ selectedDashboard: null,
isDashboardSelectorOpen: false,
isNewDashboardOpen: false,
isWidgetWizardOpen: false,
isEditModalOpen: false,
editWidgetData: {},
- dashboardInputValue: dashboard?.title || '',
+ dashboardInputValue: '',
filterValueDashboard: ''
};
- props.eventEmitter.on('projectChange', () => {
- this.clearDashboards();
- this.getDashboards();
+ props.eventEmitter.on('projectChange', (value) => {
+ this.getDashboards(value);
+ this.getDefaultDashboard(value);
});
}
- getDefaultDashboard() {
- let project = getActiveProject();
- if (project && project.defaultDashboard) {
- console
- const dashboard = JSON.stringify(project.defaultDashboard)
- localStorage.setItem('dashboard', dashboard);
- return project.defaultDashboard;
+ sync_context = () => {
+ // Active dashboard
+ const { activeDashboard } = this.context;
+ const { selectedDashboard } = this.state;
+ const paramDash = this.props.params?.dashboard_id;
+ let updatedDash = undefined;
+ // API call to update context
+ if ( paramDash != null && activeDashboard?.id !== paramDash) {
+ HttpClient.get([Settings.serverUrl, 'dashboard', paramDash])
+ .then(response => HttpClient.handleResponse(response))
+ .then(data => {
+ const { setActiveDashboard } = this.context;
+ setActiveDashboard(data);
+ updatedDash = data;
+ this.setState({
+ selectedDashboard: data,
+ isDashboardSelectorOpen: false,
+ filterValueDashboard: '',
+ dashboardInputValue: data.title,
+ }); // callback within class component won't have updated context
+ // TODO don't pass value when converting to functional component
+ this.getWidgets(data);
+ })
+ .catch(error => console.log(error));
}
- else {
- return null;
+
+ if (updatedDash && !selectedDashboard ) {
+ this.setState({
+ selectedDashboard: updatedDash,
+ dashboardInputValue: updatedDash.title
+ })
}
}
- clearDashboards() {
- localStorage.removeItem('dashboard');
- this.setState({
- selectedDashboard: null,
- filteredDashboards: [],
- dashboardInputValue: '',
- filterValueDashboard: ''
- });
- }
+ getDashboards = (handledOject = null) => {
+ // value is checked because of handler scope not seeing context state updates
+ // TODO: react-router loaders would be way better
+ const { primaryObject } = this.context;
+ const paramProject = this.props.params?.project_id;
+ const primaryObjectId = handledOject?.id ?? primaryObject?.id ?? paramProject;
- getDashboards() {
- let project = getActiveProject();
- if (!project) {
+ if (!primaryObjectId) {
this.setState({dashboardInputValue: ''})
return;
}
let params = {
- 'project_id': project.id,
+ 'project_id': primaryObjectId,
'pageSize': 10
};
@@ -116,26 +133,65 @@ export class Dashboard extends React.Component {
HttpClient.get([Settings.serverUrl, 'dashboard'], params)
.then(response => HttpClient.handleResponse(response))
.then(data => {
- this.setState({dashboards: data['dashboards'], filteredDashboards: data['dashboards']}, this.getWidgets);
- });
+ this.setState({dashboards: data['dashboards'], filteredDashboards: data['dashboards']});
+ })
+ .catch(error => console.log(error));
+ }
+
+ getDefaultDashboard = (handledObject = null) => {
+ const { primaryObject, activeDashboard, setActiveDashboard } = this.context;
+ const paramProject = this.props.params?.project_id;
+
+ let targetObject = handledObject ?? primaryObject ?? paramProject;
+
+ if (typeof(targetObject) === 'string') {
+ HttpClient.get([Settings.serverUrl, 'project', paramProject])
+ .then(response => HttpClient.handleResponse(response))
+ .then(data => {
+ targetObject = data;
+ })
+ .catch(error => console.log(error));
+
+ }
+
+ if ( !activeDashboard && targetObject?.defaultDashboard ){
+ setActiveDashboard(targetObject.defaultDashboard);
+ this.setState({
+ 'selectedDashboard': targetObject.defaultDashboard,
+ 'dashboardInputValue': targetObject.defaultDashboard?.title
+ })
+ } else {
+ this.setState({
+ 'selectedDashboard': 'Select a dashboard',
+ 'dashboardInputValue': 'Select a dashboard'
+ })
+ }
}
- getWidgets() {
+ getWidgets = (dashboard) => {
let params = {'type': 'widget'};
- let dashboard = getActiveDashboard() || this.getDefaultDashboard();
- if (!dashboard) {
+ const { activeDashboard } = this.context;
+ // TODO don't pass value when converting to functional component
+ let target_dash = null;
+ if (dashboard === undefined) {
+ target_dash = activeDashboard;
+ } else {
+ target_dash = dashboard;
+ }
+ if (!target_dash) {
return;
}
- params['filter'] = 'dashboard_id=' + dashboard.id;
+ params['filter'] = 'dashboard_id=' + target_dash.id;
HttpClient.get([Settings.serverUrl, 'widget-config'], params)
.then(response => HttpClient.handleResponse(response))
.then(data => {
// set the widget project param
data.widgets.forEach(widget => {
- widget.params['project'] = dashboard.project_id;
+ widget.params['project'] = target_dash.project_id;
});
this.setState({widgets: data.widgets});
- });
+ })
+ .catch(error => console.log(error));
}
onDashboardToggle = () => {
@@ -143,23 +199,34 @@ export class Dashboard extends React.Component {
};
onDashboardSelect = (_event, value) => {
- const dashboard = JSON.stringify(value);
- localStorage.setItem('dashboard', dashboard);
+ const { setActiveDashboard } = this.context;
+ setActiveDashboard(value);
this.setState({
selectedDashboard: value,
isDashboardSelectorOpen: false,
filterValueDashboard: '',
dashboardInputValue: value.title,
- }, this.getWidgets);
+ }); // callback within class component won't have updated context
+ // TODO don't pass value when converting to functional component
+ this.getWidgets(value);
+
+ // does it really matter whether I read from params or the context here?
+ // they should be the same, reading from params 'feels' better
+ this.props.navigate('/project/' + this.props.params?.project_id + '/dashboard/' + value?.id)
};
onDashboardClear = () => {
- localStorage.removeItem('dashboard');
+ const { setActiveDashboard } = this.context;
+ setActiveDashboard();
this.setState({
- selectedDashboard: null,
- dashboardInputValue: '',
+ selectedDashboard: 'Select a dashboard',
+ dashboardInputValue: 'Select a dashboard',
filterValueDashboard: ''
- }, this.getDashboards, this.getWidgets);
+ });
+ // TODO convert to functional component and rely on context updating within callbacks
+ this.getWidgets(null);
+
+ this.props.navigate('/project/' + this.props.params?.project_id + '/dashboard/')
}
onTextInputChange = (_event, value) => {
@@ -188,7 +255,8 @@ export class Dashboard extends React.Component {
selectedDashboard: data,
dashboardInputValue: data.title,
}, this.getWidgets);
- });
+ })
+ .catch(error => console.log(error));
}
onDeleteDashboardClick = () => {
@@ -200,18 +268,19 @@ export class Dashboard extends React.Component {
}
onDeleteDashboard = () => {
- const dashboard = getActiveDashboard();
+ const { activeDashboard, setActiveDashboard } = this.context;
- HttpClient.delete([Settings.serverUrl, 'dashboard', dashboard.id])
+ HttpClient.delete([Settings.serverUrl, 'dashboard', activeDashboard.id])
.then(response => HttpClient.handleResponse(response))
.then(() => {
- localStorage.removeItem('dashboard');
+ setActiveDashboard();
this.getDashboards();
this.setState({
isDeleteDashboardOpen: false,
selectedDashboard: null
});
- });
+ })
+ .catch(error => console.log(error));
}
onDeleteWidget = () => {
@@ -220,13 +289,14 @@ export class Dashboard extends React.Component {
.then(() => {
this.getWidgets();
this.setState({isDeleteWidgetOpen: false});
- });
+ })
+ .catch(error => console.log(error));
}
onEditWidgetSave = (editWidget) => {
- const project = getActiveProject();
- if (!editWidget.project_id && project) {
- editWidget.project_id = project.id;
+ const { primaryObject } = this.context;
+ if (!editWidget.project_id && primaryObject) {
+ editWidget.project_id = primaryObject.id;
}
this.setState({isEditModalOpen: false});
editWidget.id = this.state.currentWidgetId
@@ -234,7 +304,8 @@ export class Dashboard extends React.Component {
.then(response => HttpClient.handleResponse(response))
.then(() => {
this.getWidgets();
- });
+ })
+ .catch(error => console.log(error));
}
onEditWidgetClose = () => {
@@ -246,7 +317,8 @@ export class Dashboard extends React.Component {
.then(response => HttpClient.handleResponse(response))
.then(data => {
this.setState({isEditModalOpen: true, currentWidgetId: id, editWidgetData: data});
- });
+ })
+ .catch(error => console.log(error));
}
@@ -271,16 +343,20 @@ export class Dashboard extends React.Component {
}
onNewWidgetSave = (newWidget) => {
- const project = getActiveProject();
- if (!newWidget.project_id && project) {
- newWidget.project_id = project.id;
+ const { primaryObject } = this.context;
+ if (!newWidget.project_id && primaryObject) {
+ newWidget.project_id = primaryObject.id;
}
- HttpClient.post([Settings.serverUrl, 'widget-config'], newWidget).then(() => { this.getWidgets() });
+ HttpClient.post([Settings.serverUrl, 'widget-config'], newWidget)
+ .then(() => { this.getWidgets() })
+ .catch(error => console.log(error));
this.setState({isWidgetWizardOpen: false});
}
componentDidMount() {
+ this.sync_context();
this.getDashboards();
+ this.getDefaultDashboard();
this.getWidgets();
}
@@ -307,9 +383,8 @@ export class Dashboard extends React.Component {
render() {
document.title = 'Dashboard | Ibutsu';
- const { widgets } = this.state || this.getWidgets();
- const project = getActiveProject();
- const dashboard = getActiveDashboard() || this.getDefaultDashboard();
+ const { widgets } = this.state;
+ const { primaryObject, activeDashboard } = this.context;
const toggle = toggleRef => (
@@ -411,7 +486,7 @@ export class Dashboard extends React.Component {
aria-label="Delete dashboard"
variant="plain"
title="Delete dashboard"
- isDisabled={!dashboard}
+ isDisabled={!activeDashboard}
onClick={this.onDeleteDashboardClick}
>
@@ -434,7 +509,7 @@ export class Dashboard extends React.Component {
- {!!project && !!dashboard && !!widgets &&
+ {!!primaryObject && !!activeDashboard && !!widgets &&
{widgets.map(widget => {
if (KNOWN_WIDGETS.includes(widget.widget)) {
@@ -529,16 +604,7 @@ export class Dashboard extends React.Component {
})}
}
- {!project &&
-
- } headingLevel="h4" />
-
- There is currently no project selected. Please select a project from the dropdown in
- order to view the dashboard.
-
-
- }
- {!!project && !dashboard &&
+ {!!primaryObject && !activeDashboard &&
} headingLevel="h4" />
@@ -550,7 +616,7 @@ export class Dashboard extends React.Component {
}
- {(!!project && !!dashboard && widgets.length === 0) &&
+ {(!!primaryObject && !!activeDashboard && widgets.length === 0) &&
} headingLevel="h4" />
@@ -564,13 +630,13 @@ export class Dashboard extends React.Component {
}
);
+
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
diff --git a/frontend/src/login.js b/frontend/src/login.js
index 065fd3f5..8bc386b8 100644
--- a/frontend/src/login.js
+++ b/frontend/src/login.js
@@ -24,7 +24,7 @@ import { HttpClient } from './services/http';
import { AuthService } from './services/auth';
import { KeycloakService } from './services/keycloak';
import { Settings } from './settings';
-import { clearActiveDashboard, clearActiveProject } from './utilities';
+import { IbutsuContext } from './services/context';
function getLocationFrom(location) {
let { from } = location.state || {from: {pathname: '/'}};
@@ -63,6 +63,7 @@ function getUser(location) {
}
export class Login extends React.Component {
+ static contextType = IbutsuContext;
static propTypes = {
location: PropTypes.object
};
@@ -113,12 +114,13 @@ export class Login extends React.Component {
alert = {message: 'E-mail and/or password fields are blank', status: 'danger'};
}
this.setState({isValidEmail, isValidPassword, alert});
+ const { setPrimaryObject, setActiveDashboard } = this.context;
if (isValidEmail && isValidPassword) {
AuthService.login(this.state.emailValue, this.state.passwordValue)
.then(isLoggedIn => {
if (isLoggedIn) {
- clearActiveDashboard();
- clearActiveProject();
+ setPrimaryObject();
+ setActiveDashboard();
window.location = this.state.from.pathname;
}
else {
@@ -153,20 +155,22 @@ export class Login extends React.Component {
onOAuth2Success = (response) => {
// Make sure there are no active projects or dashboards selected
- clearActiveDashboard();
- clearActiveProject();
+ const { setPrimaryObject, setActiveDashboard } = this.context;
+ setPrimaryObject();
+ setActiveDashboard();
AuthService.setUser(response);
window.location = this.state.from.pathname;
}
onGoogleLogin = (response) => {
const { redirect_uri } = this.state.externalLogins.google;
+ const { setPrimaryObject, setActiveDashboard } = this.context;
HttpClient.get([redirect_uri], {"code": response["tokenId"]})
.then(response => response.json())
.then(user => {
// Make sure there are no active projects or dashboards selected
- clearActiveDashboard();
- clearActiveProject();
+ setPrimaryObject();
+ setActiveDashboard();
AuthService.setUser(user);
window.location = this.state.from.pathname;
});
@@ -174,10 +178,12 @@ export class Login extends React.Component {
onKeycloakLogin = () => {
const { server_url, realm, client_id } = this.state.externalLogins.keycloak;
+ const { setPrimaryObject, setActiveDashboard } = this.context;
+
this.setState({isLoggingIn: true}, () => {
// Make sure there are no active projects or dashboards selected
- clearActiveDashboard();
- clearActiveProject();
+ setPrimaryObject();
+ setActiveDashboard();
KeycloakService.login(server_url, realm, client_id);
});
}
diff --git a/frontend/src/pages/admin/home.js b/frontend/src/pages/admin/home.js
index ea3773ca..f47b006e 100644
--- a/frontend/src/pages/admin/home.js
+++ b/frontend/src/pages/admin/home.js
@@ -7,17 +7,19 @@ import {
TextContent,
} from '@patternfly/react-core';
-export class AdminHome extends React.Component {
- render() {
- return (
-
-
-
- Administration
-
-
-
-
- );
- }
-}
+const AdminHome = () => {
+ return (
+
+
+
+ Administration
+
+
+
+
+ );
+};
+
+AdminHome.propTypes = {};
+
+export default AdminHome;
diff --git a/frontend/src/pages/profile/user.js b/frontend/src/pages/profile/user.js
index e24c135e..0b7b4aa8 100644
--- a/frontend/src/pages/profile/user.js
+++ b/frontend/src/pages/profile/user.js
@@ -104,6 +104,8 @@ export class UserProfile extends React.Component {
}
render() {
+ document.title = "Profile | Ibutsu";
+
const { user, projects } = this.state;
let projectInfo = [];
// create the project rows
diff --git a/frontend/src/profile.js b/frontend/src/profile.js
index 076e4386..9f0a2456 100644
--- a/frontend/src/profile.js
+++ b/frontend/src/profile.js
@@ -1,49 +1,33 @@
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 { UserProfile } from './pages/profile/user';
import { UserTokens } from './pages/profile/tokens';
-import { IbutsuPage } from './components';
import './app.css';
import ElementWrapper from './components/elementWrapper';
+import ProfilePage from './components/profile-page';
-export class Profile extends React.Component {
- constructor(props) {
- super(props);
- this.eventEmitter = new EventEmitter();
- }
-
- render() {
- const navigation = (
-
- );
-
- return (
-
-
-
- } />
- } />
-
-
-
- );
- }
+const Profile = () => {
+ // TODO useEffect instead of eventEmitter
+ const eventEmitter = new EventEmitter();
+
+ return (
+
+ }>
+ } />
+ } />
+ }/>
+
+
+ );
}
+
+Profile.propTypes = {
+
+};
+
+export default Profile;
diff --git a/frontend/src/report-builder.js b/frontend/src/report-builder.js
index 9481f428..36885878 100644
--- a/frontend/src/report-builder.js
+++ b/frontend/src/report-builder.js
@@ -31,10 +31,10 @@ import {
toTitleCase,
parseFilter,
getSpinnerRow,
- getActiveProject,
} from './utilities';
import { DownloadButton, FilterTable } from './components';
import { OPERATIONS } from './constants';
+import { IbutsuContext } from './services/context';
function reportToRow(report) {
@@ -64,6 +64,7 @@ function reportToRow(report) {
}
export class ReportBuilder extends React.Component {
+ static contextType = IbutsuContext;
static propTypes = {
location: PropTypes.object,
eventEmitter: PropTypes.object
@@ -139,9 +140,9 @@ export class ReportBuilder extends React.Component {
pageSize: this.state.pageSize,
page: this.state.page
};
- const project = getActiveProject();
- if (project) {
- params['project'] = project.id;
+ const { primaryObject } = this.context;
+ if (primaryObject) {
+ params['project'] = primaryObject.id;
}
HttpClient.get([Settings.serverUrl, 'report'], params)
.then(response => HttpClient.handleResponse(response))
@@ -167,14 +168,14 @@ export class ReportBuilder extends React.Component {
}
onRunReportClick = () => {
- const project = getActiveProject();
+ const { primaryObject } = this.context;
let params = {
type: this.state.reportType,
filter: this.state.reportFilter,
source: this.state.reportSource
};
- if (project) {
- params['project'] = project.id;
+ if (primaryObject) {
+ params['project'] = primaryObject.id;
}
HttpClient.post([Settings.serverUrl, 'report'], params).then(() => this.getReports());
};
diff --git a/frontend/src/result-list.js b/frontend/src/result-list.js
index fdf9496b..e2dc7cc0 100644
--- a/frontend/src/result-list.js
+++ b/frontend/src/result-list.js
@@ -28,7 +28,6 @@ import { HttpClient } from './services/http';
import { Settings } from './settings';
import {
buildParams,
- getActiveProject,
getFilterMode,
getOperationMode,
getOperationsFromField,
@@ -38,8 +37,11 @@ import {
} from './utilities';
import { FilterTable, MultiValueInput } from './components';
import { OPERATIONS, RESULT_FIELDS } from './constants';
+import { IbutsuContext } from './services/context';
export class ResultList extends React.Component {
+ static contextType = IbutsuContext;
+
static propTypes = {
location: PropTypes.object,
navigate: PropTypes.func,
@@ -356,7 +358,7 @@ export class ResultList extends React.Component {
let params = buildParams(this.state.filters);
params.push('page=' + this.state.page);
params.push('pageSize=' + this.state.pageSize);
- this.props.navigate('/results?' + params.join('&'))
+ this.props.navigate('results?' + params.join('&'))
}
setPage = (_event, pageNumber) => {
@@ -378,9 +380,9 @@ export class ResultList extends React.Component {
this.setState({rows: [getSpinnerRow(5)], isEmpty: false, isError: false});
let params = {filter: []};
let filters = this.state.filters;
- const project = getActiveProject();
- if (project) {
- filters['project_id'] = {'val': project.id, 'op': 'eq'};
+ const { primaryObject } = this.context;
+ if (primaryObject) {
+ filters['project_id'] = {'val': primaryObject.id, 'op': 'eq'};
}
else if (Object.prototype.hasOwnProperty.call(filters, 'project_id')) {
delete filters['project_id']
diff --git a/frontend/src/result.js b/frontend/src/result.js
index 68a0b01e..e09b3d23 100644
--- a/frontend/src/result.js
+++ b/frontend/src/result.js
@@ -25,7 +25,7 @@ export class Result extends React.Component {
this.state = {
isResultValid: false,
testResult: null,
- id: props.params.id
+ id: props.params.result_id
};
}
diff --git a/frontend/src/run-list.js b/frontend/src/run-list.js
index 9a31b812..4e4fbc0f 100644
--- a/frontend/src/run-list.js
+++ b/frontend/src/run-list.js
@@ -28,7 +28,6 @@ import { Settings } from './settings';
import {
buildBadge,
buildParams,
- getActiveProject,
getFilterMode,
getOperationMode,
getOperationsFromField,
@@ -38,6 +37,7 @@ import {
} from './utilities';
import { MultiValueInput, FilterTable, RunSummary } from './components';
import { OPERATIONS, RUN_FIELDS } from './constants';
+import { IbutsuContext } from './services/context';
function runToRow(run, filterFunc) {
@@ -75,16 +75,18 @@ function runToRow(run, filterFunc) {
}
return {
"cells": [
- {title: {run.id} {badges}},
+ {title: {run.id} {badges}},
{title: round(run.duration) + 's'},
{title: },
{title: created.toLocaleString()},
- {title: See results }
+ {title: See results }
]
};
}
export class RunList extends React.Component {
+ static contextType = IbutsuContext;
+
static propTypes = {
location: PropTypes.object,
navigate: PropTypes.func,
@@ -136,8 +138,8 @@ export class RunList extends React.Component {
isBoolOpen: false,
};
this.params = new URLSearchParams(props.location.search);
- props.eventEmitter.on('projectChange', () => {
- this.getRuns();
+ props.eventEmitter.on('projectChange', (value) => {
+ this.getRuns(value);
});
}
@@ -265,7 +267,6 @@ export class RunList extends React.Component {
this.setState({filters: filters, page: 1}, callback);
}
-
setFilter = (field, value) => {
this.updateFilters(field, 'eq', value, () => {
this.updateUrl();
@@ -280,7 +281,6 @@ export class RunList extends React.Component {
});
}
-
removeFilter = id => {
this.updateFilters(id, null, null, () => {
this.updateUrl();
@@ -309,14 +309,15 @@ export class RunList extends React.Component {
});
}
- getRuns() {
+ getRuns = (handledOject = null) => {
// First, show a spinner
this.setState({rows: [getSpinnerRow(5)], isEmpty: false, isError: false});
let params = {filter: []};
let filters = this.state.filters;
- const project = getActiveProject();
- if (project) {
- filters['project_id'] = {'val': project.id, 'op': 'eq'};
+ const { primaryObject } = this.context;
+ const targetObject = handledOject ?? primaryObject;
+ if (targetObject) {
+ filters['project_id'] = {'val': targetObject.id, 'op': 'eq'};
}
else if (Object.prototype.hasOwnProperty.call(filters, 'project_id')) {
delete filters['project_id']
@@ -346,7 +347,7 @@ export class RunList extends React.Component {
console.error('Error fetching run data:', error);
this.setState({rows: [], isEmpty: false, isError: true});
});
- }
+ };
clearFilters = () => {
this.setState({
@@ -358,10 +359,8 @@ export class RunList extends React.Component {
textFilter: '',
inValues: [],
boolSelection: null,
- }, function () {
- this.updateUrl();
- this.getRuns();
});
+ this.updateUrl();
};
componentDidMount() {
diff --git a/frontend/src/run.js b/frontend/src/run.js
index 8a076821..c16f70a1 100644
--- a/frontend/src/run.js
+++ b/frontend/src/run.js
@@ -114,7 +114,7 @@ export class Run extends React.Component {
super(props);
this.state = {
run: MockRun,
- id: props.params.id,
+ id: props.params.run_id,
testResult: null,
columns: ['Test', 'Run', 'Result', 'Duration', 'Started'],
rows: [getSpinnerRow(5)],
@@ -236,6 +236,7 @@ export class Run extends React.Component {
}
getRunArtifacts() {
+ if (!this.state.id) {return;}
HttpClient.get([Settings.serverUrl, 'artifact'], {runId: this.state.id})
.then(response => HttpClient.handleResponse(response))
.then(data => {
@@ -356,6 +357,7 @@ export class Run extends React.Component {
}
getRun() {
+ if (!this.state.id) {return;}
HttpClient.get([Settings.serverUrl, 'run', this.state.id])
.then(response => {
response = HttpClient.handleResponse(response, 'response');
@@ -522,7 +524,7 @@ export class Run extends React.Component {
{!this.state.isRunValid &&
-
+
}
{this.state.isRunValid &&
@@ -733,7 +735,7 @@ export class Run extends React.Component {