@@ -0,0 +1,34 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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 throwGlobalError from '../app/utils/throwGlobalError'; | |||
import { getJSON, post } from '../helpers/request'; | |||
import { DumpStatus } from '../types/project-dump'; | |||
export function getStatus(componentKey: string): Promise<DumpStatus> { | |||
return getJSON('/api/project_dump/status', { key: componentKey }).catch(throwGlobalError); | |||
} | |||
export function doExport(componentKey: string) { | |||
return post('/api/project_dump/export', { key: componentKey }).catch(throwGlobalError); | |||
} | |||
export function doImport(componentKey: string) { | |||
return post('/api/project_dump/import', { key: componentKey }).catch(throwGlobalError); | |||
} |
@@ -293,6 +293,7 @@ export class Menu extends React.PureComponent<Props> { | |||
this.renderConsoleAppLink(query, isApplication), | |||
this.renderReportSettingsLink(query, isApplication), | |||
...this.renderAdminExtensions(query, isApplication), | |||
this.renderImportExportLink(query), | |||
this.renderProfilesLink(query), | |||
this.renderQualityGateLink(query), | |||
this.renderLinksLink(query), | |||
@@ -403,6 +404,16 @@ export class Menu extends React.PureComponent<Props> { | |||
); | |||
}; | |||
renderImportExportLink = (query: Query) => { | |||
return ( | |||
<li key="import-export"> | |||
<Link activeClassName="active" to={{ pathname: '/project/import_export', query }}> | |||
{translate('project_dump.page')} | |||
</Link> | |||
</li> | |||
); | |||
}; | |||
renderProfilesLink = (query: Query) => { | |||
if (!this.getConfiguration().showQualityProfiles) { | |||
return null; |
@@ -349,6 +349,24 @@ exports[`should work for a branch 1`] = ` | |||
project_baseline.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/import_export", | |||
"query": Object { | |||
"branch": "release", | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
project_dump.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -719,6 +737,23 @@ exports[`should work for all qualifiers 1`] = ` | |||
project_baseline.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/import_export", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
project_dump.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -867,6 +902,23 @@ exports[`should work for all qualifiers 2`] = ` | |||
<ul | |||
className="menu" | |||
> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/import_export", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
project_dump.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -978,7 +1030,37 @@ exports[`should work for all qualifiers 3`] = ` | |||
/> | |||
</li> | |||
</NavBarTabs> | |||
<NavBarTabs /> | |||
<NavBarTabs> | |||
<Dropdown | |||
data-test="administration" | |||
overlay={ | |||
<ul | |||
className="menu" | |||
> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/import_export", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
project_dump.page | |||
</Link> | |||
</li> | |||
</ul> | |||
} | |||
tagName="li" | |||
> | |||
<Component /> | |||
</Dropdown> | |||
</NavBarTabs> | |||
</div> | |||
`; | |||
@@ -1105,6 +1187,23 @@ exports[`should work for all qualifiers 4`] = ` | |||
application_console.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/import_export", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
project_dump.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -1502,6 +1601,23 @@ exports[`should work with extensions 2`] = ` | |||
Foo | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/import_export", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
project_dump.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
@@ -1689,6 +1805,23 @@ exports[`should work with multiple extensions 2`] = ` | |||
Bar | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project/import_export", | |||
"query": Object { | |||
"id": "foo", | |||
}, | |||
} | |||
} | |||
> | |||
project_dump.page | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
activeClassName="active" |
@@ -47,6 +47,7 @@ import portfolioRoutes from '../../apps/portfolio/routes'; | |||
import projectActivityRoutes from '../../apps/projectActivity/routes'; | |||
import projectBaselineRoutes from '../../apps/projectBaseline/routes'; | |||
import projectBranchesRoutes from '../../apps/projectBranches/routes'; | |||
import projectDumpRoutes from '../../apps/projectDump/routes'; | |||
import projectQualityGateRoutes from '../../apps/projectQualityGate/routes'; | |||
import projectQualityProfilesRoutes from '../../apps/projectQualityProfiles/routes'; | |||
import projectsRoutes from '../../apps/projects/routes'; | |||
@@ -206,6 +207,7 @@ function renderComponentRoutes() { | |||
<RouteWithChildRoutes path="project/background_tasks" childRoutes={backgroundTasksRoutes} /> | |||
<RouteWithChildRoutes path="project/baseline" childRoutes={projectBaselineRoutes} /> | |||
<RouteWithChildRoutes path="project/branches" childRoutes={projectBranchesRoutes} /> | |||
<RouteWithChildRoutes path="project/import_export" childRoutes={projectDumpRoutes} /> | |||
<RouteWithChildRoutes path="project/settings" childRoutes={settingsRoutes} /> | |||
<RouteWithChildRoutes path="project_roles" childRoutes={projectPermissionsRoutes} /> | |||
<RouteWithChildRoutes path="application/console" childRoutes={applicationConsoleRoutes} /> |
@@ -0,0 +1,200 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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 { getActivity } from '../../api/ce'; | |||
import { getStatus } from '../../api/project-dump'; | |||
import throwGlobalError from '../../app/utils/throwGlobalError'; | |||
import { withAppState } from '../../components/hoc/withAppState'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { DumpStatus, DumpTask } from '../../types/project-dump'; | |||
import { TaskStatuses, TaskTypes } from '../../types/tasks'; | |||
import Export from './components/Export'; | |||
import Import from './components/Import'; | |||
import './styles.css'; | |||
const POLL_INTERNAL = 5000; | |||
interface Props { | |||
appState: Pick<T.AppState, 'projectImportFeatureEnabled'>; | |||
component: T.Component; | |||
} | |||
interface State { | |||
lastAnalysisTask?: DumpTask; | |||
lastExportTask?: DumpTask; | |||
lastImportTask?: DumpTask; | |||
status?: DumpStatus; | |||
} | |||
export class ProjectDumpApp extends React.Component<Props, State> { | |||
mounted = false; | |||
state: State = {}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.loadStatus(); | |||
} | |||
componentDidUpdate(prevProps: Props) { | |||
if (prevProps.component.key !== this.props.component.key) { | |||
this.loadStatus(); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
getLastTask(component: string, type: TaskTypes) { | |||
const data = { | |||
type, | |||
component, | |||
onlyCurrents: true, | |||
status: [ | |||
TaskStatuses.Pending, | |||
TaskStatuses.InProgress, | |||
TaskStatuses.Success, | |||
TaskStatuses.Failed, | |||
TaskStatuses.Canceled | |||
].join(',') | |||
}; | |||
return getActivity(data) | |||
.then(({ tasks }) => (tasks.length > 0 ? tasks[0] : undefined), throwGlobalError) | |||
.catch(() => undefined); | |||
} | |||
getLastTaskOfEachType(componentKey: string) { | |||
const { | |||
appState: { projectImportFeatureEnabled } | |||
} = this.props; | |||
const all = projectImportFeatureEnabled | |||
? [ | |||
this.getLastTask(componentKey, TaskTypes.ProjectExport), | |||
this.getLastTask(componentKey, TaskTypes.ProjectImport), | |||
this.getLastTask(componentKey, TaskTypes.Report) | |||
] | |||
: [ | |||
this.getLastTask(componentKey, TaskTypes.ProjectExport), | |||
Promise.resolve(), | |||
this.getLastTask(componentKey, TaskTypes.Report) | |||
]; | |||
return Promise.all(all).then(([lastExportTask, lastImportTask, lastAnalysisTask]) => ({ | |||
lastExportTask, | |||
lastImportTask, | |||
lastAnalysisTask | |||
})); | |||
} | |||
loadStatus = () => { | |||
const { component } = this.props; | |||
return Promise.all([getStatus(component.key), this.getLastTaskOfEachType(component.key)]).then( | |||
([status, { lastExportTask, lastImportTask, lastAnalysisTask }]) => { | |||
if (this.mounted) { | |||
this.setState({ | |||
status, | |||
lastExportTask, | |||
lastImportTask, | |||
lastAnalysisTask | |||
}); | |||
} | |||
return { | |||
status, | |||
lastExportTask, | |||
lastImportTask, | |||
lastAnalysisTask | |||
}; | |||
} | |||
); | |||
}; | |||
poll = () => { | |||
this.loadStatus().then( | |||
({ lastExportTask, lastImportTask }) => { | |||
if (this.mounted) { | |||
const progressStatus = [TaskStatuses.Pending, TaskStatuses.InProgress]; | |||
const exportNotFinished = | |||
lastExportTask === undefined || progressStatus.includes(lastExportTask.status); | |||
const importNotFinished = | |||
lastImportTask === undefined || progressStatus.includes(lastImportTask.status); | |||
if (exportNotFinished || importNotFinished) { | |||
setTimeout(this.poll, POLL_INTERNAL); | |||
} else { | |||
// Since we fetch status separate from task we could not get an up to date status. | |||
// even if we detect that export / import is finish. | |||
// Doing a last call will make sur we get the latest status. | |||
this.loadStatus(); | |||
} | |||
} | |||
}, | |||
() => { | |||
/* no catch needed */ | |||
} | |||
); | |||
}; | |||
render() { | |||
const { | |||
component, | |||
appState: { projectImportFeatureEnabled } | |||
} = this.props; | |||
const { lastAnalysisTask, lastExportTask, lastImportTask, status } = this.state; | |||
return ( | |||
<div className="page page-limited" id="project-dump"> | |||
<header className="page-header"> | |||
<h1 className="page-title">{translate('project_dump.page')}</h1> | |||
<div className="page-description"> | |||
{projectImportFeatureEnabled | |||
? translate('project_dump.page.description') | |||
: translate('project_dump.page.description_without_import')} | |||
</div> | |||
</header> | |||
{status === undefined ? ( | |||
<i className="spinner" /> | |||
) : ( | |||
<div className="columns"> | |||
<div className="column-half"> | |||
<Export | |||
componentKey={component.key} | |||
loadStatus={this.poll} | |||
status={status} | |||
task={lastExportTask} | |||
/> | |||
</div> | |||
<div className="column-half"> | |||
<Import | |||
importEnabled={!!projectImportFeatureEnabled} | |||
analysis={lastAnalysisTask} | |||
componentKey={component.key} | |||
loadStatus={this.poll} | |||
status={status} | |||
task={lastImportTask} | |||
/> | |||
</div> | |||
</div> | |||
)} | |||
</div> | |||
); | |||
} | |||
} | |||
export default withAppState(ProjectDumpApp); |
@@ -0,0 +1,95 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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 { getActivity } from '../../../api/ce'; | |||
import { getStatus } from '../../../api/project-dump'; | |||
import { mockComponent } from '../../../helpers/mocks/component'; | |||
import { mockAppState, mockDumpStatus, mockDumpTask } from '../../../helpers/testMocks'; | |||
import { waitAndUpdate } from '../../../helpers/testUtils'; | |||
import { TaskStatuses } from '../../../types/tasks'; | |||
import { ProjectDumpApp } from '../ProjectDumpApp'; | |||
jest.mock('../../../api/ce', () => ({ | |||
getActivity: jest.fn().mockResolvedValue({ tasks: [] }) | |||
})); | |||
jest.mock('../../../api/project-dump', () => ({ | |||
getStatus: jest.fn().mockResolvedValue({}) | |||
})); | |||
beforeEach(() => { | |||
jest.clearAllMocks(); | |||
}); | |||
it('should render correctly', async () => { | |||
(getActivity as jest.Mock) | |||
.mockResolvedValueOnce({ tasks: [mockDumpTask()] }) | |||
.mockResolvedValueOnce({ tasks: [mockDumpTask()] }) | |||
.mockResolvedValueOnce({ tasks: [mockDumpTask()] }); | |||
let wrapper = shallowRender(); | |||
expect(wrapper).toMatchSnapshot('loading'); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot('loaded'); | |||
wrapper = shallowRender({ appState: mockAppState({ projectImportFeatureEnabled: false }) }); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper).toMatchSnapshot('loaded without import'); | |||
}); | |||
it('should poll for task status update', async () => { | |||
jest.useFakeTimers(); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
jest.clearAllMocks(); | |||
const finalStatus = mockDumpStatus({ exportedDump: 'export-path' }); | |||
(getStatus as jest.Mock) | |||
.mockResolvedValueOnce(mockDumpStatus()) | |||
.mockResolvedValueOnce(finalStatus); | |||
(getActivity as jest.Mock) | |||
.mockResolvedValueOnce({ tasks: [mockDumpTask({ status: TaskStatuses.Pending })] }) | |||
.mockResolvedValueOnce({ tasks: [mockDumpTask({ status: TaskStatuses.Success })] }); | |||
wrapper.instance().poll(); | |||
// wait for all promises | |||
await waitAndUpdate(wrapper); | |||
jest.runAllTimers(); | |||
await waitAndUpdate(wrapper); | |||
expect(getStatus).toHaveBeenCalledTimes(2); | |||
expect(wrapper.state().status).toBe(finalStatus); | |||
}); | |||
function shallowRender(overrides: Partial<ProjectDumpApp['props']> = {}) { | |||
return shallow<ProjectDumpApp>( | |||
<ProjectDumpApp | |||
appState={mockAppState({ projectImportFeatureEnabled: true })} | |||
component={mockComponent()} | |||
{...overrides} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,140 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: loaded 1`] = ` | |||
<div | |||
className="page page-limited" | |||
id="project-dump" | |||
> | |||
<header | |||
className="page-header" | |||
> | |||
<h1 | |||
className="page-title" | |||
> | |||
project_dump.page | |||
</h1> | |||
<div | |||
className="page-description" | |||
> | |||
project_dump.page.description | |||
</div> | |||
</header> | |||
<div | |||
className="columns" | |||
> | |||
<div | |||
className="column-half" | |||
> | |||
<Export | |||
componentKey="my-project" | |||
loadStatus={[Function]} | |||
status={Object {}} | |||
task={ | |||
Object { | |||
"executedAt": "2020-03-12T12:22:20Z", | |||
"startedAt": "2020-03-12T12:20:20Z", | |||
"status": "SUCCESS", | |||
"submittedAt": "2020-03-12T12:15:20Z", | |||
} | |||
} | |||
/> | |||
</div> | |||
<div | |||
className="column-half" | |||
> | |||
<Import | |||
analysis={ | |||
Object { | |||
"executedAt": "2020-03-12T12:22:20Z", | |||
"startedAt": "2020-03-12T12:20:20Z", | |||
"status": "SUCCESS", | |||
"submittedAt": "2020-03-12T12:15:20Z", | |||
} | |||
} | |||
componentKey="my-project" | |||
importEnabled={true} | |||
loadStatus={[Function]} | |||
status={Object {}} | |||
task={ | |||
Object { | |||
"executedAt": "2020-03-12T12:22:20Z", | |||
"startedAt": "2020-03-12T12:20:20Z", | |||
"status": "SUCCESS", | |||
"submittedAt": "2020-03-12T12:15:20Z", | |||
} | |||
} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: loaded without import 1`] = ` | |||
<div | |||
className="page page-limited" | |||
id="project-dump" | |||
> | |||
<header | |||
className="page-header" | |||
> | |||
<h1 | |||
className="page-title" | |||
> | |||
project_dump.page | |||
</h1> | |||
<div | |||
className="page-description" | |||
> | |||
project_dump.page.description_without_import | |||
</div> | |||
</header> | |||
<div | |||
className="columns" | |||
> | |||
<div | |||
className="column-half" | |||
> | |||
<Export | |||
componentKey="my-project" | |||
loadStatus={[Function]} | |||
status={Object {}} | |||
/> | |||
</div> | |||
<div | |||
className="column-half" | |||
> | |||
<Import | |||
componentKey="my-project" | |||
importEnabled={false} | |||
loadStatus={[Function]} | |||
status={Object {}} | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: loading 1`] = ` | |||
<div | |||
className="page page-limited" | |||
id="project-dump" | |||
> | |||
<header | |||
className="page-header" | |||
> | |||
<h1 | |||
className="page-title" | |||
> | |||
project_dump.page | |||
</h1> | |||
<div | |||
className="page-description" | |||
> | |||
project_dump.page.description | |||
</div> | |||
</header> | |||
<i | |||
className="spinner" | |||
/> | |||
</div> | |||
`; |
@@ -0,0 +1,188 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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 { doExport } from '../../../api/project-dump'; | |||
import { Button } from '../../../components/controls/buttons'; | |||
import DateFromNow from '../../../components/intl/DateFromNow'; | |||
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; | |||
import { Alert } from '../../../components/ui/Alert'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { getBaseUrl } from '../../../helpers/system'; | |||
import { DumpStatus, DumpTask } from '../../../types/project-dump'; | |||
interface Props { | |||
componentKey: string; | |||
loadStatus: () => void; | |||
status: DumpStatus; | |||
task?: DumpTask; | |||
} | |||
export default class Export extends React.Component<Props> { | |||
handleExport = () => { | |||
doExport(this.props.componentKey).then(this.props.loadStatus, () => { | |||
/* no catch needed */ | |||
}); | |||
}; | |||
renderHeader() { | |||
return ( | |||
<div className="boxed-group-header"> | |||
<h2>{translate('project_dump.export')}</h2> | |||
</div> | |||
); | |||
} | |||
renderWhenCanNotExport() { | |||
return ( | |||
<div className="boxed-group" id="project-export"> | |||
{this.renderHeader()} | |||
<div className="boxed-group-inner"> | |||
<Alert id="export-not-possible" variant="warning"> | |||
{translate('project_dump.can_not_export')} | |||
</Alert> | |||
</div> | |||
</div> | |||
); | |||
} | |||
renderWhenExportPending(task: DumpTask) { | |||
return ( | |||
<div className="boxed-group" id="project-export"> | |||
{this.renderHeader()} | |||
<div className="boxed-group-inner" id="export-pending"> | |||
<i className="spinner spacer-right" /> | |||
<DateTimeFormatter date={task.submittedAt}> | |||
{formatted => ( | |||
<span>{translateWithParameters('project_dump.pending_export', formatted)}</span> | |||
)} | |||
</DateTimeFormatter> | |||
</div> | |||
</div> | |||
); | |||
} | |||
renderWhenExportInProgress(task: DumpTask) { | |||
return ( | |||
<div className="boxed-group" id="project-export"> | |||
{this.renderHeader()} | |||
<div className="boxed-group-inner" id="export-in-progress"> | |||
<i className="spinner spacer-right" /> | |||
{task.startedAt && ( | |||
<DateFromNow date={task.startedAt}> | |||
{fromNow => ( | |||
<span>{translateWithParameters('project_dump.in_progress_export', fromNow)}</span> | |||
)} | |||
</DateFromNow> | |||
)} | |||
</div> | |||
</div> | |||
); | |||
} | |||
renderWhenExportFailed() { | |||
const { componentKey } = this.props; | |||
const detailsUrl = `${getBaseUrl()}/project/background_tasks?id=${encodeURIComponent( | |||
componentKey | |||
)}&status=FAILED&taskType=PROJECT_EXPORT`; | |||
return ( | |||
<div className="boxed-group" id="project-export"> | |||
{this.renderHeader()} | |||
<div className="boxed-group-inner"> | |||
<Alert id="export-in-progress" variant="error"> | |||
{translate('project_dump.failed_export')} | |||
<a className="spacer-left" href={detailsUrl}> | |||
{translate('project_dump.see_details')} | |||
</a> | |||
</Alert> | |||
{this.renderExport()} | |||
</div> | |||
</div> | |||
); | |||
} | |||
renderDump(task?: DumpTask) { | |||
const { status } = this.props; | |||
return ( | |||
<Alert className="export-dump" variant="success"> | |||
{task && task.executedAt && ( | |||
<DateTimeFormatter date={task.executedAt}> | |||
{formatted => ( | |||
<div className="export-dump-message"> | |||
{translateWithParameters('project_dump.latest_export_available', formatted)} | |||
</div> | |||
)} | |||
</DateTimeFormatter> | |||
)} | |||
{!task && ( | |||
<div className="export-dump-message">{translate('project_dump.export_available')}</div> | |||
)} | |||
<div className="export-dump-path"> | |||
<code tabIndex={0}>{status.exportedDump}</code> | |||
</div> | |||
</Alert> | |||
); | |||
} | |||
renderExport() { | |||
return ( | |||
<div> | |||
<div className="spacer-bottom">{translate('project_dump.export_form_description')}</div> | |||
<Button onClick={this.handleExport}>{translate('project_dump.do_export')}</Button> | |||
</div> | |||
); | |||
} | |||
render() { | |||
const { status, task } = this.props; | |||
if (!status.canBeExported) { | |||
return this.renderWhenCanNotExport(); | |||
} | |||
if (task && task.status === 'PENDING') { | |||
return this.renderWhenExportPending(task); | |||
} | |||
if (task && task.status === 'IN_PROGRESS') { | |||
return this.renderWhenExportInProgress(task); | |||
} | |||
if (task && task.status === 'FAILED') { | |||
return this.renderWhenExportFailed(); | |||
} | |||
const isDumpAvailable = Boolean(status.exportedDump); | |||
return ( | |||
<div className="boxed-group" id="project-export"> | |||
{this.renderHeader()} | |||
<div className="boxed-group-inner"> | |||
{isDumpAvailable && this.renderDump(task)} | |||
{this.renderExport()} | |||
</div> | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,180 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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 classNames from 'classnames'; | |||
import { stringify } from 'querystring'; | |||
import * as React from 'react'; | |||
import { doImport } from '../../../api/project-dump'; | |||
import { Button } from '../../../components/controls/buttons'; | |||
import DateFromNow from '../../../components/intl/DateFromNow'; | |||
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; | |||
import { Alert } from '../../../components/ui/Alert'; | |||
import { translate, translateWithParameters } from '../../../helpers/l10n'; | |||
import { getBaseUrl } from '../../../helpers/system'; | |||
import { DumpStatus, DumpTask } from '../../../types/project-dump'; | |||
import { TaskStatuses, TaskTypes } from '../../../types/tasks'; | |||
interface Props { | |||
analysis?: DumpTask; | |||
componentKey: string; | |||
importEnabled: boolean; | |||
loadStatus: () => void; | |||
status: DumpStatus; | |||
task?: DumpTask; | |||
} | |||
export default class Import extends React.Component<Props> { | |||
handleImport = () => { | |||
doImport(this.props.componentKey).then(this.props.loadStatus, () => { | |||
/* no catch needed */ | |||
}); | |||
}; | |||
renderWhenCanNotImport() { | |||
return ( | |||
<div className="boxed-group-inner" id="import-not-possible"> | |||
{translate('project_dump.can_not_import')} | |||
</div> | |||
); | |||
} | |||
renderWhenNoDump() { | |||
return ( | |||
<div className="boxed-group-inner"> | |||
<Alert id="import-no-file" variant="warning"> | |||
{translate('project_dump.no_file_to_import')} | |||
</Alert> | |||
</div> | |||
); | |||
} | |||
renderImportForm() { | |||
return ( | |||
<div> | |||
<div className="spacer-bottom">{translate('project_dump.import_form_description')}</div> | |||
<Button onClick={this.handleImport}>{translate('project_dump.do_import')}</Button> | |||
</div> | |||
); | |||
} | |||
renderWhenImportSuccess(task: DumpTask) { | |||
return ( | |||
<div className="boxed-group-inner"> | |||
{task.executedAt && ( | |||
<DateTimeFormatter date={task.executedAt}> | |||
{formatted => ( | |||
<Alert variant="success"> | |||
{translateWithParameters('project_dump.import_success', formatted)} | |||
</Alert> | |||
)} | |||
</DateTimeFormatter> | |||
)} | |||
</div> | |||
); | |||
} | |||
renderWhenImportPending(task: DumpTask) { | |||
return ( | |||
<div className="boxed-group-inner" id="import-pending"> | |||
<i className="spinner spacer-right" /> | |||
<DateTimeFormatter date={task.submittedAt}> | |||
{formatted => ( | |||
<span>{translateWithParameters('project_dump.pending_import', formatted)}</span> | |||
)} | |||
</DateTimeFormatter> | |||
</div> | |||
); | |||
} | |||
renderWhenImportInProgress(task: DumpTask) { | |||
return ( | |||
<div className="boxed-group-inner" id="import-in-progress"> | |||
<i className="spinner spacer-right" /> | |||
{task.startedAt && ( | |||
<DateFromNow date={task.startedAt}> | |||
{fromNow => ( | |||
<span>{translateWithParameters('project_dump.in_progress_import', fromNow)}</span> | |||
)} | |||
</DateFromNow> | |||
)} | |||
</div> | |||
); | |||
} | |||
renderWhenImportFailed() { | |||
const { componentKey } = this.props; | |||
const detailsUrl = `${getBaseUrl()}/project/background_tasks?${stringify({ | |||
id: encodeURIComponent(componentKey), | |||
status: TaskStatuses.Failed, | |||
taskType: TaskTypes.ProjectImport | |||
})}`; | |||
return ( | |||
<div className="boxed-group-inner"> | |||
<Alert id="export-in-progress" variant="error"> | |||
{translate('project_dump.failed_import')} | |||
<a className="spacer-left" href={detailsUrl}> | |||
{translate('project_dump.see_details')} | |||
</a> | |||
</Alert> | |||
{this.renderImportForm()} | |||
</div> | |||
); | |||
} | |||
render() { | |||
const { importEnabled, status, task, analysis } = this.props; | |||
let content: React.ReactNode = null; | |||
if (task && task.status === TaskStatuses.Success && !analysis) { | |||
content = this.renderWhenImportSuccess(task); | |||
} else if (task && task.status === TaskStatuses.Pending) { | |||
content = this.renderWhenImportPending(task); | |||
} else if (task && task.status === TaskStatuses.InProgress) { | |||
content = this.renderWhenImportInProgress(task); | |||
} else if (task && task.status === TaskStatuses.Failed) { | |||
content = this.renderWhenImportFailed(); | |||
} else if (!status.canBeImported) { | |||
content = this.renderWhenCanNotImport(); | |||
} else if (!status.dumpToImport) { | |||
content = this.renderWhenNoDump(); | |||
} else { | |||
content = <div className="boxed-group-inner">{this.renderImportForm()}</div>; | |||
} | |||
return ( | |||
<div | |||
className={classNames('boxed-group', { | |||
'import-disabled text-muted': !importEnabled | |||
})} | |||
id="project-import"> | |||
<div className="boxed-group-header"> | |||
<h2>{translate('project_dump.import')}</h2> | |||
</div> | |||
{importEnabled ? ( | |||
content | |||
) : ( | |||
<div className="boxed-group-inner"> | |||
{translate('project_dump.import_form_description_disabled')} | |||
</div> | |||
)} | |||
</div> | |||
); | |||
} | |||
} |
@@ -0,0 +1,62 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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 { mockDumpStatus, mockDumpTask } from '../../../../helpers/testMocks'; | |||
import { TaskStatuses } from '../../../../types/tasks'; | |||
import Export from '../Export'; | |||
jest.mock('../../../../api/project-dump', () => ({ | |||
doExport: jest.fn().mockResolvedValue({}) | |||
})); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot('no task'); | |||
expect(shallowRender({ status: mockDumpStatus({ canBeExported: false }) })).toMatchSnapshot( | |||
'cannot export' | |||
); | |||
expect(shallowRender({ task: mockDumpTask({ status: TaskStatuses.Pending }) })).toMatchSnapshot( | |||
'task pending' | |||
); | |||
expect( | |||
shallowRender({ task: mockDumpTask({ status: TaskStatuses.InProgress }) }) | |||
).toMatchSnapshot('task in progress'); | |||
expect(shallowRender({ task: mockDumpTask({ status: TaskStatuses.Failed }) })).toMatchSnapshot( | |||
'task failed' | |||
); | |||
expect( | |||
shallowRender({ | |||
status: mockDumpStatus({ exportedDump: 'dump-file' }), | |||
task: mockDumpTask({ status: TaskStatuses.Success }) | |||
}) | |||
).toMatchSnapshot('success'); | |||
}); | |||
function shallowRender(overrides: Partial<Export['props']> = {}) { | |||
return shallow<Export>( | |||
<Export | |||
componentKey="key" | |||
loadStatus={jest.fn()} | |||
status={mockDumpStatus()} | |||
task={undefined} | |||
{...overrides} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,71 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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 { mockDumpStatus, mockDumpTask } from '../../../../helpers/testMocks'; | |||
import { TaskStatuses } from '../../../../types/tasks'; | |||
import Import from '../Import'; | |||
jest.mock('../../../../api/project-dump', () => ({ | |||
doImport: jest.fn().mockResolvedValue({}) | |||
})); | |||
it('should render correctly', () => { | |||
expect( | |||
shallowRender({ status: mockDumpStatus({ dumpToImport: 'import-file.zip' }) }) | |||
).toMatchSnapshot('import form'); | |||
expect(shallowRender()).toMatchSnapshot('no dump to import'); | |||
expect(shallowRender({ status: mockDumpStatus({ canBeImported: false }) })).toMatchSnapshot( | |||
'cannot import' | |||
); | |||
expect(shallowRender({ task: mockDumpTask({ status: TaskStatuses.Success }) })).toMatchSnapshot( | |||
'success' | |||
); | |||
expect( | |||
shallowRender({ | |||
analysis: mockDumpTask(), | |||
task: mockDumpTask({ status: TaskStatuses.Success }) | |||
}) | |||
).toMatchSnapshot('success, but with analysis -> show form'); | |||
expect(shallowRender({ task: mockDumpTask({ status: TaskStatuses.Pending }) })).toMatchSnapshot( | |||
'pending' | |||
); | |||
expect( | |||
shallowRender({ task: mockDumpTask({ status: TaskStatuses.InProgress }) }) | |||
).toMatchSnapshot('in progress'); | |||
expect(shallowRender({ task: mockDumpTask({ status: TaskStatuses.Failed }) })).toMatchSnapshot( | |||
'failed' | |||
); | |||
expect(shallowRender({ importEnabled: false })).toMatchSnapshot('import disabled'); | |||
}); | |||
function shallowRender(overrides: Partial<Import['props']> = {}) { | |||
return shallow<Import>( | |||
<Import | |||
importEnabled={true} | |||
analysis={undefined} | |||
componentKey="key" | |||
loadStatus={jest.fn()} | |||
status={mockDumpStatus()} | |||
task={undefined} | |||
{...overrides} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,206 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: cannot export 1`] = ` | |||
<div | |||
className="boxed-group" | |||
id="project-export" | |||
> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
project_dump.export | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
> | |||
<Alert | |||
id="export-not-possible" | |||
variant="warning" | |||
> | |||
project_dump.can_not_export | |||
</Alert> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: no task 1`] = ` | |||
<div | |||
className="boxed-group" | |||
id="project-export" | |||
> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
project_dump.export | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
> | |||
<div> | |||
<div | |||
className="spacer-bottom" | |||
> | |||
project_dump.export_form_description | |||
</div> | |||
<Button | |||
onClick={[Function]} | |||
> | |||
project_dump.do_export | |||
</Button> | |||
</div> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: success 1`] = ` | |||
<div | |||
className="boxed-group" | |||
id="project-export" | |||
> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
project_dump.export | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
> | |||
<Alert | |||
className="export-dump" | |||
variant="success" | |||
> | |||
<DateTimeFormatter | |||
date="2020-03-12T12:22:20Z" | |||
> | |||
<Component /> | |||
</DateTimeFormatter> | |||
<div | |||
className="export-dump-path" | |||
> | |||
<code | |||
tabIndex={0} | |||
> | |||
dump-file | |||
</code> | |||
</div> | |||
</Alert> | |||
<div> | |||
<div | |||
className="spacer-bottom" | |||
> | |||
project_dump.export_form_description | |||
</div> | |||
<Button | |||
onClick={[Function]} | |||
> | |||
project_dump.do_export | |||
</Button> | |||
</div> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: task failed 1`] = ` | |||
<div | |||
className="boxed-group" | |||
id="project-export" | |||
> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
project_dump.export | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
> | |||
<Alert | |||
id="export-in-progress" | |||
variant="error" | |||
> | |||
project_dump.failed_export | |||
<a | |||
className="spacer-left" | |||
href="/project/background_tasks?id=key&status=FAILED&taskType=PROJECT_EXPORT" | |||
> | |||
project_dump.see_details | |||
</a> | |||
</Alert> | |||
<div> | |||
<div | |||
className="spacer-bottom" | |||
> | |||
project_dump.export_form_description | |||
</div> | |||
<Button | |||
onClick={[Function]} | |||
> | |||
project_dump.do_export | |||
</Button> | |||
</div> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: task in progress 1`] = ` | |||
<div | |||
className="boxed-group" | |||
id="project-export" | |||
> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
project_dump.export | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
id="export-in-progress" | |||
> | |||
<i | |||
className="spinner spacer-right" | |||
/> | |||
<DateFromNow | |||
date="2020-03-12T12:20:20Z" | |||
> | |||
<Component /> | |||
</DateFromNow> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: task pending 1`] = ` | |||
<div | |||
className="boxed-group" | |||
id="project-export" | |||
> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
project_dump.export | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
id="export-pending" | |||
> | |||
<i | |||
className="spinner spacer-right" | |||
/> | |||
<DateTimeFormatter | |||
date="2020-03-12T12:15:20Z" | |||
> | |||
<Component /> | |||
</DateTimeFormatter> | |||
</div> | |||
</div> | |||
`; |
@@ -0,0 +1,246 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly: cannot import 1`] = ` | |||
<div | |||
className="boxed-group" | |||
id="project-import" | |||
> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
project_dump.import | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
id="import-not-possible" | |||
> | |||
project_dump.can_not_import | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: failed 1`] = ` | |||
<div | |||
className="boxed-group" | |||
id="project-import" | |||
> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
project_dump.import | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
> | |||
<Alert | |||
id="export-in-progress" | |||
variant="error" | |||
> | |||
project_dump.failed_import | |||
<a | |||
className="spacer-left" | |||
href="/project/background_tasks?id=key&status=FAILED&taskType=PROJECT_IMPORT" | |||
> | |||
project_dump.see_details | |||
</a> | |||
</Alert> | |||
<div> | |||
<div | |||
className="spacer-bottom" | |||
> | |||
project_dump.import_form_description | |||
</div> | |||
<Button | |||
onClick={[Function]} | |||
> | |||
project_dump.do_import | |||
</Button> | |||
</div> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: import disabled 1`] = ` | |||
<div | |||
className="boxed-group import-disabled text-muted" | |||
id="project-import" | |||
> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
project_dump.import | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
> | |||
project_dump.import_form_description_disabled | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: import form 1`] = ` | |||
<div | |||
className="boxed-group" | |||
id="project-import" | |||
> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
project_dump.import | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
> | |||
<div> | |||
<div | |||
className="spacer-bottom" | |||
> | |||
project_dump.import_form_description | |||
</div> | |||
<Button | |||
onClick={[Function]} | |||
> | |||
project_dump.do_import | |||
</Button> | |||
</div> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: in progress 1`] = ` | |||
<div | |||
className="boxed-group" | |||
id="project-import" | |||
> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
project_dump.import | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
id="import-in-progress" | |||
> | |||
<i | |||
className="spinner spacer-right" | |||
/> | |||
<DateFromNow | |||
date="2020-03-12T12:20:20Z" | |||
> | |||
<Component /> | |||
</DateFromNow> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: no dump to import 1`] = ` | |||
<div | |||
className="boxed-group" | |||
id="project-import" | |||
> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
project_dump.import | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
> | |||
<Alert | |||
id="import-no-file" | |||
variant="warning" | |||
> | |||
project_dump.no_file_to_import | |||
</Alert> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: pending 1`] = ` | |||
<div | |||
className="boxed-group" | |||
id="project-import" | |||
> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
project_dump.import | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
id="import-pending" | |||
> | |||
<i | |||
className="spinner spacer-right" | |||
/> | |||
<DateTimeFormatter | |||
date="2020-03-12T12:15:20Z" | |||
> | |||
<Component /> | |||
</DateTimeFormatter> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: success 1`] = ` | |||
<div | |||
className="boxed-group" | |||
id="project-import" | |||
> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
project_dump.import | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
> | |||
<DateTimeFormatter | |||
date="2020-03-12T12:22:20Z" | |||
> | |||
<Component /> | |||
</DateTimeFormatter> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should render correctly: success, but with analysis -> show form 1`] = ` | |||
<div | |||
className="boxed-group" | |||
id="project-import" | |||
> | |||
<div | |||
className="boxed-group-header" | |||
> | |||
<h2> | |||
project_dump.import | |||
</h2> | |||
</div> | |||
<div | |||
className="boxed-group-inner" | |||
> | |||
<Alert | |||
id="import-no-file" | |||
variant="warning" | |||
> | |||
project_dump.no_file_to_import | |||
</Alert> | |||
</div> | |||
</div> | |||
`; |
@@ -0,0 +1,28 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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 ProjectDumpApp from './ProjectDumpApp'; | |||
const routes = [ | |||
{ | |||
indexRoute: { component: ProjectDumpApp } | |||
} | |||
]; | |||
export default routes; |
@@ -0,0 +1,39 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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-dump { | |||
margin-bottom: 20px; | |||
} | |||
.export-dump-path { | |||
padding: 5px 10px 5px 0; | |||
overflow: auto; | |||
white-space: nowrap; | |||
} | |||
.project-dump-check { | |||
float: right; | |||
width: 60px; | |||
height: 60px; | |||
margin-left: 15px; | |||
} | |||
#project-dump .import-disabled { | |||
background-color: transparent; | |||
} |
@@ -23,6 +23,8 @@ import { InjectedRouter } from 'react-router'; | |||
import { createStore, Store } from 'redux'; | |||
import { DocumentationEntry } from '../apps/documentation/utils'; | |||
import { Exporter, Profile } from '../apps/quality-profiles/types'; | |||
import { DumpStatus, DumpTask } from '../types/project-dump'; | |||
import { TaskStatuses } from '../types/tasks'; | |||
export function mockAlmApplication(overrides: Partial<T.AlmApplication> = {}): T.AlmApplication { | |||
return { | |||
@@ -755,3 +757,23 @@ export function mockPaging(overrides: Partial<T.Paging> = {}): T.Paging { | |||
...overrides | |||
}; | |||
} | |||
export function mockDumpTask(props: Partial<DumpTask> = {}): DumpTask { | |||
return { | |||
status: TaskStatuses.Success, | |||
startedAt: '2020-03-12T12:20:20Z', | |||
submittedAt: '2020-03-12T12:15:20Z', | |||
executedAt: '2020-03-12T12:22:20Z', | |||
...props | |||
}; | |||
} | |||
export function mockDumpStatus(props: Partial<DumpStatus> = {}): DumpStatus { | |||
return { | |||
canBeExported: true, | |||
canBeImported: true, | |||
dumpToImport: '', | |||
exportedDump: '', | |||
...props | |||
}; | |||
} |
@@ -0,0 +1,34 @@ | |||
import { TaskStatuses } from './tasks'; | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2021 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 DumpStatus { | |||
canBeExported: boolean; | |||
canBeImported: boolean; | |||
dumpToImport: string; | |||
exportedDump: string; | |||
} | |||
export interface DumpTask { | |||
executedAt?: string; | |||
startedAt?: string; | |||
status: TaskStatuses; | |||
submittedAt: string; | |||
} |
@@ -21,7 +21,9 @@ export enum TaskTypes { | |||
Report = 'REPORT', | |||
IssueSync = 'ISSUE_SYNC', | |||
AppRefresh = 'APP_REFRESH', | |||
ViewRefresh = 'VIEW_REFRESH' | |||
ViewRefresh = 'VIEW_REFRESH', | |||
ProjectExport = 'PROJECT_EXPORT', | |||
ProjectImport = 'PROJECT_IMPORT' | |||
} | |||
export enum TaskStatuses { |
@@ -93,6 +93,7 @@ declare namespace T { | |||
canAdmin?: boolean; | |||
edition: 'community' | 'developer' | 'enterprise' | 'datacenter' | undefined; | |||
globalPages?: Extension[]; | |||
projectImportFeatureEnabled?: boolean; | |||
instanceUsesDefaultAdminCredentials?: boolean; | |||
multipleAlmEnabled?: boolean; | |||
needIssueSync?: boolean; |
@@ -2970,6 +2970,36 @@ background_tasks.search_by_task_or_component=Search by Task or Component | |||
background_tasks.failing_count=Count of projects where processing of most recent analysis report failed | |||
#------------------------------------------------------------------------------ | |||
# | |||
# Project Dump | |||
# | |||
#------------------------------------------------------------------------------ | |||
project_dump.page=Import / Export | |||
project_dump.page.description=Moving a project from one SonarQube instance to another is a 3 step operation: export the project, copy the generated dump on the target server, and finally import that dump from this page on the target SonarQube instance. | |||
project_dump.page.description_without_import=Export project issues, measures and measure history for import into an Enterprise Edition or higher instance of the same version and similar configuration. The export file will be generated to the file system. It must then be copied to the target file system for import. | |||
project_dump.refresh=Refresh | |||
project_dump.see_details=See Details | |||
project_dump.export=Export | |||
project_dump.do_export=Export | |||
project_dump.can_not_export=This project cannot be exported as no analysis has been run so far. | |||
project_dump.pending_export=Export was scheduled on {0}, waiting to be processed. | |||
project_dump.in_progress_export=Export is in progress, started {0}. | |||
project_dump.failed_export=The last export failed. Please try once again. | |||
project_dump.latest_export_available=Latest project dump was generated on {0}. It can be found on the server, in the following directory: | |||
project_dump.export_available=Project dump was generated. It can be found on the server, in the following directory: | |||
project_dump.export_form_description=Export the project to the file system. The export file will need to be manually copied to the target filesystem. | |||
project_dump.import=Import | |||
project_dump.do_import=Import | |||
project_dump.import_success=The project has been successfully imported on {0}. | |||
project_dump.can_not_import=This project can not be imported because it already contains some data. | |||
project_dump.no_file_to_import=This project can not be imported because the dump file is not found. | |||
project_dump.pending_import=Import was scheduled on {0}, waiting to be processed. | |||
project_dump.in_progress_import=Import is in progress, started {0}. | |||
project_dump.failed_import=The last import has failed. Please try once again. | |||
project_dump.import_form_description=A dump has been found on the file system for this project. You can import it by clicking on the button below. | |||
project_dump.import_form_description_disabled=Projects cannot be imported. This feature is only available starting from Enterprise Edition | |||
#------------------------------------------------------------------------------ | |||
# | |||
# SYSTEM |