mirror of
https://github.com/quay/quay.git
synced 2026-01-26 06:21:37 +03:00
feat(ui): add system status banner (PROJQUAY-9494) (#4417)
displays warning banners when Quay is in read-only mode or account recovery mode, integrated into both standalone and plugin layouts with Cypress e2e tests Signed-off-by: Brady Pratt <bpratt@redhat.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
176
web/cypress/e2e/system-status-banner.cy.ts
Normal file
176
web/cypress/e2e/system-status-banner.cy.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
describe('System Status Banner', () => {
|
||||
beforeEach(() => {
|
||||
cy.request('GET', `${Cypress.env('REACT_QUAY_APP_API_URL')}/csrf_token`)
|
||||
.then((response) => response.body.csrf_token)
|
||||
.then((token) => {
|
||||
cy.loginByCSRF(token);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Read-only mode banner', () => {
|
||||
it('displays read-only banner when registry_state is readonly', () => {
|
||||
// Mock config with read-only mode enabled
|
||||
cy.intercept('GET', '/config', (req) =>
|
||||
req.reply((res) => {
|
||||
res.body.registry_state = 'readonly';
|
||||
return res;
|
||||
}),
|
||||
).as('getConfig');
|
||||
|
||||
cy.visit('/organization');
|
||||
cy.wait('@getConfig');
|
||||
|
||||
// Verify banner is visible
|
||||
cy.get('[data-testid="readonly-mode-banner"]').should('be.visible');
|
||||
|
||||
// Verify banner contains expected text
|
||||
cy.get('[data-testid="readonly-mode-banner"]').should(
|
||||
'contain',
|
||||
'is currently in read-only mode',
|
||||
);
|
||||
cy.get('[data-testid="readonly-mode-banner"]').should(
|
||||
'contain',
|
||||
'Pulls and other read-only operations will succeed',
|
||||
);
|
||||
cy.get('[data-testid="readonly-mode-banner"]').should(
|
||||
'contain',
|
||||
'all other operations are currently suspended',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not display read-only banner in normal mode', () => {
|
||||
cy.visit('/organization');
|
||||
|
||||
// Verify banner is not present
|
||||
cy.get('[data-testid="readonly-mode-banner"]').should('not.exist');
|
||||
});
|
||||
|
||||
it('displays registry name in the banner', () => {
|
||||
cy.intercept('GET', '/config', (req) =>
|
||||
req.reply((res) => {
|
||||
res.body.registry_state = 'readonly';
|
||||
return res;
|
||||
}),
|
||||
).as('getConfig');
|
||||
|
||||
cy.visit('/organization');
|
||||
cy.wait('@getConfig');
|
||||
|
||||
// Verify registry name is displayed (from config)
|
||||
cy.get('[data-testid="readonly-mode-banner"]')
|
||||
.invoke('text')
|
||||
.should('match', /\S+\s+is currently in read-only mode/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Account recovery mode banner', () => {
|
||||
it('displays account recovery banner when account_recovery_mode is true', () => {
|
||||
// Mock config with account recovery mode enabled
|
||||
cy.intercept('GET', '/config', (req) =>
|
||||
req.reply((res) => {
|
||||
res.body.account_recovery_mode = true;
|
||||
return res;
|
||||
}),
|
||||
).as('getConfig');
|
||||
|
||||
cy.visit('/organization');
|
||||
cy.wait('@getConfig');
|
||||
|
||||
// Verify banner is visible
|
||||
cy.get('[data-testid="account-recovery-mode-banner"]').should(
|
||||
'be.visible',
|
||||
);
|
||||
|
||||
// Verify banner contains expected text
|
||||
cy.get('[data-testid="account-recovery-mode-banner"]').should(
|
||||
'contain',
|
||||
'is currently in account recovery mode',
|
||||
);
|
||||
cy.get('[data-testid="account-recovery-mode-banner"]').should(
|
||||
'contain',
|
||||
'This instance should only be used to link accounts',
|
||||
);
|
||||
cy.get('[data-testid="account-recovery-mode-banner"]').should(
|
||||
'contain',
|
||||
'Registry operations such as pushes/pulls will not work',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not display account recovery banner in normal mode', () => {
|
||||
// Config defaults to account_recovery_mode: false, no intercept needed
|
||||
cy.visit('/organization');
|
||||
|
||||
// Verify banner is not present
|
||||
cy.get('[data-testid="account-recovery-mode-banner"]').should(
|
||||
'not.exist',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Banner positioning', () => {
|
||||
it('appears after the feedback banner', () => {
|
||||
cy.intercept('GET', '/config', (req) =>
|
||||
req.reply((res) => {
|
||||
res.body.registry_state = 'readonly';
|
||||
return res;
|
||||
}),
|
||||
).as('getConfig');
|
||||
|
||||
cy.visit('/organization');
|
||||
cy.wait('@getConfig');
|
||||
|
||||
// The read-only banner should exist after the feedback banner
|
||||
// We can verify this by checking the DOM order
|
||||
cy.get('[data-testid="readonly-mode-banner"]')
|
||||
.should('be.visible')
|
||||
.parent()
|
||||
.should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Both banners', () => {
|
||||
it('displays both banners when both modes are active', () => {
|
||||
// Create a config with both flags enabled
|
||||
cy.intercept('GET', '/config', (req) =>
|
||||
req.reply((res) => {
|
||||
res.body.registry_state = 'readonly';
|
||||
res.body.account_recovery_mode = true;
|
||||
return res;
|
||||
}),
|
||||
).as('getConfig');
|
||||
|
||||
cy.visit('/organization');
|
||||
cy.wait('@getConfig');
|
||||
|
||||
// Verify both banners are visible
|
||||
cy.get('[data-testid="readonly-mode-banner"]').should('be.visible');
|
||||
cy.get('[data-testid="account-recovery-mode-banner"]').should(
|
||||
'be.visible',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple page navigation', () => {
|
||||
it('displays banner across different pages', () => {
|
||||
cy.intercept('GET', '/config', (req) =>
|
||||
req.reply((res) => {
|
||||
res.body.registry_state = 'readonly';
|
||||
return res;
|
||||
}),
|
||||
).as('getConfig');
|
||||
|
||||
// Visit organization page
|
||||
cy.visit('/organization');
|
||||
cy.wait('@getConfig');
|
||||
cy.get('[data-testid="readonly-mode-banner"]').should('be.visible');
|
||||
|
||||
// Navigate to repositories page
|
||||
cy.visit('/repository');
|
||||
cy.get('[data-testid="readonly-mode-banner"]').should('be.visible');
|
||||
|
||||
// Navigate to overview page
|
||||
cy.visit('/overview');
|
||||
cy.get('[data-testid="readonly-mode-banner"]').should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import {ListVariant, LoginPage} from '@patternfly/react-core';
|
||||
import logo from 'src/assets/quay.svg';
|
||||
import {useQuayConfig} from 'src/hooks/UseQuayConfig';
|
||||
import {useLoginFooterItems} from 'src/components/LoginFooter';
|
||||
import SystemStatusBanner from 'src/components/SystemStatusBanner';
|
||||
import './LoginPageLayout.css';
|
||||
|
||||
interface LoginPageLayoutProps {
|
||||
@@ -35,17 +36,20 @@ export function LoginPageLayout({
|
||||
}, [quayConfig]);
|
||||
|
||||
return (
|
||||
<LoginPage
|
||||
className={className}
|
||||
brandImgSrc={logoUrl}
|
||||
brandImgAlt="Red Hat Quay"
|
||||
backgroundImgSrc="assets/images/rh_login.jpeg"
|
||||
textContent={description}
|
||||
loginTitle={title}
|
||||
footerListItems={footerListItems}
|
||||
footerListVariants={ListVariant.inline}
|
||||
>
|
||||
{children}
|
||||
</LoginPage>
|
||||
<>
|
||||
<SystemStatusBanner />
|
||||
<LoginPage
|
||||
className={className}
|
||||
brandImgSrc={logoUrl}
|
||||
brandImgAlt="Red Hat Quay"
|
||||
backgroundImgSrc="assets/images/rh_login.jpeg"
|
||||
textContent={description}
|
||||
loginTitle={title}
|
||||
footerListItems={footerListItems}
|
||||
footerListVariants={ListVariant.inline}
|
||||
>
|
||||
{children}
|
||||
</LoginPage>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
59
web/src/components/SystemStatusBanner.tsx
Normal file
59
web/src/components/SystemStatusBanner.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import {Banner, Flex, FlexItem} from '@patternfly/react-core';
|
||||
import {ExclamationTriangleIcon} from '@patternfly/react-icons';
|
||||
import {useQuayState} from 'src/hooks/UseQuayState';
|
||||
import {useQuayConfig} from 'src/hooks/UseQuayConfig';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const BannerContent = ({icon, children}) => (
|
||||
<Flex
|
||||
spaceItems={{default: 'spaceItemsSm'}}
|
||||
justifyContent={{default: 'justifyContentCenter'}}
|
||||
alignItems={{default: 'alignItemsCenter'}}
|
||||
>
|
||||
<FlexItem>{icon}</FlexItem>
|
||||
<FlexItem>{children}</FlexItem>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
BannerContent.propTypes = {
|
||||
icon: PropTypes.node.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default function SystemStatusBanner() {
|
||||
const {inReadOnlyMode, inAccountRecoveryMode} = useQuayState();
|
||||
const config = useQuayConfig();
|
||||
const registryName = config?.config?.REGISTRY_TITLE || 'Quay';
|
||||
|
||||
return (
|
||||
<>
|
||||
{inReadOnlyMode && (
|
||||
<Banner
|
||||
variant="default"
|
||||
data-testid="readonly-mode-banner"
|
||||
screenReaderText="Read-only mode warning"
|
||||
>
|
||||
<BannerContent icon={<ExclamationTriangleIcon />}>
|
||||
<strong>{registryName}</strong> is currently in read-only mode.
|
||||
Pulls and other read-only operations will succeed but all other
|
||||
operations are currently suspended.
|
||||
</BannerContent>
|
||||
</Banner>
|
||||
)}
|
||||
{inAccountRecoveryMode && (
|
||||
<Banner
|
||||
variant="gold"
|
||||
data-testid="account-recovery-mode-banner"
|
||||
screenReaderText="Account recovery mode warning"
|
||||
>
|
||||
<BannerContent icon={<ExclamationTriangleIcon />}>
|
||||
<strong>{registryName}</strong> is currently in account recovery
|
||||
mode. This instance should only be used to link accounts to an
|
||||
external login service, e.g., Red Hat. Registry operations such as
|
||||
pushes/pulls will not work.
|
||||
</BannerContent>
|
||||
</Banner>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import axiosIns from 'src/libs/axios';
|
||||
import ManageMembersList from './OrganizationsList/Organization/Tabs/TeamsAndMembership/TeamsView/ManageMembers/ManageMembersList';
|
||||
import OverviewList from './OverviewList/OverviewList';
|
||||
import {LoadingPage} from 'src/components/LoadingPage';
|
||||
import SystemStatusBanner from 'src/components/SystemStatusBanner';
|
||||
|
||||
const NavigationRoutes = [
|
||||
{
|
||||
@@ -138,6 +139,7 @@ function PluginMain() {
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
</Banner>
|
||||
<SystemStatusBanner />
|
||||
{user && (
|
||||
<CreateNewUser
|
||||
user={user}
|
||||
|
||||
@@ -35,6 +35,7 @@ import Conditional from 'src/components/empty/Conditional';
|
||||
import RegistryStatus from './RegistryStatus';
|
||||
import {NotificationDrawerListComponent} from 'src/components/notifications/NotificationDrawerList';
|
||||
import {OAuthError} from 'src/routes/OAuthCallback/OAuthError';
|
||||
import SystemStatusBanner from 'src/components/SystemStatusBanner';
|
||||
|
||||
const NavigationRoutes = [
|
||||
{
|
||||
@@ -135,6 +136,7 @@ export function StandaloneMain() {
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
</Banner>
|
||||
<SystemStatusBanner />
|
||||
<Conditional if={quayConfig?.features?.BILLING}>
|
||||
<ErrorBoundary fallback={<>Error loading registry status</>}>
|
||||
<RegistryStatus />
|
||||
|
||||
Reference in New Issue
Block a user