@@ -31,7 +31,7 @@ import './SystemAnnouncement.css'; | |||
interface LastSyncProps { | |||
short?: boolean; | |||
info: Required<GithubStatusEnabled>['lastSync']; | |||
info: GithubStatusEnabled['lastSync']; | |||
} | |||
interface GitHubSynchronisationWarningProps { | |||
@@ -39,6 +39,9 @@ interface GitHubSynchronisationWarningProps { | |||
} | |||
function LastSyncAlert({ info, short }: LastSyncProps) { | |||
if (info === undefined) { | |||
return null; | |||
} | |||
const { finishedAt, errorMessage, status, summary } = info; | |||
const formattedDate = finishedAt ? formatDistance(new Date(finishedAt), new Date()) : ''; | |||
@@ -73,25 +76,33 @@ function LastSyncAlert({ info, short }: LastSyncProps) { | |||
); | |||
} | |||
return status === TaskStatuses.Success ? ( | |||
<Alert variant="success"> | |||
{translateWithParameters( | |||
'settings.authentication.github.synchronization_successful', | |||
formattedDate | |||
return ( | |||
<Alert | |||
variant={status === TaskStatuses.Success ? 'success' : 'error'} | |||
role="alert" | |||
aria-live="assertive" | |||
> | |||
{status === TaskStatuses.Success ? ( | |||
<> | |||
{translateWithParameters( | |||
'settings.authentication.github.synchronization_successful', | |||
formattedDate | |||
)} | |||
<br /> | |||
{summary ?? ''} | |||
</> | |||
) : ( | |||
<React.Fragment key={`synch-alert-${finishedAt}`}> | |||
<div> | |||
{translateWithParameters( | |||
'settings.authentication.github.synchronization_failed', | |||
formattedDate | |||
)} | |||
</div> | |||
<br /> | |||
{errorMessage ?? ''} | |||
</React.Fragment> | |||
)} | |||
<br /> | |||
{summary ?? ''} | |||
</Alert> | |||
) : ( | |||
<Alert variant="error"> | |||
<div> | |||
{translateWithParameters( | |||
'settings.authentication.github.synchronization_failed', | |||
formattedDate | |||
)} | |||
</div> | |||
<br /> | |||
{errorMessage ?? ''} | |||
</Alert> | |||
); | |||
} | |||
@@ -105,19 +116,28 @@ function GitHubSynchronisationWarning({ short }: GitHubSynchronisationWarningPro | |||
return ( | |||
<> | |||
{!short && data?.nextSync && ( | |||
<> | |||
<Alert variant="loading" className="spacer-bottom"> | |||
{translate( | |||
data.nextSync.status === TaskStatuses.Pending | |||
? 'settings.authentication.github.synchronization_pending' | |||
: 'settings.authentication.github.synchronization_in_progress' | |||
)} | |||
</Alert> | |||
<br /> | |||
</> | |||
)} | |||
{data?.lastSync && <LastSyncAlert short={short} info={data.lastSync} />} | |||
<Alert | |||
variant="loading" | |||
className="spacer-bottom" | |||
aria-atomic={true} | |||
role="alert" | |||
aria-live="assertive" | |||
aria-label={ | |||
data.nextSync === undefined | |||
? translate('settings.authentication.github.synchronization_finish') | |||
: '' | |||
} | |||
> | |||
{!short && | |||
data?.nextSync && | |||
translate( | |||
data.nextSync.status === TaskStatuses.Pending | |||
? 'settings.authentication.github.synchronization_pending' | |||
: 'settings.authentication.github.synchronization_in_progress' | |||
)} | |||
</Alert> | |||
<LastSyncAlert short={short} info={data.lastSync} /> | |||
</> | |||
); | |||
} |
@@ -17,16 +17,17 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { isEmpty, keyBy, throttle } from 'lodash'; | |||
import classNames from 'classnames'; | |||
import { keyBy, throttle } from 'lodash'; | |||
import * as React from 'react'; | |||
import { getValues } from '../../api/settings'; | |||
import { Alert } from '../../components/ui/Alert'; | |||
import { Feature } from '../../types/features'; | |||
import { GlobalSettingKeys, SettingValue } from '../../types/settings'; | |||
import './SystemAnnouncement.css'; | |||
import withAvailableFeatures, { | |||
WithAvailableFeaturesProps, | |||
} from './available-features/withAvailableFeatures'; | |||
import './SystemAnnouncement.css'; | |||
const THROTTLE_TIME_MS = 10000; | |||
@@ -75,19 +76,18 @@ export class SystemAnnouncement extends React.PureComponent<WithAvailableFeature | |||
render() { | |||
const { displayMessage, message } = this.state; | |||
if (!displayMessage || isEmpty(message)) { | |||
return null; | |||
} | |||
return ( | |||
<div className="system-announcement-wrapper"> | |||
<div className={classNames({ 'system-announcement-wrapper': displayMessage && message })}> | |||
<Alert | |||
className="system-announcement-banner" | |||
title={message} | |||
display="banner" | |||
variant="warning" | |||
aria-live="assertive" | |||
role="alert" | |||
> | |||
{message} | |||
{displayMessage && message} | |||
</Alert> | |||
</div> | |||
); |
@@ -22,8 +22,8 @@ import * as React from 'react'; | |||
import { getValues } from '../../../api/settings'; | |||
import { renderComponent } from '../../../helpers/testReactTestingUtils'; | |||
import { Feature } from '../../../types/features'; | |||
import { AvailableFeaturesContext } from '../available-features/AvailableFeaturesContext'; | |||
import SystemAnnouncement from '../SystemAnnouncement'; | |||
import { AvailableFeaturesContext } from '../available-features/AvailableFeaturesContext'; | |||
jest.mock('../../../api/settings', () => ({ | |||
getValues: jest.fn(), |
@@ -97,10 +97,6 @@ export class IndexationNotification extends React.PureComponent<Props, State> { | |||
}, | |||
} = this.props; | |||
if (notificationType === undefined) { | |||
return null; | |||
} | |||
return ( | |||
<IndexationNotificationRenderer | |||
type={notificationType} |
@@ -19,6 +19,7 @@ | |||
*/ | |||
/* eslint-disable react/no-unused-prop-types */ | |||
import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import Link from '../../../components/common/Link'; | |||
@@ -29,7 +30,7 @@ import { IndexationNotificationType } from '../../../types/indexation'; | |||
import { TaskStatuses, TaskTypes } from '../../../types/tasks'; | |||
export interface IndexationNotificationRendererProps { | |||
type: IndexationNotificationType; | |||
type?: IndexationNotificationType; | |||
percentCompleted: number; | |||
isSystemAdmin: boolean; | |||
} | |||
@@ -45,20 +46,23 @@ export default function IndexationNotificationRenderer(props: IndexationNotifica | |||
const { type } = props; | |||
return ( | |||
<div className="indexation-notification-wrapper"> | |||
<div className={classNames({ 'indexation-notification-wrapper': type })}> | |||
<Alert | |||
className="indexation-notification-banner" | |||
display="banner" | |||
variant={NOTIFICATION_VARIANTS[type]} | |||
variant={type ? NOTIFICATION_VARIANTS[type] : 'success'} | |||
aria-live="assertive" | |||
> | |||
<div className="display-flex-center"> | |||
{type === IndexationNotificationType.Completed && renderCompletedBanner(props)} | |||
{type === IndexationNotificationType.CompletedWithFailure && | |||
renderCompletedWithFailureBanner(props)} | |||
{type === IndexationNotificationType.InProgress && renderInProgressBanner(props)} | |||
{type === IndexationNotificationType.InProgressWithFailure && | |||
renderInProgressWithFailureBanner(props)} | |||
</div> | |||
{type !== undefined && ( | |||
<div className="display-flex-center"> | |||
{type === IndexationNotificationType.Completed && renderCompletedBanner(props)} | |||
{type === IndexationNotificationType.CompletedWithFailure && | |||
renderCompletedWithFailureBanner(props)} | |||
{type === IndexationNotificationType.InProgress && renderInProgressBanner(props)} | |||
{type === IndexationNotificationType.InProgressWithFailure && | |||
renderInProgressWithFailureBanner(props)} | |||
</div> | |||
)} | |||
</Alert> | |||
</div> | |||
); |
@@ -5,6 +5,7 @@ exports[`should render correctly for type="Completed" & isSystemAdmin=false 1`] | |||
className="indexation-notification-wrapper" | |||
> | |||
<Alert | |||
aria-live="assertive" | |||
className="indexation-notification-banner" | |||
display="banner" | |||
variant="success" | |||
@@ -27,6 +28,7 @@ exports[`should render correctly for type="Completed" & isSystemAdmin=true 1`] = | |||
className="indexation-notification-wrapper" | |||
> | |||
<Alert | |||
aria-live="assertive" | |||
className="indexation-notification-banner" | |||
display="banner" | |||
variant="success" | |||
@@ -49,6 +51,7 @@ exports[`should render correctly for type="CompletedWithFailure" & isSystemAdmin | |||
className="indexation-notification-wrapper" | |||
> | |||
<Alert | |||
aria-live="assertive" | |||
className="indexation-notification-banner" | |||
display="banner" | |||
variant="error" | |||
@@ -79,6 +82,7 @@ exports[`should render correctly for type="CompletedWithFailure" & isSystemAdmin | |||
className="indexation-notification-wrapper" | |||
> | |||
<Alert | |||
aria-live="assertive" | |||
className="indexation-notification-banner" | |||
display="banner" | |||
variant="error" | |||
@@ -118,6 +122,7 @@ exports[`should render correctly for type="InProgress" & isSystemAdmin=false 1`] | |||
className="indexation-notification-wrapper" | |||
> | |||
<Alert | |||
aria-live="assertive" | |||
className="indexation-notification-banner" | |||
display="banner" | |||
variant="warning" | |||
@@ -148,6 +153,7 @@ exports[`should render correctly for type="InProgress" & isSystemAdmin=true 1`] | |||
className="indexation-notification-wrapper" | |||
> | |||
<Alert | |||
aria-live="assertive" | |||
className="indexation-notification-banner" | |||
display="banner" | |||
variant="warning" | |||
@@ -200,6 +206,7 @@ exports[`should render correctly for type="InProgressWithFailure" & isSystemAdmi | |||
className="indexation-notification-wrapper" | |||
> | |||
<Alert | |||
aria-live="assertive" | |||
className="indexation-notification-banner" | |||
display="banner" | |||
variant="error" | |||
@@ -238,6 +245,7 @@ exports[`should render correctly for type="InProgressWithFailure" & isSystemAdmi | |||
className="indexation-notification-wrapper" | |||
> | |||
<Alert | |||
aria-live="assertive" | |||
className="indexation-notification-banner" | |||
display="banner" | |||
variant="error" |
@@ -69,7 +69,7 @@ export function ProjectNotifications(props: WithNotificationsProps & Props) { | |||
{translate('project.info.notifications')} | |||
</h3> | |||
<Alert className="spacer-top" variant="info" aria-live="off"> | |||
<Alert className="spacer-top" variant="info"> | |||
{translate('notification.dispatcher.information')} | |||
</Alert> | |||
@@ -129,7 +129,7 @@ it('should show search filter when PAT is already set', async () => { | |||
// Should search with empty results | |||
almIntegrationHandler.setSearchAzureRepositories([]); | |||
await user.keyboard('f'); | |||
expect(screen.getByRole('alert')).toHaveTextContent('onboarding.create_project.azure.no_results'); | |||
expect(screen.getByText('onboarding.create_project.azure.no_results')).toBeInTheDocument(); | |||
}); | |||
function renderCreateProject(props: Partial<CreateProjectPageProps> = {}) { |
@@ -158,7 +158,7 @@ it('should show no result message when there are no projects', async () => { | |||
await selectEvent.select(ui.instanceSelector.get(), [/conf-bitbucketserver-2/]); | |||
}); | |||
expect(screen.getByRole('alert')).toHaveTextContent('onboarding.create_project.no_bbs_projects'); | |||
expect(screen.getByText('onboarding.create_project.no_bbs_projects')).toBeInTheDocument(); | |||
}); | |||
function renderCreateProject(props: Partial<CreateProjectPageProps> = {}) { |
@@ -189,9 +189,9 @@ it('should show no result message when there are no projects', async () => { | |||
await selectEvent.select(ui.instanceSelector.get(), [/conf-bitbucketcloud-2/]); | |||
}); | |||
expect(screen.getByRole('alert')).toHaveTextContent( | |||
'onboarding.create_project.bitbucketcloud.no_projects' | |||
); | |||
expect( | |||
screen.getByText('onboarding.create_project.bitbucketcloud.no_projects') | |||
).toBeInTheDocument(); | |||
}); | |||
it('should have load more', async () => { |
@@ -179,9 +179,7 @@ it('should show no result message when there are no projects', async () => { | |||
await selectEvent.select(ui.instanceSelector.get(), [/conf-final-2/]); | |||
}); | |||
expect(screen.getByRole('alert')).toHaveTextContent( | |||
'onboarding.create_project.gitlab.no_projects' | |||
); | |||
expect(screen.getByText('onboarding.create_project.gitlab.no_projects')).toBeInTheDocument(); | |||
}); | |||
it('should display a warning if the instance default new code definition is not CaYC compliant', async () => { | |||
@@ -197,9 +195,9 @@ it('should display a warning if the instance default new code definition is not | |||
expect(screen.getByText('Gitlab project 1')).toBeInTheDocument(); | |||
expect(screen.getByText('Gitlab project 2')).toBeInTheDocument(); | |||
expect(screen.getByRole('alert')).toHaveTextContent( | |||
'onboarding.create_project.new_code_option.warning.title' | |||
); | |||
expect( | |||
screen.getByText('onboarding.create_project.new_code_option.warning.title') | |||
).toBeInTheDocument(); | |||
}); | |||
function renderCreateProject(props: Partial<CreateProjectPageProps> = {}) { |
@@ -30,16 +30,14 @@ import { App } from '../App'; | |||
it('should render Empty Overview for Application with no analysis', async () => { | |||
renderApp({ component: mockComponent({ qualifier: ComponentQualifier.Application }) }); | |||
expect( | |||
await screen.findByRole('alert', { name: 'provisioning.no_analysis.application' }) | |||
).toBeInTheDocument(); | |||
expect(await screen.findByText('provisioning.no_analysis.application')).toBeInTheDocument(); | |||
}); | |||
it('should render Empty Overview on main branch with no analysis', async () => { | |||
renderApp({}, mockCurrentUser()); | |||
expect( | |||
await screen.findByRole('alert', { name: 'provisioning.no_analysis_on_main_branch.master' }) | |||
await screen.findByText('provisioning.no_analysis_on_main_branch.master') | |||
).toBeInTheDocument(); | |||
}); | |||
@@ -47,9 +45,9 @@ it('should render Empty Overview on main branch with multiple branches with bad | |||
renderApp({ branchLikes: [mockBranch(), mockBranch()] }); | |||
expect( | |||
await screen.findByRole('alert', { | |||
name: 'provisioning.no_analysis_on_main_branch.bad_configuration.master.branches.main_branch', | |||
}) | |||
await screen.findByText( | |||
'provisioning.no_analysis_on_main_branch.bad_configuration.master.branches.main_branch' | |||
) | |||
).toBeInTheDocument(); | |||
}); | |||
@@ -24,8 +24,8 @@ import { QualityGatesServiceMock } from '../../../api/mocks/QualityGatesServiceM | |||
import handleRequiredAuthorization from '../../../app/utils/handleRequiredAuthorization'; | |||
import { mockComponent } from '../../../helpers/mocks/component'; | |||
import { | |||
renderAppWithComponentContext, | |||
RenderContext, | |||
renderAppWithComponentContext, | |||
} from '../../../helpers/testReactTestingUtils'; | |||
import { Component } from '../../../types/types'; | |||
import routes from '../routes'; | |||
@@ -52,7 +52,7 @@ const ui = { | |||
saveButton: byRole('button', { name: 'save' }), | |||
statusMessage: byRole('status'), | |||
noConditionsNewCodeWarning: byText('project_quality_gate.no_condition_on_new_code'), | |||
alertMessage: byRole('alert'), | |||
alertMessage: byText('unknown'), | |||
}; | |||
beforeAll(() => { | |||
@@ -106,7 +106,7 @@ it('renders nothing and shows alert when any API fails', async () => { | |||
handler.setThrowOnGetGateForProject(true); | |||
renderProjectQualityGateApp(); | |||
expect(await ui.alertMessage.find()).toHaveTextContent('unknown'); | |||
expect(await ui.alertMessage.find()).toBeInTheDocument(); | |||
expect(ui.qualityGateHeading.query()).not.toBeInTheDocument(); | |||
}); | |||
@@ -139,7 +139,6 @@ it("should show a warning if there's an authorization error", async () => { | |||
const heading = await screen.findByRole('heading', { name: 'login.login_to_sonarqube' }); | |||
expect(heading).toBeInTheDocument(); | |||
expect(screen.getByRole('alert')).toBeInTheDocument(); | |||
expect(screen.getByText('login.unauthorized_access_alert')).toBeInTheDocument(); | |||
}); | |||
@@ -101,9 +101,7 @@ it('renders and behaves properly when the current value is not compliant', async | |||
expect(ui.daysInput.get()).toHaveValue('91'); | |||
// Should warn about non compliant value | |||
expect(screen.getByRole('alert')).toHaveTextContent( | |||
'baseline.number_days.compliance_warning.title' | |||
); | |||
expect(screen.getByText('baseline.number_days.compliance_warning.title')).toBeInTheDocument(); | |||
await user.clear(ui.daysInput.get()); | |||
await user.type(ui.daysInput.get(), '92'); |
@@ -19,7 +19,7 @@ | |||
*/ | |||
import userEvent from '@testing-library/user-event'; | |||
import React from 'react'; | |||
import { byRole } from 'testing-library-selector'; | |||
import { byRole, byText } from 'testing-library-selector'; | |||
import AlmSettingsServiceMock from '../../../../../api/mocks/AlmSettingsServiceMock'; | |||
import { renderComponent } from '../../../../../helpers/testReactTestingUtils'; | |||
import { AlmKeys } from '../../../../../types/alm-settings'; | |||
@@ -46,7 +46,7 @@ const ui = { | |||
byRole('textbox', { name: `settings.almintegration.form.${id}` }), | |||
saveConfigurationButton: byRole('button', { name: 'settings.almintegration.form.save' }), | |||
cancelButton: byRole('button', { name: 'cancel' }), | |||
validationError: byRole('alert'), | |||
validationError: (text: string) => byText(text), | |||
}; | |||
const onCancel = jest.fn(); | |||
@@ -61,7 +61,7 @@ it('enforceValidation enabled', async () => { | |||
await userEvent.type(ui.configurationInput('personal_access_token').get(), 'Access Token'); | |||
await userEvent.click(ui.saveConfigurationButton.get()); | |||
expect(ui.validationError.get()).toHaveTextContent('Validation Error'); | |||
expect(ui.validationError('Validation Error').get()).toBeInTheDocument(); | |||
await userEvent.click(ui.cancelButton.get()); | |||
expect(onCancel).toHaveBeenCalled(); |
@@ -176,8 +176,7 @@ function getPageObjects() { | |||
confirmDelete: byRole('button', { name: 'delete' }), | |||
checkConfigurationButton: (key: string) => | |||
byRole('button', { name: `settings.almintegration.check_configuration_x.${key}` }), | |||
validationErrorMessage: byRole('alert'), | |||
validationSuccessMessage: byRole('status'), | |||
validationMessage: (text: string) => byText(text), | |||
}; | |||
async function createConfiguration( | |||
@@ -221,15 +220,15 @@ function getPageObjects() { | |||
// Existing configuration is edited | |||
expect(screen.queryByRole('heading', { name: currentName })).not.toBeInTheDocument(); | |||
expect(screen.getByRole('heading', { name: newName })).toBeInTheDocument(); | |||
expect(ui.validationErrorMessage.get()).toHaveTextContent('Something is wrong'); | |||
expect(ui.validationMessage('Something is wrong').get()).toBeInTheDocument(); | |||
} | |||
async function checkConfiguration(name: string) { | |||
almSettings.setDefinitionErrorMessage(''); | |||
await userEvent.click(ui.checkConfigurationButton(name).get()); | |||
expect(ui.validationSuccessMessage.getAll()[0]).toHaveTextContent( | |||
'alert.tooltip.successsettings.almintegration.configuration_valid' | |||
); | |||
expect( | |||
ui.validationMessage('settings.almintegration.configuration_valid').getAll()[0] | |||
).toBeInTheDocument(); | |||
} | |||
async function deleteConfiguration(name: string) { |
@@ -103,7 +103,14 @@ function GitHubConfigurationValidity({ isAutoProvisioning }: Props) { | |||
return ( | |||
<> | |||
<Alert title={messages[0]} variant={alertVariant}> | |||
<Alert | |||
title={messages[0]} | |||
variant={alertVariant} | |||
aria-live="polite" | |||
role="status" | |||
aria-atomic={true} | |||
aria-busy={isFetching} | |||
> | |||
<div className="sw-flex sw-justify-between sw-items-center"> | |||
<div> | |||
{messages.map((msg) => ( |
@@ -170,7 +170,7 @@ const ui = { | |||
configurationValiditySuccess: byRole('status', { | |||
name: /github.configuration.validation.valid/, | |||
}), | |||
configurationValidityError: byRole('alert', { | |||
configurationValidityError: byRole('status', { | |||
name: /github.configuration.validation.invalid/, | |||
}), | |||
checkConfigButton: byRole('button', { |
@@ -20,7 +20,7 @@ | |||
import userEvent from '@testing-library/user-event'; | |||
import React from 'react'; | |||
import selectEvent from 'react-select-event'; | |||
import { byRole } from 'testing-library-selector'; | |||
import { byRole, byText } from 'testing-library-selector'; | |||
import AlmSettingsServiceMock from '../../../../../api/mocks/AlmSettingsServiceMock'; | |||
import CurrentUserContextProvider from '../../../../../app/components/current-user/CurrentUserContextProvider'; | |||
import { mockComponent } from '../../../../../helpers/mocks/component'; | |||
@@ -110,11 +110,11 @@ it.each([ | |||
} | |||
// Save form and check for errors | |||
await user.click(ui.saveButton.get()); | |||
expect(ui.validationErrorMsg.get()).toHaveTextContent('cute error'); | |||
expect(ui.validationMsg('cute error').get()).toBeInTheDocument(); | |||
// Check validation with errors | |||
await user.click(ui.validateButton.get()); | |||
expect(ui.validationErrorMsg.get()).toHaveTextContent('cute error'); | |||
expect(ui.validationMsg('cute error').get()).toBeInTheDocument(); | |||
// Save form and check for errors | |||
almSettings.setProjectBindingConfigurationErrors(undefined); | |||
@@ -123,22 +123,22 @@ it.each([ | |||
'Anything' | |||
); | |||
await user.click(ui.saveButton.get()); | |||
expect(await ui.validationSuccessMsg.find()).toHaveTextContent( | |||
'settings.pr_decoration.binding.check_configuration.success' | |||
); | |||
expect( | |||
await ui.validationMsg('settings.pr_decoration.binding.check_configuration.success').find() | |||
).toBeInTheDocument(); | |||
await user.click(ui.validateButton.get()); | |||
expect(ui.validationSuccessMsg.get()).toHaveTextContent( | |||
'settings.pr_decoration.binding.check_configuration.success' | |||
); | |||
expect( | |||
ui.validationMsg('settings.pr_decoration.binding.check_configuration.success').get() | |||
).toBeInTheDocument(); | |||
// Rerender and verify that validation is done for binding | |||
rerender( | |||
<MockedPRDecorationBinding component={mockComponent()} currentUser={mockCurrentUser()} /> | |||
); | |||
expect(await ui.validationSuccessMsg.find()).toHaveTextContent( | |||
'settings.pr_decoration.binding.check_configuration.success' | |||
); | |||
expect( | |||
await ui.validationMsg('settings.pr_decoration.binding.check_configuration.success').find() | |||
).toBeInTheDocument(); | |||
expect(ui.saveButton.query()).not.toBeInTheDocument(); | |||
// Reset binding | |||
@@ -172,8 +172,7 @@ function getPageObjects() { | |||
validateButton: byRole('button', { | |||
name: 'settings.pr_decoration.binding.check_configuration', | |||
}), | |||
validationErrorMsg: byRole('alert'), | |||
validationSuccessMsg: byRole('status'), | |||
validationMsg: (text: string) => byText(text), | |||
setInput, | |||
}; | |||
@@ -20,7 +20,7 @@ | |||
import { screen } from '@testing-library/react'; | |||
import userEvent from '@testing-library/user-event'; | |||
import { first } from 'lodash'; | |||
import { byRole } from 'testing-library-selector'; | |||
import { byRole, byText } from 'testing-library-selector'; | |||
import SystemServiceMock from '../../../../api/mocks/SystemServiceMock'; | |||
import { renderAppRoutes } from '../../../../helpers/testReactTestingUtils'; | |||
import routes from '../../routes'; | |||
@@ -59,12 +59,10 @@ describe('System Info Standalone', () => { | |||
await user.click(ui.changeLogLevelButton.get()); | |||
expect(ui.logLevelWarning.queryAll()).toHaveLength(0); | |||
await user.click(ui.logLevelsRadioButton(LogsLevels.DEBUG).get()); | |||
expect(ui.logLevelWarning.get()).toHaveTextContent( | |||
'alert.tooltip.warningsystem.log_level.warning' | |||
); | |||
expect(ui.logLevelWarning.get()).toBeInTheDocument(); | |||
await user.click(ui.saveButton.get()); | |||
expect(ui.logLevelWarning.queryAll()).toHaveLength(2); | |||
expect(ui.logLevelWarningShort.queryAll()).toHaveLength(2); | |||
}); | |||
it('can download logs & system info', async () => { | |||
@@ -100,7 +98,7 @@ describe('System Info Cluster', () => { | |||
); | |||
// Renders health checks | |||
expect(ui.healthCauseWarning.getAll()).toHaveLength(3); | |||
expect(ui.healthCauseWarning.get()).toBeInTheDocument(); | |||
// Renders App node | |||
expect(first(ui.sectionButton('server1.example.com').getAll())).toBeInTheDocument(); | |||
@@ -125,8 +123,9 @@ function getPageObjects() { | |||
sectionButton: (name: string) => byRole('button', { name }), | |||
changeLogLevelButton: byRole('button', { name: 'system.logs_level.change' }), | |||
logLevelsRadioButton: (name: LogsLevels) => byRole('radio', { name }), | |||
logLevelWarning: byRole('alert'), | |||
healthCauseWarning: byRole('alert'), | |||
logLevelWarning: byText('system.log_level.warning'), | |||
logLevelWarningShort: byText('system.log_level.warning.short'), | |||
healthCauseWarning: byText('Friendly warning'), | |||
saveButton: byRole('button', { name: 'save' }), | |||
}; | |||
@@ -24,7 +24,7 @@ import * as React from 'react'; | |||
import UserTokensMock from '../../../../api/mocks/UserTokensMock'; | |||
import { mockComponent } from '../../../../helpers/mocks/component'; | |||
import { mockLanguage, mockLoggedInUser } from '../../../../helpers/testMocks'; | |||
import { renderApp, RenderContext } from '../../../../helpers/testReactTestingUtils'; | |||
import { RenderContext, renderApp } from '../../../../helpers/testReactTestingUtils'; | |||
import { Permissions } from '../../../../types/permissions'; | |||
import { TokenType } from '../../../../types/token'; | |||
import { getCopyToClipboardValue } from '../../test-utils'; | |||
@@ -69,13 +69,13 @@ it('should render correctly and allow navigating between the different steps', a | |||
const modal = screen.getByRole('dialog'); | |||
await clickButton(user, 'onboarding.token.generate', modal); | |||
const lastToken = tokenMock.getLastToken(); | |||
if (lastToken === undefined) { | |||
throw new Error("Couldn't find the latest generated token."); | |||
} | |||
expect(lastToken.type).toBe(TokenType.Global); | |||
expect(within(modal).getByRole('alert')).toHaveTextContent( | |||
`users.tokens.new_token_created.${lastToken.token}` | |||
); | |||
expect(lastToken).toBeDefined(); | |||
expect(lastToken!.type).toBe(TokenType.Global); | |||
expect( | |||
within(modal).getByText(`users.tokens.new_token_created.${lastToken!.token}`) | |||
).toBeInTheDocument(); | |||
await clickButton(user, 'continue', modal); | |||
// Continue. |
@@ -81,9 +81,7 @@ it('should behave correctly', async () => { | |||
expect(lastToken.type).toBe(TokenType.Project); | |||
expect(lastToken.expirationDate).toBe(computeTokenExpirationDate(365)); | |||
expect(screen.getByRole('alert')).toHaveTextContent( | |||
`users.tokens.new_token_created.${lastToken.token}` | |||
); | |||
expect(screen.getByText(`users.tokens.new_token_created.${lastToken.token}`)).toBeInTheDocument(); | |||
expect(screen.getByRole('button', { name: 'copy_to_clipboard' })).toBeInTheDocument(); | |||
// Revoke token. | |||
@@ -104,9 +102,7 @@ it('should behave correctly', async () => { | |||
} | |||
expect(lastToken.type).toBe(TokenType.Project); | |||
expect(lastToken.expirationDate).toBe(computeTokenExpirationDate(365)); | |||
expect(screen.getByRole('alert')).toHaveTextContent( | |||
`users.tokens.new_token_created.${lastToken.token}` | |||
); | |||
expect(screen.getByText(`users.tokens.new_token_created.${lastToken.token}`)).toBeInTheDocument(); | |||
}); | |||
it('should allow setting a preferred token type', async () => { |
@@ -23,7 +23,6 @@ import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import { colors, sizes } from '../../app/theme'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { Dict } from '../../types/types'; | |||
import AlertErrorIcon from '../icons/AlertErrorIcon'; | |||
import AlertSuccessIcon from '../icons/AlertSuccessIcon'; | |||
import AlertWarnIcon from '../icons/AlertWarnIcon'; | |||
@@ -36,36 +35,12 @@ export type AlertVariant = 'error' | 'warning' | 'success' | 'info' | 'loading'; | |||
export interface AlertProps { | |||
display?: AlertDisplay; | |||
variant: AlertVariant; | |||
} | |||
interface AlertVariantInformation { | |||
icon: JSX.Element; | |||
color: string; | |||
borderColor: string; | |||
backGroundColor: string; | |||
role: string; | |||
live?: boolean; | |||
} | |||
const DOUBLE = 2; | |||
const QUADRUPLE = 4; | |||
const StyledAlertIcon = styled.div<{ isBanner: boolean; variantInfo: AlertVariantInformation }>` | |||
flex: 0 0 auto; | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
width: calc(${({ isBanner }) => (isBanner ? DOUBLE : QUADRUPLE)} * ${sizes.gridSize}); | |||
border-right: ${({ isBanner }) => (!isBanner ? '1px solid' : 'none')}; | |||
border-color: ${({ variantInfo }) => variantInfo.borderColor}; | |||
`; | |||
const StyledAlertContent = styled.div` | |||
flex: 1 1 auto; | |||
overflow: auto; | |||
text-align: left; | |||
padding: ${sizes.gridSize} calc(2 * ${sizes.gridSize}); | |||
`; | |||
const alertInnerIsBannerMixin = () => css` | |||
min-width: ${sizes.minPageWidth}; | |||
max-width: ${sizes.maxPageWidth}; | |||
@@ -76,19 +51,19 @@ const alertInnerIsBannerMixin = () => css` | |||
box-sizing: border-box; | |||
`; | |||
const StyledAlertInner = styled.div<{ isBanner: boolean }>` | |||
display: flex; | |||
align-items: stretch; | |||
${({ isBanner }) => (isBanner ? alertInnerIsBannerMixin : null)} | |||
`; | |||
const StyledAlert = styled.div<{ isInline: boolean; variantInfo: AlertVariantInformation }>` | |||
const StyledAlert = styled.div<{ | |||
isInline: boolean; | |||
color: string; | |||
backGroundColor: string; | |||
borderColor: string; | |||
isBanner: boolean; | |||
}>` | |||
border: 1px solid; | |||
border-radius: 2px; | |||
margin-bottom: ${sizes.gridSize}; | |||
border-color: ${({ variantInfo }) => variantInfo.borderColor}; | |||
background-color: ${({ variantInfo }) => variantInfo.backGroundColor}; | |||
color: ${({ variantInfo }) => variantInfo.color}; | |||
border-color: ${({ borderColor }) => borderColor}; | |||
background-color: ${({ backGroundColor }) => backGroundColor}; | |||
color: ${({ color }) => color}; | |||
display: ${({ isInline }) => (isInline ? 'inline-block' : 'block')}; | |||
:empty { | |||
@@ -104,10 +79,33 @@ const StyledAlert = styled.div<{ isInline: boolean; variantInfo: AlertVariantInf | |||
.button-link:hover { | |||
border-color: ${colors.darkBlue}; | |||
} | |||
& .alert-inner { | |||
display: flex; | |||
align-items: stretch; | |||
${({ isBanner }) => (isBanner ? alertInnerIsBannerMixin : null)} | |||
} | |||
& .alert-icon { | |||
flex: 0 0 auto; | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
width: calc(${({ isBanner }) => (isBanner ? DOUBLE : QUADRUPLE)} * ${sizes.gridSize}); | |||
border-right: ${({ isBanner }) => (!isBanner ? '1px solid' : 'none')}; | |||
border-color: ${({ borderColor }) => borderColor}; | |||
} | |||
& .alert-content { | |||
flex: 1 1 auto; | |||
overflow: auto; | |||
text-align: left; | |||
padding: ${sizes.gridSize} calc(2 * ${sizes.gridSize}); | |||
} | |||
`; | |||
function getAlertVariantInfo(variant: AlertVariant): AlertVariantInformation { | |||
const variantList: Dict<AlertVariantInformation> = { | |||
function getAlertVariantInfo(variant: AlertVariant) { | |||
const variantList = { | |||
error: { | |||
icon: ( | |||
<AlertErrorIcon label={translate('alert.tooltip.error')} fill={colors.alertIconError} /> | |||
@@ -115,7 +113,6 @@ function getAlertVariantInfo(variant: AlertVariant): AlertVariantInformation { | |||
color: colors.alertTextError, | |||
borderColor: colors.alertBorderError, | |||
backGroundColor: colors.alertBackgroundError, | |||
role: 'alert', | |||
}, | |||
warning: { | |||
icon: ( | |||
@@ -124,7 +121,6 @@ function getAlertVariantInfo(variant: AlertVariant): AlertVariantInformation { | |||
color: colors.alertTextWarning, | |||
borderColor: colors.alertBorderWarning, | |||
backGroundColor: colors.alertBackgroundWarning, | |||
role: 'alert', | |||
}, | |||
success: { | |||
icon: ( | |||
@@ -136,29 +132,26 @@ function getAlertVariantInfo(variant: AlertVariant): AlertVariantInformation { | |||
color: colors.alertTextSuccess, | |||
borderColor: colors.alertBorderSuccess, | |||
backGroundColor: colors.alertBackgroundSuccess, | |||
role: 'status', | |||
}, | |||
info: { | |||
icon: <InfoIcon label={translate('alert.tooltip.info')} fill={colors.alertIconInfo} />, | |||
color: colors.alertTextInfo, | |||
borderColor: colors.alertBorderInfo, | |||
backGroundColor: colors.alertBackgroundInfo, | |||
role: 'status', | |||
}, | |||
loading: { | |||
icon: <DeferredSpinner timeout={0} />, | |||
color: colors.alertTextInfo, | |||
borderColor: colors.alertBorderInfo, | |||
backGroundColor: colors.alertBackgroundInfo, | |||
role: 'status', | |||
}, | |||
}; | |||
} as const; | |||
return variantList[variant]; | |||
} | |||
export function Alert(props: AlertProps & React.HTMLAttributes<HTMLDivElement>) { | |||
const { className, display, variant, ...domProps } = props; | |||
const { className, display, variant, children, live, ...domProps } = props; | |||
const isInline = display === 'inline'; | |||
const isBanner = display === 'banner'; | |||
const variantInfo = getAlertVariantInfo(variant); | |||
@@ -166,17 +159,19 @@ export function Alert(props: AlertProps & React.HTMLAttributes<HTMLDivElement>) | |||
return ( | |||
<StyledAlert | |||
className={classNames('alert', className)} | |||
isBanner={isBanner} | |||
isInline={isInline} | |||
role={variantInfo.role} | |||
variantInfo={variantInfo} | |||
color={variantInfo.color} | |||
borderColor={variantInfo.borderColor} | |||
backGroundColor={variantInfo.backGroundColor} | |||
{...domProps} | |||
> | |||
<StyledAlertInner isBanner={isBanner}> | |||
<StyledAlertIcon isBanner={isBanner} variantInfo={variantInfo}> | |||
{variantInfo.icon} | |||
</StyledAlertIcon> | |||
<StyledAlertContent className="alert-content">{props.children}</StyledAlertContent> | |||
</StyledAlertInner> | |||
{children && ( | |||
<div className="alert-inner"> | |||
<div className="alert-icon">{variantInfo.icon}</div> | |||
<div className="alert-content">{children}</div> | |||
</div> | |||
)} | |||
</StyledAlert> | |||
); | |||
} |
@@ -25,7 +25,7 @@ exports[`should render banner alert with correct css 1`] = ` | |||
border-color: #236a97; | |||
} | |||
.emotion-1 { | |||
.emotion-0 .alert-inner { | |||
display: -webkit-box; | |||
display: -webkit-flex; | |||
display: -ms-flexbox; | |||
@@ -43,7 +43,7 @@ exports[`should render banner alert with correct css 1`] = ` | |||
box-sizing: border-box; | |||
} | |||
.emotion-2 { | |||
.emotion-0 .alert-icon { | |||
-webkit-flex: 0 0 auto; | |||
-ms-flex: 0 0 auto; | |||
flex: 0 0 auto; | |||
@@ -64,7 +64,7 @@ exports[`should render banner alert with correct css 1`] = ` | |||
border-color: #f4b1b0; | |||
} | |||
.emotion-3 { | |||
.emotion-0 .alert-content { | |||
-webkit-flex: 1 1 auto; | |||
-ms-flex: 1 1 auto; | |||
flex: 1 1 auto; | |||
@@ -75,14 +75,14 @@ exports[`should render banner alert with correct css 1`] = ` | |||
<div | |||
class="alert alert-test emotion-0" | |||
color="#862422" | |||
id="error-message" | |||
role="alert" | |||
> | |||
<div | |||
class="emotion-1" | |||
class="alert-inner" | |||
> | |||
<div | |||
class="emotion-2" | |||
class="alert-icon" | |||
> | |||
<svg | |||
height="16" | |||
@@ -103,7 +103,7 @@ exports[`should render banner alert with correct css 1`] = ` | |||
</svg> | |||
</div> | |||
<div | |||
class="alert-content emotion-3" | |||
class="alert-content" | |||
> | |||
This is an error! | |||
</div> | |||
@@ -113,115 +113,40 @@ exports[`should render banner alert with correct css 1`] = ` | |||
exports[`should render properly 1`] = ` | |||
<Styled(div) | |||
backGroundColor="#f2dede" | |||
borderColor="#f4b1b0" | |||
className="alert alert-test" | |||
color="#862422" | |||
id="error-message" | |||
isBanner={false} | |||
isInline={false} | |||
role="alert" | |||
variantInfo={ | |||
{ | |||
"backGroundColor": "#f2dede", | |||
"borderColor": "#f4b1b0", | |||
"color": "#862422", | |||
"icon": <AlertErrorIcon | |||
fill="#a4030f" | |||
label="alert.tooltip.error" | |||
/>, | |||
"role": "alert", | |||
} | |||
} | |||
> | |||
<Styled(div) | |||
isBanner={false} | |||
<div | |||
className="alert-inner" | |||
> | |||
<Styled(div) | |||
isBanner={false} | |||
variantInfo={ | |||
{ | |||
"backGroundColor": "#f2dede", | |||
"borderColor": "#f4b1b0", | |||
"color": "#862422", | |||
"icon": <AlertErrorIcon | |||
fill="#a4030f" | |||
label="alert.tooltip.error" | |||
/>, | |||
"role": "alert", | |||
} | |||
} | |||
<div | |||
className="alert-icon" | |||
> | |||
<AlertErrorIcon | |||
fill="#a4030f" | |||
label="alert.tooltip.error" | |||
/> | |||
</Styled(div)> | |||
<Styled(div) | |||
</div> | |||
<div | |||
className="alert-content" | |||
> | |||
This is an error! | |||
</Styled(div)> | |||
</Styled(div)> | |||
</div> | |||
</div> | |||
</Styled(div)> | |||
`; | |||
exports[`verification of all variants of alert 1`] = ` | |||
{ | |||
"backGroundColor": "#f2dede", | |||
"borderColor": "#f4b1b0", | |||
"color": "#862422", | |||
"icon": <AlertErrorIcon | |||
fill="#a4030f" | |||
label="alert.tooltip.error" | |||
/>, | |||
"role": "alert", | |||
} | |||
`; | |||
exports[`verification of all variants of alert 1`] = `undefined`; | |||
exports[`verification of all variants of alert 2`] = ` | |||
{ | |||
"backGroundColor": "#fcf8e3", | |||
"borderColor": "#faebcc", | |||
"color": "#6f4f17", | |||
"icon": <AlertWarnIcon | |||
fill="#db781a" | |||
label="alert.tooltip.warning" | |||
/>, | |||
"role": "alert", | |||
} | |||
`; | |||
exports[`verification of all variants of alert 2`] = `undefined`; | |||
exports[`verification of all variants of alert 3`] = ` | |||
{ | |||
"backGroundColor": "#dff0d8", | |||
"borderColor": "#d6e9c6", | |||
"color": "#215821", | |||
"icon": <AlertSuccessIcon | |||
fill="#6d9867" | |||
label="alert.tooltip.success" | |||
/>, | |||
"role": "status", | |||
} | |||
`; | |||
exports[`verification of all variants of alert 3`] = `undefined`; | |||
exports[`verification of all variants of alert 4`] = ` | |||
{ | |||
"backGroundColor": "#d9edf7", | |||
"borderColor": "#b1dff3", | |||
"color": "#0e516f", | |||
"icon": <InfoIcon | |||
fill="#0271b9" | |||
label="alert.tooltip.info" | |||
/>, | |||
"role": "status", | |||
} | |||
`; | |||
exports[`verification of all variants of alert 4`] = `undefined`; | |||
exports[`verification of all variants of alert 5`] = ` | |||
{ | |||
"backGroundColor": "#d9edf7", | |||
"borderColor": "#b1dff3", | |||
"color": "#0e516f", | |||
"icon": <DeferredSpinner | |||
timeout={0} | |||
/>, | |||
"role": "status", | |||
} | |||
`; | |||
exports[`verification of all variants of alert 5`] = `undefined`; |
@@ -1391,6 +1391,7 @@ settings.authentication.github.form.provisioning.disabled=Your current edition d | |||
settings.authentication.github.synchronize_now=Synchronize now | |||
settings.authentication.github.synchronization_in_progress=Synchronization is in progress. | |||
settings.authentication.github.synchronization_pending=Synchronization is scheduled. | |||
settings.authentication.github.synchronization_finish=Synchronization is done. | |||
settings.authentication.github.synchronization_successful=Last synchronization was done {0} ago. | |||
settings.authentication.github.synchronization_failed=Last synchronization failed {0} ago. | |||
settings.authentication.github.synchronization_failed_short=Last synchronization failed. {details} |