@@ -18,6 +18,7 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
/* eslint-disable react/no-unused-state */ | |||
import * as React from 'react'; | |||
import { withAppState } from '../../../components/hoc/withAppState'; | |||
import { IndexationContextInterface, IndexationStatus } from '../../../types/indexation'; | |||
@@ -34,19 +35,13 @@ export class IndexationContextProvider extends React.PureComponent< | |||
> { | |||
mounted = false; | |||
constructor(props: React.PropsWithChildren<Props>) { | |||
super(props); | |||
this.state = { | |||
status: { isCompleted: !props.appState.needIssueSync } | |||
}; | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
if (!this.state.status.isCompleted) { | |||
if (this.props.appState.needIssueSync) { | |||
IndexationNotificationHelper.startPolling(this.handleNewStatus); | |||
} else { | |||
this.setState({ status: { isCompleted: true, percentCompleted: 100, hasFailures: false } }); | |||
} | |||
} | |||
@@ -24,6 +24,7 @@ import withIndexationContext, { | |||
WithIndexationContextProps | |||
} from '../../../components/hoc/withIndexationContext'; | |||
import { hasGlobalPermission, isLoggedIn } from '../../../helpers/users'; | |||
import { IndexationNotificationType } from '../../../types/indexation'; | |||
import './IndexationNotification.css'; | |||
import IndexationNotificationHelper from './IndexationNotificationHelper'; | |||
import IndexationNotificationRenderer from './IndexationNotificationRenderer'; | |||
@@ -33,22 +34,16 @@ interface Props extends WithIndexationContextProps { | |||
} | |||
interface State { | |||
progression?: IndexationProgression; | |||
} | |||
export enum IndexationProgression { | |||
InProgress, | |||
Completed | |||
notificationType?: IndexationNotificationType; | |||
} | |||
export class IndexationNotification extends React.PureComponent<Props, State> { | |||
state: State; | |||
state: State = {}; | |||
isSystemAdmin = false; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { progression: undefined }; | |||
this.isSystemAdmin = | |||
isLoggedIn(this.props.currentUser) && hasGlobalPermission(this.props.currentUser, 'admin'); | |||
} | |||
@@ -57,42 +52,56 @@ export class IndexationNotification extends React.PureComponent<Props, State> { | |||
this.refreshNotification(); | |||
} | |||
componentDidUpdate() { | |||
this.refreshNotification(); | |||
componentDidUpdate(prevProps: Props) { | |||
if (prevProps.indexationContext.status !== this.props.indexationContext.status) { | |||
this.refreshNotification(); | |||
} | |||
} | |||
refreshNotification() { | |||
if (!this.props.indexationContext.status.isCompleted) { | |||
const { isCompleted, hasFailures } = this.props.indexationContext.status; | |||
if (!isCompleted) { | |||
IndexationNotificationHelper.markInProgressNotificationAsDisplayed(); | |||
this.setState({ progression: IndexationProgression.InProgress }); | |||
this.setState({ | |||
notificationType: hasFailures | |||
? IndexationNotificationType.InProgressWithFailure | |||
: IndexationNotificationType.InProgress | |||
}); | |||
} else if (hasFailures) { | |||
this.setState({ notificationType: IndexationNotificationType.CompletedWithFailure }); | |||
} else if (IndexationNotificationHelper.shouldDisplayCompletedNotification()) { | |||
this.setState({ progression: IndexationProgression.Completed }); | |||
this.setState({ | |||
notificationType: IndexationNotificationType.Completed | |||
}); | |||
} else { | |||
this.setState({ notificationType: undefined }); | |||
} | |||
} | |||
handleDismissCompletedNotification = () => { | |||
IndexationNotificationHelper.markCompletedNotificationAsDisplayed(); | |||
this.setState({ progression: undefined }); | |||
IndexationNotificationHelper.markCompletedNotificationAsDismissed(); | |||
this.refreshNotification(); | |||
}; | |||
render() { | |||
const { progression } = this.state; | |||
const { notificationType } = this.state; | |||
const { | |||
indexationContext: { | |||
status: { percentCompleted } | |||
} | |||
} = this.props; | |||
if (progression === undefined) { | |||
if (notificationType === undefined) { | |||
return null; | |||
} | |||
return ( | |||
<IndexationNotificationRenderer | |||
progression={progression} | |||
percentCompleted={percentCompleted ?? 0} | |||
type={notificationType} | |||
percentCompleted={percentCompleted} | |||
isSystemAdmin={this.isSystemAdmin} | |||
onDismissCompletedNotification={this.handleDismissCompletedNotification} | |||
displayBackgroundTaskLink={this.isSystemAdmin} | |||
/> | |||
); | |||
} |
@@ -31,10 +31,10 @@ export default class IndexationNotificationHelper { | |||
static startPolling(onNewStatus: (status: IndexationStatus) => void) { | |||
this.stopPolling(); | |||
this.interval = setInterval(async () => { | |||
const status = await getIndexationStatus(); | |||
onNewStatus(status); | |||
}, POLLING_INTERVAL_MS); | |||
// eslint-disable-next-line promise/catch-or-return | |||
this.poll(onNewStatus).finally(() => { | |||
this.interval = setInterval(() => this.poll(onNewStatus), POLLING_INTERVAL_MS); | |||
}); | |||
} | |||
static stopPolling() { | |||
@@ -43,11 +43,17 @@ export default class IndexationNotificationHelper { | |||
} | |||
} | |||
static async poll(onNewStatus: (status: IndexationStatus) => void) { | |||
const status = await getIndexationStatus(); | |||
onNewStatus(status); | |||
} | |||
static markInProgressNotificationAsDisplayed() { | |||
save(LS_INDEXATION_PROGRESS_WAS_DISPLAYED, true.toString()); | |||
} | |||
static markCompletedNotificationAsDisplayed() { | |||
static markCompletedNotificationAsDismissed() { | |||
remove(LS_INDEXATION_PROGRESS_WAS_DISPLAYED); | |||
} | |||
@@ -21,68 +21,143 @@ | |||
import * as React from 'react'; | |||
import { FormattedMessage } from 'react-intl'; | |||
import { Link } from 'react-router'; | |||
import { ButtonLink } from 'sonar-ui-common/components/controls/buttons'; | |||
import { Alert } from 'sonar-ui-common/components/ui/Alert'; | |||
import { ClearButton } from 'sonar-ui-common/components/controls/buttons'; | |||
import { Alert, AlertProps } from 'sonar-ui-common/components/ui/Alert'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import { BackgroundTaskTypes } from '../../../apps/background-tasks/constants'; | |||
import { IndexationProgression } from './IndexationNotification'; | |||
import { BackgroundTaskTypes, STATUSES } from '../../../apps/background-tasks/constants'; | |||
import { IndexationNotificationType } from '../../../types/indexation'; | |||
export interface IndexationNotificationRendererProps { | |||
progression: IndexationProgression; | |||
type: IndexationNotificationType; | |||
percentCompleted: number; | |||
isSystemAdmin: boolean; | |||
onDismissCompletedNotification: VoidFunction; | |||
displayBackgroundTaskLink?: boolean; | |||
} | |||
export default function IndexationNotificationRenderer(props: IndexationNotificationRendererProps) { | |||
const { progression, percentCompleted, displayBackgroundTaskLink } = props; | |||
const NOTIFICATION_VARIANTS: { [key in IndexationNotificationType]: AlertProps['variant'] } = { | |||
[IndexationNotificationType.InProgress]: 'warning', | |||
[IndexationNotificationType.InProgressWithFailure]: 'error', | |||
[IndexationNotificationType.Completed]: 'success', | |||
[IndexationNotificationType.CompletedWithFailure]: 'error' | |||
}; | |||
const inProgress = progression === IndexationProgression.InProgress; | |||
export default function IndexationNotificationRenderer(props: IndexationNotificationRendererProps) { | |||
const { type } = props; | |||
return ( | |||
<div className="indexation-notification-wrapper"> | |||
<Alert | |||
className="indexation-notification-banner" | |||
display="banner" | |||
variant={inProgress ? 'warning' : 'success'}> | |||
variant={NOTIFICATION_VARIANTS[type]}> | |||
<div className="display-flex-center"> | |||
{inProgress ? ( | |||
<> | |||
<span>{translate('indexation.in_progress')}</span> | |||
<i className="spinner spacer-left" /> | |||
<span className="spacer-left"> | |||
{translateWithParameters('indexation.in_progress.details', percentCompleted)} | |||
</span> | |||
{displayBackgroundTaskLink && ( | |||
<span className="spacer-left"> | |||
<FormattedMessage | |||
id="indexation.in_progress.admin_details" | |||
defaultMessage={translate('indexation.in_progress.admin_details')} | |||
values={{ | |||
link: ( | |||
<Link | |||
to={{ | |||
pathname: '/admin/background_tasks', | |||
query: { taskType: BackgroundTaskTypes.IssueSync } | |||
}}> | |||
{translate('background_tasks.page')} | |||
</Link> | |||
) | |||
}} | |||
/> | |||
</span> | |||
)} | |||
</> | |||
) : ( | |||
<> | |||
<span>{translate('indexation.completed')}</span> | |||
<ButtonLink className="spacer-left" onClick={props.onDismissCompletedNotification}> | |||
<strong>{translate('dismiss')}</strong> | |||
</ButtonLink> | |||
</> | |||
)} | |||
{type === IndexationNotificationType.Completed && renderCompletedBanner(props)} | |||
{type === IndexationNotificationType.CompletedWithFailure && | |||
renderCompletedWithFailureBanner(props)} | |||
{type === IndexationNotificationType.InProgress && renderInProgressBanner(props)} | |||
{type === IndexationNotificationType.InProgressWithFailure && | |||
renderInProgressWithFailureBanner(props)} | |||
</div> | |||
</Alert> | |||
</div> | |||
); | |||
} | |||
function renderCompletedBanner(props: IndexationNotificationRendererProps) { | |||
return ( | |||
<> | |||
<span className="spacer-right">{translate('indexation.completed')}</span> | |||
<ClearButton | |||
className="button-tiny" | |||
title={translate('dismiss')} | |||
onClick={props.onDismissCompletedNotification} | |||
/> | |||
</> | |||
); | |||
} | |||
function renderCompletedWithFailureBanner(props: IndexationNotificationRendererProps) { | |||
const { isSystemAdmin } = props; | |||
return ( | |||
<span className="spacer-right"> | |||
<FormattedMessage | |||
id="indexation.completed_with_error" | |||
defaultMessage={translate('indexation.completed_with_error')} | |||
values={{ | |||
link: isSystemAdmin | |||
? renderBackgroundTasksPageLink(true, translate('indexation.completed_with_error.link')) | |||
: translate('indexation.completed_with_error.link') | |||
}} | |||
/> | |||
</span> | |||
); | |||
} | |||
function renderInProgressBanner(props: IndexationNotificationRendererProps) { | |||
const { percentCompleted, isSystemAdmin } = props; | |||
return ( | |||
<> | |||
<span className="spacer-right">{translate('indexation.in_progress')}</span> | |||
<i className="spinner spacer-right" /> | |||
<span className="spacer-right"> | |||
{translateWithParameters('indexation.progression', percentCompleted)} | |||
</span> | |||
{isSystemAdmin && ( | |||
<span className="spacer-right"> | |||
<FormattedMessage | |||
id="indexation.admin_link" | |||
defaultMessage={translate('indexation.admin_link')} | |||
values={{ | |||
link: renderBackgroundTasksPageLink(false, translate('background_tasks.page')) | |||
}} | |||
/> | |||
</span> | |||
)} | |||
</> | |||
); | |||
} | |||
function renderInProgressWithFailureBanner(props: IndexationNotificationRendererProps) { | |||
const { percentCompleted, isSystemAdmin } = props; | |||
return ( | |||
<> | |||
<span className="spacer-right">{translate('indexation.in_progress')}</span> | |||
<i className="spinner spacer-right" /> | |||
<span className="spacer-right"> | |||
<FormattedMessage | |||
id="indexation.progression_with_error" | |||
defaultMessage={translateWithParameters( | |||
'indexation.progression_with_error', | |||
percentCompleted | |||
)} | |||
values={{ | |||
link: isSystemAdmin | |||
? renderBackgroundTasksPageLink( | |||
true, | |||
translate('indexation.progression_with_error.link') | |||
) | |||
: translate('indexation.progression_with_error.link') | |||
}} | |||
/> | |||
</span> | |||
</> | |||
); | |||
} | |||
function renderBackgroundTasksPageLink(hasError: boolean, text: string) { | |||
return ( | |||
<Link | |||
to={{ | |||
pathname: '/admin/background_tasks', | |||
query: { | |||
taskType: BackgroundTaskTypes.IssueSync, | |||
status: hasError ? STATUSES.FAILED : undefined | |||
} | |||
}}> | |||
{text} | |||
</Link> | |||
); | |||
} |
@@ -29,26 +29,24 @@ beforeEach(() => jest.clearAllMocks()); | |||
jest.mock('../IndexationNotificationHelper'); | |||
it('should render correctly & start polling', () => { | |||
it('should render correctly and start polling if issue sync is needed', () => { | |||
const wrapper = mountRender(); | |||
expect(wrapper.state().status).toEqual({ isCompleted: false }); | |||
const child = wrapper.find(TestComponent); | |||
expect(child.exists()).toBe(true); | |||
expect(child.instance().context).toEqual(wrapper.state()); | |||
}); | |||
it('should start polling if needed', () => { | |||
mountRender(); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(IndexationNotificationHelper.startPolling).toHaveBeenCalled(); | |||
}); | |||
it('should not start polling if not needed', () => { | |||
mountRender({ appState: { needIssueSync: false } }); | |||
it('should not start polling if no issue sync is needed', () => { | |||
const wrapper = mountRender({ appState: { needIssueSync: false } }); | |||
expect(IndexationNotificationHelper.startPolling).not.toHaveBeenCalled(); | |||
const expectedStatus: IndexationStatus = { | |||
isCompleted: true, | |||
percentCompleted: 100, | |||
hasFailures: false | |||
}; | |||
expect(wrapper.state().status).toEqual(expectedStatus); | |||
}); | |||
it('should update the state on new status & stop polling if indexation is complete', () => { | |||
@@ -56,7 +54,11 @@ it('should update the state on new status & stop polling if indexation is comple | |||
const triggerNewStatus = (IndexationNotificationHelper.startPolling as jest.Mock).mock | |||
.calls[0][0] as (status: IndexationStatus) => void; | |||
const newStatus = { isCompleted: true, percentCompleted: 100 }; | |||
const newStatus: IndexationStatus = { | |||
isCompleted: true, | |||
percentCompleted: 100, | |||
hasFailures: false | |||
}; | |||
triggerNewStatus(newStatus); | |||
@@ -21,7 +21,8 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { mockCurrentUser } from '../../../../helpers/testMocks'; | |||
import { IndexationNotification, IndexationProgression } from '../IndexationNotification'; | |||
import { IndexationNotificationType } from '../../../../types/indexation'; | |||
import { IndexationNotification } from '../IndexationNotification'; | |||
import IndexationNotificationHelper from '../IndexationNotificationHelper'; | |||
import IndexationNotificationRenderer from '../IndexationNotificationRenderer'; | |||
@@ -29,63 +30,95 @@ beforeEach(() => jest.clearAllMocks()); | |||
jest.mock('../IndexationNotificationHelper'); | |||
it('should display the warning banner if indexation is in progress', () => { | |||
const wrapper = shallowRender(); | |||
describe('Completed banner', () => { | |||
it('should be displayed', () => { | |||
(IndexationNotificationHelper.shouldDisplayCompletedNotification as jest.Mock).mockReturnValueOnce( | |||
true | |||
); | |||
expect(IndexationNotificationHelper.markInProgressNotificationAsDisplayed).toHaveBeenCalled(); | |||
expect(wrapper.state().progression).toBe(IndexationProgression.InProgress); | |||
}); | |||
const wrapper = shallowRender(); | |||
it('should display the success banner when indexation is complete', () => { | |||
(IndexationNotificationHelper.shouldDisplayCompletedNotification as jest.Mock).mockReturnValueOnce( | |||
true | |||
); | |||
wrapper.setProps({ | |||
indexationContext: { | |||
status: { isCompleted: true, percentCompleted: 100, hasFailures: false } | |||
} | |||
}); | |||
const wrapper = shallowRender(); | |||
expect(IndexationNotificationHelper.shouldDisplayCompletedNotification).toHaveBeenCalled(); | |||
expect(wrapper.state().notificationType).toBe(IndexationNotificationType.Completed); | |||
}); | |||
wrapper.setProps({ indexationContext: { status: { isCompleted: true } } }); | |||
it('should be displayed at startup', () => { | |||
(IndexationNotificationHelper.shouldDisplayCompletedNotification as jest.Mock).mockReturnValueOnce( | |||
true | |||
); | |||
expect(IndexationNotificationHelper.shouldDisplayCompletedNotification).toHaveBeenCalled(); | |||
expect(wrapper.state().progression).toBe(IndexationProgression.Completed); | |||
}); | |||
const wrapper = shallowRender({ | |||
indexationContext: { | |||
status: { isCompleted: true, percentCompleted: 100, hasFailures: false } | |||
} | |||
}); | |||
it('should render correctly completed notification at startup', () => { | |||
(IndexationNotificationHelper.shouldDisplayCompletedNotification as jest.Mock).mockReturnValueOnce( | |||
true | |||
); | |||
expect(IndexationNotificationHelper.shouldDisplayCompletedNotification).toHaveBeenCalled(); | |||
expect(wrapper.state().notificationType).toBe(IndexationNotificationType.Completed); | |||
}); | |||
it('should be hidden on dismiss action', () => { | |||
(IndexationNotificationHelper.shouldDisplayCompletedNotification as jest.Mock).mockReturnValueOnce( | |||
true | |||
); | |||
const wrapper = shallowRender({ | |||
indexationContext: { | |||
status: { isCompleted: true, percentCompleted: 100, hasFailures: false } | |||
} | |||
}); | |||
expect(wrapper.state().notificationType).toBe(IndexationNotificationType.Completed); | |||
wrapper | |||
.find(IndexationNotificationRenderer) | |||
.props() | |||
.onDismissCompletedNotification(); | |||
expect(IndexationNotificationHelper.markCompletedNotificationAsDismissed).toHaveBeenCalled(); | |||
expect(wrapper.state().notificationType).toBeUndefined(); | |||
}); | |||
}); | |||
it('should display the completed-with-failure banner', () => { | |||
const wrapper = shallowRender({ | |||
indexationContext: { status: { isCompleted: true } } | |||
indexationContext: { status: { isCompleted: true, percentCompleted: 100, hasFailures: true } } | |||
}); | |||
expect(IndexationNotificationHelper.markInProgressNotificationAsDisplayed).not.toHaveBeenCalled(); | |||
expect(IndexationNotificationHelper.shouldDisplayCompletedNotification).toHaveBeenCalled(); | |||
expect(wrapper.state().progression).toBe(IndexationProgression.Completed); | |||
expect(wrapper.state().notificationType).toBe(IndexationNotificationType.CompletedWithFailure); | |||
}); | |||
it('should hide the success banner on dismiss action', () => { | |||
(IndexationNotificationHelper.shouldDisplayCompletedNotification as jest.Mock).mockReturnValueOnce( | |||
true | |||
); | |||
it('should display the progress banner', () => { | |||
const wrapper = shallowRender({ | |||
indexationContext: { status: { isCompleted: true } } | |||
indexationContext: { status: { isCompleted: false, percentCompleted: 23, hasFailures: false } } | |||
}); | |||
wrapper | |||
.find(IndexationNotificationRenderer) | |||
.props() | |||
.onDismissCompletedNotification(); | |||
expect(IndexationNotificationHelper.markInProgressNotificationAsDisplayed).toHaveBeenCalled(); | |||
expect(wrapper.state().notificationType).toBe(IndexationNotificationType.InProgress); | |||
}); | |||
expect(IndexationNotificationHelper.markCompletedNotificationAsDisplayed).toHaveBeenCalled(); | |||
expect(wrapper.state().progression).toBeUndefined(); | |||
it('should display the progress-with-failure banner', () => { | |||
const wrapper = shallowRender({ | |||
indexationContext: { status: { isCompleted: false, percentCompleted: 23, hasFailures: true } } | |||
}); | |||
expect(IndexationNotificationHelper.markInProgressNotificationAsDisplayed).toHaveBeenCalled(); | |||
expect(wrapper.state().notificationType).toBe(IndexationNotificationType.InProgressWithFailure); | |||
}); | |||
function shallowRender(props?: Partial<IndexationNotification['props']>) { | |||
return shallow<IndexationNotification>( | |||
<IndexationNotification | |||
currentUser={mockCurrentUser()} | |||
indexationContext={{ status: { isCompleted: false } }} | |||
indexationContext={{ | |||
status: { isCompleted: false, percentCompleted: 23, hasFailures: false } | |||
}} | |||
{...props} | |||
/> | |||
); |
@@ -40,17 +40,22 @@ jest.mock('sonar-ui-common/helpers/storage', () => ({ | |||
it('should properly start & stop polling for indexation status', async () => { | |||
const onNewStatus = jest.fn(); | |||
const newStatus: IndexationStatus = { isCompleted: true, percentCompleted: 87 }; | |||
const newStatus: IndexationStatus = { | |||
isCompleted: true, | |||
percentCompleted: 100, | |||
hasFailures: false | |||
}; | |||
(getIndexationStatus as jest.Mock).mockResolvedValueOnce(newStatus); | |||
IndexationNotificationHelper.startPolling(onNewStatus); | |||
jest.runOnlyPendingTimers(); | |||
expect(getIndexationStatus).toHaveBeenCalled(); | |||
await new Promise(setImmediate); | |||
expect(onNewStatus).toHaveBeenCalledWith(newStatus); | |||
jest.runOnlyPendingTimers(); | |||
expect(getIndexationStatus).toHaveBeenCalledTimes(2); | |||
(getIndexationStatus as jest.Mock).mockClear(); | |||
IndexationNotificationHelper.stopPolling(); | |||
@@ -70,7 +75,7 @@ it('should properly handle the flag to show the completed banner', () => { | |||
expect(shouldDisplay).toBe(true); | |||
expect(get).toHaveBeenCalled(); | |||
IndexationNotificationHelper.markCompletedNotificationAsDisplayed(); | |||
IndexationNotificationHelper.markCompletedNotificationAsDismissed(); | |||
expect(remove).toHaveBeenCalled(); | |||
@@ -20,37 +20,46 @@ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { ButtonLink } from 'sonar-ui-common/components/controls/buttons'; | |||
import { ClearButton } from 'sonar-ui-common/components/controls/buttons'; | |||
import { click } from 'sonar-ui-common/helpers/testUtils'; | |||
import { IndexationProgression } from '../IndexationNotification'; | |||
import { IndexationNotificationType } from '../../../../types/indexation'; | |||
import IndexationNotificationRenderer, { | |||
IndexationNotificationRendererProps | |||
} from '../IndexationNotificationRenderer'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('in-progress'); | |||
expect(shallowRender({ displayBackgroundTaskLink: true })).toMatchSnapshot('in-progress-admin'); | |||
expect(shallowRender({ progression: IndexationProgression.Completed })).toMatchSnapshot( | |||
'completed' | |||
); | |||
}); | |||
it.each([ | |||
[IndexationNotificationType.InProgress, false], | |||
[IndexationNotificationType.InProgress, true], | |||
[IndexationNotificationType.InProgressWithFailure, false], | |||
[IndexationNotificationType.InProgressWithFailure, true], | |||
[IndexationNotificationType.Completed, false], | |||
[IndexationNotificationType.Completed, true], | |||
[IndexationNotificationType.CompletedWithFailure, false], | |||
[IndexationNotificationType.CompletedWithFailure, true] | |||
])( | |||
'should render correctly for type=%p & isSystemAdmin=%p', | |||
(type: IndexationNotificationType, isSystemAdmin: boolean) => { | |||
expect(shallowRender({ type, isSystemAdmin })).toMatchSnapshot(); | |||
} | |||
); | |||
it('should propagate the dismiss event', () => { | |||
it('should propagate the dismiss event from completed notification', () => { | |||
const onDismissCompletedNotification = jest.fn(); | |||
const wrapper = shallowRender({ | |||
progression: IndexationProgression.Completed, | |||
type: IndexationNotificationType.Completed, | |||
onDismissCompletedNotification | |||
}); | |||
click(wrapper.find(ButtonLink)); | |||
click(wrapper.find(ClearButton)); | |||
expect(onDismissCompletedNotification).toHaveBeenCalled(); | |||
}); | |||
function shallowRender(props: Partial<IndexationNotificationRendererProps> = {}) { | |||
return shallow<IndexationNotificationRendererProps>( | |||
<IndexationNotificationRenderer | |||
progression={IndexationProgression.InProgress} | |||
type={IndexationNotificationType.InProgress} | |||
percentCompleted={25} | |||
isSystemAdmin={false} | |||
onDismissCompletedNotification={jest.fn()} | |||
{...props} | |||
/> |
@@ -47,7 +47,7 @@ function shallowRender(props?: PageUnavailableDueToIndexation['props']) { | |||
return shallow( | |||
<PageUnavailableDueToIndexation | |||
indexationContext={{ | |||
status: { isCompleted: false } | |||
status: { isCompleted: false, percentCompleted: 23, hasFailures: false } | |||
}} | |||
pageContext={PageContext.Issues} | |||
component={{ qualifier: ComponentQualifier.Portfolio, name: 'test-portfolio' }} |
@@ -0,0 +1,17 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly and start polling if issue sync is needed 1`] = ` | |||
<IndexationContextProvider | |||
appState={ | |||
Object { | |||
"needIssueSync": true, | |||
} | |||
} | |||
> | |||
<TestComponent> | |||
<h1> | |||
TestComponent | |||
</h1> | |||
</TestComponent> | |||
</IndexationContextProvider> | |||
`; |
@@ -1,6 +1,6 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: completed 1`] = ` | |||
exports[`should render correctly for type="Completed" & isSystemAdmin=false 1`] = ` | |||
<div | |||
className="indexation-notification-wrapper" | |||
> | |||
@@ -12,23 +12,123 @@ exports[`should render correctly: completed 1`] = ` | |||
<div | |||
className="display-flex-center" | |||
> | |||
<span> | |||
<span | |||
className="spacer-right" | |||
> | |||
indexation.completed | |||
</span> | |||
<ButtonLink | |||
className="spacer-left" | |||
<ClearButton | |||
className="button-tiny" | |||
onClick={[MockFunction]} | |||
title="dismiss" | |||
/> | |||
</div> | |||
</Alert> | |||
</div> | |||
`; | |||
exports[`should render correctly for type="Completed" & isSystemAdmin=true 1`] = ` | |||
<div | |||
className="indexation-notification-wrapper" | |||
> | |||
<Alert | |||
className="indexation-notification-banner" | |||
display="banner" | |||
variant="success" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<span | |||
className="spacer-right" | |||
> | |||
indexation.completed | |||
</span> | |||
<ClearButton | |||
className="button-tiny" | |||
onClick={[MockFunction]} | |||
title="dismiss" | |||
/> | |||
</div> | |||
</Alert> | |||
</div> | |||
`; | |||
exports[`should render correctly for type="CompletedWithFailure" & isSystemAdmin=false 1`] = ` | |||
<div | |||
className="indexation-notification-wrapper" | |||
> | |||
<Alert | |||
className="indexation-notification-banner" | |||
display="banner" | |||
variant="error" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<span | |||
className="spacer-right" | |||
> | |||
<strong> | |||
dismiss | |||
</strong> | |||
</ButtonLink> | |||
<FormattedMessage | |||
defaultMessage="indexation.completed_with_error" | |||
id="indexation.completed_with_error" | |||
values={ | |||
Object { | |||
"link": "indexation.completed_with_error.link", | |||
} | |||
} | |||
/> | |||
</span> | |||
</div> | |||
</Alert> | |||
</div> | |||
`; | |||
exports[`should render correctly for type="CompletedWithFailure" & isSystemAdmin=true 1`] = ` | |||
<div | |||
className="indexation-notification-wrapper" | |||
> | |||
<Alert | |||
className="indexation-notification-banner" | |||
display="banner" | |||
variant="error" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<span | |||
className="spacer-right" | |||
> | |||
<FormattedMessage | |||
defaultMessage="indexation.completed_with_error" | |||
id="indexation.completed_with_error" | |||
values={ | |||
Object { | |||
"link": <Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/admin/background_tasks", | |||
"query": Object { | |||
"status": "FAILED", | |||
"taskType": "ISSUE_SYNC", | |||
}, | |||
} | |||
} | |||
> | |||
indexation.completed_with_error.link | |||
</Link>, | |||
} | |||
} | |||
/> | |||
</span> | |||
</div> | |||
</Alert> | |||
</div> | |||
`; | |||
exports[`should render correctly: in-progress 1`] = ` | |||
exports[`should render correctly for type="InProgress" & isSystemAdmin=false 1`] = ` | |||
<div | |||
className="indexation-notification-wrapper" | |||
> | |||
@@ -40,23 +140,25 @@ exports[`should render correctly: in-progress 1`] = ` | |||
<div | |||
className="display-flex-center" | |||
> | |||
<span> | |||
<span | |||
className="spacer-right" | |||
> | |||
indexation.in_progress | |||
</span> | |||
<i | |||
className="spinner spacer-left" | |||
className="spinner spacer-right" | |||
/> | |||
<span | |||
className="spacer-left" | |||
className="spacer-right" | |||
> | |||
indexation.in_progress.details.25 | |||
indexation.progression.25 | |||
</span> | |||
</div> | |||
</Alert> | |||
</div> | |||
`; | |||
exports[`should render correctly: in-progress-admin 1`] = ` | |||
exports[`should render correctly for type="InProgress" & isSystemAdmin=true 1`] = ` | |||
<div | |||
className="indexation-notification-wrapper" | |||
> | |||
@@ -68,23 +170,25 @@ exports[`should render correctly: in-progress-admin 1`] = ` | |||
<div | |||
className="display-flex-center" | |||
> | |||
<span> | |||
<span | |||
className="spacer-right" | |||
> | |||
indexation.in_progress | |||
</span> | |||
<i | |||
className="spinner spacer-left" | |||
className="spinner spacer-right" | |||
/> | |||
<span | |||
className="spacer-left" | |||
className="spacer-right" | |||
> | |||
indexation.in_progress.details.25 | |||
indexation.progression.25 | |||
</span> | |||
<span | |||
className="spacer-left" | |||
className="spacer-right" | |||
> | |||
<FormattedMessage | |||
defaultMessage="indexation.in_progress.admin_details" | |||
id="indexation.in_progress.admin_details" | |||
defaultMessage="indexation.admin_link" | |||
id="indexation.admin_link" | |||
values={ | |||
Object { | |||
"link": <Link | |||
@@ -94,6 +198,7 @@ exports[`should render correctly: in-progress-admin 1`] = ` | |||
Object { | |||
"pathname": "/admin/background_tasks", | |||
"query": Object { | |||
"status": undefined, | |||
"taskType": "ISSUE_SYNC", | |||
}, | |||
} | |||
@@ -109,3 +214,93 @@ exports[`should render correctly: in-progress-admin 1`] = ` | |||
</Alert> | |||
</div> | |||
`; | |||
exports[`should render correctly for type="InProgressWithFailure" & isSystemAdmin=false 1`] = ` | |||
<div | |||
className="indexation-notification-wrapper" | |||
> | |||
<Alert | |||
className="indexation-notification-banner" | |||
display="banner" | |||
variant="error" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<span | |||
className="spacer-right" | |||
> | |||
indexation.in_progress | |||
</span> | |||
<i | |||
className="spinner spacer-right" | |||
/> | |||
<span | |||
className="spacer-right" | |||
> | |||
<FormattedMessage | |||
defaultMessage="indexation.progression_with_error.25" | |||
id="indexation.progression_with_error" | |||
values={ | |||
Object { | |||
"link": "indexation.progression_with_error.link", | |||
} | |||
} | |||
/> | |||
</span> | |||
</div> | |||
</Alert> | |||
</div> | |||
`; | |||
exports[`should render correctly for type="InProgressWithFailure" & isSystemAdmin=true 1`] = ` | |||
<div | |||
className="indexation-notification-wrapper" | |||
> | |||
<Alert | |||
className="indexation-notification-banner" | |||
display="banner" | |||
variant="error" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<span | |||
className="spacer-right" | |||
> | |||
indexation.in_progress | |||
</span> | |||
<i | |||
className="spinner spacer-right" | |||
/> | |||
<span | |||
className="spacer-right" | |||
> | |||
<FormattedMessage | |||
defaultMessage="indexation.progression_with_error.25" | |||
id="indexation.progression_with_error" | |||
values={ | |||
Object { | |||
"link": <Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/admin/background_tasks", | |||
"query": Object { | |||
"status": "FAILED", | |||
"taskType": "ISSUE_SYNC", | |||
}, | |||
} | |||
} | |||
> | |||
indexation.progression_with_error.link | |||
</Link>, | |||
} | |||
} | |||
/> | |||
</span> | |||
</div> | |||
</Alert> | |||
</div> | |||
`; |
@@ -26,7 +26,7 @@ import withIndexationContext, { WithIndexationContextProps } from '../withIndexa | |||
it('should render correctly', () => { | |||
const indexationContext: IndexationContextInterface = { | |||
status: { isCompleted: true, percentCompleted: 87 } | |||
status: { isCompleted: true, percentCompleted: 87, hasFailures: false } | |||
}; | |||
const wrapper = mountRender(indexationContext); | |||
@@ -36,7 +36,11 @@ it('should render correctly', () => { | |||
function mountRender(indexationContext?: Partial<IndexationContextInterface>) { | |||
return mount( | |||
<IndexationContext.Provider value={{ status: { isCompleted: false }, ...indexationContext }}> | |||
<IndexationContext.Provider | |||
value={{ | |||
status: { isCompleted: false, percentCompleted: 23, hasFailures: false }, | |||
...indexationContext | |||
}}> | |||
<TestComponentWithIndexationContext /> | |||
</IndexationContext.Provider> | |||
); |
@@ -29,13 +29,19 @@ it('should render correctly', () => { | |||
let wrapper = mountRender(); | |||
expect(wrapper.find(TestComponent).exists()).toBe(false); | |||
wrapper = mountRender({ status: { isCompleted: true } }); | |||
wrapper = mountRender({ | |||
status: { isCompleted: true, percentCompleted: 100, hasFailures: false } | |||
}); | |||
expect(wrapper.find(TestComponent).exists()).toBe(true); | |||
}); | |||
function mountRender(context?: Partial<IndexationContextInterface>) { | |||
return mount( | |||
<IndexationContext.Provider value={{ status: { isCompleted: false }, ...context }}> | |||
<IndexationContext.Provider | |||
value={{ | |||
status: { isCompleted: false, percentCompleted: 23, hasFailures: false }, | |||
...context | |||
}}> | |||
<TestComponentWithGuard /> | |||
</IndexationContext.Provider> | |||
); |
@@ -20,9 +20,17 @@ | |||
export interface IndexationStatus { | |||
isCompleted: boolean; | |||
percentCompleted?: number; | |||
percentCompleted: number; | |||
hasFailures: boolean; | |||
} | |||
export interface IndexationContextInterface { | |||
status: IndexationStatus; | |||
} | |||
export enum IndexationNotificationType { | |||
InProgress = 'InProgress', | |||
InProgressWithFailure = 'InProgressWithFailure', | |||
Completed = 'Completed', | |||
CompletedWithFailure = 'CompletedWithFailure' | |||
} |
@@ -3563,9 +3563,13 @@ maintenance.sonarqube_is_offline.text=The connection to SonarQube is lost. Pleas | |||
# | |||
#------------------------------------------------------------------------------ | |||
indexation.in_progress=SonarQube is reloading project data. Some projects will be unavailable until this process is complete. | |||
indexation.in_progress.details={0}% completed. | |||
indexation.in_progress.admin_details=See {link}. | |||
indexation.progression={0}% complete. | |||
indexation.progression_with_error={0}% complete with some {link}. | |||
indexation.progression_with_error.link=tasks failing | |||
indexation.completed=All project data has been reloaded. | |||
indexation.completed_with_error=SonarQube completed the reload of project data. Some {link} causing some projects to remain unavailable. | |||
indexation.completed_with_error.link=tasks failed | |||
indexation.admin_link=See {link} for more information. | |||
indexation.page_unavailable.title.issues=Issues page is temporarily unavailable | |||
indexation.page_unavailable.title.portfolios=Portfolios page is temporarily unavailable | |||
indexation.page_unavailable.title={componentQualifier} {componentName} is temporarily unavailable |