diff --git a/locales/data.json b/locales/data.json index 607c36847..c0a3c0db2 100644 --- a/locales/data.json +++ b/locales/data.json @@ -9555,6 +9555,12 @@ "value": "OpenShift in Cost Management" } ], + "notAuthorizedStateRecommendations": [ + { + "type": 0, + "value": "Recommendations in Cost Management" + } + ], "notAuthorizedStateRhel": [ { "type": 0, diff --git a/locales/translations.json b/locales/translations.json index b9e2b5917..e11b78fee 100644 --- a/locales/translations.json +++ b/locales/translations.json @@ -369,6 +369,7 @@ "notAuthorizedStateIbm": "IBM Cloud in Cost Management", "notAuthorizedStateOci": "Oracle Cloud Infrastructure in Cost Management", "notAuthorizedStateOcp": "OpenShift in Cost Management", + "notAuthorizedStateRecommendations": "Recommendations in Cost Management", "notAuthorizedStateRhel": "RHEL in Cost Management", "oci": "Oracle Cloud Infrastructure", "ociComputeTitle": "Virtual machines usage", diff --git a/src/api/userAccess.ts b/src/api/userAccess.ts index cbe442cea..c2873d728 100644 --- a/src/api/userAccess.ts +++ b/src/api/userAccess.ts @@ -24,7 +24,8 @@ export const enum UserAccessType { ibm = 'ibm', oci = 'oci', ocp = 'ocp', - rhel = 'ocp', // Todo: update to use rhel when APIs are available + rhel = 'ocp', // Todo: update to use RHEL when APIs are available + ros = 'ocp', // Todo: update to use ROS when APIs are available } // If the user-access API is called without a query parameter, all types are returned in the response diff --git a/src/components/featureFlags/featureFlags.tsx b/src/components/featureFlags/featureFlags.tsx index bf3e6487c..eab9bae82 100644 --- a/src/components/featureFlags/featureFlags.tsx +++ b/src/components/featureFlags/featureFlags.tsx @@ -20,6 +20,7 @@ export const enum FeatureToggle { negativeFiltering = 'cost-management.ui.negative-filtering', // Negative (aka exclude) filtering https://issues.redhat.com/browse/COST-2773 oci = 'cost-management.ui.oci', // Oracle Cloud Infrastructure https://issues.redhat.com/browse/COST-2358 platformCosts = 'cost-management.ui.platform-costs', // OCP platform costs https://issues.redhat.com/browse/COST-2774 + ros = 'cost-management.ui.ros', // ROS support https://issues.redhat.com/browse/COST-3477 } let userId; @@ -63,11 +64,12 @@ const FeatureFlags: React.FC = ({ children = null }) => { isCostDistributionFeatureEnabled: client.isEnabled(FeatureToggle.costDistribution), isCostTypeFeatureEnabled: client.isEnabled(FeatureToggle.costType), isExportsFeatureEnabled: client.isEnabled(FeatureToggle.exports), - isFINsightsFeatureEnabled: client.isEnabled(FeatureToggle.finsights), + isFinsightsFeatureEnabled: client.isEnabled(FeatureToggle.finsights), isNegativeFilteringFeatureEnabled: client.isEnabled(FeatureToggle.negativeFiltering), isIbmFeatureEnabled: client.isEnabled(FeatureToggle.ibm), isOciFeatureEnabled: client.isEnabled(FeatureToggle.oci), isPlatformCostsFeatureEnabled: client.isEnabled(FeatureToggle.platformCosts), + isRosFeatureEnabled: client.isEnabled(FeatureToggle.ros), }) ); }); diff --git a/src/components/userAccess/permissions.tsx b/src/components/userAccess/permissions.tsx index 20200184e..abb6de1a2 100644 --- a/src/components/userAccess/permissions.tsx +++ b/src/components/userAccess/permissions.tsx @@ -21,6 +21,7 @@ import { hasOciAccess, hasOcpAccess, hasRhelAccess, + hasRosAccess, } from 'utils/userAccess'; interface PermissionsOwnProps { @@ -28,9 +29,10 @@ interface PermissionsOwnProps { } interface PermissionsStateProps { - isFINsightsFeatureEnabled?: boolean; + isFinsightsFeatureEnabled?: boolean; isIbmFeatureEnabled?: boolean; isOciFeatureEnabled?: boolean; + isRosFeatureEnabled?: boolean; userAccess: UserAccess; userAccessError: AxiosError; userAccessFetchStatus: FetchStatus; @@ -41,9 +43,10 @@ type PermissionsProps = PermissionsOwnProps & PermissionsStateProps; const PermissionsBase: React.FC = ({ children = null, - isFINsightsFeatureEnabled, + isFinsightsFeatureEnabled, isIbmFeatureEnabled, isOciFeatureEnabled, + isRosFeatureEnabled, userAccess, userAccessError, userAccessFetchStatus, @@ -65,12 +68,13 @@ const PermissionsBase: React.FC = ({ const aws = hasAwsAccess(userAccess); const azure = hasAzureAccess(userAccess); - const oci = isOciFeatureEnabled && hasOciAccess(userAccess); const costModel = hasCostModelAccess(userAccess); const gcp = hasGcpAccess(userAccess); const ibm = isIbmFeatureEnabled && hasIbmAccess(userAccess); + const oci = isOciFeatureEnabled && hasOciAccess(userAccess); const ocp = hasOcpAccess(userAccess); - const rhel = isFINsightsFeatureEnabled && hasRhelAccess(userAccess); + const rhel = isFinsightsFeatureEnabled && hasRhelAccess(userAccess); + const ros = isRosFeatureEnabled && hasRosAccess(userAccess); switch (path) { case paths.explorer: @@ -96,6 +100,9 @@ const PermissionsBase: React.FC = ({ case paths.ocpDetails: case paths.ocpDetailsBreakdown: return ocp; + case paths.recommendations: + case paths.recommendationsBreakdown: + return ros; case paths.rhelDetails: case paths.rhelDetailsBreakdown: return rhel; @@ -130,9 +137,10 @@ const mapStateToProps = createMapStateToProps import(/* webpackChunkName: "ociDetails" */ 'route const OcpBreakdown = lazy(() => import(/* webpackChunkName: "ocpBreakdown" */ 'routes/views/details/ocpBreakdown')); const OcpDetails = lazy(() => import(/* webpackChunkName: "ocpDetails" */ 'routes/views/details/ocpDetails')); const Overview = lazy(() => import(/* webpackChunkName: "overview" */ 'routes/views/overview')); +const Recommendations = lazy(() => import(/* webpackChunkName: "ocpDetails" */ 'routes/views/details/rhelDetails')); // Todo: Add page +const RecommendationsBreakdown = lazy( + () => import(/* webpackChunkName: "ocpDetails" */ 'routes/views/details/rhelBreakdown') +); // Todo: Add page const RhelDetails = lazy(() => import(/* webpackChunkName: "ocpDetails" */ 'routes/views/details/rhelDetails')); const RhelBreakdown = lazy(() => import(/* webpackChunkName: "ocpDetails" */ 'routes/views/details/rhelBreakdown')); @@ -42,6 +46,8 @@ const paths = { ocpDetails: '/ocp', ocpDetailsBreakdown: '/ocp/breakdown', overview: '/', + recommendations: '/recommendations', + recommendationsBreakdown: '/recommendations/breakdown', rhelDetails: '/rhel', rhelDetailsBreakdown: '/rhel/breakdown', }; @@ -113,6 +119,14 @@ const routes = [ element: userAccess(OcpBreakdown), path: paths.ocpDetailsBreakdown, }, + { + element: userAccess(Recommendations), + path: paths.recommendations, + }, + { + element: userAccess(RecommendationsBreakdown), + path: paths.recommendationsBreakdown, + }, { element: userAccess(RhelDetails), path: paths.rhelDetails, diff --git a/src/routes/state/notAuthorized/notAuthorizedState.tsx b/src/routes/state/notAuthorized/notAuthorizedState.tsx index 795cc7363..e98bbcdee 100644 --- a/src/routes/state/notAuthorized/notAuthorizedState.tsx +++ b/src/routes/state/notAuthorized/notAuthorizedState.tsx @@ -49,6 +49,10 @@ class NotAuthorizedStateBase extends React.Component { case paths.rhelDetailsBreakdown: msg = messages.notAuthorizedStateRhel; break; + case paths.recommendations: + case paths.recommendationsBreakdown: + msg = messages.notAuthorizedStateRecommendations; + break; case paths.explorer: default: msg = messages.costManagement; diff --git a/src/routes/views/explorer/explorer.tsx b/src/routes/views/explorer/explorer.tsx index 512c0cb7c..6d2ce2405 100644 --- a/src/routes/views/explorer/explorer.tsx +++ b/src/routes/views/explorer/explorer.tsx @@ -82,7 +82,7 @@ interface ExplorerStateProps { dateRangeType: DateRangeType; gcpProviders: Providers; ibmProviders: Providers; - isFINsightsFeatureEnabled?: boolean; + isFinsightsFeatureEnabled?: boolean; ocpProviders: Providers; perspective: PerspectiveType; providers: Providers; @@ -389,8 +389,8 @@ class Explorer extends React.Component { }; private isRhelAvailable = () => { - const { isFINsightsFeatureEnabled, rhelProviders, userAccess } = this.props; - return isFINsightsFeatureEnabled && isRhelAvailable(userAccess, rhelProviders); + const { isFinsightsFeatureEnabled, rhelProviders, userAccess } = this.props; + return isFinsightsFeatureEnabled && isRhelAvailable(userAccess, rhelProviders); }; private updateReport = () => { @@ -636,7 +636,7 @@ const mapStateToProps = createMapStateToProps { }; private isRhelAvailable = () => { - const { isFINsightsFeatureEnabled, rhelProviders, userAccess } = this.props; - return isFINsightsFeatureEnabled && isRhelAvailable(userAccess, rhelProviders); + const { isFinsightsFeatureEnabled, rhelProviders, userAccess } = this.props; + return isFinsightsFeatureEnabled && isRhelAvailable(userAccess, rhelProviders); }; public render() { @@ -384,7 +384,7 @@ const mapStateToProps = createMapStateToProps { } private getAvailableTabs = () => { - const { isFINsightsFeatureEnabled } = this.props; + const { isFinsightsFeatureEnabled } = this.props; const availableTabs = []; const infrastructureTabs = @@ -235,7 +235,7 @@ class OverviewBase extends React.Component { ] : undefined; - if (isFINsightsFeatureEnabled) { + if (isFinsightsFeatureEnabled) { if (infrastructureTabs) { availableTabs.push(...infrastructureTabs); } @@ -284,7 +284,7 @@ class OverviewBase extends React.Component { }; private getCurrentTab = () => { - const { isFINsightsFeatureEnabled } = this.props; + const { isFinsightsFeatureEnabled } = this.props; const { activeTabKey } = this.state; const hasAws = this.isAwsAvailable(); @@ -308,7 +308,7 @@ class OverviewBase extends React.Component { } else if (showRhelOnly) { return OverviewTab.rhel; } else { - if (isFINsightsFeatureEnabled) { + if (isFinsightsFeatureEnabled) { switch (activeTabKey) { case 0: return OverviewTab.infrastructure; @@ -574,10 +574,10 @@ class OverviewBase extends React.Component { }; private getTabTitle = (tab: OverviewTab) => { - const { intl, isFINsightsFeatureEnabled } = this.props; + const { intl, isFinsightsFeatureEnabled } = this.props; if (tab === OverviewTab.infrastructure) { - if (isFINsightsFeatureEnabled) { + if (isFinsightsFeatureEnabled) { return intl.formatMessage(messages.summary); } return intl.formatMessage(messages.infrastructure); @@ -707,8 +707,8 @@ class OverviewBase extends React.Component { }; private isRhelAvailable = () => { - const { isFINsightsFeatureEnabled, rhelProviders, userAccess } = this.props; - return isFINsightsFeatureEnabled && isRhelAvailable(userAccess, rhelProviders); + const { isFinsightsFeatureEnabled, rhelProviders, userAccess } = this.props; + return isFinsightsFeatureEnabled && isRhelAvailable(userAccess, rhelProviders); }; public render() { @@ -716,7 +716,7 @@ class OverviewBase extends React.Component { providersFetchStatus, intl, isCurrencyFeatureEnabled, - isFINsightsFeatureEnabled, + isFinsightsFeatureEnabled, isIbmFeatureEnabled, isOciFeatureEnabled, userAccessFetchStatus, @@ -762,7 +762,7 @@ class OverviewBase extends React.Component {

{intl.formatMessage(messages.openShift)}

{intl.formatMessage(messages.openShiftDesc)}


- {isFINsightsFeatureEnabled && ( + {isFinsightsFeatureEnabled && ( <>

{intl.formatMessage(messages.rhel)}

{intl.formatMessage(messages.rhelDesc)}

@@ -868,7 +868,7 @@ const mapStateToProps = createMapStateToProps { test('FINsights feature is enabled', async () => { const store = createUIStore(); - store.dispatch(actions.setFeatureFlags({ isFINsightsFeatureEnabled: true })); - expect(featureFlagsSelectors.selectIsFINsightsFeatureEnabled(store.getState())).toBe(true); + store.dispatch(actions.setFeatureFlags({ isFinsightsFeatureEnabled: true })); + expect(featureFlagsSelectors.selectIsFinsightsFeatureEnabled(store.getState())).toBe(true); }); test('exports feature is enabled', async () => { @@ -67,3 +67,9 @@ test('platform costs feature is enabled', async () => { store.dispatch(actions.setFeatureFlags({ isPlatformCostsFeatureEnabled: true })); expect(featureFlagsSelectors.selectIsPlatformCostsFeatureEnabled(store.getState())).toBe(true); }); + +test('ROS feature is enabled', async () => { + const store = createUIStore(); + store.dispatch(actions.setFeatureFlags({ isRosFeatureEnabled: true })); + expect(featureFlagsSelectors.selectIsRosFeatureEnabled(store.getState())).toBe(true); +}); diff --git a/src/store/featureFlags/featureFlagsActions.ts b/src/store/featureFlags/featureFlagsActions.ts index 1ccb3c320..e4e0240d2 100644 --- a/src/store/featureFlags/featureFlagsActions.ts +++ b/src/store/featureFlags/featureFlagsActions.ts @@ -5,11 +5,12 @@ export interface FeatureFlagsActionMeta { isCostTypeFeatureEnabled?: boolean; isCurrencyFeatureEnabled?: boolean; isExportsFeatureEnabled?: boolean; - isFINsightsFeatureEnabled?: boolean; + isFinsightsFeatureEnabled?: boolean; isIbmFeatureEnabled?: boolean; isNegativeFilteringFeatureEnabled?: boolean; isOciFeatureEnabled?: boolean; isPlatformCostsFeatureEnabled?: boolean; + isRosFeatureEnabled?: boolean; } export const setFeatureFlags = createAction('feature/init_feature_flags')(); diff --git a/src/store/featureFlags/featureFlagsReducer.ts b/src/store/featureFlags/featureFlagsReducer.ts index 123e33284..70a6ea548 100644 --- a/src/store/featureFlags/featureFlagsReducer.ts +++ b/src/store/featureFlags/featureFlagsReducer.ts @@ -12,11 +12,12 @@ export type FeatureFlagsState = Readonly<{ isCostTypeFeatureEnabled: boolean; isCurrencyFeatureEnabled: boolean; isExportsFeatureEnabled: boolean; - isFINsightsFeatureEnabled: boolean; + isFinsightsFeatureEnabled: boolean; isIbmFeatureEnabled: boolean; isNegativeFilteringFeatureEnabled: boolean; isOciFeatureEnabled: boolean; isPlatformCostsFeatureEnabled: boolean; + isRosFeatureEnabled: boolean; }>; export const defaultState: FeatureFlagsState = { @@ -25,11 +26,12 @@ export const defaultState: FeatureFlagsState = { isCostTypeFeatureEnabled: false, isCurrencyFeatureEnabled: false, isExportsFeatureEnabled: false, - isFINsightsFeatureEnabled: false, + isFinsightsFeatureEnabled: false, isIbmFeatureEnabled: false, isNegativeFilteringFeatureEnabled: false, isOciFeatureEnabled: false, isPlatformCostsFeatureEnabled: false, + isRosFeatureEnabled: false, }; export const stateKey = 'featureFlags'; @@ -44,11 +46,12 @@ export function featureFlagsReducer(state = defaultState, action: FeatureFlagsAc isCostTypeFeatureEnabled: action.payload.isCostTypeFeatureEnabled, isCurrencyFeatureEnabled: action.payload.isCurrencyFeatureEnabled, isExportsFeatureEnabled: action.payload.isExportsFeatureEnabled, - isFINsightsFeatureEnabled: action.payload.isFINsightsFeatureEnabled, + isFinsightsFeatureEnabled: action.payload.isFinsightsFeatureEnabled, isIbmFeatureEnabled: action.payload.isIbmFeatureEnabled, isNegativeFilteringFeatureEnabled: action.payload.isNegativeFilteringFeatureEnabled, isOciFeatureEnabled: action.payload.isOciFeatureEnabled, isPlatformCostsFeatureEnabled: action.payload.isPlatformCostsFeatureEnabled, + isRosFeatureEnabled: action.payload.isRosFeatureEnabled, }; default: diff --git a/src/store/featureFlags/featureFlagsSelectors.ts b/src/store/featureFlags/featureFlagsSelectors.ts index 6e3e0ea87..11943ae0e 100644 --- a/src/store/featureFlags/featureFlagsSelectors.ts +++ b/src/store/featureFlags/featureFlagsSelectors.ts @@ -14,11 +14,12 @@ export const selectIsCostTypeFeatureEnabled = (state: RootState) => selectFeatureFlagsState(state).isCostTypeFeatureEnabled; export const selectIsExportsFeatureEnabled = (state: RootState) => selectFeatureFlagsState(state).isExportsFeatureEnabled; -export const selectIsFINsightsFeatureEnabled = (state: RootState) => - selectFeatureFlagsState(state).isFINsightsFeatureEnabled; +export const selectIsFinsightsFeatureEnabled = (state: RootState) => + selectFeatureFlagsState(state).isFinsightsFeatureEnabled; export const selectIsIbmFeatureEnabled = (state: RootState) => selectFeatureFlagsState(state).isIbmFeatureEnabled; export const selectIsNegativeFilteringFeatureEnabled = (state: RootState) => selectFeatureFlagsState(state).isNegativeFilteringFeatureEnabled; export const selectIsOciFeatureEnabled = (state: RootState) => selectFeatureFlagsState(state).isOciFeatureEnabled; export const selectIsPlatformCostsFeatureEnabled = (state: RootState) => selectFeatureFlagsState(state).isPlatformCostsFeatureEnabled; +export const selectIsRosFeatureEnabled = (state: RootState) => selectFeatureFlagsState(state).isRosFeatureEnabled; diff --git a/src/utils/userAccess.ts b/src/utils/userAccess.ts index c285d6f64..82ac0bfc4 100644 --- a/src/utils/userAccess.ts +++ b/src/utils/userAccess.ts @@ -91,7 +91,7 @@ export const isOcpAvailable = (userAccess: UserAccess, ocpProviders: Providers) return hasOcpAccess(userAccess) && hasProviders(ocpProviders); }; -// Returns true if user has access to OCP +// Returns true if user has access to RHEL export const hasRhelAccess = (userAccess: UserAccess) => { return hasAccess(userAccess, UserAccessType.rhel); }; @@ -100,3 +100,13 @@ export const hasRhelAccess = (userAccess: UserAccess) => { export const isRhelAvailable = (userAccess: UserAccess, rhelProviders: Providers) => { return hasRhelAccess(userAccess) && hasProviders(rhelProviders); }; + +// Returns true if user has access to ROS +export const hasRosAccess = (userAccess: UserAccess) => { + return hasAccess(userAccess, UserAccessType.ros); +}; + +// Returns true if user has access to RHEL and at least one source provider +export const isRosAvailable = (userAccess: UserAccess, rosProviders: Providers) => { + return hasRosAccess(userAccess) && hasProviders(rosProviders); +};