diff --git a/src/routes/optimizations/optimizationsBreakdown/optimizationsBreakdown.scss b/.archive/routes/optimizations/optimizationsBreakdown/optimizationsBreakdown.scss similarity index 100% rename from src/routes/optimizations/optimizationsBreakdown/optimizationsBreakdown.scss rename to .archive/routes/optimizations/optimizationsBreakdown/optimizationsBreakdown.scss diff --git a/.archive/routes/optimizations/optimizationsBreakdown/optimizationsBreakdown.styles.ts b/.archive/routes/optimizations/optimizationsBreakdown/optimizationsBreakdown.styles.ts new file mode 100644 index 000000000..696df518c --- /dev/null +++ b/.archive/routes/optimizations/optimizationsBreakdown/optimizationsBreakdown.styles.ts @@ -0,0 +1,24 @@ +import global_BackgroundColor_light_100 from '@patternfly/react-tokens/dist/js/global_BackgroundColor_light_100'; +import global_spacer_lg from '@patternfly/react-tokens/dist/js/global_spacer_lg'; +import global_spacer_md from '@patternfly/react-tokens/dist/js/global_spacer_md'; +import type React from 'react'; + +export const styles = { + alertContainer: { + marginBottom: global_spacer_lg.value, + }, + codeBlock: { + display: 'flex', + }, + container: { + minHeight: '100vh', + }, + currentActions: { + height: '36px', + }, + pagination: { + backgroundColor: global_BackgroundColor_light_100.value, + paddingBottom: global_spacer_md.value, + paddingTop: global_spacer_md.value, + }, +} as { [className: string]: React.CSSProperties }; diff --git a/.archive/routes/optimizations/optimizationsBreakdown/optimizationsBreakdown.tsx b/.archive/routes/optimizations/optimizationsBreakdown/optimizationsBreakdown.tsx new file mode 100644 index 000000000..732b67acd --- /dev/null +++ b/.archive/routes/optimizations/optimizationsBreakdown/optimizationsBreakdown.tsx @@ -0,0 +1,195 @@ +import './optimizationsBreakdown.scss'; + +import { Alert, List, ListItem, PageSection } from '@patternfly/react-core'; +import type { Query } from 'api/queries/query'; +import { parseQuery } from 'api/queries/query'; +import type { RosQuery } from 'api/queries/rosQuery'; +import type { RecommendationItem, RecommendationReportData } from 'api/ros/recommendations'; +import { RosPathsType, RosType } from 'api/ros/ros'; +import type { AxiosError } from 'axios'; +import messages from 'locales/messages'; +import React, { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { useDispatch, useSelector } from 'react-redux'; +import { useLocation } from 'react-router-dom'; +import type { AnyAction } from 'redux'; +import type { ThunkDispatch } from 'redux-thunk'; +import { Loading } from 'routes/components/page/loading'; +import type { RootState } from 'store'; +import { FetchStatus } from 'store/common'; +import { rosActions, rosSelectors } from 'store/ros'; +import { breadcrumbLabelKey } from 'utils/props'; +import { getNotifications, hasRecommendation } from 'utils/recomendations'; +import type { RouterComponentProps } from 'utils/router'; + +import { styles } from './optimizationsBreakdown.styles'; +import { OptimizationsBreakdownConfiguration } from './optimizationsBreakdownConfiguration'; +import { OptimizationsBreakdownHeader } from './optimizationsBreakdownHeader'; + +interface OptimizationsBreakdownOwnProps extends RouterComponentProps { + id?: string; +} + +interface OptimizationsBreakdownStateProps { + breadcrumbLabel?: string; + report?: RecommendationReportData; + reportError?: AxiosError; + reportFetchStatus?: FetchStatus; + reportQueryString?: string; +} + +export interface OptimizationsBreakdownMapProps { + query?: RosQuery; +} + +type OptimizationsBreakdownProps = OptimizationsBreakdownOwnProps & OptimizationsBreakdownStateProps; + +// eslint-disable-next-line no-shadow +export const enum Interval { + short_term = 'short_term', // last 24 hrs + medium_term = 'medium_term', // last 7 days + long_term = 'long_term', // last 15 days +} + +const reportType = RosType.ros as any; +const reportPathsType = RosPathsType.recommendation as any; + +const OptimizationsBreakdown: React.FC = () => { + const { breadcrumbLabel, report, reportFetchStatus } = useMapToProps(); + const intl = useIntl(); + const location = useLocation(); + + const getDefaultTerm = () => { + let result = Interval.short_term; + if (!report?.recommendations?.duration_based) { + return result; + } + + const recommendation = report.recommendations.duration_based; + if (hasRecommendation(recommendation.short_term)) { + result = Interval.short_term; + } else if (hasRecommendation(recommendation.medium_term)) { + result = Interval.medium_term; + } else if (hasRecommendation(recommendation.long_term)) { + result = Interval.long_term; + } + return result as Interval; + }; + + const [currentInterval, setCurrentInterval] = useState(getDefaultTerm()); + + const getAlert = () => { + let notifications; + if (report?.recommendations?.duration_based?.[currentInterval]) { + notifications = getNotifications(report.recommendations.duration_based[currentInterval]); + } + + if (!notifications) { + return null; + } + + return ( +
+ + + {notifications.map((notification, index) => ( + {notification.message} + ))} + + +
+ ); + }; + + const getRecommendationTerm = (): RecommendationItem => { + if (!report) { + return undefined; + } + + let result; + switch (currentInterval) { + case Interval.short_term: + result = report.recommendations.duration_based.short_term; + break; + case Interval.medium_term: + result = report.recommendations.duration_based.medium_term; + break; + case Interval.long_term: + result = report.recommendations.duration_based.long_term; + break; + } + return result; + }; + + const handleOnSelected = (value: Interval) => { + setCurrentInterval(value); + }; + + const isLoading = reportFetchStatus === FetchStatus.inProgress; + + return ( +
+ + + {isLoading ? ( + + ) : ( + <> + {getAlert()} + + + )} + +
+ ); +}; + +const useQueryFromRoute = () => { + const location = useLocation(); + return parseQuery(location.search); +}; + +// eslint-disable-next-line no-empty-pattern +const useMapToProps = (): OptimizationsBreakdownStateProps => { + const dispatch: ThunkDispatch = useDispatch(); + const queryFromRoute = useQueryFromRoute(); + + const reportQueryString = queryFromRoute ? queryFromRoute.id : ''; + const report: any = useSelector((state: RootState) => + rosSelectors.selectRos(state, reportPathsType, reportType, reportQueryString) + ); + const reportFetchStatus = useSelector((state: RootState) => + rosSelectors.selectRosFetchStatus(state, reportPathsType, reportType, reportQueryString) + ); + const reportError = useSelector((state: RootState) => + rosSelectors.selectRosError(state, reportPathsType, reportType, reportQueryString) + ); + + useEffect(() => { + if (!reportError && reportFetchStatus !== FetchStatus.inProgress) { + dispatch(rosActions.fetchRosReport(reportPathsType, reportType, reportQueryString)); + } + }, [reportQueryString]); + + return { + breadcrumbLabel: queryFromRoute[breadcrumbLabelKey], + report, + reportError, + reportFetchStatus, + reportQueryString, + }; +}; + +export default OptimizationsBreakdown; diff --git a/src/routes/optimizations/optimizationsBreakdown/optimizationsBreakdownConfiguration.tsx b/.archive/routes/optimizations/optimizationsBreakdown/optimizationsBreakdownConfiguration.tsx similarity index 100% rename from src/routes/optimizations/optimizationsBreakdown/optimizationsBreakdownConfiguration.tsx rename to .archive/routes/optimizations/optimizationsBreakdown/optimizationsBreakdownConfiguration.tsx diff --git a/src/routes/optimizations/optimizationsBreakdown/optimizationsBreakdownHeader.styles.ts b/.archive/routes/optimizations/optimizationsBreakdown/optimizationsBreakdownHeader.styles.ts similarity index 100% rename from src/routes/optimizations/optimizationsBreakdown/optimizationsBreakdownHeader.styles.ts rename to .archive/routes/optimizations/optimizationsBreakdown/optimizationsBreakdownHeader.styles.ts diff --git a/src/routes/optimizations/optimizationsBreakdown/optimizationsBreakdownHeader.tsx b/.archive/routes/optimizations/optimizationsBreakdown/optimizationsBreakdownHeader.tsx similarity index 100% rename from src/routes/optimizations/optimizationsBreakdown/optimizationsBreakdownHeader.tsx rename to .archive/routes/optimizations/optimizationsBreakdown/optimizationsBreakdownHeader.tsx diff --git a/src/routes/optimizations/optimizationsBreakdown/optimizationsBreakdownToolbar.tsx b/.archive/routes/optimizations/optimizationsBreakdown/optimizationsBreakdownToolbar.tsx similarity index 100% rename from src/routes/optimizations/optimizationsBreakdown/optimizationsBreakdownToolbar.tsx rename to .archive/routes/optimizations/optimizationsBreakdown/optimizationsBreakdownToolbar.tsx diff --git a/.archive/routes/optimizations/optimizationsDetails/optimizationsDetails.tsx b/.archive/routes/optimizations/optimizationsDetails/optimizationsDetails.tsx new file mode 100644 index 000000000..dc64b0761 --- /dev/null +++ b/.archive/routes/optimizations/optimizationsDetails/optimizationsDetails.tsx @@ -0,0 +1,248 @@ +import { PageSection, Pagination, PaginationVariant } from '@patternfly/react-core'; +import type { Query } from 'api/queries/query'; +import { getQuery, parseQuery } from 'api/queries/query'; +import type { RosQuery } from 'api/queries/rosQuery'; +import type { RosReport } from 'api/ros/ros'; +import { RosPathsType, RosType } from 'api/ros/ros'; +import type { AxiosError } from 'axios'; +import messages from 'locales/messages'; +import React, { useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { useDispatch, useSelector } from 'react-redux'; +import { useLocation } from 'react-router-dom'; +import type { AnyAction } from 'redux'; +import type { ThunkDispatch } from 'redux-thunk'; +import { routes } from 'routes'; +import { OptimizationsTable, OptimizationsToolbar } from 'routes/components/optimizations'; +import { Loading } from 'routes/components/page/loading'; +import { NoOptimizations } from 'routes/components/page/noOptimizations'; +import { NotAvailable } from 'routes/components/page/notAvailable'; +import { getGroupById, getGroupByValue } from 'routes/utils/groupBy'; +import { getOrderById, getOrderByValue } from 'routes/utils/orderBy'; +import * as queryUtils from 'routes/utils/query'; +import { clearQueryState, getQueryState } from 'routes/utils/queryState'; +import type { RootState } from 'store'; +import { FetchStatus } from 'store/common'; +import { rosActions, rosSelectors } from 'store/ros'; +import { formatPath } from 'utils/paths'; + +import { styles } from './optimizationsDetails.styles'; +import { OptimizationsDetailsHeader } from './optimizationsDetailsHeader'; + +interface OptimizationsDetailsOwnProps { + // TBD... +} + +export interface OptimizationsDetailsStateProps { + groupBy?: string; + report: RosReport; + reportError: AxiosError; + reportFetchStatus: FetchStatus; + reportQueryString: string; +} + +export interface OptimizationsDetailsMapProps { + query?: RosQuery; +} + +type OptimizationsDetailsProps = OptimizationsDetailsOwnProps; + +const baseQuery: RosQuery = { + limit: 10, + offset: 0, + order_by: { + last_reported: 'desc', + }, +}; + +const reportType = RosType.ros as any; +const reportPathsType = RosPathsType.recommendations as any; + +const OptimizationsDetails: React.FC = () => { + const intl = useIntl(); + const location = useLocation(); + + const queryState = getQueryState(location, 'optimizations'); + const [query, setQuery] = useState({ ...baseQuery, ...(queryState && queryState) }); + const { groupBy, report, reportError, reportFetchStatus, reportQueryString } = useMapToProps({ + query, + }); + + // Clear queryState, returned from breakdown page, after query has been initialized + useEffect(() => { + clearQueryState(location, 'optimizations'); + }, [reportQueryString]); + + const getPagination = (isDisabled = false, isBottom = false) => { + const count = report?.meta ? report.meta.count : 0; + const limit = report?.meta ? report.meta.limit : baseQuery.limit; + const offset = report?.meta ? report.meta.offset : baseQuery.offset; + const page = Math.trunc(offset / limit + 1); + + return ( + handleOnPerPageSelect(perPage)} + onSetPage={(event, pageNumber) => handleOnSetPage(pageNumber)} + page={page} + perPage={limit} + titles={{ + paginationAriaLabel: intl.formatMessage(messages.paginationTitle, { + title: intl.formatMessage(messages.openShift), + placement: isBottom ? 'bottom' : 'top', + }), + }} + variant={isBottom ? PaginationVariant.bottom : PaginationVariant.top} + widgetId={`exports-pagination${isBottom ? '-bottom' : ''}`} + /> + ); + }; + + const getTable = () => { + return ( + handleOnSort(sortType, isSortAscending)} + orderBy={query.order_by} + query={query} + report={report} + reportQueryString={reportQueryString} + /> + ); + }; + + const getToolbar = () => { + const itemsPerPage = report?.meta ? report.meta.limit : 0; + const itemsTotal = report?.meta ? report.meta.count : 0; + const isDisabled = itemsTotal === 0; + + return ( + handleOnFilterAdded(filter)} + onFilterRemoved={filter => handleOnFilterRemoved(filter)} + pagination={getPagination(isDisabled)} + query={query} + /> + ); + }; + + const handleOnFilterAdded = filter => { + const newQuery = queryUtils.handleOnFilterAdded(query, filter); + setQuery(newQuery); + }; + + const handleOnFilterRemoved = filter => { + const newQuery = queryUtils.handleOnFilterRemoved(query, filter); + setQuery(newQuery); + }; + + const handleOnPerPageSelect = perPage => { + const newQuery = queryUtils.handleOnPerPageSelect(query, perPage, true); + setQuery(newQuery); + }; + + const handleOnSetPage = pageNumber => { + const newQuery = queryUtils.handleOnSetPage(query, report, pageNumber, true); + setQuery(newQuery); + }; + + const handleOnSort = (sortType, isSortAscending) => { + const newQuery = queryUtils.handleOnSort(query, sortType, isSortAscending); + setQuery(newQuery); + }; + + const itemsTotal = report?.meta ? report.meta.count : 0; + const isDisabled = itemsTotal === 0; + const title = intl.formatMessage(messages.optimizations); + const hasOptimizations = report?.meta && report.meta.count > 0; + + if (reportError) { + return ; + } + if (!query.filter_by && !hasOptimizations && reportFetchStatus === FetchStatus.complete) { + return ; + } + return ( +
+ + + {getToolbar()} + {reportFetchStatus === FetchStatus.inProgress ? ( + + ) : ( + <> + {getTable()} +
{getPagination(isDisabled, true)}
+ + )} +
+
+ ); +}; + +const useQueryFromRoute = () => { + const location = useLocation(); + return parseQuery(location.search); +}; + +// eslint-disable-next-line no-empty-pattern +const useMapToProps = ({ query }: OptimizationsDetailsMapProps): OptimizationsDetailsStateProps => { + const dispatch: ThunkDispatch = useDispatch(); + const queryFromRoute = useQueryFromRoute(); + + const groupBy = getGroupById(queryFromRoute); + const groupByValue = getGroupByValue(queryFromRoute); + const order_by = getOrderById(query) || getOrderById(baseQuery); + const order_how = getOrderByValue(query) || getOrderByValue(baseQuery); + + const reportQuery = { + ...(groupBy && { + [groupBy]: groupByValue, // Flattened project filter + }), + ...query.filter_by, // Flattened filter by + limit: query.limit, + offset: query.offset, + order_by, // Flattened order by + order_how, // Flattened order how + }; + const reportQueryString = getQuery(reportQuery); + const report = useSelector((state: RootState) => + rosSelectors.selectRos(state, reportPathsType, reportType, reportQueryString) + ); + const reportFetchStatus = useSelector((state: RootState) => + rosSelectors.selectRosFetchStatus(state, reportPathsType, reportType, reportQueryString) + ); + const reportError = useSelector((state: RootState) => + rosSelectors.selectRosError(state, reportPathsType, reportType, reportQueryString) + ); + + useEffect(() => { + if (!reportError && reportFetchStatus !== FetchStatus.inProgress) { + dispatch(rosActions.fetchRosReport(reportPathsType, reportType, reportQueryString)); + } + }, [query]); + + return { + groupBy, + report, + reportError, + reportFetchStatus, + reportQueryString, + }; +}; + +export default OptimizationsDetails; diff --git a/src/routes/optimizations/optimizationsDetails/optimizationsDetailsHeader.styles.ts b/.archive/routes/optimizations/optimizationsDetails/optimizationsDetailsHeader.styles.ts similarity index 100% rename from src/routes/optimizations/optimizationsDetails/optimizationsDetailsHeader.styles.ts rename to .archive/routes/optimizations/optimizationsDetails/optimizationsDetailsHeader.styles.ts diff --git a/src/routes/optimizations/optimizationsDetails/optimizationsDetailsHeader.tsx b/.archive/routes/optimizations/optimizationsDetails/optimizationsDetailsHeader.tsx similarity index 100% rename from src/routes/optimizations/optimizationsDetails/optimizationsDetailsHeader.tsx rename to .archive/routes/optimizations/optimizationsDetails/optimizationsDetailsHeader.tsx diff --git a/jest.config.js b/jest.config.js index fb9d7e8d2..5ef0d27ae 100644 --- a/jest.config.js +++ b/jest.config.js @@ -23,5 +23,5 @@ module.exports = { '^.+\\.[jt]sx?$': '/test/transformTS.js', '^.+\\.(jpg)$': '/test/transformFile.js', }, - transformIgnorePatterns: ['node_modules/(?!@patternfly/react-icons/dist/esm)'], + transformIgnorePatterns: ['node_modules/(?!(@patternfly/react-icons/dist/esm|uuid/dist/esm-browser))'], }; diff --git a/package-lock.json b/package-lock.json index b24e5cb84..f2910f021 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "@redhat-cloud-services/frontend-components-translations": "^3.2.7", "@redhat-cloud-services/frontend-components-utilities": "^4.0.2", "@redhat-cloud-services/rbac-client": "^1.2.12", - "@unleash/proxy-client-react": "^4.1.0", + "@unleash/proxy-client-react": "^4.1.1", "axios": "^1.6.2", "date-fns": "^2.30.0", "js-file-download": "^0.4.12", @@ -32,7 +32,7 @@ "react-dom": "^18.2.0", "react-intl": "^6.5.5", "react-redux": "^8.1.3", - "react-router-dom": "^6.19.0", + "react-router-dom": "^6.20.0", "redux": "^4.2.1", "redux-thunk": "^2.4.2", "typesafe-actions": "^5.1.0", @@ -46,18 +46,18 @@ "@formatjs/ecma402-abstract": "^1.18.0", "@formatjs/icu-messageformat-parser": "^2.7.3", "@redhat-cloud-services/eslint-config-redhat-cloud-services": "^2.0.3", - "@redhat-cloud-services/frontend-components-config": "^6.0.5", + "@redhat-cloud-services/frontend-components-config": "^6.0.6", "@redhat-cloud-services/tsc-transform-imports": "^1.0.4", "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.5.1", - "@types/jest": "^29.5.8", + "@types/jest": "^29.5.10", "@types/qs": "^6.9.10", - "@types/react": "^18.2.37", - "@types/react-dom": "^18.2.15", - "@types/react-redux": "^7.1.30", + "@types/react": "^18.2.39", + "@types/react-dom": "^18.2.17", + "@types/react-redux": "^7.1.31", "@types/react-router-dom": "^5.3.3", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "@typescript-eslint/parser": "^6.11.0", + "@typescript-eslint/eslint-plugin": "^6.13.1", + "@typescript-eslint/parser": "^6.13.1", "@xstate/test": "^0.5.1", "aphrodite": "^2.4.0", "copy-webpack-plugin": "^11.0.0", @@ -71,7 +71,7 @@ "eslint-plugin-react": "^7.33.2", "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-sort-keys-fix": "^1.1.2", - "eslint-plugin-testing-library": "^6.1.2", + "eslint-plugin-testing-library": "^6.2.0", "git-revision-webpack-plugin": "^5.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", @@ -81,7 +81,7 @@ "prettier": "^3.1.0", "rimraf": "^5.0.5", "ts-patch": "^3.0.2", - "typescript": "^5.2.2", + "typescript": "^5.3.2", "webpack-bundle-analyzer": "^4.10.1" }, "engines": { @@ -2415,9 +2415,9 @@ } }, "node_modules/@redhat-cloud-services/frontend-components-config": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/@redhat-cloud-services/frontend-components-config/-/frontend-components-config-6.0.5.tgz", - "integrity": "sha512-nBJue5FfClmEwdrEZJV6Wi4tiTP/n/AxDA5IwSxOTsquyK31YofIZtEPOuqVL3tDfEN8vJR0b1JUDe6X1ZOJ9g==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@redhat-cloud-services/frontend-components-config/-/frontend-components-config-6.0.6.tgz", + "integrity": "sha512-T0rZTPVm9Y8YMqQ0KqaiGLh4jOkRmli2ZkibTgZG8S08BXwUSEv7NTPV5XdqHOpJTPdY49P3MD5OTKpltinq6w==", "dev": true, "dependencies": { "@pmmmwh/react-refresh-webpack-plugin": "^0.5.8", @@ -2946,9 +2946,9 @@ "integrity": "sha512-P50stc+mnWLycID46/AKmD/760r5N1eoam//O6MUVriqVorUdht7xkUL78aJZU1vw8WW6xlrDHwz3F6BM148qg==" }, "node_modules/@remix-run/router": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.12.0.tgz", - "integrity": "sha512-2hXv036Bux90e1GXTWSMfNzfDDK8LA8JYEWfyHxzvwdp6GyoWEovKc9cotb3KCKmkdwsIBuFGX7ScTWyiHv7Eg==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.13.0.tgz", + "integrity": "sha512-5dMOnVnefRsl4uRnAdoWjtVTdh8e6aZqgM4puy9nmEADH72ck+uXwzpJLEKE9Q6F8ZljNewLgmTfkxUrBdv4WA==", "engines": { "node": ">=14.0.0" } @@ -3615,9 +3615,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.8", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz", - "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==", + "version": "29.5.10", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz", + "integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -3747,9 +3747,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.37", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.37.tgz", - "integrity": "sha512-RGAYMi2bhRgEXT3f4B92WTohopH6bIXw05FuGlmJEnv/omEn190+QYEIYxIAuIBdKgboYYdVved2p1AxZVQnaw==", + "version": "18.2.39", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz", + "integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3757,18 +3757,18 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.15", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.15.tgz", - "integrity": "sha512-HWMdW+7r7MR5+PZqJF6YFNSCtjz1T0dsvo/f1BV6HkV+6erD/nA7wd9NM00KVG83zf2nJ7uATPO9ttdIPvi3gg==", + "version": "18.2.17", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz", + "integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==", "devOptional": true, "dependencies": { "@types/react": "*" } }, "node_modules/@types/react-redux": { - "version": "7.1.30", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.30.tgz", - "integrity": "sha512-i2kqM6YaUwFKduamV6QM/uHbb0eCP8f8ZQ/0yWf+BsAVVsZPRYJ9eeGWZ3uxLfWwwA0SrPRMTPTqsPFkY3HZdA==", + "version": "7.1.31", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.31.tgz", + "integrity": "sha512-merF9AH72krBUekQY6uObXnMsEo1xTeZy9NONNRnqSwvwVe3HtLeRvNIPaKmPDIOWPsSFE51rc2WGpPMqmuCWg==", "dev": true, "dependencies": { "@types/hoist-non-react-statics": "^3.3.0", @@ -4001,16 +4001,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.11.0.tgz", - "integrity": "sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz", + "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.11.0", - "@typescript-eslint/type-utils": "6.11.0", - "@typescript-eslint/utils": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/type-utils": "6.13.1", + "@typescript-eslint/utils": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -4069,15 +4069,15 @@ "dev": true }, "node_modules/@typescript-eslint/parser": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.11.0.tgz", - "integrity": "sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz", + "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.11.0", - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/typescript-estree": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4" }, "engines": { @@ -4097,13 +4097,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.11.0.tgz", - "integrity": "sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0" + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -4114,13 +4114,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.11.0.tgz", - "integrity": "sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz", + "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.11.0", - "@typescript-eslint/utils": "6.11.0", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/utils": "6.13.1", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -4141,9 +4141,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.11.0.tgz", - "integrity": "sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -4154,13 +4154,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.11.0.tgz", - "integrity": "sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -4214,17 +4214,17 @@ "dev": true }, "node_modules/@typescript-eslint/utils": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.11.0.tgz", - "integrity": "sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", + "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.11.0", - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/typescript-estree": "6.11.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", "semver": "^7.5.4" }, "engines": { @@ -4272,12 +4272,12 @@ "dev": true }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.11.0.tgz", - "integrity": "sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", + "@typescript-eslint/types": "6.13.1", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -4307,9 +4307,9 @@ "dev": true }, "node_modules/@unleash/proxy-client-react": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@unleash/proxy-client-react/-/proxy-client-react-4.1.0.tgz", - "integrity": "sha512-4mdHtEDgjFtz7I+fQ5V3gdrDFJVhhymyV2J6ZC9G6xFPjOZGTKdA5ZJDFiHm3N23KeND/NUmdGSKJmz27b0LoQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@unleash/proxy-client-react/-/proxy-client-react-4.1.1.tgz", + "integrity": "sha512-lbKVGfS9G5ela/8Ei5g7FD+5Q3un61ha3Mk7OWmWTBteeu6GctzFl/zYHWSHzz4ZiayhfX/Km2UprR68Db9Tcg==", "engines": { "node": ">=16.0.0" }, @@ -9269,9 +9269,9 @@ } }, "node_modules/eslint-plugin-testing-library": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-6.1.2.tgz", - "integrity": "sha512-Ra16FeBlonfbScOIdZEta9o+OxtwDqiUt+4UCpIM42TuatyLdtfU/SbwnIzPcAszrbl58PGwyZ9YGU9dwIo/tA==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-6.2.0.tgz", + "integrity": "sha512-+LCYJU81WF2yQ+Xu4A135CgK8IszcFcyMF4sWkbiu6Oj+Nel0TrkZq/HvDw0/1WuO3dhDQsZA/OpEMGd0NfcUw==", "dev": true, "dependencies": { "@typescript-eslint/utils": "^5.58.0" @@ -17207,11 +17207,11 @@ } }, "node_modules/react-router": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.19.0.tgz", - "integrity": "sha512-0W63PKCZ7+OuQd7Tm+RbkI8kCLmn4GPjDbX61tWljPxWgqTKlEpeQUwPkT1DRjYhF8KSihK0hQpmhU4uxVMcdw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.20.0.tgz", + "integrity": "sha512-pVvzsSsgUxxtuNfTHC4IxjATs10UaAtvLGVSA1tbUE4GDaOSU1Esu2xF5nWLz7KPiMuW8BJWuPFdlGYJ7/rW0w==", "dependencies": { - "@remix-run/router": "1.12.0" + "@remix-run/router": "1.13.0" }, "engines": { "node": ">=14.0.0" @@ -17221,12 +17221,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.19.0.tgz", - "integrity": "sha512-N6dWlcgL2w0U5HZUUqU2wlmOrSb3ighJmtQ438SWbhB1yuLTXQ8yyTBMK3BSvVjp7gBtKurT554nCtMOgxCZmQ==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.20.0.tgz", + "integrity": "sha512-CbcKjEyiSVpA6UtCHOIYLUYn/UJfwzp55va4yEfpk7JBN3GPqWfHrdLkAvNCcpXr8QoihcDMuk0dzWZxtlB/mQ==", "dependencies": { - "@remix-run/router": "1.12.0", - "react-router": "6.19.0" + "@remix-run/router": "1.13.0", + "react-router": "6.20.0" }, "engines": { "node": ">=14.0.0" @@ -19600,9 +19600,9 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "devOptional": true, "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index ec2bcfb31..bc18f2f44 100644 --- a/package.json +++ b/package.json @@ -30,10 +30,11 @@ "postinstall": "ts-patch install && rm -rf .cache", "start": "fec dev", "start:csc": "CLOUD_SERVICES_CONFIG_PORT=8000 npm start", + "start:csc:mfe": "FEC_STATIC_PORT=8003 npm run start:csc", "start:ephemeral": "EPHEMERAL_PORT=8000 npm start", "start:hmr": "HMR=true npm start", "start:local:api": "LOCAL_API_PORT=8000 LOCAL_API_HOST=localhost KEYCLOAK_PORT=4020 npm start", - "start:mfe": "FEC_STATIC_PORT=8003 npm run start:csc", + "start:mfe": "FEC_STATIC_PORT=8003 npm run start", "stats": "npm run build:prod --profile --json > stats.json", "test": "jest --no-cache", "test:clean": "jest --clearCache", @@ -60,7 +61,7 @@ "@redhat-cloud-services/frontend-components-translations": "^3.2.7", "@redhat-cloud-services/frontend-components-utilities": "^4.0.2", "@redhat-cloud-services/rbac-client": "^1.2.12", - "@unleash/proxy-client-react": "^4.1.0", + "@unleash/proxy-client-react": "^4.1.1", "axios": "^1.6.2", "date-fns": "^2.30.0", "js-file-download": "^0.4.12", @@ -71,7 +72,7 @@ "react-dom": "^18.2.0", "react-intl": "^6.5.5", "react-redux": "^8.1.3", - "react-router-dom": "^6.19.0", + "react-router-dom": "^6.20.0", "redux": "^4.2.1", "redux-thunk": "^2.4.2", "typesafe-actions": "^5.1.0", @@ -85,18 +86,18 @@ "@formatjs/ecma402-abstract": "^1.18.0", "@formatjs/icu-messageformat-parser": "^2.7.3", "@redhat-cloud-services/eslint-config-redhat-cloud-services": "^2.0.3", - "@redhat-cloud-services/frontend-components-config": "^6.0.5", + "@redhat-cloud-services/frontend-components-config": "^6.0.6", "@redhat-cloud-services/tsc-transform-imports": "^1.0.4", "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.5.1", - "@types/jest": "^29.5.8", + "@types/jest": "^29.5.10", "@types/qs": "^6.9.10", - "@types/react": "^18.2.37", - "@types/react-dom": "^18.2.15", - "@types/react-redux": "^7.1.30", + "@types/react": "^18.2.39", + "@types/react-dom": "^18.2.17", + "@types/react-redux": "^7.1.31", "@types/react-router-dom": "^5.3.3", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "@typescript-eslint/parser": "^6.11.0", + "@typescript-eslint/eslint-plugin": "^6.13.1", + "@typescript-eslint/parser": "^6.13.1", "@xstate/test": "^0.5.1", "aphrodite": "^2.4.0", "copy-webpack-plugin": "^11.0.0", @@ -110,7 +111,7 @@ "eslint-plugin-react": "^7.33.2", "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-sort-keys-fix": "^1.1.2", - "eslint-plugin-testing-library": "^6.1.2", + "eslint-plugin-testing-library": "^6.2.0", "git-revision-webpack-plugin": "^5.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", @@ -120,11 +121,10 @@ "prettier": "^3.1.0", "rimraf": "^5.0.5", "ts-patch": "^3.0.2", - "typescript": "^5.2.2", + "typescript": "^5.3.2", "webpack-bundle-analyzer": "^4.10.1" }, - "overrides": { - }, + "overrides": {}, "insights": { "appname": "cost-management" } diff --git a/src/api/queries/rosQuery.ts b/src/api/queries/rosQuery.ts deleted file mode 100644 index cbe1edae8..000000000 --- a/src/api/queries/rosQuery.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as utils from './query'; - -export interface RosFilters extends utils.Filters { - project?: string | number; -} - -type RosGroupByValue = string | string[]; - -interface RosGroupBys { - cluster?: RosGroupByValue; - node?: RosGroupByValue; - project?: RosGroupByValue; -} - -export interface RosQuery extends utils.Query { - category?: string; - delta?: string; - filter?: RosFilters; - group_by?: RosGroupBys; - limit?: number; - offset?: number; - order_by?: any; -} - -// filter_by props are converted and returned with logical OR/AND prefix -export function getQuery(query: RosQuery) { - return utils.getQuery(query); -} - -// filter_by props are not converted -export function getQueryRoute(query: RosQuery) { - return utils.getQueryRoute(query); -} - -export function parseQuery(query: string): T { - return utils.parseQuery(query); -} diff --git a/src/api/ros/recommendations.test.ts b/src/api/ros/recommendations.test.ts deleted file mode 100644 index b7bb08ea1..000000000 --- a/src/api/ros/recommendations.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { RosType } from 'api/ros/ros'; -import axios from 'axios'; - -import { runRosReports } from './recommendations'; - -test('api run reports calls axios get', () => { - const query = 'limit=10'; - runRosReports(RosType.ros, query); - expect(axios.get).toBeCalledWith(`recommendations/openshift?${query}`); -}); diff --git a/src/api/ros/recommendations.ts b/src/api/ros/recommendations.ts deleted file mode 100644 index aef2f97f4..000000000 --- a/src/api/ros/recommendations.ts +++ /dev/null @@ -1,93 +0,0 @@ -import axios from 'axios'; - -import type { RosData, RosMeta, RosReport } from './ros'; -import { RosType } from './ros'; - -export interface RecommendationValue { - amount?: number; - format?: string; -} - -export interface Notification { - code?: number; - message?: string; - type?: string; -} - -export interface RecommendationItem { - config: { - limits: { - memory?: RecommendationValue; - cpu?: RecommendationValue; - }; - requests: { - memory?: RecommendationValue; - cpu?: RecommendationValue; - }; - }; - current: { - limits: { - memory?: RecommendationValue; - cpu?: RecommendationValue; - }; - requests: { - memory?: RecommendationValue; - cpu?: RecommendationValue; - }; - }; - variation: { - limits: { - memory?: RecommendationValue; - cpu?: RecommendationValue; - }; - requests: { - memory?: RecommendationValue; - cpu?: RecommendationValue; - }; - }; - duration_in_hours?: string; - monitoring_start_time?: string; - monitoring_end_time?: string; - notifications?: { - [key: string]: Notification; - }; -} - -export interface RecommendationItems { - short_term?: RecommendationItem; - medium_term?: RecommendationItem; - long_term?: RecommendationItem; -} - -export interface RecommendationReportData extends RosData { - recommendations?: { - duration_based?: RecommendationItems; - }; -} - -export interface RecommendationReportMeta extends RosMeta { - // TBD... -} - -export interface RecommendationReport extends RosReport { - meta: RecommendationReportMeta; - data: RecommendationReportData[]; -} - -export const RosTypePaths: Partial> = { - [RosType.ros]: 'recommendations/openshift', -}; - -// This fetches a recommendation by ID -export function runRosReport(reportType: RosType, query: string) { - const path = RosTypePaths[reportType]; - const queryString = query ? `/${query}` : ''; - return axios.get(`${path}${queryString}`); -} - -// This fetches a recommendations list -export function runRosReports(reportType: RosType, query: string) { - const path = RosTypePaths[reportType]; - const queryString = query ? `?${query}` : ''; - return axios.get(`${path}${queryString}`); -} diff --git a/src/api/ros/ros.ts b/src/api/ros/ros.ts deleted file mode 100644 index e11217507..000000000 --- a/src/api/ros/ros.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { PagedMetaData, PagedResponse } from 'api/api'; - -export interface RosData { - cluster_uuid?: string; - cluster_alias?: string; - container?: string; - id?: number; - project?: string; - last_reported?: string; - recommendations?: { - duration_based?: any; - }; - workload?: string; - workload_type?: string; -} - -export interface RosMeta extends PagedMetaData { - count: number; - limit?: number; - offset?: number; -} - -export interface RosReport extends PagedResponse {} - -// eslint-disable-next-line no-shadow -export const enum RosType { - ros = 'ros', -} - -// eslint-disable-next-line no-shadow -export const enum RosPathsType { - recommendation = 'recommendation', - recommendations = 'recommendations', -} diff --git a/src/api/ros/rosUtils.ts b/src/api/ros/rosUtils.ts deleted file mode 100644 index 2f5b64c25..000000000 --- a/src/api/ros/rosUtils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { runRosReport as runRecommendation, runRosReports as runRecommendations } from './recommendations'; -import type { RosType } from './ros'; -import { RosPathsType } from './ros'; - -export function runRosReport(rosPathsType: RosPathsType, rosType: RosType, query: string) { - let result; - switch (rosPathsType) { - case RosPathsType.recommendation: - result = runRecommendation(rosType, query); - break; - case RosPathsType.recommendations: - result = runRecommendations(rosType, query); - break; - } - return result; -} diff --git a/src/api/tags/tag.ts b/src/api/tags/tag.ts index 661a5a5b1..5b6c4f979 100644 --- a/src/api/tags/tag.ts +++ b/src/api/tags/tag.ts @@ -38,6 +38,4 @@ export const enum TagPathsType { ocp = 'ocp', ocpCloud = 'ocp_cloud', rhel = 'rhel', - // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values - ros = 'ocp', // Todo: Remove when APIs are available } diff --git a/src/routes.tsx b/src/routes.tsx index 0475b1b77..64a18df6f 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -29,7 +29,7 @@ const RhelDetails = lazy(() => import(/* webpackChunkName: "rhelDetails" */ 'rou const RhelBreakdown = lazy(() => import(/* webpackChunkName: "rhelBreakdown" */ 'routes/details/rhelBreakdown')); const Settings = lazy(() => import(/* webpackChunkName: "overview" */ 'routes/settings')); -const routes = { +export const routes = { awsBreakdown: { element: userAccess(AwsBreakdown), path: '/aws/breakdown', @@ -118,7 +118,7 @@ const routes = { }, }; -const Routes = () => ( +export const Routes = () => ( @@ -136,5 +136,3 @@ const Routes = () => ( ); - -export { routes, Routes }; diff --git a/src/routes/components/optimizations/index.ts b/src/routes/components/optimizations/index.ts deleted file mode 100644 index 9d8c4f63d..000000000 --- a/src/routes/components/optimizations/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './optimizationsBadge'; -export * from './optimizationsTable'; -export * from './optimizationsToolbar'; diff --git a/src/routes/components/optimizations/optimizationsBadge.tsx b/src/routes/components/optimizations/optimizationsBadge.tsx deleted file mode 100644 index 24b8abf37..000000000 --- a/src/routes/components/optimizations/optimizationsBadge.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Badge } from '@patternfly/react-core'; -import type { Query } from 'api/queries/query'; -import { getQuery } from 'api/queries/query'; -import { parseQuery } from 'api/queries/rosQuery'; -import type { RosReport } from 'api/ros/ros'; -import { RosPathsType, RosType } from 'api/ros/ros'; -import type { AxiosError } from 'axios'; -import messages from 'locales/messages'; -import React, { useEffect } from 'react'; -import { useIntl } from 'react-intl'; -import { useDispatch, useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; -import type { AnyAction } from 'redux'; -import type { ThunkDispatch } from 'redux-thunk'; -import { getGroupById, getGroupByValue } from 'routes/utils/groupBy'; -import type { RootState } from 'store'; -import { FetchStatus } from 'store/common'; -import { rosActions, rosSelectors } from 'store/ros'; - -export interface OptimizationsBadgeOwnProps { - // TBD... -} - -export interface OptimizationsBadgeStateProps { - report?: RosReport; - reportError?: AxiosError; - reportFetchStatus?: FetchStatus; - reportQueryString?: string; -} - -type OptimizationsBadgeProps = OptimizationsBadgeOwnProps & OptimizationsBadgeStateProps; - -const reportPathsType = RosPathsType.recommendations; -const reportType = RosType.ros; - -const OptimizationsBadge: React.FC = () => { - const { report } = useMapToProps(); - const intl = useIntl(); - - const count = report?.meta ? report.meta.count : 0; - - return {count}; -}; - -const useQueryFromRoute = () => { - const location = useLocation(); - return parseQuery(location.search); -}; - -const useMapToProps = (): OptimizationsBadgeStateProps => { - const dispatch: ThunkDispatch = useDispatch(); - const queryFromRoute = useQueryFromRoute(); - const groupBy = getGroupById(queryFromRoute); - const groupByValue = getGroupByValue(queryFromRoute); - - // Don't need pagination here - const reportQuery: any = { - ...(groupBy && { - [groupBy]: groupByValue, // project filter - }), - }; - - const reportQueryString = getQuery(reportQuery); - const report: any = useSelector((state: RootState) => - rosSelectors.selectRos(state, reportPathsType, reportType, reportQueryString) - ); - const reportFetchStatus = useSelector((state: RootState) => - rosSelectors.selectRosFetchStatus(state, reportPathsType, reportType, reportQueryString) - ); - const reportError = useSelector((state: RootState) => - rosSelectors.selectRosError(state, reportPathsType, reportType, reportQueryString) - ); - - useEffect(() => { - if (!reportError && reportFetchStatus !== FetchStatus.inProgress) { - dispatch(rosActions.fetchRosReport(reportPathsType, reportType, reportQueryString)); - } - }, [reportQueryString]); - - return { - report, - reportError, - reportFetchStatus, - reportQueryString, - }; -}; - -export { OptimizationsBadge }; diff --git a/src/routes/components/optimizations/optimizationsTable.tsx b/src/routes/components/optimizations/optimizationsTable.tsx deleted file mode 100644 index d95bc3860..000000000 --- a/src/routes/components/optimizations/optimizationsTable.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import 'routes/components/dataTable/dataTable.scss'; - -import { Icon } from '@patternfly/react-core'; -import { ExclamationTriangleIcon } from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon'; -import type { Query } from 'api/queries/query'; -import type { RecommendationReport } from 'api/ros/recommendations'; -import messages from 'locales/messages'; -import React, { useEffect, useState } from 'react'; -import { useIntl } from 'react-intl'; -import { Link, useLocation } from 'react-router-dom'; -import { DataTable } from 'routes/components/dataTable'; -import { styles } from 'routes/components/dataTable/dataTable.styles'; -import { NoOptimizationsState } from 'routes/components/page/noOptimizations/noOptimizationsState'; -import { getOptimizationsBreakdownPath } from 'routes/utils/paths'; -import { getTimeFromNow } from 'utils/dates'; -import { hasWarning } from 'utils/recomendations'; - -interface OptimizationsTableOwnProps { - basePath?: string; - breadcrumbLabel?: string; - breadcrumbPath?: string; - filterBy?: any; - groupBy?: string; - isLoading?: boolean; - onSort(value: string, isSortAscending: boolean); - orderBy?: any; - query?: Query; - report: RecommendationReport; - reportQueryString: string; -} - -type OptimizationsTableProps = OptimizationsTableOwnProps; - -const OptimizationsTable: React.FC = ({ - basePath, - breadcrumbLabel, - breadcrumbPath, - filterBy, - groupBy, - isLoading, - onSort, - orderBy, - query, - report, -}) => { - const intl = useIntl(); - const location = useLocation(); - - const [columns, setColumns] = useState([]); - const [rows, setRows] = useState([]); - - const initDatum = () => { - if (!report) { - return; - } - const hasData = report?.data && report.data.length > 0; - - const newRows = []; - const newColumns = [ - { - name: intl.formatMessage(messages.optimizationsNames, { value: 'container' }), - orderBy: 'container', - ...(hasData && { isSortable: true }), - }, - { - hidden: groupBy === 'project', - name: intl.formatMessage(messages.optimizationsNames, { value: 'project' }), - orderBy: 'project', - ...(hasData && { isSortable: true }), - }, - { - name: intl.formatMessage(messages.optimizationsNames, { value: 'workload' }), - orderBy: 'workload', - ...(hasData && { isSortable: true }), - }, - { - name: intl.formatMessage(messages.optimizationsNames, { value: 'workload_type' }), - orderBy: 'workload_type', - ...(hasData && { isSortable: true }), - }, - { - hidden: groupBy === 'cluster', - name: intl.formatMessage(messages.optimizationsNames, { value: 'cluster' }), - orderBy: 'cluster', - ...(hasData && { isSortable: true }), - }, - { - name: intl.formatMessage(messages.optimizationsNames, { value: 'last_reported' }), - orderBy: 'last_reported', - style: styles.lastItemColumn, - ...(hasData && { isSortable: true }), - }, - ]; - - hasData && - report.data.map(item => { - const cluster = item.cluster_alias ? item.cluster_alias : item.cluster_uuid ? item.cluster_uuid : ''; - const container = item.container ? item.container : ''; - const lastReported = getTimeFromNow(item.last_reported); - const project = item.project ? item.project : ''; - const workload = item.workload ? item.workload : ''; - const workloadType = item.workload_type ? item.workload_type : ''; - const showWarningIcon = hasWarning(item?.recommendations?.duration_based); - - newRows.push({ - cells: [ - { - value: ( - - {container} - - ), - }, - { value: project, hidden: groupBy === 'project' }, - { value: workload }, - { value: workloadType }, - { - value: ( - <> - {cluster} - {showWarningIcon && ( - - - - - - )} - - ), - hidden: groupBy === 'cluster', - }, - { value: lastReported, style: styles.lastItem }, - ], - optimization: { - container: item.container, - id: item.id, - project, - }, - }); - }); - - const filteredColumns = (newColumns as any[]).filter(column => !column.hidden); - const filteredRows = newRows.map(({ ...row }) => { - row.cells = row.cells.filter(cell => !cell.hidden); - return row; - }); - - setColumns(filteredColumns); - setRows(filteredRows); - }; - - const handleOnSort = (value: string, isSortAscending: boolean) => { - if (onSort) { - onSort(value, isSortAscending); - } - }; - - useEffect(() => { - initDatum(); - }, [report]); - - return ( - } - filterBy={filterBy} - isLoading={isLoading} - isSelectable={false} - onSort={handleOnSort} - orderBy={orderBy} - rows={rows} - /> - ); -}; - -export { OptimizationsTable }; diff --git a/src/routes/components/optimizations/optimizationsToolbar.tsx b/src/routes/components/optimizations/optimizationsToolbar.tsx deleted file mode 100644 index 37894244c..000000000 --- a/src/routes/components/optimizations/optimizationsToolbar.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import type { ToolbarChipGroup } from '@patternfly/react-core'; -import type { RosQuery } from 'api/queries/rosQuery'; -import messages from 'locales/messages'; -import React from 'react'; -import type { WrappedComponentProps } from 'react-intl'; -import { injectIntl } from 'react-intl'; -import { BasicToolbar } from 'routes/components/dataToolbar'; -import type { Filter } from 'routes/utils/filter'; - -interface OptimizationsToolbarOwnProps { - isDisabled?: boolean; - isProject?: boolean; - itemsPerPage?: number; - itemsTotal?: number; - onFilterAdded(filter: Filter); - onFilterRemoved(filter: Filter); - pagination?: React.ReactNode; - query?: RosQuery; -} - -interface OptimizationsToolbarState { - categoryOptions?: ToolbarChipGroup[]; -} - -type OptimizationsToolbarProps = OptimizationsToolbarOwnProps & WrappedComponentProps; - -class OptimizationsToolbarBase extends React.Component { - protected defaultState: OptimizationsToolbarState = {}; - public state: OptimizationsToolbarState = { ...this.defaultState }; - - public componentDidMount() { - this.setState({ - categoryOptions: this.getCategoryOptions(), - }); - } - - private getCategoryOptions = (): ToolbarChipGroup[] => { - const { intl, isProject } = this.props; - - const options = [ - { name: intl.formatMessage(messages.filterByValues, { value: 'container' }), key: 'container' }, - { name: intl.formatMessage(messages.filterByValues, { value: 'cluster' }), key: 'cluster' }, - { name: intl.formatMessage(messages.filterByValues, { value: 'project' }), key: 'project' }, - { name: intl.formatMessage(messages.filterByValues, { value: 'workload' }), key: 'workload' }, - { - name: intl.formatMessage(messages.filterByValues, { value: 'workload_type' }), - key: 'workload_type', - selectClassName: 'selectOverride', // A selector from routes/components/dataToolbar/dataToolbar.scss - selectOptions: [ - { name: 'daemonset', key: 'daemonset' }, - { name: 'deployment', key: 'deployment' }, - { name: 'deploymentconfig', key: 'deploymentconfig' }, - { name: 'replicaset', key: 'replicaset' }, - { name: 'replicationcontroller', key: 'replicationcontroller' }, - { name: 'statefulset', key: 'statefulset' }, - ], - }, - ]; - return isProject ? options : options.filter(option => option.key !== 'project'); - }; - - public render() { - const { isDisabled, itemsPerPage, itemsTotal, onFilterAdded, onFilterRemoved, pagination, query } = this.props; - const { categoryOptions } = this.state; - - return ( - - ); - } -} - -const OptimizationsToolbar = injectIntl(OptimizationsToolbarBase); - -export { OptimizationsToolbar }; diff --git a/src/routes/details/awsBreakdown/awsBreakdown.tsx b/src/routes/details/awsBreakdown/awsBreakdown.tsx index be3523a86..0e222831b 100644 --- a/src/routes/details/awsBreakdown/awsBreakdown.tsx +++ b/src/routes/details/awsBreakdown/awsBreakdown.tsx @@ -27,7 +27,7 @@ import { withRouter } from 'utils/router'; import { CostOverview } from './costOverview'; import { HistoricalData } from './historicalData'; -interface BreakdownDispatchProps { +interface AwsBreakdownDispatchProps { fetchReport?: typeof reportActions.fetchReport; } @@ -128,10 +128,8 @@ const mapStateToProps = createMapStateToProps((state, { intl, router }) => { +const mapStateToProps = createMapStateToProps((state, { intl, router }) => { const queryFromRoute = parseQuery(router.location.search); const queryState = getQueryState(router.location, 'details'); @@ -114,10 +114,8 @@ const mapStateToProps = createMapStateToProps { } private getAvailableTabs = () => { - const { - costOverviewComponent, - historicalDataComponent, - isRosFeatureEnabled, - optimizationsBadgeComponent, - optimizationsComponent, - } = this.props; + const { costOverviewComponent, historicalDataComponent, isRosFeatureEnabled, optimizationsComponent } = this.props; const availableTabs = []; if (costOverviewComponent) { @@ -146,15 +141,17 @@ class BreakdownBase extends React.Component { } if (optimizationsComponent && isRosFeatureEnabled) { availableTabs.push({ - badge: optimizationsBadgeComponent, contentRef: React.createRef(), + showBadge: true, tab: BreakdownTab.optimizations, }); } return availableTabs; }; - private getTab = (tab: BreakdownTab, contentRef, badge: React.ReactNode, index: number) => { + private getTab = (tab: BreakdownTab, contentRef, showBadge: boolean, index: number) => { + const { groupBy, groupByValue } = this.props; + return ( { title={ <> {this.getTabTitle(tab)} - {badge && {badge}} + {showBadge && ( + + { + + } + + )} } /> @@ -211,7 +220,7 @@ class BreakdownBase extends React.Component { return ( - {availableTabs.map((val, index) => this.getTab(val.tab, val.contentRef, val.badge, index))} + {availableTabs.map((val, index) => this.getTab(val.tab, val.contentRef, val.showBadge, index))} ); }; diff --git a/src/routes/details/components/pvcChart/modal/pvcToolbar.tsx b/src/routes/details/components/pvcChart/modal/pvcToolbar.tsx index 909be1166..8d29b4262 100644 --- a/src/routes/details/components/pvcChart/modal/pvcToolbar.tsx +++ b/src/routes/details/components/pvcChart/modal/pvcToolbar.tsx @@ -1,5 +1,5 @@ import type { ToolbarChipGroup } from '@patternfly/react-core'; -import type { RosQuery } from 'api/queries/rosQuery'; +import type { Query } from 'api/queries/query'; import messages from 'locales/messages'; import React from 'react'; import type { WrappedComponentProps } from 'react-intl'; @@ -15,7 +15,7 @@ interface PvcToolbarOwnProps { onFilterAdded(filter: Filter); onFilterRemoved(filter: Filter); pagination?: React.ReactNode; - query?: RosQuery; + query?: Query; } interface PvcToolbarState { diff --git a/src/routes/details/gcpBreakdown/gcpBreakdown.tsx b/src/routes/details/gcpBreakdown/gcpBreakdown.tsx index 31cc65894..4aaff751b 100644 --- a/src/routes/details/gcpBreakdown/gcpBreakdown.tsx +++ b/src/routes/details/gcpBreakdown/gcpBreakdown.tsx @@ -27,7 +27,7 @@ import { withRouter } from 'utils/router'; import { CostOverview } from './costOverview'; import { HistoricalData } from './historicalData'; -interface BreakdownDispatchProps { +interface GcpBreakdownDispatchProps { fetchReport?: typeof reportActions.fetchReport; } @@ -112,10 +112,8 @@ const mapStateToProps = createMapStateToProps((state, { intl, router }) => { +const mapStateToProps = createMapStateToProps((state, { intl, router }) => { const queryFromRoute = parseQuery(router.location.search); const queryState = getQueryState(router.location, 'details'); @@ -112,10 +112,8 @@ const mapStateToProps = createMapStateToProps, isOptimizationsTab: queryFromRoute.optimizationsTab !== undefined, isRosFeatureEnabled: featureFlagsSelectors.selectIsRosFeatureEnabled(state), - optimizationsBadgeComponent: , optimizationsComponent: groupBy === 'project' && groupByValue !== '*' ? : undefined, providers: filterProviders(providers, ProviderType.ocp), providersFetchStatus, @@ -135,11 +133,9 @@ const mapStateToProps = createMapStateToProps = () => { - const intl = useIntl(); - const location = useLocation(); - - const queryState = getQueryState(location, 'optimizations'); - const [query, setQuery] = useState({ ...baseQuery, ...(queryState && queryState) }); - const { groupBy, project, report, reportError, reportFetchStatus, reportQueryString } = useMapToProps({ - query, - }); - - // Clear queryState, returned from breakdown page, after query has been initialized - useEffect(() => { - clearQueryState(location, 'optimizations'); - }, [reportQueryString]); - - const getPagination = (isDisabled = false, isBottom = false) => { - const count = report?.meta ? report.meta.count : 0; - const limit = report?.meta ? report.meta.limit : baseQuery.limit; - const offset = report?.meta ? report.meta.offset : baseQuery.offset; - const page = Math.trunc(offset / limit + 1); - - return ( - handleOnPerPageSelect(perPage)} - onSetPage={(event, pageNumber) => handleOnSetPage(pageNumber)} - page={page} - perPage={limit} - titles={{ - paginationAriaLabel: intl.formatMessage(messages.paginationTitle, { - title: intl.formatMessage(messages.openShift), - placement: isBottom ? 'bottom' : 'top', - }), - }} - variant={isBottom ? PaginationVariant.bottom : PaginationVariant.top} - widgetId={`exports-pagination${isBottom ? '-bottom' : ''}`} - /> - ); - }; - - const getTable = () => { - return ( - handleOnSort(sortType, isSortAscending)} - orderBy={query.order_by} - query={query} - report={report} - reportQueryString={reportQueryString} - /> - ); - }; - - const getToolbar = () => { - const itemsPerPage = report?.meta ? report.meta.limit : 0; - const itemsTotal = report?.meta ? report.meta.count : 0; - const isDisabled = itemsTotal === 0; - - return ( - handleOnFilterAdded(filter)} - onFilterRemoved={filter => handleOnFilterRemoved(filter)} - pagination={getPagination(isDisabled)} - query={query} - /> - ); - }; - - const handleOnFilterAdded = filter => { - const newQuery = queryUtils.handleOnFilterAdded(query, filter); - setQuery(newQuery); - }; - - const handleOnFilterRemoved = filter => { - const newQuery = queryUtils.handleOnFilterRemoved(query, filter); - setQuery(newQuery); - }; - - const handleOnPerPageSelect = perPage => { - const newQuery = queryUtils.handleOnPerPageSelect(query, perPage, true); - setQuery(newQuery); - }; - - const handleOnSetPage = pageNumber => { - const newQuery = queryUtils.handleOnSetPage(query, report, pageNumber, true); - setQuery(newQuery); - }; - - const handleOnSort = (sortType, isSortAscending) => { - const newQuery = queryUtils.handleOnSort(query, sortType, isSortAscending); - setQuery(newQuery); - }; - - const itemsTotal = report?.meta ? report.meta.count : 0; - const isDisabled = itemsTotal === 0; - const hasOptimizations = report?.meta && report.meta.count > 0; - - if (reportError) { - return ; - } - if (!query.filter_by && !hasOptimizations && reportFetchStatus === FetchStatus.complete) { - return ; - } - return ( - <> - {getToolbar()} - {reportFetchStatus === FetchStatus.inProgress ? ( - - ) : ( - <> - {getTable()} -
{getPagination(isDisabled, true)}
- - )} - - ); -}; - const useQueryFromRoute = () => { const location = useLocation(); return parseQuery(location.search); }; -// eslint-disable-next-line no-empty-pattern -const useMapToProps = ({ query }: OcpOptimizationsBreakdownMapProps): OcpOptimizationsBreakdownStateProps => { - const dispatch: ThunkDispatch = useDispatch(); +const OcpBreakdownOptimizations: React.FC = () => { + const intl = useIntl(); + const location = useLocation(); const queryFromRoute = useQueryFromRoute(); const groupBy = getGroupById(queryFromRoute); const groupByValue = getGroupByValue(queryFromRoute); - const order_by = getOrderById(query) || getOrderById(baseQuery); - const order_how = getOrderByValue(query) || getOrderByValue(baseQuery); + const otimizationsTab = location.search.indexOf('optimizationsTab') === -1 ? '&optimizationsTab=true' : ''; - const reportQuery = { - ...(groupBy && { - [groupBy]: groupByValue, // Flattened project filter - }), - ...query.filter_by, // Flattened filter by - limit: query.limit, - offset: query.offset, - order_by, // Flattened order by - order_how, // Flattened order how - }; - const reportQueryString = getQuery(reportQuery); - const report = useSelector((state: RootState) => - rosSelectors.selectRos(state, reportPathsType, reportType, reportQueryString) - ); - const reportFetchStatus = useSelector((state: RootState) => - rosSelectors.selectRosFetchStatus(state, reportPathsType, reportType, reportQueryString) - ); - const reportError = useSelector((state: RootState) => - rosSelectors.selectRosError(state, reportPathsType, reportType, reportQueryString) + return ( + ); - - useEffect(() => { - if (!reportError && reportFetchStatus !== FetchStatus.inProgress) { - dispatch(rosActions.fetchRosReport(reportPathsType, reportType, reportQueryString)); - } - }, [query]); - - return { - groupBy, - project: queryFromRoute[breakdownTitleKey], - report, - reportError, - reportFetchStatus, - reportQueryString, - }; }; export { OcpBreakdownOptimizations }; diff --git a/src/routes/details/ocpDetails/detailsOptimization.tsx b/src/routes/details/ocpDetails/detailsOptimization.tsx deleted file mode 100644 index fe111e788..000000000 --- a/src/routes/details/ocpDetails/detailsOptimization.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import type { Query } from 'api/queries/query'; -import { getQuery } from 'api/queries/query'; -import type { RosReport } from 'api/ros/ros'; -import { RosPathsType, RosType } from 'api/ros/ros'; -import type { AxiosError } from 'axios'; -import React from 'react'; -import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; -import { getBreakdownPath } from 'routes/utils/paths'; -import type { FetchStatus } from 'store/common'; -import { createMapStateToProps } from 'store/common'; -import { rosActions, rosSelectors } from 'store/ros'; -import type { RouterComponentProps } from 'utils/router'; -import { withRouter } from 'utils/router'; - -export interface DetailsOptimizationOwnProps { - basePath?: string; - breadcrumbPath?: string; - project?: string; - query?: Query; -} - -export interface DetailsOptimizationStateProps { - report?: RosReport; - reportError?: AxiosError; - reportFetchStatus?: FetchStatus; - reportQueryString?: string; -} - -interface DetailsOptimizationDispatchProps { - fetchReport: typeof rosActions.fetchRosReport; -} - -interface DetailsOptimizationState {} - -type DetailsOptimizationProps = DetailsOptimizationOwnProps & - DetailsOptimizationStateProps & - DetailsOptimizationDispatchProps & - RouterComponentProps; - -const reportPathsType = RosPathsType.recommendations; -const reportType = RosType.ros; - -class DetailsOptimization extends React.Component { - protected defaultState: DetailsOptimizationState = { - // TBD... - }; - public state: DetailsOptimizationState = { ...this.defaultState }; - - public componentDidMount() { - this.updateReport(); - } - - private updateReport = () => { - const { fetchReport, reportQueryString } = this.props; - fetchReport(reportPathsType, reportType, reportQueryString); - }; - - private getBreakdownLink = count => { - const { basePath, breadcrumbPath, project, query, router } = this.props; - - if (count === 0 || project === undefined) { - return count; - } - return ( - - {count} - - ); - }; - - public render() { - const { report } = this.props; - - const count = report?.meta ? report.meta.count : 0; - - // Todo: Add link to breakdown page - return {this.getBreakdownLink(count)}; - } -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const mapStateToProps = createMapStateToProps( - (state, { project }) => { - const reportQuery = { - project, // project filter - }; - const reportQueryString = getQuery(reportQuery); - const report = rosSelectors.selectRos(state, reportPathsType, reportType, reportQueryString); - const reportError = rosSelectors.selectRosError(state, reportPathsType, reportType, reportQueryString); - const reportFetchStatus = rosSelectors.selectRosFetchStatus(state, reportPathsType, reportType, reportQueryString); - - return { - report, - reportError, - reportFetchStatus, - reportQueryString, - } as any; - } -); - -const mapDispatchToProps: DetailsOptimizationDispatchProps = { - fetchReport: rosActions.fetchRosReport, -}; - -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(DetailsOptimization)); diff --git a/src/routes/details/ocpDetails/detailsTable.tsx b/src/routes/details/ocpDetails/detailsTable.tsx index a44db0d00..1ad176e93 100644 --- a/src/routes/details/ocpDetails/detailsTable.tsx +++ b/src/routes/details/ocpDetails/detailsTable.tsx @@ -1,6 +1,7 @@ import 'routes/components/dataTable/dataTable.scss'; import { Label, Tooltip } from '@patternfly/react-core'; +import AsyncComponent from '@redhat-cloud-services/frontend-components/AsyncComponent'; import { ProviderType } from 'api/providers'; import type { Query } from 'api/queries/query'; import type { OcpReport, OcpReportItem } from 'api/reports/ocpReports'; @@ -10,7 +11,6 @@ import React from 'react'; import type { WrappedComponentProps } from 'react-intl'; import { injectIntl } from 'react-intl'; import { Link } from 'react-router-dom'; -import { routes } from 'routes'; import { ComputedReportItemValueType } from 'routes/components/charts/common'; import { DataTable } from 'routes/components/dataTable'; import { styles } from 'routes/components/dataTable/dataTable.styles'; @@ -21,13 +21,10 @@ import { getUnsortedComputedReportItems } from 'routes/utils/computedReport/getC import { getBreakdownPath } from 'routes/utils/paths'; import { getForDateRangeString, getNoDataForDateRangeString } from 'utils/dates'; import { formatCurrency, formatPercentage } from 'utils/format'; -import { formatPath } from 'utils/paths'; import { classificationDefault, classificationPlatform, classificationUnallocated, noPrefix } from 'utils/props'; import type { RouterComponentProps } from 'utils/router'; import { withRouter } from 'utils/router'; -import DetailsOptimization from './detailsOptimization'; - interface DetailsTableOwnProps extends RouterComponentProps, WrappedComponentProps { basePath?: string; breadcrumbPath?: string; @@ -281,11 +278,28 @@ class DetailsTableBase extends React.Component ), }, diff --git a/src/routes/details/rhelBreakdown/rhelBreakdown.tsx b/src/routes/details/rhelBreakdown/rhelBreakdown.tsx index 6a95d1f3a..c69c6cc19 100644 --- a/src/routes/details/rhelBreakdown/rhelBreakdown.tsx +++ b/src/routes/details/rhelBreakdown/rhelBreakdown.tsx @@ -27,7 +27,7 @@ import { withRouter } from 'utils/router'; import { CostOverview } from './costOverview'; import { HistoricalData } from './historicalData'; -interface BreakdownDispatchProps { +interface RhelBreakdownDispatchProps { fetchReport?: typeof reportActions.fetchReport; } @@ -113,10 +113,8 @@ const mapStateToProps = createMapStateToProps { + const location = useLocation(); + return parseQuery(location.search); +}; const OptimizationsBreakdown: React.FC = () => { - const { breadcrumbLabel, report, reportFetchStatus } = useMapToProps(); const intl = useIntl(); - const location = useLocation(); - - const getDefaultTerm = () => { - let result = Interval.short_term; - if (!report?.recommendations?.duration_based) { - return result; - } - - const recommendation = report.recommendations.duration_based; - if (hasRecommendation(recommendation.short_term)) { - result = Interval.short_term; - } else if (hasRecommendation(recommendation.medium_term)) { - result = Interval.medium_term; - } else if (hasRecommendation(recommendation.long_term)) { - result = Interval.long_term; - } - return result as Interval; - }; - - const [currentInterval, setCurrentInterval] = useState(getDefaultTerm()); - - const getAlert = () => { - let notifications; - if (report?.recommendations?.duration_based?.[currentInterval]) { - notifications = getNotifications(report.recommendations.duration_based[currentInterval]); - } - - if (!notifications) { - return null; - } - - return ( -
- - - {notifications.map((notification, index) => ( - {notification.message} - ))} - - -
- ); - }; - - const getRecommendationTerm = (): RecommendationItem => { - if (!report) { - return undefined; - } - - let result; - switch (currentInterval) { - case Interval.short_term: - result = report.recommendations.duration_based.short_term; - break; - case Interval.medium_term: - result = report.recommendations.duration_based.medium_term; - break; - case Interval.long_term: - result = report.recommendations.duration_based.long_term; - break; - } - return result; - }; - - const handleOnSelected = (value: Interval) => { - setCurrentInterval(value); - }; - - const isLoading = reportFetchStatus === FetchStatus.inProgress; + const queryFromRoute = useQueryFromRoute(); return (
- - - {isLoading ? ( - - ) : ( - <> - {getAlert()} - - - )} -
); }; -const useQueryFromRoute = () => { - const location = useLocation(); - return parseQuery(location.search); -}; - -// eslint-disable-next-line no-empty-pattern -const useMapToProps = (): OptimizationsBreakdownStateProps => { - const dispatch: ThunkDispatch = useDispatch(); - const queryFromRoute = useQueryFromRoute(); - - const reportQueryString = queryFromRoute ? queryFromRoute.id : ''; - const report: any = useSelector((state: RootState) => - rosSelectors.selectRos(state, reportPathsType, reportType, reportQueryString) - ); - const reportFetchStatus = useSelector((state: RootState) => - rosSelectors.selectRosFetchStatus(state, reportPathsType, reportType, reportQueryString) - ); - const reportError = useSelector((state: RootState) => - rosSelectors.selectRosError(state, reportPathsType, reportType, reportQueryString) - ); - - useEffect(() => { - if (!reportError && reportFetchStatus !== FetchStatus.inProgress) { - dispatch(rosActions.fetchRosReport(reportPathsType, reportType, reportQueryString)); - } - }, [reportQueryString]); - - return { - breadcrumbLabel: queryFromRoute[breadcrumbLabelKey], - report, - reportError, - reportFetchStatus, - reportQueryString, - }; -}; - export default OptimizationsBreakdown; diff --git a/src/routes/optimizations/optimizationsDetails/optimizationsDetails.tsx b/src/routes/optimizations/optimizationsDetails/optimizationsDetails.tsx index dc64b0761..1042343e0 100644 --- a/src/routes/optimizations/optimizationsDetails/optimizationsDetails.tsx +++ b/src/routes/optimizations/optimizationsDetails/optimizationsDetails.tsx @@ -1,248 +1,38 @@ -import { PageSection, Pagination, PaginationVariant } from '@patternfly/react-core'; -import type { Query } from 'api/queries/query'; -import { getQuery, parseQuery } from 'api/queries/query'; -import type { RosQuery } from 'api/queries/rosQuery'; -import type { RosReport } from 'api/ros/ros'; -import { RosPathsType, RosType } from 'api/ros/ros'; -import type { AxiosError } from 'axios'; +import AsyncComponent from '@redhat-cloud-services/frontend-components/AsyncComponent'; import messages from 'locales/messages'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { useIntl } from 'react-intl'; -import { useDispatch, useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; -import type { AnyAction } from 'redux'; -import type { ThunkDispatch } from 'redux-thunk'; import { routes } from 'routes'; -import { OptimizationsTable, OptimizationsToolbar } from 'routes/components/optimizations'; -import { Loading } from 'routes/components/page/loading'; -import { NoOptimizations } from 'routes/components/page/noOptimizations'; -import { NotAvailable } from 'routes/components/page/notAvailable'; -import { getGroupById, getGroupByValue } from 'routes/utils/groupBy'; -import { getOrderById, getOrderByValue } from 'routes/utils/orderBy'; -import * as queryUtils from 'routes/utils/query'; -import { clearQueryState, getQueryState } from 'routes/utils/queryState'; -import type { RootState } from 'store'; -import { FetchStatus } from 'store/common'; -import { rosActions, rosSelectors } from 'store/ros'; import { formatPath } from 'utils/paths'; import { styles } from './optimizationsDetails.styles'; -import { OptimizationsDetailsHeader } from './optimizationsDetailsHeader'; interface OptimizationsDetailsOwnProps { // TBD... } -export interface OptimizationsDetailsStateProps { - groupBy?: string; - report: RosReport; - reportError: AxiosError; - reportFetchStatus: FetchStatus; - reportQueryString: string; -} - -export interface OptimizationsDetailsMapProps { - query?: RosQuery; -} - type OptimizationsDetailsProps = OptimizationsDetailsOwnProps; -const baseQuery: RosQuery = { - limit: 10, - offset: 0, - order_by: { - last_reported: 'desc', - }, -}; - -const reportType = RosType.ros as any; -const reportPathsType = RosPathsType.recommendations as any; - const OptimizationsDetails: React.FC = () => { const intl = useIntl(); const location = useLocation(); - const queryState = getQueryState(location, 'optimizations'); - const [query, setQuery] = useState({ ...baseQuery, ...(queryState && queryState) }); - const { groupBy, report, reportError, reportFetchStatus, reportQueryString } = useMapToProps({ - query, - }); - - // Clear queryState, returned from breakdown page, after query has been initialized - useEffect(() => { - clearQueryState(location, 'optimizations'); - }, [reportQueryString]); - - const getPagination = (isDisabled = false, isBottom = false) => { - const count = report?.meta ? report.meta.count : 0; - const limit = report?.meta ? report.meta.limit : baseQuery.limit; - const offset = report?.meta ? report.meta.offset : baseQuery.offset; - const page = Math.trunc(offset / limit + 1); - - return ( - handleOnPerPageSelect(perPage)} - onSetPage={(event, pageNumber) => handleOnSetPage(pageNumber)} - page={page} - perPage={limit} - titles={{ - paginationAriaLabel: intl.formatMessage(messages.paginationTitle, { - title: intl.formatMessage(messages.openShift), - placement: isBottom ? 'bottom' : 'top', - }), - }} - variant={isBottom ? PaginationVariant.bottom : PaginationVariant.top} - widgetId={`exports-pagination${isBottom ? '-bottom' : ''}`} - /> - ); - }; - - const getTable = () => { - return ( - + handleOnSort(sortType, isSortAscending)} - orderBy={query.order_by} - query={query} - report={report} - reportQueryString={reportQueryString} - /> - ); - }; - - const getToolbar = () => { - const itemsPerPage = report?.meta ? report.meta.limit : 0; - const itemsTotal = report?.meta ? report.meta.count : 0; - const isDisabled = itemsTotal === 0; - - return ( - handleOnFilterAdded(filter)} - onFilterRemoved={filter => handleOnFilterRemoved(filter)} - pagination={getPagination(isDisabled)} - query={query} + linkPath={formatPath(routes.optimizationsBreakdown.path)} + linkState={{ + ...(location.state && location.state), + }} /> - ); - }; - - const handleOnFilterAdded = filter => { - const newQuery = queryUtils.handleOnFilterAdded(query, filter); - setQuery(newQuery); - }; - - const handleOnFilterRemoved = filter => { - const newQuery = queryUtils.handleOnFilterRemoved(query, filter); - setQuery(newQuery); - }; - - const handleOnPerPageSelect = perPage => { - const newQuery = queryUtils.handleOnPerPageSelect(query, perPage, true); - setQuery(newQuery); - }; - - const handleOnSetPage = pageNumber => { - const newQuery = queryUtils.handleOnSetPage(query, report, pageNumber, true); - setQuery(newQuery); - }; - - const handleOnSort = (sortType, isSortAscending) => { - const newQuery = queryUtils.handleOnSort(query, sortType, isSortAscending); - setQuery(newQuery); - }; - - const itemsTotal = report?.meta ? report.meta.count : 0; - const isDisabled = itemsTotal === 0; - const title = intl.formatMessage(messages.optimizations); - const hasOptimizations = report?.meta && report.meta.count > 0; - - if (reportError) { - return ; - } - if (!query.filter_by && !hasOptimizations && reportFetchStatus === FetchStatus.complete) { - return ; - } - return ( -
- - - {getToolbar()} - {reportFetchStatus === FetchStatus.inProgress ? ( - - ) : ( - <> - {getTable()} -
{getPagination(isDisabled, true)}
- - )} -
); }; -const useQueryFromRoute = () => { - const location = useLocation(); - return parseQuery(location.search); -}; - -// eslint-disable-next-line no-empty-pattern -const useMapToProps = ({ query }: OptimizationsDetailsMapProps): OptimizationsDetailsStateProps => { - const dispatch: ThunkDispatch = useDispatch(); - const queryFromRoute = useQueryFromRoute(); - - const groupBy = getGroupById(queryFromRoute); - const groupByValue = getGroupByValue(queryFromRoute); - const order_by = getOrderById(query) || getOrderById(baseQuery); - const order_how = getOrderByValue(query) || getOrderByValue(baseQuery); - - const reportQuery = { - ...(groupBy && { - [groupBy]: groupByValue, // Flattened project filter - }), - ...query.filter_by, // Flattened filter by - limit: query.limit, - offset: query.offset, - order_by, // Flattened order by - order_how, // Flattened order how - }; - const reportQueryString = getQuery(reportQuery); - const report = useSelector((state: RootState) => - rosSelectors.selectRos(state, reportPathsType, reportType, reportQueryString) - ); - const reportFetchStatus = useSelector((state: RootState) => - rosSelectors.selectRosFetchStatus(state, reportPathsType, reportType, reportQueryString) - ); - const reportError = useSelector((state: RootState) => - rosSelectors.selectRosError(state, reportPathsType, reportType, reportQueryString) - ); - - useEffect(() => { - if (!reportError && reportFetchStatus !== FetchStatus.inProgress) { - dispatch(rosActions.fetchRosReport(reportPathsType, reportType, reportQueryString)); - } - }, [query]); - - return { - groupBy, - report, - reportError, - reportFetchStatus, - reportQueryString, - }; -}; - export default OptimizationsDetails; diff --git a/src/routes/overview/components/dashboardWidgetBase.tsx b/src/routes/overview/components/dashboardWidgetBase.tsx index a42110913..42bbfe84a 100644 --- a/src/routes/overview/components/dashboardWidgetBase.tsx +++ b/src/routes/overview/components/dashboardWidgetBase.tsx @@ -1,14 +1,15 @@ import type { MessageDescriptor } from '@formatjs/intl/src/types'; import { Tab, Tabs, TabTitleText } from '@patternfly/react-core'; +import AsyncComponent from '@redhat-cloud-services/frontend-components/AsyncComponent'; import type { Forecast } from 'api/forecasts/forecast'; import { getQuery } from 'api/queries/query'; import type { Report } from 'api/reports/report'; -import type { RosReport } from 'api/ros/ros'; import type { AxiosError } from 'axios'; import messages from 'locales/messages'; import React from 'react'; import type { WrappedComponentProps } from 'react-intl'; import { Link } from 'react-router-dom'; +import { routes } from 'routes'; import { ComputedReportItemType, DatumType, transformReport } from 'routes/components/charts/common/chartDatum'; import { getComputedForecast, @@ -27,11 +28,11 @@ import { ReportSummaryTrend, ReportSummaryUsage, } from 'routes/components/reports/reportSummary'; -import { OptimizationsSummary } from 'routes/overview/components/optimizationsSummary'; import type { DashboardWidget } from 'store/dashboard/common/dashboardCommon'; import { DashboardChartType } from 'store/dashboard/common/dashboardCommon'; import { OcpDashboardTab } from 'store/dashboard/ocpDashboard'; import { formatCurrency, formatUnits, unitsLookupKey } from 'utils/format'; +import { formatPath } from 'utils/paths'; import { ChartComparison } from './chartComparison'; import { chartStyles, styles } from './dashboardWidget.styles'; @@ -61,9 +62,6 @@ export interface DashboardWidgetStateProps extends DashboardWidget { previousReport?: Report; previousReportError?: AxiosError; previousReportFetchStatus?: number; - rosReport?: RosReport; - rosReportError?: AxiosError; - rosReportFetchStatus?: number; tabsReport?: Report; tabsReportError?: AxiosError; tabsReportFetchStatus?: number; @@ -77,7 +75,6 @@ export interface DashboardWidgetState { interface DashboardWidgetDispatchProps { fetchForecasts: (widgetId) => void; fetchReports: (widgetId) => void; - fetchRosReports: (widgetId) => void; updateTab: (id, availableTabs) => void; } @@ -105,9 +102,6 @@ class DashboardWidgetBase extends React.Component { - const { rosReportFetchStatus, rosReport, titleKey } = this.props; - - return ; + return ( + + ); }; private getTab = (tab: string, index: number) => { @@ -573,13 +572,6 @@ class DashboardWidgetBase extends React.Component { - const { fetchRosReports, isRosFeatureEnabled, widgetId } = this.props; - if (fetchRosReports && isRosFeatureEnabled) { - fetchRosReports(widgetId); - } - }; - public render() { const { details, isRosFeatureEnabled } = this.props; if (details.showOptimizations) { diff --git a/src/routes/overview/components/optimizationsSummary/index.ts b/src/routes/overview/components/optimizationsSummary/index.ts deleted file mode 100644 index 0cfb40f8b..000000000 --- a/src/routes/overview/components/optimizationsSummary/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as OptimizationsSummary } from './optimizationsSummary'; diff --git a/src/routes/overview/components/optimizationsSummary/optimizations.styles.ts b/src/routes/overview/components/optimizationsSummary/optimizations.styles.ts deleted file mode 100644 index cf85bacc2..000000000 --- a/src/routes/overview/components/optimizationsSummary/optimizations.styles.ts +++ /dev/null @@ -1,8 +0,0 @@ -import global_FontSize_md from '@patternfly/react-tokens/dist/js/global_FontSize_md'; -import type React from 'react'; - -export const styles = { - infoIcon: { - fontSize: global_FontSize_md.value, - }, -} as { [className: string]: React.CSSProperties }; diff --git a/src/routes/overview/components/optimizationsSummary/optimizationsSummary.scss b/src/routes/overview/components/optimizationsSummary/optimizationsSummary.scss deleted file mode 100644 index 7a01575bb..000000000 --- a/src/routes/overview/components/optimizationsSummary/optimizationsSummary.scss +++ /dev/null @@ -1,11 +0,0 @@ -@import url("~@patternfly/patternfly/base/patternfly-variables.css"); - -.skeleton { - height: 125px; - margin-bottom: var(--pf-v5-global--spacer--md); - margin-top: var(--pf-v5-global--spacer--md); -} - -.summary { - height: 100%; -} diff --git a/src/routes/overview/components/optimizationsSummary/optimizationsSummary.tsx b/src/routes/overview/components/optimizationsSummary/optimizationsSummary.tsx deleted file mode 100644 index edc565d61..000000000 --- a/src/routes/overview/components/optimizationsSummary/optimizationsSummary.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import './optimizationsSummary.scss'; - -import { - Button, - ButtonVariant, - Card, - CardBody, - CardTitle, - Popover, - Skeleton, - Title, - TitleSizes, -} from '@patternfly/react-core'; -import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons/dist/esm/icons/outlined-question-circle-icon'; -import type { RosReport } from 'api/ros/ros'; -import messages from 'locales/messages'; -import React from 'react'; -import type { WrappedComponentProps } from 'react-intl'; -import type { MessageDescriptor } from 'react-intl'; -import { injectIntl } from 'react-intl'; -import { Link } from 'react-router-dom'; -import { routes } from 'routes'; -import { skeletonWidth } from 'routes/utils/skeleton'; -import { FetchStatus } from 'store/common'; -import { formatPath } from 'utils/paths'; - -import { styles } from './optimizations.styles'; - -export interface OptimizationsSummaryProps extends WrappedComponentProps { - report: RosReport; - status: number; - title: MessageDescriptor; -} - -const OptimizationsSummaryBase: React.FC = ({ intl, report, status, title }) => { - const count = report?.meta ? report.meta.count : 0; - const description = intl.formatMessage(messages.optimizationsDetails, { count }); - return ( - - - - {intl.formatMessage(title)} - <span style={styles.infoIcon}> - <Popover - aria-label={intl.formatMessage(messages.optimizationsInfoArialLabel)} - enableFlip - bodyContent={<p style={styles.infoTitle}>{intl.formatMessage(messages.optimizationsInfo)}</p>} - > - <Button - aria-label={intl.formatMessage(messages.optimizationsInfoButtonArialLabel)} - variant={ButtonVariant.plain} - > - <OutlinedQuestionCircleIcon /> - </Button> - </Popover> - </span> - - - - {status === FetchStatus.inProgress ? ( - <> - - - - ) : count > 0 ? ( - {description} - ) : ( - description - )} - - - ); -}; - -const OptimizationsSummary = injectIntl(OptimizationsSummaryBase); - -export default OptimizationsSummary; diff --git a/src/routes/overview/ocpDashboard/ocpDashboardWidget.tsx b/src/routes/overview/ocpDashboard/ocpDashboardWidget.tsx index da09be703..54bdcce4d 100644 --- a/src/routes/overview/ocpDashboard/ocpDashboardWidget.tsx +++ b/src/routes/overview/ocpDashboard/ocpDashboardWidget.tsx @@ -8,7 +8,6 @@ import { ocpDashboardActions, ocpDashboardSelectors, OcpDashboardTab } from 'sto import { featureFlagsSelectors } from 'store/featureFlags'; import { forecastSelectors } from 'store/forecasts'; import { reportSelectors } from 'store/reports'; -import { rosSelectors } from 'store/ros'; import { getCurrency } from 'utils/localStorage'; import { chartStyles } from './ocpDashboardWidget.styles'; @@ -16,7 +15,6 @@ import { chartStyles } from './ocpDashboardWidget.styles'; interface OcpDashboardWidgetDispatchProps { fetchForecasts: typeof ocpDashboardActions.fetchWidgetForecasts; fetchReports: typeof ocpDashboardActions.fetchWidgetReports; - fetchRosReports: typeof ocpDashboardActions.fetchWidgetRosReports; updateTab: typeof ocpDashboardActions.changeWidgetTab; } @@ -114,23 +112,9 @@ const mapStateToProps = createMapStateToProps { }; }; -export const fetchWidgetRosReports = (id: number): ThunkAction => { - return (dispatch, getState) => { - const state = getState(); - const widget = selectWidget(state, id); - const { optimizations } = selectWidgetQueries(state, id); - - if (widget.rosPathsType && widget.rosType) { - dispatch(rosActions.fetchRosReport(widget.rosPathsType, widget.rosType, optimizations)); - } - }; -}; - export const setWidgetTab = createAction('ocpDashboard/widget/tab')<{ id: number; tab: OcpDashboardTab; diff --git a/src/store/dashboard/ocpDashboard/ocpDashboardWidgets.ts b/src/store/dashboard/ocpDashboard/ocpDashboardWidgets.ts index b32d4d98d..31c73b23a 100644 --- a/src/store/dashboard/ocpDashboard/ocpDashboardWidgets.ts +++ b/src/store/dashboard/ocpDashboard/ocpDashboardWidgets.ts @@ -1,6 +1,5 @@ import { ForecastPathsType, ForecastType } from 'api/forecasts/forecast'; import { ReportPathsType, ReportType } from 'api/reports/report'; -import { RosPathsType, RosType } from 'api/ros/ros'; import messages from 'locales/messages'; import { routes } from 'routes'; import { @@ -99,8 +98,6 @@ export const memoryWidget: OcpDashboardWidget = { export const optimizationsWidget: OcpDashboardWidget = { id: getId(), titleKey: messages.optimizations, - rosPathsType: RosPathsType.recommendations, - rosType: RosType.ros, details: { showOptimizations: true, }, diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts index 26e0edd84..765204aba 100644 --- a/src/store/rootReducer.ts +++ b/src/store/rootReducer.ts @@ -39,7 +39,6 @@ import { orgReducer, orgStateKey } from 'store/orgs'; import { priceListReducer, priceListStateKey } from 'store/priceList'; import { reportReducer, reportStateKey } from 'store/reports'; import { resourceReducer, resourceStateKey } from 'store/resources'; -import { rosReducer, rosStateKey } from 'store/ros'; import { settingsReducer, settingsStateKey } from 'store/settings'; import { sourcesReducer, sourcesStateKey } from 'store/sourceSettings'; import { tagReducer, tagStateKey } from 'store/tags'; @@ -92,7 +91,6 @@ export const rootReducer = combineReducers({ [rhelCostOverviewStateKey]: rhelCostOverviewReducer, [rhelDashboardStateKey]: rhelDashboardReducer, [rhelHistoricalDataStateKey]: rhelHistoricalDataReducer, - [rosStateKey]: rosReducer, [settingsStateKey]: settingsReducer, [sourcesStateKey]: sourcesReducer, [tagStateKey]: tagReducer, diff --git a/src/store/ros/__snapshots__/ros.test.ts.snap b/src/store/ros/__snapshots__/ros.test.ts.snap deleted file mode 100644 index c40443195..000000000 --- a/src/store/ros/__snapshots__/ros.test.ts.snap +++ /dev/null @@ -1,9 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`default state 1`] = ` -{ - "byId": Map {}, - "errors": Map {}, - "fetchStatus": Map {}, -} -`; diff --git a/src/store/ros/index.ts b/src/store/ros/index.ts deleted file mode 100644 index 80e11b672..000000000 --- a/src/store/ros/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as rosActions from './rosActions'; -import { rosStateKey } from './rosCommon'; -import type { CachedRos, RosAction, RosState } from './rosReducer'; -import { rosReducer } from './rosReducer'; -import * as rosSelectors from './rosSelectors'; - -export type { RosAction, CachedRos, RosState }; -export { rosActions, rosReducer, rosSelectors, rosStateKey }; diff --git a/src/store/ros/ros.test.ts b/src/store/ros/ros.test.ts deleted file mode 100644 index f9f17a6ab..000000000 --- a/src/store/ros/ros.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -jest.mock('api/ros/rosUtils'); - -import { waitFor } from '@testing-library/react'; -import type { RosReport } from 'api/ros/ros'; -import { RosPathsType, RosType } from 'api/ros/ros'; -import { runRosReport } from 'api/ros/rosUtils'; -import { FetchStatus } from 'store/common'; -import { createMockStoreCreator } from 'store/mockStore'; - -import * as actions from './rosActions'; -import { rosStateKey } from './rosCommon'; -import { rosReducer } from './rosReducer'; -import * as selectors from './rosSelectors'; - -const createRossStore = createMockStoreCreator({ - [rosStateKey]: rosReducer, -}); - -const runRosMock = runRosReport as jest.Mock; - -const mockRos: RosReport = { - data: [], - total: { - value: 100, - units: 'USD', - }, -} as any; - -const rosType = RosType.cost; -const rosPathsType = RosPathsType.recommendations; -const rosQueryString = 'rosQueryString'; - -runRosMock.mockResolvedValue({ data: mockRos }); -global.Date.now = jest.fn(() => 12345); - -jest.spyOn(actions, 'fetchRosReport'); -jest.spyOn(selectors, 'selectRosFetchStatus'); - -test('default state', () => { - const store = createRossStore(); - expect(selectors.selectRosState(store.getState())).toMatchSnapshot(); -}); - -test('fetch ros success', async () => { - const store = createRossStore(); - store.dispatch(actions.fetchRosReport(rosPathsType, rosType, rosQueryString)); - expect(runRosMock).toBeCalled(); - expect(selectors.selectRosFetchStatus(store.getState(), rosPathsType, rosType, rosQueryString)).toBe( - FetchStatus.inProgress - ); - await waitFor(() => expect(selectors.selectRosFetchStatus).toHaveBeenCalled()); - const finishedState = store.getState(); - expect(selectors.selectRosFetchStatus(finishedState, rosPathsType, rosType, rosQueryString)).toBe( - FetchStatus.complete - ); - expect(selectors.selectRosError(finishedState, rosPathsType, rosType, rosQueryString)).toBe(null); -}); - -test('fetch ros failure', async () => { - const store = createRossStore(); - const error = Symbol('ros error'); - runRosMock.mockRejectedValueOnce(error); - store.dispatch(actions.fetchRosReport(rosPathsType, rosType, rosQueryString)); - expect(runRosReport).toBeCalled(); - expect(selectors.selectRosFetchStatus(store.getState(), rosPathsType, rosType, rosQueryString)).toBe( - FetchStatus.inProgress - ); - await waitFor(() => expect(selectors.selectRosFetchStatus).toHaveBeenCalled()); - const finishedState = store.getState(); - expect(selectors.selectRosFetchStatus(finishedState, rosPathsType, rosType, rosQueryString)).toBe( - FetchStatus.complete - ); - // Todo: Temporarily disable test while overriding the fail state to feed fake data - // expect(selectors.selectRosError(finishedState, rosPathsType, rosType, rosQueryString)).toBe(error); -}); - -test('does not fetch ros if the request is in progress', () => { - const store = createRossStore(); - store.dispatch(actions.fetchRosReport(rosPathsType, rosType, rosQueryString)); - store.dispatch(actions.fetchRosReport(rosPathsType, rosType, rosQueryString)); - expect(runRosReport).toHaveBeenCalledTimes(1); -}); - -test('ros is not refetched if it has not expired', async () => { - const store = createRossStore(); - store.dispatch(actions.fetchRosReport(rosPathsType, rosType, rosQueryString)); - await waitFor(() => expect(actions.fetchRosReport).toHaveBeenCalled()); - store.dispatch(actions.fetchRosReport(rosPathsType, rosType, rosQueryString)); - expect(runRosReport).toHaveBeenCalledTimes(1); -}); diff --git a/src/store/ros/rosActions.ts b/src/store/ros/rosActions.ts deleted file mode 100644 index 3a056ab88..000000000 --- a/src/store/ros/rosActions.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { RosReport, RosType } from 'api/ros/ros'; -import type { RosPathsType } from 'api/ros/ros'; -import { runRosReport } from 'api/ros/rosUtils'; -import type { AxiosError } from 'axios'; -import type { ThunkAction } from 'store/common'; -import { FetchStatus } from 'store/common'; -import type { RootState } from 'store/rootReducer'; -import { createAction } from 'typesafe-actions'; - -import { getFetchId } from './rosCommon'; -import { selectRos, selectRosError, selectRosFetchStatus } from './rosSelectors'; - -const expirationMS = 30 * 60 * 1000; // 30 minutes - -interface RosActionMeta { - fetchId: string; -} - -export const fetchRosRequest = createAction('ros/request')(); -export const fetchRosSuccess = createAction('ros/success')(); -export const fetchRosFailure = createAction('ros/failure')(); - -export function fetchRosReport(rosPathsType: RosPathsType, rosType: RosType, rosQueryString: string): ThunkAction { - return (dispatch, getState) => { - if (!isRosExpired(getState(), rosPathsType, rosType, rosQueryString)) { - return; - } - - const meta: RosActionMeta = { - fetchId: getFetchId(rosPathsType, rosType, rosQueryString), - }; - - dispatch(fetchRosRequest(meta)); - runRosReport(rosPathsType, rosType, rosQueryString) - .then(res => { - dispatch(fetchRosSuccess(res.data, meta)); - }) - .catch(err => { - dispatch(fetchRosFailure(err, meta)); - }); - }; -} - -function isRosExpired(state: RootState, rosPathsType: RosPathsType, rosType: RosType, rosQueryString: string) { - const ros = selectRos(state, rosPathsType, rosType, rosQueryString); - const fetchError = selectRosError(state, rosPathsType, rosType, rosQueryString); - const fetchStatus = selectRosFetchStatus(state, rosPathsType, rosType, rosQueryString); - if (fetchError || fetchStatus === FetchStatus.inProgress) { - return false; - } - - if (!ros) { - return true; - } - - const now = Date.now(); - return now > ros.timeRequested + expirationMS; -} diff --git a/src/store/ros/rosCommon.ts b/src/store/ros/rosCommon.ts deleted file mode 100644 index 8c61d8d8c..000000000 --- a/src/store/ros/rosCommon.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { RosPathsType, RosType } from 'api/ros/ros'; -export const rosStateKey = 'ros'; - -export function getFetchId(rosPathsType: RosPathsType, rosType: RosType, rosQueryString: string) { - return `${rosPathsType}--${rosType}--${rosQueryString}`; -} diff --git a/src/store/ros/rosReducer.ts b/src/store/ros/rosReducer.ts deleted file mode 100644 index 8ea30fd40..000000000 --- a/src/store/ros/rosReducer.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { RosReport } from 'api/ros/ros'; -import type { AxiosError } from 'axios'; -import { FetchStatus } from 'store/common'; -import { resetState } from 'store/ui/uiActions'; -import type { ActionType } from 'typesafe-actions'; -import { getType } from 'typesafe-actions'; - -import { fetchRosFailure, fetchRosRequest, fetchRosSuccess } from './rosActions'; - -export interface CachedRos extends RosReport { - timeRequested: number; -} - -export type RosState = Readonly<{ - byId: Map; - fetchStatus: Map; - errors: Map; -}>; - -const defaultState: RosState = { - byId: new Map(), - fetchStatus: new Map(), - errors: new Map(), -}; - -export type RosAction = ActionType< - typeof fetchRosFailure | typeof fetchRosRequest | typeof fetchRosSuccess | typeof resetState ->; - -export function rosReducer(state = defaultState, action: RosAction): RosState { - switch (action.type) { - case getType(resetState): - state = defaultState; - return state; - - case getType(fetchRosRequest): - return { - ...state, - fetchStatus: new Map(state.fetchStatus).set(action.payload.fetchId, FetchStatus.inProgress), - }; - - case getType(fetchRosSuccess): - return { - ...state, - fetchStatus: new Map(state.fetchStatus).set(action.meta.fetchId, FetchStatus.complete), - byId: new Map(state.byId).set(action.meta.fetchId, { - ...action.payload, - timeRequested: Date.now(), - }), - errors: new Map(state.errors).set(action.meta.fetchId, null), - }; - - case getType(fetchRosFailure): - return { - ...state, - fetchStatus: new Map(state.fetchStatus).set(action.meta.fetchId, FetchStatus.complete), - errors: new Map(state.errors).set(action.meta.fetchId, action.payload), - }; - default: - return state; - } -} diff --git a/src/store/ros/rosSelectors.ts b/src/store/ros/rosSelectors.ts deleted file mode 100644 index 883211d6b..000000000 --- a/src/store/ros/rosSelectors.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { RosPathsType, RosType } from 'api/ros/ros'; -import type { RootState } from 'store/rootReducer'; - -import { getFetchId, rosStateKey } from './rosCommon'; - -export const selectRosState = (state: RootState) => state[rosStateKey]; - -export const selectRos = (state: RootState, rosPathsType: RosPathsType, rosType: RosType, rosQueryString: string) => - selectRosState(state).byId.get(getFetchId(rosPathsType, rosType, rosQueryString)); - -export const selectRosFetchStatus = ( - state: RootState, - rosPathsType: RosPathsType, - rosType: RosType, - rosQueryString: string -) => selectRosState(state).fetchStatus.get(getFetchId(rosPathsType, rosType, rosQueryString)); - -export const selectRosError = ( - state: RootState, - rosPathsType: RosPathsType, - rosType: RosType, - rosQueryString: string -) => selectRosState(state).errors.get(getFetchId(rosPathsType, rosType, rosQueryString)); diff --git a/src/utils/recomendations.ts b/src/utils/recomendations.ts deleted file mode 100644 index f15929fad..000000000 --- a/src/utils/recomendations.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { Notification, RecommendationItem } from 'api/ros/recommendations'; -import type { RecommendationItems } from 'api/ros/recommendations'; - -export const getNotifications = (term: RecommendationItem): Notification[] => { - if (!hasNotification(term)) { - return undefined; - } - return Object.keys(term.notifications).map(key => term.notifications[key]); -}; - -export const hasNotification = (term: RecommendationItem) => { - if (!term?.notifications) { - return false; - } - const keys = Object.keys(term.notifications); - return keys.length > 0; -}; - -export const hasRecommendation = (term: RecommendationItem) => { - if (!term) { - return false; - } - - const hasConfigLimitsCpu = hasRecommendationValues(term, 'config', 'limits', 'cpu'); - const hasConfigLimitsMemory = hasRecommendationValues(term, 'config', 'limits', 'memory'); - const hasConfigRequestsCpu = hasRecommendationValues(term, 'config', 'requests', 'cpu'); - const hasConfigRequestsMemory = hasRecommendationValues(term, 'config', 'requests', 'memory'); - - return hasConfigLimitsCpu || hasConfigLimitsMemory || hasConfigRequestsCpu || hasConfigRequestsMemory; -}; - -// Helper to determine if config and variation are empty objects -// Example: key1 = config, key2 = limits, key3 = cpu -export const hasRecommendationValues = (term: RecommendationItem, key1: string, key2: string, key3: string) => { - let result = false; - if (term && term[key1] && term[key1][key2] && term[key1][key2][key3]) { - result = Object.keys(term[key1][key2][key3]).length > 0; - } - return result; -}; - -export const hasWarning = (recommendations: RecommendationItems) => { - if (!recommendations) { - return false; - } - return ( - hasNotification(recommendations.short_term) || - hasNotification(recommendations.medium_term) || - hasNotification(recommendations.long_term) - ); -}; diff --git a/test/transformTS.js b/test/transformTS.js index 06f5bef80..5326b2b2f 100644 --- a/test/transformTS.js +++ b/test/transformTS.js @@ -13,7 +13,12 @@ delete options.sourceMap; module.exports = { process(src, path) { - if (path.endsWith('.ts') || path.endsWith('.tsx') || path.includes('@patternfly/react-icons/dist/esm')) { + if ( + path.endsWith('.ts') || + path.endsWith('.tsx') || + path.includes('@patternfly/react-icons/dist/esm') || + path.includes('uuid/dist/esm-browser') + ) { return { code: tsc.transpile(src, options, path, []) }; } return { code: src };