Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/deploy-backstage-empty-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@openchoreo/backstage-plugin': patch
---

Use the standard Backstage `EmptyState` for the Deploy tab's empty and error states (Component, Project, and Resource), matching the look of other empty states in the app (e.g. "Workflows Not Available" on the Build tab). Replaces the card + custom message + Retry button with a title + description empty state, and the "no environments" state now links to the project's deployment pipeline so it can be reviewed/configured.
7 changes: 7 additions & 0 deletions .changeset/observability-env-error-messages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@openchoreo/backstage-plugin-openchoreo-observability': patch
'@openchoreo/backstage-plugin-react': patch
'@openchoreo/backstage-plugin-backend': patch
---

Replace the generic "No environments found. Make sure your component is properly configured." message on the observability pages (Runtime Logs, Runtime Events, Alerts, Wirelogs, Metrics, Traces, Incidents, Cost Analysis, RCA — component and project scoped) with cause-specific messaging. `useProjectEnvironments` now reports a discriminated status — `empty-pipeline` (the deployment pipeline has no environments), `forbidden` (permission to view the pipeline is denied), or `unavailable` (the pipeline is missing or couldn't be loaded) — and the pages render a cause-specific state via a shared `EnvironmentsStatusNotice` component, using the standard Backstage `EmptyState` (matching the Deploy tab). A missing `deploymentPipelineRef` now returns a clean 404 instead of a 500.
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ describe('ProjectInfoService', () => {
expect(mockGET).toHaveBeenCalledTimes(2);
});

it('throws when project has no pipeline ref', async () => {
it('throws a NotFoundError when project has no pipeline ref', async () => {
const noPipeline = {
...k8sProject,
spec: { deploymentPipelineRef: undefined },
Expand All @@ -162,7 +162,10 @@ describe('ProjectInfoService', () => {
'my-project',
'token-123',
),
).rejects.toThrow('no deployment pipeline reference');
).rejects.toMatchObject({
name: 'NotFoundError',
message: expect.stringContaining('no deployment pipeline reference'),
});
});

it('throws when pipeline fetch fails', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LoggerService } from '@backstage/backend-plugin-api';
import { NotFoundError } from '@backstage/errors';
import {
createOpenChoreoApiClient,
assertApiResponse,
Expand Down Expand Up @@ -94,7 +95,7 @@ export class ProjectInfoService {

const pipelineName = project!.spec?.deploymentPipelineRef?.name;
if (!pipelineName) {
throw new Error(
throw new NotFoundError(
`Project ${projectName} has no deployment pipeline reference`,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ function setupDefaultMocks() {
mockUseProjectEnvironments.mockReturnValue({
environments: [{ name: 'development', displayName: 'Development' }],
loading: false,
status: 'ok',
error: null,
refetch: jest.fn(),
});

mockUseUrlFiltersForAlerts.mockReturnValue({
Expand Down Expand Up @@ -196,17 +198,19 @@ describe('ObservabilityAlertsPage', () => {
expect(screen.getByTestId('total-count')).toHaveTextContent('2');
});

it('shows environment error', async () => {
it('shows the pipeline-unavailable notice when environments fail to load', async () => {
mockUseProjectEnvironments.mockReturnValue({
environments: [],
loading: false,
status: 'unavailable',
error: 'Failed to fetch environments',
refetch: jest.fn(),
});

await renderPage();

expect(
screen.getByText('Failed to fetch environments'),
screen.getByText(/Couldn't load this project's deployment pipeline/i),
).toBeInTheDocument();
});

Expand Down Expand Up @@ -244,11 +248,13 @@ describe('ObservabilityAlertsPage', () => {
expect(screen.queryByText('Retry')).not.toBeInTheDocument();
});

it('shows no environments alert when none found', async () => {
it('shows the empty-pipeline notice when the pipeline has no environments', async () => {
mockUseProjectEnvironments.mockReturnValue({
environments: [],
loading: false,
status: 'empty-pipeline',
error: null,
refetch: jest.fn(),
});

mockUseUrlFiltersForAlerts.mockReturnValue({
Expand All @@ -260,7 +266,7 @@ describe('ObservabilityAlertsPage', () => {

expect(
screen.getByText(
'No environments found. Make sure your component is properly configured.',
/This project's deployment pipeline has no environments configured/i,
),
).toBeInTheDocument();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
pickRangeForAge,
useProjectEnvironments,
} from '@openchoreo/backstage-plugin-react';
import { EnvironmentsStatusNotice } from '../common';
import { useRuntimeLogsStyles } from '../RuntimeLogs/styles';
import type { AlertSummary } from '../../types';

Expand All @@ -29,7 +30,7 @@ const ObservabilityAlertsContent = () => {
const {
environments,
loading: environmentsLoading,
error: environmentsError,
status: environmentsStatus,
} = useProjectEnvironments(project, namespace);

const { filters, updateFilters } = useUrlFiltersForAlerts({
Expand Down Expand Up @@ -213,8 +214,19 @@ const ObservabilityAlertsContent = () => {
);
};

if (environmentsError) {
return <Box>{renderError(environmentsError)}</Box>;
if (environmentsLoading) {
return <Progress />;
}

if (environmentsStatus !== 'ok') {
return (
<Box>
<EnvironmentsStatusNotice
status={environmentsStatus}
feature="alerts"
/>
</Box>
);
}

return (
Expand All @@ -229,17 +241,6 @@ const ObservabilityAlertsContent = () => {

{alertsError && renderError(alertsError)}

{!filters.environment &&
!environmentsLoading &&
environments.length === 0 && (
<Alert severity="info" className={classes.errorContainer}>
<Typography variant="body1">
No environments found. Make sure your component is properly
configured.
</Typography>
</Alert>
)}

{filters.environment && (
<>
<AlertsActions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ function setupDefaultMocks() {
mockUseProjectEnvironments.mockReturnValue({
environments: [defaultEnvironment],
loading: false,
status: 'ok',
error: null,
});
mockUseUrlFilters.mockReturnValue({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ForbiddenState,
useProjectEnvironments,
} from '@openchoreo/backstage-plugin-react';
import { EnvironmentsStatusNotice } from '../common';
import { CostAnalysisReport } from './CostAnalysisReport';
import { EntityLinkContext } from '../RCA/RCAReport/EntityLinkContext';

Expand All @@ -29,7 +30,7 @@ const CostAnalysisListContent = () => {
const {
environments,
loading: environmentsLoading,
error: environmentsError,
status: environmentsStatus,
} = useProjectEnvironments(projectName, namespace);
const { filters, updateFilters } = useUrlFilters({ environments });

Expand Down Expand Up @@ -95,6 +96,25 @@ const CostAnalysisListContent = () => {
);
};

// Wait for the environment resolution before rendering the filters, so the
// filter bar doesn't flash before we know whether to show the notice.
if (environmentsLoading) {
return <Progress />;
}

// No resolvable environments (empty, forbidden, or unavailable) → show only
// the notice, without the filters or reports.
if (environmentsStatus !== 'ok') {
return (
<Box>
<EnvironmentsStatusNotice
status={environmentsStatus}
feature="cost reports"
/>
</Box>
);
}

return (
<Box>
{reportsLoading && <Progress />}
Expand All @@ -108,17 +128,6 @@ const CostAnalysisListContent = () => {
environmentsLoading={environmentsLoading}
/>

{environmentsError && (
<Box mt={2} mb={2}>
<Alert severity="warning">
<Typography variant="body1">
Failed to load environments. Environment filtering may not be
available.
</Typography>
</Alert>
</Box>
)}

{reportsError && renderError(reportsError)}

<RCAActions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ function setupDefaultMocks() {
mockUseProjectEnvironments.mockReturnValue({
environments: [defaultEnvironment],
loading: false,
status: 'ok',
error: null,
refetch: jest.fn(),
});

mockUseGetComponentsByProject.mockReturnValue({
Expand Down Expand Up @@ -208,11 +210,13 @@ describe('ObservabilityProjectIncidentsPage', () => {
expect(screen.getByText('Retry')).toBeInTheDocument();
});

it('shows no environments alert', async () => {
it('shows the empty-pipeline notice when the pipeline has no environments', async () => {
mockUseProjectEnvironments.mockReturnValue({
environments: [],
loading: false,
status: 'empty-pipeline',
error: null,
refetch: jest.fn(),
});

mockUseUrlFiltersForIncidents.mockReturnValue({
Expand All @@ -226,7 +230,9 @@ describe('ObservabilityProjectIncidentsPage', () => {
await renderPage();

expect(
screen.getByText('No environments found for this project.'),
screen.getByText(
/This project's deployment pipeline has no environments configured/i,
),
).toBeInTheDocument();
});

Expand All @@ -246,19 +252,19 @@ describe('ObservabilityProjectIncidentsPage', () => {
expect(screen.queryByTestId('incidents-table')).not.toBeInTheDocument();
});

it('renders environments error as observability disabled info', async () => {
it('shows the pipeline-unavailable notice when environments fail to load', async () => {
mockUseProjectEnvironments.mockReturnValue({
environments: [],
loading: false,
error: 'Observability is not enabled for this project',
status: 'unavailable',
error: 'Failed to load deployment pipeline: 500 Internal Server Error',
refetch: jest.fn(),
});

await renderPage();

expect(
screen.getByText(
'Observability is not enabled for this project. Enable observability to view incidents.',
),
screen.getByText(/Couldn't load this project's deployment pipeline/i),
).toBeInTheDocument();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
useIncidentsPermission,
useProjectEnvironments,
} from '@openchoreo/backstage-plugin-react';
import { EnvironmentsStatusNotice } from '../common';
import { useRuntimeLogsStyles } from '../RuntimeLogs/styles';
import type { IncidentSummary } from '../../types';

Expand All @@ -31,7 +32,7 @@ const ObservabilityProjectIncidentsContent = () => {
const {
environments,
loading: environmentsLoading,
error: environmentsError,
status: environmentsStatus,
} = useProjectEnvironments(projectName, namespace);

const {
Expand Down Expand Up @@ -253,8 +254,19 @@ const ObservabilityProjectIncidentsContent = () => {
);
};

if (environmentsError) {
return <Box>{renderError(environmentsError)}</Box>;
if (environmentsLoading) {
return <Progress />;
}

if (environmentsStatus !== 'ok') {
return (
<Box>
<EnvironmentsStatusNotice
status={environmentsStatus}
feature="incidents"
/>
</Box>
);
}

if (componentsError) {
Expand All @@ -275,16 +287,6 @@ const ObservabilityProjectIncidentsContent = () => {

{incidentsError && renderError(incidentsError)}

{!filters.environment &&
!environmentsLoading &&
environments.length === 0 && (
<Alert severity="info" className={classes.errorContainer}>
<Typography variant="body1">
No environments found for this project.
</Typography>
</Alert>
)}

{filters.environment && (
<>
<IncidentsActions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ function setupDefaultMocks() {
mockUseProjectEnvironments.mockReturnValue({
environments: [defaultEnvironment],
loading: false,
status: 'ok',
error: null,
});

Expand Down Expand Up @@ -218,6 +219,7 @@ describe('ObservabilityMetricsPage', () => {
mockUseProjectEnvironments.mockReturnValue({
environments: [defaultEnvironment],
loading: true,
status: 'ok',
error: null,
});

Expand Down
Loading
Loading