From 868ef0d50c032ff5dca4cf64c15bd4e36dd78119 Mon Sep 17 00:00:00 2001 From: Ewoud Kohl van Wijngaarden Date: Fri, 22 Dec 2023 15:30:37 +0100 Subject: [PATCH 1/4] Add host monitoring results to the REST API This allows querying of the host results, which can be used in the UI or otherwise. --- .../v2/hosts_monitoring_results_controller.rb | 26 +++++++++++++++++++ app/models/monitoring_result.rb | 2 ++ .../hosts_monitoring_results/base.json.rabl | 7 +++++ .../hosts_monitoring_results/index.json.rabl | 3 +++ config/routes.rb | 9 ++++++- 5 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 app/controllers/api/v2/hosts_monitoring_results_controller.rb create mode 100644 app/views/api/v2/hosts_monitoring_results/base.json.rabl create mode 100644 app/views/api/v2/hosts_monitoring_results/index.json.rabl diff --git a/app/controllers/api/v2/hosts_monitoring_results_controller.rb b/app/controllers/api/v2/hosts_monitoring_results_controller.rb new file mode 100644 index 0000000..40d3261 --- /dev/null +++ b/app/controllers/api/v2/hosts_monitoring_results_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Api + module V2 + class HostsMonitoringResultsController < ::Api::V2::BaseController + include ::Api::Version2 + + api :GET, "/hosts/:host_id/monitoring/results", N_('Get the monitoring results') + param :host_id, :identifier_dottable, required: true + def index + @monitoring_results = resource_scope + end + + private + + def resource_class + MonitoringResult + end + + def resource_scope(*args) + # TODO: only show for hosts with monitoring enabled? + resource_class.authorized(:view_monitoring_results).where(host_id: params[:host_id]) + end + end + end +end diff --git a/app/models/monitoring_result.rb b/app/models/monitoring_result.rb index ed41202..2e9704a 100644 --- a/app/models/monitoring_result.rb +++ b/app/models/monitoring_result.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class MonitoringResult < ApplicationRecord + include Authorizable + enum :result => { :ok => 0, :warning => 1, :critical => 2, :unknown => 3 } belongs_to_host diff --git a/app/views/api/v2/hosts_monitoring_results/base.json.rabl b/app/views/api/v2/hosts_monitoring_results/base.json.rabl new file mode 100644 index 0000000..bad17e9 --- /dev/null +++ b/app/views/api/v2/hosts_monitoring_results/base.json.rabl @@ -0,0 +1,7 @@ +object @monitoring_result + +attributes :id, :service, :status, :result, :downtime, :acknowledged, :timestamp + +node :status_label do |result| + result.to_label +end diff --git a/app/views/api/v2/hosts_monitoring_results/index.json.rabl b/app/views/api/v2/hosts_monitoring_results/index.json.rabl new file mode 100644 index 0000000..0987252 --- /dev/null +++ b/app/views/api/v2/hosts_monitoring_results/index.json.rabl @@ -0,0 +1,3 @@ +collection @monitoring_results + +extends "api/v2/hosts_monitoring_results/base" diff --git a/config/routes.rb b/config/routes.rb index 9e1598c..d3fd4c3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,9 +5,16 @@ scope '(:apiv)', :module => :v2, :defaults => { :apiv => 'v2' }, :apiv => /v1|v2/, - :constraints => ApiConstraints.new(:version => 2) do + :constraints => ApiConstraints.new(:version => 2, default: true) do resources :monitoring_results, :only => [:create] resources :downtime, :only => [:create] + resources :hosts, param: :host_id, only: [] do + member do + scope 'monitoring' do + resources :results, controller: :hosts_monitoring_results, as: :host_monitoring_results, only: [:index] + end + end + end end end From 7f525b2a2232d4781b34b55dec84e9d3247760f1 Mon Sep 17 00:00:00 2001 From: Ewoud Kohl van Wijngaarden Date: Fri, 22 Dec 2023 15:32:17 +0100 Subject: [PATCH 2/4] Add a monitoring tab on the modern host details page --- lib/foreman_monitoring/engine.rb | 2 + package.json | 39 +++++ webpack/fills_index.js | 3 + webpack/index.js | 0 webpack/src/Extends/Fills/index.js | 26 ++++ .../src/Extends/Host/MonitoringTab/index.js | 139 ++++++++++++++++++ 6 files changed, 209 insertions(+) create mode 100644 package.json create mode 100644 webpack/fills_index.js create mode 100644 webpack/index.js create mode 100644 webpack/src/Extends/Fills/index.js create mode 100644 webpack/src/Extends/Host/MonitoringTab/index.js diff --git a/lib/foreman_monitoring/engine.rb b/lib/foreman_monitoring/engine.rb index 9502ea2..78ecad4 100644 --- a/lib/foreman_monitoring/engine.rb +++ b/lib/foreman_monitoring/engine.rb @@ -22,6 +22,8 @@ class Engine < ::Rails::Engine Foreman::Plugin.register :foreman_monitoring do requires_foreman '>= 3.0' + register_global_js_file 'fills' + settings do category(:monitoring, N_('Monitoring')) do setting('monitoring_affect_global_status', diff --git a/package.json b/package.json new file mode 100644 index 0000000..1828503 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "foreman_monitoring", + "version": "1.0.0", + "description": "DESCRIPTION", + "main": "index.js", + "scripts": { + "lint": "tfm-lint --plugin -d /webpack", + "test": "tfm-test --plugin", + "test:watch": "tfm-test --plugin --watchAll", + "test:current": "tfm-test --plugin --watch", + "publish-coverage": "tfm-publish-coverage", + "create-react-component": "yo react-domain" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/theforeman/foreman_monitoring.git" + }, + "bugs": { + "url": "http://projects.theforeman.org/projects/foreman_monitoring/issues" + }, + "peerDependencies": { + "@theforeman/vendor": ">= 12.1.0" + }, + "devDependencies": { + "@babel/core": "^7.7.0", + "@sheerun/mutationobserver-shim": "^0.3.3", + "@theforeman/builder": ">= 10.1.0", + "@theforeman/eslint-plugin-foreman": ">= 10.1.0", + "@theforeman/find-foreman": ">= 10.1.0", + "@theforeman/test": ">= 10.1.0", + "@theforeman/vendor-dev": ">= 10.1.0", + "babel-eslint": "^10.0.3", + "eslint": "^6.7.2", + "jed": "^1.1.1", + "prettier": "^1.19.1", + "stylelint-config-standard": "^18.0.0", + "stylelint": "^9.3.0" + } +} diff --git a/webpack/fills_index.js b/webpack/fills_index.js new file mode 100644 index 0000000..6779286 --- /dev/null +++ b/webpack/fills_index.js @@ -0,0 +1,3 @@ +import { registerFills } from './src/Extends/Fills'; + +registerFills(); diff --git a/webpack/index.js b/webpack/index.js new file mode 100644 index 0000000..e69de29 diff --git a/webpack/src/Extends/Fills/index.js b/webpack/src/Extends/Fills/index.js new file mode 100644 index 0000000..706f3b1 --- /dev/null +++ b/webpack/src/Extends/Fills/index.js @@ -0,0 +1,26 @@ +import React from 'react'; +import { translate as __ } from 'foremanReact/common/I18n'; +import { addGlobalFill } from 'foremanReact/components/common/Fill/GlobalFill'; +import MonitoringTab from '../Host/MonitoringTab'; + +const fills = [ + { + slot: 'host-details-page-tabs', + name: 'Monitoring', + component: props => , + weight: 500, + metadata: { title: __('Monitoring') }, + }, +]; + +export const registerFills = () => { + fills.forEach(({ slot, name, component: Component, weight, metadata }) => + addGlobalFill( + slot, + name, + , + weight, + metadata + ) + ); +}; diff --git a/webpack/src/Extends/Host/MonitoringTab/index.js b/webpack/src/Extends/Host/MonitoringTab/index.js new file mode 100644 index 0000000..2c8fa07 --- /dev/null +++ b/webpack/src/Extends/Host/MonitoringTab/index.js @@ -0,0 +1,139 @@ +import React, { useEffect, useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import { + DescriptionList, + DescriptionListTerm, + DescriptionListGroup, + DescriptionListDescription, + Grid, + GridItem, +} from '@patternfly/react-core'; +import { Table, Thead, Tbody, Tr, Th, Td } from '@patternfly/react-table'; +import { STATUS } from 'foremanReact/constants'; +import { translate as __ } from 'foremanReact/common/I18n'; +import { get } from 'foremanReact/redux/API'; +import { selectAPIStatus, selectAPIResponse } from 'foremanReact/redux/API/APISelectors'; +import Loading from 'foremanReact/components/Loading'; +import SkeletonLoader from 'foremanReact/components/common/SkeletonLoader'; +import DefaultLoaderEmptyState from 'foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState'; +import CardTemplate from 'foremanReact/components/HostDetails/Templates/CardItem/CardTemplate'; + +const STATUS_ICONS = { + 'ok': 'pficon-ok status-ok', + 'warning': 'pficon-info status-warn', + 'critical': 'pficon-error-circle-o status-error', +} + +const status_icon_class = (result_status) => { + return STATUS_ICONS[result_status] || 'pficon-help status-question'; +}; + +const MonitoringResults = ({ hostId }) => { + const dispatch = useDispatch(); + const API_KEY = `get-monitoring-results-{hostId}`; + const status = useSelector(state => selectAPIStatus(state, API_KEY)); + const { results, itemCount, response } = useSelector(state => + selectAPIResponse(state, API_KEY) + ); + + const fetchResults = useCallback( + () => { + if (!hostId) return; + dispatch( + get({ + key: API_KEY, + url: `/api/hosts/${hostId}/monitoring/results`, + params: { + per_page: 'all', + }, + }) + ); + }, + [API_KEY, dispatch, hostId], + ); + + useEffect(() => { + fetchResults(); + }, [fetchResults]); + + if (response?.status === 403) { + return ; + } + + return ( + + + + + + + + + } + status={status || STATUS.PENDING} + > + {results && results.map((result) => ( + + + + + ))} + + +
{__('Service')}{__('Status')}
{result.service} {result.status_label}
+ ); +}; + +MonitoringResults.propTypes = { + hostId: PropTypes.number.isrequired, +}; + +const MonitoringTab = ({ + response: { + id: hostId, + monitoring_proxy_id: monitoringProxyId, + monitoring_proxy_name: monitoringProxyName, + }, + status, +}) => ( + + + + + + {__('Monitoring Proxy')} + + } + status={status} + > + {monitoringProxyId && ({monitoringProxyName})} + + + + + + + + } + status={status} + > + {monitoringProxyId && hostId && ()} + + + +); + +MonitoringTab.propTypes = { + response: PropTypes.object, + status: PropTypes.string, +}; +MonitoringTab.defaultProps = { + response: {}, + status: STATUS.PENDING, +}; + +export default MonitoringTab; From 5d026a221aae15d1b1c54e1508ba68467fb9d8c8 Mon Sep 17 00:00:00 2001 From: Ewoud Kohl van Wijngaarden Date: Thu, 28 Dec 2023 14:26:29 +0100 Subject: [PATCH 3/4] Address review feedback --- .../src/Extends/Host/MonitoringTab/index.js | 127 +++++++++--------- 1 file changed, 64 insertions(+), 63 deletions(-) diff --git a/webpack/src/Extends/Host/MonitoringTab/index.js b/webpack/src/Extends/Host/MonitoringTab/index.js index 2c8fa07..5576eb5 100644 --- a/webpack/src/Extends/Host/MonitoringTab/index.js +++ b/webpack/src/Extends/Host/MonitoringTab/index.js @@ -1,4 +1,4 @@ -import React, { useEffect, useCallback } from 'react'; +import React, { createElement } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { @@ -9,85 +9,86 @@ import { Grid, GridItem, } from '@patternfly/react-core'; +import { + CheckCircleIcon, + ExclamationCircleIcon, + ExclamationTriangleIcon, + QuestionCircleIcon, +} from '@patternfly/react-icons'; import { Table, Thead, Tbody, Tr, Th, Td } from '@patternfly/react-table'; import { STATUS } from 'foremanReact/constants'; import { translate as __ } from 'foremanReact/common/I18n'; -import { get } from 'foremanReact/redux/API'; -import { selectAPIStatus, selectAPIResponse } from 'foremanReact/redux/API/APISelectors'; +import { useAPI } from 'foremanReact/common/hooks/API/APIHooks'; +import PermissionDenied from 'foremanReact/components/PermissionDenied'; import Loading from 'foremanReact/components/Loading'; import SkeletonLoader from 'foremanReact/components/common/SkeletonLoader'; import DefaultLoaderEmptyState from 'foremanReact/components/HostDetails/DetailsCard/DefaultLoaderEmptyState'; import CardTemplate from 'foremanReact/components/HostDetails/Templates/CardItem/CardTemplate'; const STATUS_ICONS = { - 'ok': 'pficon-ok status-ok', - 'warning': 'pficon-info status-warn', - 'critical': 'pficon-error-circle-o status-error', -} + 'ok': CheckCircleIcon, + 'warning': ExclamationTriangleIcon, + 'critical': ExclamationCircleIcon, +}; -const status_icon_class = (result_status) => { - return STATUS_ICONS[result_status] || 'pficon-help status-question'; +const STATUS_STYLES = { + 'ok': 'ok', + 'warning': 'warn', + 'critical': 'critical', +}; + +const status_icon = (result_status) => { + const cls = STATUS_ICONS[result_status] || QuestionCircleIcon; + const style = STATUS_STYLES[result_status] || 'question'; + return createElement(cls, { className: `status-${style}` }); }; const MonitoringResults = ({ hostId }) => { - const dispatch = useDispatch(); - const API_KEY = `get-monitoring-results-{hostId}`; - const status = useSelector(state => selectAPIStatus(state, API_KEY)); - const { results, itemCount, response } = useSelector(state => - selectAPIResponse(state, API_KEY) - ); + const { + response, + status, + } = useAPI('get', `/api/hosts/${hostId}/monitoring/results`, `get-monitoring-results-${hostId}`); - const fetchResults = useCallback( - () => { - if (!hostId) return; - dispatch( - get({ - key: API_KEY, - url: `/api/hosts/${hostId}/monitoring/results`, - params: { - per_page: 'all', - }, - }) + switch (status) { + case STATUS.PENDING: { + return + } + case STATUS.ERROR: { + // In case of an error, response is an Error object + if (response.response?.status === 403) { + return ; + } else { + // TODO + } + } + case STATUS.RESOLVED: { + return ( + + + + + + + + + {response?.results?.map((result) => ( + + + + + ))} + +
{__('Service')}{__('Status')}
{result.service}{status_icon(result.status)} {result.status_label}
); - }, - [API_KEY, dispatch, hostId], - ); - - useEffect(() => { - fetchResults(); - }, [fetchResults]); - - if (response?.status === 403) { - return ; + } + default: { + return __('N/A'); + } } - - return ( - - - - - - - - - } - status={status || STATUS.PENDING} - > - {results && results.map((result) => ( - - - - - ))} - - -
{__('Service')}{__('Status')}
{result.service} {result.status_label}
- ); }; MonitoringResults.propTypes = { - hostId: PropTypes.number.isrequired, + hostId: PropTypes.number.isRequired, }; const MonitoringTab = ({ @@ -109,7 +110,7 @@ const MonitoringTab = ({ emptyState={} status={status} > - {monitoringProxyId && ({monitoringProxyName})} + {monitoringProxyId && {monitoringProxyName}} @@ -121,7 +122,7 @@ const MonitoringTab = ({ emptyState={} status={status} > - {monitoringProxyId && hostId && ()} + {hostId && monitoringProxyId && } From 329e40895448976a9e53cd8abf69628f4e910c26 Mon Sep 17 00:00:00 2001 From: Ewoud Kohl van Wijngaarden Date: Thu, 28 Dec 2023 16:27:47 +0100 Subject: [PATCH 4/4] Rewrite to a SkeletonLoader --- .../src/Extends/Host/MonitoringTab/index.js | 62 ++++++++----------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/webpack/src/Extends/Host/MonitoringTab/index.js b/webpack/src/Extends/Host/MonitoringTab/index.js index 5576eb5..81cb6a6 100644 --- a/webpack/src/Extends/Host/MonitoringTab/index.js +++ b/webpack/src/Extends/Host/MonitoringTab/index.js @@ -43,48 +43,40 @@ const status_icon = (result_status) => { return createElement(cls, { className: `status-${style}` }); }; +const ErrorHandler = ({ response }) => { + // In case of an error, response is an Error object + if (response.response?.status === 403) { + return ; + } + // TODO +} + const MonitoringResults = ({ hostId }) => { const { response, status, } = useAPI('get', `/api/hosts/${hostId}/monitoring/results`, `get-monitoring-results-${hostId}`); - switch (status) { - case STATUS.PENDING: { - return - } - case STATUS.ERROR: { - // In case of an error, response is an Error object - if (response.response?.status === 403) { - return ; - } else { - // TODO - } - } - case STATUS.RESOLVED: { - return ( - - - - - + return ( + errorNode=> +
{__('Service')}{__('Status')}
+ + + + + + + + {response?.results?.map((result) => ( + + + - - - {response?.results?.map((result) => ( - - - - - ))} - -
{__('Service')}{__('Status')}
{result.service}{status_icon(result.status)} {result.status_label}
{result.service}{status_icon(result.status)} {result.status_label}
- ); - } - default: { - return __('N/A'); - } - } + ))} + + + + ); }; MonitoringResults.propTypes = {