aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorMathieu Suen <mathieu.suen@sonarsource.com>2021-10-26 14:09:52 +0200
committersonartech <sonartech@sonarsource.com>2021-11-04 20:03:24 +0000
commit8e7bf7fed4d80abe61ea7aefc32b8464e5dc819e (patch)
treecaec9feed43b55d82cdeff0fb7c9d5272434bca3 /server
parentad08e7f7912d9fed1f334803f7d71dc999fdbecd (diff)
downloadsonarqube-8e7bf7fed4d80abe61ea7aefc32b8464e5dc819e.tar.gz
sonarqube-8e7bf7fed4d80abe61ea7aefc32b8464e5dc819e.zip
SONAR-15567 Add export UI to community edition
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/api/project-dump.ts34
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx11
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Menu-test.tsx.snap135
-rw-r--r--server/sonar-web/src/main/js/app/utils/startReactApp.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectDump/ProjectDumpApp.tsx200
-rw-r--r--server/sonar-web/src/main/js/apps/projectDump/__tests__/ProjectDumpApp-test.tsx95
-rw-r--r--server/sonar-web/src/main/js/apps/projectDump/__tests__/__snapshots__/ProjectDumpApp-test.tsx.snap140
-rw-r--r--server/sonar-web/src/main/js/apps/projectDump/components/Export.tsx188
-rw-r--r--server/sonar-web/src/main/js/apps/projectDump/components/Import.tsx180
-rw-r--r--server/sonar-web/src/main/js/apps/projectDump/components/__tests__/Export-test.tsx62
-rw-r--r--server/sonar-web/src/main/js/apps/projectDump/components/__tests__/Import-test.tsx71
-rw-r--r--server/sonar-web/src/main/js/apps/projectDump/components/__tests__/__snapshots__/Export-test.tsx.snap206
-rw-r--r--server/sonar-web/src/main/js/apps/projectDump/components/__tests__/__snapshots__/Import-test.tsx.snap246
-rw-r--r--server/sonar-web/src/main/js/apps/projectDump/routes.ts28
-rw-r--r--server/sonar-web/src/main/js/apps/projectDump/styles.css39
-rw-r--r--server/sonar-web/src/main/js/helpers/testMocks.ts22
-rw-r--r--server/sonar-web/src/main/js/types/project-dump.ts34
-rw-r--r--server/sonar-web/src/main/js/types/tasks.ts4
-rw-r--r--server/sonar-web/src/main/js/types/types.d.ts1
19 files changed, 1696 insertions, 2 deletions
diff --git a/server/sonar-web/src/main/js/api/project-dump.ts b/server/sonar-web/src/main/js/api/project-dump.ts
new file mode 100644
index 00000000000..9b0d4faaa72
--- /dev/null
+++ b/server/sonar-web/src/main/js/api/project-dump.ts
@@ -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);
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx
index b1ac495c980..4130d4b855a 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx
@@ -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;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Menu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Menu-test.tsx.snap
index a2426279952..79f7a7b1acc 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Menu-test.tsx.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Menu-test.tsx.snap
@@ -356,6 +356,24 @@ exports[`should work for a branch 1`] = `
style={Object {}}
to={
Object {
+ "pathname": "/project/import_export",
+ "query": Object {
+ "branch": "release",
+ "id": "foo",
+ },
+ }
+ }
+ >
+ project_dump.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
"pathname": "/project/webhooks",
"query": Object {
"branch": "release",
@@ -726,6 +744,23 @@ exports[`should work for all qualifiers 1`] = `
style={Object {}}
to={
Object {
+ "pathname": "/project/import_export",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+ >
+ project_dump.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
"pathname": "/project/webhooks",
"query": Object {
"id": "foo",
@@ -874,6 +909,23 @@ exports[`should work for all qualifiers 2`] = `
style={Object {}}
to={
Object {
+ "pathname": "/project/import_export",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+ >
+ project_dump.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
"pathname": "/project/deletion",
"query": Object {
"id": "foo",
@@ -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>
`;
@@ -1112,6 +1194,23 @@ exports[`should work for all qualifiers 4`] = `
style={Object {}}
to={
Object {
+ "pathname": "/project/import_export",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+ >
+ project_dump.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
"pathname": "/project/deletion",
"query": Object {
"id": "foo",
@@ -1509,6 +1608,23 @@ exports[`should work with extensions 2`] = `
style={Object {}}
to={
Object {
+ "pathname": "/project/import_export",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+ >
+ project_dump.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
"pathname": "/project/webhooks",
"query": Object {
"id": "foo",
@@ -1696,6 +1812,23 @@ exports[`should work with multiple extensions 2`] = `
style={Object {}}
to={
Object {
+ "pathname": "/project/import_export",
+ "query": Object {
+ "id": "foo",
+ },
+ }
+ }
+ >
+ project_dump.page
+ </Link>
+ </li>
+ <li>
+ <Link
+ activeClassName="active"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
"pathname": "/project/webhooks",
"query": Object {
"id": "foo",
diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx
index c1c7b4fdc37..6a195e67229 100644
--- a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx
+++ b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx
@@ -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} />
diff --git a/server/sonar-web/src/main/js/apps/projectDump/ProjectDumpApp.tsx b/server/sonar-web/src/main/js/apps/projectDump/ProjectDumpApp.tsx
new file mode 100644
index 00000000000..8414da52c12
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectDump/ProjectDumpApp.tsx
@@ -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);
diff --git a/server/sonar-web/src/main/js/apps/projectDump/__tests__/ProjectDumpApp-test.tsx b/server/sonar-web/src/main/js/apps/projectDump/__tests__/ProjectDumpApp-test.tsx
new file mode 100644
index 00000000000..5c034d3231e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectDump/__tests__/ProjectDumpApp-test.tsx
@@ -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}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectDump/__tests__/__snapshots__/ProjectDumpApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectDump/__tests__/__snapshots__/ProjectDumpApp-test.tsx.snap
new file mode 100644
index 00000000000..7282eaac0b9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectDump/__tests__/__snapshots__/ProjectDumpApp-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectDump/components/Export.tsx b/server/sonar-web/src/main/js/apps/projectDump/components/Export.tsx
new file mode 100644
index 00000000000..6712e7a80d5
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectDump/components/Export.tsx
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectDump/components/Import.tsx b/server/sonar-web/src/main/js/apps/projectDump/components/Import.tsx
new file mode 100644
index 00000000000..87acd4efea4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectDump/components/Import.tsx
@@ -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>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectDump/components/__tests__/Export-test.tsx b/server/sonar-web/src/main/js/apps/projectDump/components/__tests__/Export-test.tsx
new file mode 100644
index 00000000000..36bd980955a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectDump/components/__tests__/Export-test.tsx
@@ -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}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectDump/components/__tests__/Import-test.tsx b/server/sonar-web/src/main/js/apps/projectDump/components/__tests__/Import-test.tsx
new file mode 100644
index 00000000000..a9479ace689
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectDump/components/__tests__/Import-test.tsx
@@ -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}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectDump/components/__tests__/__snapshots__/Export-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectDump/components/__tests__/__snapshots__/Export-test.tsx.snap
new file mode 100644
index 00000000000..788257b21d0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectDump/components/__tests__/__snapshots__/Export-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectDump/components/__tests__/__snapshots__/Import-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectDump/components/__tests__/__snapshots__/Import-test.tsx.snap
new file mode 100644
index 00000000000..c5cbbc7c15b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectDump/components/__tests__/__snapshots__/Import-test.tsx.snap
@@ -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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectDump/routes.ts b/server/sonar-web/src/main/js/apps/projectDump/routes.ts
new file mode 100644
index 00000000000..1b02e00004a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectDump/routes.ts
@@ -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;
diff --git a/server/sonar-web/src/main/js/apps/projectDump/styles.css b/server/sonar-web/src/main/js/apps/projectDump/styles.css
new file mode 100644
index 00000000000..7d33912e7a0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectDump/styles.css
@@ -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;
+}
diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts
index 4a8b891a9cd..773815f499d 100644
--- a/server/sonar-web/src/main/js/helpers/testMocks.ts
+++ b/server/sonar-web/src/main/js/helpers/testMocks.ts
@@ -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
+ };
+}
diff --git a/server/sonar-web/src/main/js/types/project-dump.ts b/server/sonar-web/src/main/js/types/project-dump.ts
new file mode 100644
index 00000000000..26da42f6ba1
--- /dev/null
+++ b/server/sonar-web/src/main/js/types/project-dump.ts
@@ -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;
+}
diff --git a/server/sonar-web/src/main/js/types/tasks.ts b/server/sonar-web/src/main/js/types/tasks.ts
index b23ae39d7f4..886bf8b0bfd 100644
--- a/server/sonar-web/src/main/js/types/tasks.ts
+++ b/server/sonar-web/src/main/js/types/tasks.ts
@@ -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 {
diff --git a/server/sonar-web/src/main/js/types/types.d.ts b/server/sonar-web/src/main/js/types/types.d.ts
index 3b82fbe999d..d350f26b38f 100644
--- a/server/sonar-web/src/main/js/types/types.d.ts
+++ b/server/sonar-web/src/main/js/types/types.d.ts
@@ -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;