@@ -19,6 +19,7 @@ | |||
*/ | |||
import { getJSON, post, RequestData } from 'sonar-ui-common/helpers/request'; | |||
import throwGlobalError from '../app/utils/throwGlobalError'; | |||
import { IndexationStatus } from '../types/indexation'; | |||
export function getAnalysisStatus(data: { | |||
component: string; | |||
@@ -83,3 +84,7 @@ export function getWorkers(): Promise<{ canSetWorkerCount: boolean; value: numbe | |||
export function setWorkerCount(count: number): Promise<void | Response> { | |||
return post('/api/ce/set_worker_count', { count }).catch(throwGlobalError); | |||
} | |||
export function getIndexationStatus(): Promise<IndexationStatus> { | |||
return getJSON('/api/ce/indexation_status').catch(throwGlobalError); | |||
} |
@@ -24,6 +24,8 @@ import A11ySkipLinks from './a11y/A11ySkipLinks'; | |||
import SuggestionsProvider from './embed-docs-modal/SuggestionsProvider'; | |||
import GlobalFooterContainer from './GlobalFooterContainer'; | |||
import GlobalMessagesContainer from './GlobalMessagesContainer'; | |||
import IndexationContextProvider from './indexation/IndexationContextProvider'; | |||
import IndexationNotification from './indexation/IndexationNotification'; | |||
import GlobalNav from './nav/global/GlobalNav'; | |||
import StartupModal from './StartupModal'; | |||
@@ -46,9 +48,12 @@ export default function GlobalContainer(props: Props) { | |||
<div className="page-wrapper" id="container"> | |||
<div className="page-container"> | |||
<Workspace> | |||
<GlobalNav location={props.location} /> | |||
<GlobalMessagesContainer /> | |||
{props.children} | |||
<IndexationContextProvider> | |||
<GlobalNav location={props.location} /> | |||
<GlobalMessagesContainer /> | |||
<IndexationNotification /> | |||
{props.children} | |||
</IndexationContextProvider> | |||
</Workspace> | |||
</div> | |||
</div> |
@@ -16,21 +16,24 @@ exports[`should render correctly 1`] = ` | |||
className="page-container" | |||
> | |||
<Workspace> | |||
<Connect(GlobalNav) | |||
location={ | |||
Object { | |||
"action": "PUSH", | |||
"hash": "", | |||
"key": "key", | |||
"pathname": "/path", | |||
"query": Object {}, | |||
"search": "", | |||
"state": Object {}, | |||
<Connect(withAppState(IndexationContextProvider))> | |||
<Connect(GlobalNav) | |||
location={ | |||
Object { | |||
"action": "PUSH", | |||
"hash": "", | |||
"key": "key", | |||
"pathname": "/path", | |||
"query": Object {}, | |||
"search": "", | |||
"state": Object {}, | |||
} | |||
} | |||
} | |||
/> | |||
<Connect(GlobalMessages) /> | |||
<ChildComponent /> | |||
/> | |||
<Connect(GlobalMessages) /> | |||
<withIndexationContext(IndexationNotification) /> | |||
<ChildComponent /> | |||
</Connect(withAppState(IndexationContextProvider))> | |||
</Workspace> | |||
</div> | |||
</div> |
@@ -0,0 +1,25 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { createContext } from 'react'; | |||
import { IndexationContextInterface } from '../../../types/indexation'; | |||
// eslint-disable-next-line import/prefer-default-export | |||
export const IndexationContext = createContext<IndexationContextInterface | null>(null); |
@@ -0,0 +1,78 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { withAppState } from '../../../components/hoc/withAppState'; | |||
import { IndexationContextInterface, IndexationStatus } from '../../../types/indexation'; | |||
import { IndexationContext } from './IndexationContext'; | |||
import IndexationNotificationHelper from './IndexationNotificationHelper'; | |||
interface Props { | |||
appState: Pick<T.AppState, 'needIssueSync'>; | |||
} | |||
export class IndexationContextProvider extends React.PureComponent< | |||
React.PropsWithChildren<Props>, | |||
IndexationContextInterface | |||
> { | |||
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) { | |||
IndexationNotificationHelper.startPolling(this.handleNewStatus); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
IndexationNotificationHelper.stopPolling(); | |||
} | |||
handleNewStatus = (newIndexationStatus: IndexationStatus) => { | |||
if (newIndexationStatus.isCompleted) { | |||
IndexationNotificationHelper.stopPolling(); | |||
} | |||
if (this.mounted) { | |||
this.setState({ status: newIndexationStatus }); | |||
} | |||
}; | |||
render() { | |||
return ( | |||
<IndexationContext.Provider value={this.state}> | |||
{this.props.children} | |||
</IndexationContext.Provider> | |||
); | |||
} | |||
} | |||
export default withAppState(IndexationContextProvider); |
@@ -0,0 +1,30 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
.indexation-notification-wrapper { | |||
height: 34px; | |||
} | |||
.indexation-notification-banner { | |||
position: fixed; | |||
width: 100%; | |||
z-index: var(--globalBannerZIndex); | |||
margin-bottom: 0 !important; | |||
} |
@@ -0,0 +1,87 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import withIndexationContext, { | |||
WithIndexationContextProps | |||
} from '../../../components/hoc/withIndexationContext'; | |||
import './IndexationNotification.css'; | |||
import IndexationNotificationHelper from './IndexationNotificationHelper'; | |||
import IndexationNotificationRenderer from './IndexationNotificationRenderer'; | |||
interface State { | |||
progression?: IndexationProgression; | |||
} | |||
export enum IndexationProgression { | |||
InProgress, | |||
Completed | |||
} | |||
export class IndexationNotification extends React.PureComponent<WithIndexationContextProps, State> { | |||
state: State = { | |||
progression: undefined | |||
}; | |||
componentDidMount() { | |||
this.refreshNotification(); | |||
} | |||
componentDidUpdate() { | |||
this.refreshNotification(); | |||
} | |||
refreshNotification() { | |||
if (!this.props.indexationContext.status.isCompleted) { | |||
IndexationNotificationHelper.markInProgressNotificationAsDisplayed(); | |||
this.setState({ progression: IndexationProgression.InProgress }); | |||
} else if (IndexationNotificationHelper.shouldDisplayCompletedNotification()) { | |||
this.setState({ progression: IndexationProgression.Completed }); | |||
} | |||
} | |||
handleDismissCompletedNotification = () => { | |||
IndexationNotificationHelper.markCompletedNotificationAsDisplayed(); | |||
this.setState({ progression: undefined }); | |||
}; | |||
render() { | |||
const { progression } = this.state; | |||
const { | |||
indexationContext: { | |||
status: { percentCompleted } | |||
} | |||
} = this.props; | |||
if (progression === undefined) { | |||
return null; | |||
} | |||
return ( | |||
<IndexationNotificationRenderer | |||
progression={progression} | |||
percentCompleted={percentCompleted ?? 0} | |||
onDismissCompletedNotification={this.handleDismissCompletedNotification} | |||
/> | |||
); | |||
} | |||
} | |||
export default withIndexationContext(IndexationNotification); |
@@ -0,0 +1,57 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { get, remove, save } from 'sonar-ui-common/helpers/storage'; | |||
import { getIndexationStatus } from '../../../api/ce'; | |||
import { IndexationStatus } from '../../../types/indexation'; | |||
const POLLING_INTERVAL_MS = 5000; | |||
const LS_INDEXATION_PROGRESS_WAS_DISPLAYED = 'indexation.progress.was.displayed'; | |||
export default class IndexationNotificationHelper { | |||
private static interval?: NodeJS.Timeout; | |||
static startPolling(onNewStatus: (status: IndexationStatus) => void) { | |||
this.stopPolling(); | |||
this.interval = setInterval(async () => { | |||
const status = await getIndexationStatus(); | |||
onNewStatus(status); | |||
}, POLLING_INTERVAL_MS); | |||
} | |||
static stopPolling() { | |||
if (this.interval) { | |||
clearInterval(this.interval); | |||
} | |||
} | |||
static markInProgressNotificationAsDisplayed() { | |||
save(LS_INDEXATION_PROGRESS_WAS_DISPLAYED, true.toString()); | |||
} | |||
static markCompletedNotificationAsDisplayed() { | |||
remove(LS_INDEXATION_PROGRESS_WAS_DISPLAYED); | |||
} | |||
static shouldDisplayCompletedNotification() { | |||
return JSON.parse(get(LS_INDEXATION_PROGRESS_WAS_DISPLAYED) || false.toString()); | |||
} | |||
} |
@@ -0,0 +1,65 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { ButtonLink } from 'sonar-ui-common/components/controls/buttons'; | |||
import { Alert } from 'sonar-ui-common/components/ui/Alert'; | |||
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; | |||
import { IndexationProgression } from './IndexationNotification'; | |||
export interface IndexationNotificationRendererProps { | |||
progression: IndexationProgression; | |||
percentCompleted: number; | |||
onDismissCompletedNotification: VoidFunction; | |||
} | |||
export default function IndexationNotificationRenderer(props: IndexationNotificationRendererProps) { | |||
const { progression, percentCompleted } = props; | |||
const inProgress = progression === IndexationProgression.InProgress; | |||
return ( | |||
<div className="indexation-notification-wrapper"> | |||
<Alert | |||
className="indexation-notification-banner" | |||
display="banner" | |||
variant={inProgress ? 'warning' : 'success'}> | |||
<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> | |||
</> | |||
) : ( | |||
<> | |||
<span>{translate('indexation.completed')}</span> | |||
<ButtonLink className="spacer-left" onClick={props.onDismissCompletedNotification}> | |||
<strong>{translate('dismiss')}</strong> | |||
</ButtonLink> | |||
</> | |||
)} | |||
</div> | |||
</Alert> | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,90 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { mount } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { IndexationStatus } from '../../../../types/indexation'; | |||
import { IndexationContext } from '../IndexationContext'; | |||
import { IndexationContextProvider } from '../IndexationContextProvider'; | |||
import IndexationNotificationHelper from '../IndexationNotificationHelper'; | |||
beforeEach(() => jest.clearAllMocks()); | |||
jest.mock('../IndexationNotificationHelper'); | |||
it('should render correctly & start polling', () => { | |||
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(IndexationNotificationHelper.startPolling).toHaveBeenCalled(); | |||
}); | |||
it('should not start polling if not needed', () => { | |||
mountRender({ appState: { needIssueSync: false } }); | |||
expect(IndexationNotificationHelper.startPolling).not.toHaveBeenCalled(); | |||
}); | |||
it('should update the state on new status & stop polling if indexation is complete', () => { | |||
const wrapper = mountRender(); | |||
const triggerNewStatus = (IndexationNotificationHelper.startPolling as jest.Mock).mock | |||
.calls[0][0] as (status: IndexationStatus) => void; | |||
const newStatus = { isCompleted: true, percentCompleted: 100 }; | |||
triggerNewStatus(newStatus); | |||
expect(wrapper.state().status).toEqual(newStatus); | |||
expect(IndexationNotificationHelper.stopPolling).toHaveBeenCalled(); | |||
}); | |||
it('should stop polling when component is destroyed', () => { | |||
const wrapper = mountRender(); | |||
wrapper.unmount(); | |||
expect(IndexationNotificationHelper.stopPolling).toHaveBeenCalled(); | |||
}); | |||
function mountRender(props?: IndexationContextProvider['props']) { | |||
return mount<IndexationContextProvider>( | |||
<IndexationContextProvider appState={{ needIssueSync: true }} {...props}> | |||
<TestComponent /> | |||
</IndexationContextProvider> | |||
); | |||
} | |||
class TestComponent extends React.PureComponent { | |||
context!: IndexationStatus; | |||
static contextType = IndexationContext; | |||
render() { | |||
return <h1>TestComponent</h1>; | |||
} | |||
} |
@@ -0,0 +1,87 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { IndexationNotification, IndexationProgression } from '../IndexationNotification'; | |||
import IndexationNotificationHelper from '../IndexationNotificationHelper'; | |||
import IndexationNotificationRenderer from '../IndexationNotificationRenderer'; | |||
beforeEach(() => jest.clearAllMocks()); | |||
jest.mock('../IndexationNotificationHelper'); | |||
it('should display the warning banner if indexation is in progress', () => { | |||
const wrapper = shallowRender(); | |||
expect(IndexationNotificationHelper.markInProgressNotificationAsDisplayed).toHaveBeenCalled(); | |||
expect(wrapper.state().progression).toBe(IndexationProgression.InProgress); | |||
}); | |||
it('should display the success banner when indexation is complete', () => { | |||
(IndexationNotificationHelper.shouldDisplayCompletedNotification as jest.Mock).mockReturnValueOnce( | |||
true | |||
); | |||
const wrapper = shallowRender(); | |||
wrapper.setProps({ indexationContext: { status: { isCompleted: true } } }); | |||
expect(IndexationNotificationHelper.shouldDisplayCompletedNotification).toHaveBeenCalled(); | |||
expect(wrapper.state().progression).toBe(IndexationProgression.Completed); | |||
}); | |||
it('should render correctly completed notification at startup', () => { | |||
(IndexationNotificationHelper.shouldDisplayCompletedNotification as jest.Mock).mockReturnValueOnce( | |||
true | |||
); | |||
const wrapper = shallowRender({ | |||
indexationContext: { status: { isCompleted: true } } | |||
}); | |||
expect(IndexationNotificationHelper.markInProgressNotificationAsDisplayed).not.toHaveBeenCalled(); | |||
expect(IndexationNotificationHelper.shouldDisplayCompletedNotification).toHaveBeenCalled(); | |||
expect(wrapper.state().progression).toBe(IndexationProgression.Completed); | |||
}); | |||
it('should hide the success banner on dismiss action', () => { | |||
(IndexationNotificationHelper.shouldDisplayCompletedNotification as jest.Mock).mockReturnValueOnce( | |||
true | |||
); | |||
const wrapper = shallowRender({ | |||
indexationContext: { status: { isCompleted: true } } | |||
}); | |||
wrapper | |||
.find(IndexationNotificationRenderer) | |||
.props() | |||
.onDismissCompletedNotification(); | |||
expect(IndexationNotificationHelper.markCompletedNotificationAsDisplayed).toHaveBeenCalled(); | |||
expect(wrapper.state().progression).toBeUndefined(); | |||
}); | |||
function shallowRender(props?: Partial<IndexationNotification['props']>) { | |||
return shallow<IndexationNotification>( | |||
<IndexationNotification indexationContext={{ status: { isCompleted: false } }} {...props} /> | |||
); | |||
} |
@@ -0,0 +1,80 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { get, remove, save } from 'sonar-ui-common/helpers/storage'; | |||
import { getIndexationStatus } from '../../../../api/ce'; | |||
import { IndexationStatus } from '../../../../types/indexation'; | |||
import IndexationNotificationHelper from '../IndexationNotificationHelper'; | |||
beforeEach(() => { | |||
jest.clearAllMocks(); | |||
jest.useFakeTimers(); | |||
}); | |||
jest.mock('../../../../api/ce', () => ({ | |||
getIndexationStatus: jest.fn() | |||
})); | |||
jest.mock('sonar-ui-common/helpers/storage', () => ({ | |||
get: jest.fn(), | |||
remove: jest.fn(), | |||
save: jest.fn() | |||
})); | |||
it('should properly start & stop polling for indexation status', async () => { | |||
const onNewStatus = jest.fn(); | |||
const newStatus: IndexationStatus = { isCompleted: true, percentCompleted: 87 }; | |||
(getIndexationStatus as jest.Mock).mockResolvedValueOnce(newStatus); | |||
IndexationNotificationHelper.startPolling(onNewStatus); | |||
jest.runOnlyPendingTimers(); | |||
expect(getIndexationStatus).toHaveBeenCalled(); | |||
await new Promise(setImmediate); | |||
expect(onNewStatus).toHaveBeenCalledWith(newStatus); | |||
(getIndexationStatus as jest.Mock).mockClear(); | |||
IndexationNotificationHelper.stopPolling(); | |||
jest.runAllTimers(); | |||
expect(getIndexationStatus).not.toHaveBeenCalled(); | |||
}); | |||
it('should properly handle the flag to show the completed banner', () => { | |||
IndexationNotificationHelper.markInProgressNotificationAsDisplayed(); | |||
expect(save).toHaveBeenCalledWith(expect.any(String), 'true'); | |||
(get as jest.Mock).mockReturnValueOnce('true'); | |||
let shouldDisplay = IndexationNotificationHelper.shouldDisplayCompletedNotification(); | |||
expect(shouldDisplay).toBe(true); | |||
expect(get).toHaveBeenCalled(); | |||
IndexationNotificationHelper.markCompletedNotificationAsDisplayed(); | |||
expect(remove).toHaveBeenCalled(); | |||
shouldDisplay = IndexationNotificationHelper.shouldDisplayCompletedNotification(); | |||
expect(shouldDisplay).toBe(false); | |||
}); |
@@ -0,0 +1,57 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { ButtonLink } from 'sonar-ui-common/components/controls/buttons'; | |||
import { click } from 'sonar-ui-common/helpers/testUtils'; | |||
import { IndexationProgression } from '../IndexationNotification'; | |||
import IndexationNotificationRenderer, { | |||
IndexationNotificationRendererProps | |||
} from '../IndexationNotificationRenderer'; | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('in-progress'); | |||
expect(shallowRender({ progression: IndexationProgression.Completed })).toMatchSnapshot( | |||
'completed' | |||
); | |||
}); | |||
it('should propagate the dismiss event', () => { | |||
const onDismissCompletedNotification = jest.fn(); | |||
const wrapper = shallowRender({ | |||
progression: IndexationProgression.Completed, | |||
onDismissCompletedNotification | |||
}); | |||
click(wrapper.find(ButtonLink)); | |||
expect(onDismissCompletedNotification).toHaveBeenCalled(); | |||
}); | |||
function shallowRender(props: Partial<IndexationNotificationRendererProps> = {}) { | |||
return shallow<IndexationNotificationRendererProps>( | |||
<IndexationNotificationRenderer | |||
progression={IndexationProgression.InProgress} | |||
percentCompleted={25} | |||
onDismissCompletedNotification={jest.fn()} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,57 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: completed 1`] = ` | |||
<div | |||
className="indexation-notification-wrapper" | |||
> | |||
<Alert | |||
className="indexation-notification-banner" | |||
display="banner" | |||
variant="success" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<span> | |||
indexation.completed | |||
</span> | |||
<ButtonLink | |||
className="spacer-left" | |||
onClick={[MockFunction]} | |||
> | |||
<strong> | |||
dismiss | |||
</strong> | |||
</ButtonLink> | |||
</div> | |||
</Alert> | |||
</div> | |||
`; | |||
exports[`should render correctly: in-progress 1`] = ` | |||
<div | |||
className="indexation-notification-wrapper" | |||
> | |||
<Alert | |||
className="indexation-notification-banner" | |||
display="banner" | |||
variant="warning" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<span> | |||
indexation.in_progress | |||
</span> | |||
<i | |||
className="spinner spacer-left" | |||
/> | |||
<span | |||
className="spacer-left" | |||
> | |||
indexation.in_progress.details.25 | |||
</span> | |||
</div> | |||
</Alert> | |||
</div> | |||
`; |
@@ -193,6 +193,8 @@ module.exports = { | |||
pageMainZIndex: '50', | |||
pageSideZIndex: '51', | |||
globalBannerZIndex: '60', | |||
tooltipZIndex: '8000', | |||
dropdownMenuZIndex: '7500', |
@@ -0,0 +1,51 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { mount } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { IndexationContext } from '../../../app/components/indexation/IndexationContext'; | |||
import { IndexationContextInterface } from '../../../types/indexation'; | |||
import withIndexationContext, { WithIndexationContextProps } from '../withIndexationContext'; | |||
it('should render correctly', () => { | |||
const indexationContext: IndexationContextInterface = { | |||
status: { isCompleted: true, percentCompleted: 87 } | |||
}; | |||
const wrapper = mountRender(indexationContext); | |||
expect(wrapper.find(TestComponent).props().indexationContext).toEqual(indexationContext); | |||
}); | |||
function mountRender(indexationContext?: Partial<IndexationContextInterface>) { | |||
return mount( | |||
<IndexationContext.Provider value={{ status: { isCompleted: false }, ...indexationContext }}> | |||
<TestComponentWithIndexationContext /> | |||
</IndexationContext.Provider> | |||
); | |||
} | |||
class TestComponent extends React.PureComponent<WithIndexationContextProps> { | |||
render() { | |||
return <h1>TestComponent</h1>; | |||
} | |||
} | |||
const TestComponentWithIndexationContext = withIndexationContext(TestComponent); |
@@ -0,0 +1,54 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { IndexationContext } from '../../app/components/indexation/IndexationContext'; | |||
import { IndexationContextInterface } from '../../types/indexation'; | |||
import { getWrappedDisplayName } from './utils'; | |||
export interface WithIndexationContextProps { | |||
indexationContext: IndexationContextInterface; | |||
} | |||
export default function withIndexationContext<P>( | |||
WrappedComponent: React.ComponentType<P & WithIndexationContextProps> | |||
) { | |||
return class WithIndexationContext extends React.PureComponent< | |||
Omit<P, keyof WithIndexationContextProps> | |||
> { | |||
static displayName = getWrappedDisplayName(WrappedComponent, 'withIndexationContext'); | |||
render() { | |||
return ( | |||
<IndexationContext.Consumer> | |||
{indexationContext => { | |||
if (indexationContext) { | |||
return ( | |||
<WrappedComponent indexationContext={indexationContext} {...(this.props as P)} /> | |||
); | |||
} | |||
return null; | |||
}} | |||
</IndexationContext.Consumer> | |||
); | |||
} | |||
}; | |||
} |
@@ -0,0 +1,28 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2020 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
export interface IndexationStatus { | |||
isCompleted: boolean; | |||
percentCompleted?: number; | |||
} | |||
export interface IndexationContextInterface { | |||
status: IndexationStatus; | |||
} |
@@ -100,6 +100,7 @@ declare namespace T { | |||
edition: 'community' | 'developer' | 'enterprise' | 'datacenter' | undefined; | |||
globalPages?: Extension[]; | |||
multipleAlmEnabled?: boolean; | |||
needIssueSync?: boolean; | |||
organizationsEnabled?: boolean; | |||
productionDatabase: boolean; | |||
qualifiers: string[]; |
@@ -62,6 +62,7 @@ descending=Descending | |||
description=Description | |||
directories=Directories | |||
directory=Directory | |||
dismiss=Dismiss | |||
display=Display | |||
download_verb=Download | |||
duplications=Duplications | |||
@@ -3556,7 +3557,14 @@ maintenance.all_systems_opetational=All systems operational. | |||
maintenance.is_offline={instance} is offline | |||
maintenance.sonarqube_is_offline.text=The connection to SonarQube is lost. Please contact your system administrator. | |||
#------------------------------------------------------------------------------ | |||
# | |||
# INDEXATION | |||
# | |||
#------------------------------------------------------------------------------ | |||
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.completed=All project data has been reloaded. | |||
#------------------------------------------------------------------------------ | |||
# |