Skip to content
Open
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
4 changes: 4 additions & 0 deletions src/Messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ const messages = defineMessages({
id: 'helpPanel.support.customerPortalLink',
defaultMessage: 'Customer Portal',
},
supportCasesLoadError: {
id: 'supportCasesLoadError',
defaultMessage: 'Unable to load support cases. Please try again later.',
},
supportCasesTableTitle: {
id: 'helpPanel.support.casesTableTitle',
defaultMessage: 'My open support cases',
Expand Down
9 changes: 5 additions & 4 deletions src/components/HelpPanel/HelpPanelContent.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { supportCasesResponse } from '../../user-journeys/_shared/supportCasesResponse';
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import React, { useState } from 'react';
import { MemoryRouter } from 'react-router-dom';
Expand Down Expand Up @@ -130,11 +131,11 @@ const mockHelpPanelHandlers = [
return HttpResponse.json({ success: true });
}),
// Support cases API (empty state) - prevents "Failed to fetch" errors
http.post('https://api.access.redhat.com/support/v1/cases/filter', () =>
HttpResponse.json({ cases: [] })
http.post('https://graphql.redhat.com', () =>
HttpResponse.json(supportCasesResponse([]))
),
http.post('https://api.access.stage.redhat.com/support/v1/cases/filter', () =>
HttpResponse.json({ cases: [] })
http.post('https://graphql.stage.redhat.com', () =>
HttpResponse.json(supportCasesResponse([]))
),
];

Expand Down
30 changes: 11 additions & 19 deletions src/components/HelpPanel/HelpPanelTabs/SupportPanel.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { supportCasesResponse } from '../../../user-journeys/_shared/supportCasesResponse';
import type { Meta, StoryObj } from '@storybook/react-webpack5';
import React from 'react';
import { IntlProvider } from 'react-intl';
Expand All @@ -9,8 +10,7 @@ import {
supportPanelMswHandlersWithCases,
} from '../../../user-journeys/_shared/helpPanelJourneyHelpers';

const supportCasesFilterUrlStage =
'https://api.access.stage.redhat.com/support/v1/cases/filter';
const supportCasesFilterUrlStage = 'https://graphql.stage.redhat.com';

/**
* Wrapper to provide IntlProvider (component uses useIntl and Messages).
Expand Down Expand Up @@ -73,7 +73,7 @@ export const Loading: Story = {
handlers: [
http.post(supportCasesFilterUrlStage, async () => {
await delay(2000);
return HttpResponse.json({ cases: [] });
return HttpResponse.json(supportCasesResponse([]));
}),
],
},
Expand Down Expand Up @@ -156,7 +156,7 @@ export const WithCasesPagination: Story = {
msw: {
handlers: [
http.post(supportCasesFilterUrlStage, () =>
HttpResponse.json({ cases: manyCases })
HttpResponse.json(supportCasesResponse(manyCases))
),
],
},
Expand Down Expand Up @@ -189,34 +189,26 @@ export const WithCasesPagination: Story = {
};

/**
* API error: component shows empty state after failed fetch.
* API error: component distinguishes a failure from an empty case list.
*/
export const ApiError: Story = {
parameters: {
testRunner: { ignoreConsoleErrors: true },
msw: {
handlers: [
http.post(supportCasesFilterUrlStage, () =>
HttpResponse.json({ error: 'Server error' }, { status: 500 })
HttpResponse.json({ errors: [{ message: 'Missing CRM contact' }] })
),
],
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

await waitFor(
() => {
const emptyState = document.querySelector(
'[data-ouia-component-id="help-panel-support-empty-state"]'
);
expect(emptyState).toBeInTheDocument();
},
{ timeout: 5000 }
);

expect(canvas.getByText('No open support cases')).toBeInTheDocument();
expect(
canvas.getByRole('button', { name: /open a support case/i })
await canvas.findByText(
'Unable to load support cases. Please try again later.'
)
).toBeInTheDocument();
expect(canvas.queryByText('No open support cases')).not.toBeInTheDocument();
},
};
53 changes: 53 additions & 0 deletions src/components/HelpPanel/HelpPanelTabs/SupportPanel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import SupportPanel from './SupportPanel';
import { fetchSupportCases } from '../../../utils/fetchSupportCases';

jest.mock('../../../utils/fetchSupportCases');
jest.mock('@redhat-cloud-services/frontend-components/useChrome', () => ({
__esModule: true,
default: () => ({ auth: {}, getEnvironment: () => 'stage' }),
}));
const fetchCases = fetchSupportCases as jest.MockedFunction<
typeof fetchSupportCases
>;
const renderPanel = () =>
render(
<IntlProvider locale="en">
<SupportPanel />
</IntlProvider>
);
afterEach(() => jest.restoreAllMocks());
it('shows new status values and changes the visible page while preserving the total', async () => {
fetchCases.mockResolvedValue(
Array.from({ length: 25 }, (_, i) => ({
id: String(i),
caseNumber: `000${i}`,
summary: `Case title ${i}`,
lastModifiedById: '',
lastModifiedDate: '',
severity: '3 (Medium)',
status: 'Waiting on Engineering',
}))
);
renderPanel();
expect(await screen.findByText('Case title 0')).toBeInTheDocument();
expect(screen.getAllByText('Waiting on Engineering')).toHaveLength(20);
expect(screen.queryByText('Case title 20')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /go to next page/i }));
expect(await screen.findByText('Case title 20')).toBeInTheDocument();
expect(screen.queryByText('Case title 0')).not.toBeInTheDocument();
expect(screen.getByRole('heading', { name: /25/ })).toBeInTheDocument();
});
it('renders an error rather than an empty state when access fails', async () => {
jest.spyOn(console, 'error').mockImplementation(() => undefined);
fetchCases.mockRejectedValue(new Error('GraphQL failed'));
renderPanel();
expect(
await screen.findByText(
'Unable to load support cases. Please try again later.'
)
).toBeInTheDocument();
expect(screen.queryByText('No open support cases')).not.toBeInTheDocument();
});
92 changes: 35 additions & 57 deletions src/components/HelpPanel/HelpPanelTabs/SupportPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import {
Alert,
Button,
Content,
ContentVariants,
Expand All @@ -21,18 +22,14 @@ import InProgressIcon from '@patternfly/react-icons/dist/dynamic/icons/in-progre
import { Table, TableVariant, Tbody, Td, Tr } from '@patternfly/react-table';
import { useIntl } from 'react-intl';
import messages from '../../../Messages';
import {
SupportCase,
fetchSupportCases,
} from '../../../utils/fetchSupportCases';

const SUPPORT_CASE_URL =
'https://access.redhat.com/support/cases/#/case/new/get-support?caseCreate=true';

type Case = {
id: string;
caseNumber: string;
summary: string;
lastModifiedDate: string;
status: string;
};

const columnNames = {
summary: 'Title',
status: 'Status',
Expand Down Expand Up @@ -74,13 +71,14 @@ export const statusIcons = (status: string) => {
</span>
),
};
return statusMapper[status] ?? '';
return statusMapper[status] ?? status;
};
const SupportPanel: React.FunctionComponent = () => {
const intl = useIntl();
const [cases, setCases] = useState<Case[]>([]);
const [cases, setCases] = useState<SupportCase[]>([]);
const chrome = useChrome();
const [isLoading, setIsLoading] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);

const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(20);
Expand All @@ -101,59 +99,39 @@ const SupportPanel: React.FunctionComponent = () => {
setPage(newPage);
};

const getUrl = (env: string) =>
`https://api.access${
env === 'stage' || env === 'frhStage' ? '.stage' : ''
}.redhat.com/support/v1/cases/filter`;

const fetchSupportCases = async () => {
const token = await chrome.auth.getToken();
const user = await chrome.auth.getUser();
const options = {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
createdBySSOName: `${user?.identity.user?.username}`,
}),
};

