diff --git a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/report-clients.api.md b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/report-clients.api.md index ec75f5519c..cc8325195e 100644 --- a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/report-clients.api.md +++ b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/report-clients.api.md @@ -410,6 +410,10 @@ export class OptimizationsClient implements OptimizationsApi { // @public (undocumented) export interface OrchestratorSlimApi { + // (undocumented) + checkWorkflowAvailability( + workflowId: string, + ): Promise; // (undocumented) executeWorkflow( workflowId: string, @@ -429,6 +433,10 @@ export class OrchestratorSlimClient implements OrchestratorSlimApi { identityApi: IdentityApi; }); // (undocumented) + checkWorkflowAvailability( + workflowId: string, + ): Promise; + // (undocumented) executeWorkflow( workflowId: string, workflowInputData: D, @@ -487,5 +495,22 @@ export interface Tag { values: ProjectValue[]; } +// @public (undocumented) +export interface WorkflowAvailabilityResult { + // (undocumented) + available: boolean; + // (undocumented) + errorMessage?: string; + // (undocumented) + reason?: WorkflowUnavailableReason; +} + +// @public (undocumented) +export type WorkflowUnavailableReason = + | 'not_configured' + | 'not_found' + | 'access_denied' + | 'service_unavailable'; + // (No @packageDocumentation comment for this package) ``` diff --git a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/report.api.md b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/report.api.md index 5ca7deb099..bd03cf3477 100644 --- a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/report.api.md +++ b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/report.api.md @@ -733,6 +733,10 @@ export class OptimizationsClient implements OptimizationsApi { // @public (undocumented) export interface OrchestratorSlimApi { + // (undocumented) + checkWorkflowAvailability( + workflowId: string, + ): Promise; // (undocumented) executeWorkflow( workflowId: string, @@ -752,6 +756,10 @@ export class OrchestratorSlimClient implements OrchestratorSlimApi { identityApi: IdentityApi; }); // (undocumented) + checkWorkflowAvailability( + workflowId: string, + ): Promise; + // (undocumented) executeWorkflow( workflowId: string, workflowInputData: D, @@ -1145,5 +1153,22 @@ export type TypedResponse = Omit & { json: () => Promise; }; +// @public (undocumented) +export interface WorkflowAvailabilityResult { + // (undocumented) + available: boolean; + // (undocumented) + errorMessage?: string; + // (undocumented) + reason?: WorkflowUnavailableReason; +} + +// @public (undocumented) +export type WorkflowUnavailableReason = + | 'not_configured' + | 'not_found' + | 'access_denied' + | 'service_unavailable'; + // (No @packageDocumentation comment for this package) ``` diff --git a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/src/clients/orchestrator-slim/OrchestratorSlimApi.ts b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/src/clients/orchestrator-slim/OrchestratorSlimApi.ts index fdbf835d49..de48395466 100644 --- a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/src/clients/orchestrator-slim/OrchestratorSlimApi.ts +++ b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/src/clients/orchestrator-slim/OrchestratorSlimApi.ts @@ -16,9 +16,26 @@ import type { JsonObject } from '@backstage/types'; +/** @public */ +export type WorkflowUnavailableReason = + | 'not_configured' + | 'not_found' + | 'access_denied' + | 'service_unavailable'; + +/** @public */ +export interface WorkflowAvailabilityResult { + available: boolean; + reason?: WorkflowUnavailableReason; + errorMessage?: string; +} + /** @public */ export interface OrchestratorSlimApi { isWorkflowAvailable(workflowId: string): Promise; + checkWorkflowAvailability( + workflowId: string, + ): Promise; executeWorkflow( workflowId: string, workflowInputData: D, diff --git a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/src/clients/orchestrator-slim/OrchestratorSlimClient.ts b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/src/clients/orchestrator-slim/OrchestratorSlimClient.ts index a409bf00ab..f5ace3e1b3 100644 --- a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/src/clients/orchestrator-slim/OrchestratorSlimClient.ts +++ b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization-common/src/clients/orchestrator-slim/OrchestratorSlimClient.ts @@ -19,7 +19,10 @@ import { FetchApi, IdentityApi, } from '@backstage/core-plugin-api'; -import type { OrchestratorSlimApi } from './OrchestratorSlimApi'; +import type { + OrchestratorSlimApi, + WorkflowAvailabilityResult, +} from './OrchestratorSlimApi'; import type { JsonObject } from '@backstage/types'; /** @public */ @@ -40,6 +43,21 @@ export class OrchestratorSlimClient implements OrchestratorSlimApi { } async isWorkflowAvailable(workflowId: string): Promise { + const result = await this.checkWorkflowAvailability(workflowId); + return result.available; + } + + async checkWorkflowAvailability( + workflowId: string, + ): Promise { + if (!workflowId) { + return { + available: false, + reason: 'not_configured', + errorMessage: 'No workflow configured to apply recommendations', + }; + } + if (!this.baseUrl) { this.baseUrl = await this.discoveryApi.getBaseUrl('orchestrator'); } @@ -48,15 +66,61 @@ export class OrchestratorSlimClient implements OrchestratorSlimApi { workflowId, )}/overview`; - let result = false; try { const response = await this.fetchApi.fetch(url, { method: 'GET' }); - result = response.ok; - } catch { - // Carry on... - } - return result; + if (response.ok) { + return { available: true }; + } + + // Try to extract error message from the response + let errorMessage: string | undefined; + try { + const errorResponse = (await response.json()) as { + error?: { message?: string }; + message?: string; + }; + errorMessage = + errorResponse?.error?.message || errorResponse?.message || undefined; + } catch { + // Ignore JSON parsing errors + } + + if (response.status === 404) { + return { + available: false, + reason: 'not_found', + errorMessage: errorMessage || 'Workflow not found', + }; + } + + if (response.status === 403 || response.status === 401) { + return { + available: false, + reason: 'access_denied', + errorMessage: + errorMessage || + 'You do not have permission to access this workflow', + }; + } + + return { + available: false, + reason: 'service_unavailable', + errorMessage: + errorMessage || 'Workflow service is currently unavailable', + }; + } catch (err) { + const errorMessage = + err instanceof Error + ? err.message + : 'Unable to connect to workflow service'; + return { + available: false, + reason: 'service_unavailable', + errorMessage, + }; + } } /** @public */ diff --git a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/pages/optimizations-breakdown/OptimizationsBreakdownPage.tsx b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/pages/optimizations-breakdown/OptimizationsBreakdownPage.tsx index c32c824c57..ea4a4871be 100644 --- a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/pages/optimizations-breakdown/OptimizationsBreakdownPage.tsx +++ b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/pages/optimizations-breakdown/OptimizationsBreakdownPage.tsx @@ -24,6 +24,7 @@ import { } from '@backstage/core-components'; import { configApiRef, useApi } from '@backstage/core-plugin-api'; import type { RecommendationBoxPlots } from '@red-hat-developer-hub/plugin-redhat-resource-optimization-common/models'; +import type { WorkflowUnavailableReason } from '@red-hat-developer-hub/plugin-redhat-resource-optimization-common/clients'; import { getTimeFromNow } from '../../utils/dates'; import { BasePage } from '../../components/BasePage'; import { type Interval, OptimizationType } from './models/ChartEnums'; @@ -143,6 +144,10 @@ export const OptimizationsBreakdownPage = () => { 'resourceOptimization.optimizationWorkflowId', ) ?? '', ); + const workflowUnavailableReasonRef = useRef< + WorkflowUnavailableReason | undefined + >(undefined); + const workflowErrorMessageRef = useRef(undefined); const orchestratorSlimApi = useApi(orchestratorSlimApiRef); const optimizationsApi = useApi(optimizationsApiRef); const { @@ -150,11 +155,17 @@ export const OptimizationsBreakdownPage = () => { loading, error, } = useAsync(async () => { - const isWorkflowAvailable = await orchestratorSlimApi.isWorkflowAvailable( - workflowIdRef.current, - ); - if (!isWorkflowAvailable) { + const availabilityResult = + await orchestratorSlimApi.checkWorkflowAvailability( + workflowIdRef.current, + ); + if (availabilityResult.available) { + workflowUnavailableReasonRef.current = undefined; + workflowErrorMessageRef.current = undefined; + } else { workflowIdRef.current = ''; + workflowUnavailableReasonRef.current = availabilityResult.reason; + workflowErrorMessageRef.current = availabilityResult.errorMessage; } const apiQuery = { @@ -263,6 +274,8 @@ export const OptimizationsBreakdownPage = () => { onRecommendationTermChange={handleRecommendationTermChange} onApplyRecommendation={handleApplyRecommendation} workflowId={workflowIdRef.current} + workflowUnavailableReason={workflowUnavailableReasonRef.current} + workflowErrorMessage={workflowErrorMessageRef.current} /> @@ -280,6 +293,8 @@ export const OptimizationsBreakdownPage = () => { onRecommendationTermChange={handleRecommendationTermChange} onApplyRecommendation={handleApplyRecommendation} workflowId={workflowIdRef.current} + workflowUnavailableReason={workflowUnavailableReasonRef.current} + workflowErrorMessage={workflowErrorMessageRef.current} /> diff --git a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/pages/optimizations-breakdown/components/optimization-engine-tab/OptimizationEngineTab.tsx b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/pages/optimizations-breakdown/components/optimization-engine-tab/OptimizationEngineTab.tsx index a5f5eebd10..425e44b227 100644 --- a/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/pages/optimizations-breakdown/components/optimization-engine-tab/OptimizationEngineTab.tsx +++ b/workspaces/redhat-resource-optimization/plugins/redhat-resource-optimization/src/pages/optimizations-breakdown/components/optimization-engine-tab/OptimizationEngineTab.tsx @@ -14,8 +14,9 @@ * limitations under the License. */ -import React from 'react'; -import { Button, Grid } from '@material-ui/core'; +import React, { useMemo } from 'react'; +import { Box, Button, Grid, Tooltip } from '@material-ui/core'; +import type { WorkflowUnavailableReason } from '@red-hat-developer-hub/plugin-redhat-resource-optimization-common/clients'; import { RecommendationType } from '../../models/ChartEnums'; import { ChartInfoCard } from './components/chart-info-card/ChartInfoCard'; import { CodeInfoCard } from './components/CodeInfoCard'; @@ -25,6 +26,13 @@ type ContainerInfoCardProps = Parameters[0]; type CodeInfoCardProps = Parameters[0]; type ChartInfoCardProps = Parameters[0]; +const DEFAULT_WORKFLOW_MESSAGES: Record = { + not_configured: 'No workflow configured to apply recommendations', + not_found: 'Workflow not found', + access_denied: 'You do not have permission to access this workflow', + service_unavailable: 'Workflow service is currently unavailable', +}; + interface OptimizationEngineTabProps extends ContainerInfoCardProps { currentConfiguration: CodeInfoCardProps['yamlCodeData']; recommendedConfiguration: CodeInfoCardProps['yamlCodeData']; @@ -32,9 +40,32 @@ interface OptimizationEngineTabProps extends ContainerInfoCardProps { optimizationType: ChartInfoCardProps['optimizationType']; onApplyRecommendation?: React.MouseEventHandler; workflowId?: string; + workflowUnavailableReason?: WorkflowUnavailableReason; + workflowErrorMessage?: string; } export const OptimizationEngineTab = (props: OptimizationEngineTabProps) => { + const isWorkflowAvailable = !!props.workflowId; + + const tooltipMessage = useMemo(() => { + if (isWorkflowAvailable) { + return ''; + } + // Prefer the actual error message from the API + if (props.workflowErrorMessage) { + return props.workflowErrorMessage; + } + // Fall back to default messages based on reason + if (props.workflowUnavailableReason) { + return DEFAULT_WORKFLOW_MESSAGES[props.workflowUnavailableReason]; + } + return DEFAULT_WORKFLOW_MESSAGES.not_configured; + }, [ + isWorkflowAvailable, + props.workflowErrorMessage, + props.workflowUnavailableReason, + ]); + return ( @@ -57,14 +88,23 @@ export const OptimizationEngineTab = (props: OptimizationEngineTabProps) => { showCopyCodeButton yamlCodeData={props.recommendedConfiguration} action={ - + + + + } />