Переглянути джерело

SONAR-15567 Add export UI to community edition

tags/9.2.0.49834
Mathieu Suen 2 роки тому
джерело
коміт
8e7bf7fed4
20 змінених файлів з 1726 додано та 2 видалено
  1. 34
    0
      server/sonar-web/src/main/js/api/project-dump.ts
  2. 11
    0
      server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx
  3. 134
    1
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Menu-test.tsx.snap
  4. 2
    0
      server/sonar-web/src/main/js/app/utils/startReactApp.tsx
  5. 200
    0
      server/sonar-web/src/main/js/apps/projectDump/ProjectDumpApp.tsx
  6. 95
    0
      server/sonar-web/src/main/js/apps/projectDump/__tests__/ProjectDumpApp-test.tsx
  7. 140
    0
      server/sonar-web/src/main/js/apps/projectDump/__tests__/__snapshots__/ProjectDumpApp-test.tsx.snap
  8. 188
    0
      server/sonar-web/src/main/js/apps/projectDump/components/Export.tsx
  9. 180
    0
      server/sonar-web/src/main/js/apps/projectDump/components/Import.tsx
  10. 62
    0
      server/sonar-web/src/main/js/apps/projectDump/components/__tests__/Export-test.tsx
  11. 71
    0
      server/sonar-web/src/main/js/apps/projectDump/components/__tests__/Import-test.tsx
  12. 206
    0
      server/sonar-web/src/main/js/apps/projectDump/components/__tests__/__snapshots__/Export-test.tsx.snap
  13. 246
    0
      server/sonar-web/src/main/js/apps/projectDump/components/__tests__/__snapshots__/Import-test.tsx.snap
  14. 28
    0
      server/sonar-web/src/main/js/apps/projectDump/routes.ts
  15. 39
    0
      server/sonar-web/src/main/js/apps/projectDump/styles.css
  16. 22
    0
      server/sonar-web/src/main/js/helpers/testMocks.ts
  17. 34
    0
      server/sonar-web/src/main/js/types/project-dump.ts
  18. 3
    1
      server/sonar-web/src/main/js/types/tasks.ts
  19. 1
    0
      server/sonar-web/src/main/js/types/types.d.ts
  20. 30
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 34
- 0
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);
}

+ 11
- 0
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;

+ 134
- 1
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Menu-test.tsx.snap Переглянути файл

@@ -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"

+ 2
- 0
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} />

+ 200
- 0
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);

+ 95
- 0
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}
/>
);
}

+ 140
- 0
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>
`;

+ 188
- 0
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>
);
}
}

+ 180
- 0
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>
);
}
}

+ 62
- 0
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}
/>
);
}

+ 71
- 0
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}
/>
);
}

+ 206
- 0
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>
`;

+ 246
- 0
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>
`;

+ 28
- 0
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;

+ 39
- 0
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;
}

+ 22
- 0
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
};
}

+ 34
- 0
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;
}

+ 3
- 1
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 {

+ 1
- 0
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;

+ 30
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Переглянути файл

@@ -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

Завантаження…
Відмінити
Зберегти