try {
const response = await fetch(getUrl(chrome.getEnvironment()), options);
const data = await response.json();
const { cases } = data;

if (cases && Array.isArray(cases)) {
cases.sort(
(a: { lastModifiedDate: number }, b: { lastModifiedDate: number }) =>
new Date(b.lastModifiedDate).getTime() -
new Date(a.lastModifiedDate).getTime()
);
setCases(cases);
} else {
setCases([]);
}

setIsLoading(false);
} catch (error) {
console.error('Unable to fetch support cases', error);
setCases([]);
setIsLoading(false);
}
};

useEffect(() => {
const controller = new AbortController();
setIsLoading(true);
fetchSupportCases();
setHasError(false);
fetchSupportCases(chrome.auth, chrome.getEnvironment(), controller.signal)
.then((data) => {
if (!controller.signal.aborted) {
setCases(data);
setPage(1);
}
})
.catch((error) => {
if (!controller.signal.aborted) {
console.error('Unable to fetch support cases', error);
setHasError(true);
}
})
.finally(() => {
if (!controller.signal.aborted) setIsLoading(false);
});
return () => controller.abort();
}, []);

return (
<>
{isLoading ? (
<SkeletonTable rows={3} />
) : hasError ? (
<Alert
variant="danger"
isInline
title={intl.formatMessage(messages.supportCasesLoadError)}
/>
) : cases.length === 0 ? (
<EmptyState
icon={HeadsetIcon}
Expand Down Expand Up @@ -203,7 +181,7 @@ const SupportPanel: React.FunctionComponent = () => {
data-ouia-component-id="help-panel-support-cases-table"
>
<Tbody>
{cases.map((c) => (
{cases.slice((page - 1) * perPage, page * perPage).map((c) => (
<Tr key={c.id}>
<Td dataLabel={columnNames.summary} modifier="wrap">
<a
Expand Down
30 changes: 15 additions & 15 deletions src/user-journeys/_shared/helpPanelJourneyHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { supportCasesResponse } from './supportCasesResponse';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { HttpResponse, http } from 'msw';
import { TEST_TIMEOUTS, delay } from './testConstants';
Expand Down Expand Up @@ -316,11 +317,11 @@ export const helpPanelMswHandlers = [
return HttpResponse.json({ success: true });
}),
// Support cases API (empty state) - prevents "Failed to fetch" errors
http.post('https://api.access.redhat.com/support/v1/cases/filter', () =>
HttpResponse.json({ cases: [] })
http.post('https://graphql.redhat.com', () =>
HttpResponse.json(supportCasesResponse([]))
),
http.post('https://api.access.stage.redhat.com/support/v1/cases/filter', () =>
HttpResponse.json({ cases: [] })
http.post('https://graphql.stage.redhat.com', () =>
HttpResponse.json(supportCasesResponse([]))
),
];

Expand Down Expand Up @@ -511,11 +512,11 @@ export const searchPanelJourneyMswHandlers = [
return HttpResponse.json({ success: true });
}),
// Support cases API (empty state) - prevents "Failed to fetch" errors
http.post('https://api.access.redhat.com/support/v1/cases/filter', () =>
HttpResponse.json({ cases: [] })
http.post('https://graphql.redhat.com', () =>
HttpResponse.json(supportCasesResponse([]))
),
http.post('https://api.access.stage.redhat.com/support/v1/cases/filter', () =>
HttpResponse.json({ cases: [] })
http.post('https://graphql.stage.redhat.com', () =>
HttpResponse.json(supportCasesResponse([]))
),
// Base handlers last (overridden routes above take priority in MSW)
...helpPanelMswHandlers,
Expand Down Expand Up @@ -744,12 +745,11 @@ export const mockApiBundles = [
},
];

const supportCasesFilterUrlProd =
'https://api.access.redhat.com/support/v1/cases/filter';
const supportCasesFilterUrlStage =
'https://api.access.stage.redhat.com/support/v1/cases/filter';
const supportCasesFilterUrlProd = 'https://graphql.redhat.com';
const supportCasesFilterUrlStage = 'https://graphql.stage.redhat.com';

const emptySupportCasesResponse = () => HttpResponse.json({ cases: [] });
const emptySupportCasesResponse = () =>
HttpResponse.json(supportCasesResponse([]));

/**
* MSW handlers for Support Panel - empty state (no open support cases).
Expand Down Expand Up @@ -929,10 +929,10 @@ const mockSupportCases = [
*/
export const supportPanelMswHandlersWithCases = [
http.post(supportCasesFilterUrlProd, () =>
HttpResponse.json({ cases: mockSupportCases })
HttpResponse.json(supportCasesResponse(mockSupportCases))
),
http.post(supportCasesFilterUrlStage, () =>
HttpResponse.json({ cases: mockSupportCases })
HttpResponse.json(supportCasesResponse(mockSupportCases))
),
];

Expand Down
Loading
Loading