diff --git a/security-admin/src/main/webapp/react-webapp/package-lock.json b/security-admin/src/main/webapp/react-webapp/package-lock.json index e372ac8f2f..a08f908d16 100644 --- a/security-admin/src/main/webapp/react-webapp/package-lock.json +++ b/security-admin/src/main/webapp/react-webapp/package-lock.json @@ -16,6 +16,7 @@ "babel-loader": "^8.2.2", "bootstrap": "^5.3.2", "bootstrap-switch-button-react": "^1.2.0", + "chart.js": "^4.1.2", "classnames": "^2.3.1", "clipboard-polyfill": "^4.0.1", "create-react-class": "^15.7.0", @@ -35,6 +36,7 @@ "qs": "^6.15.2", "react": "^18.2.0", "react-bootstrap": "^2.10.0", + "react-chartjs-2": "^5.2.0", "react-datetime": "^3.2.0", "react-dom": "^18.2.0", "react-final-form": "^6.5.9", @@ -2037,6 +2039,13 @@ "tslib": "2" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "dev": true, + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -3414,6 +3423,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -6457,6 +6479,17 @@ } } }, + "node_modules/react-chartjs-2": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-datetime": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/react-datetime/-/react-datetime-3.2.0.tgz", @@ -9742,6 +9775,12 @@ "@jsonjoy.com/codegen": "^1.0.0" } }, + "@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "dev": true + }, "@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -10814,6 +10853,15 @@ "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", "dev": true }, + "chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "dev": true, + "requires": { + "@kurkle/color": "^0.3.0" + } + }, "chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -12991,6 +13039,13 @@ "warning": "^4.0.3" } }, + "react-chartjs-2": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", + "dev": true, + "requires": {} + }, "react-datetime": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/react-datetime/-/react-datetime-3.2.0.tgz", diff --git a/security-admin/src/main/webapp/react-webapp/package.json b/security-admin/src/main/webapp/react-webapp/package.json index 1b3092df08..e6b9bd935a 100644 --- a/security-admin/src/main/webapp/react-webapp/package.json +++ b/security-admin/src/main/webapp/react-webapp/package.json @@ -24,6 +24,7 @@ "babel-loader": "^8.2.2", "bootstrap": "^5.3.2", "bootstrap-switch-button-react": "^1.2.0", + "chart.js": "^4.1.2", "classnames": "^2.3.1", "clipboard-polyfill": "^4.0.1", "create-react-class": "^15.7.0", @@ -43,6 +44,7 @@ "qs": "^6.15.2", "react": "^18.2.0", "react-bootstrap": "^2.10.0", + "react-chartjs-2": "^5.2.0", "react-datetime": "^3.2.0", "react-dom": "^18.2.0", "react-final-form": "^6.5.9", diff --git a/security-admin/src/main/webapp/react-webapp/src/App.jsx b/security-admin/src/main/webapp/react-webapp/src/App.jsx index 520dba6df9..265e5ec934 100755 --- a/security-admin/src/main/webapp/react-webapp/src/App.jsx +++ b/security-admin/src/main/webapp/react-webapp/src/App.jsx @@ -84,6 +84,7 @@ const PluginStatusLogs = lazy( const UserSyncLogs = lazy( () => import("Views/AuditEvent/UserSync/UserSyncLogs") ); +const MetricsLogs = lazy(() => import("Views/AuditEvent/Metrics/MetricsLogs")); const PolicyListingTabView = lazy( () => import("Views/PolicyListing/PolicyListingTabView") ); @@ -343,6 +344,7 @@ export default class App extends Component { } /> } /> } /> + } /> {/* AUDIT LOGS DETAILS VIEW */} tr > td:hover:first-child:after { width: 6px; } +.btn-default-selected { + color: #0b7fad; + border-color: #0b7fad; + background-color: #fff; + outline: none; +} + /* SideBar */ .wrapper { display: flex; diff --git a/security-admin/src/main/webapp/react-webapp/src/utils/XAEnums.js b/security-admin/src/main/webapp/react-webapp/src/utils/XAEnums.js index b0f73fa904..5b07e14e8d 100755 --- a/security-admin/src/main/webapp/react-webapp/src/utils/XAEnums.js +++ b/security-admin/src/main/webapp/react-webapp/src/utils/XAEnums.js @@ -657,7 +657,8 @@ export const PathAssociateWithModule = { "/reports/audit/agent", "/reports/audit/pluginStatus", "/reports/audit/userSync", - "/reports/audit/eventlog/:eventId" + "/reports/audit/eventlog/:eventId", + "/reports/audit/metric" ], "Security Zone": [ "/zones/zone/list", diff --git a/security-admin/src/main/webapp/react-webapp/src/views/AuditEvent/AuditLayout.jsx b/security-admin/src/main/webapp/react-webapp/src/views/AuditEvent/AuditLayout.jsx index 7d6d8b5013..7d9c5d41ee 100644 --- a/security-admin/src/main/webapp/react-webapp/src/views/AuditEvent/AuditLayout.jsx +++ b/security-admin/src/main/webapp/react-webapp/src/views/AuditEvent/AuditLayout.jsx @@ -88,8 +88,10 @@ class AuditLayout extends Component { this.props.location.pathname == "/reports/audit/pluginStatus" ) { activeTabVal = "pluginStatus"; - } else { + } else if (this.props.location.pathname == "/reports/audit/userSync") { activeTabVal = "userSync"; + } else { + activeTabVal = "metric"; } } return activeTabVal; @@ -112,6 +114,7 @@ class AuditLayout extends Component { + { + const { metricData } = props; + const [loader, setLoader] = useState(false); + const [hrsMetricsInfo, setHrsMetricsInfo] = useState([]); + const [daysMetricsInfo, setDaysMetricsInfo] = useState([]); + const [showHideGraph, setShowHideGraph] = useState(true); + const hrsRef = useRef(); + + useEffect(() => { + hrsRef.current.click(); + }, []); + + ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend + ); + + const params = { + serviceName: metricData.serviceName, + serviceType: metricData.serviceType, + clusterName: metricData.clusterName, + clientIP: metricData.clientIP, + appId: metricData.appId + }; + + const hrsMetrics = async () => { + setLoader(true); + let hrsMetricData = []; + try { + hrsMetricData = await fetchApi({ + url: "audit/dailymetrics", + params: params + }); + setHrsMetricsInfo(hrsMetricData.data.auditMetricsByHours); + } catch (error) { + serverError(error); + } + setLoader(false); + }; + + const daysMetrics = async () => { + setLoader(true); + let daysMetricInfo = []; + try { + daysMetricInfo = await fetchApi({ + url: "audit/daysmetrics", + params: params + }); + + setDaysMetricsInfo(daysMetricInfo.data.auditMetricsByDays); + } catch (error) { + serverError(error); + } + setLoader(false); + }; + + const hrsOptions = { + responsive: true, + plugins: { + legend: { + position: "top" + } + } + }; + const hrsLabels = hrsMetricsInfo?.map((metrics) => { + return `${metrics.hours} hrs`; + }); + + const hrsData = { + labels: hrsLabels, + datasets: [ + { + label: "Audit Metrics By Hours", + data: hrsMetricsInfo?.map((metrics) => { + return metrics.numberOfAudits; + }), + backgroundColor: "#0f3554", + borderColor: "#0f3554" + } + ] + }; + + const dayOptions = { + responsive: true, + plugins: { + legend: { + position: "top" + } + } + }; + const dayLabels = daysMetricsInfo?.map((metrics) => { + return dateFormat(parseInt(metrics.auditDate), "mm/dd/yyyy"); + }); + + const dayData = { + labels: dayLabels, + datasets: [ + { + label: "Audit Metrics By Day", + data: daysMetricsInfo?.map((metrics) => { + return metrics.numberOfAudits; + }), + backgroundColor: "#0f3554", + borderColor: "#0f3554" + } + ] + }; + + const NoDataFound = () => { + return ( +
+
+ No Metrics Data found +
+
+ ); + }; + + return loader ? ( + + ) : ( + <> +
+ + +
+ + {showHideGraph ? ( + !isEmpty(hrsMetricsInfo) ? ( +
+ +
+ ) : ( + + ) + ) : !isEmpty(daysMetricsInfo) ? ( +
+ +
+ ) : ( + + )} + + ); +}; + +export default MetricGraphs; diff --git a/security-admin/src/main/webapp/react-webapp/src/views/AuditEvent/Metrics/MetricsLogs.jsx b/security-admin/src/main/webapp/react-webapp/src/views/AuditEvent/Metrics/MetricsLogs.jsx new file mode 100644 index 0000000000..a901b005f9 --- /dev/null +++ b/security-admin/src/main/webapp/react-webapp/src/views/AuditEvent/Metrics/MetricsLogs.jsx @@ -0,0 +1,407 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useCallback, useEffect, useReducer } from "react"; +import { + useSearchParams, + useOutletContext, + useLocation +} from "react-router-dom"; +import { Table, Modal, Button, Row, Col } from "react-bootstrap"; +import XATableLayout from "Components/XATableLayout"; +import { fetchApi } from "Utils/fetchAPI"; +import moment from "moment-timezone"; +import StructuredFilter from "Components/structured-filter/react-typeahead/tokenizer"; +import { + fetchSearchFilterParams, + parseSearchFilter, + serverError, + isKeyAdmin, + isKMSAuditor +} from "Utils/XAUtils"; +import { AuditFilterEntries, Loader } from "Components/CommonComponents"; +import { toUpper, sortBy, filter, map, cloneDeep, pick } from "lodash"; +import MetricGraphs from "./MetricGraphs"; +import { getServiceDef } from "Utils/appState"; +import { ACTIONS } from "Views/AuditEvent/action"; +import { reducer, METRICS_INITIAL_STATE } from "Views/AuditEvent/reducer"; + +function MetricsLogs() { + const [state, dispatch] = useReducer(reducer, METRICS_INITIAL_STATE); + + const location = useLocation(); + + const { services, servicesAvailable } = useOutletContext(); + + const { allServiceDefs } = cloneDeep(getServiceDef()); + const isKMSRole = isKeyAdmin() || isKMSAuditor(); + + const [searchParams, setSearchParams] = useSearchParams(); + + useEffect(() => { + if (servicesAvailable !== null) { + const { searchFilterParam, defaultSearchFilterParam, searchParam } = + fetchSearchFilterParams("metrics", searchParams, searchFilterOptions); + + // Updating the states for search params, search filter, default search filter and localStorage + setSearchParams(searchParam, { replace: true }); + if ( + JSON.stringify(state.searchFilterParams) !== + JSON.stringify(searchFilterParam) + ) { + dispatch({ + type: ACTIONS.SET_SEARCH_FILTER_PARAMS, + searchFilterParams: searchFilterParam, + refreshTableData: moment.now() + }); + } + dispatch({ + type: ACTIONS.SET_DEFAULT_SEARCH_FILTER_PARAMS, + defaultSearchFilterParams: defaultSearchFilterParam + }); + localStorage.setItem("metrics", JSON.stringify(searchParam)); + dispatch({ type: ACTIONS.SET_CONTENT_LOADER, contentLoader: false }); + } + }, [location.search, servicesAvailable]); + + const fetchMetricInfo = useCallback( + async ({ pageSize, pageIndex, gotoPage }) => { + dispatch({ type: ACTIONS.SET_TABLE_LOADER, loader: true }); + if (servicesAvailable !== null) { + const params = { + ...state.searchFilterParams, + pageSize, + startIndex: pageIndex * pageSize + }; + + try { + const response = await fetchApi({ + url: "audit/metrics", + params: params + }); + + const logsEntries = pick(response.data, [ + "startIndex", + "pageSize", + "totalCount", + "resultSize" + ]); + const logsResp = response.data?.rangerAuditMetricsList || []; + const totalCount = response.data?.totalCount || 0; + + dispatch({ + type: ACTIONS.SET_TABLE_DATA, + tableListingData: logsResp, + entries: logsEntries, + pageCount: Math.ceil(totalCount / pageSize), + resetPage: { page: gotoPage } + }); + } catch (error) { + serverError(error); + console.error(`Error occurred while fetching Metric logs! ${error}`); + } + + dispatch({ type: ACTIONS.SET_TABLE_LOADER, loader: false }); + } + }, + [state.refreshTableData, servicesAvailable] + ); + + const refreshTable = () => { + dispatch({ + type: ACTIONS.SET_SEARCH_FILTER_PARAMS, + searchFilterParams: state.searchFilterParams, + refreshTableData: moment.now() + }); + }; + + const closeMetricModal = () => { + dispatch({ + type: ACTIONS.SHOW_METRIC_MODAL, + showMetricModal: false, + metricData: null + }); + }; + + const openMetricGraphModal = (metricData) => { + dispatch({ + type: ACTIONS.SHOW_METRIC_GRAPH_MODAL, + showMetricGraphModal: true, + metricGraphData: metricData + }); + }; + + const closeMetricGraphModal = () => { + dispatch({ + type: ACTIONS.SHOW_METRIC_GRAPH_MODAL, + showMetricGraphModal: false, + metricGraphData: state.metricGraphData + }); + }; + + const columns = React.useMemo( + () => [ + { + Header: "Service Name", + accessor: "serviceName", + Cell: (rawValue) => { + return rawValue?.value ? rawValue.value :
--
; + }, + disableSortBy: true + }, + { + Header: "Service Type", + accessor: "serviceType", + Cell: (rawValue) => { + return rawValue?.value ? rawValue.value :
--
; + }, + disableSortBy: true + }, + { + Header: "Application Type", + accessor: "appId", + Cell: (rawValue) => { + return rawValue?.value ? ( + {rawValue.value} + ) : ( +
"--"
+ ); + }, + disableSortBy: true + }, + { + Header: "Cluster Name", + accessor: "clusterName", + Cell: (rawValue) => { + return rawValue?.value ? ( + {rawValue.value} + ) : ( +
--
+ ); + }, + disableSortBy: true + }, + { + Header: "Client IP", + accessor: "clientIP", + Cell: (rawValue) => { + return rawValue?.value ? ( + {rawValue.value} + ) : ( +
"--"
+ ); + }, + disableSortBy: true + }, + { + Header: "Metrics Graph", + accessor: "metricsGraph", + Cell: (rawValue) => { + return ( +
+ +
+ ); + }, + disableSortBy: true + } + ], + [] + ); + + const updateSearchFilter = (filter) => { + const { searchFilterParam, searchParam } = parseSearchFilter( + filter, + searchFilterOptions + ); + + dispatch({ + type: ACTIONS.SET_SEARCH_FILTER_PARAMS, + searchFilterParams: searchFilterParam, + refreshTableData: moment.now() + }); + + setSearchParams(searchParam, { replace: true }); + localStorage.setItem("metrics", JSON.stringify(searchParam)); + + if (typeof state.resetPage?.page === "function") { + state.resetPage.page(0); + } + }; + + const getServiceDefType = () => { + let serviceDefType = []; + + serviceDefType = map(allServiceDefs, function (serviceDef) { + return { + label: toUpper(serviceDef.displayName), + value: serviceDef.name + }; + }); + + return serviceDefType; + }; + + const getServices = () => { + let servicesName = []; + servicesName = filter(services, function (service) { + return !isKMSRole + ? service.type !== "tag" && service.type !== "kms" + : service.type !== "tag"; + }); + + return sortBy(servicesName, "name")?.map((service) => ({ + label: service.displayName, + value: service.name + })); + }; + + const searchFilterOptions = [ + { + category: "serviceName", + label: "Service Name", + urlLabel: "serviceName", + type: "textoptions", + options: getServices + }, + { + category: "serviceType", + label: "Service Type", + urlLabel: "serviceType", + type: "textoptions", + options: getServiceDefType + } + ]; + + const showMetricDetails = (metrics) => { + let metricText = metrics?.metricsText; + if (metricText !== undefined) { + for (let val in metricText) { + return ( + <> + {val} + {JSON.stringify(metricText[val])} + + ); + } + } else { + return ( + +
+
No Data Founds
+
+ + ); + } + }; + + return state.contentLoader ? ( + + ) : ( +
+ + + +
+ +
+ +
+ + + + + + + + +

Metrics Details

+
+
+ + + + + + + + + + {showMetricDetails(state.metricData)} + +
NameValue
+
+ + + +
+ + + + +

Metric Graph

+
+
+ + + + + + +
+
+
+ ); +} + +export default MetricsLogs; diff --git a/security-admin/src/main/webapp/react-webapp/src/views/AuditEvent/action.js b/security-admin/src/main/webapp/react-webapp/src/views/AuditEvent/action.js index 9379f102ae..002dae416d 100644 --- a/security-admin/src/main/webapp/react-webapp/src/views/AuditEvent/action.js +++ b/security-admin/src/main/webapp/react-webapp/src/views/AuditEvent/action.js @@ -26,6 +26,8 @@ export const ACTIONS = { SHOW_POLICY_MODAL: "show-policy-modal", SHOW_ROW_MODAL: "show-row-modal", SHOW_SESSION_MODAL: "show-session-modal", + SHOW_METRIC_MODAL: "show-metric-modal", + SHOW_METRIC_GRAPH_MODAL: "show-metric-graph-modal", SHOW_SYNC_TABLE_MODAL: "show-sync-table-modal", SET_SECURITY_ZONES: "set-security-zones" }; diff --git a/security-admin/src/main/webapp/react-webapp/src/views/AuditEvent/reducer.js b/security-admin/src/main/webapp/react-webapp/src/views/AuditEvent/reducer.js index b1223d4d2c..96d3cedf07 100644 --- a/security-admin/src/main/webapp/react-webapp/src/views/AuditEvent/reducer.js +++ b/security-admin/src/main/webapp/react-webapp/src/views/AuditEvent/reducer.js @@ -65,6 +65,14 @@ export const PLUGIN_STATUS_INITIAL_STATE = { ...INITIAL_STATE }; +export const METRICS_INITIAL_STATE = { + ...INITIAL_STATE, + showMetricModal: false, + metricData: null, + showMetricGraphModal: false, + metricGraphData: null +}; + export const USERSYNC_INITIAL_STATE = { ...INITIAL_STATE, showSyncTableModal: false, @@ -121,6 +129,18 @@ export const reducer = (state, action) => { showSessionModal: action.showSessionModal, sessionId: action.sessionId }; + case ACTIONS.SHOW_METRIC_MODAL: + return { + ...state, + showMetricModal: action.showMetricModal, + metricData: action.metricData + }; + case ACTIONS.SHOW_METRIC_GRAPH_MODAL: + return { + ...state, + showMetricGraphModal: action.showMetricGraphModal, + metricGraphData: action.metricGraphData + }; case ACTIONS.SHOW_SYNC_TABLE_MODAL: return { ...state, diff --git a/security-admin/src/main/webapp/react-webapp/src/views/SideBar/SideBarBody.jsx b/security-admin/src/main/webapp/react-webapp/src/views/SideBar/SideBarBody.jsx index a8e87f8ab3..1a1daacb1b 100644 --- a/security-admin/src/main/webapp/react-webapp/src/views/SideBar/SideBarBody.jsx +++ b/security-admin/src/main/webapp/react-webapp/src/views/SideBar/SideBarBody.jsx @@ -538,6 +538,17 @@ export const SideBarBody = (props) => { User Sync +
  • + { + props.closeCollapse(); + }} + className="list-group-item" + > + Metrics + +
  • )}