diff --git a/frontend/src/app.js b/frontend/src/app.js index 63019d2f..4d957b12 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -5,7 +5,7 @@ import ElementWrapper from './components/elementWrapper'; import { Navigate, Route, Routes } from 'react-router-dom'; -import { Dashboard } from './dashboard'; +import Dashboard from './dashboard'; import { ReportBuilder } from './report-builder'; import { RunList } from './run-list'; import { Run } from './run'; diff --git a/frontend/src/dashboard.js b/frontend/src/dashboard.js index 2567bffd..bb96b7e9 100644 --- a/frontend/src/dashboard.js +++ b/frontend/src/dashboard.js @@ -1,4 +1,5 @@ -import React from 'react'; +/* eslint-disable no-unused-vars */ +import React, { useContext, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { @@ -46,107 +47,78 @@ import { ResultSummaryWidget } from './widgets'; import { IbutsuContext } from './services/context.js'; +import { useNavigate, useParams } from 'react-router-dom'; -export class Dashboard extends React.Component { - static contextType = IbutsuContext; - static propTypes = { - eventEmitter: PropTypes.object, - navigate: PropTypes.func, - params: PropTypes.object, - } +function Dashboard() { + const context = useContext(IbutsuContext); + const params = useParams(); - constructor(props) { - super(props); - this.state = { - widgets: [], - filteredDashboards: [], - dashboards: [], - selectedDashboard: null, - isDashboardSelectorOpen: false, - isNewDashboardOpen: false, - isWidgetWizardOpen: false, - isEditModalOpen: false, - editWidgetData: {}, - dashboardInputValue: '', - filterValueDashboard: '' - }; - props.eventEmitter.on('projectChange', (value) => { - this.getDashboards(value); - this.getDefaultDashboard(value); - }); - } + const navigate = useNavigate(); - sync_context = () => { - // Active dashboard - const { activeDashboard, setActiveDashboard } = this.context; - const { selectedDashboard } = this.state; - const paramDash = this.props.params?.dashboard_id; - if (!paramDash) { - // No dashboard in the URL, clear context - setActiveDashboard(); - } - 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)); - } + // dashboard states + const [dashboards, setDashboards] = useState([]); + const [filteredDBs, setFilteredDBs] = useState([]); + const [selectedDB, setSelectedDB] = useState(); + const [isDBSelectorOpen, setIsDBSelectorOpen] = useState(false); + const [isNewDBOpen, setIsNewDBOpen] = useState(false); + const [isDeleteDBOpen, setIsDeleteDBOpen] = useState(false); - if (updatedDash && !selectedDashboard ) { - this.setState({ - selectedDashboard: updatedDash, - dashboardInputValue: updatedDash.title - }) - } - } + // widget states + const [widgets, setWidgets] = useState([]); + const [isNewWidgetOpen, setIsNewWidgetOpen] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [editWidgetData, setEditWidgetData] = useState({}); + const [isDeleteWidgetOpen, setIsDeleteWidgetOpen] = useState(false); + const [currentWidget, setCurrentWidget] = useState(); - getDashboards = (handledOject = null) => { - // value is checked because of handler scope not seeing context state updates + // typeahead input value states + const [selectDBInputValue, setSelectDBInputValue] = useState(''); + const [filterDBValue, setFilterDBValue] = useState(''); + + useEffect(() => { + // TODO is sync necessary with functional component...? + //sync_context(); + + getDashboards(); // dependent: filterDBValue + getDefaultDashboard(); // TODO: only changes on project selection, not needed on setup? + + getWidgets(); // dependent: selectedDB + + }, [selectedDB, filterDBValue]); + + function getDashboards() { // 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; + const { primaryObject } = context; + const paramProject = params?.project_id; + const primaryObjectId = primaryObject?.id ?? paramProject; if (!primaryObjectId) { - this.setState({dashboardInputValue: ''}) + setSelectDBInputValue('') return; } - let params = { + let api_params = { 'project_id': primaryObjectId, 'pageSize': 10 }; - if (this.state.filterValueDashboard) { - params['filter'] = ['title%' + this.state.filterValueDashboard]; + if (filterDBValue) { + api_params['filter'] = ['title%' + filterDBValue]; } - HttpClient.get([Settings.serverUrl, 'dashboard'], params) + HttpClient.get([Settings.serverUrl, 'dashboard'], api_params) .then(response => HttpClient.handleResponse(response)) .then(data => { - this.setState({dashboards: data['dashboards'], filteredDashboards: data['dashboards']}); + setDashboards(data['dashboards']); + setFilteredDBs(data['dashboards']); }) .catch(error => console.log(error)); } - getDefaultDashboard = (handledObject = null) => { - const { primaryObject, activeDashboard, setActiveDashboard } = this.context; - const paramProject = this.props.params?.project_id; + function getDefaultDashboard() { + const { primaryObject, activeDashboard, setActiveDashboard } = context; + const paramProject = params?.project_id; - let targetObject = handledObject ?? primaryObject ?? paramProject; + let targetObject = primaryObject ?? paramProject; if (typeof(targetObject) === 'string') { HttpClient.get([Settings.serverUrl, 'project', paramProject]) @@ -158,511 +130,531 @@ export class Dashboard extends React.Component { } + // Maybe change this behavior for last-visted preference on dashboard page if ( !activeDashboard && targetObject?.defaultDashboard ){ setActiveDashboard(targetObject.defaultDashboard); - this.setState({ - 'selectedDashboard': targetObject.defaultDashboard, - 'dashboardInputValue': targetObject.defaultDashboard?.title - }) - } + setSelectedDB(targetObject.defaultDashboard); + setSelectDBInputValue(targetObject.defaultDashboard?.title); + } } - getWidgets = (dashboard) => { - let params = {'type': 'widget'}; - 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) { + function getWidgets() { + let api_params = {'type': 'widget'}; + const { activeDashboard } = context; + if (!activeDashboard) { return; } - params['filter'] = 'dashboard_id=' + target_dash.id; - HttpClient.get([Settings.serverUrl, 'widget-config'], params) + api_params['filter'] = 'dashboard_id=' + activeDashboard.id; + HttpClient.get([Settings.serverUrl, 'widget-config'], api_params) .then(response => HttpClient.handleResponse(response)) .then(data => { // set the widget project param data.widgets.forEach(widget => { - widget.params['project'] = target_dash.project_id; + widget.params['project'] = activeDashboard.project_id; }); - this.setState({widgets: data.widgets}); + setWidgets(data.widgets); }) .catch(error => console.log(error)); } - onDashboardToggle = () => { - this.setState({isDashboardSelectorOpen: !this.state.isDashboardSelectorOpen}); - }; + function onDashboardToggle() { + setIsDBSelectorOpen({isDBSelectorOpen: !isDBSelectorOpen}); + } - onDashboardSelect = (_event, value) => { - const { setActiveDashboard } = this.context; + function onDashboardSelect(_event, value) { + // context update + const { setActiveDashboard } = context; setActiveDashboard(value); - this.setState({ - selectedDashboard: value, - isDashboardSelectorOpen: false, - filterValueDashboard: '', - dashboardInputValue: value.title, - }); // 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 = () => { - const { setActiveDashboard } = this.context; + + // state update + setSelectedDB(value); + setIsDBSelectorOpen(false); + setFilterDBValue(''); + setSelectDBInputValue(value.title); + + navigate('/project/' + params?.project_id + '/dashboard/' + value?.id) + } + + function onDashboardClear() { + // context update + const { setActiveDashboard } = context; setActiveDashboard(); - this.setState({ - selectedDashboard: 'Select a dashboard', - dashboardInputValue: 'Select a dashboard', - filterValueDashboard: '' - }); - // 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/') + + // state update + setSelectedDB(null); + setIsDBSelectorOpen(false); + setSelectDBInputValue('Select a dashboard'); + setFilterDBValue(''); + + navigate('/project/' + params?.project_id + '/dashboard/') } - onTextInputChange = (_event, value) => { - this.setState({ - dashboardInputValue: value, - filterValueDashboard: value - }, this.getDashboards); - }; + function handleDBFilterInput(e) { + setSelectDBInputValue(e.target.value); + setFilterDBValue(e.target.value); + } - onNewDashboardClick = () => { - this.setState({isNewDashboardOpen: true}); + // TODO dump directly into return + function onNewDashboardClick() { + setIsNewDBOpen(true); } - onNewDashboardClose = () => { - this.setState({isNewDashboardOpen: false}); + // TODO dump directly into return + function onNewDashboardClose() { + this.setState(false); } - onNewDashboardSave = (newDashboard) => { + function onNewDashboardSave(newDashboard) { HttpClient.post([Settings.serverUrl, 'dashboard'], newDashboard) .then(response => HttpClient.handleResponse(response)) .then(data => { localStorage.setItem('dashboard', JSON.stringify(data)); - this.getDashboards(); - this.setState({ - isNewDashboardOpen: false, - selectedDashboard: data, - dashboardInputValue: data.title, - }, this.getWidgets); + getDashboards(); + setIsNewDBOpen(false); + setSelectedDB(data); + setSelectDBInputValue(data); }) .catch(error => console.log(error)); } - onDeleteDashboardClick = () => { - this.setState({isDeleteDashboardOpen: true}); + // TODO dump directly into return + function onDeleteDashboardClick() { + setIsDeleteDBOpen(true); } - onDeleteDashboardClose = () => { - this.setState({isDeleteDashboardOpen: false}); + // TODO dump directly into return + function onDeleteDashboardClose() { + setIsDeleteDBOpen(false); } - onDeleteDashboard = () => { - const { activeDashboard, setActiveDashboard } = this.context; + function onDeleteDashboard() { + const { activeDashboard, setActiveDashboard } = context; HttpClient.delete([Settings.serverUrl, 'dashboard', activeDashboard.id]) .then(response => HttpClient.handleResponse(response)) .then(() => { setActiveDashboard(); - this.getDashboards(); - this.setState({ - isDeleteDashboardOpen: false, - selectedDashboard: null - }); + getDashboards(); + setIsDeleteDBOpen(false); + setSelectedDB(null); }) .catch(error => console.log(error)); } - onDeleteWidget = () => { - HttpClient.delete([Settings.serverUrl, 'widget-config', this.state.currentWidgetId]) - .then(response => HttpClient.handleResponse(response)) - .then(() => { - this.getWidgets(); - this.setState({isDeleteWidgetOpen: false}); - }) - .catch(error => console.log(error)); + function onDeleteWidgetClick(id) { + setIsDeleteWidgetOpen(true); + setCurrentWidget(id) } - onEditWidgetSave = (editWidget) => { - const { primaryObject } = this.context; - if (!editWidget.project_id && primaryObject) { - editWidget.project_id = primaryObject.id; - } - this.setState({isEditModalOpen: false}); - editWidget.id = this.state.currentWidgetId - HttpClient.put([Settings.serverUrl, 'widget-config', this.state.currentWidgetId], "", editWidget) + function onDeleteWidget() { + HttpClient.delete([Settings.serverUrl, 'widget-config', currentWidget]) .then(response => HttpClient.handleResponse(response)) .then(() => { this.getWidgets(); + setIsDeleteWidgetOpen(false); }) .catch(error => console.log(error)); } - onEditWidgetClose = () => { - this.setState({isEditModalOpen: false}); + // TODO dump directly into return + function onDeleteWidgetClose() { + setIsDeleteWidgetOpen(false); } - - onEditWidgetClick = (id) => { - HttpClient.get([Settings.serverUrl, 'widget-config', id]) - .then(response => HttpClient.handleResponse(response)) - .then(data => { - this.setState({isEditModalOpen: true, currentWidgetId: id, editWidgetData: data}); - }) - .catch(error => console.log(error)); - + // TODO dump directly into return + function onNewWidgetClick() { + setIsNewWidgetOpen(true); } - - onEditWidgetClose = () => { - this.setState({isEditModalOpen: false}); + // TODO dump directly into return + function onNewWidgetClose() { + setIsNewWidgetOpen(false); } - onDeleteWidgetClick = (id) => { - this.setState({isDeleteWidgetOpen: true, currentWidgetId: id}); + function onNewWidgetSave(newWidget) { + const { primaryObject } = context; + if (!newWidget.project_id && primaryObject) { + newWidget.project_id = primaryObject.id; + } + HttpClient.post([Settings.serverUrl, 'widget-config'], newWidget) + .then(() => { getWidgets() }) + .catch(error => console.log(error)); + setIsNewWidgetOpen(false); } - onDeleteWidgetClose = () => { - this.setState({isDeleteWidgetOpen: false}); + function onEditWidgetSave(widget_data) { + const { primaryObject } = context; + if (!widget_data.project_id && primaryObject) { + widget_data.project_id = primaryObject.id; + } + setIsEditModalOpen({isEditModalOpen: false}); + widget_data.id = this.currentWidgetId + HttpClient.put([Settings.serverUrl, 'widget-config', currentWidget], "", widget_data) + .then(response => HttpClient.handleResponse(response)) + .then(() => {getWidgets()}) + .catch(error => console.log(error)); } - onAddWidgetClick = () => { - this.setState({isWidgetWizardOpen: true}); + function onEditWidgetClose() { + setIsEditModalOpen(false); } - onNewWidgetClose = () => { - this.setState({isWidgetWizardOpen: false}); - } + function onEditWidgetClick(id) { + HttpClient.get([Settings.serverUrl, 'widget-config', id]) + .then(response => HttpClient.handleResponse(response)) + .then(data => { + setIsEditModalOpen(true); + setCurrentWidget(id); + setEditWidgetData(data); + }) + .catch(error => console.log(error)); - onNewWidgetSave = (newWidget) => { - const { primaryObject } = this.context; - if (!newWidget.project_id && primaryObject) { - newWidget.project_id = primaryObject.id; - } - 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(); - } + function handleFilter() { + // previously componentDidUpdate in class component + let newSelectOptionsDashboard = dashboards; + if (filterDBValue) { + newSelectOptionsDashboard = dashboards.filter(menuItem => + String(menuItem.title).toLowerCase().includes(filterDBValue.toLowerCase()) + ); - componentDidUpdate(prevProps, prevState) { - if ( - prevState.filterValueDashboard !== this.state.filterValueDashboard - ) { - let newSelectOptionsDashboard = this.state.dashboards; - if (this.state.dashboardInputValue) { - newSelectOptionsDashboard = this.state.dashboards.filter(menuItem => - String(menuItem.title).toLowerCase().includes(this.state.filterValueDashboard.toLowerCase()) - ); - - if (!this.state.isDashboardSelectorOpen) { - this.setState({ isDashboardSelectorOpen: true }); - } + if (!isDBSelectorOpen) { + setIsDBSelectorOpen(true); } - - this.setState({ - filteredDashboards: newSelectOptionsDashboard, - }); } + + setFilteredDBs(newSelectOptionsDashboard); } - render() { - document.title = 'Dashboard | Ibutsu'; - const { widgets } = this.state; - const { primaryObject, activeDashboard } = this.context; - - const toggle = toggleRef => ( - - - - - {!!this.state.dashboardInputValue && ( + document.title = 'Dashboard | Ibutsu'; + + + const { primaryObject, activeDashboard } = context; + + return ( + + + + + + + Dashboard + + + + + + - )} - - - - ) - - return ( - - - - - - - Dashboard - - - - - - - - - - - - - - - - - + + + + - - - {!!primaryObject && !!activeDashboard && !!widgets && - - {widgets.map(widget => { - if (KNOWN_WIDGETS.includes(widget.widget)) { - return ( - - {(widget.type === "widget" && widget.widget === "jenkins-heatmap") && - this.onDeleteWidgetClick(widget.id)} - onEditClick={() => this.onEditWidgetClick(widget.id)} - /> - } - {(widget.type === "widget" && widget.widget === "filter-heatmap") && - this.onDeleteWidgetClick(widget.id)} - onEditClick={() => this.onEditWidgetClick(widget.id)} - /> - } - {(widget.type === "widget" && widget.widget === "run-aggregator") && - this.onDeleteWidgetClick(widget.id)} - onEditClick={() => this.onEditWidgetClick(widget.id)} - /> - } - {(widget.type === "widget" && widget.widget === "result-summary") && - this.onDeleteWidgetClick(widget.id)} - onEditClick={() => this.onEditWidgetClick(widget.id)} - /> - } - {(widget.type === "widget" && widget.widget === "result-aggregator") && - this.onDeleteWidgetClick(widget.id)} - onEditClick={() => this.onEditWidgetClick(widget.id)} - /> - } - {(widget.type === "widget" && widget.widget === "jenkins-line-chart") && - this.onDeleteWidgetClick(widget.id)} - onEditClick={() => this.onEditWidgetClick(widget.id)} - /> - } - {(widget.type === "widget" && widget.widget === "jenkins-bar-chart") && - this.onDeleteWidgetClick(widget.id)} - onEditClick={() => this.onEditWidgetClick(widget.id)} - /> - } - {(widget.type === "widget" && widget.widget === "importance-component") && - this.onDeleteWidgetClick(widget.id)} - onEditClick={() => this.onEditWidgetClick(widget.id)} - /> - } - - ); - } - else { - return ''; - } - })} - - } - {!!primaryObject && !activeDashboard && - - } headingLevel="h4" /> - - There is currently no dashboard selected. Please select a dashboard from the dropdown - in order to view widgets, or create a new dashboard. - - - - - - } - {(!!primaryObject && !!activeDashboard && widgets.length === 0) && - - } headingLevel="h4" /> - - This dashboard currently has no widgets defined.
Click on the "Add Widget" button - below to add a widget to this dashboard. -
- - - -
- } -
- - - Would you like to delete the current dashboard? ALL WIDGETS on the dashboard will also be deleted.} - isOpen={this.state.isDeleteDashboardOpen} - onDelete={this.onDeleteDashboard} - onClose={this.onDeleteDashboardClose} - /> - + + + + + + + + {!!primaryObject && !!activeDashboard && !!widgets && + + {widgets?.map(widget => { + if (KNOWN_WIDGETS.includes(widget.widget)) { + return ( + + {(widget.type === "widget" && widget.widget === "jenkins-heatmap") && + onDeleteWidgetClick(widget.id)} + onEditClick={() => onEditWidgetClick(widget.id)} + /> + } + {(widget.type === "widget" && widget.widget === "filter-heatmap") && + onDeleteWidgetClick(widget.id)} + onEditClick={() => onEditWidgetClick(widget.id)} + /> + } + {(widget.type === "widget" && widget.widget === "run-aggregator") && + onDeleteWidgetClick(widget.id)} + onEditClick={() => onEditWidgetClick(widget.id)} + /> + } + {(widget.type === "widget" && widget.widget === "result-summary") && + onDeleteWidgetClick(widget.id)} + onEditClick={() => onEditWidgetClick(widget.id)} + /> + } + {(widget.type === "widget" && widget.widget === "result-aggregator") && + onDeleteWidgetClick(widget.id)} + onEditClick={() => onEditWidgetClick(widget.id)} + /> + } + {(widget.type === "widget" && widget.widget === "jenkins-line-chart") && + onDeleteWidgetClick(widget.id)} + onEditClick={() => onEditWidgetClick(widget.id)} + /> + } + {(widget.type === "widget" && widget.widget === "jenkins-bar-chart") && + onDeleteWidgetClick(widget.id)} + onEditClick={() => onEditWidgetClick(widget.id)} + /> + } + {(widget.type === "widget" && widget.widget === "importance-component") && + onDeleteWidgetClick(widget.id)} + onEditClick={() => onEditWidgetClick(widget.id)} + /> + } + + ); + } + else { + return ''; + } + })} + + } + {!!primaryObject && !activeDashboard && + + } headingLevel="h4" /> + + There is currently no dashboard selected. Please select a dashboard from the dropdown + in order to view widgets, or create a new dashboard. + + + + + + } + {(!!primaryObject && !!activeDashboard && widgets.length === 0) && + + } headingLevel="h4" /> + + This dashboard currently has no widgets defined.
Click on the "Add Widget" button + below to add a widget to this dashboard. +
+ + + +
+ } +
+ + + Would you like to delete the current dashboard? ALL WIDGETS on the dashboard will also be deleted.} + isOpen={isDeleteDBOpen} + onDelete={onDeleteDashboard} + onClose={onDeleteDashboardClose} + /> + + {isEditModalOpen ? + - {this.state.isEditModalOpen ? - - : ''} -
- ); - } + : ''} + + ); } + +Dashboard.propTypes = { +}; + +export default Dashboard; + + + // TODO replaced by context forcing render on project selection changing? + // props.eventEmitter.on('projectChange', (value) => { + // this.getDashboards(value); + // this.getDefaultDashboard(value); + // }); + + + +// TODO what actually needs synced now... + // sync_context = () => { + // // Active dashboard + // const { activeDashboard, setActiveDashboard } = this.context; + // const { selectedDashboard } = this.state; + // const paramDash = this.props.params?.dashboard_id; + // if (!paramDash) { + // // No dashboard in the URL, clear context + // setActiveDashboard(); + // } + // 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)); + // } + + // if (updatedDash && !selectedDashboard ) { + // this.setState({ + // selectedDashboard: updatedDash, + // dashboardInputValue: updatedDash.title + // }) + // } + // }